Skip to main content

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}