lighty_java/runtime.rs
1// Copyright (c) 2025 Hamadi
2// Licensed under the MIT License
3
4//! Java Runtime Execution
5//!
6//! This module provides a wrapper for executing Java processes with proper
7//! I/O handling and lifecycle management.
8
9use crate::errors::{JavaRuntimeError, JavaRuntimeResult};
10use std::path::{Path, PathBuf};
11use std::process::Stdio;
12use tokio::io::AsyncReadExt;
13use tokio::process::{Child, Command};
14use tokio::sync::oneshot::Receiver;
15
16/// Wrapper around a Java binary path for process execution
17pub struct JavaRuntime(pub PathBuf);
18
19impl JavaRuntime {
20 /// Creates a new JavaRuntime from a binary path
21 pub fn new(path: PathBuf) -> Self {
22 Self(path)
23 }
24
25 /// Spawns a Java process with the given arguments
26 ///
27 /// # Arguments
28 /// * `arguments` - Command-line arguments for the Java process
29 /// * `game_dir` - Working directory for the process
30 ///
31 /// # Returns
32 /// A handle to the spawned child process
33 ///
34 /// # Errors
35 /// Returns an error if the binary doesn't exist or the spawn fails
36 pub async fn execute(&self, arguments: Vec<String>, game_dir: &Path) -> JavaRuntimeResult<Child> {
37 // Validate binary exists
38 if !self.0.exists() {
39 return Err(JavaRuntimeError::NotFound {
40 path: self.0.clone(),
41 });
42 }
43
44 lighty_core::trace_debug!("Spawning Java process: {:?}", &self.0);
45 lighty_core::trace_info!("Java arguments: {:?}", &arguments);
46
47 // Build and spawn command
48 let child = Command::new(&self.0)
49 .current_dir(game_dir)
50 .args(arguments)
51 .stdout(Stdio::piped())
52 .stderr(Stdio::piped())
53 .spawn()?;
54
55 lighty_core::trace_info!("Java process spawned successfully");
56 Ok(child)
57 }
58
59 /// Streams stdout/stderr from the process with custom handlers
60 ///
61 /// This method handles I/O from the Java process, calling provided callbacks
62 /// for stdout and stderr output. It continues until the process exits or
63 /// the terminator signal is received.
64 ///
65 /// # Arguments
66 /// * `process` - Mutable reference to the child process
67 /// * `on_stdout` - Callback for stdout data
68 /// * `on_stderr` - Callback for stderr data
69 /// * `terminator` - Channel to signal early termination
70 /// * `data` - User data passed to callbacks
71 ///
72 /// # Returns
73 /// Ok(()) on clean exit, or error if the process exits with non-zero code
74 ///
75 /// # Note
76 /// Exit code -1073740791 (Windows forceful termination) is not treated as an error
77 pub async fn handle_io<D: Send + Sync>(
78 &self,
79 process: &mut Child,
80 on_stdout: fn(&D, &[u8]) -> JavaRuntimeResult<()>,
81 on_stderr: fn(&D, &[u8]) -> JavaRuntimeResult<()>,
82 terminator: Receiver<()>,
83 data: &D,
84 ) -> JavaRuntimeResult<()> {
85 // Extract stdout and stderr pipes
86 let mut stdout = process
87 .stdout
88 .take()
89 .ok_or(JavaRuntimeError::IoCaptureFailure)?;
90 let mut stderr = process
91 .stderr
92 .take()
93 .ok_or(JavaRuntimeError::IoCaptureFailure)?;
94
95 // Prepare read buffers (stack-allocated for better performance)
96 // 8KB is optimal for most Java logs while avoiding stack overflow
97 let mut stdout_buffer = [0u8; 8192];
98 let mut stderr_buffer = [0u8; 8192];
99
100 tokio::pin!(terminator);
101
102 // Main I/O loop
103 loop {
104 tokio::select! {
105 // Handle stdout data
106 result = stdout.read(&mut stdout_buffer) => {
107 match result {
108 Ok(bytes_read) if bytes_read > 0 => {
109 let _ = on_stdout(data, &stdout_buffer[..bytes_read]);
110 }
111 Ok(_) => {}, // EOF reached
112 Err(_) => break, // Stream closed
113 }
114 },
115
116 // Handle stderr data
117 result = stderr.read(&mut stderr_buffer) => {
118 match result {
119 Ok(bytes_read) if bytes_read > 0 => {
120 let _ = on_stderr(data, &stderr_buffer[..bytes_read]);
121 }
122 Ok(_) => {}, // EOF reached
123 Err(_) => break, // Stream closed
124 }
125 },
126
127 // Handle early termination signal
128 _ = &mut terminator => {
129 lighty_core::trace_debug!("Termination signal received, killing process");
130 process.kill().await?;
131 break;
132 },
133
134 // Handle process exit
135 exit_result = process.wait() => {
136 let exit_status = exit_result?;
137 let exit_code = exit_status.code().unwrap_or(7900);
138
139 lighty_core::trace_debug!("Java process exited with code: {}", exit_code);
140
141 // Check for error exit codes
142 // -1073740791 = Windows forceful termination (not an error)
143 if exit_code != 0 && exit_code != -1073740791 {
144 return Err(JavaRuntimeError::NonZeroExit { code: exit_code });
145 }
146
147 break;
148 },
149 }
150 }
151
152 Ok(())
153 }
154}