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