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().map_while(Result::ok) {
82                    println!("{}", line);
83                }
84            }))
85        } else {
86            None
87        };
88
89        let stderr_handle = if matches!(mode, OutputMode::Both | OutputMode::ErrorOnly) {
90            Some(thread::spawn(move || {
91                let reader = BufReader::new(stderr);
92                for line in reader.lines().map_while(Result::ok) {
93                    eprintln!("{}", line);
94                }
95            }))
96        } else {
97            None
98        };
99
100        let status = child.wait().map_err(JavaError::IoError)?;
101
102        if let Some(handle) = stdout_handle {
103            handle.join().unwrap();
104        }
105        if let Some(handle) = stderr_handle {
106            handle.join().unwrap();
107        }
108
109        if status.success() {
110            Ok(())
111        } else {
112            Err(JavaError::ExecutionFailed(format!(
113                "Execution failed: {}",
114                status.code().unwrap()
115            )))
116        }
117    }
118
119    /// Returns the path to the `java` executable inside this installation's `JAVA_HOME/bin`.
120    ///
121    /// # Errors
122    ///
123    /// Returns `JavaError::NotFound` if the executable does not exist.
124    fn java_executable(&self) -> Result<PathBuf, JavaError> {
125        let java_home = &self.java_home;
126        let exe_name = if cfg!(windows) { "java.exe" } else { "java" };
127        let java_exe = java_home.join("bin").join(exe_name);
128        if java_exe.exists() {
129            Ok(java_exe)
130        } else {
131            Err(JavaError::NotFound(format!(
132                "Java executable not found: {:?}",
133                java_exe
134            )))
135        }
136    }
137}
138
139/// A builder for configuring and executing a Java program (JAR or main class).
140///
141/// This struct allows you to set the Java runtime, JAR file or main class,
142/// memory limits, program arguments, and I/O redirection before spawning the
143/// process.
144///
145/// # Examples
146///
147/// ```no_run
148/// use java_manager::{JavaRunner, JavaRedirect};
149///
150/// # let java = java_manager::java_home().unwrap();
151/// JavaRunner::new()
152///     .java(java)
153///     .jar("myapp.jar")
154///     .min_memory(256 * 1024 * 1024)   // 256 MB
155///     .max_memory(1024 * 1024 * 1024)  // 1 GB
156///     .arg("--server")
157///     .redirect(JavaRedirect::new().output("out.log").error("err.log"))
158///     .execute()?;
159/// # Ok::<_, java_manager::JavaError>(())
160/// ```
161#[derive(Debug, Default)]
162pub struct JavaRunner {
163    java: Option<JavaInfo>,
164    jar: Option<PathBuf>,
165    min_memory: Option<String>,
166    max_memory: Option<String>,
167    main_class: Option<String>,
168    args: Vec<String>,
169    redirect: JavaRedirect,
170}
171
172/// I/O redirection options for a Java process.
173///
174/// Use the builder methods to specify files for stdout, stderr, and stdin.
175/// If a stream is not redirected, it will inherit the parent's corresponding
176/// stream (i.e., print to console or read from keyboard).
177#[derive(Debug, Default)]
178pub struct JavaRedirect {
179    output: Option<PathBuf>,
180    error: Option<PathBuf>,
181    input: Option<PathBuf>,
182}
183
184impl JavaRedirect {
185    /// Creates a new empty redirection configuration.
186    pub fn new() -> Self {
187        Self::default()
188    }
189
190    /// Redirects the Java process's standard output to the given file.
191    /// The file will be created (or truncated) before execution.
192    pub fn output(mut self, path: impl AsRef<Path>) -> Self {
193        self.output = Some(path.as_ref().to_path_buf());
194        self
195    }
196
197    /// Redirects the Java process's standard error to the given file.
198    /// The file will be created (or truncated) before execution.
199    pub fn error(mut self, path: impl AsRef<Path>) -> Self {
200        self.error = Some(path.as_ref().to_path_buf());
201        self
202    }
203
204    /// Redirects the Java process's standard input from the given file.
205    /// The file must exist and be readable.
206    pub fn input(mut self, path: impl AsRef<Path>) -> Self {
207        self.input = Some(path.as_ref().to_path_buf());
208        self
209    }
210}
211
212impl JavaRunner {
213    /// Creates a new builder with default settings.
214    pub fn new() -> Self {
215        Self::default()
216    }
217
218    /// Sets the Java installation to use.
219    ///
220    /// This is mandatory before calling `execute`.
221    pub fn java(mut self, java: JavaInfo) -> Self {
222        self.java = Some(java);
223        self
224    }
225
226    /// Sets the JAR file to execute (implies the `-jar` flag).
227    ///
228    /// Either `jar` or `main_class` must be set.
229    pub fn jar(mut self, jar: impl AsRef<Path>) -> Self {
230        self.jar = Some(jar.as_ref().to_path_buf());
231        self
232    }
233
234    /// Sets the initial heap size (`-Xms`).
235    ///
236    /// The value is given in bytes and will be formatted as a memory string
237    /// (e.g., `256m`, `1g`). If the size is not a multiple of a megabyte or gigabyte,
238    /// it will be rounded to the nearest megabyte.
239    pub fn min_memory(mut self, bytes: usize) -> Self {
240        self.min_memory = Some(format_memory(bytes));
241        self
242    }
243
244    /// Sets the maximum heap size (`-Xmx`).
245    ///
246    /// See [`min_memory`](JavaRunner::min_memory) for formatting details.
247    pub fn max_memory(mut self, bytes: usize) -> Self {
248        self.max_memory = Some(format_memory(bytes));
249        self
250    }
251
252    /// Sets the main class to execute (instead of a JAR file).
253    ///
254    /// Either `jar` or `main_class` must be set.
255    pub fn main_class(mut self, class: impl Into<String>) -> Self {
256        self.main_class = Some(class.into());
257        self
258    }
259
260    /// Adds a single argument to be passed to the Java program.
261    ///
262    /// Arguments are appended in the order they are added.
263    pub fn arg(mut self, arg: impl Into<String>) -> Self {
264        self.args.push(arg.into());
265        self
266    }
267
268    /// Sets I/O redirection options.
269    pub fn redirect(mut self, redirect: JavaRedirect) -> Self {
270        self.redirect = redirect;
271        self
272    }
273
274    /// Executes the configured Java program.
275    ///
276    /// # Errors
277    ///
278    /// Returns `JavaError::Other` if no Java installation has been set, or if
279    /// neither a JAR file nor a main class has been specified.
280    /// Returns `JavaError::NotFound` if the Java executable does not exist.
281    /// Returns `JavaError::IoError` if file operations or process spawning fail.
282    /// Returns `JavaError::ExecutionFailed` if the Java process exits with a non‑zero status.
283    pub fn execute(self) -> Result<(), JavaError> {
284        let java = self.java.ok_or_else(|| {
285            JavaError::Other("Must set Java environment via `.java(...)`".to_string())
286        })?;
287        let java_exe = java.java_executable()?;
288
289        let mut cmd = Command::new(java_exe);
290
291        if let Some(min) = &self.min_memory {
292            cmd.arg(format!("-Xms{}", min));
293        }
294        if let Some(max) = &self.max_memory {
295            cmd.arg(format!("-Xmx{}", max));
296        }
297
298        if let Some(jar) = self.jar {
299            cmd.arg("-jar");
300            cmd.arg(jar);
301        } else if let Some(main) = self.main_class {
302            cmd.arg(main);
303        } else {
304            return Err(JavaError::Other(
305                "Must specify JAR file or main class".into(),
306            ));
307        }
308
309        cmd.args(&self.args);
310
311        // Configure redirection
312        if let Some(output) = self.redirect.output {
313            let file = File::create(output).map_err(JavaError::IoError)?;
314            cmd.stdout(Stdio::from(file));
315        } else {
316            cmd.stdout(Stdio::inherit());
317        }
318
319        if let Some(error) = self.redirect.error {
320            let file = File::create(error).map_err(JavaError::IoError)?;
321            cmd.stderr(Stdio::from(file));
322        } else {
323            cmd.stderr(Stdio::inherit());
324        }
325
326        if let Some(input) = self.redirect.input {
327            let file = File::open(input).map_err(JavaError::IoError)?;
328            cmd.stdin(Stdio::from(file));
329        } else {
330            cmd.stdin(Stdio::inherit());
331        }
332
333        let status = cmd.status().map_err(JavaError::IoError)?;
334
335        if status.success() {
336            Ok(())
337        } else {
338            Err(JavaError::ExecutionFailed(format!(
339                "Execution failed: {}",
340                status.code().unwrap()
341            )))
342        }
343    }
344}
345
346/// Formats a memory size in bytes into a Java‑compatible string (`<n>m` or `<n>g`).
347///
348/// If the size is an exact multiple of 1 GiB, it is formatted as `<n>g`.
349/// Otherwise, if it is an exact multiple of 1 MiB, it is formatted as `<n>m`.
350/// If neither, it is rounded to the nearest mebibyte and formatted as `<n>m`.
351fn format_memory(bytes: usize) -> String {
352    const MB: usize = 1024 * 1024;
353    const GB: usize = MB * 1024;
354
355    if bytes.is_multiple_of(GB) {
356        format!("{}g", bytes / GB)
357    } else if bytes.is_multiple_of(MB) {
358        format!("{}m", bytes / MB)
359    } else {
360        let mb = (bytes + MB / 2) / MB;
361        format!("{}m", mb)
362    }
363}