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