Skip to main content

java_manager/
execute.rs

1//! Running Java programs with controlled output and redirection.
2
3use crate::{JavaError, JavaInfo};
4use std::fs::File;
5use std::io::{BufRead, BufReader};
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8use std::thread;
9
10/// Controls which output streams are printed to the console.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12enum OutputMode {
13    Both,
14    OutputOnly,
15    ErrorOnly,
16}
17
18impl JavaInfo {
19    /// Executes the Java executable with the given arguments, printing both
20    /// stdout and stderr to the console.
21    ///
22    /// The argument string is split using shell‑like rules (via `shell_words`).
23    /// The child process's stdout and stderr are captured and printed line by line
24    /// while the process runs.
25    ///
26    /// # Errors
27    ///
28    /// Returns `JavaError::IoError` if spawning or waiting fails.
29    /// Returns `JavaError::Other` if the argument string cannot be parsed.
30    /// Returns `JavaError::ExecutionFailed` if the Java process exits with a non‑zero status.
31    ///
32    /// # Examples
33    ///
34    /// ```no_run
35    /// # use java_manager::JavaInfo;
36    /// # let java = JavaInfo::new("/path/to/java".into())?;
37    /// java.execute("-version")?;
38    /// # Ok::<_, java_manager::JavaError>(())
39    /// ```
40    pub fn execute(&self, args: &str) -> Result<(), JavaError> {
41        self.run_java(args, OutputMode::Both)
42    }
43
44    /// Executes the Java executable, printing only stderr to the console.
45    /// Stdout is captured and discarded.
46    ///
47    /// See [`execute`](JavaInfo::execute) for details.
48    pub fn execute_with_error(&self, args: &str) -> Result<(), JavaError> {
49        self.run_java(args, OutputMode::ErrorOnly)
50    }
51
52    /// Executes the Java executable, printing only stdout to the console.
53    /// Stderr is captured and discarded.
54    ///
55    /// See [`execute`](JavaInfo::execute) for details.
56    pub fn execute_with_output(&self, args: &str) -> Result<(), JavaError> {
57        self.run_java(args, OutputMode::OutputOnly)
58    }
59
60    /// Internal implementation of Java execution with configurable output.
61    fn run_java(&self, args: &str, mode: OutputMode) -> Result<(), JavaError> {
62        let java_exe = self.java_executable()?;
63
64        let arg_vec = shell_words::split(args)
65            .map_err(|e| JavaError::Other(format!("Failed to parse arguments: {}", e)))?;
66
67        let mut cmd = Command::new(java_exe);
68        cmd.args(&arg_vec);
69
70        cmd.stdout(Stdio::piped());
71        cmd.stderr(Stdio::piped());
72
73        let mut child = cmd.spawn().map_err(JavaError::IoError)?;
74
75        let stdout = child.stdout.take().expect("Failed to get stdout pipe");
76        let stderr = child.stderr.take().expect("Failed to get stderr pipe");
77
78        let stdout_handle = if matches!(mode, OutputMode::Both | OutputMode::OutputOnly) {
79            Some(thread::spawn(move || {
80                let reader = BufReader::new(stdout);
81                for line in reader.lines() {
82                    if let Ok(line) = line {
83                        println!("{}", line);
84                    }
85                }
86            }))
87        } else {
88            None
89        };
90
91        let stderr_handle = if matches!(mode, OutputMode::Both | OutputMode::ErrorOnly) {
92            Some(thread::spawn(move || {
93                let reader = BufReader::new(stderr);
94                for line in reader.lines() {
95                    if let Ok(line) = line {
96                        eprintln!("{}", line);
97                    }
98                }
99            }))
100        } else {
101            None
102        };
103
104        let status = child.wait().map_err(JavaError::IoError)?;
105
106        if let Some(handle) = stdout_handle {
107            handle.join().unwrap();
108        }
109        if let Some(handle) = stderr_handle {
110            handle.join().unwrap();
111        }
112
113        if status.success() {
114            Ok(())
115        } else {
116            Err(JavaError::ExecutionFailed(format!("Execution failed: {}", status.code().unwrap())))
117        }
118    }
119
120    /// Returns the path to the `java` executable inside this installation's `JAVA_HOME/bin`.
121    ///
122    /// # Errors
123    ///
124    /// Returns `JavaError::NotFound` if the executable does not exist.
125    fn java_executable(&self) -> Result<PathBuf, JavaError> {
126        let java_home = &self.java_home;
127        let exe_name = if cfg!(windows) { "java.exe" } else { "java" };
128        let java_exe = java_home.join("bin").join(exe_name);
129        if java_exe.exists() {
130            Ok(java_exe)
131        } else {
132            Err(JavaError::NotFound(format!(
133                "Java executable not found: {:?}",
134                java_exe
135            )))
136        }
137    }
138}
139
140/// A builder for configuring and executing a Java program (JAR or main class).
141///
142/// This struct allows you to set the Java runtime, JAR file or main class,
143/// memory limits, program arguments, and I/O redirection before spawning the
144/// process.
145///
146/// # Examples
147///
148/// ```no_run
149/// use java_manager::{JavaRunner, JavaRedirect};
150///
151/// # let java = java_manager::java_home().unwrap();
152/// JavaRunner::new()
153///     .java(java)
154///     .jar("myapp.jar")
155///     .min_memory(256 * 1024 * 1024)   // 256 MB
156///     .max_memory(1024 * 1024 * 1024)  // 1 GB
157///     .arg("--server")
158///     .redirect(JavaRedirect::new().output("out.log").error("err.log"))
159///     .execute()?;
160/// # Ok::<_, java_manager::JavaError>(())
161/// ```
162#[derive(Debug, Default)]
163pub struct JavaRunner {
164    java: Option<JavaInfo>,
165    jar: Option<PathBuf>,
166    min_memory: Option<String>,
167    max_memory: Option<String>,
168    main_class: Option<String>,
169    args: Vec<String>,
170    redirect: JavaRedirect,
171}
172
173/// I/O redirection options for a Java process.
174///
175/// Use the builder methods to specify files for stdout, stderr, and stdin.
176/// If a stream is not redirected, it will inherit the parent's corresponding
177/// stream (i.e., print to console or read from keyboard).
178#[derive(Debug, Default)]
179pub struct JavaRedirect {
180    output: Option<PathBuf>,
181    error: Option<PathBuf>,
182    input: Option<PathBuf>,
183}
184
185impl JavaRedirect {
186    /// Creates a new empty redirection configuration.
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    /// Redirects the Java process's standard output to the given file.
192    /// The file will be created (or truncated) before execution.
193    pub fn output(mut self, path: impl AsRef<Path>) -> Self {
194        self.output = Some(path.as_ref().to_path_buf());
195        self
196    }
197
198    /// Redirects the Java process's standard error to the given file.
199    /// The file will be created (or truncated) before execution.
200    pub fn error(mut self, path: impl AsRef<Path>) -> Self {
201        self.error = Some(path.as_ref().to_path_buf());
202        self
203    }
204
205    /// Redirects the Java process's standard input from the given file.
206    /// The file must exist and be readable.
207    pub fn input(mut self, path: impl AsRef<Path>) -> Self {
208        self.input = Some(path.as_ref().to_path_buf());
209        self
210    }
211}
212
213impl JavaRunner {
214    /// Creates a new builder with default settings.
215    pub fn new() -> Self {
216        Self::default()
217    }
218
219    /// Sets the Java installation to use.
220    ///
221    /// This is mandatory before calling `execute`.
222    pub fn java(mut self, java: JavaInfo) -> Self {
223        self.java = Some(java);
224        self
225    }
226
227    /// Sets the JAR file to execute (implies the `-jar` flag).
228    ///
229    /// Either `jar` or `main_class` must be set.
230    pub fn jar(mut self, jar: impl AsRef<Path>) -> Self {
231        self.jar = Some(jar.as_ref().to_path_buf());
232        self
233    }
234
235    /// Sets the initial heap size (`-Xms`).
236    ///
237    /// The value is given in bytes and will be formatted as a memory string
238    /// (e.g., `256m`, `1g`). If the size is not a multiple of a megabyte or gigabyte,
239    /// it will be rounded to the nearest megabyte.
240    pub fn min_memory(mut self, bytes: usize) -> Self {
241        self.min_memory = Some(format_memory(bytes));
242        self
243    }
244
245    /// Sets the maximum heap size (`-Xmx`).
246    ///
247    /// See [`min_memory`](JavaRunner::min_memory) for formatting details.
248    pub fn max_memory(mut self, bytes: usize) -> Self {
249        self.max_memory = Some(format_memory(bytes));
250        self
251    }
252
253    /// Sets the main class to execute (instead of a JAR file).
254    ///
255    /// Either `jar` or `main_class` must be set.
256    pub fn main_class(mut self, class: impl Into<String>) -> Self {
257        self.main_class = Some(class.into());
258        self
259    }
260
261    /// Adds a single argument to be passed to the Java program.
262    ///
263    /// Arguments are appended in the order they are added.
264    pub fn arg(mut self, arg: impl Into<String>) -> Self {
265        self.args.push(arg.into());
266        self
267    }
268
269    /// Sets I/O redirection options.
270    pub fn redirect(mut self, redirect: JavaRedirect) -> Self {
271        self.redirect = redirect;
272        self
273    }
274
275    /// Executes the configured Java program.
276    ///
277    /// # Errors
278    ///
279    /// Returns `JavaError::Other` if no Java installation has been set, or if
280    /// neither a JAR file nor a main class has been specified.
281    /// Returns `JavaError::NotFound` if the Java executable does not exist.
282    /// Returns `JavaError::IoError` if file operations or process spawning fail.
283    /// Returns `JavaError::ExecutionFailed` if the Java process exits with a non‑zero status.
284    pub fn execute(self) -> Result<(), JavaError> {
285        let java = self.java.ok_or_else(|| {
286            JavaError::Other("Must set Java environment via `.java(...)`".to_string())
287        })?;
288        let java_exe = java.java_executable()?;
289
290        let mut cmd = Command::new(java_exe);
291
292        if let Some(min) = &self.min_memory {
293            cmd.arg(format!("-Xms{}", min));
294        }
295        if let Some(max) = &self.max_memory {
296            cmd.arg(format!("-Xmx{}", max));
297        }
298
299        if let Some(jar) = self.jar {
300            cmd.arg("-jar");
301            cmd.arg(jar);
302        } else if let Some(main) = self.main_class {
303            cmd.arg(main);
304        } else {
305            return Err(JavaError::Other("Must specify JAR file or main class".into()));
306        }
307
308        cmd.args(&self.args);
309
310        // Configure redirection
311        if let Some(output) = self.redirect.output {
312            let file = File::create(output).map_err(JavaError::IoError)?;
313            cmd.stdout(Stdio::from(file));
314        } else {
315            cmd.stdout(Stdio::inherit());
316        }
317
318        if let Some(error) = self.redirect.error {
319            let file = File::create(error).map_err(JavaError::IoError)?;
320            cmd.stderr(Stdio::from(file));
321        } else {
322            cmd.stderr(Stdio::inherit());
323        }
324
325        if let Some(input) = self.redirect.input {
326            let file = File::open(input).map_err(JavaError::IoError)?;
327            cmd.stdin(Stdio::from(file));
328        } else {
329            cmd.stdin(Stdio::inherit());
330        }
331
332        let status = cmd.status().map_err(JavaError::IoError)?;
333
334        if status.success() {
335            Ok(())
336        } else {
337            Err(JavaError::ExecutionFailed(format!("Execution failed: {}", status.code().unwrap())))
338        }
339    }
340}
341
342/// Formats a memory size in bytes into a Java‑compatible string (`<n>m` or `<n>g`).
343///
344/// If the size is an exact multiple of 1 GiB, it is formatted as `<n>g`.
345/// Otherwise, if it is an exact multiple of 1 MiB, it is formatted as `<n>m`.
346/// If neither, it is rounded to the nearest mebibyte and formatted as `<n>m`.
347fn format_memory(bytes: usize) -> String {
348    const MB: usize = 1024 * 1024;
349    const GB: usize = MB * 1024;
350
351    if bytes % GB == 0 {
352        format!("{}g", bytes / GB)
353    } else if bytes % MB == 0 {
354        format!("{}m", bytes / MB)
355    } else {
356        let mb = (bytes + MB / 2) / MB;
357        format!("{}m", mb)
358    }
359}