leviath_sys/process.rs
1//! Process control: detached spawning and console-window suppression.
2
3use std::process::Command;
4
5/// Configure `cmd` so the spawned child detaches into its own process group,
6/// surviving the terminal that launched it.
7///
8/// On Unix this uses the safe, stable `Command::process_group(0)`; on non-Unix
9/// platforms it is a no-op. Call this before `cmd.spawn()`.
10pub fn configure_detached(cmd: &mut Command) {
11 crate::platform::configure_detached(cmd);
12}
13
14/// Configure `cmd` so the spawned child gets no console window.
15///
16/// On Windows a console application is given a console. A child of a process
17/// that has one shares it and draws nothing; a child of a process that has
18/// none, such as the daemon started from Explorer, from a service, or from a UI
19/// console, gets a brand new window on the interactive desktop. Agent tooling
20/// spawns `cmd.exe` many times per run, so without this the desktop fills with
21/// flashing consoles (issue #228). Elsewhere this is a no-op: no other platform
22/// hands a child a window.
23///
24/// Apply it to a child whose stdout/stderr are already piped or nulled, which
25/// is what every caller in this workspace does. Do **not** apply it to a child
26/// meant to share the user's terminal: the editor launched by `lev run` needs
27/// that console to draw in, and starting `vim` without one just breaks it.
28///
29/// One sharp edge worth knowing: `Command::creation_flags` *assigns* the flag
30/// word rather than OR-ing into it, so two callers setting different flags on
31/// one command would silently clobber each other. This function is deliberately
32/// the only writer of creation flags in the workspace. Add any future flag
33/// here, alongside `CREATE_NO_WINDOW`, rather than at a call site.
34pub fn hide_console_window(cmd: &mut Command) {
35 crate::platform::hide_console_window(cmd);
36}
37
38/// SIGKILL every process in the group led by `pgid` (a no-op on platforms
39/// without process groups).
40///
41/// Killing a child shell is not enough to stop what it started: the shell's own
42/// children are reparented to init and keep running. A cancelled agent's
43/// `sleep 400` outliving the run that started it is exactly that. Spawning the
44/// shell into its own group (via [`configure_detached`]) and signalling the
45/// group tears down the whole tree.
46///
47/// Errors are the ordinary case (the group already exited) and are the caller's
48/// to ignore.
49pub fn kill_process_group(pgid: u32) -> std::io::Result<()> {
50 crate::platform::kill_process_group(pgid)
51}
52
53/// The calling user's numeric id.
54///
55/// Used to address a per-user service domain (`launchctl bootstrap gui/<uid>`).
56/// Returns `0` on platforms with no POSIX uid, where no such domain exists.
57pub fn current_uid() -> u32 {
58 crate::platform::current_uid()
59}
60
61/// The effective uid of the process on the other end of a connected
62/// Unix-domain socket.
63///
64/// The kernel's answer, not the peer's claim, so it cannot be spoofed by
65/// anything the peer sends. This is what makes the daemon's control socket
66/// safe: file permissions on a Unix socket are advisory on macOS and the BSDs
67/// (the mode is not consulted on `connect`), so the mode alone was never the
68/// guarantee it was documented to be.
69///
70/// `None` when the platform cannot report it - which the caller must treat as
71/// "refuse", since an unidentifiable peer is not an authorized one.
72///
73/// Unix-only: on Windows the control channel is not a Unix socket, so there is
74/// no fd to interrogate and no caller for this.
75#[cfg(unix)]
76pub fn peer_uid(sock: &impl std::os::fd::AsFd) -> Option<u32> {
77 crate::platform::peer_uid(sock)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 /// The check that makes the daemon's control socket safe: the peer's uid
85 /// comes from the kernel, not from anything the peer claims. A socket we
86 /// connected to ourselves must report our own uid.
87 #[cfg(unix)]
88 #[test]
89 fn peer_uid_reports_our_own_uid_for_a_self_connection() {
90 use std::os::unix::net::UnixListener;
91
92 let dir = tempfile::tempdir().unwrap();
93 let path = dir.path().join("s.sock");
94 let listener = UnixListener::bind(&path).expect("bind");
95 let client = std::os::unix::net::UnixStream::connect(&path).expect("connect");
96 let (server, _) = listener.accept().expect("accept");
97
98 // Both ends agree, and both agree with the process's own uid - a peer
99 // check that returned `None` here would refuse every legitimate
100 // connection, which is the failure mode worth catching.
101 assert_eq!(peer_uid(&server), Some(current_uid()));
102 assert_eq!(peer_uid(&client), Some(current_uid()));
103 }
104
105 #[test]
106 fn current_uid_is_reported() {
107 // Just has to answer without panicking; root (0) is a legitimate value
108 // on Unix and the only value on non-Unix.
109 let _uid: u32 = current_uid();
110 }
111
112 #[test]
113 fn kill_process_group_public_wrapper_reports_a_missing_group() {
114 // Exercises the public shim (distinct from the platform impl its own
115 // tests cover). A group that cannot exist errors on Unix and is a no-op
116 // where the platform has no process groups - both are outcomes the
117 // caller ignores, so either result is acceptable here.
118 let _ = kill_process_group(0x7FFF_FFFF);
119 }
120
121 #[test]
122 fn configure_detached_public_wrapper_registers_without_spawning() {
123 // Exercises the public `process::configure_detached` shim (distinct
124 // from the platform impl its own tests cover); registering the hook
125 // must not fork/exec or panic.
126 let mut cmd = Command::new("true");
127 configure_detached(&mut cmd);
128 }
129
130 #[test]
131 fn hide_console_window_public_wrapper_registers_without_spawning() {
132 // Same shape as the shim above: callers apply this unconditionally on
133 // every platform, so it must configure and return rather than fail
134 // where there is no window to suppress. Whether it took effect is only
135 // observable on Windows, and is asserted in that platform module.
136 let mut cmd = Command::new("true");
137 hide_console_window(&mut cmd);
138 }
139}