Skip to main content

lighty_java/
runtime.rs

1// Copyright (c) 2025 Hamadi
2// Licensed under the MIT License
3
4//! Java process execution wrapper.
5
6use crate::errors::{JavaRuntimeError, JavaRuntimeResult};
7use std::path::{Path, PathBuf};
8use std::process::Stdio;
9use tokio::io::AsyncReadExt;
10use tokio::process::{Child, Command};
11use tokio::sync::oneshot::Receiver;
12
13/// Wrapper around a Java binary path for process execution
14pub struct JavaRuntime(pub PathBuf);
15
16impl JavaRuntime {
17    /// Creates a new JavaRuntime from a binary path
18    pub fn new(path: PathBuf) -> Self {
19        Self(path)
20    }
21
22    /// Spawns a Java process with the given arguments in `game_dir`.
23    pub async fn execute(&self, arguments: Vec<String>, game_dir: &Path) -> JavaRuntimeResult<Child> {
24        if !self.0.exists() {
25            return Err(JavaRuntimeError::NotFound {
26                path: self.0.clone(),
27            });
28        }
29
30        lighty_core::trace_debug!("Spawning Java process: {:?}", &self.0);
31
32        let mut command = Command::new(&self.0);
33        command
34            .current_dir(game_dir)
35            .args(arguments)
36            .stdin(Stdio::null())
37            .stdout(Stdio::piped())
38            .stderr(Stdio::piped());
39
40        // On Windows, hide the console window
41        #[cfg(windows)]
42        {
43            use std::os::windows::process::CommandExt;
44            const CREATE_NO_WINDOW: u32 = 0x08000000;
45            command.creation_flags(CREATE_NO_WINDOW);
46        }
47
48        let child = command.spawn()?;
49
50        lighty_core::trace_info!("Java process spawned successfully");
51        Ok(child)
52    }
53
54    /// Streams stdout/stderr from the process with custom handlers.
55    ///
56    /// Calls `on_stdout`/`on_stderr` callbacks until the process exits or
57    /// `terminator` fires. Exit code -1073740791 (Windows forceful termination)
58    /// is not treated as an error.
59    pub async fn handle_io<D: Send + Sync>(
60        &self,
61        process: &mut Child,
62        on_stdout: fn(&D, &[u8]) -> JavaRuntimeResult<()>,
63        on_stderr: fn(&D, &[u8]) -> JavaRuntimeResult<()>,
64        terminator: Receiver<()>,
65        data: &D,
66    ) -> JavaRuntimeResult<()> {
67        let mut stdout = process
68            .stdout
69            .take()
70            .ok_or(JavaRuntimeError::IoCaptureFailure)?;
71        let mut stderr = process
72            .stderr
73            .take()
74            .ok_or(JavaRuntimeError::IoCaptureFailure)?;
75
76        // 8KB is optimal for most Java logs while avoiding stack overflow
77        let mut stdout_buffer = [0u8; 8192];
78        let mut stderr_buffer = [0u8; 8192];
79
80        tokio::pin!(terminator);
81
82        loop {
83            tokio::select! {
84                result = stdout.read(&mut stdout_buffer) => {
85                    match result {
86                        Ok(bytes_read) if bytes_read > 0 => {
87                            let _ = on_stdout(data, &stdout_buffer[..bytes_read]);
88                        }
89                        Ok(_) => {}, // EOF reached
90                        Err(_) => break, // Stream closed
91                    }
92                },
93
94                result = stderr.read(&mut stderr_buffer) => {
95                    match result {
96                        Ok(bytes_read) if bytes_read > 0 => {
97                            let _ = on_stderr(data, &stderr_buffer[..bytes_read]);
98                        }
99                        Ok(_) => {}, // EOF reached
100                        Err(_) => break, // Stream closed
101                    }
102                },
103
104                _ = &mut terminator => {
105                    lighty_core::trace_debug!("Termination signal received, killing process");
106                    process.kill().await?;
107                    break;
108                },
109
110                exit_result = process.wait() => {
111                    let exit_status = exit_result?;
112                    let exit_code = exit_status.code().unwrap_or(7900);
113
114                    lighty_core::trace_debug!("Java process exited with code: {}", exit_code);
115
116                    // -1073740791 = Windows forceful termination (not an error)
117                    if exit_code != 0 && exit_code != -1073740791 {
118                        return Err(JavaRuntimeError::NonZeroExit { code: exit_code });
119                    }
120
121                    break;
122                },
123            }
124        }
125
126        Ok(())
127    }
128}