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//!   Also, [`UnixChild::require_identity`] reaps a just-killed child with a blocking
70//!   `Child::wait`, before the child is shared at all.
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::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    /// It is killed and reaped here, where the pid is certainly still its own.
214    pub(crate) fn require_identity(&mut self) -> io::Result<()> {
215        if !matches!(self.origin, Origin::Missing) {
216            return Ok(());
217        }
218        let _ = self.child.kill();
219        let _ = self.child.wait();
220        self.leader = Leader::Lost;
221        Err(io::Error::other(
222            "the spawned child could not be identified; it was stopped because a later \
223             signal could not be proven to reach it",
224        ))
225    }
226
227    /// The one proof every action by pid starts with: `Ok(true)` when the pid still names the
228    /// process this type spawned and has not reaped. `Ok(false)` (and the leader becomes
229    /// `Lost`, never touched again) when the number changed hands or the process is gone.
230    /// `Err` when the kernel cannot be asked: the caller must then act on nothing.
231    fn prove(&mut self) -> io::Result<bool> {
232        if !matches!(self.leader, Leader::Unreaped) {
233            return Ok(false);
234        }
235        #[cfg(test)]
236        trace::record(trace::Event::Proof { leader: self.pid });
237        if self.origin.check(self.pid)? == Proof::Same {
238            Ok(true)
239        } else {
240            self.leader = Leader::Lost;
241            Ok(false)
242        }
243    }
244
245    /// `prove`, as the guard of a wait: an unproven pid is reported as `ECHILD`, as the kernel
246    /// would for a child that is not ours.
247    fn require_ours(&mut self) -> io::Result<()> {
248        if self.prove()? {
249            Ok(())
250        } else {
251            Err(io::Error::from_raw_os_error(libc::ECHILD))
252        }
253    }
254
255    /// Test only: pretend the pid was described differently at spawn, as if our child had been
256    /// collected elsewhere and the number given to the process now holding it.
257    #[cfg(test)]
258    pub(crate) fn simulate_replaced(&mut self) {
259        self.origin = Origin::Known(crate::identity::Identity {
260            parent: 0,
261            started_sec: 1,
262            started_usec: 1,
263        });
264    }
265
266    /// Non-blocking. A `Some` result means the leader has been reaped, and ends group
267    /// signalling for this child.
268    pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
269        match self.leader {
270            Leader::Reaped(status) => return Ok(Some(status)),
271            Leader::Lost => return Err(io::Error::from_raw_os_error(libc::ECHILD)),
272            Leader::Unreaped => {}
273        }
274        if let Err(error) = self.require_ours() {
275            return Err(self.note_error(error));
276        }
277        #[cfg(test)]
278        trace::record(trace::Event::Wait { leader: self.pid });
279        match self.child.try_wait() {
280            Ok(Some(status)) => {
281                self.note_reaped(status);
282                Ok(Some(status))
283            }
284            Ok(None) => Ok(None),
285            Err(error) => Err(self.note_error(error)),
286        }
287    }
288
289    /// Closes stdin as std does, then waits until the leader is reaped. The reap itself is only
290    /// ever a non-blocking `waitpid(WNOHANG)` between sleeps, but this method cannot release
291    /// the lock: when it is reached through the `OwnedChild::child()` guard the caller holds
292    /// it for the whole wait, so other threads' waits, kills and drops on the same child
293    /// queue behind it (as with `std::process::Child::wait` in 0.3.3). Use `OwnedChild::wait`
294    /// or `ProcessHandle::wait` where that matters.
295    pub fn wait(&mut self) -> io::Result<ExitStatus> {
296        drop(self.stdin.take());
297        self.wait_reaping()
298    }
299
300    /// `SIGKILL` to the leader alone, only while it is unreaped. Nothing is sent, and `Ok` is
301    /// returned, once it has been reaped.
302    pub fn kill(&mut self) -> io::Result<()> {
303        self.signal(SignalTarget::Leader, libc::SIGKILL).map(|_| ())
304    }
305
306    /// Reaps without touching stdin, for the guard's `wait` and for cleanup of a child that
307    /// has just been killed. Each look is proven and non-blocking; between looks the thread
308    /// sleeps on the hint. A blocking `waitpid(pid)` from the start would name the number for
309    /// as long as it sleeps, and if other code collected the leader meanwhile it would wait on
310    /// whatever child the kernel gave the number to, and take its status.
311    pub(crate) fn wait_reaping(&mut self) -> io::Result<ExitStatus> {
312        let watch = Arc::clone(&self.watch);
313        watch
314            .wait_for(None, || self.try_wait())?
315            .ok_or_else(|| io::Error::other("an unbounded wait ended without a status"))
316    }
317
318    /// Looks at the leader without reaping it. `Lost` is recorded here when the kernel no
319    /// longer lists the pid as our child.
320    pub(crate) fn leader_state(&mut self) -> io::Result<LeaderState> {
321        if !self.prove()? {
322            return Ok(LeaderState::Reaped);
323        }
324        match probe_exit(self.pid) {
325            // The probe names the number once more; a replacement that took it in between and
326            // has itself exited would read as our zombie. A zombie is describable, so ask again.
327            Ok(true) => Ok(if self.prove()? {
328                LeaderState::Exited
329            } else {
330                LeaderState::Reaped
331            }),
332            Ok(false) => Ok(LeaderState::Running),
333            Err(error) if error.raw_os_error() == Some(libc::ECHILD) => {
334                self.leader = Leader::Lost;
335                Ok(LeaderState::Reaped)
336            }
337            Err(error) => Err(error),
338        }
339    }
340
341    /// Sends `signal` only if the leader is still unreaped, and does so under the caller's
342    /// lock, so no reap can fall between the check and the signal.
343    pub(crate) fn signal(
344        &mut self,
345        target: SignalTarget,
346        signal: i32,
347    ) -> io::Result<SignalOutcome> {
348        if self.leader_state()? == LeaderState::Reaped {
349            return Ok(SignalOutcome::Retired);
350        }
351        // The state above looked at the pid; prove it once more immediately before it is used
352        // as a signal target.
353        if !self.prove()? {
354            return Ok(SignalOutcome::Retired);
355        }
356        let id = self.signal_id() as i32;
357        let target_id = match target {
358            SignalTarget::Group => -id,
359            SignalTarget::Leader => id,
360        };
361        if raw_kill(self.pid, target_id, signal) == 0 {
362            return Ok(SignalOutcome::Sent);
363        }
364        let error = io::Error::last_os_error();
365        match error.raw_os_error() {
366            Some(libc::ESRCH) => Ok(SignalOutcome::Gone),
367            // macOS answers a group signal with EPERM, not ESRCH, when no member can take it:
368            // only zombies are left, or the last members are mid-exit. Our own children are
369            // always signalable, so this is never a real denial. Once the leader is no longer
370            // running (exited, or reaped by a handle clone) the group is gone. A leader that
371            // still reads as running is mid-exit: say so, and let the caller look again once it
372            // has released the lock (`OwnedChild::signal`).
373            Some(libc::EPERM) if target == SignalTarget::Group => {
374                Ok(if self.leader_state()? == LeaderState::Running {
375                    SignalOutcome::MidExit
376                } else {
377                    SignalOutcome::Gone
378                })
379            }
380            _ => Err(error),
381        }
382    }
383
384    /// The processes other than the leader still in the group.
385    /// Valid only while the leader is unreaped; a reaped leader has no group to speak of. An
386    /// unknown answer is [`Members::Unknown`]: the caller then waits out its grace instead of
387    /// cutting it short.
388    pub(crate) fn group_other_members(&mut self) -> io::Result<Members> {
389        if self.leader_state()? == LeaderState::Reaped {
390            return Ok(Members::Pids(Vec::new()));
391        }
392        group_members_besides(self.pid, self.signal_id())
393    }
394
395    /// True when a process other than the leader is still in the group (unknown counts as yes).
396    #[cfg(test)]
397    pub(crate) fn group_has_other_members(&mut self) -> io::Result<bool> {
398        Ok(match self.group_other_members()? {
399            Members::Pids(pids) => !pids.is_empty(),
400            Members::Unknown => true,
401        })
402    }
403
404    fn signal_id(&self) -> u32 {
405        #[cfg(test)]
406        if let Some(id) = self.recycled_id {
407            return id;
408        }
409        self.pid
410    }
411
412    /// Test only: aim every later signal at `id`, as if the kernel had recycled the leader's
413    /// pid and group id for an unrelated group.
414    #[cfg(test)]
415    pub(crate) fn simulate_recycled_id(&mut self, id: u32) {
416        self.recycled_id = Some(id);
417    }
418
419    fn note_reaped(&mut self, status: ExitStatus) {
420        self.leader = Leader::Reaped(status);
421        #[cfg(test)]
422        trace::record(trace::Event::Reaped { leader: self.pid });
423    }
424
425    fn note_error(&mut self, error: io::Error) -> io::Error {
426        if error.raw_os_error() == Some(libc::ECHILD) {
427            self.leader = Leader::Lost;
428        }
429        error
430    }
431}
432
433/// `waitid(P_PID, pid, WEXITED | WNOHANG | WNOWAIT)`: has the child exited, without reaping
434/// it? `ECHILD` means the pid is not (or no longer) our child.
435#[cfg(any(
436    target_os = "linux",
437    target_os = "android",
438    target_os = "macos",
439    target_os = "ios"
440))]
441fn probe_exit(pid: u32) -> io::Result<bool> {
442    loop {
443        #[cfg(test)]
444        trace::record(trace::Event::Wait { leader: pid });
445        // SAFETY: an all-zero siginfo_t is valid, and waitid fills it.
446        let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
447        // SAFETY: valid out pointer; WNOWAIT leaves the zombie in place.
448        let result = unsafe {
449            libc::waitid(
450                libc::P_PID,
451                pid as libc::id_t,
452                &mut info,
453                libc::WEXITED | libc::WNOHANG | libc::WNOWAIT,
454            )
455        };
456        if result == 0 {
457            #[cfg(any(target_os = "linux", target_os = "android"))]
458            // SAFETY: si_pid is valid after a successful waitid.
459            let reported = unsafe { info.si_pid() };
460            #[cfg(not(any(target_os = "linux", target_os = "android")))]
461            let reported = info.si_pid;
462            return Ok(reported as u32 == pid);
463        }
464        let error = io::Error::last_os_error();
465        if error.raw_os_error() != Some(libc::EINTR) {
466            return Err(error);
467        }
468    }
469}
470
471/// Other Unix hosts have no verified non-reaping probe here. The reaped state still guards
472/// every signal; only the early "leader exited" observation is unavailable.
473#[cfg(not(any(
474    target_os = "linux",
475    target_os = "android",
476    target_os = "macos",
477    target_os = "ios"
478)))]
479fn probe_exit(_pid: u32) -> io::Result<bool> {
480    Ok(false)
481}
482
483/// The single place a signal reaches a child or its group, so tests can observe every one
484/// (signal 0 included).
485fn raw_kill(leader: u32, target: i32, signal: i32) -> i32 {
486    #[cfg(test)]
487    trace::record(trace::Event::Signal {
488        leader,
489        target,
490        signal,
491    });
492    #[cfg(not(test))]
493    let _ = leader;
494    // SAFETY: plain kill(2). The callers hold the lock and have just confirmed the leader is
495    // unreaped, which pins the target id.
496    unsafe { libc::kill(target, signal) }
497}
498
499#[cfg(target_os = "macos")]
500fn group_members_besides(leader: u32, group: u32) -> io::Result<Members> {
501    /// `PROC_PGRP_ONLY` from `<libproc.h>`: list the members of one process group.
502    const PROC_PGRP_ONLY: u32 = 2;
503    const CAPACITY: usize = 256;
504    let mut pids = [0 as libc::pid_t; CAPACITY];
505    // SAFETY: the buffer is CAPACITY pids and the size passed says so.
506    let bytes = unsafe {
507        libc::proc_listpids(
508            PROC_PGRP_ONLY,
509            group,
510            pids.as_mut_ptr().cast(),
511            std::mem::size_of_val(&pids) as libc::c_int,
512        )
513    };
514    if bytes <= 0 {
515        // Fall back to signal 0: ESRCH, or EPERM when only zombies remain, means empty.
516        if raw_kill(leader, -(group as i32), 0) == 0 {
517            return Ok(Members::Unknown);
518        }
519        return Ok(match io::Error::last_os_error().raw_os_error() {
520            Some(libc::ESRCH) | Some(libc::EPERM) => Members::Pids(Vec::new()),
521            _ => Members::Unknown,
522        });
523    }
524    let count = (bytes as usize / std::mem::size_of::<libc::pid_t>()).min(CAPACITY);
525    if count >= CAPACITY {
526        return Ok(Members::Unknown);
527    }
528    Ok(Members::Pids(
529        pids[..count]
530            .iter()
531            .filter(|&&pid| pid > 0 && pid as u32 != leader)
532            .map(|&pid| pid as u32)
533            .collect(),
534    ))
535}
536
537#[cfg(any(target_os = "linux", target_os = "android"))]
538fn group_members_besides(leader: u32, group: u32) -> io::Result<Members> {
539    // kill(-pgid, 0) cannot tell an exited, unreaped leader from a live member, so read
540    // /proc: a member is any non-zombie process whose group is this one.
541    let Ok(entries) = std::fs::read_dir("/proc") else {
542        return Ok(Members::Unknown);
543    };
544    let mut members = Vec::new();
545    for entry in entries.flatten() {
546        let Some(pid) = entry
547            .file_name()
548            .to_str()
549            .and_then(|name| name.parse::<u32>().ok())
550        else {
551            continue;
552        };
553        if pid == leader {
554            continue;
555        }
556        let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
557            continue; // exited while we looked
558        };
559        let Some(rest) = stat.rsplit_once(')').map(|(_, rest)| rest) else {
560            return Ok(Members::Unknown);
561        };
562        let mut fields = rest.split_whitespace();
563        let (Some(state), Some(_parent), Some(pgrp)) =
564            (fields.next(), fields.next(), fields.next())
565        else {
566            return Ok(Members::Unknown);
567        };
568        if pgrp.parse::<u32>().ok() == Some(group) && state != "Z" && state != "X" {
569            members.push(pid);
570        }
571    }
572    Ok(Members::Pids(members))
573}
574
575#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
576fn group_members_besides(_leader: u32, _group: u32) -> io::Result<Members> {
577    Ok(Members::Unknown)
578}
579
580/// Waits for the leader of `shared` to be reaped, or for `deadline` to pass (`Ok(None)`); `None`
581/// waits without limit.
582///
583/// Every look is one non-blocking `try_wait` under the lock, released before the thread sleeps
584/// (see the module documentation), so a deadline is honoured whatever the child does, a
585/// `kill` or `terminate_tree` from another thread runs at once, and no wait-class system call
586/// names the pid while the lock is free. `ProcessHandle` waits this way, and so does the
587/// failed-spawn cleanup that has a timeout; the cleanup without one waits through
588/// `UnixChild::wait` instead (see the module documentation, item 2).
589pub(crate) fn wait_shared(
590    shared: &Mutex<UnixChild>,
591    deadline: Option<Instant>,
592) -> io::Result<Option<ExitStatus>> {
593    let (watch, pid) = {
594        let child = lock_child(shared);
595        (child.exit_watch(), child.id())
596    };
597    let mut first = true;
598    watch.wait_for(deadline, || {
599        let looked = lock_child(shared).try_wait();
600        pause_for_test(&mut first, pid, matches!(looked, Ok(None)));
601        looked
602    })
603}
604
605/// Like [`wait_shared`] but does not reap: `Ok(true)` once the leader has exited (or was reaped
606/// by someone), `Ok(false)` when `deadline` passed first. The zombie keeps pinning the group id.
607pub(crate) fn observe_shared(
608    shared: &Mutex<UnixChild>,
609    deadline: Option<Instant>,
610) -> io::Result<bool> {
611    let (watch, pid) = {
612        let child = lock_child(shared);
613        (child.exit_watch(), child.id())
614    };
615    let mut first = true;
616    let seen = watch.wait_for(deadline, || {
617        let state = lock_child(shared).leader_state();
618        pause_for_test(&mut first, pid, matches!(state, Ok(LeaderState::Running)));
619        Ok(match state? {
620            LeaderState::Running => None,
621            LeaderState::Exited | LeaderState::Reaped => Some(()),
622        })
623    })?;
624    Ok(seen.is_some())
625}
626
627/// Test seam: after the first look that found the child still running, with the lock already
628/// released and before the thread sleeps, run the hook installed for `pid` (see [`gate`]).
629#[cfg(test)]
630fn pause_for_test(first: &mut bool, pid: u32, running: bool) {
631    if *first && running {
632        *first = false;
633        gate::pause(pid);
634    }
635}
636
637#[cfg(not(test))]
638fn pause_for_test(_first: &mut bool, _pid: u32, _running: bool) {}
639
640/// Test instrumentation: every signal, every reap and every wait-class system call that names
641/// the leader's pid, in the order they happened. All are recorded under the child's lock, so the
642/// order is the real order.
643#[cfg(test)]
644pub(crate) mod trace {
645    use std::sync::Mutex;
646
647    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
648    pub(crate) enum Event {
649        Signal {
650            leader: u32,
651            target: i32,
652            signal: i32,
653        },
654        Reaped {
655            leader: u32,
656        },
657        /// The pid was proven to still name the spawned process (see `UnixChild::prove`).
658        Proof {
659            leader: u32,
660        },
661        /// A `waitpid`/`waitid` (probe or reap) was issued naming this pid. After `Reaped` the
662        /// number may belong to someone else, so none may follow.
663        Wait {
664            leader: u32,
665        },
666    }
667
668    static EVENTS: Mutex<Vec<Event>> = Mutex::new(Vec::new());
669
670    pub(crate) fn record(event: Event) {
671        EVENTS.lock().unwrap_or_else(|p| p.into_inner()).push(event);
672    }
673
674    pub(crate) fn mark() -> usize {
675        EVENTS.lock().unwrap_or_else(|p| p.into_inner()).len()
676    }
677
678    /// Signals and reaps about `leader` recorded since `mark` (wait calls are left out; see
679    /// [`since_with_waits`]).
680    pub(crate) fn since(mark: usize, leader: u32) -> Vec<Event> {
681        since_with_waits(mark, leader)
682            .into_iter()
683            .filter(|event| !matches!(event, Event::Wait { .. } | Event::Proof { .. }))
684            .collect()
685    }
686
687    /// Every event about `leader` recorded since `mark`, wait calls included.
688    pub(crate) fn since_with_waits(mark: usize, leader: u32) -> Vec<Event> {
689        EVENTS.lock().unwrap_or_else(|p| p.into_inner())[mark..]
690            .iter()
691            .copied()
692            .filter(|event| match event {
693                Event::Signal { leader: l, .. }
694                | Event::Reaped { leader: l }
695                | Event::Proof { leader: l }
696                | Event::Wait { leader: l } => *l == leader,
697            })
698            .collect()
699    }
700}
701
702/// Test seam for waiters: a pause point a test can use to hold a waiter at the exact moment
703/// the old blocking wait had checked the shared state and was about to name the pid.
704#[cfg(test)]
705pub(crate) mod gate {
706    use std::sync::{Arc, Mutex};
707
708    type Hook = Arc<dyn Fn() + Send + Sync>;
709
710    static HOOKS: Mutex<Vec<(u32, Hook)>> = Mutex::new(Vec::new());
711
712    /// Runs `hook` each time a waiter on `pid` reaches its pause point.
713    pub(crate) fn install(pid: u32, hook: impl Fn() + Send + Sync + 'static) {
714        let mut hooks = HOOKS.lock().unwrap_or_else(|p| p.into_inner());
715        hooks.retain(|(id, _)| *id != pid);
716        hooks.push((pid, Arc::new(hook)));
717    }
718
719    pub(crate) fn remove(pid: u32) {
720        HOOKS
721            .lock()
722            .unwrap_or_else(|p| p.into_inner())
723            .retain(|(id, _)| *id != pid);
724    }
725
726    /// Called by a waiter after it released the child lock and before it sleeps.
727    pub(crate) fn pause(pid: u32) {
728        let hook = HOOKS
729            .lock()
730            .unwrap_or_else(|p| p.into_inner())
731            .iter()
732            .find(|(id, _)| *id == pid)
733            .map(|(_, hook)| Arc::clone(hook));
734        if let Some(hook) = hook {
735            hook();
736        }
737    }
738}