1use crate::{JavaError, JavaInfo};
4use std::fs::{File, OpenOptions};
5use std::io::{BufRead, BufReader};
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8use std::thread;
9use std::time::Duration;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum OutputMode {
16 Both,
18 OutputOnly,
20 ErrorOnly,
22}
23
24impl JavaInfo {
25 pub fn execute(&self, args: &str) -> Result<(), JavaError> {
47 self.run_java(args, OutputMode::Both)
48 }
49
50 pub fn execute_with_error(&self, args: &str) -> Result<(), JavaError> {
59 self.run_java(args, OutputMode::ErrorOnly)
60 }
61
62 pub fn execute_with_output(&self, args: &str) -> Result<(), JavaError> {
71 self.run_java(args, OutputMode::OutputOnly)
72 }
73
74 fn run_java(&self, args: &str, mode: OutputMode) -> Result<(), JavaError> {
76 let java_exe = self.java_executable()?;
77
78 let arg_vec = shell_words::split(args)
79 .map_err(|e| JavaError::Other(format!("Failed to parse arguments: {}", e)))?;
80
81 let mut cmd = Command::new(java_exe);
82 cmd.args(&arg_vec);
83
84 cmd.stdout(Stdio::piped());
85 cmd.stderr(Stdio::piped());
86
87 let mut child = cmd.spawn().map_err(JavaError::IoError)?;
88
89 let stdout = child.stdout.take().expect("Failed to get stdout pipe");
90 let stderr = child.stderr.take().expect("Failed to get stderr pipe");
91
92 let stdout_handle = if matches!(mode, OutputMode::Both | OutputMode::OutputOnly) {
93 Some(thread::spawn(move || {
94 let reader = BufReader::new(stdout);
95 for line in reader.lines().map_while(Result::ok) {
96 println!("{}", line);
97 }
98 }))
99 } else {
100 None
101 };
102
103 let stderr_handle = if matches!(mode, OutputMode::Both | OutputMode::ErrorOnly) {
104 Some(thread::spawn(move || {
105 let reader = BufReader::new(stderr);
106 for line in reader.lines().map_while(Result::ok) {
107 eprintln!("{}", line);
108 }
109 }))
110 } else {
111 None
112 };
113
114 let status = child.wait().map_err(JavaError::IoError)?;
115
116 if let Some(handle) = stdout_handle {
117 handle.join().unwrap();
118 }
119 if let Some(handle) = stderr_handle {
120 handle.join().unwrap();
121 }
122
123 if status.success() {
124 Ok(())
125 } else {
126 Err(JavaError::ExecutionFailed(format!(
127 "Execution failed: {}",
128 status.code().unwrap()
129 )))
130 }
131 }
132
133 fn java_executable(&self) -> Result<PathBuf, JavaError> {
139 let java_home = &self.java_home;
140 let exe_name = if cfg!(windows) { "java.exe" } else { "java" };
141 let java_exe = java_home.join("bin").join(exe_name);
142 if java_exe.exists() {
143 Ok(java_exe)
144 } else {
145 Err(JavaError::NotFound(format!(
146 "Java executable not found: {:?}",
147 java_exe
148 )))
149 }
150 }
151}
152
153#[derive(Debug, Default)]
176pub struct JavaRunner {
177 java: Option<JavaInfo>,
178 jar: Option<PathBuf>,
179 min_memory: Option<String>,
180 max_memory: Option<String>,
181 main_class: Option<String>,
182 args: Vec<String>,
183 redirect: JavaRedirect,
184 classpath: Option<String>,
185 module_path: Option<PathBuf>,
186 add_opens: Vec<String>,
187 add_exports: Vec<String>,
188 system_properties: Vec<String>,
189 env_vars: Vec<(String, String)>,
190 working_dir: Option<PathBuf>,
191 timeout: Option<Duration>,
192}
193
194#[derive(Debug, Default)]
200pub struct JavaRedirect {
201 output: Option<PathBuf>,
202 error: Option<PathBuf>,
203 input: Option<PathBuf>,
204 append_output: bool,
205 append_error: bool,
206}
207
208impl JavaRedirect {
209 pub fn new() -> Self {
211 Self::default()
212 }
213
214 pub fn output(mut self, path: impl AsRef<Path>) -> Self {
217 self.output = Some(path.as_ref().to_path_buf());
218 self
219 }
220
221 pub fn error(mut self, path: impl AsRef<Path>) -> Self {
224 self.error = Some(path.as_ref().to_path_buf());
225 self
226 }
227
228 pub fn input(mut self, path: impl AsRef<Path>) -> Self {
231 self.input = Some(path.as_ref().to_path_buf());
232 self
233 }
234
235 pub fn append_output(mut self) -> Self {
238 self.append_output = true;
239 self
240 }
241
242 pub fn append_error(mut self) -> Self {
245 self.append_error = true;
246 self
247 }
248}
249
250impl JavaRunner {
251 pub fn new() -> Self {
253 Self::default()
254 }
255
256 pub fn java(mut self, java: JavaInfo) -> Self {
260 self.java = Some(java);
261 self
262 }
263
264 pub fn jar(mut self, jar: impl AsRef<Path>) -> Self {
268 self.jar = Some(jar.as_ref().to_path_buf());
269 self
270 }
271
272 pub fn min_memory(mut self, bytes: usize) -> Self {
278 self.min_memory = Some(format_memory(bytes));
279 self
280 }
281
282 pub fn max_memory(mut self, bytes: usize) -> Self {
286 self.max_memory = Some(format_memory(bytes));
287 self
288 }
289
290 pub fn main_class(mut self, class: impl Into<String>) -> Self {
294 self.main_class = Some(class.into());
295 self
296 }
297
298 pub fn arg(mut self, arg: impl Into<String>) -> Self {
302 self.args.push(arg.into());
303 self
304 }
305
306 pub fn redirect(mut self, redirect: JavaRedirect) -> Self {
308 self.redirect = redirect;
309 self
310 }
311
312 pub fn classpath(mut self, paths: &[impl AsRef<Path>]) -> Self {
316 let separator = if cfg!(windows) { ";" } else { ":" };
317 let joined: Vec<String> = paths
318 .iter()
319 .map(|p| p.as_ref().to_string_lossy().to_string())
320 .collect();
321 self.classpath = Some(joined.join(separator));
322 self
323 }
324
325 pub fn module_path(mut self, path: impl AsRef<Path>) -> Self {
327 self.module_path = Some(path.as_ref().to_path_buf());
328 self
329 }
330
331 pub fn add_opens(
333 mut self,
334 module: impl Into<String>,
335 package: impl Into<String>,
336 target: impl Into<String>,
337 ) -> Self {
338 self.add_opens.push(format!(
339 "{}/{}.{}",
340 module.into(),
341 package.into(),
342 target.into()
343 ));
344 self
345 }
346
347 pub fn add_exports(
349 mut self,
350 module: impl Into<String>,
351 package: impl Into<String>,
352 target: impl Into<String>,
353 ) -> Self {
354 self.add_exports.push(format!(
355 "{}/{}.{}",
356 module.into(),
357 package.into(),
358 target.into()
359 ));
360 self
361 }
362
363 pub fn system_property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
365 self.system_properties
366 .push(format!("-D{}={}", key.into(), value.into()));
367 self
368 }
369
370 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
372 self.env_vars.push((key.into(), value.into()));
373 self
374 }
375
376 pub fn working_dir(mut self, path: impl AsRef<Path>) -> Self {
378 self.working_dir = Some(path.as_ref().to_path_buf());
379 self
380 }
381
382 pub fn timeout(mut self, duration: Duration) -> Self {
387 self.timeout = Some(duration);
388 self
389 }
390
391 pub fn execute(&self) -> Result<(), JavaError> {
401 let java = self.java.as_ref().ok_or_else(|| {
402 JavaError::Other("Must set Java environment via `.java(...)`".to_string())
403 })?;
404 let java_exe = java.java_executable()?;
405
406 let mut cmd = Command::new(java_exe);
407
408 if let Some(cp) = &self.classpath {
410 cmd.arg("-cp");
411 cmd.arg(cp);
412 }
413
414 if let Some(mp) = &self.module_path {
416 cmd.arg("--module-path");
417 cmd.arg(mp);
418 }
419
420 for open in &self.add_opens {
422 cmd.arg("--add-opens");
423 cmd.arg(open);
424 }
425
426 for export in &self.add_exports {
428 cmd.arg("--add-exports");
429 cmd.arg(export);
430 }
431
432 for prop in &self.system_properties {
434 cmd.arg(prop);
435 }
436
437 if let Some(min) = &self.min_memory {
438 cmd.arg(format!("-Xms{}", min));
439 }
440 if let Some(max) = &self.max_memory {
441 cmd.arg(format!("-Xmx{}", max));
442 }
443
444 if let Some(jar) = &self.jar {
445 cmd.arg("-jar");
446 cmd.arg(jar);
447 } else if let Some(main) = &self.main_class {
448 cmd.arg(main);
449 } else {
450 return Err(JavaError::Other(
451 "Must specify JAR file or main class".into(),
452 ));
453 }
454
455 cmd.args(&self.args);
456
457 for (key, value) in &self.env_vars {
459 cmd.env(key, value);
460 }
461
462 if let Some(dir) = &self.working_dir {
464 cmd.current_dir(dir);
465 }
466
467 if let Some(output) = &self.redirect.output {
469 let file = if self.redirect.append_output {
470 OpenOptions::new().append(true).create(true).open(output)
471 } else {
472 File::create(output)
473 }
474 .map_err(JavaError::IoError)?;
475 cmd.stdout(Stdio::from(file));
476 } else {
477 cmd.stdout(Stdio::inherit());
478 }
479
480 if let Some(error) = &self.redirect.error {
481 let file = if self.redirect.append_error {
482 OpenOptions::new().append(true).create(true).open(error)
483 } else {
484 File::create(error)
485 }
486 .map_err(JavaError::IoError)?;
487 cmd.stderr(Stdio::from(file));
488 } else {
489 cmd.stderr(Stdio::inherit());
490 }
491
492 if let Some(input) = &self.redirect.input {
493 let file = File::open(input).map_err(JavaError::IoError)?;
494 cmd.stdin(Stdio::from(file));
495 } else {
496 cmd.stdin(Stdio::inherit());
497 }
498
499 if let Some(timeout) = self.timeout {
501 let mut child = cmd.spawn().map_err(JavaError::IoError)?;
502 let start = std::time::Instant::now();
503 loop {
504 if child.try_wait().map_err(JavaError::IoError)?.is_some() {
505 let status = child.wait().map_err(JavaError::IoError)?;
507 return if status.success() {
508 Ok(())
509 } else {
510 Err(JavaError::ExecutionFailed(format!(
511 "Execution failed: {}",
512 status.code().unwrap()
513 )))
514 };
515 }
516 if start.elapsed() >= timeout {
517 kill_process(&child)?;
519 return Err(JavaError::ExecutionFailed(format!(
520 "Process timed out after {}ms",
521 timeout.as_millis()
522 )));
523 }
524 std::thread::sleep(Duration::from_millis(50));
525 }
526 }
527
528 let status = cmd.status().map_err(JavaError::IoError)?;
529
530 if status.success() {
531 Ok(())
532 } else {
533 Err(JavaError::ExecutionFailed(format!(
534 "Execution failed: {}",
535 status.code().unwrap()
536 )))
537 }
538 }
539}
540
541fn format_memory(bytes: usize) -> String {
547 const MB: usize = 1024 * 1024;
548 const GB: usize = MB * 1024;
549
550 if bytes.is_multiple_of(GB) {
551 format!("{}g", bytes / GB)
552 } else if bytes.is_multiple_of(MB) {
553 format!("{}m", bytes / MB)
554 } else {
555 let mb = (bytes + MB / 2) / MB;
556 format!("{}m", mb)
557 }
558}
559
560fn kill_process(child: &std::process::Child) -> Result<(), JavaError> {
561 let pid = child.id();
564 #[cfg(windows)]
565 let status = std::process::Command::new("taskkill")
566 .args(["/PID", &pid.to_string(), "/F"])
567 .status()
568 .map_err(JavaError::IoError)?;
569 #[cfg(not(windows))]
570 let status = std::process::Command::new("kill")
571 .args(["-9", &pid.to_string()])
572 .status()
573 .map_err(JavaError::IoError)?;
574
575 if status.success() {
576 Ok(())
577 } else {
578 Err(JavaError::Other(format!("Failed to kill process {}", pid)))
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use crate::JavaInfo;
586
587 #[test]
588 fn test_format_memory_exact_mb() {
589 assert_eq!(format_memory(256 * 1024 * 1024), "256m");
590 }
591
592 #[test]
593 fn test_format_memory_exact_gb() {
594 assert_eq!(format_memory(2 * 1024 * 1024 * 1024), "2g");
595 }
596
597 #[test]
598 fn test_format_memory_rounded() {
599 let result = format_memory(100 * 1024 * 1024 + 512 * 1024);
600 assert!(result.ends_with('m'));
601 let num: usize = result[..result.len() - 1].parse().unwrap();
602 assert!(num >= 100);
603 }
604
605 #[test]
606 fn test_format_memory_zero() {
607 assert_eq!(format_memory(0), "0g");
608 }
609
610 #[test]
611 fn test_format_memory_small() {
612 let result = format_memory(1);
613 assert_eq!(result, "0m");
614 }
615
616 #[test]
617 fn test_runner_missing_java() {
618 let result = JavaRunner::new().jar("test.jar").execute();
619 assert!(result.is_err());
620 let err = result.unwrap_err().to_string();
621 assert!(err.contains("Must set Java environment"));
622 }
623
624 #[test]
625 fn test_runner_missing_jar_or_main() {
626 let info = JavaInfo::default();
628 let result = JavaRunner::new().java(info).execute();
629 assert!(result.is_err());
630 }
631
632 #[test]
633 fn test_java_redirect_default() {
634 let r = JavaRedirect::new();
635 assert!(r.output.is_none());
636 assert!(r.error.is_none());
637 assert!(r.input.is_none());
638 assert!(!r.append_output);
639 assert!(!r.append_error);
640 }
641
642 #[test]
643 fn test_java_redirect_append() {
644 let r = JavaRedirect::new()
645 .output("out.log")
646 .append_output()
647 .error("err.log")
648 .append_error();
649 assert!(r.append_output);
650 assert!(r.append_error);
651 }
652
653 #[test]
654 fn test_runner_builder_methods() {
655 let runner = JavaRunner::new()
656 .system_property("foo", "bar")
657 .add_opens("java.base", "java.lang", "ALL-UNNAMED")
658 .add_exports("java.base", "com.sun.internal", "ALL-UNNAMED");
659 assert_eq!(runner.system_properties, vec!["-Dfoo=bar"]);
660 assert_eq!(runner.add_opens, vec!["java.base/java.lang.ALL-UNNAMED"]);
661 assert_eq!(
662 runner.add_exports,
663 vec!["java.base/com.sun.internal.ALL-UNNAMED"]
664 );
665 }
666
667 #[test]
668 fn test_output_mode_debug_clone() {
669 let mode = OutputMode::Both;
670 let cloned = mode;
671 assert_eq!(mode, cloned);
672 let _ = format!("{:?}", mode);
673 }
674
675 #[test]
676 fn test_format_memory_1gb_plus_1b() {
677 let result = format_memory(1024 * 1024 * 1024 + 1);
678 assert_eq!(result, "1024m");
679 }
680
681 #[test]
682 fn test_format_memory_1_mib() {
683 assert_eq!(format_memory(1024 * 1024), "1m");
684 }
685
686 #[test]
687 fn test_format_memory_1024_mib_is_1g() {
688 assert_eq!(format_memory(1024 * 1024 * 1024), "1g");
689 }
690
691 #[test]
692 fn test_runner_env_var() {
693 let runner = JavaRunner::new().env("MY_VAR", "my_value");
694 assert_eq!(runner.env_vars, vec![("MY_VAR".into(), "my_value".into())]);
695 }
696
697 #[test]
698 fn test_runner_working_dir() {
699 let runner = JavaRunner::new().working_dir("/tmp");
700 assert_eq!(runner.working_dir, Some(PathBuf::from("/tmp")));
701 }
702
703 #[test]
704 fn test_runner_timeout() {
705 let runner = JavaRunner::new().timeout(Duration::from_secs(30));
706 assert_eq!(runner.timeout, Some(Duration::from_secs(30)));
707 }
708
709 #[test]
710 fn test_runner_classpath() {
711 let separator = if cfg!(windows) { ";" } else { ":" };
712 let runner = JavaRunner::new().classpath(&["lib/a.jar", "config"]);
713 assert_eq!(
714 runner.classpath,
715 Some(format!("lib/a.jar{separator}config"))
716 );
717 }
718
719 #[test]
720 fn test_runner_module_path() {
721 let runner = JavaRunner::new().module_path("./modules");
722 assert_eq!(runner.module_path, Some(PathBuf::from("./modules")));
723 }
724
725 #[test]
726 fn test_runner_multiple_args() {
727 let runner = JavaRunner::new().arg("--verbose").arg("--debug");
728 assert_eq!(runner.args, vec!["--verbose", "--debug"]);
729 }
730
731 #[test]
732 fn test_runner_all_builder_methods() {
733 let runner = JavaRunner::new()
734 .classpath(&["lib/*"])
735 .module_path("mods")
736 .add_opens("java.base", "java.lang", "ALL-UNNAMED")
737 .add_exports("java.base", "sun.security", "ALL-UNNAMED")
738 .system_property("key", "val")
739 .env("HOME", "/root")
740 .working_dir("/app")
741 .timeout(Duration::from_secs(10))
742 .min_memory(256 * 1024 * 1024)
743 .max_memory(1024 * 1024 * 1024);
744
745 assert!(runner.classpath.is_some());
746 assert!(runner.module_path.is_some());
747 assert_eq!(runner.add_opens.len(), 1);
748 assert_eq!(runner.add_exports.len(), 1);
749 assert_eq!(runner.system_properties.len(), 1);
750 assert_eq!(runner.env_vars.len(), 1);
751 assert!(runner.working_dir.is_some());
752 assert_eq!(runner.timeout, Some(Duration::from_secs(10)));
753 assert!(runner.min_memory.is_some());
754 assert!(runner.max_memory.is_some());
755 }
756}