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}