1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//! Raw wait statuses the SIGCHLD reaper collected, keyed by pid.
//!
//! !!! WARNING: RUST-ONLY HELPER — NO C COUNTERPART !!!
//!
//! C has no need for this because C has exactly ONE `waitpid` in the
//! whole shell: `wait_for_processes` (c:Src/signals.c:285). The
//! foreground waits do NOT call `waitpid` — `waitforpid` polls with
//! `kill(pid, 0)` and sleeps in `signal_suspend(SIGCHLD, …)`
//! (c:Src/jobs.c:1652-1666), and `zwaitjob` does the same on the job's
//! STAT_DONE bit (c:Src/jobs.c:1702-1710). So the reaper is the only
//! collector: it stores every status it takes into `pn->status` through
//! `update_process` (c:Src/jobs.c:366-388), and `storepipestats` reads
//! `$pipestatus` back out of those same `pn->status` fields
//! (c:Src/jobs.c:423-435). A status can never be "stolen" there because
//! nothing else ever wanted to collect it.
//!
//! zshrs's pipeline reaps its own forked stages with a targeted
//! `waitpid(pid)` (`fusevm_bridge::waitpid_eintr`), so it has a SECOND
//! collector that races the reaper. When the reaper wins, the targeted
//! wait gets `ECHILD` and the stage's status is gone: with the reaper
//! armed, `(exit 5) | (sleep 0.5; exit 3)` published `$pipestatus` as
//! `0 3` where zsh says `5 3`. This ring hands the status back to the
//! losing collector, i.e. it is what makes zshrs's two-collector design
//! observe what C's one-collector design observes.
//!
//! Written from `zhandler`'s SIGCHLD arm, so it is plain atomics: no
//! allocation, no locks, nothing that is unsafe to touch in a handler.
use ;
/// Number of slots. C bounds its own reaped-status store
/// (`bgstatus_list`) at `sysconf(_SC_CHILD_MAX)` and drops the OLDEST
/// entry once it is full (c:Src/jobs.c:2377-2379 — `if (bgstatus_count
/// == child_max) rembgstatus(firstnode(bgstatus_list));`). This ring has
/// the same shape with a fixed bound, which is what lets it be written
/// without allocating.
const SLOTS: usize = 64;
static PIDS: = ;
static STATUSES: = ;
/// Next slot to overwrite — the "drop the oldest" half of c:Src/jobs.c:2378.
static CURSOR: AtomicUsize = new;
/// Publish the raw status the reaper just took for `pid`.
///
/// Only a status that actually ENDED the process is recorded, which is
/// the same gate C puts on `addbgstatus` (c:Src/jobs.c:695-697 records
/// only `WIFEXITED` and `WIFSIGNALED`). A `WUNTRACED`/`WCONTINUED`
/// status leaves the child alive and reapable, so a later targeted
/// `waitpid` on it still collects the real thing and must not be handed
/// a stop status instead.
/// Claim the status the reaper took for `pid`, if it is still on record.
///
/// Removing the entry on read is C's rule for its own reaped-status
/// store: "This is only used by wait, which must only work on each pid
/// once, so we need to remove the entry if we find it"
/// (c:Src/jobs.c:2398-2404, `getbgstatus`).