zsh/exec_jobs.rs
1//! Executor-side bg-job tracker. NOT a port of `Src/jobs.c`.
2//!
3//! `Src/jobs.c` uses a flat `struct job jobtab[]` global keyed by pid
4//! (ported to `crate::ported::jobs::JOBTAB`). C tracks child processes
5//! through their pid + waitpid(2). Rust prefers safe-Rust ownership
6//! of `std::process::Child` handles so the executor needs a parallel
7//! registry that owns those handles. That's what this file is.
8//!
9//! This module is segregated from `src/ported/jobs.rs` (the faithful
10//! C port) so the port file contains only direct ports of jobs.c
11//! decls. `JobState` / `JobInfo` / `JobTable` here are zshrs runtime
12//! state with no C counterpart by design.
13
14use std::process::Child;
15use std::sync::Mutex;
16
17use crate::ported::jobs::stat;
18use crate::ported::jobs::{deletejob, CURJOB, MAXJOB, PREVJOB, THISJOB};
19use crate::ported::zsh_h::job;
20
21/// !!! WARNING: RUST-ONLY ADAPTER !!! The job status C derives from a
22/// finished process's wait status — update_job's `val`
23/// (c:Src/jobs.c:492-496): `WIFSIGNALED ? 0200 | WTERMSIG : WIFSTOPPED ?
24/// 0200 | WSTOPSIG : WEXITSTATUS`. zshrs waits on its foreground externals
25/// through `std::process::ExitStatus`, whose `code()` is `None` for a
26/// signalled child; the call sites turned that into 1, so
27/// `sh -c 'kill $$'; print $?` printed 1 where zsh prints 143.
28pub fn wait_status_val(s: std::process::ExitStatus) -> i32 {
29 use std::os::unix::process::ExitStatusExt as _;
30 if let Some(sig) = s.signal() {
31 0o200 | sig // c:493
32 } else if let Some(sig) = s.stopped_signal() {
33 0o200 | sig // c:495
34 } else {
35 s.code().unwrap_or(0) // c:496
36 }
37}
38
39thread_local! {
40 /// Non-zero while a pipeline's in-shell last stage runs. update_job's
41 /// foreground tail reads the status of the job's process-group leader
42 /// (c:Src/jobs.c:497-498), which is the FIRST stage, so the externals
43 /// the last stage waits for do not drive it.
44 ///
45 /// !!! WARNING: RUST-ONLY COUNTER — C READS `jn->gleader` !!!
46 pub static NOT_JOB_LEADER: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
47}
48
49/// !!! WARNING: RUST-ONLY HELPER — C WRITES THROUGH THE `shout` FILE* !!!
50/// printjob's asynchronous report goes to `fout = (synch == 2 || !shout) ?
51/// stdout : shout` (c:Src/jobs.c:1153), and an interactive shell's `shout`
52/// is the terminal, `fdopen(SHTTY, "w")`, or stderr when there is no tty
53/// (c:Src/init.c:734-748). Writing to fd 2 instead sent the report through
54/// the command's own redirection: `sh -c 'kill -HUP $$' 2>/dev/null` lost
55/// its `zsh: hangup` line.
56fn shout_write(s: &str) {
57 let tty = crate::ported::init::SHTTY.load(std::sync::atomic::Ordering::Relaxed);
58 let fd = if tty >= 0 { tty } else { libc::STDERR_FILENO };
59 let _ = crate::ported::utils::write_loop(fd, s.as_bytes());
60}
61
62/// !!! WARNING: RUST-ONLY ADAPTER !!! update_job's report for the foreground
63/// job (c:Src/jobs.c:645-650), for a simple external zshrs waited for outside
64/// the job table: `if ((isset(NOTIFY) || job == thisjob) && (jn->stat &
65/// STAT_LOCKED)) printjob(jn, !!isset(LONGLISTJOBS), 0);`. The job is one
66/// process whose text is the command's job text (`None` when the dispatch
67/// carried none, e.g. inside a function, c:Src/exec.c:3536-3538).
68///
69/// printjob prints nothing for a job marked STAT_NOPRINT, and execpline marks
70/// every job run while another pipeline is executing (`pline_level`,
71/// c:Src/exec.c:1829-1831): a command inside a function, `eval`, a loop, a
72/// `{ … }` or an `if` is never reported, only one at the top of the command
73/// line — including either side of `&&`/`||`. `nested` is the caller's
74/// in-process `( … )` (C forks it, and the child has no MONITOR).
75///
76/// Then printjob's own gate (exec_jobs::printjob_print): an interactive
77/// shell with MONITOR reports a signal other than SIGINT/SIGPIPE as
78/// `zsh: terminated cmd`, and a SIGINT only with a newline (c:1203-1205,
79/// c:1260-1263, c:1338-1341).
80pub fn foreground_job_report(status: i32, text: Option<String>, nested: bool) {
81 use crate::ported::zsh_h::{CS_CMDAND, CS_CMDOR, SFC_NONE};
82 use std::sync::atomic::Ordering;
83 let Some(text) = text else { return };
84 if nested
85 || !libc::WIFSIGNALED(status)
86 || NOT_JOB_LEADER.with(|d| d.get()) > 0
87 || crate::ported::exec::sfcontext.load(Ordering::Relaxed) != SFC_NONE
88 || crate::vm_helper::EVAL_RECURSION_DEPTH.with(|d| d.get()) > 0
89 {
90 return;
91 }
92 // c:Src/exec.c:1829-1831 — nested in a compound command (cmdstack holds
93 // more than the `&&`/`||` list connectors).
94 let in_compound = crate::ported::prompt::CMDSTACK.with(|s| {
95 s.borrow()
96 .iter()
97 .any(|&cs| cs as i32 != CS_CMDAND as i32 && cs as i32 != CS_CMDOR as i32)
98 });
99 if in_compound {
100 return;
101 }
102 let jn = job {
103 stat: stat::INUSE | stat::LOCKED | stat::DONE | stat::CHANGED,
104 procs: vec![crate::ported::zsh_h::process {
105 pid: 0,
106 text,
107 status,
108 ti: Default::default(),
109 bgtime: None,
110 endtime: None,
111 }],
112 ..Default::default()
113 };
114 // The job sits at index 1 and is `thisjob` (c:Src/jobs.c:645).
115 let mut tab = vec![job::default(), jn];
116 printjob_print(&mut tab, 1, i32::from(crate::ported::zsh_h::isset(crate::ported::zsh_h::LONGLISTJOBS)), 0, 1);
117}
118
119/// !!! WARNING: RUST-ONLY ADAPTER !!! The tail of C `update_job` for the
120/// FOREGROUND job (`job == thisjob`, not STAT_CURSH, so `inforeground = 2`,
121/// c:Src/jobs.c:558-561), for an external zshrs waited for outside the job
122/// table. `status` is the raw wait status of the job's last process.
123///
124/// c:Src/jobs.c:654-679 —
125/// ```c
126/// /* When MONITOR is set, the foreground process runs in a different *
127/// * process group from the shell, so the shell will not receive *
128/// * terminal signals, therefore we pretend that the shell got *
129/// * the signal too. */
130/// if (inforeground == 2 && isset(MONITOR) && WIFSIGNALED(status)) {
131/// int sig = WTERMSIG(status);
132/// if (sig == SIGINT || sig == SIGQUIT) {
133/// if (sigtrapped[sig]) {
134/// dotrap(sig);
135/// if (errflag) breaks = loops;
136/// } else {
137/// breaks = loops;
138/// errflag |= ERRFLAG_INT;
139/// }
140/// check_cursh_sig(sig);
141/// }
142/// }
143/// ```
144/// So a child that dies of SIGINT/SIGQUIT ends the rest of the command line
145/// (`for i in 1 2; do sh -c 'kill -INT $$'; print $i; done` prints nothing)
146/// unless a trap for the signal returns zero.
147pub fn foreground_job_signalled(status: i32) {
148 use crate::ported::zsh_h::{isset, ERRFLAG_INT, MONITOR};
149 use std::sync::atomic::Ordering;
150 if NOT_JOB_LEADER.with(|d| d.get()) > 0 || !isset(MONITOR) || !libc::WIFSIGNALED(status) {
151 return;
152 }
153 let sig = libc::WTERMSIG(status); // c:659
154 if sig != libc::SIGINT && sig != libc::SIGQUIT {
155 return; // c:661
156 }
157 let loops = crate::ported::builtin::LOOPS.load(Ordering::Relaxed);
158 let trapped = crate::ported::signals::sigtrapped
159 .lock()
160 .ok()
161 .and_then(|t| t.get(sig as usize).copied())
162 .unwrap_or(0);
163 if trapped != 0 {
164 crate::ported::signals::dotrap(sig); // c:663
165 if crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 {
166 crate::ported::builtin::BREAKS.store(loops, Ordering::Relaxed); // c:671-672
167 }
168 } else {
169 crate::ported::builtin::BREAKS.store(loops, Ordering::Relaxed); // c:674
170 crate::ported::utils::errflag.fetch_or(ERRFLAG_INT, Ordering::Relaxed); // c:675
171 }
172 // c:677 — `check_cursh_sig(sig);`
173 if let Some(tab) = crate::ported::jobs::JOBTAB.get() {
174 if let Ok(jt) = tab.lock() {
175 crate::ported::jobs::check_cursh_sig(&jt, sig);
176 }
177 }
178}
179
180/// !!! WARNING: RUST-ONLY SPLIT OF `printjob` — C HAS ONE FUNCTION !!!
181/// Port of the done-job tail of C `printjob`, `Src/jobs.c:1350-1363`:
182/// ```c
183/// if (jn->stat & STAT_DONE) {
184/// /* This looks silly, but see update_job() */
185/// if (synch <= 1)
186/// storepipestats(jn, job == thisjob, job == thisjob);
187/// if (should_report_time(jn))
188/// dumptime(jn);
189/// deletejob(jn, 0);
190/// if (job == curjob) { curjob = prevjob; prevjob = job; }
191/// if (job == prevjob) setprevjob();
192/// }
193/// ```
194/// `jn` is the table index of the job being deleted and `job` the number
195/// printjob was called with: they differ when printjob reported a subjob in
196/// place of its superjob (c:1171-1185). Callers that formatted the job
197/// themselves (the `wait` builtin) run this tail alone, as C's update_job
198/// reaches it through printjob.
199///
200/// `tab` is the caller's locked slice of `JOBTAB`; nothing here locks it.
201pub fn printjob_delete_tail(tab: &mut [job], jn: usize, job: usize, synch: i32) {
202 if jn >= tab.len() || (tab[jn].stat & stat::DONE) == 0 {
203 return;
204 }
205 let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
206 let is_thisjob = i32::from(job as i32 == thisjob);
207 if synch <= 1 {
208 crate::ported::jobs::storepipestats(&tab[jn], is_thisjob, is_thisjob); // c:1352
209 }
210 // c:1354-1355 — `if (should_report_time(jn)) dumptime(jn);`
211 let reporttime: f64 = crate::ported::params::getsparam("REPORTTIME")
212 .and_then(|s| s.parse().ok())
213 .unwrap_or(-1.0);
214 if crate::ported::jobs::should_report_time(&tab[jn], reporttime) {
215 if let Some(timing) = crate::ported::jobs::dumptime(&tab[jn]) {
216 eprintln!("{}", timing); // printtime writes to stderr
217 }
218 }
219 deletejob(tab, jn, false); // c:1356
220 let mut cj = CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
221 let mut pj = PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
222 if *cj == job as i32 {
223 // c:1357-1360
224 *cj = *pj;
225 *pj = job as i32;
226 }
227 let need_setprev = *pj == job as i32; // c:1361
228 drop(cj);
229 drop(pj);
230 if need_setprev {
231 setprevjob_locked(tab); // c:1362
232 }
233}
234
235/// !!! WARNING: RUST-ONLY SPLIT OF `printjob` — C HAS ONE FUNCTION !!!
236/// Port of C `printjob(Job jn, int lng, int synch)`, `Src/jobs.c:1147-1365`,
237/// up to but excluding the done-job tail: the superjob redirect
238/// (c:1171-1185), the scan that decides whether a state change is worth
239/// reporting (c:1187-1221), the print gate and the status lines
240/// (c:1226-1337), and the `(pwd now: …)` line (c:1344-1353). The lines
241/// themselves are laid out by `ported::jobs::printjob`. Returns
242/// `(doneprint, jn, skip_print)` where `jn` is the index of the job reported
243/// (the subjob when it stood in for the superjob).
244///
245/// `ji` is C's `job` (`jn - jobtab` before the redirect), `thisjob` C's global.
246///
247/// `lng` is never negative here and `synch` is 0 or 1 or 2: the `fg`/`bg`
248/// "continued" and POSIX plain-format arms (`lng < 0`, `synch == 3`) have
249/// no caller that reaches this function.
250///
251/// WARNING: param names don't match C — Rust=(tab, ji, lng, synch, thisjob)
252/// vs C=(jn, lng, synch)
253pub fn printjob_print(tab: &mut [job], ji: usize, lng: i32, synch: i32, thisjob: i32) -> (bool, usize, bool) {
254 use crate::ported::builtins::sched::zleactive;
255 use crate::ported::zsh_h::{isset, INTERACTIVE, PRINTEXITVALUE, SHINSTDIN, SP_RUNNING};
256 use std::sync::atomic::Ordering;
257 let is_thisjob = ji as i32 == thisjob; // `job == thisjob`
258 // c:1163-1164 — `if (jn->stat & STAT_NOPRINT) skip_print = 1;`
259 let mut skip_print = (tab[ji].stat & stat::NOPRINT) != 0;
260 let mut jn = ji;
261 // c:1171-1185 — a subjob that still has processes is reported as if it
262 // were the user-visible superjob.
263 if (tab[jn].stat & stat::SUPERJOB) != 0 && tab[jn].other != 0 {
264 let sjn = tab[jn].other as usize;
265 if sjn < tab.len() && (!tab[sjn].procs.is_empty() || !tab[sjn].auxprocs.is_empty()) {
266 jn = sjn;
267 }
268 }
269 let mut sflag = false; // c:1150
270 let mut doputnl = false; // c:1151
271 let superjob = (tab[jn].stat & stat::SUPERJOB) != 0;
272 let nprocs = tab[jn].procs.len();
273 // c:1187-1221 — does any finished process force a report?
274 for k in 0..nprocs {
275 if superjob && tab[jn].procs[0].status == SP_RUNNING && k + 1 == nprocs {
276 tab[jn].procs[k].status = SP_RUNNING; // c:1192-1194
277 }
278 let status = tab[jn].procs[k].status;
279 if status == SP_RUNNING {
280 continue; // c:1195
281 }
282 if libc::WIFSIGNALED(status) {
283 let sig = libc::WTERMSIG(status); // c:1197
284 if sig != libc::SIGINT && sig != libc::SIGPIPE {
285 sflag = true; // c:1202-1203
286 }
287 if is_thisjob && sig == libc::SIGINT {
288 doputnl = true; // c:1204-1205
289 }
290 if isset(PRINTEXITVALUE) && isset(SHINSTDIN) {
291 sflag = true; // c:1206-1208
292 skip_print = false;
293 }
294 } else if libc::WIFSTOPPED(status) {
295 let sig = libc::WSTOPSIG(status); // c:1210
296 if is_thisjob && sig == libc::SIGTSTP {
297 doputnl = true; // c:1214-1215
298 }
299 } else if isset(PRINTEXITVALUE) && isset(SHINSTDIN) && libc::WEXITSTATUS(status) != 0 {
300 sflag = true; // c:1216-1219
301 skip_print = false;
302 }
303 }
304 // c:1224-1238 — a skipped job is only deleted (the caller's tail).
305 if skip_print {
306 return (false, jn, true);
307 }
308 let mut doneprint = false; // c:1152
309 let interact = isset(INTERACTIVE);
310 let stopped = (tab[jn].stat & stat::STOPPED) != 0;
311 // c:1248-1250 — `synch == 2 || ((interact || synch) && jobbing &&
312 // ((jn->stat & STAT_STOPPED) || sflag || job != thisjob))`
313 if synch == 2
314 || ((interact || synch != 0) && crate::ported::zsh_h::jobbing() && (stopped || sflag || !is_thisjob))
315 {
316 // c:1258-1259 — `if (!synch) zleentry(ZLE_CMD_TRASH);` (trashzle
317 // itself is a no-op unless zleactive).
318 if synch == 0 && zleactive.load(Ordering::Relaxed) != 0 {
319 crate::ported::init::zleentry(crate::ported::zsh_h::ZLE_CMD_TRASH);
320 }
321 let mut out = String::new();
322 if doputnl && synch == 0 {
323 doneprint = true; // c:1262-1263
324 out.push('\n');
325 }
326 let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
327 let prevjob = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
328 let s = crate::ported::jobs::printjob(
329 &tab[jn],
330 ji,
331 lng,
332 (curjob >= 0).then_some(curjob as usize),
333 (prevjob >= 0).then_some(prevjob as usize),
334 is_thisjob && synch != 2, // c:1255 — `thisfmt = job == thisjob && synch != 2`
335 );
336 if !s.is_empty() {
337 doneprint = true; // c:1275
338 out.push_str(&s);
339 out.push('\n');
340 }
341 shout_write(&out);
342 } else if doputnl && interact && synch == 0 {
343 // c:1338-1341
344 doneprint = true;
345 shout_write("\n");
346 }
347 // c:1344-1353 — `(pwd now: …)` once a later `cd` has moved the shell away
348 // from the directory the job started in (`jobs -d`, lng & 4, is the
349 // layout routine's).
350 if (lng & 4) == 0 && interact && is_thisjob {
351 if let Some(jpwd) = tab[jn].pwd.as_deref() {
352 let pwd = crate::ported::params::getsparam("PWD").unwrap_or_default();
353 if jpwd != pwd {
354 doneprint = true;
355 shout_write(&format!("(pwd now: {})\n", crate::ported::utils::fprintdir(&pwd)));
356 }
357 }
358 }
359 (doneprint, jn, false)
360}
361
362/// !!! WARNING: RUST-ONLY SPLIT OF `printjob` — C HAS ONE FUNCTION !!!
363/// Port of C `printjob(Job jn, int lng, int synch)`, `Src/jobs.c:1147-1365`,
364/// reading `thisjob` from the global: [`printjob_print`] followed by the
365/// done-job tail ([`printjob_delete_tail`]) or, for a job that is not done,
366/// `jn->stat &= ~STAT_CHANGED` (c:1364). Returns C's `doneprint`.
367///
368/// `tab` is the caller's locked slice of `JOBTAB`; nothing here locks it, so
369/// update_job, handle_sub, zwaitjob and scanjobs call it with their guard
370/// held.
371///
372/// WARNING: param names don't match C — Rust=(tab, ji, lng, synch) vs
373/// C=(jn, lng, synch)
374pub fn printjob_synch(tab: &mut [job], ji: usize, lng: i32, synch: i32) -> bool {
375 let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
376 let (doneprint, jn, skipped) = printjob_print(tab, ji, lng, synch, thisjob);
377 if (tab[jn].stat & stat::DONE) != 0 {
378 printjob_delete_tail(tab, jn, ji, synch);
379 } else if !skipped {
380 tab[jn].stat &= !stat::CHANGED; // c:1364
381 }
382 doneprint
383}
384
385/// `setprevjob` (Src/jobs.c:698-717) body operating on an
386/// already-locked table slice — `printjob_delete_tail` callers hold
387/// the JOBTAB lock, so the re-locking ported `setprevjob()` would
388/// deadlock. Same walk, same candidate order.
389fn setprevjob_locked(tab: &[job]) {
390 let maxjob = *MAXJOB
391 .get_or_init(|| Mutex::new(0))
392 .lock()
393 .expect("maxjob poisoned");
394 let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
395 let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
396 let pick = |want_stopped: bool| -> i32 {
397 for i in (1..=maxjob).rev() {
398 if i >= tab.len() {
399 continue;
400 }
401 let j = &tab[i];
402 let stat_ok = if want_stopped {
403 (j.stat & (stat::INUSE | stat::STOPPED)) == (stat::INUSE | stat::STOPPED)
404 } else {
405 (j.stat & stat::INUSE) != 0
406 };
407 if stat_ok && (j.stat & stat::SUBJOB) == 0 && i as i32 != curjob && i as i32 != thisjob
408 {
409 return i as i32;
410 }
411 }
412 -1
413 };
414 let mut found = pick(true); // c:Src/jobs.c:702-707
415 if found < 0 {
416 found = pick(false); // c:Src/jobs.c:709-714
417 }
418 *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = found; // c:716
419}
420
421/// One job slot held by a running synchronous pipeline.
422///
423/// !!! WARNING: RUST-ONLY ADAPTER !!! C keeps this on the C stack:
424/// execpline's locals `pj` and `newjob` (c:Src/exec.c:1637,1656,1666) live
425/// exactly as long as the pipeline runs. A compiled chunk has no such frame
426/// around the pipeline, and can jump past its end (`break`, `return`, an
427/// errflag abort), so the slot is recorded here with the VM that opened it
428/// (`frame`) and the pipeline's nesting depth inside that chunk (`depth`).
429struct HeldSlot {
430 frame: usize,
431 depth: u32,
432 slot: usize,
433 pj: i32,
434}
435
436thread_local! {
437 /// The job slots of the synchronous pipelines running on this thread,
438 /// outermost first. See [`HeldSlot`].
439 static HELD_SLOTS: std::cell::RefCell<Vec<HeldSlot>> =
440 const { std::cell::RefCell::new(Vec::new()) };
441}
442
443/// !!! WARNING: RUST-ONLY ADAPTER !!! The opening half of execpline's job
444/// frame, run by BUILTIN_EXECPLINE_CHILD_BLOCK:
445/// ```c
446/// pj = thisjob; /* c:Src/exec.c:1656 */
447/// if ((thisjob = newjob = initjob()) == -1) { /* c:Src/exec.c:1666 */
448/// ```
449/// Every synchronous pipeline that is not `execsimple` holds a slot while
450/// it runs, including a function call and a builtin. That is visible:
451/// a job started inside a function gets the slot after the function's
452/// own (`f() { sleep 1 & }; f` prints `[2]`), and setprevjob
453/// (c:Src/jobs.c:672-677) can pick the running function's slot as
454/// `prevjob`, which deletejob later frees without touching `prevjob`.
455///
456/// Slots this frame left at `depth` or deeper — skipped by a `break` or a
457/// `continue` before their close ran — are released first.
458///
459/// Worker threads take no slot: the job table is the main shell's, and C
460/// has no second thread to allocate from it.
461pub fn execpline_slot_open(frame: usize, depth: u32) {
462 if crate::worker::in_worker_thread() {
463 return;
464 }
465 execpline_slot_close(frame, depth);
466 let table = crate::ported::jobs::JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
467 let slot = {
468 let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
469 crate::ported::jobs::initjob(&mut tab) // c:Src/exec.c:1666
470 };
471 let mut tj = THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
472 let pj = *tj; // c:Src/exec.c:1656
473 *tj = slot as i32; // c:Src/exec.c:1666
474 drop(tj);
475 HELD_SLOTS.with(|h| h.borrow_mut().push(HeldSlot { frame, depth, slot, pj }));
476}
477
478/// !!! WARNING: RUST-ONLY ADAPTER !!! The closing half of execpline's job
479/// frame, run by BUILTIN_EXECPLINE_CHILD_UNBLOCK: the procs-less job is
480/// deleted (waitonejob, c:Src/jobs.c:1750-1756) and `thisjob = pj` restores
481/// the enclosing pipeline's (c:Src/exec.c:1981).
482///
483/// Closes the slot `frame` holds at `depth` together with every slot opened
484/// after it: those belong to pipelines nested inside this one, which have
485/// necessarily finished, whether or not their own close ran.
486pub fn execpline_slot_close(frame: usize, depth: u32) {
487 let start = HELD_SLOTS.with(|h| {
488 h.borrow()
489 .iter()
490 .position(|s| s.frame == frame && s.depth >= depth)
491 });
492 if let Some(start) = start {
493 release_held_slots(start);
494 }
495}
496
497/// !!! WARNING: RUST-ONLY ADAPTER !!! How many pipeline slots are held now.
498/// A chunk runner takes this before running a chunk and hands it to
499/// [`execpline_slots_release_to`] afterwards, so a chunk abandoned mid-
500/// pipeline (errflag, `return` from a sourced file) holds nothing after it.
501pub fn execpline_slots_mark() -> usize {
502 HELD_SLOTS.with(|h| h.borrow().len())
503}
504
505/// !!! WARNING: RUST-ONLY ADAPTER !!! Close every pipeline slot opened since
506/// `mark`, as their closes would have.
507pub fn execpline_slots_release_to(mark: usize) {
508 release_held_slots(mark);
509}
510
511/// !!! WARNING: RUST-ONLY ADAPTER !!! Drop the record of every slot opened
512/// since `mark` WITHOUT touching the job table. For the in-process stand-ins
513/// for a fork (`$(...)`, `( ... )`): the slots were taken from the child's
514/// cleared table, which the parent's restored table has since replaced, so
515/// the indices name nothing of the child's any more.
516pub fn execpline_slots_forget_to(mark: usize) {
517 HELD_SLOTS.with(|h| h.borrow_mut().truncate(mark));
518}
519
520/// Port of execcmd_exec's current-shell arm (c:Src/exec.c:3674-3682), for
521/// the VM's direct builtin and shell-function dispatch, which does not pass
522/// through `execcmd_exec`:
523/// ```c
524/// } else if (is_cursh) {
525/// jobtab[thisjob].stat |= STAT_CURSH;
526/// if (!jobtab[thisjob].procs)
527/// jobtab[thisjob].stat |= STAT_NOPRINT;
528/// if (is_builtin)
529/// jobtab[thisjob].stat |= STAT_BUILTIN;
530/// ```
531/// Without STAT_NOPRINT the running function's own slot is a live job to
532/// getjob/bin_fg: `f() { wait %1 }; f` waited on it and returned 0 where
533/// zsh reports `%1: no such job` and returns 127 (c:Src/jobs.c:2585-2590).
534pub fn mark_thisjob_cursh(is_builtin: bool) {
535 let tj = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
536 if tj < 0 {
537 return;
538 }
539 let table = crate::ported::jobs::JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
540 let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
541 if let Some(j) = tab.get_mut(tj as usize) {
542 j.stat |= stat::CURSH; // c:3678
543 if j.procs.is_empty() {
544 j.stat |= stat::NOPRINT; // c:3679-3680
545 }
546 if is_builtin {
547 j.stat |= stat::BUILTIN; // c:3681-3682
548 }
549 }
550}
551
552/// The job slot of the innermost running pipeline, if one is held.
553pub fn execpline_slot_current() -> Option<usize> {
554 HELD_SLOTS.with(|h| h.borrow().last().map(|s| s.slot))
555}
556
557/// Delete the job slots held from position `start` on, innermost first, and
558/// put `thisjob` back to what it was before the outermost of them opened.
559fn release_held_slots(start: usize) {
560 let released: Vec<HeldSlot> = HELD_SLOTS.with(|h| {
561 let mut h = h.borrow_mut();
562 if start >= h.len() {
563 return Vec::new();
564 }
565 h.split_off(start)
566 });
567 let Some(outermost) = released.first() else {
568 return;
569 };
570 let table = crate::ported::jobs::JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
571 {
572 let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
573 for held in released.iter().rev() {
574 // A stopped job stays in the table for `fg` (zwaitjob returns on
575 // STAT_STOPPED and nothing deletes it), and a superjob belongs to
576 // the list_pipe machinery, not to this frame.
577 if let Some(jn) = tab.get(held.slot) {
578 if (jn.stat & stat::INUSE) != 0
579 && (jn.stat & (stat::STOPPED | stat::SUPERJOB)) == 0
580 {
581 deletejob(&mut tab, held.slot, false); // c:Src/jobs.c:1754
582 }
583 }
584 }
585 // c:Src/jobs.c:1441-1443 — freejob's `maxjob` shrink, which the
586 // ported freejob (one job, no table) cannot do.
587 let mut mj = MAXJOB.get_or_init(|| Mutex::new(0)).lock().unwrap_or_else(|e| e.into_inner());
588 while *mj > 0 && tab.get(*mj).map_or(true, |j| (j.stat & stat::INUSE) == 0) {
589 *mj -= 1;
590 }
591 }
592 *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner()) =
593 outermost.pj; // c:Src/exec.c:1981 `thisjob = pj;`
594}
595
596/// Running-job state tracked alongside each `Child` handle.
597/// Maps to C's `STAT_*` bits but is exposed as a typed enum since
598/// the executor's safe-Rust path doesn't manipulate the bitfield.
599#[derive(Clone, Copy, Debug, PartialEq, Eq)]
600pub enum JobState {
601 /// `Running` variant.
602 Running,
603 /// `Stopped` variant.
604 Stopped,
605 /// `Done` variant.
606 Done,
607}
608
609/// One entry in the executor's bg-job registry.
610#[derive(Debug)]
611pub struct JobInfo {
612 /// `id` field.
613 pub id: usize,
614 /// `pid` field.
615 pub pid: i32,
616 /// `child` field.
617 pub child: Option<Child>,
618 /// `command` field.
619 pub command: String,
620 /// `state` field.
621 pub state: JobState,
622 /// `is_current` field.
623 pub is_current: bool,
624}
625
626/// The executor's bg-job registry. Distinct from the C-port
627/// `JOBTAB` (a `Vec<Job>` keyed by index that mirrors `jobtab[]`):
628/// this table owns the `std::process::Child` handles needed for
629/// `try_wait` / `kill` on the safe-Rust path.
630pub struct JobTable {
631 /// `jobs` field.
632 jobs: Vec<Option<JobInfo>>,
633 /// `current_id` field.
634 current_id: Option<usize>,
635 /// `next_id` field.
636 next_id: usize,
637}
638
639impl Default for JobTable {
640 fn default() -> Self {
641 Self::new()
642 }
643}
644
645impl JobTable {
646 /// `new` — see implementation.
647 pub fn new() -> Self {
648 JobTable {
649 jobs: Vec::with_capacity(16),
650 current_id: None,
651 next_id: 1,
652 }
653 }
654
655 /// Peek at the next id that would be assigned by `add_job`/`add_pid`.
656 /// Used by `wait %N` to distinguish a never-issued id (clear user
657 /// error) from a job that was issued and already reaped (silent
658 /// success in zshrs to keep the `cmd & wait %1` idiom working
659 /// across the races introduced by the threaded job table).
660 pub fn peek_next_id(&self) -> usize {
661 self.next_id
662 }
663
664 /// Add a job with a Child process
665 pub fn add_job(&mut self, child: Child, command: String, state: JobState) -> usize {
666 let id = self.next_id;
667 self.next_id += 1;
668
669 let pid = child.id() as i32;
670 let job = JobInfo {
671 id,
672 pid,
673 child: Some(child),
674 command,
675 state,
676 is_current: true,
677 };
678
679 // Mark previous current as not current
680 if let Some(cur_id) = self.current_id {
681 if let Some(j) = self.get_mut_internal(cur_id) {
682 j.is_current = false;
683 }
684 }
685
686 // Add new job
687 let slot = self.get_free_slot();
688 if slot >= self.jobs.len() {
689 self.jobs.resize_with(slot + 1, || None);
690 }
691 self.jobs[slot] = Some(job);
692 self.current_id = Some(id);
693
694 id
695 }
696
697 /// Register a backgrounded job that was forked via raw `libc::fork()`
698 /// (no `std::process::Child` wrapper). The wait path then has to
699 /// `waitpid(pid)` instead of `Child::wait()`. Used by
700 /// BUILTIN_RUN_BG so `wait` (no args) can synchronize on it.
701 pub fn add_pid_job(&mut self, pid: i32, command: String, state: JobState) -> usize {
702 let id = self.next_id;
703 self.next_id += 1;
704 let job = JobInfo {
705 id,
706 pid,
707 child: None,
708 command,
709 state,
710 is_current: true,
711 };
712 if let Some(cur_id) = self.current_id {
713 if let Some(j) = self.get_mut_internal(cur_id) {
714 j.is_current = false;
715 }
716 }
717 let slot = self.get_free_slot();
718 if slot >= self.jobs.len() {
719 self.jobs.resize_with(slot + 1, || None);
720 }
721 self.jobs[slot] = Some(job);
722 self.current_id = Some(id);
723 id
724 }
725
726 fn get_free_slot(&self) -> usize {
727 for (i, slot) in self.jobs.iter().enumerate() {
728 if slot.is_none() {
729 return i;
730 }
731 }
732 self.jobs.len()
733 }
734
735 fn get_mut_internal(&mut self, id: usize) -> Option<&mut JobInfo> {
736 self.jobs.iter_mut().flatten().find(|job| job.id == id)
737 }
738
739 /// Get a job by ID
740 pub fn get(&self, id: usize) -> Option<&JobInfo> {
741 self.jobs
742 .iter()
743 .flatten()
744 .find(|&job| job.id == id)
745 .map(|v| v as _)
746 }
747
748 /// Get a mutable job by ID
749 pub fn get_mut(&mut self, id: usize) -> Option<&mut JobInfo> {
750 self.get_mut_internal(id)
751 }
752
753 /// Remove a job by ID
754 pub fn remove(&mut self, id: usize) -> Option<JobInfo> {
755 for slot in self.jobs.iter_mut() {
756 if slot.as_ref().map(|j| j.id == id).unwrap_or(false) {
757 let job = slot.take();
758 if self.current_id == Some(id) {
759 self.current_id = None;
760 }
761 return job;
762 }
763 }
764 None
765 }
766
767 /// List all active jobs
768 pub fn list(&self) -> Vec<&JobInfo> {
769 self.jobs.iter().filter_map(|j| j.as_ref()).collect()
770 }
771
772 /// Iterate over jobs with their IDs (for compatibility)
773 pub fn iter(&self) -> impl Iterator<Item = (usize, &JobInfo)> {
774 self.jobs
775 .iter()
776 .filter_map(|j| j.as_ref().map(|job| (job.id, job)))
777 }
778
779 /// Count number of active jobs
780 pub fn count(&self) -> usize {
781 self.jobs.iter().filter(|j| j.is_some()).count()
782 }
783
784 /// Check if there are any jobs
785 pub fn is_empty(&self) -> bool {
786 self.count() == 0
787 }
788
789 /// Get current job
790 pub fn current(&self) -> Option<&JobInfo> {
791 self.current_id.and_then(|id| self.get(id))
792 }
793
794 /// Reap finished jobs (check for completed processes)
795 pub fn reap_finished(&mut self) -> Vec<JobInfo> {
796 let mut finished = Vec::new();
797
798 for job in self.jobs.iter_mut().flatten() {
799 if let Some(ref mut child) = job.child {
800 // Try to check if child has finished without blocking
801 match child.try_wait() {
802 Ok(Some(_status)) => {
803 // Child finished
804 job.state = JobState::Done;
805 }
806 Ok(None) => {
807 // Still running
808 }
809 Err(_) => {
810 // Error checking, assume done
811 job.state = JobState::Done;
812 }
813 }
814 }
815 }
816
817 // Remove done jobs
818 for slot in self.jobs.iter_mut() {
819 if slot
820 .as_ref()
821 .map(|j| j.state == JobState::Done)
822 .unwrap_or(false)
823 {
824 if let Some(job) = slot.take() {
825 finished.push(job);
826 }
827 }
828 }
829
830 finished
831 }
832}
833
834#[cfg(test)]
835mod tests {
836 use super::*;
837
838 #[test]
839 fn test_job_table_new() {
840 let _g = crate::test_util::global_state_lock();
841 let table = JobTable::new();
842 assert!(table.is_empty());
843 }
844
845 #[test]
846 fn test_job_state_enum() {
847 let _g = crate::test_util::global_state_lock();
848 let state = JobState::Running;
849 assert_eq!(state, JobState::Running);
850 assert_ne!(state, JobState::Stopped);
851 assert_ne!(state, JobState::Done);
852 }
853
854 #[test]
855 fn test_add_pid_job_assigns_id() {
856 let _g = crate::test_util::global_state_lock();
857 let mut t = JobTable::new();
858 let id1 = t.add_pid_job(1234, "cmd1".into(), JobState::Running);
859 let id2 = t.add_pid_job(5678, "cmd2".into(), JobState::Running);
860 assert_ne!(id1, id2);
861 assert_eq!(t.list().len(), 2);
862 assert_eq!(t.current().map(|j| j.id), Some(id2));
863 }
864
865 #[test]
866 fn test_remove_drops_current() {
867 let _g = crate::test_util::global_state_lock();
868 let mut t = JobTable::new();
869 let id = t.add_pid_job(99, "x".into(), JobState::Running);
870 assert!(t.remove(id).is_some());
871 assert!(t.is_empty());
872 assert!(t.current().is_none());
873 }
874}