Skip to main content

rightkit_child_process/
owned.rs

1use crate::process_handle::{lock_child, ProcessHandle};
2#[cfg(unix)]
3use crate::unix_child::{LeaderState, SignalOutcome, SignalTarget};
4use std::{
5    ffi::OsStr,
6    io,
7    num::NonZeroU32,
8    process::{ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus},
9    sync::{Arc, Mutex, MutexGuard},
10    thread,
11    time::{Duration, Instant, SystemTime},
12};
13#[cfg(unix)]
14type PlatformChild = crate::unix_child::UnixChild;
15#[cfg(windows)]
16type PlatformChild = crate::spawn_windows::WindowsChild;
17
18#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
19pub enum JobAssignmentMode {
20    #[default]
21    Strict,
22    BestEffort,
23}
24
25#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
26pub enum UnixContainment {
27    #[default]
28    ProcessGroup,
29    Session,
30}
31
32/// Scheduling priority requested for the child. `Normal` (the default) means "inherit": it
33/// leaves the spawn exactly as it was before this option existed, so the child keeps the
34/// parent's priority class (Windows) or nice value (Unix). It does not reset a lowered parent
35/// back to normal. The other values only lower priority, which needs no
36/// privilege and never raises a value the child already inherits above a lower one.
37///
38/// Windows: `BelowNormal` adds `BELOW_NORMAL_PRIORITY_CLASS` and `Idle` adds
39/// `IDLE_PRIORITY_CLASS` to the creation flags. Unix: the child's nice value is raised to at
40/// least 10 (`BelowNormal`) or 19 (`Idle`) between fork and exec.
41///
42/// `Efficient` is `BelowNormal` plus the platform's energy-efficiency class, for background
43/// work that should not wake the fast cores. Windows: `BELOW_NORMAL_PRIORITY_CLASS` plus
44/// EcoQoS (`PROCESS_POWER_THROTTLING_EXECUTION_SPEED` through `SetProcessInformation`, set
45/// while the child is still suspended; a Windows without EcoQoS ignores it, best effort).
46/// macOS: nice 10 and the `QOS_CLASS_UTILITY` quality of service on the child's initial
47/// thread (set between fork and exec; advisory). Other Unix: nice 10.
48#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
49#[non_exhaustive]
50pub enum ProcessPriority {
51    #[default]
52    Normal,
53    BelowNormal,
54    Idle,
55    Efficient,
56}
57
58impl ProcessPriority {
59    /// Minimum Unix nice value for this priority; `None` leaves the inherited value alone.
60    #[cfg(unix)]
61    pub(crate) fn nice_floor(self) -> Option<libc::c_int> {
62        match self {
63            Self::Normal => None,
64            Self::BelowNormal | Self::Efficient => Some(10),
65            Self::Idle => Some(19),
66        }
67    }
68}
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub struct TerminationOptions {
72    /// Send TERM immediately on Unix, then KILL after this tree-wide grace.
73    pub grace: Duration,
74    /// Passed to TerminateJobObject/TerminateProcess on Windows.
75    pub windows_exit_code: u32,
76}
77impl Default for TerminationOptions {
78    fn default() -> Self {
79        Self {
80            grace: Duration::ZERO,
81            windows_exit_code: 1,
82        }
83    }
84}
85impl TerminationOptions {
86    pub fn with_grace(mut self, grace: Duration) -> Self {
87        self.grace = grace;
88        self
89    }
90    pub fn with_windows_exit_code(mut self, code: u32) -> Self {
91        self.windows_exit_code = code;
92        self
93    }
94}
95
96/// Every failed assignment in a best-effort Windows spawn is retained.
97/// Windows job breakaway policy for an owned child; Unix ignores it.
98#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
99pub(crate) enum BreakawayMode {
100    /// Full containment.
101    #[default]
102    None,
103    /// `JOB_OBJECT_LIMIT_BREAKAWAY_OK`: only processes that ask leave the job.
104    Explicit,
105    /// `BREAKAWAY_OK` plus `SILENT_BREAKAWAY_OK`: every created process leaves the job.
106    Silent,
107}
108
109#[derive(Debug)]
110pub struct JobAssignmentFailure {
111    pub stage: &'static str,
112    pub error: io::Error,
113}
114impl JobAssignmentFailure {
115    pub fn raw_os_error(&self) -> Option<i32> {
116        self.error.raw_os_error()
117    }
118}
119
120/// Borrowed OS handle/fd value; keep it valid until spawn returns.
121#[cfg(windows)]
122pub type RawHandleOrFd = std::os::windows::io::RawHandle;
123#[cfg(unix)]
124pub type RawHandleOrFd = std::os::fd::RawFd;
125#[cfg(windows)]
126type InheritedValue = usize;
127#[cfg(unix)]
128type InheritedValue = RawHandleOrFd;
129
130#[derive(Debug)]
131pub struct OwnedCommand {
132    command: Command,
133    windows_hidden: bool,
134    windows_breakaway: BreakawayMode,
135    caller_job: CallerJob,
136    job_assignment: JobAssignmentMode,
137    unix_containment: UnixContainment,
138    process_priority: ProcessPriority,
139    kill_on_parent_exit: bool,
140    // Set by `spawn_suspended`: Windows leaves the child suspended until `OwnedChild::resume`.
141    hold_suspended: bool,
142    // None preserves the existing unbounded partial-spawn reap.
143    partial_spawn_cleanup_timeout: Option<Duration>,
144    inherited_handles: Option<Vec<InheritedValue>>,
145    #[cfg(windows)]
146    windows_spawn_config: Option<crate::WindowsSpawnConfig>,
147    #[cfg(windows)]
148    windows_spawn_config_auto: bool,
149    env_cleared: bool,
150}
151#[cfg(windows)]
152type CallerJob = Option<std::os::windows::io::OwnedHandle>;
153#[cfg(not(windows))]
154type CallerJob = Option<std::convert::Infallible>;
155
156impl OwnedCommand {
157    pub fn new(program: impl AsRef<OsStr>) -> Self {
158        let command = Self::from_command(Command::new(program));
159        #[cfg(windows)]
160        let command = {
161            let mut command = command;
162            // Only a fresh Command has known inherited stdio/environment defaults. The
163            // config is marked auto, and `command_mut` clears it.
164            command.windows_spawn_config = Some(crate::WindowsSpawnConfig::default());
165            command.windows_spawn_config_auto = true;
166            command
167        };
168        command
169    }
170    /// GUI/service children are hidden on Windows by default. A wrapped `Command` may carry
171    /// stdio, environment and raw-argument settings this crate cannot read back, so on
172    /// Windows an allowlisted spawn (`inherit_handles_only`) from `from_command` requires an
173    /// explicit `windows_spawn_config`. The crate never widens what a child inherits by
174    /// default.
175    pub fn from_command(command: Command) -> Self {
176        Self {
177            command,
178            windows_hidden: true,
179            windows_breakaway: BreakawayMode::None,
180            caller_job: None,
181            job_assignment: JobAssignmentMode::Strict,
182            unix_containment: UnixContainment::ProcessGroup,
183            process_priority: ProcessPriority::Normal,
184            kill_on_parent_exit: false,
185            hold_suspended: false,
186            partial_spawn_cleanup_timeout: None,
187            inherited_handles: None,
188            #[cfg(windows)]
189            windows_spawn_config: None,
190            #[cfg(windows)]
191            windows_spawn_config_auto: false,
192            env_cleared: false,
193        }
194    }
195    pub fn command_mut(&mut self) -> &mut Command {
196        #[cfg(windows)]
197        if self.windows_spawn_config_auto {
198            // A mutable std Command can change settings with no stable getter.
199            self.windows_spawn_config = None;
200        }
201        &mut self.command
202    }
203    /// Inherit only these handles/fds plus configured stdio; values stay caller-owned.
204    pub fn inherit_handles_only(
205        &mut self,
206        handles: impl IntoIterator<Item = RawHandleOrFd>,
207    ) -> &mut Self {
208        #[cfg(windows)]
209        let handles = handles.into_iter().map(|handle| handle as usize).collect();
210        #[cfg(unix)]
211        let handles = handles.into_iter().collect();
212        self.inherited_handles = Some(handles);
213        self
214    }
215    /// Supply opaque stdio/environment/raw-arg settings for native allowlisted spawns.
216    /// Required after from_command/command_mut when inherit_handles_only is set.
217    /// Ordinary std spawns ignore it; fresh new() Commands have known defaults.
218    #[cfg(windows)]
219    pub fn windows_spawn_config(&mut self, config: crate::WindowsSpawnConfig) -> &mut Self {
220        self.windows_spawn_config = Some(config);
221        self.windows_spawn_config_auto = false;
222        self
223    }
224    /// Clear environment, then copy only named variables set in this process.
225    pub fn env_allowlist<I, S>(&mut self, keys: I) -> &mut Self
226    where
227        I: IntoIterator<Item = S>,
228        S: AsRef<OsStr>,
229    {
230        let kept: Vec<_> = keys
231            .into_iter()
232            .filter_map(|k| std::env::var_os(k.as_ref()).map(|v| (k.as_ref().to_os_string(), v)))
233            .collect();
234        self.command.env_clear();
235        self.env_cleared = true;
236        for (k, v) in kept {
237            self.command.env(k, v);
238        }
239        self
240    }
241    /// [`Self::env_allowlist`] over [`DEFAULT_ENV_ALLOWLIST`] plus `extra` (a consumer's own
242    /// variables, for instance a model-cache location). Call it once: like `env_allowlist` it
243    /// starts from an empty environment, so a second call drops what the first kept.
244    pub fn env_allowlist_with_defaults<I, S>(&mut self, extra: I) -> &mut Self
245    where
246        I: IntoIterator<Item = S>,
247        S: AsRef<OsStr>,
248    {
249        let keys: Vec<std::ffi::OsString> = DEFAULT_ENV_ALLOWLIST
250            .iter()
251            .map(std::ffi::OsString::from)
252            .chain(extra.into_iter().map(|k| k.as_ref().to_os_string()))
253            .collect();
254        self.env_allowlist(keys)
255    }
256    /// True once [`Self::env_allowlist`] (or `env_allowlist_with_defaults`) cleared the inherited
257    /// environment on this command. A `Command` that was cleared with `env_clear` directly, or
258    /// wrapped by `from_command` after it was cleared, reads `false`: the state is not observable
259    /// from `std::process::Command`.
260    pub fn env_is_cleared(&self) -> bool {
261        self.env_cleared
262    }
263    pub fn env_strip<I, S>(&mut self, keys: I) -> &mut Self
264    where
265        I: IntoIterator<Item = S>,
266        S: AsRef<OsStr>,
267    {
268        for k in keys {
269            self.command.env_remove(k);
270        }
271        self
272    }
273    /// Compatibility no-op: hidden is already the default.
274    pub fn windows_hide(&mut self) -> &mut Self {
275        self.windows_hidden = true;
276        self
277    }
278    /// Windows: leaves the console window visible, clearing the hidden default, for a
279    /// user-facing program (an editor, a terminal at a project path) that must keep its window.
280    /// Pair it with [`spawn_uncontained_detached`](Self::spawn_uncontained_detached) for a
281    /// fire-and-forget launch. Unix ignores it.
282    pub fn windows_visible(&mut self) -> &mut Self {
283        self.windows_hidden = false;
284        self
285    }
286    /// Windows: explicit-only breakaway (`JOB_OBJECT_LIMIT_BREAKAWAY_OK`). A process in the
287    /// job leaves it only by asking (`CREATE_BREAKAWAY_FROM_JOB`); every descendant that does
288    /// not ask stays contained and is terminated with the job. Dropping the owned child still
289    /// terminates the helper's tree; a grandchild that asked and broke away stays running.
290    /// `false` restores full containment. Unix ignores it. For descendants that escape without
291    /// asking, see [`windows_allow_silent_breakaway`](Self::windows_allow_silent_breakaway).
292    pub fn windows_allow_breakaway(&mut self, allow: bool) -> &mut Self {
293        self.windows_breakaway = if allow {
294            BreakawayMode::Explicit
295        } else {
296            BreakawayMode::None
297        };
298        self
299    }
300    /// Windows: silent breakaway (`JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK` plus `BREAKAWAY_OK`).
301    /// Every process the job's processes create is outside the job without asking, so
302    /// descendants are NOT contained and survive the helper. Use only for a helper whose
303    /// children must all outlive it. `false` restores full containment. Unix ignores it.
304    pub fn windows_allow_silent_breakaway(&mut self, allow: bool) -> &mut Self {
305        self.windows_breakaway = if allow {
306            BreakawayMode::Silent
307        } else {
308            BreakawayMode::None
309        };
310        self
311    }
312    /// Duplicate caller job; assign before the empty nested RightKit job.
313    #[cfg(windows)]
314    pub fn windows_job(
315        &mut self,
316        job: std::os::windows::io::BorrowedHandle<'_>,
317    ) -> io::Result<&mut Self> {
318        self.caller_job = Some(job.try_clone_to_owned()?);
319        Ok(self)
320    }
321    /// Ignored on Unix. BestEffort reports errors on OwnedChild.
322    pub fn job_assignment_mode(&mut self, mode: JobAssignmentMode) -> &mut Self {
323        self.job_assignment = mode;
324        self
325    }
326    /// Ignored on Windows. Session runs setsid before exec.
327    pub fn unix_containment(&mut self, mode: UnixContainment) -> &mut Self {
328        self.unix_containment = mode;
329        self
330    }
331    /// Scheduling priority for the child. The default, `ProcessPriority::Normal`, adds nothing
332    /// to the spawn. See `ProcessPriority` for the Windows and Unix mappings. Applies to
333    /// detached spawns too.
334    pub fn process_priority(&mut self, priority: ProcessPriority) -> &mut Self {
335        self.process_priority = priority;
336        self
337    }
338    /// Opt-in, off by default. Linux only: the kernel sends `SIGTERM` to the direct child
339    /// (`PR_SET_PDEATHSIG`) when the thread that spawned it exits, which includes this
340    /// process dying without running cleanup (for example SIGKILL). The signal reaches the
341    /// direct child only, never the rest of its group, it can be caught, and there is no
342    /// `SIGKILL` escalation. It is keyed to the spawning *thread*, so spawn from a thread
343    /// that lives as long as the child.
344    ///
345    /// macOS has no `PR_SET_PDEATHSIG`, so the tie is a small watchdog process started right
346    /// after the child (a `kqueue` with `EVFILT_PROC`/`NOTE_EXIT` on this process and on the
347    /// child; see `process_unix::arm_parent_death_watch`). When this process dies, SIGKILL
348    /// included, the watchdog sends `SIGTERM` to the child's whole process group and `SIGKILL`
349    /// two seconds later. Differences from Linux: the trigger is the whole process exiting,
350    /// not the spawning thread; the signal goes to the group and escalates; it costs one
351    /// extra helper process per tied child (it exits when the child or this process does);
352    /// the child runs untied between its spawn and the moment `spawn` returns (a few
353    /// milliseconds), and `spawn` kills the child and fails if the watchdog cannot be armed.
354    ///
355    /// Other Unix hosts have no parent-death primitive this crate relies on: the spawn fails
356    /// with `ErrorKind::Unsupported` before anything is started, rather than run the child
357    /// untied. Windows ignores the option because kill-on-close job objects already cover
358    /// parent death. Rejected (`InvalidInput`) for detached spawns.
359    pub fn kill_on_parent_exit(&mut self, enabled: bool) -> &mut Self {
360        self.kill_on_parent_exit = enabled;
361        self
362    }
363    /// Bound failed-spawn cleanup. Timed-out children go to a background reaper.
364    pub fn partial_spawn_cleanup_timeout(&mut self, timeout: Duration) -> &mut Self {
365        self.partial_spawn_cleanup_timeout = Some(timeout);
366        self
367    }
368    pub fn spawn(self) -> io::Result<OwnedChild> {
369        self.spawn_inner(false, false, None)
370    }
371    /// Like [`OwnedCommand::spawn`], but on Windows the child is created suspended, assigned to
372    /// its Job Object, and held there: it executes no instruction until
373    /// [`OwnedChild::resume`] is called. A caller can persist the child's identity (pid and
374    /// start time) first, then release it. Dropping a child that was never resumed terminates it
375    /// like any owned child.
376    ///
377    /// Unix has no suspended creation: this is `spawn`, the child runs at once, and `resume` is
378    /// a no-op that returns `Ok`. A caller that needs a hold on Unix gates the child itself (for
379    /// example a pipe the child blocks reading).
380    pub fn spawn_suspended(mut self) -> io::Result<OwnedChild> {
381        self.hold_suspended = true;
382        self.spawn_inner(false, false, None)
383    }
384    /// Shared launch with no owner job or terminate-on-drop. Unix uses setsid.
385    /// Stdio follows Command: dropping piped stdin can still deliver EOF.
386    ///
387    /// Windows first asks to break away from the caller's job object
388    /// (`CREATE_BREAKAWAY_FROM_JOB`). If the caller's job denies breakaway
389    /// (`ERROR_ACCESS_DENIED`), the launch is retried once without it, as
390    /// `DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP` with the same stdio and handle
391    /// allowlist (an ordinary spawn without an allowlist still inherits what std inherits).
392    /// The child is then detached from the console and process group but stays inside the
393    /// caller's job, so the job's limits (including kill-on-close) still reach it. Check
394    /// [`OwnedChild::stayed_in_parent_job`] to learn which happened. A caller that must not
395    /// run inside the parent's job uses
396    /// [`spawn_uncontained_detached_strict`](Self::spawn_uncontained_detached_strict). Any
397    /// other launch failure is returned as is, without a retry. Unix behaviour is unchanged.
398    pub fn spawn_uncontained_detached(self) -> io::Result<OwnedChild> {
399        self.spawn_inner_with(true, false, None, true)
400    }
401    /// Like [`spawn_uncontained_detached`](Self::spawn_uncontained_detached), but never falls
402    /// back. On Windows, when the caller's job denies breakaway the launch fails with an
403    /// [`io::Error`] of kind `PermissionDenied` wrapping [`BreakawayDenied`] (recover it with
404    /// `error.get_ref().and_then(|e| e.downcast_ref::<BreakawayDenied>())`), and nothing is
405    /// started. On Unix this is identical to `spawn_uncontained_detached`.
406    pub fn spawn_uncontained_detached_strict(self) -> io::Result<OwnedChild> {
407        match self.spawn_inner_with(true, false, None, false) {
408            Err(error) if cfg!(windows) && is_denied_breakaway(&error) => Err(io::Error::new(
409                io::ErrorKind::PermissionDenied,
410                BreakawayDenied,
411            )),
412            other => other,
413        }
414    }
415    fn spawn_inner(
416        self,
417        detached: bool,
418        fail_after_spawn: bool,
419        captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
420    ) -> io::Result<OwnedChild> {
421        self.spawn_inner_with(detached, fail_after_spawn, captured_pid, false)
422    }
423    fn spawn_inner_with(
424        mut self,
425        detached: bool,
426        fail_after_spawn: bool,
427        captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
428        in_job_fallback: bool,
429    ) -> io::Result<OwnedChild> {
430        if detached && self.caller_job.is_some() {
431            return Err(io::Error::new(
432                io::ErrorKind::InvalidInput,
433                "detached launch cannot join a caller job",
434            ));
435        }
436        if detached && self.kill_on_parent_exit {
437            return Err(io::Error::new(
438                io::ErrorKind::InvalidInput,
439                "kill_on_parent_exit cannot combine with a detached launch",
440            ));
441        }
442        #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
443        if self.kill_on_parent_exit {
444            return Err(io::Error::new(
445                io::ErrorKind::Unsupported,
446                "kill_on_parent_exit is supported on Linux and macOS only",
447            ));
448        }
449        #[cfg(unix)]
450        {
451            use std::os::unix::process::CommandExt;
452            let session = detached || self.unix_containment == UnixContainment::Session;
453            let nice_floor = self.process_priority.nice_floor();
454            #[cfg(target_os = "macos")]
455            let utility_qos = self.process_priority == ProcessPriority::Efficient;
456            let inheritance = self
457                .inherited_handles
458                .take()
459                .map(crate::process_unix::Inheritance::new)
460                .transpose()?;
461            // The supervisor's pid, read here and compared in the child (see `die_with_parent`).
462            #[cfg(target_os = "linux")]
463            let supervisor = self
464                .kill_on_parent_exit
465                // SAFETY: getpid has no preconditions.
466                .then(|| unsafe { libc::getpid() });
467            // Only async-signal-safe libc calls run between fork and exec.
468            unsafe {
469                self.command.pre_exec(move || {
470                    let result = if session {
471                        libc::setsid()
472                    } else {
473                        libc::setpgid(0, 0)
474                    };
475                    if result < 0 {
476                        return Err(io::Error::last_os_error());
477                    }
478                    if let Some(floor) = nice_floor {
479                        crate::process_unix::raise_nice_to(floor)?;
480                    }
481                    #[cfg(target_os = "macos")]
482                    if utility_qos {
483                        crate::process_unix::set_utility_qos();
484                    }
485                    if let Some(inheritance) = &inheritance {
486                        inheritance.apply()?;
487                    }
488                    #[cfg(target_os = "linux")]
489                    if let Some(supervisor) = supervisor {
490                        crate::process_unix::die_with_parent(supervisor)?;
491                    }
492                    Ok(())
493                });
494            }
495        }
496        #[cfg(windows)]
497        crate::process_windows::configure(
498            &mut self.command,
499            detached,
500            self.windows_hidden,
501            self.process_priority,
502        );
503        #[cfg(unix)]
504        let _ = (
505            self.windows_hidden,
506            self.windows_breakaway,
507            self.job_assignment,
508        );
509        #[cfg(windows)]
510        let _ = (self.unix_containment, self.kill_on_parent_exit);
511        let spawned_at = SystemTime::now();
512        let hold_suspended = self.hold_suspended && !detached;
513        #[cfg(unix)]
514        let _ = hold_suspended;
515        #[cfg(unix)]
516        let child = {
517            let mut child = crate::unix_child::UnixChild::new(self.command.spawn()?);
518            // A child whose pid cannot be tied to it is stopped here, while the pid is
519            // certainly still its own: no later signal could be proven to reach it.
520            child.require_identity()?;
521            // macOS: the tie is a watchdog armed while the pid is still certainly ours. A
522            // child that could not be tied is stopped, never handed out untied.
523            #[cfg(target_os = "macos")]
524            if self.kill_on_parent_exit {
525                if let Err(error) = crate::process_unix::arm_parent_death_watch(child.id()) {
526                    let _ = child.signal(SignalTarget::Group, libc::SIGKILL);
527                    let _ = child.kill();
528                    let _ = child.wait();
529                    return Err(error);
530                }
531            }
532            child
533        };
534        #[cfg(windows)]
535        let (child, stayed_in_parent_job) = match self.inherited_handles.as_deref() {
536            None => {
537                // An ordinary spawn inherits every inheritable handle. Held so it never runs
538                // while an allowlisted spawn has private handles raised.
539                let _spawning = crate::spawn_windows::spawn_guard();
540                match self.command.spawn() {
541                    Ok(child) => (PlatformChild::from(child), false),
542                    // The caller's job denied breakaway: one retry, detached but inside it.
543                    Err(error)
544                        if detached
545                            && in_job_fallback
546                            && crate::process_windows::is_breakaway_denied(&error) =>
547                    {
548                        crate::process_windows::configure_in_job_detached(
549                            &mut self.command,
550                            self.process_priority,
551                        );
552                        (PlatformChild::from(self.command.spawn()?), true)
553                    }
554                    Err(error) => return Err(error),
555                }
556            }
557            Some(handles) => {
558                let config = self.windows_spawn_config.take().ok_or_else(|| {
559                    io::Error::new(io::ErrorKind::InvalidInput,
560                        "allowlisted Windows spawn requires windows_spawn_config for opaque Command settings")
561                })?;
562                crate::spawn_windows::spawn(
563                    &self.command,
564                    config,
565                    handles,
566                    detached,
567                    self.windows_hidden,
568                    self.process_priority,
569                    self.env_cleared,
570                    self.partial_spawn_cleanup_timeout,
571                    in_job_fallback,
572                )?
573            }
574        };
575        #[cfg(not(windows))]
576        let _ = in_job_fallback;
577        #[cfg(not(windows))]
578        let stayed_in_parent_job = false;
579        let pid = child.id();
580        if let Some(captured) = captured_pid {
581            captured.store(pid, std::sync::atomic::Ordering::SeqCst);
582        }
583        let child = Arc::new(Mutex::new(child));
584        let handle = match ProcessHandle::new(&child, spawned_at) {
585            Ok(handle) => handle,
586            Err(error) => {
587                cleanup_direct(child, self.partial_spawn_cleanup_timeout);
588                return Err(error);
589            }
590        };
591        let mut partial = PartialChild {
592            child: Some(OwnedChild {
593                child,
594                handle,
595                pid,
596                detached,
597                terminate_on_drop: !detached,
598                drop_reap_timeout: DEFAULT_DROP_REAP_TIMEOUT,
599                tree_terminated: false,
600                assignment_failures: Vec::new(),
601                suspended: std::sync::atomic::AtomicBool::new(cfg!(windows) && hold_suspended),
602                stayed_in_parent_job,
603                #[cfg(windows)]
604                job: None,
605            }),
606            timeout: self.partial_spawn_cleanup_timeout,
607        };
608        #[cfg(windows)]
609        if !detached {
610            let owned = partial.child.as_mut().expect("partial child is present");
611            crate::process_windows::assign_jobs(
612                &owned.handle,
613                self.caller_job.take(),
614                self.job_assignment,
615                self.windows_breakaway,
616                &mut owned.job,
617                &mut owned.assignment_failures,
618            )?;
619        }
620        #[cfg(windows)]
621        {
622            let owned = partial.child.as_ref().expect("partial child is present");
623            if self.process_priority == ProcessPriority::Efficient {
624                // Best effort: EcoQoS is a hint and older Windows builds lack it.
625                let _ = crate::process_windows::apply_efficiency(&*owned.child());
626            }
627            if !hold_suspended {
628                owned.child().resume(detached)?;
629            }
630        }
631        if fail_after_spawn {
632            return Err(io::Error::other("injected post-spawn wrapping failure"));
633        }
634        let owned = partial.child.take().expect("partial child is present");
635        if !detached {
636            // Detached children are deliberately not owned: they outlive their handle.
637            #[cfg(unix)]
638            crate::registry::register(&owned.child, owned.pid);
639            #[cfg(windows)]
640            crate::registry::register(
641                &owned.child,
642                &owned.handle,
643                owned
644                    .job
645                    .as_ref()
646                    .map_or_else(std::sync::Weak::new, std::sync::Arc::downgrade),
647                owned.pid,
648            );
649        }
650        Ok(owned)
651    }
652}
653
654/// The error source returned (inside an [`io::Error`] of kind `PermissionDenied`) by
655/// [`OwnedCommand::spawn_uncontained_detached_strict`] on Windows when the caller's job object
656/// denies `CREATE_BREAKAWAY_FROM_JOB`. The OS reports this as `ERROR_ACCESS_DENIED`, which this
657/// crate cannot tell apart from other access-denied launch failures; a job that forbids
658/// breakaway is the cause this crate expects.
659#[derive(Clone, Copy, Debug, Eq, PartialEq)]
660pub struct BreakawayDenied;
661
662impl std::fmt::Display for BreakawayDenied {
663    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
664        f.write_str(
665            "detached launch denied: the caller's job object does not allow breakaway \
666             (ERROR_ACCESS_DENIED); no fallback was attempted",
667        )
668    }
669}
670
671impl std::error::Error for BreakawayDenied {}
672
673#[cfg(windows)]
674fn is_denied_breakaway(error: &io::Error) -> bool {
675    crate::process_windows::is_breakaway_denied(error)
676}
677#[cfg(not(windows))]
678fn is_denied_breakaway(_error: &io::Error) -> bool {
679    false
680}
681
682/// How long `Drop` waits for a killed leader to be reaped before handing the reap to a
683/// background thread.
684pub const DEFAULT_DROP_REAP_TIMEOUT: Duration = Duration::from_secs(2);
685
686pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
687    "PATH",
688    "HOME",
689    "USER",
690    "LOGNAME",
691    "LANG",
692    "LC_ALL",
693    "TMPDIR",
694    "TEMP",
695    "TMP",
696    "SystemRoot",
697    "SYSTEMROOT",
698    "USERPROFILE",
699    "APPDATA",
700    "LOCALAPPDATA",
701    "ProgramData",
702    "COMSPEC",
703    "PATHEXT",
704    "XDG_DATA_HOME",
705    "XDG_RUNTIME_DIR",
706];
707
708#[derive(Debug)]
709pub struct OwnedChild {
710    child: Arc<Mutex<PlatformChild>>,
711    handle: ProcessHandle,
712    // Captured before any wait can reap the direct child.
713    pid: u32,
714    detached: bool,
715    terminate_on_drop: bool,
716    drop_reap_timeout: Duration,
717    tree_terminated: bool,
718    assignment_failures: Vec<JobAssignmentFailure>,
719    // True from `spawn_suspended` (Windows) until `resume` releases the child.
720    suspended: std::sync::atomic::AtomicBool,
721    // Windows detached launch only: breakaway was denied and the child runs inside the parent's job.
722    stayed_in_parent_job: bool,
723    // Shared so the owned-children registry can reach the job without keeping it open
724    // (it holds a `Weak`): the kill-on-close job still closes with this child.
725    #[cfg(windows)]
726    job: Option<std::sync::Arc<crate::process_windows::KillOnCloseJob>>,
727}
728/// The result of [`OwnedChild::terminate_tree_deep`].
729#[derive(Clone, Debug, Eq, PartialEq)]
730pub struct TreeTermination {
731    /// The leader's exit status.
732    pub status: ExitStatus,
733    /// What happened to the descendants recorded before the leader was stopped.
734    pub descendants: crate::SweepReport,
735}
736#[derive(Clone, Copy, Debug, Eq, PartialEq)]
737pub enum ReapOutcome {
738    Exited(ExitStatus),
739    TimedOut,
740}
741#[derive(Clone, Copy, Debug, Eq, PartialEq)]
742pub enum WaitOutcome {
743    Exited(ExitStatus),
744    Terminated(ExitStatus),
745}
746
747impl OwnedChild {
748    pub fn id(&self) -> u32 {
749        self.pid
750    }
751    /// True only for a Windows [`OwnedCommand::spawn_uncontained_detached`] child that could
752    /// not break away because the caller's job object denied it, so it was launched detached
753    /// but still inside that job (the job's limits, such as kill-on-close, still reach it).
754    /// False for every other child, including every Unix child and any child that broke away.
755    pub fn stayed_in_parent_job(&self) -> bool {
756        self.stayed_in_parent_job
757    }
758    /// Borrow child state, including stdio, wait and try_wait. Windows uses a
759    /// native view with std pipe types for both spawn paths; Unix uses `UnixChild`, which
760    /// has the same members and does not expose the std `Child`. Release this guard
761    /// before another accessor or exit reader; it holds the shared reap lock.
762    ///
763    /// Unix: a `wait` or `try_wait` through the guard that reaps the leader permanently
764    /// ends process-group signalling for this child, exactly like `OwnedChild::wait`.
765    ///
766    /// Unix: `wait` through the guard holds the shared lock until the child has exited, because
767    /// the guard is the lock. A bounded wait on a `ProcessHandle` clone, `kill`,
768    /// `terminate_tree` and `Drop` on other threads queue behind it and can exceed their own
769    /// deadlines, as with `std::process::Child::wait` through this accessor in 0.3.3. Wait
770    /// through `OwnedChild::wait`/`wait_timeout` or a `ProcessHandle` when anything else must
771    /// stay live meanwhile. (Windows: `WindowsChild::wait` through the guard blocks in
772    /// `WaitForSingleObject` while holding the lock, as in 0.3.3.)
773    pub fn child(&self) -> MutexGuard<'_, PlatformChild> {
774        lock_child(&self.child)
775    }
776    /// Release a child created by [`OwnedCommand::spawn_suspended`]. It is already in its Job
777    /// Object; this lets it run. Returns `Ok` and does nothing for a child that is not held
778    /// (an ordinary spawn, a child already resumed, and every Unix child). On error the child
779    /// stays held and `resume` can be retried.
780    pub fn resume(&self) -> io::Result<()> {
781        if self
782            .suspended
783            .swap(false, std::sync::atomic::Ordering::SeqCst)
784        {
785            #[cfg(windows)]
786            if let Err(error) = self.child().resume(self.detached) {
787                self.suspended
788                    .store(true, std::sync::atomic::Ordering::SeqCst);
789                return Err(error);
790            }
791        }
792        Ok(())
793    }
794    /// True while a child from [`OwnedCommand::spawn_suspended`] has not been resumed.
795    pub fn is_suspended(&self) -> bool {
796        self.suspended.load(std::sync::atomic::Ordering::SeqCst)
797    }
798    /// Runs `act` with the leader's pid while the leader is proven to be this child and
799    /// unreaped, holding the shared lock so no reap can fall between the proof and `act`.
800    /// Returns `Ok(None)` without calling `act` once the leader has been reaped or its pid no
801    /// longer provably names it. For code that must signal by pid (a descendant sweep) and
802    /// would otherwise act on a number that may have been recycled. The lock is held for as
803    /// long as `act` runs (that is what keeps a reap out), so every other thread's wait,
804    /// including a bounded one, is delayed by that long: keep `act` short, and never call back
805    /// into this child from it. Windows has no recyclable-pid hazard here and always runs it.
806    pub fn while_unreaped<R>(&self, act: impl FnOnce(u32) -> R) -> io::Result<Option<R>> {
807        #[cfg(unix)]
808        {
809            let mut child = lock_child(&self.child);
810            if child.leader_state()? == LeaderState::Reaped {
811                return Ok(None);
812            }
813            Ok(Some(act(self.pid)))
814        }
815        #[cfg(windows)]
816        Ok(Some(act(self.pid)))
817    }
818    pub fn take_stdin(&mut self) -> Option<ChildStdin> {
819        self.child().stdin.take()
820    }
821    pub fn take_stdout(&mut self) -> Option<ChildStdout> {
822        self.child().stdout.take()
823    }
824    pub fn take_stderr(&mut self) -> Option<ChildStderr> {
825        self.child().stderr.take()
826    }
827    pub fn job_assignment_failures(&self) -> &[JobAssignmentFailure] {
828        &self.assignment_failures
829    }
830    /// An independent exit reader. A monitor thread that only needs to learn that the leader
831    /// exited should call [`ProcessHandle::wait_exited`], which does not reap; `wait` reaps, and
832    /// on Unix that ends the owner's ability to signal descendants that outlive the leader.
833    pub fn process_handle(&self) -> io::Result<ProcessHandle> {
834        self.handle.try_clone()
835    }
836    pub fn creation_time(&self) -> Option<SystemTime> {
837        self.handle.creation_time()
838    }
839    #[cfg(windows)]
840    pub fn creation_time_ticks(&self) -> Option<u64> {
841        self.handle.creation_time_ticks()
842    }
843    pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
844        // Through the handle so the reap also seals the process group's state.
845        self.handle.try_wait()
846    }
847    // Preserve std Child::wait's stdin-close behavior. ProcessHandle performs
848    // native blocking waits & keeps Unix reap results in std's shared cache.
849    pub fn wait(&mut self) -> io::Result<ExitStatus> {
850        drop(self.child().stdin.take());
851        self.handle.wait()
852    }
853    pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
854        self.handle.wait_timeout(timeout)
855    }
856    /// Asks the tree to stop, waits up to `grace` for the leader, then forces the tree down.
857    ///
858    /// Unix: TERM goes to the group while the leader is still unreaped. If the leader exits
859    /// within `grace` the result is `Exited`, but descendants that outlived it (or ignored
860    /// TERM) are killed before the leader is reaped, while it still pins the group id. If it
861    /// does not exit in time the whole group is killed and the result is `Terminated`.
862    ///
863    /// Windows: if the leader exits within `grace` the result is `Exited` with its status, but
864    /// the retained job is terminated first, so descendants that outlived it do not survive.
865    pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
866        self.wait_or_kill_inner(grace, true)
867    }
868    /// Like [`wait_or_kill`](Self::wait_or_kill) but without the opening TERM on Unix: the
869    /// leader gets the whole `grace` to exit on its own (for example after its stdin was
870    /// closed) before the tree is killed. A leader that exits in time is never signalled, so
871    /// its status is its own; descendants are still killed before the reap. Windows has no
872    /// TERM step, so it behaves exactly like `wait_or_kill`.
873    pub fn wait_then_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
874        self.wait_or_kill_inner(grace, false)
875    }
876    fn wait_or_kill_inner(&mut self, grace: Duration, term_first: bool) -> io::Result<WaitOutcome> {
877        #[cfg(not(unix))]
878        let _ = term_first;
879        #[cfg(unix)]
880        {
881            if term_first && !self.tree_terminated {
882                self.signal(libc::SIGTERM)?;
883            }
884            // Looks without reaping until the leader has exited or the grace has passed.
885            if self.handle.wait_exited_timeout(grace)? {
886                // Exited, not reaped yet: the zombie still pins the group id, so the
887                // stragglers can be killed safely, and only then reaped. (If a handle clone
888                // reaped it, the signal is a no-op, the cached status is returned and group
889                // signalling is over.)
890                self.force_kill(1)?;
891                return self.wait().map(WaitOutcome::Exited);
892            }
893            self.terminate_tree().map(WaitOutcome::Terminated)
894        }
895        #[cfg(windows)]
896        {
897            if let Some(status) = self.wait_timeout(grace)? {
898                // The leader left, but descendants may still be in the retained job (and hold
899                // the pipes). Bring the job down before reporting; the leader's own status,
900                // already collected, is what the caller gets.
901                self.force_kill(1)?;
902                return Ok(WaitOutcome::Exited(status));
903            }
904            self.terminate_tree().map(WaitOutcome::Terminated)
905        }
906    }
907    /// Immediate forced termination, preserving the existing default.
908    /// Windows: the retained job is signalled even after the parent was reaped.
909    /// Unix: the group is signalled only while the leader is unreaped (see the type
910    /// documentation); once any path has reaped it, only the reap result is returned.
911    pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
912        self.force_kill(1)?;
913        self.wait()
914    }
915    /// Unix: TERM now, tree-wide grace, then KILL, all while the leader is unreaped (an
916    /// exited leader stays unreaped until this call reaps it, so descendants still receive
917    /// the KILL). If another path already reaped the leader, nothing is signalled.
918    /// Windows: immediately terminate retained job with supplied exit code.
919    pub fn terminate_tree_with_options(
920        &mut self,
921        options: TerminationOptions,
922    ) -> io::Result<ExitStatus> {
923        self.start_termination(options)?;
924        self.wait()
925    }
926    pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
927        self.terminate_tree_bounded_with_options(timeout, TerminationOptions::default())
928    }
929    /// timeout covers grace plus reap; grace is clipped to timeout.
930    pub fn terminate_tree_bounded_with_options(
931        &mut self,
932        timeout: Duration,
933        mut options: TerminationOptions,
934    ) -> io::Result<ReapOutcome> {
935        let started = Instant::now();
936        options.grace = options.grace.min(timeout);
937        self.start_termination(options)?;
938        Ok(
939            match self.wait_timeout(timeout.saturating_sub(started.elapsed()))? {
940                Some(status) => ReapOutcome::Exited(status),
941                None => ReapOutcome::TimedOut,
942            },
943        )
944    }
945    /// On-demand termination of the whole tree, for a child that is not terminated on drop (a
946    /// [`spawn_uncontained_detached`](OwnedCommand::spawn_uncontained_detached) child whose
947    /// helpers should die now) as well as for an ordinary one. Unlike
948    /// [`terminate_tree_with_options`](Self::terminate_tree_with_options), a detached child's
949    /// descendants are not left alone:
950    ///
951    /// 1. While the leader is proven to be this child, the identities of its descendants are
952    ///    recorded (parent links; descendants that already outlived a parent that exited are
953    ///    not findable, as for [`descendant_identities`](Self::descendant_identities)).
954    /// 2. Unix: `SIGTERM` to the leader's whole process group (a detached child leads its own
955    ///    session, so that group is its own), up to `options.grace` for the group to leave,
956    ///    then `SIGKILL` to the group, all before the leader is reaped, so the group id cannot
957    ///    be recycled. Windows: the retained job if there is one (ordinary children), and the
958    ///    leader (`options.windows_exit_code`).
959    /// 3. The leader is reaped.
960    /// 4. The recorded descendants that are still the same process (pid and start time; a
961    ///    recycled pid is refused, never signalled) are swept, leaves first, with
962    ///    `options.grace`. This is what reaches a helper that left the group (a daemon that
963    ///    called `setsid`) and, on Windows, the descendants of a detached child.
964    ///
965    /// If another path already reaped the leader on Unix, nothing is signalled and the cached
966    /// status is returned with an empty sweep. No terminate-on-drop is implied: calling this
967    /// or not does not change what `Drop` does.
968    pub fn terminate_tree_deep(
969        &mut self,
970        options: TerminationOptions,
971    ) -> io::Result<TreeTermination> {
972        let mut found = self.descendant_identities()?;
973        found.reverse(); // leaves first
974        #[cfg(unix)]
975        if !self.tree_terminated {
976            signal_shared(&self.child, SignalTarget::Group, libc::SIGTERM)?;
977            await_tree_shared(&self.child, &self.handle, options.grace, true)?;
978            signal_shared(&self.child, SignalTarget::Group, libc::SIGKILL)?;
979            self.tree_terminated = true;
980        }
981        #[cfg(windows)]
982        self.force_kill(options.windows_exit_code)?;
983        let status = self.wait()?;
984        let descendants = crate::sweep(&found, options.grace)?;
985        Ok(TreeTermination {
986            status,
987            descendants,
988        })
989    }
990    fn start_termination(&mut self, options: TerminationOptions) -> io::Result<()> {
991        #[cfg(unix)]
992        if !self.tree_terminated {
993            self.signal(libc::SIGTERM)?;
994            // Stops waiting as soon as nothing but the exited, unreaped leader is left (or the
995            // leader was reaped elsewhere, which ends signalling). The leader is not reaped
996            // here: it must keep pinning the group id for the KILL below.
997            self.await_tree(options.grace)?;
998        }
999        self.force_kill(options.windows_exit_code)
1000    }
1001    /// Waits, for at most `grace`, until the leader has exited and every other member of its
1002    /// group has too. The leader is observed without reaping it; then the group is listed
1003    /// every [`POLL_INTERVAL`](crate::exit_watch::POLL_INTERVAL) (the published 0.3.3 interval)
1004    /// until nothing but the leader is left. A group that cannot be listed is waited out for
1005    /// the rest of the grace in one timed sleep. Each listing takes the child lock briefly and
1006    /// releases it before the thread sleeps. Bounded by `grace` in every case.
1007    #[cfg(unix)]
1008    fn await_tree(&self, grace: Duration) -> io::Result<()> {
1009        // A detached child owns only its direct pid.
1010        await_tree_shared(&self.child, &self.handle, grace, !self.detached)
1011    }
1012    /// Sends `signal` to the group (or, for a detached child, to the leader alone) while the
1013    /// leader is unreaped and proven. Takes the child lock for each attempt and releases it
1014    /// between attempts.
1015    #[cfg(unix)]
1016    fn signal(&self, signal: i32) -> io::Result<()> {
1017        // Detached children only own their direct pid. Under the child's lock, and only while
1018        // the leader is unreaped, so the id cannot have been recycled.
1019        let target = if self.detached {
1020            SignalTarget::Leader
1021        } else {
1022            SignalTarget::Group
1023        };
1024        signal_shared(&self.child, target, signal).map(|_| ())
1025    }
1026    fn force_kill(&mut self, code: u32) -> io::Result<()> {
1027        if self.tree_terminated {
1028            return Ok(());
1029        }
1030        #[cfg(unix)]
1031        {
1032            let _ = code;
1033            self.signal(libc::SIGKILL)?;
1034        }
1035        #[cfg(windows)]
1036        crate::process_windows::terminate(&self.handle, self.job.as_deref(), code)?;
1037        self.tree_terminated = true;
1038        Ok(())
1039    }
1040}
1041impl OwnedChild {
1042    /// How long dropping this child waits for the killed leader to be reaped (default
1043    /// [`DEFAULT_DROP_REAP_TIMEOUT`]). `Drop` sends the kill at once and never waits longer than
1044    /// this: a leader that has not been reaped by then (it is in an uninterruptible state, for
1045    /// instance) is reaped later by a background thread on Unix, and on Windows the process
1046    /// handle is closed. `Duration::ZERO` means "do not wait". Does not affect
1047    /// [`terminate_tree`](Self::terminate_tree), which still waits for the reap.
1048    pub fn set_drop_reap_timeout(&mut self, timeout: Duration) -> &mut Self {
1049        self.drop_reap_timeout = timeout;
1050        self
1051    }
1052    /// The bound `Drop` applies to the reap.
1053    pub fn drop_reap_timeout(&self) -> Duration {
1054        self.drop_reap_timeout
1055    }
1056    /// Reaps the leader on a thread of its own, so an unbounded wait never blocks the caller.
1057    fn reap_in_background(&self) {
1058        #[cfg(unix)]
1059        if let Ok(reader) = self.handle.try_clone() {
1060            let _ = thread::Builder::new()
1061                .name("rightkit-drop-reap".into())
1062                .spawn(move || {
1063                    let _ = reader.wait();
1064                });
1065        }
1066    }
1067    /// Identities of this child's descendants (see [`crate::AdoptedIdentity::descendants`]), taken
1068    /// while the leader is proven to be this child. Empty once the leader has been reaped:
1069    /// its children are re-parented then and can no longer be found by their parent.
1070    pub fn descendant_identities(&self) -> io::Result<Vec<crate::AdoptedIdentity>> {
1071        let root = self.while_unreaped(|pid| match NonZeroU32::new(pid) {
1072            Some(pid) => crate::AdoptedIdentity::capture(pid),
1073            None => Ok(None),
1074        })?;
1075        match root {
1076            Some(Ok(Some(root))) => root.descendants(),
1077            Some(Err(error)) => Err(error),
1078            _ => Ok(Vec::new()),
1079        }
1080    }
1081}
1082impl Drop for OwnedChild {
1083    fn drop(&mut self) {
1084        // No longer owned: leave the registry before anything below can take long.
1085        crate::registry::forget(&self.child);
1086        if self.terminate_on_drop {
1087            // Bounded: the kill is sent at once, the reap is awaited for at most
1088            // `drop_reap_timeout`, and a child that has not been reaped by then is reaped by
1089            // a background thread. Dropping never waits on a process that will not die.
1090            if self.force_kill(1).is_ok() {
1091                drop(self.child().stdin.take());
1092                if !matches!(
1093                    self.handle.wait_timeout(self.drop_reap_timeout),
1094                    Ok(Some(_))
1095                ) {
1096                    self.reap_in_background();
1097                }
1098            }
1099        } else if self.detached {
1100            #[cfg(unix)]
1101            {
1102                // Shared launch still needs eventual reaping. Close owner pipes
1103                // now; the reaper thread makes non-blocking looks and releases the child
1104                // lock between them, so exit readers and kills are not held behind it.
1105                {
1106                    let mut child = self.child();
1107                    drop(child.stdin.take());
1108                    drop(child.stdout.take());
1109                    drop(child.stderr.take());
1110                }
1111                if let Ok(reader) = self.handle.try_clone() {
1112                    let _ = thread::Builder::new()
1113                        .name("rightkit-detached-reap".into())
1114                        .spawn(move || {
1115                            let _ = reader.wait();
1116                        });
1117                }
1118            }
1119        }
1120    }
1121}
1122
1123struct PartialChild {
1124    child: Option<OwnedChild>,
1125    timeout: Option<Duration>,
1126}
1127impl Drop for PartialChild {
1128    fn drop(&mut self) {
1129        if let Some(mut child) = self.child.take() {
1130            // Disable unbounded normal Drop before bounded cleanup.
1131            child.terminate_on_drop = false;
1132            let _ = child.force_kill(1);
1133            cleanup_direct(Arc::clone(&child.child), self.timeout);
1134        }
1135    }
1136}
1137/// How long a group signal that macOS refused with `EPERM` (the last members are mid-exit) is
1138/// retried before the refusal is reported.
1139#[cfg(unix)]
1140const MID_EXIT_RETRY: Duration = Duration::from_millis(300);
1141
1142/// One signal to a shared child's group or leader (see `OwnedChild::signal`), retrying for a
1143/// bounded time while macOS reports the last members mid-exit. The lock is taken for each
1144/// attempt and released between attempts. Returns the final outcome.
1145#[cfg(unix)]
1146pub(crate) fn signal_shared(
1147    shared: &Mutex<crate::unix_child::UnixChild>,
1148    target: SignalTarget,
1149    signal: i32,
1150) -> io::Result<SignalOutcome> {
1151    let started = Instant::now();
1152    loop {
1153        let (outcome, watch) = {
1154            let mut child = lock_child(shared);
1155            (child.signal(target, signal)?, child.exit_watch())
1156        };
1157        if outcome != SignalOutcome::MidExit {
1158            return Ok(outcome);
1159        }
1160        // macOS refused the group signal while its last members are mid-exit. The group is
1161        // gone once the exit completes: look again, for a bounded time, without the lock.
1162        let remaining = MID_EXIT_RETRY.saturating_sub(started.elapsed());
1163        if remaining.is_zero() {
1164            return Err(io::Error::from_raw_os_error(libc::EPERM));
1165        }
1166        watch.pause(remaining);
1167    }
1168}
1169
1170/// `OwnedChild::await_tree` over a shared child. `whole_group` false waits only for the leader.
1171#[cfg(unix)]
1172pub(crate) fn await_tree_shared(
1173    shared: &Mutex<crate::unix_child::UnixChild>,
1174    handle: &ProcessHandle,
1175    grace: Duration,
1176    whole_group: bool,
1177) -> io::Result<()> {
1178    use crate::{exit_watch::POLL_INTERVAL, unix_child::Members};
1179    let Some(deadline) = Instant::now().checked_add(grace) else {
1180        return Ok(());
1181    };
1182    let left = |deadline: Instant| deadline.saturating_duration_since(Instant::now());
1183    if !handle.wait_exited_timeout(left(deadline))? {
1184        return Ok(()); // the leader outlived the grace
1185    }
1186    if !whole_group {
1187        return Ok(());
1188    }
1189    loop {
1190        match lock_child(shared).group_other_members()? {
1191            Members::Pids(pids) if pids.is_empty() => return Ok(()),
1192            Members::Pids(_) => {}
1193            Members::Unknown => return wait_out(left(deadline)),
1194        }
1195        let remaining = left(deadline);
1196        if remaining.is_zero() {
1197            return Ok(()); // the grace is over
1198        }
1199        thread::sleep(POLL_INTERVAL.min(remaining));
1200    }
1201}
1202
1203/// The rest of a grace for a group that cannot be listed. There is nothing to look at, so this
1204/// is one timed sleep, not a loop.
1205#[cfg(unix)]
1206fn wait_out(remaining: Duration) -> io::Result<()> {
1207    thread::sleep(remaining);
1208    Ok(())
1209}
1210/// Stops and reaps a child whose spawn failed after the process existed. Every caller passes
1211/// an `Arc` that nothing but the spawning thread holds (the partial `OwnedChild`, from which no
1212/// `ProcessHandle` was cloned and which no caller has seen, or a child that was never wrapped),
1213/// so no other thread can contend for its lock. That is why the two blocking-under-lock waits
1214/// below, the unbounded one and the `rightkit-partial-reap` thread, cannot hold a deadline-
1215/// bearing caller back; both are what 0.3.3 did with `std::process::Child::wait`.
1216pub(crate) fn cleanup_direct(child: Arc<Mutex<PlatformChild>>, timeout: Option<Duration>) {
1217    let _ = lock_child(&child).kill();
1218    let Some(timeout) = timeout else {
1219        // Unbounded by default, as before. Exclusive child: see above.
1220        let _ = lock_child(&child).wait();
1221        return;
1222    };
1223    #[cfg(unix)]
1224    {
1225        // Non-blocking looks, each under the lock and released at once, until the child is
1226        // reaped or the timeout passes. A killed child is gone within one hint or interval.
1227        let watch = lock_child(&child).exit_watch();
1228        let deadline = Instant::now().checked_add(timeout);
1229        if matches!(
1230            watch.wait_for(deadline, || lock_child(&child).try_wait()),
1231            Ok(Some(_))
1232        ) {
1233            return;
1234        }
1235    }
1236    #[cfg(windows)]
1237    {
1238        let started = Instant::now();
1239        loop {
1240            match lock_child(&child).try_wait() {
1241                Ok(Some(_)) => return,
1242                Err(_) => break,
1243                Ok(None) => {}
1244            }
1245            if started.elapsed() >= timeout {
1246                break;
1247            }
1248            thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
1249        }
1250    }
1251    // std Child does not reap on Drop. Transfer timed-out children to a waiter. This thread
1252    // holds the child's lock for the whole wait and is the only holder of the `Arc` (see
1253    // above), so nothing can queue behind it.
1254    let _ = thread::Builder::new()
1255        .name("rightkit-partial-reap".into())
1256        .spawn(move || {
1257            let _ = lock_child(&child).wait();
1258        });
1259}
1260
1261#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1262pub struct AdoptedProcess {
1263    pid: NonZeroU32,
1264}
1265impl AdoptedProcess {
1266    pub fn new(pid: NonZeroU32) -> Self {
1267        Self { pid }
1268    }
1269    pub fn id(&self) -> u32 {
1270        self.pid.get()
1271    }
1272    pub fn is_running(&self) -> io::Result<bool> {
1273        process_is_running(self.pid)
1274    }
1275    /// The identity (start time and executable) of the process behind this pid now, for
1276    /// identity-checked signalling through [`AdoptedIdentity`](crate::AdoptedIdentity).
1277    /// `Ok(None)` when no process has the pid. Capture it only when the pid is known to name
1278    /// the intended process.
1279    pub fn identity(&self) -> io::Result<Option<crate::AdoptedIdentity>> {
1280        crate::AdoptedIdentity::capture(self.pid)
1281    }
1282}
1283#[cfg(test)]
1284fn spawn_for_test(
1285    command: Command,
1286    captured_pid: Arc<std::sync::atomic::AtomicU32>,
1287) -> io::Result<OwnedChild> {
1288    OwnedCommand::from_command(command).spawn_inner(false, true, Some(captured_pid))
1289}
1290
1291#[cfg(windows)]
1292fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1293    use windows::Win32::{
1294        Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
1295        System::Threading::{
1296            OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
1297            PROCESS_SYNCHRONIZE,
1298        },
1299    };
1300
1301    let handle = unsafe {
1302        OpenProcess(
1303            PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
1304            false,
1305            pid.get(),
1306        )
1307    };
1308    let handle = match handle {
1309        Ok(handle) => handle,
1310        Err(error) => {
1311            if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
1312                return Ok(false);
1313            }
1314            return Err(io::Error::other(error.to_string()));
1315        }
1316    };
1317    let wait = unsafe { WaitForSingleObject(handle, 0) };
1318    let close = unsafe { CloseHandle(handle) };
1319    close.map_err(|error| io::Error::other(error.to_string()))?;
1320    match wait.0 {
1321        0 => Ok(false),
1322        value if value == WAIT_TIMEOUT.0 => Ok(true),
1323        _ => Err(io::Error::last_os_error()),
1324    }
1325}
1326
1327#[cfg(unix)]
1328fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
1329    let pid = i32::try_from(pid.get()).map_err(io::Error::other)?;
1330    loop {
1331        // SAFETY: signal 0 performs only existence and permission checks; nothing is sent.
1332        if unsafe { libc::kill(pid, 0) } == 0 {
1333            return Ok(true);
1334        }
1335        let error = io::Error::last_os_error();
1336        match error.raw_os_error() {
1337            Some(libc::EINTR) => continue,
1338            Some(libc::EPERM) => return Ok(true),
1339            Some(libc::ESRCH) => return Ok(false),
1340            _ => return Err(error),
1341        }
1342    }
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347    #[test]
1348    fn option_defaults_preserve_existing_behavior() {
1349        let command = super::OwnedCommand::new("unused");
1350        assert_eq!(command.job_assignment, super::JobAssignmentMode::Strict);
1351        assert_eq!(
1352            command.unix_containment,
1353            super::UnixContainment::ProcessGroup
1354        );
1355        assert_eq!(command.partial_spawn_cleanup_timeout, None);
1356        assert_eq!(
1357            super::TerminationOptions::default().grace,
1358            std::time::Duration::ZERO
1359        );
1360        assert_eq!(super::TerminationOptions::default().windows_exit_code, 1);
1361    }
1362
1363    #[cfg(not(windows))]
1364    #[test]
1365    fn new_and_from_command_share_spawn_settings() {
1366        use super::OwnedCommand;
1367        use std::process::Command;
1368        // Debug covers every field. Off Windows nothing differs between the constructors.
1369        let by_new = OwnedCommand::new("unused");
1370        let by_from = OwnedCommand::from_command(Command::new("unused"));
1371        assert_eq!(format!("{by_new:?}"), format!("{by_from:?}"));
1372    }
1373
1374    /// Only `new` knows its Command's stdio and environment are std defaults. A wrapped
1375    /// Command may carry settings the crate cannot read, so `from_command` must not invent a
1376    /// default config: the allowlisted spawn then refuses instead of widening inheritance.
1377    #[cfg(windows)]
1378    #[test]
1379    fn only_new_supplies_a_default_windows_spawn_config() {
1380        use super::OwnedCommand;
1381        use std::process::Command;
1382        let by_new = OwnedCommand::new("unused");
1383        let by_from = OwnedCommand::from_command(Command::new("unused"));
1384        assert!(by_new.windows_spawn_config.is_some() && by_new.windows_spawn_config_auto);
1385        assert!(by_from.windows_spawn_config.is_none() && !by_from.windows_spawn_config_auto);
1386    }
1387
1388    #[test]
1389    fn process_priority_defaults_to_normal_and_keeps_the_requested_value() {
1390        use super::{OwnedCommand, ProcessPriority};
1391        let mut command = OwnedCommand::new("unused");
1392        assert_eq!(command.process_priority, ProcessPriority::Normal);
1393        command.process_priority(ProcessPriority::BelowNormal);
1394        assert_eq!(command.process_priority, ProcessPriority::BelowNormal);
1395        command.process_priority(ProcessPriority::Idle);
1396        assert_eq!(command.process_priority, ProcessPriority::Idle);
1397    }
1398
1399    /// Normal adds no nice change, so the default Unix spawn is unchanged. Lower priorities
1400    /// map to the documented floors.
1401    #[cfg(unix)]
1402    #[test]
1403    fn process_priority_maps_to_nice_floors() {
1404        use super::ProcessPriority;
1405        assert_eq!(ProcessPriority::Normal.nice_floor(), None);
1406        assert_eq!(ProcessPriority::BelowNormal.nice_floor(), Some(10));
1407        assert_eq!(ProcessPriority::Idle.nice_floor(), Some(19));
1408        assert_eq!(ProcessPriority::Efficient.nice_floor(), Some(10));
1409    }
1410
1411    /// A real child at `BelowNormal` reports the class Windows was asked for.
1412    #[cfg(windows)]
1413    #[test]
1414    fn below_normal_child_has_below_normal_priority_class() {
1415        use super::{OwnedCommand, ProcessPriority};
1416        use std::process::Stdio;
1417        use windows::Win32::{
1418            Foundation::CloseHandle,
1419            System::Threading::{
1420                GetPriorityClass, OpenProcess, BELOW_NORMAL_PRIORITY_CLASS,
1421                PROCESS_QUERY_LIMITED_INFORMATION,
1422            },
1423        };
1424        let mut command = OwnedCommand::new("ping");
1425        command
1426            .command_mut()
1427            .args(["-n", "6", "127.0.0.1"])
1428            .stdin(Stdio::null())
1429            .stdout(Stdio::null())
1430            .stderr(Stdio::null());
1431        command.process_priority(ProcessPriority::BelowNormal);
1432        let mut child = command.spawn().expect("spawn ping");
1433        // SAFETY: the handle is opened for this live child, queried once and closed.
1434        let class = unsafe {
1435            let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, child.id())
1436                .expect("open the child");
1437            let class = GetPriorityClass(handle);
1438            let _ = CloseHandle(handle);
1439            class
1440        };
1441        let _ = child.terminate_tree();
1442        assert_eq!(class, BELOW_NORMAL_PRIORITY_CLASS.0);
1443    }
1444
1445    /// A real child at `Idle` or `BelowNormal` runs with a nice value at or above the floor.
1446    #[cfg(unix)]
1447    #[test]
1448    fn lowered_priority_child_runs_at_or_above_the_nice_floor() {
1449        use super::{OwnedCommand, ProcessPriority};
1450        use std::{io::Read, process::Stdio};
1451        for (priority, floor) in [
1452            (ProcessPriority::BelowNormal, 10),
1453            (ProcessPriority::Idle, 19),
1454        ] {
1455            let mut command = OwnedCommand::new("/bin/sh");
1456            command
1457                .command_mut()
1458                .args(["-c", "ps -o ni= -p $$"])
1459                .stdin(Stdio::null())
1460                .stdout(Stdio::piped())
1461                .stderr(Stdio::null());
1462            command.process_priority(priority);
1463            let mut child = command.spawn().expect("spawn sh");
1464            let mut text = String::new();
1465            child
1466                .take_stdout()
1467                .expect("piped stdout")
1468                .read_to_string(&mut text)
1469                .expect("read ps output");
1470            let _ = child.wait();
1471            let nice: i32 = text.trim().parse().expect("ps printed a nice value");
1472            assert!(nice >= floor, "nice {nice} below floor {floor}");
1473        }
1474    }
1475
1476    #[test]
1477    fn breakaway_is_off_by_default_explicit_by_default_opt_in_and_silent_is_separate() {
1478        use super::BreakawayMode;
1479        let mut command = super::OwnedCommand::new("unused");
1480        assert_eq!(command.windows_breakaway, BreakawayMode::None);
1481        command.windows_allow_breakaway(true);
1482        assert_eq!(command.windows_breakaway, BreakawayMode::Explicit);
1483        command.windows_allow_silent_breakaway(true);
1484        assert_eq!(command.windows_breakaway, BreakawayMode::Silent);
1485        command.windows_allow_silent_breakaway(false);
1486        assert_eq!(command.windows_breakaway, BreakawayMode::None);
1487    }
1488
1489    #[test]
1490    fn kill_on_parent_exit_is_opt_in() {
1491        let mut command = super::OwnedCommand::new("unused");
1492        assert!(!command.kill_on_parent_exit);
1493        command.kill_on_parent_exit(true);
1494        assert!(command.kill_on_parent_exit);
1495    }
1496
1497    #[test]
1498    fn breakaway_denied_is_a_typed_recoverable_error() {
1499        let error =
1500            std::io::Error::new(std::io::ErrorKind::PermissionDenied, super::BreakawayDenied);
1501        assert!(error
1502            .get_ref()
1503            .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1504            .is_some());
1505        assert!(super::BreakawayDenied.to_string().contains("breakaway"));
1506    }
1507
1508    #[test]
1509    fn a_strict_detached_spawn_passes_other_failures_through_untyped() {
1510        let command = super::OwnedCommand::new("rightkit-no-such-program-for-strict-detach");
1511        let Err(error) = command.spawn_uncontained_detached_strict() else {
1512            panic!("a missing program must not spawn");
1513        };
1514        assert!(error
1515            .get_ref()
1516            .and_then(|source| source.downcast_ref::<super::BreakawayDenied>())
1517            .is_none());
1518    }
1519
1520    #[test]
1521    fn a_strict_detached_spawn_still_rejects_kill_on_parent_exit() {
1522        let mut command = super::OwnedCommand::new("unused");
1523        command.kill_on_parent_exit(true);
1524        let Err(error) = command.spawn_uncontained_detached_strict() else {
1525            panic!("a detached spawn must not accept the parent-death tie");
1526        };
1527        assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1528    }
1529
1530    #[test]
1531    fn a_detached_spawn_rejects_kill_on_parent_exit() {
1532        // Rejected before anything is started, so the program never needs to exist.
1533        let mut command = super::OwnedCommand::new("unused");
1534        command.kill_on_parent_exit(true);
1535        let Err(error) = command.spawn_uncontained_detached() else {
1536            panic!("a detached spawn must not accept the parent-death tie");
1537        };
1538        assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1539    }
1540
1541    /// Hosts other than Linux and macOS have no parent-death primitive: the option fails closed,
1542    /// and the program is never started (it would create the marker file).
1543    #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
1544    #[test]
1545    fn a_host_without_a_parent_death_primitive_refuses_before_starting_anything() {
1546        let marker =
1547            std::env::temp_dir().join(format!("rightkit-process-untied-{}", std::process::id()));
1548        let _ = std::fs::remove_file(&marker);
1549        let mut command = super::OwnedCommand::new("/usr/bin/touch");
1550        command.command_mut().arg(&marker);
1551        command.kill_on_parent_exit(true);
1552        let Err(error) = command.spawn() else {
1553            panic!("kill_on_parent_exit must not run the child untied");
1554        };
1555        assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);
1556        std::thread::sleep(std::time::Duration::from_millis(200));
1557        assert!(!marker.exists(), "the program ran");
1558    }
1559
1560    #[test]
1561    fn option_builders_keep_every_requested_value() {
1562        use super::{JobAssignmentMode, OwnedCommand, TerminationOptions, UnixContainment};
1563        use std::time::Duration;
1564        let mut command = OwnedCommand::new("unused");
1565        command
1566            .job_assignment_mode(JobAssignmentMode::BestEffort)
1567            .unix_containment(UnixContainment::Session)
1568            .partial_spawn_cleanup_timeout(Duration::from_secs(5));
1569        assert_eq!(command.job_assignment, JobAssignmentMode::BestEffort);
1570        assert_eq!(command.unix_containment, UnixContainment::Session);
1571        assert_eq!(
1572            command.partial_spawn_cleanup_timeout,
1573            Some(Duration::from_secs(5))
1574        );
1575        let options = TerminationOptions::default()
1576            .with_grace(Duration::from_millis(100))
1577            .with_windows_exit_code(1067);
1578        assert_eq!(options.grace, Duration::from_millis(100));
1579        assert_eq!(options.windows_exit_code, 1067);
1580    }
1581
1582    #[test]
1583    fn zero_bound_partial_spawn_failure_still_reaps_the_child() {
1584        use std::{
1585            num::NonZeroU32,
1586            process::Command,
1587            sync::{
1588                atomic::{AtomicU32, Ordering},
1589                Arc,
1590            },
1591            thread,
1592            time::{Duration, Instant},
1593        };
1594        let command = if cfg!(windows) {
1595            let mut command = Command::new("powershell.exe");
1596            command.args([
1597                "-NoLogo",
1598                "-NoProfile",
1599                "-NonInteractive",
1600                "-Command",
1601                "Start-Sleep -Seconds 30",
1602            ]);
1603            command
1604        } else {
1605            let mut command = Command::new("/bin/sh");
1606            command.args(["-c", "exec sleep 30"]);
1607            command
1608        };
1609        let mut command = super::OwnedCommand::from_command(command);
1610        command.partial_spawn_cleanup_timeout(Duration::ZERO);
1611        let captured = Arc::new(AtomicU32::new(0));
1612        let started = Instant::now();
1613        assert!(command
1614            .spawn_inner(false, true, Some(Arc::clone(&captured)))
1615            .is_err());
1616        assert!(started.elapsed() < Duration::from_secs(3));
1617        let pid = captured.load(Ordering::SeqCst);
1618        assert_ne!(pid, 0);
1619        let process = super::AdoptedProcess::new(NonZeroU32::new(pid).unwrap());
1620        let started = Instant::now();
1621        while process.is_running().unwrap() {
1622            assert!(started.elapsed() < Duration::from_secs(5));
1623            thread::sleep(Duration::from_millis(5));
1624        }
1625    }
1626
1627    #[test]
1628    fn owned_commands_hide_their_console_window_by_default() {
1629        assert!(super::OwnedCommand::new("x").windows_hidden);
1630        assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
1631    }
1632
1633    /// A kill-on-close job standing in for an in-process host's own job object.
1634    #[cfg(windows)]
1635    fn host_job() -> std::os::windows::io::OwnedHandle {
1636        use std::os::windows::io::FromRawHandle;
1637        use windows::Win32::System::JobObjects::{
1638            CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
1639            JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
1640        };
1641        let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
1642        let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
1643        let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
1644        info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
1645        unsafe {
1646            SetInformationJobObject(
1647                job,
1648                JobObjectExtendedLimitInformation,
1649                &info as *const _ as *const _,
1650                std::mem::size_of_val(&info) as u32,
1651            )
1652        }
1653        .unwrap();
1654        job_handle
1655    }
1656
1657    #[cfg(windows)]
1658    fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
1659        use std::os::windows::io::AsRawHandle;
1660        #[link(name = "kernel32")]
1661        unsafe extern "system" {
1662            fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
1663            fn IsProcessInJob(
1664                process: *mut std::ffi::c_void,
1665                job: *mut std::ffi::c_void,
1666                result: *mut i32,
1667            ) -> i32;
1668            fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
1669        }
1670        unsafe {
1671            let process = OpenProcess(0x1000, 0, pid); // PROCESS_QUERY_LIMITED_INFORMATION
1672            assert!(!process.is_null());
1673            let mut inside = 0;
1674            assert_ne!(
1675                IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
1676                0
1677            );
1678            CloseHandle(process);
1679            inside != 0
1680        }
1681    }
1682
1683    #[cfg(windows)]
1684    #[test]
1685    fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
1686        use std::os::windows::io::AsHandle;
1687        use std::time::{Duration, Instant};
1688        let job = host_job();
1689        let sleeper = || {
1690            let mut command = super::OwnedCommand::new("powershell.exe");
1691            command.command_mut().args([
1692                "-NoLogo",
1693                "-NoProfile",
1694                "-NonInteractive",
1695                "-Command",
1696                "Start-Sleep -Seconds 60",
1697            ]);
1698            command.windows_job(job.as_handle()).unwrap();
1699            command.spawn().unwrap()
1700        };
1701        // The second child joins a job that already holds a process: binding must happen
1702        // before RightKit's own jobs, or Windows refuses it.
1703        let (first, second) = (sleeper(), sleeper());
1704        let pids = [first.id(), second.id()];
1705        assert!(
1706            pids.iter().all(|pid| in_job(*pid, &job)),
1707            "children must be in the caller's job"
1708        );
1709
1710        // The caller closes its job while the owned children are still alive: they die.
1711        drop(job);
1712        let deadline = Instant::now() + Duration::from_secs(5);
1713        for pid in pids {
1714            let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1715            while process.is_running().unwrap_or(false) {
1716                assert!(
1717                    Instant::now() < deadline,
1718                    "child {pid} survived its caller's job"
1719                );
1720                std::thread::sleep(Duration::from_millis(20));
1721            }
1722        }
1723        drop((first, second));
1724    }
1725
1726    #[test]
1727    fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
1728        use std::{
1729            process::Command,
1730            sync::{
1731                atomic::{AtomicU32, Ordering},
1732                Arc,
1733            },
1734            thread,
1735            time::{Duration, Instant},
1736        };
1737
1738        // A long-lived child that survives on its own if the partial-spawn cleanup fails.
1739        let command = if cfg!(windows) {
1740            // windowless: spawned through spawn_owned (CREATE_NO_WINDOW).
1741            let mut command = Command::new("powershell.exe");
1742            command.args([
1743                "-NoLogo",
1744                "-NoProfile",
1745                "-NonInteractive",
1746                "-Command",
1747                "Start-Sleep -Seconds 60",
1748            ]);
1749            command
1750        } else {
1751            // windowless: spawned through spawn_owned (CREATE_NO_WINDOW).
1752            let mut command = Command::new("sleep");
1753            command.arg("60");
1754            command
1755        };
1756        let spawned_pid = Arc::new(AtomicU32::new(0));
1757
1758        assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
1759        let pid = spawned_pid.load(Ordering::SeqCst);
1760        assert_ne!(pid, 0, "test must fail after the OS process was spawned");
1761        let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
1762        let deadline = Instant::now() + Duration::from_secs(5);
1763        while process.is_running().unwrap_or(false) {
1764            assert!(
1765                Instant::now() < deadline,
1766                "partially spawned process leaked"
1767            );
1768            thread::sleep(Duration::from_millis(20));
1769        }
1770    }
1771}