Skip to main content

rightkit_child_process/
unix_child.rs

1//! The Unix child shared by `OwnedChild` and every `ProcessHandle` clone, and the one place a
2//! signal to a child or its process group is sent.
3//!
4//! # Identity rule
5//!
6//! A pid, and with it the process-group id the child leads, cannot be recycled while the
7//! child is a zombie: the kernel releases the number only when the parent reaps it, and only
8//! this process can reap its own child. So a group signal is safe exactly while this process
9//! still holds the leader as its own **unreaped** child.
10//!
11//! * Every reap goes through [`UnixChild`], under the lock that guards it. The std `Child` is
12//!   private, so no path can reap behind this type's back.
13//! * Every group signal is sent under that same lock, after the leader is confirmed unreaped,
14//!   and therefore before any reap that could follow it.
15//! * After any reap, by any path (a wait, a `try_wait`, a handle or a clone, the detached
16//!   reaper, the public [`OwnedChild::child`](crate::OwnedChild::child) accessor), the
17//!   state is `Reaped` for good. Group signalling is over for this child and all its clones:
18//!   no signal, and no signal-0 membership probe, is ever sent again.
19//!
20//! * **Unreaped by this type is not unreaped by the kernel.** Other code in the process (a
21//!   `waitpid(-1)` loop) can collect our child behind our back, and a replacement child can then
22//!   take the same number. So every action that names the pid (a signal, a `waitpid`, a
23//!   `waitid` probe, a membership probe) is preceded by [`UnixChild::prove`], which compares the
24//!   kernel's description of the pid (parent and start time, read at spawn) with the process
25//!   we spawned. A different or missing description marks the leader `Lost` and nothing is
26//!   sent or waited. The proof and the call that follows it are two system calls: foreign code
27//!   that reaps the child in exactly that gap, and a replacement that takes the number there,
28//!   defeat it. This is a known limit; see `identity`.
29//!
30//! # Waiting
31//!
32//! * **A wait on a shared child is a loop of non-blocking looks.** [`wait_shared`] takes the
33//!   lock, makes one non-blocking `try_wait` (a `waitpid(WNOHANG)`), releases the lock, sleeps,
34//!   and looks again until the leader is reaped or the caller's deadline passes.
35//!   [`observe_shared`] does the same with the non-reaping `waitid(WNOHANG | WNOWAIT)` probe.
36//!   `ProcessHandle::wait`, `wait_timeout` and `wait_exited*`, `OwnedChild::wait*`,
37//!   `wait_or_kill`, `terminate_tree*`, `Drop`, the detached reaper thread
38//!   (`rightkit-detached-reap`) and, up to its timeout, the failed-spawn cleanup all wait this
39//!   way.
40//!   None of them reaps with a blocking call or sleeps while holding the lock, so `kill`,
41//!   `terminate_tree` and every other waiter run while one of them is in progress, and a
42//!   deadline is honoured whatever the child does.
43//! * The sleep is [`ExitWatch::pause`](crate::exit_watch::ExitWatch::pause): a hint. With an
44//!   event source it ends early at the child's exit; without one it is a 5 ms interval. The
45//!   event never decides anything, so it cannot make a wait return early or hang.
46//! * **Where the lock is held across a wait or a long action (the complete list).** None of
47//!   these is a bounded wait that entered a blocking reap; the first is the only one a
48//!   deadline-bearing caller on another thread can be held behind.
49//!   1. [`UnixChild::wait`] through the `OwnedChild::child()` guard. The caller holds the lock
50//!      by holding the guard, so the wait cannot release it: a bounded wait on a clone, a
51//!      `kill`, `terminate_tree` or `Drop` on another thread waits behind it until the child
52//!      has exited, and can exceed its own deadline. That is the caller's choice, not the
53//!      crate's: `std::process::Child::wait` through the same guard did exactly this in
54//!      0.3.3 (documented: "release this guard before another accessor or exit reader").
55//!      The wait still reaps only with non-blocking looks. A program that needs a deadline
56//!      or a cancel to stay live must wait through `OwnedChild`/`ProcessHandle` instead.
57//!   2. `cleanup_direct` with no timeout (`partial_spawn_cleanup_timeout` not set), and
58//!   3. the `rightkit-partial-reap` thread it hands a timed-out child to. Both call
59//!      `UnixChild::wait` under the lock, as 0.3.3 called `std::process::Child::wait`. They
60//!      run only for a child whose spawn failed after the process existed, and that `Arc`
61//!      is held by nothing but the spawning thread (the partial `OwnedChild`, which no
62//!      caller has seen and from which no `ProcessHandle` was cloned) and then by the reaper
63//!      thread: no other thread can reach the lock, so nothing can be held behind it.
64//!   4. `OwnedChild::while_unreaped` holds the lock while the caller's closure runs, which
65//!      is its purpose (no reap can fall between the proof and the action). A bounded wait on
66//!      another thread can be delayed by as long as the closure takes.
67//!      `rightkit-sandbox` passes a descendant sweep with a 200 ms grace. New since 0.3.3.
68//!
69//!   Unidentified spawn rejection uses `cleanup_unidentified`: runner callers supply a finite
70//!   cleanup budget; ordinary commands retain their existing unlimited cleanup default.
71//! * No wait-class system call names the pid while the lock is released. A blocked
72//!   `waitpid(pid)`/`waitid(P_PID, pid)` outside the lock could be entered after another reader
73//!   reaped the leader and the kernel recycled the number, and would then wait on an unrelated
74//!   child.
75//! * Seeing that the leader exited does not require reaping it (`UnixChild::leader_state`), so
76//!   an exit monitor can observe the exit and leave the reap, and with it the group's identity,
77//!   to the owner.
78//!
79//! The state is one lock-guarded enum, never separate atomics, so no thread can observe a
80//! half-published transition.
81
82use crate::exit_watch::ExitWatch;
83use crate::identity::{Origin, Proof};
84use crate::process_handle::lock_child;
85use std::{
86    io,
87    process::{Child, ChildStderr, ChildStdin, ChildStdout, ExitStatus},
88    sync::{Arc, Mutex},
89    time::{Duration, Instant},
90};
91
92/// Whether this host can see that the leader exited without reaping it (`probe_exit` is real).
93/// Where it cannot, `ProcessHandle::wait_exited` falls back to a reaping wait.
94pub(crate) const CAN_OBSERVE_WITHOUT_REAPING: bool = cfg!(any(
95    target_os = "linux",
96    target_os = "android",
97    target_os = "macos",
98    target_os = "ios"
99));
100
101/// What this process knows about the leader. Only [`UnixChild`] changes it, under the lock.
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103enum Leader {
104    /// Not reaped by this process: the zombie, or the running process, still pins the id.
105    Unreaped,
106    /// Reaped through this type; the cached status is returned to every later reader.
107    Reaped(ExitStatus),
108    /// The kernel says the pid is no longer our child although we never reaped it (something
109    /// else in the process reaped it). Treated as reaped; the status is lost.
110    Lost,
111}
112
113/// Where the leader is, as far as a non-reaping look can tell.
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub(crate) enum LeaderState {
116    Running,
117    /// Exited but not reaped: a zombie that still pins the id. The last moment a group
118    /// signal is possible.
119    Exited,
120    Reaped,
121}
122
123/// Who is in the leader's process group, besides the leader.
124#[derive(Clone, Debug, PartialEq, Eq)]
125pub(crate) enum Members {
126    Pids(Vec<u32>),
127    /// The host cannot enumerate the group (or it is larger than one listing).
128    Unknown,
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub(crate) enum SignalTarget {
133    /// `kill(-pgid, ..)`: the leader's whole process group.
134    Group,
135    /// `kill(pid, ..)`: the leader alone (detached children, which own only their pid).
136    Leader,
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub(crate) enum SignalOutcome {
141    Sent,
142    /// The leader was already reaped, so nothing was sent.
143    Retired,
144    /// The target was already gone (`ESRCH`, or only zombies were left).
145    Gone,
146    /// A group signal answered `EPERM` while the leader still reads as running (macOS does
147    /// this when the last members are mid-exit). Nothing was sent. The caller may look again
148    /// after the lock is released: the group is gone as soon as the exit completes.
149    MidExit,
150}
151
152/// A spawned Unix child. The same API as Windows' `WindowsChild`: public stdio fields, `id`,
153/// `try_wait`, `wait` and `kill`. It does not expose the std `Child`, because a std reap the
154/// crate cannot see would make later group signals unsafe (see the module documentation).
155///
156/// Obtained through [`OwnedChild::child`](crate::OwnedChild::child), which holds the shared
157/// lock while the guard lives. `wait` through the guard therefore holds that lock until the
158/// child has exited, and every other thread's access to the same child (a bounded wait on a
159/// clone, `kill`, `terminate_tree`, `Drop`) waits behind it, as `std::process::Child::wait`
160/// through the guard did in 0.3.3. Prefer `OwnedChild::wait`, `wait_timeout` or
161/// `ProcessHandle::wait`, which release the lock between looks.
162#[derive(Debug)]
163pub struct UnixChild {
164    pub stdin: Option<ChildStdin>,
165    pub stdout: Option<ChildStdout>,
166    pub stderr: Option<ChildStderr>,
167    child: Child,
168    pid: u32,
169    leader: Leader,
170    /// Who the leader was at spawn. Checked by [`UnixChild::prove`] before any action by pid.
171    origin: Origin,
172    /// The wake-up hint for waiters (never a verdict), opened at spawn and shared with every
173    /// waiter.
174    watch: Arc<ExitWatch>,
175    /// Test only: the id used as the signal target, to stand in for a recycled id.
176    #[cfg(test)]
177    recycled_id: Option<u32>,
178}
179
180impl UnixChild {
181    pub(crate) fn new(mut child: Child) -> Self {
182        // Both are taken now, while the child is certainly still ours: the hint is bound to the
183        // process, and the identity names it. (A child collected by foreign code before this
184        // point would hand its replacement's description over as the baseline; that is part of
185        // the known limit described in `identity`.)
186        let watch = Arc::new(ExitWatch::open(child.id()));
187        let origin = Origin::capture(child.id());
188        Self {
189            stdin: child.stdin.take(),
190            stdout: child.stdout.take(),
191            stderr: child.stderr.take(),
192            pid: child.id(),
193            origin,
194            watch,
195            child,
196            leader: Leader::Unreaped,
197            #[cfg(test)]
198            recycled_id: None,
199        }
200    }
201
202    /// The wake-up hint, for waiters that sleep without holding the lock.
203    pub(crate) fn exit_watch(&self) -> Arc<ExitWatch> {
204        Arc::clone(&self.watch)
205    }
206
207    pub fn id(&self) -> u32 {
208        self.pid
209    }
210
211    /// Spawn-time check: a host that has an identity source but would not describe the child
212    /// cannot prove later that its pid is still the child's, so the child is not handed out.
213    /// Caller must stop & reap a rejected child through `cleanup_unidentified` before returning.
214    pub(crate) fn require_identity(&mut self) -> io::Result<()> {
215        if !matches!(self.origin, Origin::Missing) {
216            return Ok(());
217        }
218        Err(io::Error::other(
219            "the spawned child could not be identified; it must be stopped because a later \
220             signal could not be proven to reach it",
221        ))
222    }
223
224    /// Used only immediately after creation when identity capture failed. std Child owns
225    /// this just-created child, so kill/try_wait are directed at it before any wrapper escapes.
226    /// A finite caller budget transfers an unreaped child to an exclusive background reaper.
227    pub(crate) fn cleanup_unidentified(mut self, timeout: Option<Duration>) -> bool {
228        let _ = self.child.kill();
229        let Some(timeout) = timeout else {
230            return self.child.wait().is_ok();
231        };
232        let deadline = std::time::Instant::now().checked_add(timeout);
233        #[cfg(test)]
234        let refused = crate::owned::cleanup_test_fault::refuse(timeout);
235        #[cfg(not(test))]
236        let refused = false;
237        let reaped = if refused {
238            Ok(None)
239        } else {
240            self.watch.wait_for(deadline, || self.child.try_wait())
241        };
242        if matches!(reaped, Ok(Some(_))) {
243            return true;
244        }
245        let mut child = self.child;
246        let _ = std::thread::Builder::new()
247            .name("rightkit-partial-reap".into())
248            .spawn(move || {
249                let _ = child.wait();
250            });
251        false
252    }
253
254    /// The one proof every action by pid starts with: `Ok(true)` when the pid still names the
255    /// process this type spawned and has not reaped. `Ok(false)` (and the leader becomes
256    /// `Lost`, never touched again) when the number changed hands or the process is gone.
257    /// `Err` when the kernel cannot be asked: the caller must then act on nothing.
258    fn prove(&mut self) -> io::Result<bool> {
259        if !matches!(self.leader, Leader::Unreaped) {
260            return Ok(false);
261        }
262        #[cfg(test)]
263        trace::record(trace::Event::Proof { leader: self.pid });
264        if self.origin.check(self.pid)? == Proof::Same {
265            Ok(true)
266        } else {
267            self.leader = Leader::Lost;
268            Ok(false)
269        }
270    }
271
272    /// `prove`, as the guard of a wait: an unproven pid is reported as `ECHILD`, as the kernel
273    /// would for a child that is not ours.
274    fn require_ours(&mut self) -> io::Result<()> {
275        if self.prove()? {
276            Ok(())
277        } else {
278            Err(io::Error::from_raw_os_error(libc::ECHILD))
279        }
280    }
281
282    /// Test only: pretend the pid was described differently at spawn, as if our child had been
283    /// collected elsewhere and the number given to the process now holding it.
284    #[cfg(test)]
285    pub(crate) fn simulate_replaced(&mut self) {
286        self.origin = Origin::Known(crate::identity::Identity {
287            parent: 0,
288            started_sec: 1,
289            started_usec: 1,
290        });
291    }
292
293    /// Non-blocking. A `Some` result means the leader has been reaped, and ends group
294    /// signalling for this child.
295    pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
296        match self.leader {
297            Leader::Reaped(status) => return Ok(Some(status)),
298            Leader::Lost => return Err(io::Error::from_raw_os_error(libc::ECHILD)),
299            Leader::Unreaped => {}
300        }
301        if let Err(error) = self.require_ours() {
302            return Err(self.note_error(error));
303        }
304        #[cfg(test)]
305        trace::record(trace::Event::Wait { leader: self.pid });
306        match self.child.try_wait() {
307            Ok(Some(status)) => {
308                self.note_reaped(status);
309                Ok(Some(status))
310            }
311            Ok(None) => Ok(None),
312            Err(error) => Err(self.note_error(error)),
313        }
314    }
315
316    /// Closes stdin as std does, then waits until the leader is reaped. The reap itself is only
317    /// ever a non-blocking `waitpid(WNOHANG)` between sleeps, but this method cannot release
318    /// the lock: when it is reached through the `OwnedChild::child()` guard the caller holds
319    /// it for the whole wait, so other threads' waits, kills and drops on the same child
320    /// queue behind it (as with `std::process::Child::wait` in 0.3.3). Use `OwnedChild::wait`
321    /// or `ProcessHandle::wait` where that matters.
322    pub fn wait(&mut self) -> io::Result<ExitStatus> {
323        drop(self.stdin.take());
324        self.wait_reaping()
325    }
326
327    /// `SIGKILL` to the leader alone, only while it is unreaped. Nothing is sent, and `Ok` is
328    /// returned, once it has been reaped.
329    pub fn kill(&mut self) -> io::Result<()> {
330        self.signal(SignalTarget::Leader, libc::SIGKILL).map(|_| ())
331    }
332
333    /// Reaps without touching stdin, for the guard's `wait` and for cleanup of a child that
334    /// has just been killed. Each look is proven and non-blocking; between looks the thread
335    /// sleeps on the hint. A blocking `waitpid(pid)` from the start would name the number for
336    /// as long as it sleeps, and if other code collected the leader meanwhile it would wait on
337    /// whatever child the kernel gave the number to, and take its status.
338    pub(crate) fn wait_reaping(&mut self) -> io::Result<ExitStatus> {
339        let watch = Arc::clone(&self.watch);
340        watch
341            .wait_for(None, || self.try_wait())?
342            .ok_or_else(|| io::Error::other("an unbounded wait ended without a status"))
343    }
344
345    /// Looks at the leader without reaping it. `Lost` is recorded here when the kernel no
346    /// longer lists the pid as our child.
347    pub(crate) fn leader_state(&mut self) -> io::Result<LeaderState> {
348        if !self.prove()? {
349            return Ok(LeaderState::Reaped);
350        }
351        match probe_exit(self.pid) {
352            // The probe names the number once more; a replacement that took it in between and
353            // has itself exited would read as our zombie. A zombie is describable, so ask again.
354            Ok(true) => Ok(if self.prove()? {
355                LeaderState::Exited
356            } else {
357                LeaderState::Reaped
358            }),
359            Ok(false) => Ok(LeaderState::Running),
360            Err(error) if error.raw_os_error() == Some(libc::ECHILD) => {
361                self.leader = Leader::Lost;
362                Ok(LeaderState::Reaped)
363            }
364            Err(error) => Err(error),
365        }
366    }
367
368    /// Sends `signal` only if the leader is still unreaped, and does so under the caller's
369    /// lock, so no reap can fall between the check and the signal.
370    pub(crate) fn signal(
371        &mut self,
372        target: SignalTarget,
373        signal: i32,
374    ) -> io::Result<SignalOutcome> {
375        if self.leader_state()? == LeaderState::Reaped {
376            return Ok(SignalOutcome::Retired);
377        }
378        // The state above looked at the pid; prove it once more immediately before it is used
379        // as a signal target.
380        if !self.prove()? {
381            return Ok(SignalOutcome::Retired);
382        }
383        let id = self.signal_id() as i32;
384        let target_id = match target {
385            SignalTarget::Group => -id,
386            SignalTarget::Leader => id,
387        };
388        if raw_kill(self.pid, target_id, signal) == 0 {
389            return Ok(SignalOutcome::Sent);
390        }
391        let error = io::Error::last_os_error();
392        match error.raw_os_error() {
393            Some(libc::ESRCH) => Ok(SignalOutcome::Gone),
394            // macOS answers a group signal with EPERM, not ESRCH, when no member can take it:
395            // only zombies are left, or the last members are mid-exit. Our own children are
396            // always signalable, so this is never a real denial. Once the leader is no longer
397            // running (exited, or reaped by a handle clone) the group is gone. A leader that
398            // still reads as running is mid-exit: say so, and let the caller look again once it
399            // has released the lock (`OwnedChild::signal`).
400            Some(libc::EPERM) if target == SignalTarget::Group => {
401                Ok(if self.leader_state()? == LeaderState::Running {
402                    SignalOutcome::MidExit
403                } else {
404                    SignalOutcome::Gone
405                })
406            }
407            _ => Err(error),
408        }
409    }
410
411    /// The processes other than the leader still in the group.
412    /// Valid only while the leader is unreaped; a reaped leader has no group to speak of. An
413    /// unknown answer is [`Members::Unknown`]: the caller then waits out its grace instead of
414    /// cutting it short.
415    pub(crate) fn group_other_members(&mut self) -> io::Result<Members> {
416        if self.leader_state()? == LeaderState::Reaped {
417            return Ok(Members::Pids(Vec::new()));
418        }
419        group_members_besides(self.pid, self.signal_id())
420    }
421
422    /// True when a process other than the leader is still in the group (unknown counts as yes).
423    #[cfg(test)]
424    pub(crate) fn group_has_other_members(&mut self) -> io::Result<bool> {
425        Ok(match self.group_other_members()? {
426            Members::Pids(pids) => !pids.is_empty(),
427            Members::Unknown => true,
428        })
429    }
430
431    fn signal_id(&self) -> u32 {
432        #[cfg(test)]
433        if let Some(id) = self.recycled_id {
434            return id;
435        }
436        self.pid
437    }
438
439    /// Test only: aim every later signal at `id`, as if the kernel had recycled the leader's
440    /// pid and group id for an unrelated group.
441    #[cfg(test)]
442    pub(crate) fn simulate_recycled_id(&mut self, id: u32) {
443        self.recycled_id = Some(id);
444    }
445
446    fn note_reaped(&mut self, status: ExitStatus) {
447        self.leader = Leader::Reaped(status);
448        #[cfg(test)]
449        trace::record(trace::Event::Reaped { leader: self.pid });
450    }
451
452    fn note_error(&mut self, error: io::Error) -> io::Error {
453        if error.raw_os_error() == Some(libc::ECHILD) {
454            self.leader = Leader::Lost;
455        }
456        error
457    }
458}
459
460/// `waitid(P_PID, pid, WEXITED | WNOHANG | WNOWAIT)`: has the child exited, without reaping
461/// it? `ECHILD` means the pid is not (or no longer) our child.
462#[cfg(any(
463    target_os = "linux",
464    target_os = "android",
465    target_os = "macos",
466    target_os = "ios"
467))]
468fn probe_exit(pid: u32) -> io::Result<bool> {
469    loop {
470        #[cfg(test)]
471        trace::record(trace::Event::Wait { leader: pid });
472        // SAFETY: an all-zero siginfo_t is valid, and waitid fills it.
473        let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
474        // SAFETY: valid out pointer; WNOWAIT leaves the zombie in place.
475        let result = unsafe {
476            libc::waitid(
477                libc::P_PID,
478                pid as libc::id_t,
479                &mut info,
480                libc::WEXITED | libc::WNOHANG | libc::WNOWAIT,
481            )
482        };
483        if result == 0 {
484            #[cfg(any(target_os = "linux", target_os = "android"))]
485            // SAFETY: si_pid is valid after a successful waitid.
486            let reported = unsafe { info.si_pid() };
487            #[cfg(not(any(target_os = "linux", target_os = "android")))]
488            let reported = info.si_pid;
489            return Ok(reported as u32 == pid);
490        }
491        let error = io::Error::last_os_error();
492        if error.raw_os_error() != Some(libc::EINTR) {
493            return Err(error);
494        }
495    }
496}
497
498/// Other Unix hosts have no verified non-reaping probe here. The reaped state still guards
499/// every signal; only the early "leader exited" observation is unavailable.
500#[cfg(not(any(
501    target_os = "linux",
502    target_os = "android",
503    target_os = "macos",
504    target_os = "ios"
505)))]
506fn probe_exit(_pid: u32) -> io::Result<bool> {
507    Ok(false)
508}
509
510/// The single place a signal reaches a child or its group, so tests can observe every one
511/// (signal 0 included).
512fn raw_kill(leader: u32, target: i32, signal: i32) -> i32 {
513    #[cfg(test)]
514    trace::record(trace::Event::Signal {
515        leader,
516        target,
517        signal,
518    });
519    #[cfg(not(test))]
520    let _ = leader;
521    // SAFETY: plain kill(2). The callers hold the lock and have just confirmed the leader is
522    // unreaped, which pins the target id.
523    unsafe { libc::kill(target, signal) }
524}
525
526#[cfg(target_os = "macos")]
527fn group_members_besides(leader: u32, group: u32) -> io::Result<Members> {
528    /// `PROC_PGRP_ONLY` from `<libproc.h>`: list the members of one process group.
529    const PROC_PGRP_ONLY: u32 = 2;
530    const CAPACITY: usize = 256;
531    let mut pids = [0 as libc::pid_t; CAPACITY];
532    // SAFETY: the buffer is CAPACITY pids and the size passed says so.
533    let bytes = unsafe {
534        libc::proc_listpids(
535            PROC_PGRP_ONLY,
536            group,
537            pids.as_mut_ptr().cast(),
538            std::mem::size_of_val(&pids) as libc::c_int,
539        )
540    };
541    if bytes <= 0 {
542        // Fall back to signal 0: ESRCH, or EPERM when only zombies remain, means empty.
543        if raw_kill(leader, -(group as i32), 0) == 0 {
544            return Ok(Members::Unknown);
545        }
546        return Ok(match io::Error::last_os_error().raw_os_error() {
547            Some(libc::ESRCH) | Some(libc::EPERM) => Members::Pids(Vec::new()),
548            _ => Members::Unknown,
549        });
550    }
551    let count = (bytes as usize / std::mem::size_of::<libc::pid_t>()).min(CAPACITY);
552    if count >= CAPACITY {
553        return Ok(Members::Unknown);
554    }
555    Ok(Members::Pids(
556        pids[..count]
557            .iter()
558            .filter(|&&pid| pid > 0 && pid as u32 != leader)
559            .map(|&pid| pid as u32)
560            .collect(),
561    ))
562}
563
564#[cfg(any(target_os = "linux", target_os = "android"))]
565fn group_members_besides(leader: u32, group: u32) -> io::Result<Members> {
566    // kill(-pgid, 0) cannot tell an exited, unreaped leader from a live member, so read
567    // /proc: a member is any non-zombie process whose group is this one.
568    let Ok(entries) = std::fs::read_dir("/proc") else {
569        return Ok(Members::Unknown);
570    };
571    let mut members = Vec::new();
572    for entry in entries.flatten() {
573        let Some(pid) = entry
574            .file_name()
575            .to_str()
576            .and_then(|name| name.parse::<u32>().ok())
577        else {
578            continue;
579        };
580        if pid == leader {
581            continue;
582        }
583        let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
584            continue; // exited while we looked
585        };
586        let Some(rest) = stat.rsplit_once(')').map(|(_, rest)| rest) else {
587            return Ok(Members::Unknown);
588        };
589        let mut fields = rest.split_whitespace();
590        let (Some(state), Some(_parent), Some(pgrp)) =
591            (fields.next(), fields.next(), fields.next())
592        else {
593            return Ok(Members::Unknown);
594        };
595        if pgrp.parse::<u32>().ok() == Some(group) && state != "Z" && state != "X" {
596            members.push(pid);
597        }
598    }
599    Ok(Members::Pids(members))
600}
601
602#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
603fn group_members_besides(_leader: u32, _group: u32) -> io::Result<Members> {
604    Ok(Members::Unknown)
605}
606
607/// Waits for the leader of `shared` to be reaped, or for `deadline` to pass (`Ok(None)`); `None`
608/// waits without limit.
609///
610/// Every look is one non-blocking `try_wait` under the lock, released before the thread sleeps
611/// (see the module documentation), so a deadline is honoured whatever the child does, a
612/// `kill` or `terminate_tree` from another thread runs at once, and no wait-class system call
613/// names the pid while the lock is free. `ProcessHandle` waits this way, and so does the
614/// failed-spawn cleanup that has a timeout; the cleanup without one waits through
615/// `UnixChild::wait` instead (see the module documentation, item 2).
616pub(crate) fn wait_shared(
617    shared: &Mutex<UnixChild>,
618    deadline: Option<Instant>,
619) -> io::Result<Option<ExitStatus>> {
620    let (watch, pid) = {
621        let child = lock_child(shared);
622        (child.exit_watch(), child.id())
623    };
624    let mut first = true;
625    watch.wait_for(deadline, || {
626        let looked = lock_child(shared).try_wait();
627        pause_for_test(&mut first, pid, matches!(looked, Ok(None)));
628        looked
629    })
630}
631
632/// Like [`wait_shared`] but does not reap: `Ok(true)` once the leader has exited (or was reaped
633/// by someone), `Ok(false)` when `deadline` passed first. The zombie keeps pinning the group id.
634pub(crate) fn observe_shared(
635    shared: &Mutex<UnixChild>,
636    deadline: Option<Instant>,
637) -> io::Result<bool> {
638    let (watch, pid) = {
639        let child = lock_child(shared);
640        (child.exit_watch(), child.id())
641    };
642    let mut first = true;
643    let seen = watch.wait_for(deadline, || {
644        let state = lock_child(shared).leader_state();
645        pause_for_test(&mut first, pid, matches!(state, Ok(LeaderState::Running)));
646        Ok(match state? {
647            LeaderState::Running => None,
648            LeaderState::Exited | LeaderState::Reaped => Some(()),
649        })
650    })?;
651    Ok(seen.is_some())
652}
653
654/// Test seam: after the first look that found the child still running, with the lock already
655/// released and before the thread sleeps, run the hook installed for `pid` (see [`gate`]).
656#[cfg(test)]
657fn pause_for_test(first: &mut bool, pid: u32, running: bool) {
658    if *first && running {
659        *first = false;
660        gate::pause(pid);
661    }
662}
663
664#[cfg(not(test))]
665fn pause_for_test(_first: &mut bool, _pid: u32, _running: bool) {}
666
667/// Test instrumentation: every signal, every reap and every wait-class system call that names
668/// the leader's pid, in the order they happened. All are recorded under the child's lock, so the
669/// order is the real order.
670#[cfg(test)]
671pub(crate) mod trace {
672    use std::sync::Mutex;
673
674    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
675    pub(crate) enum Event {
676        Signal {
677            leader: u32,
678            target: i32,
679            signal: i32,
680        },
681        Reaped {
682            leader: u32,
683        },
684        /// The pid was proven to still name the spawned process (see `UnixChild::prove`).
685        Proof {
686            leader: u32,
687        },
688        /// A `waitpid`/`waitid` (probe or reap) was issued naming this pid. After `Reaped` the
689        /// number may belong to someone else, so none may follow.
690        Wait {
691            leader: u32,
692        },
693    }
694
695    static EVENTS: Mutex<Vec<Event>> = Mutex::new(Vec::new());
696
697    pub(crate) fn record(event: Event) {
698        EVENTS.lock().unwrap_or_else(|p| p.into_inner()).push(event);
699    }
700
701    pub(crate) fn mark() -> usize {
702        EVENTS.lock().unwrap_or_else(|p| p.into_inner()).len()
703    }
704
705    /// Signals and reaps about `leader` recorded since `mark` (wait calls are left out; see
706    /// [`since_with_waits`]).
707    pub(crate) fn since(mark: usize, leader: u32) -> Vec<Event> {
708        since_with_waits(mark, leader)
709            .into_iter()
710            .filter(|event| !matches!(event, Event::Wait { .. } | Event::Proof { .. }))
711            .collect()
712    }
713
714    /// Every event about `leader` recorded since `mark`, wait calls included.
715    pub(crate) fn since_with_waits(mark: usize, leader: u32) -> Vec<Event> {
716        EVENTS.lock().unwrap_or_else(|p| p.into_inner())[mark..]
717            .iter()
718            .copied()
719            .filter(|event| match event {
720                Event::Signal { leader: l, .. }
721                | Event::Reaped { leader: l }
722                | Event::Proof { leader: l }
723                | Event::Wait { leader: l } => *l == leader,
724            })
725            .collect()
726    }
727}
728
729/// Test seam for waiters: a pause point a test can use to hold a waiter at the exact moment
730/// the old blocking wait had checked the shared state and was about to name the pid.
731#[cfg(test)]
732pub(crate) mod gate {
733    use std::sync::{Arc, Mutex};
734
735    type Hook = Arc<dyn Fn() + Send + Sync>;
736
737    static HOOKS: Mutex<Vec<(u32, Hook)>> = Mutex::new(Vec::new());
738
739    /// Runs `hook` each time a waiter on `pid` reaches its pause point.
740    pub(crate) fn install(pid: u32, hook: impl Fn() + Send + Sync + 'static) {
741        let mut hooks = HOOKS.lock().unwrap_or_else(|p| p.into_inner());
742        hooks.retain(|(id, _)| *id != pid);
743        hooks.push((pid, Arc::new(hook)));
744    }
745
746    pub(crate) fn remove(pid: u32) {
747        HOOKS
748            .lock()
749            .unwrap_or_else(|p| p.into_inner())
750            .retain(|(id, _)| *id != pid);
751    }
752
753    /// Called by a waiter after it released the child lock and before it sleeps.
754    pub(crate) fn pause(pid: u32) {
755        let hook = HOOKS
756            .lock()
757            .unwrap_or_else(|p| p.into_inner())
758            .iter()
759            .find(|(id, _)| *id == pid)
760            .map(|(_, hook)| Arc::clone(hook));
761        if let Some(hook) = hook {
762            hook();
763        }
764    }
765}