Skip to main content

leviath_sys/
process.rs

1//! Process control: detached spawning and console-window suppression.
2
3use std::ffi::OsStr;
4use std::process::Command;
5
6/// Configure `cmd` so the spawned child detaches into its own process group,
7/// surviving the terminal that launched it.
8///
9/// On Unix this uses the safe, stable `Command::process_group(0)`; on non-Unix
10/// platforms it is a no-op. Call this before `cmd.spawn()`.
11pub fn configure_detached(cmd: &mut Command) {
12    crate::platform::configure_detached(cmd);
13}
14
15/// Configure `cmd` so the spawned child gets no console window.
16///
17/// On Windows a console application is given a console. A child of a process
18/// that has one shares it and draws nothing; a child of a process that has
19/// none, such as the daemon started from Explorer, from a service, or from a UI
20/// console, gets a brand new window on the interactive desktop. Agent tooling
21/// spawns `cmd.exe` many times per run, so without this the desktop fills with
22/// flashing consoles (issue #228). Elsewhere this is a no-op: no other platform
23/// hands a child a window.
24///
25/// Apply it to a child whose stdout/stderr are already piped or nulled, which
26/// is what every caller in this workspace does. Do **not** apply it to a child
27/// meant to share the user's terminal: the editor launched by `lev run` needs
28/// that console to draw in, and starting `vim` without one just breaks it.
29///
30/// One sharp edge worth knowing: `Command::creation_flags` *assigns* the flag
31/// word rather than OR-ing into it, so two callers setting different flags on
32/// one command would silently clobber each other. This function is deliberately
33/// the only writer of creation flags in the workspace. Add any future flag
34/// here, alongside `CREATE_NO_WINDOW`, rather than at a call site.
35pub fn hide_console_window(cmd: &mut Command) {
36    crate::platform::hide_console_window(cmd);
37}
38
39/// A [`Command`] for a child that must not take a console window.
40///
41/// **This is where the decision is made.** Hiding used to be a second call the
42/// caller made after building the command, at seven sites across five crates,
43/// and a new spawn site that forgot it looked exactly like one that did not
44/// need it. Making it part of construction means the only way to get a child
45/// process is to have already answered the question.
46///
47/// The counterpart is [`terminal_command`], for the single child that is meant
48/// to be seen. There is no third option on purpose: a `Command::new` outside
49/// this module is rejected by `.sgrules/no-raw-command-new.yml`.
50pub fn child_command(program: impl AsRef<OsStr>) -> Command {
51    let mut cmd = Command::new(program);
52    hide_console_window(&mut cmd);
53    cmd
54}
55
56/// The tokio twin of [`child_command`].
57///
58/// `tokio::process::Command` wraps a `std::process::Command`, so `as_std_mut`
59/// reaches the one the flag is written on and both flavours share a single
60/// implementation rather than a second copy of the `#[cfg]`.
61pub fn child_command_async(program: impl AsRef<OsStr>) -> tokio::process::Command {
62    let mut cmd = tokio::process::Command::new(program);
63    hide_console_window(cmd.as_std_mut());
64    cmd
65}
66
67/// A [`Command`] for a child that *must* inherit the user's terminal.
68///
69/// The editor `lev run` opens is the only one: it draws in the terminal it
70/// inherits, and starting `vim` without a console leaves it nowhere to draw and
71/// nothing to read from. Named rather than reached for with a bare
72/// `Command::new` so the exception is a decision in the source instead of an
73/// omission, and so the lint has something to point at.
74pub fn terminal_command(program: impl AsRef<OsStr>) -> Command {
75    Command::new(program)
76}
77
78/// SIGKILL every process in the group led by `pgid` (a no-op on platforms
79/// without process groups).
80///
81/// Killing a child shell is not enough to stop what it started: the shell's own
82/// children are reparented to init and keep running. A cancelled agent's
83/// `sleep 400` outliving the run that started it is exactly that. Spawning the
84/// shell into its own group (via [`configure_detached`]) and signalling the
85/// group tears down the whole tree.
86///
87/// Errors are the ordinary case (the group already exited) and are the caller's
88/// to ignore.
89pub fn kill_process_group(pgid: u32) -> std::io::Result<()> {
90    crate::platform::kill_process_group(pgid)
91}
92
93/// The calling user's numeric id.
94///
95/// Used to address a per-user service domain (`launchctl bootstrap gui/<uid>`).
96/// Returns `0` on platforms with no POSIX uid, where no such domain exists.
97pub fn current_uid() -> u32 {
98    crate::platform::current_uid()
99}
100
101/// The effective uid of the process on the other end of a connected
102/// Unix-domain socket.
103///
104/// The kernel's answer, not the peer's claim, so it cannot be spoofed by
105/// anything the peer sends. This is what makes the daemon's control socket
106/// safe: file permissions on a Unix socket are advisory on macOS and the BSDs
107/// (the mode is not consulted on `connect`), so the mode alone was never the
108/// guarantee it was documented to be.
109///
110/// `None` when the platform cannot report it - which the caller must treat as
111/// "refuse", since an unidentifiable peer is not an authorized one.
112///
113/// Unix-only: on Windows the control channel is not a Unix socket, so there is
114/// no fd to interrogate and no caller for this.
115#[cfg(unix)]
116pub fn peer_uid(sock: &impl std::os::fd::AsFd) -> Option<u32> {
117    crate::platform::peer_uid(sock)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    /// The check that makes the daemon's control socket safe: the peer's uid
125    /// comes from the kernel, not from anything the peer claims. A socket we
126    /// connected to ourselves must report our own uid.
127    #[cfg(unix)]
128    #[test]
129    fn peer_uid_reports_our_own_uid_for_a_self_connection() {
130        use std::os::unix::net::UnixListener;
131
132        let dir = tempfile::tempdir().unwrap();
133        let path = dir.path().join("s.sock");
134        let listener = UnixListener::bind(&path).expect("bind");
135        let client = std::os::unix::net::UnixStream::connect(&path).expect("connect");
136        let (server, _) = listener.accept().expect("accept");
137
138        // Both ends agree, and both agree with the process's own uid - a peer
139        // check that returned `None` here would refuse every legitimate
140        // connection, which is the failure mode worth catching.
141        assert_eq!(peer_uid(&server), Some(current_uid()));
142        assert_eq!(peer_uid(&client), Some(current_uid()));
143    }
144
145    #[test]
146    fn current_uid_is_reported() {
147        // Just has to answer without panicking; root (0) is a legitimate value
148        // on Unix and the only value on non-Unix.
149        let _uid: u32 = current_uid();
150    }
151
152    #[test]
153    fn kill_process_group_public_wrapper_reports_a_missing_group() {
154        // Exercises the public shim (distinct from the platform impl its own
155        // tests cover). A group that cannot exist errors on Unix and is a no-op
156        // where the platform has no process groups - both are outcomes the
157        // caller ignores, so either result is acceptable here.
158        let _ = kill_process_group(0x7FFF_FFFF);
159    }
160
161    #[test]
162    fn configure_detached_public_wrapper_registers_without_spawning() {
163        // Exercises the public `process::configure_detached` shim (distinct
164        // from the platform impl its own tests cover); registering the hook
165        // must not fork/exec or panic.
166        let mut cmd = Command::new("true");
167        configure_detached(&mut cmd);
168    }
169
170    #[test]
171    fn hide_console_window_public_wrapper_registers_without_spawning() {
172        // Same shape as the shim above: callers apply this unconditionally on
173        // every platform, so it must configure and return rather than fail
174        // where there is no window to suppress. Whether it took effect is only
175        // observable on Windows, and is asserted in that platform module.
176        let mut cmd = Command::new("true");
177        hide_console_window(&mut cmd);
178    }
179
180    /// The constructors have to *be* the decision, not just offer it: a caller
181    /// that builds through them gets a hidden console without a second call.
182    /// The flag itself is only observable on Windows and is asserted in that
183    /// platform module; here the point is that construction succeeds and the
184    /// program survives, so a caller cannot be silently handed a broken command.
185    #[test]
186    fn child_command_builds_a_usable_command() {
187        let cmd = child_command("true");
188        assert_eq!(cmd.get_program(), "true");
189    }
190
191    #[test]
192    fn child_command_async_builds_a_usable_command() {
193        let cmd = child_command_async("true");
194        assert_eq!(cmd.as_std().get_program(), "true");
195    }
196
197    /// The editor is the one child meant to be seen, so this deliberately does
198    /// *not* hide the console - see `editor.rs`, its only caller.
199    #[test]
200    fn terminal_command_builds_a_usable_command() {
201        let cmd = terminal_command("true");
202        assert_eq!(cmd.get_program(), "true");
203    }
204}