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