use crate::{JavaError, JavaInfo};
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
Both,
OutputOnly,
ErrorOnly,
}
impl JavaInfo {
pub fn execute(&self, args: &str) -> Result<(), JavaError> {
self.run_java(args, OutputMode::Both)
}
pub fn execute_with_error(&self, args: &str) -> Result<(), JavaError> {
self.run_java(args, OutputMode::ErrorOnly)
}
pub fn execute_with_output(&self, args: &str) -> Result<(), JavaError> {
self.run_java(args, OutputMode::OutputOnly)
}
fn run_java(&self, args: &str, mode: OutputMode) -> Result<(), JavaError> {
let java_exe = self.java_executable()?;
let arg_vec = shell_words::split(args)
.map_err(|e| JavaError::Other(format!("Failed to parse arguments: {}", e)))?;
let mut cmd = Command::new(java_exe);
cmd.args(&arg_vec);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(JavaError::IoError)?;
let stdout = child.stdout.take().expect("Failed to get stdout pipe");
let stderr = child.stderr.take().expect("Failed to get stderr pipe");
let stdout_handle = if matches!(mode, OutputMode::Both | OutputMode::OutputOnly) {
Some(thread::spawn(move || {
let reader = BufReader::new(stdout);
for line in reader.lines().map_while(Result::ok) {
println!("{}", line);
}
}))
} else {
None
};
let stderr_handle = if matches!(mode, OutputMode::Both | OutputMode::ErrorOnly) {
Some(thread::spawn(move || {
let reader = BufReader::new(stderr);
for line in reader.lines().map_while(Result::ok) {
eprintln!("{}", line);
}
}))
} else {
None
};
let status = child.wait().map_err(JavaError::IoError)?;
if let Some(handle) = stdout_handle {
handle.join().unwrap();
}
if let Some(handle) = stderr_handle {
handle.join().unwrap();
}
if status.success() {
Ok(())
} else {
Err(JavaError::ExecutionFailed(format!(
"Execution failed: {}",
status.code().unwrap()
)))
}
}
fn java_executable(&self) -> Result<PathBuf, JavaError> {
let java_home = &self.java_home;
let exe_name = if cfg!(windows) { "java.exe" } else { "java" };
let java_exe = java_home.join("bin").join(exe_name);
if java_exe.exists() {
Ok(java_exe)
} else {
Err(JavaError::NotFound(format!(
"Java executable not found: {:?}",
java_exe
)))
}
}
}
#[derive(Debug, Default)]
pub struct JavaRunner {
java: Option<JavaInfo>,
jar: Option<PathBuf>,
min_memory: Option<String>,
max_memory: Option<String>,
main_class: Option<String>,
cmd_args: Option<Vec<String>>,
args: Vec<String>,
redirect: JavaRedirect,
classpath: Option<String>,
module_path: Option<PathBuf>,
add_opens: Vec<String>,
add_exports: Vec<String>,
system_properties: Vec<String>,
env_vars: Vec<(String, String)>,
working_dir: Option<PathBuf>,
timeout: Option<Duration>,
}
#[derive(Debug, Default)]
pub struct JavaRedirect {
output: Option<PathBuf>,
error: Option<PathBuf>,
input: Option<PathBuf>,
append_output: bool,
append_error: bool,
}
impl JavaRedirect {
pub fn new() -> Self {
Self::default()
}
pub fn output(mut self, path: impl AsRef<Path>) -> Self {
self.output = Some(path.as_ref().to_path_buf());
self
}
pub fn error(mut self, path: impl AsRef<Path>) -> Self {
self.error = Some(path.as_ref().to_path_buf());
self
}
pub fn input(mut self, path: impl AsRef<Path>) -> Self {
self.input = Some(path.as_ref().to_path_buf());
self
}
pub fn append_output(mut self) -> Self {
self.append_output = true;
self
}
pub fn append_error(mut self) -> Self {
self.append_error = true;
self
}
}
impl JavaRunner {
pub fn new() -> Self {
Self::default()
}
pub fn java(mut self, java: JavaInfo) -> Self {
self.java = Some(java);
self
}
pub fn jar(mut self, jar: impl AsRef<Path>) -> Self {
self.jar = Some(jar.as_ref().to_path_buf());
self
}
pub fn min_memory(mut self, bytes: usize) -> Self {
self.min_memory = Some(format_memory(bytes));
self
}
pub fn max_memory(mut self, bytes: usize) -> Self {
self.max_memory = Some(format_memory(bytes));
self
}
pub fn main_class(mut self, class: impl Into<String>) -> Self {
self.main_class = Some(class.into());
self
}
pub fn cmd(mut self, args: &[&str]) -> Self {
self.cmd_args = Some(args.iter().map(|s| s.to_string()).collect());
self
}
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
pub fn redirect(mut self, redirect: JavaRedirect) -> Self {
self.redirect = redirect;
self
}
pub fn classpath(mut self, paths: &[impl AsRef<Path>]) -> Self {
let separator = if cfg!(windows) { ";" } else { ":" };
let joined: Vec<String> = paths
.iter()
.map(|p| p.as_ref().to_string_lossy().to_string())
.collect();
self.classpath = Some(joined.join(separator));
self
}
pub fn module_path(mut self, path: impl AsRef<Path>) -> Self {
self.module_path = Some(path.as_ref().to_path_buf());
self
}
pub fn add_opens(
mut self,
module: impl Into<String>,
package: impl Into<String>,
target: impl Into<String>,
) -> Self {
self.add_opens.push(format!(
"{}/{}.{}",
module.into(),
package.into(),
target.into()
));
self
}
pub fn add_exports(
mut self,
module: impl Into<String>,
package: impl Into<String>,
target: impl Into<String>,
) -> Self {
self.add_exports.push(format!(
"{}/{}.{}",
module.into(),
package.into(),
target.into()
));
self
}
pub fn system_property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.system_properties
.push(format!("-D{}={}", key.into(), value.into()));
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env_vars.push((key.into(), value.into()));
self
}
pub fn working_dir(mut self, path: impl AsRef<Path>) -> Self {
self.working_dir = Some(path.as_ref().to_path_buf());
self
}
pub fn timeout(mut self, duration: Duration) -> Self {
self.timeout = Some(duration);
self
}
pub fn execute(&self) -> Result<(), JavaError> {
let java = self.java.as_ref().ok_or_else(|| {
JavaError::Other("Must set Java environment via `.java(...)`".to_string())
})?;
let java_exe = java.java_executable()?;
let mut cmd = Command::new(java_exe);
if let Some(cp) = &self.classpath {
cmd.arg("-cp");
cmd.arg(cp);
}
if let Some(mp) = &self.module_path {
cmd.arg("--module-path");
cmd.arg(mp);
}
for open in &self.add_opens {
cmd.arg("--add-opens");
cmd.arg(open);
}
for export in &self.add_exports {
cmd.arg("--add-exports");
cmd.arg(export);
}
for prop in &self.system_properties {
cmd.arg(prop);
}
if let Some(min) = &self.min_memory {
cmd.arg(format!("-Xms{}", min));
}
if let Some(max) = &self.max_memory {
cmd.arg(format!("-Xmx{}", max));
}
if let Some(cmd_args) = &self.cmd_args {
cmd.args(cmd_args);
} else if let Some(jar) = &self.jar {
cmd.arg("-jar");
cmd.arg(jar);
} else if let Some(main) = &self.main_class {
cmd.arg(main);
} else {
return Err(JavaError::Other(
"Must specify JAR file, main class, or cmd args".into(),
));
}
cmd.args(&self.args);
for (key, value) in &self.env_vars {
cmd.env(key, value);
}
if let Some(dir) = &self.working_dir {
cmd.current_dir(dir);
}
if let Some(output) = &self.redirect.output {
let file = if self.redirect.append_output {
OpenOptions::new().append(true).create(true).open(output)
} else {
File::create(output)
}
.map_err(JavaError::IoError)?;
cmd.stdout(Stdio::from(file));
} else {
cmd.stdout(Stdio::inherit());
}
if let Some(error) = &self.redirect.error {
let file = if self.redirect.append_error {
OpenOptions::new().append(true).create(true).open(error)
} else {
File::create(error)
}
.map_err(JavaError::IoError)?;
cmd.stderr(Stdio::from(file));
} else {
cmd.stderr(Stdio::inherit());
}
if let Some(input) = &self.redirect.input {
let file = File::open(input).map_err(JavaError::IoError)?;
cmd.stdin(Stdio::from(file));
} else {
cmd.stdin(Stdio::inherit());
}
if let Some(timeout) = self.timeout {
let mut child = cmd.spawn().map_err(JavaError::IoError)?;
let start = std::time::Instant::now();
loop {
if child.try_wait().map_err(JavaError::IoError)?.is_some() {
let status = child.wait().map_err(JavaError::IoError)?;
return if status.success() {
Ok(())
} else {
Err(JavaError::ExecutionFailed(format!(
"Execution failed: {}",
status.code().unwrap()
)))
};
}
if start.elapsed() >= timeout {
kill_process(&child)?;
return Err(JavaError::ExecutionFailed(format!(
"Process timed out after {}ms",
timeout.as_millis()
)));
}
std::thread::sleep(Duration::from_millis(50));
}
}
let status = cmd.status().map_err(JavaError::IoError)?;
if status.success() {
Ok(())
} else {
Err(JavaError::ExecutionFailed(format!(
"Execution failed: {}",
status.code().unwrap()
)))
}
}
}
fn format_memory(bytes: usize) -> String {
const MB: usize = 1024 * 1024;
const GB: usize = MB * 1024;
if bytes.is_multiple_of(GB) {
format!("{}g", bytes / GB)
} else if bytes.is_multiple_of(MB) {
format!("{}m", bytes / MB)
} else {
let mb = (bytes + MB / 2) / MB;
format!("{}m", mb)
}
}
fn kill_process(child: &std::process::Child) -> Result<(), JavaError> {
let pid = child.id();
#[cfg(windows)]
let status = std::process::Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/F"])
.status()
.map_err(JavaError::IoError)?;
#[cfg(not(windows))]
let status = std::process::Command::new("kill")
.args(["-9", &pid.to_string()])
.status()
.map_err(JavaError::IoError)?;
if status.success() {
Ok(())
} else {
Err(JavaError::Other(format!("Failed to kill process {}", pid)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::JavaInfo;
#[test]
fn test_format_memory_exact_mb() {
assert_eq!(format_memory(256 * 1024 * 1024), "256m");
}
#[test]
fn test_format_memory_exact_gb() {
assert_eq!(format_memory(2 * 1024 * 1024 * 1024), "2g");
}
#[test]
fn test_format_memory_rounded() {
let result = format_memory(100 * 1024 * 1024 + 512 * 1024);
assert!(result.ends_with('m'));
let num: usize = result[..result.len() - 1].parse().unwrap();
assert!(num >= 100);
}
#[test]
fn test_format_memory_zero() {
assert_eq!(format_memory(0), "0g");
}
#[test]
fn test_format_memory_small() {
let result = format_memory(1);
assert_eq!(result, "0m");
}
#[test]
fn test_runner_missing_java() {
let result = JavaRunner::new().jar("test.jar").execute();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Must set Java environment"));
}
#[test]
fn test_runner_missing_jar_or_main() {
let info = JavaInfo::default();
let result = JavaRunner::new().java(info).execute();
assert!(result.is_err());
}
#[test]
fn test_java_redirect_default() {
let r = JavaRedirect::new();
assert!(r.output.is_none());
assert!(r.error.is_none());
assert!(r.input.is_none());
assert!(!r.append_output);
assert!(!r.append_error);
}
#[test]
fn test_java_redirect_append() {
let r = JavaRedirect::new()
.output("out.log")
.append_output()
.error("err.log")
.append_error();
assert!(r.append_output);
assert!(r.append_error);
}
#[test]
fn test_runner_builder_methods() {
let runner = JavaRunner::new()
.system_property("foo", "bar")
.add_opens("java.base", "java.lang", "ALL-UNNAMED")
.add_exports("java.base", "com.sun.internal", "ALL-UNNAMED");
assert_eq!(runner.system_properties, vec!["-Dfoo=bar"]);
assert_eq!(runner.add_opens, vec!["java.base/java.lang.ALL-UNNAMED"]);
assert_eq!(
runner.add_exports,
vec!["java.base/com.sun.internal.ALL-UNNAMED"]
);
}
#[test]
fn test_output_mode_debug_clone() {
let mode = OutputMode::Both;
let cloned = mode;
assert_eq!(mode, cloned);
let _ = format!("{:?}", mode);
}
#[test]
fn test_format_memory_1gb_plus_1b() {
let result = format_memory(1024 * 1024 * 1024 + 1);
assert_eq!(result, "1024m");
}
#[test]
fn test_format_memory_1_mib() {
assert_eq!(format_memory(1024 * 1024), "1m");
}
#[test]
fn test_format_memory_1024_mib_is_1g() {
assert_eq!(format_memory(1024 * 1024 * 1024), "1g");
}
#[test]
fn test_runner_env_var() {
let runner = JavaRunner::new().env("MY_VAR", "my_value");
assert_eq!(runner.env_vars, vec![("MY_VAR".into(), "my_value".into())]);
}
#[test]
fn test_runner_working_dir() {
let runner = JavaRunner::new().working_dir("/tmp");
assert_eq!(runner.working_dir, Some(PathBuf::from("/tmp")));
}
#[test]
fn test_runner_timeout() {
let runner = JavaRunner::new().timeout(Duration::from_secs(30));
assert_eq!(runner.timeout, Some(Duration::from_secs(30)));
}
#[test]
fn test_runner_classpath() {
let separator = if cfg!(windows) { ";" } else { ":" };
let runner = JavaRunner::new().classpath(&["lib/a.jar", "config"]);
assert_eq!(
runner.classpath,
Some(format!("lib/a.jar{separator}config"))
);
}
#[test]
fn test_runner_module_path() {
let runner = JavaRunner::new().module_path("./modules");
assert_eq!(runner.module_path, Some(PathBuf::from("./modules")));
}
#[test]
fn test_runner_multiple_args() {
let runner = JavaRunner::new().arg("--verbose").arg("--debug");
assert_eq!(runner.args, vec!["--verbose", "--debug"]);
}
#[test]
fn test_runner_all_builder_methods() {
let runner = JavaRunner::new()
.classpath(&["lib/*"])
.module_path("mods")
.add_opens("java.base", "java.lang", "ALL-UNNAMED")
.add_exports("java.base", "sun.security", "ALL-UNNAMED")
.system_property("key", "val")
.env("HOME", "/root")
.working_dir("/app")
.timeout(Duration::from_secs(10))
.min_memory(256 * 1024 * 1024)
.max_memory(1024 * 1024 * 1024);
assert!(runner.classpath.is_some());
assert!(runner.module_path.is_some());
assert_eq!(runner.add_opens.len(), 1);
assert_eq!(runner.add_exports.len(), 1);
assert_eq!(runner.system_properties.len(), 1);
assert_eq!(runner.env_vars.len(), 1);
assert!(runner.working_dir.is_some());
assert_eq!(runner.timeout, Some(Duration::from_secs(10)));
assert!(runner.min_memory.is_some());
assert!(runner.max_memory.is_some());
}
#[test]
fn test_runner_cmd_sets_field() {
let runner = JavaRunner::new().cmd(&["-version"]);
assert_eq!(runner.cmd_args, Some(vec!["-version".to_string()]));
}
#[test]
fn test_runner_cmd_multiple_args() {
let runner = JavaRunner::new().cmd(&["--list-modules", "--add-modules", "java.se"]);
assert_eq!(
runner.cmd_args,
Some(vec![
"--list-modules".to_string(),
"--add-modules".to_string(),
"java.se".to_string(),
])
);
}
#[test]
fn test_runner_cmd_bypasses_jar_main_check() {
let info = JavaInfo::default();
let result = JavaRunner::new().java(info).cmd(&["-version"]).execute();
let err = result.unwrap_err().to_string();
assert!(!err.contains("Must specify JAR file"));
assert!(err.contains("Not found") || err.contains("Java executable not found"));
}
}