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}