Skip to main content

launcher/
spawn_process.rs

1//! # Process Spawner Module
2//!
3//! Provides platform-independent facilities for spawning, detaching, and
4//! managing child processes (such as the REKTAL Kernel and GUI client).
5//!
6//! ## Process Independence & Detachment
7//! All processes spawned via this module are detached from the parent Launcher
8//! process lifecycle:
9//! - On **Linux / macOS (Unix)**: Child processes and terminal emulators are placed in their own
10//!   process group (`process_group(0)` / `setpgid`) and have standard streams decoupled, ensuring
11//!   they outlive the Launcher when it closes.
12//! - On **Windows**: Processes are detached using `CREATE_NEW_PROCESS_GROUP` and (optionally)
13//!   `CREATE_NEW_CONSOLE`.
14
15use std::process::{Command, Stdio};
16use common::logging::LogLevel::Info;
17use common::r_log;
18
19/// Spawns a REKTAL binary (e.g. `"kernel"` or `"gui"`) as an independent, detached process.
20///
21/// Resolves the sibling binary located alongside the current launcher executable and launches
22/// it with the provided CLI arguments.
23///
24/// # Arguments
25///
26/// * `bin_name` - The base name of the target binary to spawn (e.g. `"kernel"`, `"gui"`).
27///   On Windows, `.exe` is automatically checked and appended if applicable.
28/// * `args` - A slice of command-line argument strings to pass to the spawned process.
29/// * `show_console` - When `true`, opens the process inside a visible, native OS terminal
30///   window with a pause prompt on termination. When `false`, launches the process silently
31///   in the background with suppressed standard streams.
32///
33/// # Returns
34///
35/// Returns a [`std::io::Result<std::process::Child>`] representing the spawned child process handle.
36///
37/// # Errors
38///
39/// Returns an [`std::io::Error`] if:
40/// * The current executable path cannot be determined.
41/// * The target binary cannot be found or executed.
42/// * The platform terminal emulator cannot be spawned.
43///
44/// # Platform Behavior
45///
46/// * **Windows:** Utilizes `CREATE_NEW_CONSOLE` and `CREATE_NEW_PROCESS_GROUP` creation flags.
47/// * **Linux:** Prioritizes `$TERMINAL`, `xdg-terminal-exec`, `x-terminal-emulator`, and fallback
48///   terminal emulators (`gnome-terminal`, `konsole`, `xterm`). Sets `process_group(0)` to decouple.
49/// * **macOS:** Dispatches script execution to `Terminal.app` via `osascript` or runs headless with `process_group(0)`.
50pub fn spawn_process(bin_name: &str, args: &[&str], show_console: bool) -> std::io::Result<std::process::Child> {
51    let exe_dir = std::env::current_exe()?
52        .parent()
53        .map(|p| p.to_path_buf())
54        .unwrap_or_else(|| std::path::PathBuf::from("."));
55
56    #[cfg(target_os = "windows")]
57    let target_exe = if exe_dir.join(format!("{}.exe", bin_name)).exists() {
58        exe_dir.join(format!("{}.exe", bin_name))
59    } else {
60        exe_dir.join(bin_name)
61    };
62
63    #[cfg(not(target_os = "windows"))]
64    let target_exe = exe_dir.join(bin_name);
65
66    #[cfg(target_os = "windows")]
67    {
68        spawn_windows(bin_name, &target_exe, &exe_dir, args, show_console)
69    }
70    #[cfg(target_os = "linux")]
71    {
72        spawn_linux(bin_name, &target_exe, &exe_dir, args, show_console)
73    }
74    #[cfg(target_os = "macos")]
75    {
76        spawn_macos(bin_name, &target_exe, &exe_dir, args, show_console)
77    }
78    #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
79    {
80        let mut cmd = Command::new(&target_exe);
81        cmd.args(args).current_dir(&exe_dir);
82        #[cfg(unix)]
83        {
84            use std::os::unix::process::CommandExt;
85            cmd.process_group(0);
86        }
87        cmd.spawn()
88    }
89}
90
91/// Spawns the REKTAL Kernel executable with optional arguments and console window.
92///
93/// Convenience wrapper around [`spawn_process`] with `bin_name = "kernel"`.
94///
95/// # Arguments
96///
97/// * `args` - Command-line arguments passed to the Kernel binary.
98/// * `show_console` - Whether to display a native console window.
99///
100/// # Examples
101///
102/// ```no_run
103/// use launcher::spawn_process::spawn_kernel;
104///
105/// // Spawns the kernel in a visible terminal window
106/// let _child = spawn_kernel(&[], true);
107/// ```
108pub fn spawn_kernel(args: &[&str], show_console: bool) -> std::io::Result<std::process::Child> {
109    spawn_process("rektal_kernel", args, show_console)
110}
111
112/// Spawns the REKTAL GUI client executable with optional arguments and console window.
113///
114/// Convenience wrapper around [`spawn_process`] with `bin_name = "gui"`.
115///
116/// # Arguments
117///
118/// * `args` - Command-line arguments passed to the GUI binary.
119/// * `show_console` - Whether to display a native console window alongside the GUI.
120///
121/// # Examples
122///
123/// ```no_run
124/// use launcher::spawn_process::spawn_gui;
125///
126/// // Spawns the GUI quietly in the background
127/// let _child = spawn_gui(&[], false);
128/// ```
129pub fn spawn_gui(args: &[&str], show_console: bool) -> std::io::Result<std::process::Child> {
130    spawn_process("rektal_client", args, show_console)
131}
132
133/// Spawns a process on Windows with process group detachment and optional console window.
134#[cfg(target_os = "windows")]
135fn spawn_windows(
136    _bin_name: &str,
137    target_exe: &std::path::Path,
138    cwd: &std::path::Path,
139    args: &[&str],
140    show_console: bool,
141) -> std::io::Result<std::process::Child> {
142    use std::os::windows::process::CommandExt;
143    const CREATE_NEW_CONSOLE: u32 = 0x00000010;
144    const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
145
146    let mut cmd = Command::new(target_exe);
147    cmd.args(args).current_dir(cwd);
148
149    if show_console {
150        cmd.creation_flags(CREATE_NEW_CONSOLE | CREATE_NEW_PROCESS_GROUP);
151    } else {
152        cmd.creation_flags(CREATE_NEW_PROCESS_GROUP);
153    }
154
155    cmd.spawn()
156}
157
158/// Spawns a process on Linux, querying available terminal emulators when console output is requested.
159///
160/// Decouples standard I/O and creates a separate process group (`process_group(0)`) so the process
161/// outlives the Launcher.
162#[cfg(target_os = "linux")]
163fn spawn_linux(
164    bin_name: &str,
165    target_exe: &std::path::Path,
166    cwd: &std::path::Path,
167    args: &[&str],
168    show_console: bool,
169) -> std::io::Result<std::process::Child> {
170    use std::os::unix::process::CommandExt;
171
172    if !show_console {
173        let mut cmd = Command::new(target_exe);
174        cmd.args(args)
175            .current_dir(cwd)
176            .stdin(Stdio::null())
177            .stdout(Stdio::null())
178            .stderr(Stdio::null())
179            .process_group(0);
180        return cmd.spawn();
181    }
182
183    let title = format!("REKTAL {}", bin_name.to_uppercase());
184    let formatted_args = args
185        .iter()
186        .map(|a| format!("'{}'", a.replace('\'', "'\\''")))
187        .collect::<Vec<_>>()
188        .join(" ");
189
190    let full_cmd = if formatted_args.is_empty() {
191        format!(
192            "cd '{}' && '{}'; echo ''; read -p '{} terminated. Press Enter to close...' -r",
193            cwd.display(),
194            target_exe.display(),
195            bin_name
196        )
197    } else {
198        format!(
199            "cd '{}' && '{}' {}; echo ''; read -p '{} terminated. Press Enter to close...' -r",
200            cwd.display(),
201            target_exe.display(),
202            formatted_args,
203            bin_name
204        )
205    };
206
207    let mut candidates = Vec::new();
208    if let Ok(user_term) = std::env::var("TERMINAL") {
209        candidates.push(user_term);
210    }
211    candidates.extend([
212        "xdg-terminal-exec".to_string(),
213        "x-terminal-emulator".to_string(),
214        "gnome-terminal".to_string(),
215        "konsole".to_string(),
216        "xterm".to_string(),
217    ]);
218
219    for term in &candidates {
220        let mut cmd = Command::new(term);
221        if term == "xdg-terminal-exec" {
222            cmd.args(["bash", "-c", &full_cmd]);
223        } else if term == "gnome-terminal" {
224            cmd.args(["--title", &title, "--", "bash", "-c", &full_cmd]);
225        } else if term == "konsole" {
226            cmd.args(["-p", &format!("tabtitle={}", title), "-e", "bash", "-c", &full_cmd]);
227        } else {
228            cmd.args(["-e", "bash", "-c", &full_cmd]);
229        }
230
231        cmd.stdin(Stdio::null())
232            .stdout(Stdio::null())
233            .stderr(Stdio::null())
234            .process_group(0);
235
236        if let Ok(child) = cmd.spawn() {
237            return Ok(child);
238        }
239    }
240
241    r_log!(Info, "Could not find terminal-application! running command discretely!");
242    let mut cmd = Command::new(target_exe);
243    cmd.args(args)
244        .current_dir(cwd)
245        .stdin(Stdio::null())
246        .stdout(Stdio::null())
247        .stderr(Stdio::null())
248        .process_group(0);
249    cmd.spawn()
250}
251
252/// Spawns a process on macOS, dispatching to `Terminal.app` via AppleScript when console output is requested.
253#[cfg(target_os = "macos")]
254fn spawn_macos(
255    bin_name: &str,
256    target_exe: &std::path::Path,
257    cwd: &std::path::Path,
258    args: &[&str],
259    show_console: bool,
260) -> std::io::Result<std::process::Child> {
261    use std::os::unix::process::CommandExt;
262
263    if !show_console {
264        let mut cmd = Command::new(target_exe);
265        cmd.args(args)
266            .current_dir(cwd)
267            .stdin(Stdio::null())
268            .stdout(Stdio::null())
269            .stderr(Stdio::null())
270            .process_group(0);
271        return cmd.spawn();
272    }
273
274    let formatted_args = args
275        .iter()
276        .map(|a| format!("'{}'", a.replace('\'', "'\\''")))
277        .collect::<Vec<_>>()
278        .join(" ");
279
280    let full_cmd = if formatted_args.is_empty() {
281        format!(
282            "cd '{}' && '{}'; echo ''; read -p '{} terminated. Press Enter to close...' -r",
283            cwd.display(),
284            target_exe.display(),
285            bin_name
286        )
287    } else {
288        format!(
289            "cd '{}' && '{}' {}; echo ''; read -p '{} terminated. Press Enter to close...' -r",
290            cwd.display(),
291            target_exe.display(),
292            formatted_args,
293            bin_name
294        )
295    };
296
297    let script = format!(
298        "tell application \"Terminal\" to do script \"{}\"",
299        full_cmd.replace('\\', "\\\\").replace('\"', "\\\"")
300    );
301
302    let mut cmd = Command::new("osascript");
303    cmd.args(["-e", &script])
304        .stdin(Stdio::null())
305        .stdout(Stdio::null())
306        .stderr(Stdio::null())
307        .process_group(0);
308    cmd.spawn()
309}