Skip to main content

hatchery_node/
windows_process.rs

1//! Windows process-creation helpers for non-PTY children.
2//!
3//! PTY / ConPTY spawns must keep a console; everything else (ACP, app-server,
4//! m4a-agent, spawn profiles, probes, schtasks, powershell helpers) must not
5//! pop a visible window. Closing a console must not kill a supervised node.
6
7/// `CREATE_NO_WINDOW` (0x08000000). Safe to OR with other creation flags.
8pub const CREATE_NO_WINDOW: u32 = 0x0800_0000;
9/// `DETACHED_PROCESS` (0x00000008) — no console inheritance.
10pub const DETACHED_PROCESS: u32 = 0x0000_0008;
11/// `CREATE_NEW_PROCESS_GROUP` (0x00000200).
12pub const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
13
14/// Flags for a headless node / child: no console at all (DETACHED_PROCESS), own process group.
15/// CREATE_NO_WINDOW is left out: with DETACHED_PROCESS it is ignored, and on its own it still
16/// creates a (hidden) console that can flash.
17pub const HEADLESS_SPAWN_FLAGS: u32 = DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP;
18
19/// Apply [`CREATE_NO_WINDOW`] on Windows. No-op elsewhere. Does not touch
20/// program, args, env, cwd, or stdio. Call once: a second `creation_flags`
21/// replaces the previous value — use [`set_creation_flags`] to OR.
22pub fn hide_console(command: &mut std::process::Command) {
23    set_creation_flags(command, CREATE_NO_WINDOW);
24}
25
26/// Same for tokio's command (via `as_std_mut`).
27pub fn hide_console_tokio(command: &mut tokio::process::Command) {
28    #[cfg(windows)]
29    {
30        hide_console(command.as_std_mut());
31    }
32    #[cfg(not(windows))]
33    {
34        let _ = command;
35    }
36}
37
38/// OR `flags` into the command's Windows creation flags.
39pub fn set_creation_flags(command: &mut std::process::Command, flags: u32) {
40    #[cfg(windows)]
41    {
42        use std::os::windows::process::CommandExt;
43        // creation_flags replaces; callers that need OR must pass the full mask.
44        command.creation_flags(flags);
45    }
46    #[cfg(not(windows))]
47    {
48        let _ = (command, flags);
49    }
50}
51
52/// Spawn flags for the supervised child node (no console, detached).
53pub fn hide_console_detached(command: &mut std::process::Command) {
54    set_creation_flags(command, HEADLESS_SPAWN_FLAGS);
55}
56
57/// Detach this process from its console (if any). No-op on non-Windows.
58pub fn free_console() {
59    #[cfg(windows)]
60    {
61        // SAFETY: FreeConsole is process-wide and idempotent when unattached.
62        unsafe {
63            let _ = windows_sys::Win32::System::Console::FreeConsole();
64        }
65    }
66}
67
68/// Ignore CTRL_CLOSE / CTRL_LOGOFF / CTRL_SHUTDOWN so closing a terminal that
69/// somehow still owns us does not kill a supervised node. CTRL_C still works
70/// for interactive CLI. No-op on non-Windows.
71pub fn ignore_console_close_events() {
72    #[cfg(windows)]
73    {
74        unsafe extern "system" fn handler(ctrl: u32) -> i32 {
75            use windows_sys::Win32::System::Console::{
76                CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT, CTRL_SHUTDOWN_EVENT,
77            };
78            match ctrl {
79                CTRL_CLOSE_EVENT | CTRL_LOGOFF_EVENT | CTRL_SHUTDOWN_EVENT => 1, // TRUE = handled
80                _ => 0,
81            }
82        }
83        unsafe {
84            let _ = windows_sys::Win32::System::Console::SetConsoleCtrlHandler(Some(handler), 1);
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn hide_console_keeps_program_and_args() {
95        let mut c = std::process::Command::new("prog");
96        c.arg("a").arg("b");
97        hide_console(&mut c);
98        assert_eq!(c.get_program(), std::ffi::OsStr::new("prog"));
99        assert_eq!(c.get_args().collect::<Vec<_>>(), ["a", "b"]);
100    }
101
102    #[test]
103    fn create_no_window_constant() {
104        assert_eq!(CREATE_NO_WINDOW, 0x0800_0000);
105        assert_eq!(DETACHED_PROCESS, 0x0000_0008);
106        assert_eq!(HEADLESS_SPAWN_FLAGS, DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
107    }
108}