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)]
190pub struct JavaRunner {
191 java: Option<JavaInfo>,
192 jar: Option<PathBuf>,
193 min_memory: Option<String>,
194 max_memory: Option<String>,
195 main_class: Option<String>,
196 cmd_args: Option<Vec<String>>,
197 args: Vec<String>,
198 redirect: JavaRedirect,
199 classpath: Option<String>,
200 module_path: Option<PathBuf>,
201 add_opens: Vec<String>,
202 add_exports: Vec<String>,
203 system_properties: Vec<String>,
204 env_vars: Vec<(String, String)>,
205 working_dir: Option<PathBuf>,
206 timeout: Option<Duration>,
207}
208
209#[derive(Debug, Default)]
215pub struct JavaRedirect {
216 output: Option<PathBuf>,
217 error: Option<PathBuf>,
218 input: Option<PathBuf>,
219 append_output: bool,
220 append_error: bool,
221}
222
223impl JavaRedirect {
224 pub fn new() -> Self {
226 Self::default()
227 }
228
229 pub fn output(mut self, path: impl AsRef<Path>) -> Self {
232 self.output = Some(path.as_ref().to_path_buf());
233 self
234 }
235
236 pub fn error(mut self, path: impl AsRef<Path>) -> Self {
239 self.error = Some(path.as_ref().to_path_buf());
240 self
241 }
242
243 pub fn input(mut self, path: impl AsRef<Path>) -> Self {
246 self.input = Some(path.as_ref().to_path_buf());
247 self
248 }
249
250 pub fn append_output(mut self) -> Self {
253 self.append_output = true;
254 self
255 }
256
257 pub fn append_error(mut self) -> Self {
260 self.append_error = true;
261 self
262 }
263}
264
265impl JavaRunner {
266 pub fn new() -> Self {
268 Self::default()
269 }
270
271 pub fn java(mut self, java: JavaInfo) -> Self {
275 self.java = Some(java);
276 self
277 }
278
279 pub fn jar(mut self, jar: impl AsRef<Path>) -> Self {
283 self.jar = Some(jar.as_ref().to_path_buf());
284 self
285 }
286
287 pub fn min_memory(mut self, bytes: usize) -> Self {
293 self.min_memory = Some(format_memory(bytes));
294 self
295 }
296
297 pub fn max_memory(mut self, bytes: usize) -> Self {
301 self.max_memory = Some(format_memory(bytes));
302 self
303 }
304
305 pub fn main_class(mut self, class: impl Into<String>) -> Self {
309 self.main_class = Some(class.into());
310 self
311 }
312
313 pub fn cmd(mut self, args: &[&str]) -> Self {
331 self.cmd_args = Some(args.iter().map(|s| s.to_string()).collect());
332 self
333 }
334
335 pub fn arg(mut self, arg: impl Into<String>) -> Self {
339 self.args.push(arg.into());
340 self
341 }
342
343 pub fn redirect(mut self, redirect: JavaRedirect) -> Self {
345 self.redirect = redirect;
346 self
347 }
348
349 pub fn classpath(mut self, paths: &[impl AsRef<Path>]) -> Self {
353 let separator = if cfg!(windows) { ";" } else { ":" };
354 let joined: Vec<String> = paths
355 .iter()
356 .map(|p| p.as_ref().to_string_lossy().to_string())
357 .collect();
358 self.classpath = Some(joined.join(separator));
359 self
360 }
361
362 pub fn module_path(mut self, path: impl AsRef<Path>) -> Self {
364 self.module_path = Some(path.as_ref().to_path_buf());
365 self
366 }
367
368 pub fn add_opens(
370 mut self,
371 module: impl Into<String>,
372 package: impl Into<String>,
373 target: impl Into<String>,
374 ) -> Self {
375 self.add_opens.push(format!(
376 "{}/{}.{}",
377 module.into(),
378 package.into(),
379 target.into()
380 ));
381 self
382 }
383
384 pub fn add_exports(
386 mut self,
387 module: impl Into<String>,
388 package: impl Into<String>,
389 target: impl Into<String>,
390 ) -> Self {
391 self.add_exports.push(format!(
392 "{}/{}.{}",
393 module.into(),
394 package.into(),
395 target.into()
396 ));
397 self
398 }
399
400 pub fn system_property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
402 self.system_properties
403 .push(format!("-D{}={}", key.into(), value.into()));
404 self
405 }
406
407 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
409 self.env_vars.push((key.into(), value.into()));
410 self
411 }
412
413 pub fn working_dir(mut self, path: impl AsRef<Path>) -> Self {
415 self.working_dir = Some(path.as_ref().to_path_buf());
416 self
417 }
418
419 pub fn timeout(mut self, duration: Duration) -> Self {
424 self.timeout = Some(duration);
425 self
426 }
427
428 pub fn execute(&self) -> Result<(), JavaError> {
441 let java = self.java.as_ref().ok_or_else(|| {
442 JavaError::Other("Must set Java environment via `.java(...)`".to_string())
443 })?;
444 let java_exe = java.java_executable()?;
445
446 let mut cmd = Command::new(java_exe);
447
448 if let Some(cp) = &self.classpath {
450 cmd.arg("-cp");
451 cmd.arg(cp);
452 }
453
454 if let Some(mp) = &self.module_path {
456 cmd.arg("--module-path");
457 cmd.arg(mp);
458 }
459
460 for open in &self.add_opens {
462 cmd.arg("--add-opens");
463 cmd.arg(open);
464 }
465
466 for export in &self.add_exports {
468 cmd.arg("--add-exports");
469 cmd.arg(export);
470 }
471
472 for prop in &self.system_properties {
474 cmd.arg(prop);
475 }
476
477 if let Some(min) = &self.min_memory {
478 cmd.arg(format!("-Xms{}", min));
479 }
480 if let Some(max) = &self.max_memory {
481 cmd.arg(format!("-Xmx{}", max));
482 }
483
484 if let Some(cmd_args) = &self.cmd_args {
485 cmd.args(cmd_args);
486 } else if let Some(jar) = &self.jar {
487 cmd.arg("-jar");
488 cmd.arg(jar);
489 } else if let Some(main) = &self.main_class {
490 cmd.arg(main);
491 } else {
492 return Err(JavaError::Other(
493 "Must specify JAR file, main class, or cmd args".into(),
494 ));
495 }
496
497 cmd.args(&self.args);
498
499 for (key, value) in &self.env_vars {
501 cmd.env(key, value);
502 }
503
504 if let Some(dir) = &self.working_dir {
506 cmd.current_dir(dir);
507 }
508
509 if let Some(output) = &self.redirect.output {
511 let file = if self.redirect.append_output {
512 OpenOptions::new().append(true).create(true).open(output)
513 } else {
514 File::create(output)
515 }
516 .map_err(JavaError::IoError)?;
517 cmd.stdout(Stdio::from(file));
518 } else {
519 cmd.stdout(Stdio::inherit());
520 }
521
522 if let Some(error) = &self.redirect.error {
523 let file = if self.redirect.append_error {
524 OpenOptions::new().append(true).create(true).open(error)
525 } else {
526 File::create(error)
527 }
528 .map_err(JavaError::IoError)?;
529 cmd.stderr(Stdio::from(file));
530 } else {
531 cmd.stderr(Stdio::inherit());
532 }
533
534 if let Some(input) = &self.redirect.input {
535 let file = File::open(input).map_err(JavaError::IoError)?;
536 cmd.stdin(Stdio::from(file));
537 } else {
538 cmd.stdin(Stdio::inherit());
539 }
540
541 if let Some(timeout) = self.timeout {
543 let mut child = cmd.spawn().map_err(JavaError::IoError)?;
544 let start = std::time::Instant::now();
545 loop {
546 if child.try_wait().map_err(JavaError::IoError)?.is_some() {
547 let status = child.wait().map_err(JavaError::IoError)?;
549 return if status.success() {
550 Ok(())
551 } else {
552 Err(JavaError::ExecutionFailed(format!(
553 "Execution failed: {}",
554 status.code().unwrap()
555 )))
556 };
557 }
558 if start.elapsed() >= timeout {
559 kill_process(&child)?;
561 return Err(JavaError::ExecutionFailed(format!(
562 "Process timed out after {}ms",
563 timeout.as_millis()
564 )));
565 }
566 std::thread::sleep(Duration::from_millis(50));
567 }
568 }
569
570 let status = cmd.status().map_err(JavaError::IoError)?;
571
572 if status.success() {
573 Ok(())
574 } else {
575 Err(JavaError::ExecutionFailed(format!(
576 "Execution failed: {}",
577 status.code().unwrap()
578 )))
579 }
580 }
581}
582
583fn format_memory(bytes: usize) -> String {
589 const MB: usize = 1024 * 1024;
590 const GB: usize = MB * 1024;
591
592 if bytes.is_multiple_of(GB) {
593 format!("{}g", bytes / GB)
594 } else if bytes.is_multiple_of(MB) {
595 format!("{}m", bytes / MB)
596 } else {
597 let mb = (bytes + MB / 2) / MB;
598 format!("{}m", mb)
599 }
600}
601
602fn kill_process(child: &std::process::Child) -> Result<(), JavaError> {
603 let pid = child.id();
606 #[cfg(windows)]
607 let status = std::process::Command::new("taskkill")
608 .args(["/PID", &pid.to_string(), "/F"])
609 .status()
610 .map_err(JavaError::IoError)?;
611 #[cfg(not(windows))]
612 let status = std::process::Command::new("kill")
613 .args(["-9", &pid.to_string()])
614 .status()
615 .map_err(JavaError::IoError)?;
616
617 if status.success() {
618 Ok(())
619 } else {
620 Err(JavaError::Other(format!("Failed to kill process {}", pid)))
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use crate::JavaInfo;
628
629 #[test]
630 fn test_format_memory_exact_mb() {
631 assert_eq!(format_memory(256 * 1024 * 1024), "256m");
632 }
633
634 #[test]
635 fn test_format_memory_exact_gb() {
636 assert_eq!(format_memory(2 * 1024 * 1024 * 1024), "2g");
637 }
638
639 #[test]
640 fn test_format_memory_rounded() {
641 let result = format_memory(100 * 1024 * 1024 + 512 * 1024);
642 assert!(result.ends_with('m'));
643 let num: usize = result[..result.len() - 1].parse().unwrap();
644 assert!(num >= 100);
645 }
646
647 #[test]
648 fn test_format_memory_zero() {
649 assert_eq!(format_memory(0), "0g");
650 }
651
652 #[test]
653 fn test_format_memory_small() {
654 let result = format_memory(1);
655 assert_eq!(result, "0m");
656 }
657
658 #[test]
659 fn test_runner_missing_java() {
660 let result = JavaRunner::new().jar("test.jar").execute();
661 assert!(result.is_err());
662 let err = result.unwrap_err().to_string();
663 assert!(err.contains("Must set Java environment"));
664 }
665
666 #[test]
667 fn test_runner_missing_jar_or_main() {
668 let info = JavaInfo::default();
670 let result = JavaRunner::new().java(info).execute();
671 assert!(result.is_err());
672 }
673
674 #[test]
675 fn test_java_redirect_default() {
676 let r = JavaRedirect::new();
677 assert!(r.output.is_none());
678 assert!(r.error.is_none());
679 assert!(r.input.is_none());
680 assert!(!r.append_output);
681 assert!(!r.append_error);
682 }
683
684 #[test]
685 fn test_java_redirect_append() {
686 let r = JavaRedirect::new()
687 .output("out.log")
688 .append_output()
689 .error("err.log")
690 .append_error();
691 assert!(r.append_output);
692 assert!(r.append_error);
693 }
694
695 #[test]
696 fn test_runner_builder_methods() {
697 let runner = JavaRunner::new()
698 .system_property("foo", "bar")
699 .add_opens("java.base", "java.lang", "ALL-UNNAMED")
700 .add_exports("java.base", "com.sun.internal", "ALL-UNNAMED");
701 assert_eq!(runner.system_properties, vec!["-Dfoo=bar"]);
702 assert_eq!(runner.add_opens, vec!["java.base/java.lang.ALL-UNNAMED"]);
703 assert_eq!(
704 runner.add_exports,
705 vec!["java.base/com.sun.internal.ALL-UNNAMED"]
706 );
707 }
708
709 #[test]
710 fn test_output_mode_debug_clone() {
711 let mode = OutputMode::Both;
712 let cloned = mode;
713 assert_eq!(mode, cloned);
714 let _ = format!("{:?}", mode);
715 }
716
717 #[test]
718 fn test_format_memory_1gb_plus_1b() {
719 let result = format_memory(1024 * 1024 * 1024 + 1);
720 assert_eq!(result, "1024m");
721 }
722
723 #[test]
724 fn test_format_memory_1_mib() {
725 assert_eq!(format_memory(1024 * 1024), "1m");
726 }
727
728 #[test]
729 fn test_format_memory_1024_mib_is_1g() {
730 assert_eq!(format_memory(1024 * 1024 * 1024), "1g");
731 }
732
733 #[test]
734 fn test_runner_env_var() {
735 let runner = JavaRunner::new().env("MY_VAR", "my_value");
736 assert_eq!(runner.env_vars, vec![("MY_VAR".into(), "my_value".into())]);
737 }
738
739 #[test]
740 fn test_runner_working_dir() {
741 let runner = JavaRunner::new().working_dir("/tmp");
742 assert_eq!(runner.working_dir, Some(PathBuf::from("/tmp")));
743 }
744
745 #[test]
746 fn test_runner_timeout() {
747 let runner = JavaRunner::new().timeout(Duration::from_secs(30));
748 assert_eq!(runner.timeout, Some(Duration::from_secs(30)));
749 }
750
751 #[test]
752 fn test_runner_classpath() {
753 let separator = if cfg!(windows) { ";" } else { ":" };
754 let runner = JavaRunner::new().classpath(&["lib/a.jar", "config"]);
755 assert_eq!(
756 runner.classpath,
757 Some(format!("lib/a.jar{separator}config"))
758 );
759 }
760
761 #[test]
762 fn test_runner_module_path() {
763 let runner = JavaRunner::new().module_path("./modules");
764 assert_eq!(runner.module_path, Some(PathBuf::from("./modules")));
765 }
766
767 #[test]
768 fn test_runner_multiple_args() {
769 let runner = JavaRunner::new().arg("--verbose").arg("--debug");
770 assert_eq!(runner.args, vec!["--verbose", "--debug"]);
771 }
772
773 #[test]
774 fn test_runner_all_builder_methods() {
775 let runner = JavaRunner::new()
776 .classpath(&["lib/*"])
777 .module_path("mods")
778 .add_opens("java.base", "java.lang", "ALL-UNNAMED")
779 .add_exports("java.base", "sun.security", "ALL-UNNAMED")
780 .system_property("key", "val")
781 .env("HOME", "/root")
782 .working_dir("/app")
783 .timeout(Duration::from_secs(10))
784 .min_memory(256 * 1024 * 1024)
785 .max_memory(1024 * 1024 * 1024);
786
787 assert!(runner.classpath.is_some());
788 assert!(runner.module_path.is_some());
789 assert_eq!(runner.add_opens.len(), 1);
790 assert_eq!(runner.add_exports.len(), 1);
791 assert_eq!(runner.system_properties.len(), 1);
792 assert_eq!(runner.env_vars.len(), 1);
793 assert!(runner.working_dir.is_some());
794 assert_eq!(runner.timeout, Some(Duration::from_secs(10)));
795 assert!(runner.min_memory.is_some());
796 assert!(runner.max_memory.is_some());
797 }
798
799 #[test]
802 fn test_runner_cmd_sets_field() {
803 let runner = JavaRunner::new().cmd(&["-version"]);
804 assert_eq!(runner.cmd_args, Some(vec!["-version".to_string()]));
805 }
806
807 #[test]
808 fn test_runner_cmd_multiple_args() {
809 let runner = JavaRunner::new().cmd(&["--list-modules", "--add-modules", "java.se"]);
810 assert_eq!(
811 runner.cmd_args,
812 Some(vec![
813 "--list-modules".to_string(),
814 "--add-modules".to_string(),
815 "java.se".to_string(),
816 ])
817 );
818 }
819
820 #[test]
821 fn test_runner_cmd_bypasses_jar_main_check() {
822 let info = JavaInfo::default();
827 let result = JavaRunner::new().java(info).cmd(&["-version"]).execute();
828 let err = result.unwrap_err().to_string();
829 assert!(!err.contains("Must specify JAR file"));
830 assert!(err.contains("Not found") || err.contains("Java executable not found"));
831 }
832}