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, sanitized file-or-NUL stdio,
7//! sanitized handle list, no console window, ignores parent's Ctrl-C. The
8//! returned [`DaemonChild`] 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/// Selects the base environment used for a newly spawned process.
38///
39/// Explicit mutations added through [`Command::env`], [`Command::envs`], or
40/// [`Command::env_remove`] are applied after the selected base and therefore
41/// always win.
42#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
43pub enum EnvironmentPolicy {
44 /// Choose from the process lifetime: contained subprocesses inherit,
45 /// while detached daemons start from the logged-in user's baseline.
46 #[default]
47 Auto,
48 /// Inherit the spawning process's environment.
49 Inherit,
50 /// Start from the logged-in user's machine + user environment, discarding
51 /// the spawning process's ambient environment except for the documented
52 /// Unix locale, time-zone, and temporary-directory allowlist.
53 ///
54 /// Windows implements this with `CreateEnvironmentBlock`. Unix
55 /// reconstructs a clean login environment from the user's identity
56 /// (`getpwuid_r` → `USER`/`LOGNAME`/`HOME`/`SHELL`, platform default
57 /// `PATH`, carried-over locale/`TZ`/`TMPDIR`), falling back to inheritance
58 /// only when the passwd entry cannot be resolved.
59 ///
60 /// Consumers that need values such as `CARGO_HOME`, `RUSTUP_HOME`,
61 /// `SOLDR_*`, credentials, or runner-specific paths must pass them
62 /// explicitly on the [`Command`].
63 UserBaseline,
64 /// Start from an empty environment.
65 Clear,
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub(crate) enum SpawnLifetime {
70 Contained,
71 Daemon,
72}
73
74impl EnvironmentPolicy {
75 pub(crate) fn resolve(self, lifetime: SpawnLifetime) -> Self {
76 match (self, lifetime) {
77 (Self::Auto, SpawnLifetime::Contained) => Self::Inherit,
78 (Self::Auto, SpawnLifetime::Daemon) => Self::UserBaseline,
79 (explicit, _) => explicit,
80 }
81 }
82}
83
84// ── Public API ──────────────────────────────────────────────────────────────
85
86/// Caller-supplied stdio bindings for [`spawn`].
87///
88/// Each of `stdin`, `stdout`, `stderr` is independently a [`StdioSource`].
89/// `drain_timeout` bounds the post-mortem wait the watcher thread applies
90/// before force-closing any wrapper-held pipe ends so the parent observes
91/// EOF after the child exits. `None` means the wrapper never auto-closes;
92/// the parent is responsible for closing the pipes when it's done reading.
93///
94/// `show_console` (Windows-only effect) controls whether the child gets a
95/// console window. Default is `false` — `CREATE_NO_WINDOW` is set, so the
96/// child has no console regardless of how the parent was launched. Set this
97/// to `true` only when you actually want the child to inherit / allocate a
98/// console (interactive subprocess that should be visible to the user).
99pub struct SpawnStdio<'a> {
100 /// Source connected to the child's standard input.
101 pub stdin: StdioSource<'a>,
102 /// Source connected to the child's standard output.
103 pub stdout: StdioSource<'a>,
104 /// Source connected to the child's standard error.
105 pub stderr: StdioSource<'a>,
106 /// Maximum time the watcher waits before closing wrapper-held pipe ends.
107 pub drain_timeout: Option<Duration>,
108 /// Whether Windows children may inherit or allocate a visible console.
109 pub show_console: bool,
110}
111
112/// Creation policy for [`spawn_tokio`].
113///
114/// This compatibility entrypoint lets async daemons keep Tokio's pipe and
115/// wait APIs while making `running-process` the sole owner of child-creation
116/// policy. It defaults to contained, console-less children.
117#[cfg(feature = "client-async")]
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119pub struct TokioSpawnOptions {
120 /// Terminate the child when Tokio's child handle is dropped.
121 pub kill_on_drop: bool,
122 /// Whether Windows children may inherit or allocate a visible console.
123 pub show_console: bool,
124 /// Kill this child at the OS level when the spawning process dies.
125 ///
126 /// - **Linux**: installs `PR_SET_PDEATHSIG(SIGTERM)` in the child.
127 /// - **Windows**: assigns the child to a process-wide `KILL_ON_JOB_CLOSE`
128 /// Job Object, so the child (and its descendants) die when the spawner's
129 /// handle to the job closes — i.e. when the spawner process exits.
130 /// - **macOS / other Unix**: best-effort no-op for now (running-process#885).
131 ///
132 /// `kill_on_drop` only fires if the spawner runs its `Drop`; this option
133 /// covers the crash / SIGKILL / taskkill case where `Drop` never runs. Use
134 /// for transient children of a long-lived process (e.g. a daemon's compiler
135 /// subprocesses) that must not outlive their owner.
136 pub kill_when_owner_dies: bool,
137}
138
139#[cfg(feature = "client-async")]
140impl Default for TokioSpawnOptions {
141 fn default() -> Self {
142 Self {
143 kill_on_drop: true,
144 show_console: false,
145 kill_when_owner_dies: false,
146 }
147 }
148}
149
150impl Default for SpawnStdio<'_> {
151 fn default() -> Self {
152 Self {
153 stdin: StdioSource::Null,
154 stdout: StdioSource::Parent,
155 stderr: StdioSource::Parent,
156 drain_timeout: Some(Duration::from_secs(2)),
157 show_console: false,
158 }
159 }
160}
161
162/// Caller-supplied output bindings for a detached daemon.
163///
164/// Detached children may write only to the platform null device or to
165/// caller-owned file handles. Parent stdio and anonymous pipes are
166/// intentionally unavailable: either can retain the launching process's
167/// lifetime or fail after that process exits. The child always receives a
168/// fresh inheritable duplicate, and the caller retains its original handle.
169pub struct DaemonStdio<'a> {
170 /// Source connected to the daemon's standard output.
171 pub stdout: DaemonStdioSource<'a>,
172 /// Source connected to the daemon's standard error.
173 pub stderr: DaemonStdioSource<'a>,
174}
175
176impl Default for DaemonStdio<'_> {
177 fn default() -> Self {
178 Self {
179 stdout: DaemonStdioSource::Null,
180 stderr: DaemonStdioSource::Null,
181 }
182 }
183}
184
185/// Safe output source for a detached daemon.
186pub enum DaemonStdioSource<'a> {
187 /// Connect this slot to the platform null device (`NUL` / `/dev/null`).
188 Null,
189 /// Bind this slot to a caller-owned OS handle. The wrapper duplicates the
190 /// handle into an inheritable copy for the child.
191 #[cfg(windows)]
192 Handle(BorrowedHandle<'a>),
193 /// Bind this slot to a caller-owned file descriptor. Equivalent to
194 /// `DaemonStdioSource::Handle` on Windows.
195 #[cfg(unix)]
196 Fd(BorrowedFd<'a>),
197 #[doc(hidden)]
198 _Phantom(std::marker::PhantomData<&'a ()>),
199}
200
201/// Per-slot source describing what the child should inherit for one of
202/// stdin / stdout / stderr.
203pub enum StdioSource<'a> {
204 /// Connect this slot to the platform null device (`NUL` / `/dev/null`).
205 Null,
206 /// Inherit the parent's corresponding standard handle. The kernel
207 /// receives a fresh inheritable duplicate; the parent's original slot
208 /// is untouched.
209 Parent,
210 /// Bind this slot to a caller-owned OS handle. The wrapper duplicates
211 /// the handle into an inheritable copy for the child; the caller
212 /// retains its own handle and is responsible for closing it.
213 #[cfg(windows)]
214 Handle(BorrowedHandle<'a>),
215 /// Bind this slot to a caller-owned file descriptor. Equivalent to
216 /// `StdioSource::Handle` on Unix.
217 #[cfg(unix)]
218 Fd(BorrowedFd<'a>),
219 /// Create a fresh anonymous pipe. The child gets one end; the parent
220 /// gets the other via [`SpawnedChild`]'s `stdin` / `stdout` / `stderr`
221 /// fields.
222 Pipe,
223 #[doc(hidden)]
224 _Phantom(std::marker::PhantomData<&'a ()>),
225}
226
227// _Phantom is uninhabitable from outside: PhantomData<&'a ()> is a private
228// constructor in practice (the variant is doc(hidden) and not constructed
229// anywhere in this crate). It's only here so the `'a` lifetime is always
230// used regardless of which cfg branch is active.
231
232/// Handle to a detached daemon spawned via [`spawn_daemon`].
233///
234/// The daemon child always has stdin connected to the platform null device.
235/// Stdout and stderr also default to null, but [`spawn_daemon_with_stdio`]
236/// can bind them to caller-owned files. A detached process can never inherit
237/// parent stdio or caller pipes through this API. Dropping `DaemonChild` does
238/// NOT terminate the daemon; it only closes the OS handle the wrapper held.
239/// Call [`DaemonChild::kill`] to terminate.
240pub struct DaemonChild {
241 pid: u32,
242 #[cfg(windows)]
243 handle: imp::OwnedHandle,
244 #[cfg(unix)]
245 child: std::process::Child,
246}
247
248impl DaemonChild {
249 /// Process ID.
250 pub fn id(&self) -> u32 {
251 self.pid
252 }
253
254 /// Forcibly terminate the child. Best-effort.
255 pub fn kill(&mut self) -> std::io::Result<()> {
256 #[cfg(windows)]
257 {
258 imp::terminate(&self.handle)
259 }
260 #[cfg(unix)]
261 {
262 self.child.kill()
263 }
264 }
265
266 /// Block until the child exits and return its exit code.
267 pub fn wait(&mut self) -> std::io::Result<i32> {
268 #[cfg(windows)]
269 {
270 imp::wait(&self.handle)
271 }
272 #[cfg(unix)]
273 {
274 let status = self.child.wait()?;
275 Ok(unix_exit_code(status))
276 }
277 }
278
279 /// Non-blocking variant of [`Self::wait`].
280 pub fn try_wait(&mut self) -> std::io::Result<Option<i32>> {
281 #[cfg(windows)]
282 {
283 imp::try_wait(&self.handle)
284 }
285 #[cfg(unix)]
286 {
287 Ok(self.child.try_wait()?.map(unix_exit_code))
288 }
289 }
290}
291
292/// Handle to a contained child spawned via [`spawn`].
293///
294/// On Drop, `SpawnedChild` synchronously kills the child:
295/// * Windows: closes the Job Object handle; `KILL_ON_JOB_CLOSE` causes the
296/// kernel to terminate every process in the job (the child and its
297/// descendants).
298/// * Unix: `killpg(pgid, SIGKILL)` and `waitpid` to reap.
299///
300/// The optional `stdin` / `stdout` / `stderr` fields are present when the
301/// corresponding [`StdioSource`] was [`StdioSource::Pipe`]; otherwise they
302/// are `None`.
303pub struct SpawnedChild {
304 /// Parent-side pipe for writing to child stdin when requested.
305 pub stdin: Option<std::process::ChildStdin>,
306 /// Parent-side pipe for reading child stdout when requested.
307 pub stdout: Option<std::process::ChildStdout>,
308 /// Parent-side pipe for reading child stderr when requested.
309 pub stderr: Option<std::process::ChildStderr>,
310 pid: u32,
311 #[cfg(windows)]
312 inner: imp::SpawnedInner,
313 #[cfg(unix)]
314 inner: unix_impl::SpawnedInner,
315}
316
317impl SpawnedChild {
318 /// Process ID of the spawned child.
319 pub fn id(&self) -> u32 {
320 self.pid
321 }
322
323 /// Forcibly terminate the child. Best-effort.
324 pub fn kill(&mut self) -> std::io::Result<()> {
325 #[cfg(windows)]
326 {
327 self.inner.kill()
328 }
329 #[cfg(unix)]
330 {
331 self.inner.kill()
332 }
333 }
334
335 /// Block until the child exits and return its exit code.
336 pub fn wait(&mut self) -> std::io::Result<i32> {
337 #[cfg(windows)]
338 {
339 self.inner.wait()
340 }
341 #[cfg(unix)]
342 {
343 self.inner.wait()
344 }
345 }
346
347 /// Non-blocking variant of [`Self::wait`].
348 pub fn try_wait(&mut self) -> std::io::Result<Option<i32>> {
349 #[cfg(windows)]
350 {
351 self.inner.try_wait()
352 }
353 #[cfg(unix)]
354 {
355 self.inner.try_wait()
356 }
357 }
358}
359
360impl Drop for SpawnedChild {
361 fn drop(&mut self) {
362 #[cfg(windows)]
363 {
364 self.inner.shutdown();
365 }
366 #[cfg(unix)]
367 {
368 self.inner.shutdown();
369 }
370 }
371}
372
373/// Set on every child spawned through the daemon path, so a process can be
374/// recognized as a *declared daemon* rather than inferred to be one.
375///
376/// # Why a positive marker
377///
378/// Reapers previously had to infer daemon-ness from the **absence** of
379/// [`crate::ORIGINATOR_ENV_VAR`], which `spawn_daemon` strips. But absence is
380/// overloaded: it means both "this process deliberately detached itself" and
381/// "something in the chain clobbered the environment" — and those are
382/// byte-identical at the observation point, so no amount of process-lineage
383/// tracking can separate them. See zackees/clud#522, where an
384/// ancestry-fallback proposal and a daemon exemption read the same signal and
385/// drew opposite conclusions.
386///
387/// A positive declaration removes the ambiguity: only a process that actually
388/// went through the daemon path carries this.
389///
390/// # Caveat
391///
392/// This is still an environment variable, so a chain that strips
393/// `RUNNING_PROCESS_ORIGINATOR` strips this too. It narrows the ambiguous case
394/// rather than eliminating it; a durable answer would need the daemon's
395/// supervisor to register the PID somewhere the reaper can read.
396///
397/// Distinct from `RUNNING_PROCESS_DAEMON_SCOPE`, which names a broker scope
398/// and is unrelated.
399pub const DAEMON_MARKER_ENV_VAR: &str = "RUNNING_PROCESS_IS_DAEMON";
400
401/// Spawn `command` as a detached daemon. NUL stdio, sanitized handles,
402/// no console window, ignores parent's Ctrl-C / SIGINT (Windows:
403/// `CREATE_NEW_PROCESS_GROUP` + `DETACHED_PROCESS`; Unix: `setsid` puts the
404/// daemon in a new session so it's not in the parent's foreground group).
405///
406/// Use [`spawn_daemon_with_stdio`] when the daemon must write to stable
407/// caller-owned files. Parent stdio and anonymous pipes remain unavailable
408/// for detached children.
409pub fn spawn_daemon(command: &mut Command) -> std::io::Result<DaemonChild> {
410 spawn_daemon_inner(
411 command,
412 DaemonStdio::default(),
413 EnvironmentPolicy::Auto,
414 false,
415 )
416}
417
418/// Spawn a detached daemon with file-or-NUL stdout and stderr.
419///
420/// Stdin remains connected to null. The supplied handles are duplicated into
421/// the sanitized child handle list, so the caller can close its files after
422/// this function returns without affecting the daemon.
423pub fn spawn_daemon_with_stdio(
424 command: &mut Command,
425 stdio: DaemonStdio<'_>,
426) -> std::io::Result<DaemonChild> {
427 spawn_daemon_with_stdio_and_env_policy(command, stdio, EnvironmentPolicy::Auto)
428}
429
430/// [`spawn_daemon_with_stdio`] with an explicit environment policy.
431pub fn spawn_daemon_with_stdio_and_env_policy(
432 command: &mut Command,
433 stdio: DaemonStdio<'_>,
434 policy: EnvironmentPolicy,
435) -> std::io::Result<DaemonChild> {
436 spawn_daemon_inner(command, stdio, policy, false)
437}
438
439/// Like [`spawn_daemon`] but with explicit control over whether the
440/// daemon's inherited env is passed through to the child.
441///
442/// `clear_env = false` uses [`EnvironmentPolicy::Auto`], matching
443/// [`spawn_daemon`].
444///
445/// `clear_env = true`: child sees ONLY the explicit `command.env(...)`
446/// entries. Mirrors `command.env_clear()` semantics for callers using
447/// the manual `CreateProcessW` path (Rust stdlib's `env_clear` flag
448/// isn't observable through `Command::get_envs`, so our sanitized
449/// spawn machinery can't otherwise honour it).
450pub fn spawn_daemon_with_clear_env(
451 command: &mut Command,
452 clear_env: bool,
453) -> std::io::Result<DaemonChild> {
454 let policy = if clear_env {
455 EnvironmentPolicy::Clear
456 } else {
457 EnvironmentPolicy::Auto
458 };
459 spawn_daemon_inner(command, DaemonStdio::default(), policy, false)
460}
461
462/// Spawn a detached daemon using an explicit environment policy.
463///
464/// [`EnvironmentPolicy::Auto`] resolves to
465/// [`EnvironmentPolicy::UserBaseline`] for daemons, excluding unlisted
466/// ambient variables. Use [`EnvironmentPolicy::Inherit`] as the explicit
467/// escape hatch for trusted callers that require the full parent environment.
468/// In every mode, explicit command environment additions, overrides, and
469/// removals are applied last.
470pub fn spawn_daemon_with_env_policy(
471 command: &mut Command,
472 policy: EnvironmentPolicy,
473) -> std::io::Result<DaemonChild> {
474 spawn_daemon_inner(command, DaemonStdio::default(), policy, false)
475}
476
477/// Like [`spawn_daemon`], but the child also **breaks away from any Job
478/// Object the spawner belongs to** (Windows; a no-op elsewhere).
479///
480/// Use this for a daemon that must outlive the process tree that happened to
481/// start it — a build cache server, a language server, anything discovered
482/// and reused by later, unrelated invocations.
483///
484/// # Why this is separate from [`spawn_daemon`]
485///
486/// "Detached lifetime" and "escapes my caller's containment" are different
487/// properties, and callers genuinely want them independently. Job Object
488/// membership is inherited by every descendant at any depth, and jobs created
489/// by this crate carry `KILL_ON_JOB_CLOSE` — so without breakaway the kernel
490/// terminates such a daemon the moment the spawner's job handle drops, no
491/// matter how detached the daemon made itself.
492///
493/// But making that unconditional breaks the opposite use: a child spawned as
494/// a daemon purely to obtain a sanitized handle list must stay inside the
495/// caller's job. `testbins/src/bin/spawner.rs` does exactly this, and
496/// `containment_test::test_contained_group_kills_grandchildren` fails if its
497/// sleepers escape.
498///
499/// # Refusal is not silent
500///
501/// `CREATE_BREAKAWAY_FROM_JOB` is *refused*, not ignored, when the spawner
502/// sits inside a job that lacks `JOB_OBJECT_LIMIT_BREAKAWAY_OK`:
503/// `CreateProcessW` fails with `ERROR_ACCESS_DENIED`. Outer jobs we do not
504/// control are common (CI runners, container supervisors, debuggers), so the
505/// spawn retries once with the flag cleared — a daemon that stays contained
506/// beats a daemon that fails to start.
507pub fn spawn_daemon_breaking_away_from_job(command: &mut Command) -> std::io::Result<DaemonChild> {
508 spawn_daemon_inner(
509 command,
510 DaemonStdio::default(),
511 EnvironmentPolicy::Auto,
512 true,
513 )
514}
515
516/// [`spawn_daemon_breaking_away_from_job`] with an explicit env policy.
517pub fn spawn_daemon_breaking_away_with_env_policy(
518 command: &mut Command,
519 policy: EnvironmentPolicy,
520) -> std::io::Result<DaemonChild> {
521 spawn_daemon_inner(command, DaemonStdio::default(), policy, true)
522}
523
524/// Apply the daemon self-declaration to `command`. Split out from
525/// [`spawn_daemon_inner`] so the policy is unit-testable without spawning a
526/// real detached process.
527pub(crate) fn mark_as_daemon(command: &mut Command) {
528 command.env(DAEMON_MARKER_ENV_VAR, "1");
529}
530
531fn spawn_daemon_inner(
532 command: &mut Command,
533 stdio: DaemonStdio<'_>,
534 policy: EnvironmentPolicy,
535 breakaway: bool,
536) -> std::io::Result<DaemonChild> {
537 // Every daemon-spawn variant funnels through here, so this is the one
538 // place that can mark them all — including the free functions consumers
539 // like zccache call directly.
540 mark_as_daemon(command);
541 let policy = policy.resolve(SpawnLifetime::Daemon);
542 #[cfg(windows)]
543 {
544 imp::spawn_daemon(command, stdio, policy, breakaway)
545 }
546 #[cfg(unix)]
547 {
548 // Unix has no Job Object; `setsid` already detaches the daemon from
549 // the parent's session and process group, so breakaway is moot.
550 let _ = breakaway;
551 unix_impl::spawn_daemon(command, stdio, policy)
552 }
553}
554
555/// Spawn `command` as a contained child with caller-controlled stdio.
556/// Sanitized handles, and no console (`DETACHED_PROCESS` on Windows). Child
557/// dies when the returned
558/// [`SpawnedChild`] is dropped.
559pub fn spawn(command: &mut Command, stdio: SpawnStdio<'_>) -> std::io::Result<SpawnedChild> {
560 spawn_with_env_policy(command, stdio, EnvironmentPolicy::Auto)
561}
562
563/// Spawn a contained child using an explicit environment policy.
564pub fn spawn_with_env_policy(
565 command: &mut Command,
566 stdio: SpawnStdio<'_>,
567 policy: EnvironmentPolicy,
568) -> std::io::Result<SpawnedChild> {
569 let policy = policy.resolve(SpawnLifetime::Contained);
570 #[cfg(windows)]
571 {
572 imp::spawn(command, stdio, policy)
573 }
574 #[cfg(unix)]
575 {
576 unix_impl::spawn(command, stdio, policy)
577 }
578}
579
580/// Spawn a Tokio child through the centralized process-creation boundary.
581///
582/// Callers retain Tokio's async stdin/stdout/stderr and wait APIs, but may not
583/// apply platform creation flags themselves. On Windows, console suppression
584/// is owned here. Use [`spawn`] when the stronger sanitized-handle-list and
585/// kill-on-close Job Object contract is required.
586#[cfg(feature = "client-async")]
587pub fn spawn_tokio(
588 command: &mut tokio::process::Command,
589 options: TokioSpawnOptions,
590) -> std::io::Result<tokio::process::Child> {
591 command.kill_on_drop(options.kill_on_drop);
592 #[cfg(windows)]
593 command.creation_flags(tokio_creation_flags(options.show_console));
594 #[cfg(not(windows))]
595 let _ = options.show_console;
596
597 // Linux: link the child's death to the spawner's via PR_SET_PDEATHSIG,
598 // installed in the child just before exec. macOS/other Unix have no
599 // equivalent primitive, so this is a no-op there (running-process#885).
600 #[cfg(target_os = "linux")]
601 if options.kill_when_owner_dies {
602 use std::os::unix::process::CommandExt;
603 // SAFETY: the closure calls only prctl(2), which is async-signal-safe.
604 unsafe {
605 command.pre_exec(|| {
606 let rc = libc::prctl(
607 libc::PR_SET_PDEATHSIG,
608 libc::SIGTERM as libc::c_ulong,
609 0,
610 0,
611 0,
612 );
613 if rc == -1 {
614 return Err(std::io::Error::last_os_error());
615 }
616 Ok(())
617 });
618 }
619 }
620
621 let child = command.spawn()?;
622
623 // Windows: place the child in the process-wide kill-on-close job so the OS
624 // reaps it (and its descendants) when the spawner exits — including a
625 // SIGKILL/taskkill that skips `Drop`/`kill_on_drop`.
626 #[cfg(windows)]
627 if options.kill_when_owner_dies {
628 if let Some(handle) = child.raw_handle() {
629 owner_death_job::assign(handle);
630 }
631 }
632 #[cfg(not(any(windows, target_os = "linux")))]
633 let _ = options.kill_when_owner_dies;
634
635 Ok(child)
636}
637
638/// Process-wide `KILL_ON_JOB_CLOSE` job used by
639/// [`TokioSpawnOptions::kill_when_owner_dies`] on Windows. Children assigned to
640/// it die when this process's handle to the job closes, i.e. when this process
641/// exits — the crash/taskkill path that `kill_on_drop` cannot cover.
642#[cfg(all(windows, feature = "client-async"))]
643mod owner_death_job {
644 use std::os::windows::io::RawHandle;
645 use std::sync::OnceLock;
646 use winapi::shared::minwindef::DWORD;
647 use winapi::um::jobapi2::{
648 AssignProcessToJobObject, CreateJobObjectW, SetInformationJobObject,
649 };
650 use winapi::um::winnt::{
651 JobObjectExtendedLimitInformation, HANDLE, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
652 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
653 };
654
655 struct Job(HANDLE);
656 // The handle is used only via AssignProcessToJobObject and is never freed
657 // (its closure on process exit is the whole point).
658 unsafe impl Send for Job {}
659 unsafe impl Sync for Job {}
660
661 static JOB: OnceLock<Option<Job>> = OnceLock::new();
662
663 fn create() -> Option<Job> {
664 unsafe {
665 let handle = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());
666 if handle.is_null() {
667 return None;
668 }
669 let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
670 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
671 let ok = SetInformationJobObject(
672 handle,
673 JobObjectExtendedLimitInformation,
674 &mut info as *mut _ as *mut _,
675 std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as DWORD,
676 );
677 if ok == 0 {
678 return None;
679 }
680 Some(Job(handle))
681 }
682 }
683
684 pub(super) fn assign(child: RawHandle) {
685 let Some(job) = JOB.get_or_init(create).as_ref() else {
686 return;
687 };
688 // Best-effort: a child that already belongs to a no-breakaway job, or
689 // has exited, cannot be assigned — neither is worth failing the spawn.
690 unsafe {
691 AssignProcessToJobObject(job.0, child as HANDLE);
692 }
693 }
694}
695
696#[cfg(all(feature = "client-async", windows))]
697fn tokio_creation_flags(show_console: bool) -> u32 {
698 if show_console {
699 0
700 } else {
701 // CREATE_NO_WINDOW. Keep this policy private so consumers cannot
702 // duplicate or partially apply Windows creation flags.
703 0x0800_0000
704 }
705}
706
707#[cfg(unix)]
708fn unix_exit_code(status: std::process::ExitStatus) -> i32 {
709 use std::os::unix::process::ExitStatusExt;
710 status
711 .code()
712 .unwrap_or_else(|| -status.signal().unwrap_or(1))
713}
714
715// ── Windows implementation ──────────────────────────────────────────────────
716
717#[cfg(windows)]
718#[path = "spawn_imp_windows.rs"]
719mod imp;
720
721#[cfg(unix)]
722#[path = "spawn_imp_unix.rs"]
723mod unix_impl;
724#[cfg(test)]
725mod tests {
726 use super::*;
727
728 #[cfg(feature = "client-async")]
729 #[test]
730 fn kill_when_owner_dies_defaults_off() {
731 // Opt-in only — existing callers keep today's behavior.
732 assert!(!TokioSpawnOptions::default().kill_when_owner_dies);
733 }
734
735 #[test]
736 fn spawn_stdio_default_has_sane_values() {
737 let s = SpawnStdio::default();
738 assert!(matches!(s.stdin, StdioSource::Null));
739 assert!(matches!(s.stdout, StdioSource::Parent));
740 assert!(matches!(s.stderr, StdioSource::Parent));
741 assert_eq!(s.drain_timeout, Some(Duration::from_secs(2)));
742 // No console window by default — opt-in only.
743 assert!(!s.show_console);
744 }
745
746 #[test]
747 fn daemon_stdio_default_is_null() {
748 let stdio = DaemonStdio::default();
749 assert!(matches!(stdio.stdout, DaemonStdioSource::Null));
750 assert!(matches!(stdio.stderr, DaemonStdioSource::Null));
751 }
752
753 #[test]
754 fn auto_environment_policy_depends_on_lifetime() {
755 assert_eq!(
756 EnvironmentPolicy::Auto.resolve(SpawnLifetime::Contained),
757 EnvironmentPolicy::Inherit
758 );
759 assert_eq!(
760 EnvironmentPolicy::Auto.resolve(SpawnLifetime::Daemon),
761 EnvironmentPolicy::UserBaseline
762 );
763 }
764
765 #[test]
766 fn explicit_environment_policy_is_not_rewritten() {
767 for policy in [
768 EnvironmentPolicy::Inherit,
769 EnvironmentPolicy::UserBaseline,
770 EnvironmentPolicy::Clear,
771 ] {
772 assert_eq!(policy.resolve(SpawnLifetime::Contained), policy);
773 assert_eq!(policy.resolve(SpawnLifetime::Daemon), policy);
774 }
775 }
776
777 #[cfg(feature = "client-async")]
778 #[test]
779 fn tokio_spawn_defaults_to_contained_consoleless_children() {
780 assert_eq!(
781 TokioSpawnOptions::default(),
782 TokioSpawnOptions {
783 kill_on_drop: true,
784 show_console: false,
785 kill_when_owner_dies: false,
786 }
787 );
788 }
789
790 #[cfg(all(feature = "client-async", windows))]
791 #[test]
792 fn tokio_spawn_owns_console_creation_flags() {
793 assert_eq!(tokio_creation_flags(false), 0x0800_0000);
794 assert_eq!(tokio_creation_flags(true), 0);
795 }
796}