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