Skip to main content

coreshift_core/spawn/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! Process spawning and lifecycle management.
6//!
7//! This module exposes explicit Linux/Android process primitives. Callers must
8//! provide the exact argument vector and choose the spawn backend. Core does not
9//! infer shell/root behavior, select backends from platform properties, or
10//! silently switch between backends.
11
12use std::os::unix::io::RawFd;
13use std::time::{Duration, Instant};
14
15use crate::CoreError;
16use crate::error::syscall_ret;
17use crate::fd::Fd;
18use crate::io::DrainState;
19use crate::reactor::Reactor;
20use libc::{O_CLOEXEC, WEXITSTATUS, WIFEXITED, WIFSIGNALED, WTERMSIG, pid_t, pipe2, waitpid};
21use std::collections::HashSet;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::sync::{Mutex, OnceLock};
24
25mod exec;
26mod fork;
27mod posix;
28
29use exec::ExecContext;
30use fork::spawn_fork_internal;
31use posix::spawn_posix_internal;
32
33unsafe extern "C" {
34    pub(crate) static mut environ: *mut *mut libc::c_char;
35}
36
37/// Upper bound on how long to keep polling for a reap after SIGKILL has been
38/// sent. A child stuck in uninterruptible sleep (D-state) cannot be reaped at
39/// all — SIGKILL stays pending until it leaves D-state — so after this window
40/// the wait loop gives up and returns the partial output instead of spinning
41/// forever. Mirrors the bounded reap wait in [`ManagedProcess`]'s `Drop`.
42const D_STATE_REAP_BOUND: Duration = Duration::from_millis(500);
43
44/// Orphaned children: processes this library spawned whose caller will never
45/// call `wait` (the `wait = false` path) or that the wait loop gave up on
46/// reaping (D-state / cancel-timeout give-up). A reaper thread `waitpid`s each
47/// registered pid so they do not accumulate as zombies — a long-lived daemon
48/// that detaches children would otherwise exhaust the pid space (finding 15).
49///
50/// Only *registered* pids are reaped. A global `waitpid(-1)` loop would race
51/// with callers explicitly waiting on other children of this process; targeting
52/// registered pids is safe because they are our own direct children — the pid
53/// cannot be recycled until we reap it.
54static ORPHANED: OnceLock<Mutex<HashSet<pid_t>>> = OnceLock::new();
55static REAPER_STARTED: AtomicBool = AtomicBool::new(false);
56
57/// Register `pid` as orphaned (nobody will `wait` on it) and ensure the
58/// background reaper is running. No-op if the pid is already registered.
59fn orphan_child(pid: pid_t) {
60    ORPHANED
61        .get_or_init(|| Mutex::new(HashSet::new()))
62        .lock()
63        .unwrap()
64        .insert(pid);
65    start_reaper();
66}
67
68/// Spawn (once) the background reaper thread that reaps [`ORPHANED`] pids.
69fn start_reaper() {
70    if REAPER_STARTED.load(Ordering::SeqCst) {
71        return;
72    }
73    let r = REAPER_STARTED.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst);
74    if r.is_err() {
75        return;
76    }
77    std::thread::Builder::new()
78        .name("spawn-orphan-reaper".into())
79        .spawn(reap_orphaned)
80        .ok();
81}
82
83/// Reaper body: periodically `waitpid` (non-blocking) every orphaned pid and
84/// drop it from the set once it has been reaped (or is already gone, which can
85/// only mean it was reaped elsewhere — the pid was still registered).
86fn reap_orphaned() {
87    loop {
88        let pids: Vec<pid_t> = ORPHANED
89            .get_or_init(|| Mutex::new(HashSet::new()))
90            .lock()
91            .unwrap()
92            .iter()
93            .copied()
94            .collect();
95        let mut still_orphaned = Vec::new();
96        for pid in pids {
97            let mut status: libc::c_int = 0;
98            let r = unsafe { waitpid(pid, &mut status, libc::WNOHANG) };
99            if r == pid
100                || (r < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ECHILD))
101            {
102                continue; // reaped or gone — drop from the set
103            }
104            still_orphaned.push(pid);
105        }
106        if !still_orphaned.is_empty() {
107            if let Some(set) = ORPHANED.get() {
108                if let Ok(mut guard) = set.lock() {
109                    for pid in still_orphaned {
110                        guard.insert(pid);
111                    }
112                }
113            }
114        }
115        std::thread::sleep(Duration::from_millis(250));
116    }
117}
118
119/// Detach a pid from the orphan set (used when a previously-orphaned process
120/// turns out to be waitable again; currently unused by callers but keeps the
121/// registry honest).
122#[allow(dead_code)]
123fn deorphan_child(pid: pid_t) {
124    if let Some(set) = ORPHANED.get() {
125        if let Ok(mut guard) = set.lock() {
126            guard.remove(&pid);
127        }
128    }
129}
130
131/// Policy for handling process cancellation or timeouts.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133pub enum CancelPolicy {
134    /// Do nothing on cancellation; let the process run to completion.
135    #[default]
136    None,
137    /// Send SIGTERM, then SIGKILL after a grace period.
138    Graceful,
139    /// Send SIGKILL immediately.
140    Kill,
141}
142
143/// Process group and session configuration.
144#[derive(Debug, Clone, Copy, Default)]
145pub struct ProcessGroup {
146    /// Join an existing process group leader.
147    pub leader: Option<pid_t>,
148    /// Create a new session (`setsid`).
149    pub isolated: bool,
150}
151
152impl ProcessGroup {
153    /// Create a new process group configuration.
154    pub fn new(leader: Option<pid_t>, isolated: bool) -> Self {
155        Self { leader, isolated }
156    }
157}
158
159#[inline(always)]
160fn errno() -> i32 {
161    std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
162}
163
164/// Relocate `fd` to the lowest available descriptor `>= 3`, closing the
165/// original. Guards against `pipe2` handing back fds 0/1/2 when the daemon
166/// runs with stdio closed: a pipe on 0/1/2 would collide with the child's
167/// `dup2(…, 0/1/2)` setup (clobbering a still-needed end) and with the
168/// stdio-tracking in `close_child_fds_for_policy`.
169fn relocate_above_stdio(fd: RawFd, op: &'static str) -> Result<RawFd, CoreError> {
170    if fd >= 3 {
171        return Ok(fd);
172    }
173    let new = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 3) };
174    syscall_ret(new, op)?;
175    unsafe {
176        libc::close(fd);
177    }
178    Ok(new)
179}
180
181/// Creates a pipe with O_CLOEXEC, relocated above stdio. Both ends stay
182/// blocking; the parent-facing ends are flipped to O_NONBLOCK by
183/// [`DrainState`] after spawn so the child never inherits a non-blocking
184/// stdio (which would silently truncate child output on `EAGAIN`).
185/// Invariants: FDs returned are strictly >= 3 and will close automatically on drop.
186#[inline(always)]
187fn make_pipe() -> Result<(Fd, Fd), CoreError> {
188    let mut fds = [0; 2];
189    let r = unsafe { pipe2(fds.as_mut_ptr(), O_CLOEXEC) };
190    syscall_ret(r, "pipe2")?;
191    let r0 = match relocate_above_stdio(fds[0], "pipe2:relocate") {
192        Ok(fd) => fd,
193        Err(e) => {
194            // fds[0] is still open when its relocation fails; close to avoid
195            // leaking under fd pressure (EMFILE).
196            unsafe {
197                libc::close(fds[0]);
198            }
199            return Err(e);
200        }
201    };
202    let r1 = match relocate_above_stdio(fds[1], "pipe2:relocate") {
203        Ok(fd) => fd,
204        Err(e) => {
205            // fds[1] is still open (relocation failed), and r0 was relocated
206            // above — both would leak on this error path.
207            unsafe {
208                libc::close(r0);
209                libc::close(fds[1]);
210            }
211            return Err(e);
212        }
213    };
214    Ok((Fd::new(r0, "pipe2")?, Fd::new(r1, "pipe2")?))
215}
216
217fn make_cloexec_pipe() -> Result<(RawFd, RawFd), CoreError> {
218    let mut fds = [0; 2];
219    let r = unsafe { pipe2(fds.as_mut_ptr(), O_CLOEXEC) };
220    syscall_ret(r, "pipe2")?;
221    let r0 = match relocate_above_stdio(fds[0], "pipe2:relocate") {
222        Ok(fd) => fd,
223        Err(e) => {
224            unsafe {
225                libc::close(fds[0]);
226            }
227            return Err(e);
228        }
229    };
230    let r1 = match relocate_above_stdio(fds[1], "pipe2:relocate") {
231        Ok(fd) => fd,
232        Err(e) => {
233            unsafe {
234                libc::close(r0);
235                libc::close(fds[1]);
236            }
237            return Err(e);
238        }
239    };
240    Ok((r0, r1))
241}
242
243struct Pipes {
244    stdin_r: Option<Fd>,
245    stdin_w: Option<Fd>,
246    stdout_r: Option<Fd>,
247    stdout_w: Option<Fd>,
248    stderr_r: Option<Fd>,
249    stderr_w: Option<Fd>,
250}
251
252impl Pipes {
253    fn new(in_buf: Option<&[u8]>, out: bool, err: bool) -> Result<Self, CoreError> {
254        let (stdin_r, stdin_w) = if in_buf.is_some() {
255            let (r, w) = make_pipe()?;
256            (Some(r), Some(w))
257        } else {
258            (None, None)
259        };
260
261        let (stdout_r, stdout_w) = if out {
262            let (r, w) = make_pipe()?;
263            (Some(r), Some(w))
264        } else {
265            (None, None)
266        };
267
268        let (stderr_r, stderr_w) = if err {
269            let (r, w) = make_pipe()?;
270            (Some(r), Some(w))
271        } else {
272            (None, None)
273        };
274
275        Ok(Self {
276            stdin_r,
277            stdin_w,
278            stdout_r,
279            stdout_w,
280            stderr_r,
281            stderr_w,
282        })
283    }
284
285    #[inline(always)]
286    fn close_all(&mut self) {
287        self.stdin_r.take();
288        self.stdin_w.take();
289        self.stdout_r.take();
290        self.stdout_w.take();
291        self.stderr_r.take();
292        self.stderr_w.take();
293    }
294}
295
296/// Represents the termination status of a process.
297#[derive(Debug, PartialEq, Eq)]
298pub enum ExitStatus {
299    /// Process exited normally with the specified code.
300    Exited(i32),
301    /// Process was terminated by a signal.
302    Signaled(i32),
303}
304
305/// Explicit process spawning backend.
306#[derive(Debug, Clone, Copy, PartialEq, Eq)]
307pub enum SpawnBackend {
308    /// Force the use of `posix_spawn`.
309    PosixSpawn,
310    /// Force the use of `fork`/`exec`.
311    ///
312    /// The fork backend supports explicit [`SpawnFdPolicy`] handling before
313    /// `execve`.
314    Fork,
315}
316
317/// Explicit file-descriptor inheritance policy for spawned children.
318#[derive(Debug, Clone, PartialEq, Eq, Default)]
319pub enum SpawnFdPolicy {
320    /// Inherit descriptors according to their existing `FD_CLOEXEC` flags.
321    #[default]
322    CloexecOnly,
323    /// For the fork backend, close every descriptor >= 3 before `execve`,
324    /// except Core-required pipe descriptors.
325    CloseFrom3,
326    /// For the fork backend, close every descriptor >= 3 before `execve`,
327    /// except Core-required pipe descriptors and the listed descriptors.
328    ///
329    /// Core does not close allowlisted descriptors, but their existing
330    /// `FD_CLOEXEC` state still applies. Callers that want an allowlisted
331    /// descriptor to survive `execve` must clear `FD_CLOEXEC` before spawning.
332    Allowlist(Vec<RawFd>),
333}
334
335#[inline(always)]
336fn decode_status(status: i32) -> ExitStatus {
337    if WIFEXITED(status) {
338        ExitStatus::Exited(WEXITSTATUS(status))
339    } else if WIFSIGNALED(status) {
340        ExitStatus::Signaled(WTERMSIG(status))
341    } else {
342        ExitStatus::Exited(-1)
343    }
344}
345
346/// A handle to a spawned process.
347///
348/// ### Fork Safety
349/// The process handle contains a PID. After a `fork`, the child process will
350/// have a copy of this PID, but it refers to the same original process.
351/// Calling `wait` or `kill` from the child may lead to confusing results
352/// if multiple processes are managing the same PID.
353pub struct Process {
354    pid: pid_t,
355}
356
357impl Process {
358    /// Create a handle for an existing PID.
359    pub fn new(pid: pid_t) -> Self {
360        Self { pid }
361    }
362
363    /// Return the process ID.
364    pub fn pid(&self) -> pid_t {
365        self.pid
366    }
367
368    /// Perform a non-blocking wait for process termination.
369    ///
370    /// ### Errors
371    /// - `ECHILD`: The process does not exist or is not a child of the caller.
372    /// - `EINTR`: The call was interrupted by a signal (handled internally).
373    pub fn wait_step(&self) -> Result<Option<ExitStatus>, CoreError> {
374        loop {
375            let mut status = 0;
376            let r = unsafe { waitpid(self.pid, &mut status, libc::WNOHANG) };
377            if r == 0 {
378                return Ok(None);
379            }
380            if r < 0 {
381                let e = errno();
382                if e == libc::EINTR {
383                    continue;
384                }
385                return Err(CoreError::sys(e, "waitpid_step"));
386            }
387            return Ok(Some(decode_status(status)));
388        }
389    }
390
391    /// Block until the process terminates.
392    ///
393    /// ### Errors
394    /// - `ECHILD`: The process does not exist or is not a child of the caller.
395    pub fn wait_blocking(&self) -> Result<ExitStatus, CoreError> {
396        loop {
397            let mut status = 0;
398            let r = unsafe { waitpid(self.pid, &mut status, 0) };
399            if r < 0 {
400                let e = errno();
401                if e == libc::EINTR {
402                    continue;
403                }
404                return Err(CoreError::sys(e, "waitpid_blocking"));
405            }
406            return Ok(decode_status(status));
407        }
408    }
409
410    /// Send a signal to the process.
411    ///
412    /// ### Errors
413    /// - `EINVAL`: Invalid signal number, or a non-positive pid (pid `0`
414    ///   would signal the caller's own process group).
415    /// - `EPERM`: The caller does not have permission to send the signal.
416    /// - `ESRCH`: The process does not exist.
417    pub fn kill(&self, sig: i32) -> Result<(), CoreError> {
418        if self.pid <= 0 {
419            return Err(CoreError::sys(libc::EINVAL, "kill: invalid pid"));
420        }
421        let r = unsafe { libc::kill(self.pid, sig) };
422        if r < 0 {
423            let e = errno();
424            if e == libc::ESRCH {
425                return Ok(());
426            }
427            syscall_ret(-1, "kill")?;
428        }
429        Ok(())
430    }
431
432    /// Signal the process group whose id equals [`Self::pid`] — valid only
433    /// when the process is its own group/session leader. For a child placed
434    /// into a custom leader's group use [`Self::kill_group`].
435    ///
436    /// ### Errors
437    /// Same as [`Self::kill`].
438    pub fn kill_pgroup(&self, sig: i32) -> Result<(), CoreError> {
439        self.kill_group(self.pid, sig)
440    }
441
442    /// Send a signal to an explicit process group.
443    ///
444    /// The pgid must be the child's actual group (its own pid after `setsid`,
445    /// or the configured leader's id after `setpgid`), never guessed from the
446    /// pid, and never `0` or negative — `kill(-0)` would signal the caller's
447    /// own process group.
448    ///
449    /// ### Errors
450    /// Same as [`Self::kill`], plus `EINVAL` for a non-positive pgid.
451    pub fn kill_group(&self, pgid: pid_t, sig: i32) -> Result<(), CoreError> {
452        if pgid <= 0 {
453            return Err(CoreError::sys(libc::EINVAL, "kill_group: invalid pgid"));
454        }
455        let r = unsafe { libc::kill(-pgid, sig) };
456        if r < 0 {
457            let e = errno();
458            if e == libc::ESRCH {
459                return Ok(());
460            }
461            syscall_ret(-1, "kill_group")?;
462        }
463        Ok(())
464    }
465}
466
467/// Configuration options for spawning a new process.
468#[derive(Clone)]
469pub struct SpawnOptions {
470    ctx: ExecContext,
471    stdin: Option<Box<[u8]>>,
472    capture_stdout: bool,
473    capture_stderr: bool,
474    wait: bool,
475    pgroup: ProcessGroup,
476    max_output: usize,
477    timeout_ms: Option<u32>,
478    kill_grace_ms: u32,
479    cancel: CancelPolicy,
480    backend: SpawnBackend,
481    fd_policy: SpawnFdPolicy,
482    early_exit: Option<fn(&[u8]) -> bool>,
483}
484
485impl SpawnOptions {
486    /// Create a new builder for process spawning.
487    pub fn builder(argv: Vec<String>, backend: SpawnBackend) -> SpawnOptionsBuilder {
488        SpawnOptionsBuilder::new(argv, backend)
489    }
490
491    /// Execute the process according to the options and block until completion.
492    pub fn run(self) -> Result<Output, CoreError> {
493        spawn(self)
494    }
495}
496
497/// Builder for [`SpawnOptions`].
498#[derive(Clone)]
499pub struct SpawnOptionsBuilder {
500    argv: Vec<String>,
501    env: Option<Vec<String>>,
502    cwd: Option<String>,
503    stdin: Option<Box<[u8]>>,
504    capture_stdout: bool,
505    capture_stderr: bool,
506    wait: bool,
507    pgroup: ProcessGroup,
508    max_output: usize,
509    timeout_ms: Option<u32>,
510    kill_grace_ms: u32,
511    cancel: CancelPolicy,
512    backend: SpawnBackend,
513    fd_policy: SpawnFdPolicy,
514    early_exit: Option<fn(&[u8]) -> bool>,
515}
516
517impl SpawnOptionsBuilder {
518    /// Create a new builder with the specified argument vector.
519    pub fn new(argv: Vec<String>, backend: SpawnBackend) -> Self {
520        Self {
521            argv,
522            env: None,
523            cwd: None,
524            stdin: None,
525            capture_stdout: false,
526            capture_stderr: false,
527            wait: true,
528            pgroup: ProcessGroup::default(),
529            max_output: 1024 * 1024,
530            timeout_ms: None,
531            kill_grace_ms: 2000,
532            cancel: CancelPolicy::Kill,
533            backend,
534            fd_policy: SpawnFdPolicy::default(),
535            early_exit: None,
536        }
537    }
538
539    /// Set environment variables.
540    pub fn env(mut self, env: Vec<String>) -> Self {
541        self.env = Some(env);
542        self
543    }
544
545    /// Set the working directory.
546    pub fn cwd(mut self, cwd: String) -> Self {
547        self.cwd = Some(cwd);
548        self
549    }
550
551    /// Provide data to be written to the child's stdin.
552    pub fn stdin(mut self, data: impl Into<Box<[u8]>>) -> Self {
553        self.stdin = Some(data.into());
554        self
555    }
556
557    /// Enable stdout capture.
558    pub fn capture_stdout(mut self) -> Self {
559        self.capture_stdout = true;
560        self
561    }
562
563    /// Enable stderr capture.
564    pub fn capture_stderr(mut self) -> Self {
565        self.capture_stderr = true;
566        self
567    }
568
569    /// Set whether to wait for the process to terminate (default: true).
570    pub fn wait(mut self, wait: bool) -> Self {
571        self.wait = wait;
572        self
573    }
574
575    /// Set process group and isolation policy.
576    pub fn pgroup(mut self, pgroup: ProcessGroup) -> Self {
577        self.pgroup = pgroup;
578        self
579    }
580
581    /// Set the combined stdout+stderr output buffer size (default: 1MB).
582    ///
583    /// If captured output exceeds this limit, spawn drains the child pipes to
584    /// completion and returns `EOVERFLOW`.
585    pub fn max_output(mut self, max: usize) -> Self {
586        self.max_output = max;
587        self
588    }
589
590    /// Set the execution timeout in milliseconds.
591    pub fn timeout_ms(mut self, ms: u32) -> Self {
592        self.timeout_ms = Some(ms);
593        self
594    }
595
596    /// Set the grace period before SIGKILL (default: 2s).
597    pub fn kill_grace_ms(mut self, ms: u32) -> Self {
598        self.kill_grace_ms = ms;
599        self
600    }
601
602    /// Set the cancellation policy (default: Kill).
603    pub fn cancel(mut self, policy: CancelPolicy) -> Self {
604        self.cancel = policy;
605        self
606    }
607
608    /// Set the child file-descriptor inheritance policy.
609    pub fn fd_policy(mut self, policy: SpawnFdPolicy) -> Self {
610        self.fd_policy = policy;
611        self
612    }
613
614    /// Set an early exit callback.
615    pub fn early_exit(mut self, callback: fn(&[u8]) -> bool) -> Self {
616        self.early_exit = Some(callback);
617        self
618    }
619
620    /// Build the spawn options.
621    pub fn build(self) -> Result<SpawnOptions, CoreError> {
622        let ctx = ExecContext::new(self.argv, self.env, self.cwd)?;
623        Ok(SpawnOptions {
624            ctx,
625            stdin: self.stdin,
626            capture_stdout: self.capture_stdout,
627            capture_stderr: self.capture_stderr,
628            wait: self.wait,
629            pgroup: self.pgroup,
630            max_output: self.max_output,
631            timeout_ms: self.timeout_ms,
632            kill_grace_ms: self.kill_grace_ms,
633            cancel: self.cancel,
634            backend: self.backend,
635            fd_policy: self.fd_policy,
636            early_exit: self.early_exit,
637        })
638    }
639}
640
641/// The result of a process execution.
642#[derive(Debug)]
643pub struct Output {
644    /// The PID of the finished process.
645    pub pid: pid_t,
646    /// Final exit status (None if `wait=false`).
647    pub status: Option<ExitStatus>,
648    /// Captured stdout buffer.
649    pub stdout: Vec<u8>,
650    /// Captured stderr buffer.
651    pub stderr: Vec<u8>,
652    /// Whether the process timed out.
653    pub timed_out: bool,
654    /// Whether stdout drain stopped because the early-exit callback matched.
655    pub stdout_early_exited: bool,
656}
657
658fn validate_backend(opts: &SpawnOptions) -> Result<(), CoreError> {
659    validate_fd_policy(&opts.fd_policy)?;
660    match opts.backend {
661        SpawnBackend::PosixSpawn => {
662            if opts.ctx.cwd.is_some() {
663                return Err(CoreError::sys(libc::EINVAL, "posix_spawn cwd unsupported"));
664            }
665            if opts.pgroup.isolated {
666                return Err(CoreError::sys(
667                    libc::EINVAL,
668                    "posix_spawn setsid unsupported",
669                ));
670            }
671            if opts.fd_policy != SpawnFdPolicy::CloexecOnly {
672                return Err(CoreError::sys(
673                    libc::EINVAL,
674                    "posix_spawn fd policy unsupported",
675                ));
676            }
677            Ok(())
678        }
679        SpawnBackend::Fork => {
680            // After `setsid` the child is a session leader in a brand-new
681            // session; `setpgid(0, leader)` for a leader outside that session
682            // always fails with EPERM. A zero leader means "own pid" (the
683            // child's own group after setsid), which is valid.
684            if opts.pgroup.isolated && opts.pgroup.leader.is_some_and(|l| l != 0) {
685                return Err(CoreError::sys(
686                    libc::EINVAL,
687                    "fork isolated + custom setpgid leader unsupported",
688                ));
689            }
690            Ok(())
691        }
692    }
693}
694
695fn validate_fd_policy(policy: &SpawnFdPolicy) -> Result<(), CoreError> {
696    if let SpawnFdPolicy::Allowlist(fds) = policy {
697        let mut seen = Vec::with_capacity(fds.len());
698        for &fd in fds {
699            if fd < 0 {
700                return Err(CoreError::sys(libc::EINVAL, "spawn fd allowlist invalid"));
701            }
702            let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
703            if flags < 0 {
704                return Err(CoreError::sys(errno(), "spawn fd allowlist fcntl(F_GETFD)"));
705            }
706            if seen.contains(&fd) {
707                return Err(CoreError::sys(libc::EINVAL, "spawn fd allowlist duplicate"));
708            }
709            seen.push(fd);
710        }
711    }
712    Ok(())
713}
714
715/// Specialized drain state for process spawning.
716pub type SpawnDrain = DrainState<fn(&[u8]) -> bool>;
717
718/// A process that is currently running and being monitored.
719///
720/// ### Fork Safety
721/// This handle contains both a PID and owned file descriptors for process I/O.
722/// Upon `fork`, the descriptors are inherited. Standard `O_CLOEXEC` behavior
723/// applies after `exec`.
724pub struct RunningProcess {
725    /// Handle to the process.
726    pub process: Process,
727    drain: SpawnDrain,
728}
729
730/// Full process lifecycle driven by a caller-owned reactor.
731///
732/// `ManagedProcess` preserves the blocking [`spawn`] semantics while allowing
733/// an application reactor to stay responsive: Core owns timeout/cancellation
734/// escalation, process-group signaling, pipe draining, overflow reporting, and
735/// `waitpid` reaping; the caller only routes readiness events and polls on
736/// [`Self::next_deadline`].
737pub struct ManagedProcess {
738    running: Option<RunningProcess>,
739    pid: pid_t,
740    timeout_at: Option<Instant>,
741    kill_grace: Duration,
742    cancel: CancelPolicy,
743    pgroup: ProcessGroup,
744    cancel_at: Option<Instant>,
745    kill_state: KillState,
746    status: Option<ExitStatus>,
747    timed_out: bool,
748    kill_sent_at: Option<Instant>,
749}
750
751impl RunningProcess {
752    /// Register active stdio pipe descriptors with a reactor.
753    ///
754    /// Call this once after [`spawn_start`] when the process was started with
755    /// captured output or stdin data. The assigned tokens are kept internally
756    /// and later matched by [`Self::handle_reactor_event`].
757    pub fn register_with_reactor(&mut self, reactor: &mut Reactor) -> Result<(), CoreError> {
758        self.drain.register_with_reactor(reactor)
759    }
760
761    /// Apply one reactor readiness event to this process' stdio drain state.
762    ///
763    /// Events for unrelated tokens are ignored. Callers remain responsible for
764    /// waiting on [`Self::process`] and driving the reactor until [`Self::io_done`]
765    /// returns true.
766    pub fn handle_reactor_event(
767        &mut self,
768        reactor: &mut Reactor,
769        event: &crate::fd::Event,
770    ) -> Result<(), CoreError> {
771        if self.drain.stdout_matches(event.token) {
772            if event.readable || event.hangup {
773                self.drain.handle_stdout_ready(reactor)?;
774            } else if event.error {
775                self.drain.drop_stdout(reactor)?;
776            }
777        } else if self.drain.stderr_matches(event.token) {
778            if event.readable || event.hangup {
779                self.drain.handle_stderr_ready(reactor)?;
780            } else if event.error {
781                self.drain.drop_stderr(reactor)?;
782            }
783        } else if self.drain.stdin_matches(event.token) {
784            if event.writable {
785                self.drain.handle_stdin_writable(reactor)?;
786            } else if event.error || event.hangup {
787                self.drain.drop_stdin(reactor)?;
788            }
789        }
790        Ok(())
791    }
792
793    /// Return whether all managed stdio pipes have been drained or closed.
794    pub fn io_done(&self) -> bool {
795        self.drain.is_done()
796    }
797
798    /// Consume the running process handle and return captured stdout/stderr buffers.
799    pub fn into_output_parts(self) -> (Vec<u8>, Vec<u8>) {
800        self.drain.into_parts()
801    }
802}
803
804impl ManagedProcess {
805    /// Return the child PID.
806    ///
807    /// The PID is captured at spawn time, so this remains available after the
808    /// process has completed (unlike the running handle, which is consumed).
809    pub fn pid(&self) -> pid_t {
810        self.pid
811    }
812
813    /// Register active child I/O descriptors with the caller's reactor.
814    pub fn register_with_reactor(&mut self, reactor: &mut Reactor) -> Result<(), CoreError> {
815        self.running
816            .as_mut()
817            .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?
818            .register_with_reactor(reactor)
819    }
820
821    /// Route one reactor event to the child's I/O drain state.
822    pub fn handle_reactor_event(
823        &mut self,
824        reactor: &mut Reactor,
825        event: &crate::fd::Event,
826    ) -> Result<(), CoreError> {
827        self.running
828            .as_mut()
829            .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?
830            .handle_reactor_event(reactor, event)
831    }
832
833    /// Request cancellation using the daemon-owned policy from
834    /// [`SpawnOptionsBuilder::cancel`]. Repeated requests are idempotent.
835    pub fn request_cancel(&mut self) {
836        self.cancel_at.get_or_insert_with(Instant::now);
837    }
838
839    /// Earliest time at which [`Self::poll_completion`] should run again.
840    ///
841    /// A bounded reap tick is returned while the child is live, and exact
842    /// timeout / TERM-to-KILL deadlines take precedence. `None` means the
843    /// completion was already consumed.
844    pub fn next_deadline(&self) -> Option<Instant> {
845        self.running.as_ref()?;
846        let now = Instant::now();
847        let mut next = now + Duration::from_millis(100);
848        if !self.timed_out
849            && let Some(timeout_at) = self.timeout_at
850            && timeout_at < next
851        {
852            next = timeout_at;
853        }
854        if self.kill_state == KillState::TermSent
855            && let Some(cancel_at) = self.cancel_at
856        {
857            let kill_at = cancel_at + self.kill_grace;
858            if kill_at < next {
859                next = kill_at;
860            }
861        }
862        // D-state bound: wake the caller once the post-SIGKILL reap window has
863        // elapsed so `poll_completion` can give up on an unreapable child.
864        if let Some(sent_at) = self.kill_sent_at {
865            let bail_at = sent_at + D_STATE_REAP_BOUND;
866            if bail_at < next {
867                next = bail_at;
868            }
869        }
870        Some(next)
871    }
872
873    /// Advance timeout/cancellation, reap state, and completion.
874    ///
875    /// Returns `Ok(None)` while work remains, the normal [`Output`] once the
876    /// child is reaped and its pipes are drained, or `EOVERFLOW` when the
877    /// configured combined output limit was exceeded on the fully-drained
878    /// path. A forced-close (timeout/cancel with a wedged pipe) returns the
879    /// partial output and the `timed_out` flag instead, matching blocking
880    /// [`spawn`].
881    pub fn poll_completion(&mut self, reactor: &mut Reactor) -> Result<Option<Output>, CoreError> {
882        let now = Instant::now();
883        if !self.timed_out
884            && let Some(timeout_at) = self.timeout_at
885            && now >= timeout_at
886        {
887            self.timed_out = true;
888            self.cancel_at.get_or_insert(timeout_at);
889        }
890
891        self.advance_cancel(now)?;
892
893        let running = self
894            .running
895            .as_ref()
896            .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?;
897        if self.status.is_none() {
898            self.status = running.process.wait_step()?;
899        }
900
901        let io_done = running.io_done();
902        if self.status.is_some() && (io_done || self.cancel_at.is_some()) {
903            return self.finish(reactor, !io_done).map(Some);
904        }
905        // D-state: SIGKILL sent but the child still cannot be reaped. A child
906        // stuck in uninterruptible sleep keeps the signal pending until it
907        // leaves D-state; return the partial output instead of polling forever.
908        if self.status.is_none()
909            && self
910                .kill_sent_at
911                .is_some_and(|t| now.duration_since(t) >= D_STATE_REAP_BOUND)
912        {
913            return self.finish(reactor, true).map(Some);
914        }
915        Ok(None)
916    }
917
918    fn advance_cancel(&mut self, now: Instant) -> Result<(), CoreError> {
919        let Some(cancel_at) = self.cancel_at else {
920            return Ok(());
921        };
922        // The child is already reaped — its pid may be recycled. Never signal.
923        if self.status.is_some() {
924            return Ok(());
925        }
926        let running = self
927            .running
928            .as_ref()
929            .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?;
930        let process = &running.process;
931        let pid = process.pid();
932        let pgid = effective_pgid(pid, self.pgroup);
933        let target_is_group = self.pgroup.isolated || self.pgroup.leader.is_some();
934        match self.kill_state {
935            KillState::None => match self.cancel {
936                CancelPolicy::None => {}
937                CancelPolicy::Graceful => {
938                    let result = signal_process(process, target_is_group, pgid, libc::SIGTERM);
939                    self.kill_state = if result.is_ok() {
940                        KillState::TermSent
941                    } else {
942                        KillState::KillSent
943                    };
944                    if self.kill_state == KillState::KillSent {
945                        self.kill_sent_at = Some(now);
946                    }
947                }
948                CancelPolicy::Kill => {
949                    let _ = signal_process(process, target_is_group, pgid, libc::SIGKILL);
950                    self.kill_state = KillState::KillSent;
951                    self.kill_sent_at = Some(now);
952                }
953            },
954            KillState::TermSent if now >= cancel_at + self.kill_grace => {
955                let _ = signal_process(process, target_is_group, pgid, libc::SIGKILL);
956                self.kill_state = KillState::KillSent;
957                self.kill_sent_at = Some(now);
958            }
959            _ => {}
960        }
961        Ok(())
962    }
963
964    fn finish(&mut self, reactor: &mut Reactor, force_close: bool) -> Result<Output, CoreError> {
965        let mut running = self
966            .running
967            .take()
968            .ok_or_else(|| CoreError::sys(libc::EINVAL, "managed process completed"))?;
969        for slot in running.drain.take_all_slots() {
970            if force_close {
971                let _ = reactor.del(&slot.fd);
972            } else {
973                reactor.del(&slot.fd)?;
974            }
975        }
976        let pid = running.process.pid();
977        let (stdout, stderr, output_limit_exceeded, stdout_early_exited) =
978            running.drain.into_parts_with_state();
979        // If the child was never reaped (D-state give-up / forced close with an
980        // unreapable child), it will eventually exit and become a zombie — hand
981        // it to the reaper so it does not accumulate in a long-lived daemon
982        // (finding 15).
983        if self.status.is_none() {
984            orphan_child(pid);
985        }
986        // Mirror blocking `spawn`: overflow is reported only when the drain
987        // completed naturally. On the forced-close path (timeout/cancel with a
988        // wedged pipe) the caller gets the partial output and the timed-out
989        // flag instead, matching the blocking N4 behavior.
990        if output_limit_exceeded && !force_close {
991            return Err(CoreError::sys(libc::EOVERFLOW, "spawn output limit"));
992        }
993        Ok(Output {
994            pid,
995            status: self.status.take(),
996            stdout,
997            stderr,
998            timed_out: self.timed_out,
999            stdout_early_exited,
1000        })
1001    }
1002}
1003
1004impl Drop for ManagedProcess {
1005    fn drop(&mut self) {
1006        let Some(running) = self.running.take() else {
1007            return;
1008        };
1009        // If the child was already reaped by `poll_completion`, the pid may
1010        // have been recycled — never signal it. The pipes are dropped with
1011        // `running`, so there is nothing left to clean up.
1012        if self.status.is_some() {
1013            return;
1014        }
1015        let process = &running.process;
1016        let pid = process.pid();
1017        let pgid = effective_pgid(pid, self.pgroup);
1018        let target_is_group = self.pgroup.isolated || self.pgroup.leader.is_some();
1019        let _ = signal_process(process, target_is_group, pgid, libc::SIGKILL);
1020        // Bound the reap wait: SIGKILL terminates a runnable child
1021        // immediately, but a child stuck in uninterruptible sleep (D-state)
1022        // never dies. Poll with WNOHANG so `Drop` cannot wedge the caller's
1023        // reactor thread forever on a stuck child.
1024        let deadline = Instant::now() + Duration::from_millis(100);
1025        while Instant::now() < deadline {
1026            match process.wait_step() {
1027                Ok(Some(_)) => return,
1028                Ok(None) => std::thread::sleep(Duration::from_millis(5)),
1029                Err(_) => return,
1030            }
1031        }
1032    }
1033}
1034
1035fn effective_pgid(pid: pid_t, pgroup: ProcessGroup) -> pid_t {
1036    match pgroup.leader {
1037        Some(0) | None => pid,
1038        Some(leader) => leader,
1039    }
1040}
1041
1042fn signal_process(
1043    process: &Process,
1044    target_is_group: bool,
1045    pgid: pid_t,
1046    signal: i32,
1047) -> Result<(), CoreError> {
1048    if target_is_group {
1049        process.kill_group(pgid, signal)
1050    } else {
1051        process.kill(signal)
1052    }
1053}
1054
1055/// Start spawning a process and return a monitor handle.
1056///
1057/// This initializes the pipes and starts the process, but does not block. Use
1058/// [`RunningProcess::register_with_reactor`],
1059/// [`RunningProcess::handle_reactor_event`], [`RunningProcess::io_done`], and
1060/// [`RunningProcess::into_output_parts`] to drive captured stdio without
1061/// exposing internal drain state.
1062///
1063/// ### Errors
1064/// - `EACCES`: Permission denied for the executable.
1065/// - `EINVAL`: Invalid spawn options (e.g. background capture without wait).
1066/// - `EMFILE`: Process limit on open file descriptors hit.
1067/// - `ENOENT`: The executable was not found.
1068/// - `ENOMEM`: Insufficient memory to spawn the process.
1069pub fn spawn_start(opts: SpawnOptions) -> Result<RunningProcess, CoreError> {
1070    if !opts.wait && (opts.stdin.is_some() || opts.capture_stdout || opts.capture_stderr) {
1071        return Err(CoreError::sys(
1072            libc::EINVAL,
1073            "background I/O capture not supported (wait must be true)",
1074        ));
1075    }
1076
1077    validate_backend(&opts)?;
1078
1079    let (pid, drain) = match opts.backend {
1080        SpawnBackend::PosixSpawn => spawn_posix_internal(opts)?,
1081        SpawnBackend::Fork => spawn_fork_internal(opts)?,
1082    };
1083
1084    Ok(RunningProcess {
1085        process: Process::new(pid),
1086        drain,
1087    })
1088}
1089
1090/// Start a process whose complete lifecycle is driven by a caller-owned
1091/// reactor.
1092pub fn spawn_managed(opts: SpawnOptions) -> Result<ManagedProcess, CoreError> {
1093    if !opts.wait {
1094        return Err(CoreError::sys(
1095            libc::EINVAL,
1096            "managed process requires wait=true",
1097        ));
1098    }
1099    let timeout_at = opts
1100        .timeout_ms
1101        .map(|ms| Instant::now() + Duration::from_millis(ms as u64));
1102    let kill_grace = Duration::from_millis(opts.kill_grace_ms as u64);
1103    let cancel = opts.cancel;
1104    let pgroup = opts.pgroup;
1105    let running = spawn_start(opts)?;
1106    let pid = running.process.pid();
1107    Ok(ManagedProcess {
1108        running: Some(running),
1109        pid,
1110        timeout_at,
1111        kill_grace,
1112        cancel,
1113        pgroup,
1114        cancel_at: None,
1115        kill_state: KillState::None,
1116        status: None,
1117        timed_out: false,
1118        kill_sent_at: None,
1119    })
1120}
1121
1122/// Spawn a process and block until completion or timeout.
1123///
1124/// This is the primary high-level interface for process execution. It handles
1125/// the full lifecycle, including I/O multiplexing and signal management.
1126///
1127/// ### Errors
1128/// Returns the same errors as [`spawn_start`], plus any I/O or reactor errors
1129/// encountered during the wait loop.
1130pub fn spawn(opts: SpawnOptions) -> Result<Output, CoreError> {
1131    let wait = opts.wait;
1132    let timeout_ms = opts.timeout_ms;
1133    let kill_grace_ms = opts.kill_grace_ms;
1134    let cancel = opts.cancel;
1135    let pgroup = opts.pgroup;
1136
1137    let mut reactor = Reactor::new()?;
1138    let running = spawn_start(opts)?;
1139
1140    let pid = running.process.pid();
1141    let mut drain = running.drain;
1142
1143    drain.register_with_reactor(&mut reactor)?;
1144
1145    if !wait {
1146        let (stdout, stderr) = drain.into_parts();
1147        // The caller will never `wait` on this pid — hand it to the reaper so
1148        // it does not become a zombie when it exits (finding 15).
1149        orphan_child(pid);
1150        return Ok(Output {
1151            pid,
1152            status: None,
1153            stdout,
1154            stderr,
1155            timed_out: false,
1156            stdout_early_exited: false,
1157        });
1158    }
1159
1160    wait_loop(
1161        pid,
1162        drain,
1163        reactor,
1164        timeout_ms,
1165        kill_grace_ms,
1166        cancel,
1167        pgroup,
1168    )
1169}
1170
1171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1172enum KillState {
1173    None,
1174    TermSent,
1175    KillSent,
1176}
1177
1178fn wait_loop(
1179    pid: pid_t,
1180    mut drain: crate::io::DrainState<fn(&[u8]) -> bool>,
1181    mut reactor: Reactor,
1182    timeout_ms: Option<u32>,
1183    kill_grace_ms: u32,
1184    cancel: CancelPolicy,
1185    pgroup: ProcessGroup,
1186) -> Result<Output, CoreError> {
1187    let process = Process::new(pid);
1188    // M8: the child's effective pgid is the configured leader when one is set
1189    // (Setpgid is applied after Setsid in the child), else its own pid. A
1190    // timeout must signal `-pgid`; `kill(-pid)` would target a different
1191    // group for a custom leader and the child would never die.
1192    let pgid = effective_pgid(pid, pgroup);
1193    let mut status_raw = process.wait_step()?;
1194    let mut state = KillState::None;
1195    let mut timed_out = false;
1196    // D-state bound: recorded once SIGKILL has been sent. If the child still
1197    // refuses to die (or be reaped) after `D_STATE_REAP_BOUND`, give up and
1198    // return the partial output instead of spinning on a stuck child.
1199    let mut kill_sent_at: Option<Instant> = None;
1200    // Deadline give-up bound for `CancelPolicy::None`: no signal is ever sent,
1201    // so `kill_sent_at` stays unset and the D-state bound never fires. A wedged
1202    // child (pipe held open by a descendant, child unreaped) would otherwise
1203    // poll at 100 ms forever. Once the deadline has passed we give up after the
1204    // same bound, returning the partial output with `timed_out` set.
1205    let mut deadline_passed_at: Option<Instant> = None;
1206
1207    let start_time = std::time::Instant::now();
1208    let deadline = timeout_ms.map(|t| std::time::Duration::from_millis(t as u64));
1209
1210    loop {
1211        let mut poll_timeout = -1;
1212
1213        if let Some(dl) = deadline {
1214            let elapsed = start_time.elapsed();
1215            if elapsed >= dl {
1216                timed_out = true;
1217                deadline_passed_at = Some(deadline_passed_at.unwrap_or_else(Instant::now));
1218                let elapsed_over = (elapsed - dl).as_millis();
1219
1220                let target_is_group = pgroup.isolated || pgroup.leader.is_some();
1221
1222                // Only signal while the child is unreaped. Once waitpid has
1223                // reaped it the pid may already be recycled by the OS — killing
1224                // it would hit an unrelated process. The wedged-pipe path below
1225                // returns the partial output without sending any signal.
1226                if status_raw.is_none() {
1227                    match state {
1228                        KillState::None => {
1229                            if cancel == CancelPolicy::Graceful {
1230                                let r = if target_is_group {
1231                                    process.kill_group(pgid, libc::SIGTERM)
1232                                } else {
1233                                    process.kill(libc::SIGTERM)
1234                                };
1235                                if r.is_err() {
1236                                    state = KillState::KillSent; // Process already gone
1237                                    kill_sent_at = Some(Instant::now());
1238                                } else {
1239                                    state = KillState::TermSent;
1240                                }
1241                            } else if cancel == CancelPolicy::Kill {
1242                                let _ = if target_is_group {
1243                                    process.kill_group(pgid, libc::SIGKILL)
1244                                } else {
1245                                    process.kill(libc::SIGKILL)
1246                                };
1247                                state = KillState::KillSent;
1248                                kill_sent_at = Some(Instant::now());
1249                            } else {
1250                                // CancelPolicy::None just times out without killing
1251                            }
1252                        }
1253                        KillState::TermSent if elapsed_over > kill_grace_ms as u128 => {
1254                            let _ = if target_is_group {
1255                                process.kill_group(pgid, libc::SIGKILL)
1256                            } else {
1257                                process.kill(libc::SIGKILL)
1258                            };
1259                            state = KillState::KillSent;
1260                            kill_sent_at = Some(Instant::now());
1261                        }
1262                        _ => {}
1263                    }
1264                }
1265                poll_timeout = 100; // Poll frequently while waiting for kill to take effect
1266            } else {
1267                let remaining = dl - elapsed;
1268                poll_timeout = remaining.as_millis().min(i32::MAX as u128) as i32;
1269            }
1270        }
1271
1272        if status_raw.is_none()
1273            && let Some(s) = process.wait_step()?
1274        {
1275            status_raw = Some(s);
1276        }
1277
1278        if drain.is_done() {
1279            let s = if status_raw.is_some() {
1280                status_raw.take()
1281            } else if deadline.is_none() {
1282                // C1: all pipes drained but the child is still alive, and no
1283                // deadline is set → block until it exits (intended semantics).
1284                Some(process.wait_blocking()?)
1285            } else {
1286                // C1: pipes drained with a deadline set → never block here; fall
1287                // through to the bounded `reactor.wait` below so the deadline
1288                // logic at the top of the loop kills and reaps. A later
1289                // `wait_step` reaps the child and we return from this branch.
1290                None
1291            };
1292
1293            if let Some(s) = s {
1294                for slot in drain.take_all_slots() {
1295                    reactor.del(&slot.fd)?;
1296                }
1297                let (stdout, stderr, output_limit_exceeded, stdout_early_exited) =
1298                    drain.into_parts_with_state();
1299                if output_limit_exceeded {
1300                    return Err(CoreError::sys(libc::EOVERFLOW, "spawn output limit"));
1301                }
1302                return Ok(Output {
1303                    pid,
1304                    status: Some(s),
1305                    stdout,
1306                    stderr,
1307                    timed_out,
1308                    stdout_early_exited,
1309                });
1310            }
1311        }
1312
1313        // N4: the deadline has elapsed and the child is reaped, but a wedged
1314        // pipe (a descendant inheriting the write end) keeps the drain from
1315        // closing. The absolute deadline is authoritative — return the partial
1316        // output instead of spinning forever.
1317        if timed_out && status_raw.is_some() {
1318            for slot in drain.take_all_slots() {
1319                let _ = reactor.del(&slot.fd);
1320            }
1321            let (stdout, stderr, _output_limit_exceeded, stdout_early_exited) =
1322                drain.into_parts_with_state();
1323            return Ok(Output {
1324                pid,
1325                status: status_raw,
1326                stdout,
1327                stderr,
1328                timed_out: true,
1329                stdout_early_exited,
1330            });
1331        }
1332
1333        // D-state: SIGKILL has been sent but the child is still unreaped after
1334        // the bound. A child stuck in uninterruptible sleep keeps the signal
1335        // pending until it leaves D-state, so no further wait can succeed —
1336        // return the partial output rather than polling forever. The pid is
1337        // not signaled again (it may be recycled once it finally exits).
1338        if let Some(sent_at) = kill_sent_at
1339            && sent_at.elapsed() >= D_STATE_REAP_BOUND
1340            && status_raw.is_none()
1341        {
1342            for slot in drain.take_all_slots() {
1343                let _ = reactor.del(&slot.fd);
1344            }
1345            // The child is unreapable right now but will eventually leave
1346            // D-state and exit; nobody will wait on it after this give-up, so
1347            // hand it to the reaper (finding 15).
1348            orphan_child(pid);
1349            let (stdout, stderr, _output_limit_exceeded, stdout_early_exited) =
1350                drain.into_parts_with_state();
1351            return Ok(Output {
1352                pid,
1353                status: None,
1354                stdout,
1355                stderr,
1356                timed_out: true,
1357                stdout_early_exited,
1358            });
1359        }
1360
1361        // `CancelPolicy::None`: the deadline elapsed but nothing was ever
1362        // signaled, so the child may stay wedged (pipe held by a descendant,
1363        // child unreaped) indefinitely. Give up with the partial output after
1364        // the same bound as the D-state path — otherwise this polls at 100 ms
1365        // forever (finding 14).
1366        if cancel == CancelPolicy::None
1367            && timed_out
1368            && status_raw.is_none()
1369            && deadline_passed_at.is_some_and(|passed| passed.elapsed() >= D_STATE_REAP_BOUND)
1370        {
1371            for slot in drain.take_all_slots() {
1372                let _ = reactor.del(&slot.fd);
1373            }
1374            // The child was never signaled and may still be running; nobody
1375            // will wait on it now — hand it to the reaper (finding 15).
1376            orphan_child(pid);
1377            let (stdout, stderr, _output_limit_exceeded, stdout_early_exited) =
1378                drain.into_parts_with_state();
1379            return Ok(Output {
1380                pid,
1381                status: None,
1382                stdout,
1383                stderr,
1384                timed_out: true,
1385                stdout_early_exited,
1386            });
1387        }
1388
1389        let timeout = poll_timeout;
1390
1391        let mut events = Vec::new();
1392        let nevents = reactor.wait(&mut events, 64, timeout)?;
1393
1394        for ev in events.iter().take(nevents) {
1395            if drain.stdout_matches(ev.token) {
1396                if ev.readable || ev.hangup {
1397                    drain.handle_stdout_ready(&mut reactor)?;
1398                } else if ev.error {
1399                    drain.drop_stdout(&mut reactor)?;
1400                }
1401            } else if drain.stderr_matches(ev.token) {
1402                if ev.readable || ev.hangup {
1403                    drain.handle_stderr_ready(&mut reactor)?;
1404                } else if ev.error {
1405                    drain.drop_stderr(&mut reactor)?;
1406                }
1407            } else if drain.stdin_matches(ev.token) {
1408                if ev.writable {
1409                    drain.handle_stdin_writable(&mut reactor)?;
1410                } else if ev.error || ev.hangup {
1411                    drain.drop_stdin(&mut reactor)?;
1412                }
1413            }
1414        }
1415    }
1416}