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