Skip to main content

rightkit_process/
owned.rs

1use crate::process_handle::{lock_child, ProcessHandle};
2use std::{
3    ffi::OsStr,
4    io,
5    num::NonZeroU32,
6    process::{ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus},
7    sync::{Arc, Mutex, MutexGuard},
8    thread,
9    time::{Duration, Instant, SystemTime},
10};
11#[cfg(unix)]
12type PlatformChild = std::process::Child;
13#[cfg(windows)]
14type PlatformChild = crate::spawn_windows::WindowsChild;
15
16#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
17pub enum JobAssignmentMode {
18    #[default]
19    Strict,
20    BestEffort,
21}
22
23#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
24pub enum UnixContainment {
25    #[default]
26    ProcessGroup,
27    Session,
28}
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub struct TerminationOptions {
32    /// Send TERM immediately on Unix, then KILL after this tree-wide grace.
33    pub grace: Duration,
34    /// Passed to TerminateJobObject/TerminateProcess on Windows.
35    pub windows_exit_code: u32,
36}
37impl Default for TerminationOptions {
38    fn default() -> Self {
39        Self {
40            grace: Duration::ZERO,
41            windows_exit_code: 1,
42        }
43    }
44}
45impl TerminationOptions {
46    pub fn with_grace(mut self, grace: Duration) -> Self {
47        self.grace = grace;
48        self
49    }
50    pub fn with_windows_exit_code(mut self, code: u32) -> Self {
51        self.windows_exit_code = code;
52        self
53    }
54}
55
56/// Every failed assignment in a best-effort Windows spawn is retained.
57#[derive(Debug)]
58pub struct JobAssignmentFailure {
59    pub stage: &'static str,
60    pub error: io::Error,
61}
62impl JobAssignmentFailure {
63    pub fn raw_os_error(&self) -> Option<i32> {
64        self.error.raw_os_error()
65    }
66}
67
68/// Borrowed OS handle/fd value; keep it valid until spawn returns.
69#[cfg(windows)]
70pub type RawHandleOrFd = std::os::windows::io::RawHandle;
71#[cfg(unix)]
72pub type RawHandleOrFd = std::os::fd::RawFd;
73#[cfg(windows)]
74type InheritedValue = usize;
75#[cfg(unix)]
76type InheritedValue = RawHandleOrFd;
77
78#[derive(Debug)]
79pub struct OwnedCommand {
80    command: Command,
81    windows_hidden: bool,
82    caller_job: CallerJob,
83    job_assignment: JobAssignmentMode,
84    unix_containment: UnixContainment,
85    // None preserves the existing unbounded partial-spawn reap.
86    partial_spawn_cleanup_timeout: Option<Duration>,
87    inherited_handles: Option<Vec<InheritedValue>>,
88    #[cfg(windows)]
89    windows_spawn_config: Option<crate::WindowsSpawnConfig>,
90    #[cfg(windows)]
91    windows_spawn_config_auto: bool,
92    #[cfg(windows)]
93    env_cleared: bool,
94}
95#[cfg(windows)]
96type CallerJob = Option<std::os::windows::io::OwnedHandle>;
97#[cfg(not(windows))]
98type CallerJob = Option<std::convert::Infallible>;
99
100impl OwnedCommand {
101    pub fn new(program: impl AsRef<OsStr>) -> Self {
102        let command = Self::from_command(Command::new(program));
103        #[cfg(windows)]
104        let command = {
105            let mut command = command;
106            // Fresh Commands have known inherited stdio/environment defaults.
107            command.windows_spawn_config = Some(crate::WindowsSpawnConfig::default());
108            command.windows_spawn_config_auto = true;
109            command
110        };
111        command
112    }
113    /// GUI/service children are hidden on Windows by default.
114    pub fn from_command(command: Command) -> Self {
115        Self {
116            command,
117            windows_hidden: true,
118            caller_job: None,
119            job_assignment: JobAssignmentMode::Strict,
120            unix_containment: UnixContainment::ProcessGroup,
121            partial_spawn_cleanup_timeout: None,
122            inherited_handles: None,
123            #[cfg(windows)]
124            windows_spawn_config: None,
125            #[cfg(windows)]
126            windows_spawn_config_auto: false,
127            #[cfg(windows)]
128            env_cleared: false,
129        }
130    }
131    pub fn command_mut(&mut self) -> &mut Command {
132        #[cfg(windows)]
133        if self.windows_spawn_config_auto {
134            // A mutable std Command can change settings with no stable getter.
135            self.windows_spawn_config = None;
136        }
137        &mut self.command
138    }
139    /// Inherit only these handles/fds plus configured stdio; values stay caller-owned.
140    pub fn inherit_handles_only(
141        &mut self,
142        handles: impl IntoIterator<Item = RawHandleOrFd>,
143    ) -> &mut Self {
144        #[cfg(windows)]
145        let handles = handles.into_iter().map(|handle| handle as usize).collect();
146        #[cfg(unix)]
147        let handles = handles.into_iter().collect();
148        self.inherited_handles = Some(handles);
149        self
150    }
151    /// Supply opaque stdio/environment/raw-arg settings for native allowlisted spawns.
152    /// Required after from_command/command_mut when inherit_handles_only is set.
153    /// Ordinary std spawns ignore it; fresh new() Commands have known defaults.
154    #[cfg(windows)]
155    pub fn windows_spawn_config(&mut self, config: crate::WindowsSpawnConfig) -> &mut Self {
156        self.windows_spawn_config = Some(config);
157        self.windows_spawn_config_auto = false;
158        self
159    }
160    /// Clear environment, then copy only named variables set in this process.
161    pub fn env_allowlist<I, S>(&mut self, keys: I) -> &mut Self
162    where
163        I: IntoIterator<Item = S>,
164        S: AsRef<OsStr>,
165    {
166        let kept: Vec<_> = keys
167            .into_iter()
168            .filter_map(|k| std::env::var_os(k.as_ref()).map(|v| (k.as_ref().to_os_string(), v)))
169            .collect();
170        self.command.env_clear();
171        #[cfg(windows)]
172        {
173            self.env_cleared = true;
174        }
175        for (k, v) in kept {
176            self.command.env(k, v);
177        }
178        self
179    }
180    pub fn env_strip<I, S>(&mut self, keys: I) -> &mut Self
181    where
182        I: IntoIterator<Item = S>,
183        S: AsRef<OsStr>,
184    {
185        for k in keys {
186            self.command.env_remove(k);
187        }
188        self
189    }
190    /// Compatibility no-op: hidden is already the default.
191    pub fn windows_hide(&mut self) -> &mut Self {
192        self.windows_hidden = true;
193        self
194    }
195    /// Duplicate caller job; assign before the empty nested RightKit job.
196    #[cfg(windows)]
197    pub fn windows_job(
198        &mut self,
199        job: std::os::windows::io::BorrowedHandle<'_>,
200    ) -> io::Result<&mut Self> {
201        self.caller_job = Some(job.try_clone_to_owned()?);
202        Ok(self)
203    }
204    /// Ignored on Unix. BestEffort reports errors on OwnedChild.
205    pub fn job_assignment_mode(&mut self, mode: JobAssignmentMode) -> &mut Self {
206        self.job_assignment = mode;
207        self
208    }
209    /// Ignored on Windows. Session runs setsid before exec.
210    pub fn unix_containment(&mut self, mode: UnixContainment) -> &mut Self {
211        self.unix_containment = mode;
212        self
213    }
214    /// Bound failed-spawn cleanup. Timed-out children go to a background reaper.
215    pub fn partial_spawn_cleanup_timeout(&mut self, timeout: Duration) -> &mut Self {
216        self.partial_spawn_cleanup_timeout = Some(timeout);
217        self
218    }
219    pub fn spawn(self) -> io::Result<OwnedChild> {
220        self.spawn_inner(false, false, None)
221    }
222    /// Shared launch with no owner job or terminate-on-drop. Unix uses setsid;
223    /// Windows requires breakaway and never falls back when escape is denied.
224    /// Stdio follows Command: dropping piped stdin can still deliver EOF.
225    pub fn spawn_uncontained_detached(self) -> io::Result<OwnedChild> {
226        self.spawn_inner(true, false, None)
227    }
228    fn spawn_inner(
229        mut self,
230        detached: bool,
231        fail_after_spawn: bool,
232        captured_pid: Option<Arc<std::sync::atomic::AtomicU32>>,
233    ) -> io::Result<OwnedChild> {
234        if detached && self.caller_job.is_some() {
235            return Err(io::Error::new(
236                io::ErrorKind::InvalidInput,
237                "detached launch cannot join a caller job",
238            ));
239        }
240        #[cfg(unix)]
241        {
242            use std::os::unix::process::CommandExt;
243            let session = detached || self.unix_containment == UnixContainment::Session;
244            let inheritance = self
245                .inherited_handles
246                .take()
247                .map(crate::process_unix::Inheritance::new)
248                .transpose()?;
249            // Only async-signal-safe libc calls run between fork and exec.
250            unsafe {
251                self.command.pre_exec(move || {
252                    let result = if session {
253                        libc::setsid()
254                    } else {
255                        libc::setpgid(0, 0)
256                    };
257                    if result < 0 {
258                        return Err(io::Error::last_os_error());
259                    }
260                    if let Some(inheritance) = &inheritance {
261                        inheritance.apply()?;
262                    }
263                    Ok(())
264                });
265            }
266        }
267        #[cfg(windows)]
268        crate::process_windows::configure(&mut self.command, detached, self.windows_hidden);
269        #[cfg(unix)]
270        let _ = (self.windows_hidden, self.job_assignment);
271        #[cfg(windows)]
272        let _ = self.unix_containment;
273        let spawned_at = SystemTime::now();
274        #[cfg(unix)]
275        let child = self.command.spawn()?;
276        #[cfg(windows)]
277        let child: PlatformChild = match self.inherited_handles.as_deref() {
278            None => self.command.spawn()?.into(),
279            Some(handles) => {
280                let config = self.windows_spawn_config.take().ok_or_else(|| {
281                    io::Error::new(io::ErrorKind::InvalidInput,
282                        "allowlisted Windows spawn requires windows_spawn_config for opaque Command settings")
283                })?;
284                crate::spawn_windows::spawn(
285                    &self.command,
286                    config,
287                    handles,
288                    detached,
289                    self.windows_hidden,
290                    self.env_cleared,
291                    self.partial_spawn_cleanup_timeout,
292                )?
293            }
294        };
295        let pid = child.id();
296        if let Some(captured) = captured_pid {
297            captured.store(pid, std::sync::atomic::Ordering::SeqCst);
298        }
299        let child = Arc::new(Mutex::new(child));
300        let handle = match ProcessHandle::new(&child, spawned_at) {
301            Ok(handle) => handle,
302            Err(error) => {
303                cleanup_direct(child, self.partial_spawn_cleanup_timeout);
304                return Err(error);
305            }
306        };
307        let mut partial = PartialChild {
308            child: Some(OwnedChild {
309                child,
310                handle,
311                pid,
312                detached,
313                terminate_on_drop: !detached,
314                tree_terminated: false,
315                assignment_failures: Vec::new(),
316                #[cfg(windows)]
317                job: None,
318            }),
319            timeout: self.partial_spawn_cleanup_timeout,
320        };
321        #[cfg(windows)]
322        if !detached {
323            let owned = partial.child.as_mut().expect("partial child is present");
324            crate::process_windows::assign_jobs(
325                &owned.handle,
326                self.caller_job.take(),
327                self.job_assignment,
328                &mut owned.job,
329                &mut owned.assignment_failures,
330            )?;
331        }
332        #[cfg(windows)]
333        partial
334            .child
335            .as_ref()
336            .expect("partial child is present")
337            .child()
338            .resume(detached)?;
339        if fail_after_spawn {
340            return Err(io::Error::other("injected post-spawn wrapping failure"));
341        }
342        Ok(partial.child.take().expect("partial child is present"))
343    }
344}
345
346pub const DEFAULT_ENV_ALLOWLIST: &[&str] = &[
347    "PATH",
348    "HOME",
349    "USER",
350    "LOGNAME",
351    "LANG",
352    "LC_ALL",
353    "TMPDIR",
354    "TEMP",
355    "TMP",
356    "SystemRoot",
357    "SYSTEMROOT",
358    "USERPROFILE",
359    "APPDATA",
360    "LOCALAPPDATA",
361    "ProgramData",
362    "COMSPEC",
363    "PATHEXT",
364    "XDG_DATA_HOME",
365    "XDG_RUNTIME_DIR",
366];
367
368#[derive(Debug)]
369pub struct OwnedChild {
370    child: Arc<Mutex<PlatformChild>>,
371    handle: ProcessHandle,
372    // Captured before any wait can reap the direct child.
373    pid: u32,
374    detached: bool,
375    terminate_on_drop: bool,
376    tree_terminated: bool,
377    assignment_failures: Vec<JobAssignmentFailure>,
378    #[cfg(windows)]
379    job: Option<crate::process_windows::KillOnCloseJob>,
380}
381#[derive(Clone, Copy, Debug, Eq, PartialEq)]
382pub enum ReapOutcome {
383    Exited(ExitStatus),
384    TimedOut,
385}
386#[derive(Clone, Copy, Debug, Eq, PartialEq)]
387pub enum WaitOutcome {
388    Exited(ExitStatus),
389    Terminated(ExitStatus),
390}
391
392impl OwnedChild {
393    pub fn id(&self) -> u32 {
394        self.pid
395    }
396    /// Borrow child state, including stdio, wait and try_wait. Windows uses a
397    /// native view with std pipe types for both spawn paths. Release this guard
398    /// before another accessor or exit reader; it holds the shared reap lock.
399    pub fn child(&self) -> MutexGuard<'_, PlatformChild> {
400        lock_child(&self.child)
401    }
402    pub fn take_stdin(&mut self) -> Option<ChildStdin> {
403        self.child().stdin.take()
404    }
405    pub fn take_stdout(&mut self) -> Option<ChildStdout> {
406        self.child().stdout.take()
407    }
408    pub fn take_stderr(&mut self) -> Option<ChildStderr> {
409        self.child().stderr.take()
410    }
411    pub fn job_assignment_failures(&self) -> &[JobAssignmentFailure] {
412        &self.assignment_failures
413    }
414    pub fn process_handle(&self) -> io::Result<ProcessHandle> {
415        self.handle.try_clone()
416    }
417    pub fn creation_time(&self) -> Option<SystemTime> {
418        self.handle.creation_time()
419    }
420    #[cfg(windows)]
421    pub fn creation_time_ticks(&self) -> Option<u64> {
422        self.handle.creation_time_ticks()
423    }
424    pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
425        self.child().try_wait()
426    }
427    // Preserve std Child::wait's stdin-close behavior. ProcessHandle performs
428    // native blocking waits & keeps Unix reap results in std's shared cache.
429    pub fn wait(&mut self) -> io::Result<ExitStatus> {
430        drop(self.child().stdin.take());
431        self.handle.wait()
432    }
433    pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
434        self.handle.wait_timeout(timeout)
435    }
436    pub fn wait_or_kill(&mut self, grace: Duration) -> io::Result<WaitOutcome> {
437        #[cfg(unix)]
438        if !self.tree_terminated {
439            self.signal(libc::SIGTERM)?;
440        }
441        if let Some(status) = self.wait_timeout(grace)? {
442            return Ok(WaitOutcome::Exited(status));
443        }
444        self.terminate_tree().map(WaitOutcome::Terminated)
445    }
446    /// Immediate forced termination, preserving the existing default.
447    /// Retained group/job is signalled even after the parent was reaped.
448    pub fn terminate_tree(&mut self) -> io::Result<ExitStatus> {
449        self.force_kill(1)?;
450        self.wait()
451    }
452    /// Unix: TERM now, tree-wide grace, then KILL even if parent exited.
453    /// Windows: immediately terminate retained job with supplied exit code.
454    pub fn terminate_tree_with_options(
455        &mut self,
456        options: TerminationOptions,
457    ) -> io::Result<ExitStatus> {
458        self.start_termination(options)?;
459        self.wait()
460    }
461    pub fn terminate_tree_bounded(&mut self, timeout: Duration) -> io::Result<ReapOutcome> {
462        self.terminate_tree_bounded_with_options(timeout, TerminationOptions::default())
463    }
464    /// timeout covers grace plus reap; grace is clipped to timeout.
465    pub fn terminate_tree_bounded_with_options(
466        &mut self,
467        timeout: Duration,
468        mut options: TerminationOptions,
469    ) -> io::Result<ReapOutcome> {
470        let started = Instant::now();
471        options.grace = options.grace.min(timeout);
472        self.start_termination(options)?;
473        Ok(
474            match self.wait_timeout(timeout.saturating_sub(started.elapsed()))? {
475                Some(status) => ReapOutcome::Exited(status),
476                None => ReapOutcome::TimedOut,
477            },
478        )
479    }
480    fn start_termination(&mut self, options: TerminationOptions) -> io::Result<()> {
481        #[cfg(unix)]
482        if !self.tree_terminated {
483            self.signal(libc::SIGTERM)?;
484            let started = Instant::now();
485            while started.elapsed() < options.grace {
486                // Reap an exited leader promptly; descendants still keep the
487                // captured group alive, so their grace is never shortened.
488                let _ = self.handle.try_wait()?;
489                if !self.tree_exists()? {
490                    break;
491                }
492                thread::sleep(
493                    Duration::from_millis(5).min(options.grace.saturating_sub(started.elapsed())),
494                );
495            }
496        }
497        self.force_kill(options.windows_exit_code)
498    }
499    #[cfg(unix)]
500    fn signal(&self, signal: i32) -> io::Result<()> {
501        // Detached children only own their direct pid. Never signal a reused
502        // direct pid once std has observed exit.
503        if self.detached && self.handle.try_wait()?.is_some() {
504            return Ok(());
505        }
506        let target = if self.detached {
507            self.pid as i32
508        } else {
509            -(self.pid as i32)
510        };
511        if unsafe { libc::kill(target, signal) } == 0 {
512            return Ok(());
513        }
514        let error = io::Error::last_os_error();
515        if error.raw_os_error() == Some(libc::ESRCH) || self.group_eperm_after_exit(&error)? {
516            Ok(())
517        } else {
518            Err(error)
519        }
520    }
521    #[cfg(unix)]
522    fn tree_exists(&self) -> io::Result<bool> {
523        if self.detached && self.handle.try_wait()?.is_some() {
524            return Ok(false);
525        }
526        let target = if self.detached {
527            self.pid as i32
528        } else {
529            -(self.pid as i32)
530        };
531        if unsafe { libc::kill(target, 0) } == 0 {
532            return Ok(true);
533        }
534        let error = io::Error::last_os_error();
535        if error.raw_os_error() == Some(libc::ESRCH) || self.group_eperm_after_exit(&error)? {
536            Ok(false)
537        } else {
538            Err(error)
539        }
540    }
541    /// macOS answers `kill(-pgid, ..)` with EPERM (not ESRCH) when the group's
542    /// remaining members are zombies; once the leader has exited, treat that as gone.
543    #[cfg(unix)]
544    fn group_eperm_after_exit(&self, error: &io::Error) -> io::Result<bool> {
545        Ok(!self.detached
546            && error.raw_os_error() == Some(libc::EPERM)
547            && self.handle.try_wait()?.is_some())
548    }
549    fn force_kill(&mut self, code: u32) -> io::Result<()> {
550        if self.tree_terminated {
551            return Ok(());
552        }
553        #[cfg(unix)]
554        {
555            let _ = code;
556            self.signal(libc::SIGKILL)?;
557        }
558        #[cfg(windows)]
559        crate::process_windows::terminate(&self.handle, self.job.as_ref(), code)?;
560        self.tree_terminated = true;
561        Ok(())
562    }
563}
564impl Drop for OwnedChild {
565    fn drop(&mut self) {
566        if self.terminate_on_drop {
567            let _ = self.terminate_tree();
568        } else if self.detached {
569            #[cfg(unix)]
570            {
571                // Shared launch still needs eventual reaping. Close owner pipes
572                // now; poll cached std state without blocking exit-reader locks.
573                {
574                    let mut child = self.child();
575                    drop(child.stdin.take());
576                    drop(child.stdout.take());
577                    drop(child.stderr.take());
578                }
579                if let Ok(reader) = self.handle.try_clone() {
580                    let _ = thread::Builder::new()
581                        .name("rightkit-detached-reap".into())
582                        .spawn(move || {
583                            let _ = reader.wait();
584                        });
585                }
586            }
587        }
588    }
589}
590
591struct PartialChild {
592    child: Option<OwnedChild>,
593    timeout: Option<Duration>,
594}
595impl Drop for PartialChild {
596    fn drop(&mut self) {
597        if let Some(mut child) = self.child.take() {
598            // Disable unbounded normal Drop before bounded cleanup.
599            child.terminate_on_drop = false;
600            let _ = child.force_kill(1);
601            cleanup_direct(Arc::clone(&child.child), self.timeout);
602        }
603    }
604}
605pub(crate) fn cleanup_direct(child: Arc<Mutex<PlatformChild>>, timeout: Option<Duration>) {
606    let _ = lock_child(&child).kill();
607    let Some(timeout) = timeout else {
608        let _ = lock_child(&child).wait();
609        return;
610    };
611    let started = Instant::now();
612    loop {
613        match lock_child(&child).try_wait() {
614            Ok(Some(_)) => return,
615            Err(_) => break,
616            Ok(None) => {}
617        }
618        if started.elapsed() >= timeout {
619            break;
620        }
621        thread::sleep(Duration::from_millis(5).min(timeout.saturating_sub(started.elapsed())));
622    }
623    // std Child does not reap on Drop. Transfer timed-out children to a waiter.
624    let _ = thread::Builder::new()
625        .name("rightkit-partial-reap".into())
626        .spawn(move || {
627            let _ = lock_child(&child).wait();
628        });
629}
630
631#[derive(Clone, Copy, Debug, Eq, PartialEq)]
632pub struct AdoptedProcess {
633    pid: NonZeroU32,
634}
635impl AdoptedProcess {
636    pub fn new(pid: NonZeroU32) -> Self {
637        Self { pid }
638    }
639    pub fn id(&self) -> u32 {
640        self.pid.get()
641    }
642    pub fn is_running(&self) -> io::Result<bool> {
643        process_is_running(self.pid)
644    }
645}
646#[cfg(test)]
647fn spawn_for_test(
648    command: Command,
649    captured_pid: Arc<std::sync::atomic::AtomicU32>,
650) -> io::Result<OwnedChild> {
651    OwnedCommand::from_command(command).spawn_inner(false, true, Some(captured_pid))
652}
653
654#[cfg(windows)]
655fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
656    use windows::Win32::{
657        Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_TIMEOUT},
658        System::Threading::{
659            OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
660            PROCESS_SYNCHRONIZE,
661        },
662    };
663
664    let handle = unsafe {
665        OpenProcess(
666            PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
667            false,
668            pid.get(),
669        )
670    };
671    let handle = match handle {
672        Ok(handle) => handle,
673        Err(error) => {
674            if error.code() == ERROR_INVALID_PARAMETER.to_hresult() {
675                return Ok(false);
676            }
677            return Err(io::Error::other(error.to_string()));
678        }
679    };
680    let wait = unsafe { WaitForSingleObject(handle, 0) };
681    let close = unsafe { CloseHandle(handle) };
682    close.map_err(|error| io::Error::other(error.to_string()))?;
683    match wait.0 {
684        0 => Ok(false),
685        value if value == WAIT_TIMEOUT.0 => Ok(true),
686        _ => Err(io::Error::last_os_error()),
687    }
688}
689
690#[cfg(unix)]
691fn process_is_running(pid: NonZeroU32) -> io::Result<bool> {
692    let pid = i32::try_from(pid.get()).map_err(io::Error::other)?;
693    loop {
694        // SAFETY: signal 0 performs only existence and permission checks; nothing is sent.
695        if unsafe { libc::kill(pid, 0) } == 0 {
696            return Ok(true);
697        }
698        let error = io::Error::last_os_error();
699        match error.raw_os_error() {
700            Some(libc::EINTR) => continue,
701            Some(libc::EPERM) => return Ok(true),
702            Some(libc::ESRCH) => return Ok(false),
703            _ => return Err(error),
704        }
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    #[test]
711    fn option_defaults_preserve_existing_behavior() {
712        let command = super::OwnedCommand::new("unused");
713        assert_eq!(command.job_assignment, super::JobAssignmentMode::Strict);
714        assert_eq!(
715            command.unix_containment,
716            super::UnixContainment::ProcessGroup
717        );
718        assert_eq!(command.partial_spawn_cleanup_timeout, None);
719        assert_eq!(
720            super::TerminationOptions::default().grace,
721            std::time::Duration::ZERO
722        );
723        assert_eq!(super::TerminationOptions::default().windows_exit_code, 1);
724    }
725
726    #[test]
727    fn option_builders_keep_every_requested_value() {
728        use super::{JobAssignmentMode, OwnedCommand, TerminationOptions, UnixContainment};
729        use std::time::Duration;
730        let mut command = OwnedCommand::new("unused");
731        command
732            .job_assignment_mode(JobAssignmentMode::BestEffort)
733            .unix_containment(UnixContainment::Session)
734            .partial_spawn_cleanup_timeout(Duration::from_secs(5));
735        assert_eq!(command.job_assignment, JobAssignmentMode::BestEffort);
736        assert_eq!(command.unix_containment, UnixContainment::Session);
737        assert_eq!(
738            command.partial_spawn_cleanup_timeout,
739            Some(Duration::from_secs(5))
740        );
741        let options = TerminationOptions::default()
742            .with_grace(Duration::from_millis(100))
743            .with_windows_exit_code(1067);
744        assert_eq!(options.grace, Duration::from_millis(100));
745        assert_eq!(options.windows_exit_code, 1067);
746    }
747
748    #[test]
749    fn zero_bound_partial_spawn_failure_still_reaps_the_child() {
750        use std::{
751            num::NonZeroU32,
752            process::Command,
753            sync::{
754                atomic::{AtomicU32, Ordering},
755                Arc,
756            },
757            thread,
758            time::{Duration, Instant},
759        };
760        let command = if cfg!(windows) {
761            let mut command = Command::new("powershell.exe");
762            command.args([
763                "-NoLogo",
764                "-NoProfile",
765                "-NonInteractive",
766                "-Command",
767                "Start-Sleep -Seconds 30",
768            ]);
769            command
770        } else {
771            let mut command = Command::new("/bin/sh");
772            command.args(["-c", "exec sleep 30"]);
773            command
774        };
775        let mut command = super::OwnedCommand::from_command(command);
776        command.partial_spawn_cleanup_timeout(Duration::ZERO);
777        let captured = Arc::new(AtomicU32::new(0));
778        let started = Instant::now();
779        assert!(command
780            .spawn_inner(false, true, Some(Arc::clone(&captured)))
781            .is_err());
782        assert!(started.elapsed() < Duration::from_secs(3));
783        let pid = captured.load(Ordering::SeqCst);
784        assert_ne!(pid, 0);
785        let process = super::AdoptedProcess::new(NonZeroU32::new(pid).unwrap());
786        let started = Instant::now();
787        while process.is_running().unwrap() {
788            assert!(started.elapsed() < Duration::from_secs(5));
789            thread::sleep(Duration::from_millis(5));
790        }
791    }
792
793    #[test]
794    fn owned_commands_hide_their_console_window_by_default() {
795        assert!(super::OwnedCommand::new("x").windows_hidden);
796        assert!(super::OwnedCommand::from_command(std::process::Command::new("x")).windows_hidden);
797    }
798
799    /// A kill-on-close job standing in for an in-process host's own job object.
800    #[cfg(windows)]
801    fn host_job() -> std::os::windows::io::OwnedHandle {
802        use std::os::windows::io::FromRawHandle;
803        use windows::Win32::System::JobObjects::{
804            CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject,
805            JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
806        };
807        let job = unsafe { CreateJobObjectW(None, None) }.unwrap();
808        let job_handle = unsafe { std::os::windows::io::OwnedHandle::from_raw_handle(job.0 as _) };
809        let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
810        info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
811        unsafe {
812            SetInformationJobObject(
813                job,
814                JobObjectExtendedLimitInformation,
815                &info as *const _ as *const _,
816                std::mem::size_of_val(&info) as u32,
817            )
818        }
819        .unwrap();
820        job_handle
821    }
822
823    #[cfg(windows)]
824    fn in_job(pid: u32, job: &std::os::windows::io::OwnedHandle) -> bool {
825        use std::os::windows::io::AsRawHandle;
826        #[link(name = "kernel32")]
827        unsafe extern "system" {
828            fn OpenProcess(access: u32, inherit: i32, pid: u32) -> *mut std::ffi::c_void;
829            fn IsProcessInJob(
830                process: *mut std::ffi::c_void,
831                job: *mut std::ffi::c_void,
832                result: *mut i32,
833            ) -> i32;
834            fn CloseHandle(handle: *mut std::ffi::c_void) -> i32;
835        }
836        unsafe {
837            let process = OpenProcess(0x1000, 0, pid); // PROCESS_QUERY_LIMITED_INFORMATION
838            assert!(!process.is_null());
839            let mut inside = 0;
840            assert_ne!(
841                IsProcessInJob(process, job.as_raw_handle() as _, &mut inside),
842                0
843            );
844            CloseHandle(process);
845            inside != 0
846        }
847    }
848
849    #[cfg(windows)]
850    #[test]
851    fn children_join_the_callers_job_and_die_when_the_caller_closes_it() {
852        use std::os::windows::io::AsHandle;
853        use std::time::{Duration, Instant};
854        let job = host_job();
855        let sleeper = || {
856            let mut command = super::OwnedCommand::new("powershell.exe");
857            command.command_mut().args([
858                "-NoLogo",
859                "-NoProfile",
860                "-NonInteractive",
861                "-Command",
862                "Start-Sleep -Seconds 60",
863            ]);
864            command.windows_job(job.as_handle()).unwrap();
865            command.spawn().unwrap()
866        };
867        // The second child joins a job that already holds a process: binding must happen
868        // before RightKit's own jobs, or Windows refuses it.
869        let (first, second) = (sleeper(), sleeper());
870        let pids = [first.id(), second.id()];
871        assert!(
872            pids.iter().all(|pid| in_job(*pid, &job)),
873            "children must be in the caller's job"
874        );
875
876        // The caller closes its job while the owned children are still alive: they die.
877        drop(job);
878        let deadline = Instant::now() + Duration::from_secs(5);
879        for pid in pids {
880            let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
881            while process.is_running().unwrap_or(false) {
882                assert!(
883                    Instant::now() < deadline,
884                    "child {pid} survived its caller's job"
885                );
886                std::thread::sleep(Duration::from_millis(20));
887            }
888        }
889        drop((first, second));
890    }
891
892    #[test]
893    fn a_failure_after_spawn_cleans_up_the_partially_wrapped_child() {
894        use std::{
895            process::Command,
896            sync::{
897                atomic::{AtomicU32, Ordering},
898                Arc,
899            },
900            thread,
901            time::{Duration, Instant},
902        };
903
904        // A long-lived child that survives on its own if the partial-spawn cleanup fails.
905        let command = if cfg!(windows) {
906            // windowless: spawned through spawn_owned (CREATE_NO_WINDOW).
907            let mut command = Command::new("powershell.exe");
908            command.args([
909                "-NoLogo",
910                "-NoProfile",
911                "-NonInteractive",
912                "-Command",
913                "Start-Sleep -Seconds 60",
914            ]);
915            command
916        } else {
917            // windowless: spawned through spawn_owned (CREATE_NO_WINDOW).
918            let mut command = Command::new("sleep");
919            command.arg("60");
920            command
921        };
922        let spawned_pid = Arc::new(AtomicU32::new(0));
923
924        assert!(super::spawn_for_test(command, Arc::clone(&spawned_pid)).is_err());
925        let pid = spawned_pid.load(Ordering::SeqCst);
926        assert_ne!(pid, 0, "test must fail after the OS process was spawned");
927        let process = super::AdoptedProcess::new(std::num::NonZeroU32::new(pid).unwrap());
928        let deadline = Instant::now() + Duration::from_secs(5);
929        while process.is_running().unwrap_or(false) {
930            assert!(
931                Instant::now() < deadline,
932                "partially spawned process leaked"
933            );
934            thread::sleep(Duration::from_millis(20));
935        }
936    }
937}