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}