Skip to main content

running_process/
spawn.rs

1//! Two-mode process spawning. Free functions only — no module-internal traits.
2//!
3//! Modes (only two; the dangerous combination `detached + caller-pipes` has no
4//! API surface):
5//!
6//!   * [`spawn_daemon`] — detached lifetime, NUL stdio, sanitized handle list,
7//!     no console window, ignores parent's Ctrl-C. The returned [`DaemonChild`]
8//!     does NOT die when dropped.
9//!   * [`spawn`] — contained lifetime, caller-controlled stdio via
10//!     [`SpawnStdio`], sanitized handle list, no console window by default
11//!     (opt in via [`SpawnStdio::show_console`]), bounded drain. The returned
12//!     [`SpawnedChild`] kills the child on Drop.
13//!
14//! ## Sanitized handle inheritance
15//!
16//! Both modes inherit ONLY the three stdio handles we resolve here. On
17//! Windows we use `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` to whitelist exactly
18//! the resolved handles. On Unix the spawned child runs a `pre_exec` closure
19//! that walks `/proc/self/fd` (or `/dev/fd`) and closes every fd > 2.
20//!
21//! Motivation: when a process tree has a pipe-redirected ancestor (Python
22//! `subprocess.Popen(stdout=PIPE)`, IDE language-server hosts, CI runners,
23//! etc.), every intermediate `CreateProcessW(bInheritHandles=TRUE)` on
24//! Windows — and every `fork`+`exec` of a non-`O_CLOEXEC` fd on Unix —
25//! duplicates that orphaned pipe write-end into the new child. The original
26//! reader at the top never sees EOF.
27//!
28//! Issue: <https://github.com/zackees/running-process/issues/110>.
29
30#[cfg(unix)]
31use std::os::fd::BorrowedFd;
32#[cfg(windows)]
33use std::os::windows::io::BorrowedHandle;
34use std::process::Command;
35use std::time::Duration;
36
37// ── Public API ──────────────────────────────────────────────────────────────
38
39/// Caller-supplied stdio bindings for [`spawn`].
40///
41/// Each of `stdin`, `stdout`, `stderr` is independently a [`StdioSource`].
42/// `drain_timeout` bounds the post-mortem wait the watcher thread applies
43/// before force-closing any wrapper-held pipe ends so the parent observes
44/// EOF after the child exits. `None` means the wrapper never auto-closes;
45/// the parent is responsible for closing the pipes when it's done reading.
46///
47/// `show_console` (Windows-only effect) controls whether the child gets a
48/// console window. Default is `false` — `CREATE_NO_WINDOW` is set, so the
49/// child has no console regardless of how the parent was launched. Set this
50/// to `true` only when you actually want the child to inherit / allocate a
51/// console (interactive subprocess that should be visible to the user).
52pub struct SpawnStdio<'a> {
53    /// Source connected to the child's standard input.
54    pub stdin: StdioSource<'a>,
55    /// Source connected to the child's standard output.
56    pub stdout: StdioSource<'a>,
57    /// Source connected to the child's standard error.
58    pub stderr: StdioSource<'a>,
59    /// Maximum time the watcher waits before closing wrapper-held pipe ends.
60    pub drain_timeout: Option<Duration>,
61    /// Whether Windows children may inherit or allocate a visible console.
62    pub show_console: bool,
63}
64
65impl Default for SpawnStdio<'_> {
66    fn default() -> Self {
67        Self {
68            stdin: StdioSource::Null,
69            stdout: StdioSource::Parent,
70            stderr: StdioSource::Parent,
71            drain_timeout: Some(Duration::from_secs(2)),
72            show_console: false,
73        }
74    }
75}
76
77/// Per-slot source describing what the child should inherit for one of
78/// stdin / stdout / stderr.
79pub enum StdioSource<'a> {
80    /// Connect this slot to the platform null device (`NUL` / `/dev/null`).
81    Null,
82    /// Inherit the parent's corresponding standard handle. The kernel
83    /// receives a fresh inheritable duplicate; the parent's original slot
84    /// is untouched.
85    Parent,
86    /// Bind this slot to a caller-owned OS handle. The wrapper duplicates
87    /// the handle into an inheritable copy for the child; the caller
88    /// retains its own handle and is responsible for closing it.
89    #[cfg(windows)]
90    Handle(BorrowedHandle<'a>),
91    /// Bind this slot to a caller-owned file descriptor. Equivalent to
92    /// `StdioSource::Handle` on Unix.
93    #[cfg(unix)]
94    Fd(BorrowedFd<'a>),
95    /// Create a fresh anonymous pipe. The child gets one end; the parent
96    /// gets the other via [`SpawnedChild`]'s `stdin` / `stdout` / `stderr`
97    /// fields.
98    Pipe,
99    #[doc(hidden)]
100    _Phantom(std::marker::PhantomData<&'a ()>),
101}
102
103// _Phantom is uninhabitable from outside: PhantomData<&'a ()> is a private
104// constructor in practice (the variant is doc(hidden) and not constructed
105// anywhere in this crate). It's only here so the `'a` lifetime is always
106// used regardless of which cfg branch is active.
107
108/// Handle to a detached daemon spawned via [`spawn_daemon`].
109///
110/// The daemon child always has stdin/stdout/stderr connected to the
111/// platform null device (`NUL` on Windows, `/dev/null` on Unix) — a
112/// detached process with inherited stdio is the classic crash-on-first-
113/// `println!` failure mode after the parent closes its end, so the
114/// daemon-spawn path forecloses that by construction. Dropping
115/// `DaemonChild` does NOT terminate the daemon; it only closes the OS
116/// handle the wrapper held. Call [`DaemonChild::kill`] to terminate.
117pub struct DaemonChild {
118    pid: u32,
119    #[cfg(windows)]
120    handle: imp::OwnedHandle,
121    #[cfg(unix)]
122    child: std::process::Child,
123}
124
125impl DaemonChild {
126    /// Process ID.
127    pub fn id(&self) -> u32 {
128        self.pid
129    }
130
131    /// Forcibly terminate the child. Best-effort.
132    pub fn kill(&mut self) -> std::io::Result<()> {
133        #[cfg(windows)]
134        {
135            imp::terminate(&self.handle)
136        }
137        #[cfg(unix)]
138        {
139            self.child.kill()
140        }
141    }
142
143    /// Block until the child exits and return its exit code.
144    pub fn wait(&mut self) -> std::io::Result<i32> {
145        #[cfg(windows)]
146        {
147            imp::wait(&self.handle)
148        }
149        #[cfg(unix)]
150        {
151            let status = self.child.wait()?;
152            Ok(unix_exit_code(status))
153        }
154    }
155
156    /// Non-blocking variant of [`Self::wait`].
157    pub fn try_wait(&mut self) -> std::io::Result<Option<i32>> {
158        #[cfg(windows)]
159        {
160            imp::try_wait(&self.handle)
161        }
162        #[cfg(unix)]
163        {
164            Ok(self.child.try_wait()?.map(unix_exit_code))
165        }
166    }
167}
168
169/// Handle to a contained child spawned via [`spawn`].
170///
171/// On Drop, `SpawnedChild` synchronously kills the child:
172///   * Windows: closes the Job Object handle; `KILL_ON_JOB_CLOSE` causes the
173///     kernel to terminate every process in the job (the child and its
174///     descendants).
175///   * Unix: `killpg(pgid, SIGKILL)` and `waitpid` to reap.
176///
177/// The optional `stdin` / `stdout` / `stderr` fields are present when the
178/// corresponding [`StdioSource`] was [`StdioSource::Pipe`]; otherwise they
179/// are `None`.
180pub struct SpawnedChild {
181    /// Parent-side pipe for writing to child stdin when requested.
182    pub stdin: Option<std::process::ChildStdin>,
183    /// Parent-side pipe for reading child stdout when requested.
184    pub stdout: Option<std::process::ChildStdout>,
185    /// Parent-side pipe for reading child stderr when requested.
186    pub stderr: Option<std::process::ChildStderr>,
187    pid: u32,
188    #[cfg(windows)]
189    inner: imp::SpawnedInner,
190    #[cfg(unix)]
191    inner: unix_impl::SpawnedInner,
192}
193
194impl SpawnedChild {
195    /// Process ID of the spawned child.
196    pub fn id(&self) -> u32 {
197        self.pid
198    }
199
200    /// Forcibly terminate the child. Best-effort.
201    pub fn kill(&mut self) -> std::io::Result<()> {
202        #[cfg(windows)]
203        {
204            self.inner.kill()
205        }
206        #[cfg(unix)]
207        {
208            self.inner.kill()
209        }
210    }
211
212    /// Block until the child exits and return its exit code.
213    pub fn wait(&mut self) -> std::io::Result<i32> {
214        #[cfg(windows)]
215        {
216            self.inner.wait()
217        }
218        #[cfg(unix)]
219        {
220            self.inner.wait()
221        }
222    }
223
224    /// Non-blocking variant of [`Self::wait`].
225    pub fn try_wait(&mut self) -> std::io::Result<Option<i32>> {
226        #[cfg(windows)]
227        {
228            self.inner.try_wait()
229        }
230        #[cfg(unix)]
231        {
232            self.inner.try_wait()
233        }
234    }
235}
236
237impl Drop for SpawnedChild {
238    fn drop(&mut self) {
239        #[cfg(windows)]
240        {
241            self.inner.shutdown();
242        }
243        #[cfg(unix)]
244        {
245            self.inner.shutdown();
246        }
247    }
248}
249
250/// Spawn `command` as a detached daemon. NUL stdio, sanitized handles,
251/// no console window, ignores parent's Ctrl-C / SIGINT (Windows:
252/// `CREATE_NEW_PROCESS_GROUP` + `DETACHED_PROCESS`; Unix: `setsid` puts the
253/// daemon in a new session so it's not in the parent's foreground group).
254///
255/// The NUL-stdio guarantee is enforced internally by the platform impls
256/// and is not configurable — a detached daemon needs sunk stdio to
257/// avoid crashing on later `println!`/`eprintln!` after the parent
258/// closes its handles.
259pub fn spawn_daemon(command: &mut Command) -> std::io::Result<DaemonChild> {
260    spawn_daemon_with_clear_env(command, false)
261}
262
263/// Like [`spawn_daemon`] but with explicit control over whether the
264/// daemon's inherited env is passed through to the child.
265///
266/// `clear_env = false` (default for [`spawn_daemon`]): child inherits the
267/// current process's env, layered with anything set via
268/// `command.env(...)`.
269///
270/// `clear_env = true`: child sees ONLY the explicit `command.env(...)`
271/// entries. Mirrors `command.env_clear()` semantics for callers using
272/// the manual `CreateProcessW` path (Rust stdlib's `env_clear` flag
273/// isn't observable through `Command::get_envs`, so our sanitized
274/// spawn machinery can't otherwise honour it).
275pub fn spawn_daemon_with_clear_env(
276    command: &mut Command,
277    clear_env: bool,
278) -> std::io::Result<DaemonChild> {
279    #[cfg(windows)]
280    {
281        imp::spawn_daemon(command, clear_env)
282    }
283    #[cfg(unix)]
284    {
285        unix_impl::spawn_daemon(command, clear_env)
286    }
287}
288
289/// Spawn `command` as a contained child with caller-controlled stdio.
290/// Sanitized handles, CREATE_NO_WINDOW. Child dies when the returned
291/// [`SpawnedChild`] is dropped.
292pub fn spawn(command: &mut Command, stdio: SpawnStdio<'_>) -> std::io::Result<SpawnedChild> {
293    #[cfg(windows)]
294    {
295        imp::spawn(command, stdio)
296    }
297    #[cfg(unix)]
298    {
299        unix_impl::spawn(command, stdio)
300    }
301}
302
303#[cfg(unix)]
304fn unix_exit_code(status: std::process::ExitStatus) -> i32 {
305    use std::os::unix::process::ExitStatusExt;
306    status
307        .code()
308        .unwrap_or_else(|| -status.signal().unwrap_or(1))
309}
310
311// ── Windows implementation ──────────────────────────────────────────────────
312
313#[cfg(windows)]
314#[path = "spawn_imp_windows.rs"]
315mod imp;
316
317#[cfg(unix)]
318#[path = "spawn_imp_unix.rs"]
319mod unix_impl;
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn spawn_stdio_default_has_sane_values() {
326        let s = SpawnStdio::default();
327        assert!(matches!(s.stdin, StdioSource::Null));
328        assert!(matches!(s.stdout, StdioSource::Parent));
329        assert!(matches!(s.stderr, StdioSource::Parent));
330        assert_eq!(s.drain_timeout, Some(Duration::from_secs(2)));
331        // No console window by default — opt-in only.
332        assert!(!s.show_console);
333    }
334}