Skip to main content

zsh/ported/
jobs.rs

1//! Job control for zshrs
2//!
3//! Port from zsh/Src/jobs.c
4//!
5//! the process group of the shell                                           // c:60
6//! the job we are working on, or -1 if none                                 // c:70
7//! the current job (%+)                                                     // c:75
8//! the previous job (%-)                                                    // c:80
9//! the job table                                                            // c:85
10//! Size of the job table.                                                   // c:90
11//! Update status of job, possibly printing it                               // c:456
12//! wait for running job to finish                                           // c:1759
13//! clear job table when entering subshells                                  // c:1776
14//! Initialise job handling.                                                 // c:2160
15//!
16//! Provides job control, process management, and signal handling for jobs.
17
18use std::env;
19use std::process::Child;
20use std::time::{Duration, Instant};
21
22use crate::ported::utils::zwarnnam;
23use std::os::unix::process::ExitStatusExt;
24use crate::ported::signals::{signal_block, signal_setmask};
25use std::sync::atomic::Ordering;
26use crate::ported::zsh_h::OPT_ISSET;
27use crate::ported::hashtable_h::{BIN_FG, BIN_BG, BIN_JOBS};
28use crate::ported::signals_h::{signal_default, signal_ignore};
29
30/// Job status flags. `i32` to match C's `int stat` field on
31/// `struct job` (`Src/zsh.h:1062`).
32pub mod stat {
33    pub const STOPPED:  i32 = 1 << 0;  // Job is stopped
34    pub const DONE:     i32 = 1 << 1;  // Job is finished
35    pub const SUBJOB:   i32 = 1 << 2;  // Job is a subjob
36    pub const CURSH:    i32 = 1 << 3;  // Last pipeline elem in current shell
37    pub const SUPERJOB: i32 = 1 << 4;  // Job is a superjob
38    pub const WASSUPER: i32 = 1 << 5;  // Was a superjob
39    pub const INUSE:    i32 = 1 << 6;  // Entry in use
40    pub const BUILTIN:  i32 = 1 << 7;  // Job has builtin
41    pub const DISOWN:   i32 = 1 << 8;  // Disowned
42    pub const NOTIFY:   i32 = 1 << 9;  // Notify when done
43    pub const ATTACH:   i32 = 1 << 10; // Attached to tty
44}
45
46/// Special process status values
47pub const SP_RUNNING: i32 = -1;
48
49/// Maximum pipestats
50pub const MAX_PIPESTATS: usize = 256;
51
52/// Job-table allocation chunk size.
53/// Port of `MAXJOBS_ALLOC` from `Src/zsh.h:1107`.
54pub const MAXJOBS_ALLOC: usize = 50;
55
56/// Hard upper bound on job-table growth.
57/// Port of `MAX_MAXJOBS` from `Src/jobs.c:2221`.
58pub const MAX_MAXJOBS: usize = 1000;
59
60// `TimeInfo` / `ChildTimes` deleted — both folded into canonical
61// `timeinfo` at `zsh_h.rs:2153` (direct port of `struct timeinfo`
62// from `Src/zsh.h:1099`).
63pub use crate::ported::zsh_h::timeinfo;
64
65// Canonical `process` / `job` live in `zsh_h.rs:1166,1180` — direct
66// ports of `struct process` / `struct job` from `Src/zsh.h:1117,1058`.
67// jobs.rs uses them via `Process` / `Job` aliases to keep call sites
68// readable (Rust convention favors CamelCase at use-sites; the
69// underlying type is the lowercase C-faithful canonical).
70pub use crate::ported::zsh_h::process as Process;
71pub use crate::ported::zsh_h::job as Job;
72
73impl Process {
74    /// Build a fresh entry. Matches C's `update_process()` init shape
75    /// (`Src/jobs.c:363` — `pn->pid = pid; pn->status = SP_RUNNING;`
76    /// before the first wait).
77    pub fn new(pid: i32) -> Self {
78        Process {
79            pid,
80            status: SP_RUNNING,
81            text: String::new(),
82            ti: timeinfo::default(),
83            bgtime: Some(std::time::Instant::now()),
84            endtime: None,
85        }
86    }
87
88    /// `SP_RUNNING` sentinel check — equivalent to C's `pn->status ==
89    /// SP_RUNNING` test at e.g. `Src/jobs.c:1242`.
90    pub fn is_running(&self) -> bool { self.status == SP_RUNNING }
91
92    /// Mirrors C's `WIFSTOPPED(status)` macro.
93    pub fn is_stopped(&self) -> bool { self.status & 0xff == 0x7f }
94
95    /// Mirrors C's `WIFSIGNALED(status)` macro.
96    pub fn is_signaled(&self) -> bool {
97        (self.status & 0x7f) > 0 && (self.status & 0x7f) < 0x7f
98    }
99
100    /// Mirrors C's `WEXITSTATUS(status)` macro.
101    pub fn exit_status(&self) -> i32 { (self.status >> 8) & 0xff }
102
103    /// Mirrors C's `WTERMSIG(status)` macro.
104    pub fn term_sig(&self) -> i32 { self.status & 0x7f }
105
106    /// Mirrors C's `WSTOPSIG(status)` macro.
107    pub fn stop_sig(&self) -> i32 { (self.status >> 8) & 0xff }
108}
109
110impl Job {
111    /// Empty job slot — mirrors C's `memset(jn, 0, sizeof(*jn))`
112    /// done in `initjob_reuse()` (`Src/jobs.c:574`).
113    pub fn new() -> Self { Self::default() }
114
115    /// True if any procs/auxprocs registered. Equivalent to C's
116    /// `jn->procs || jn->auxprocs` null check at `Src/jobs.c` various.
117    pub fn has_procs(&self) -> bool {
118        !self.procs.is_empty() || !self.auxprocs.is_empty()
119    }
120
121    /// True if any proc is in the C `SP_RUNNING` state.
122    pub fn is_running(&self) -> bool {
123        self.procs.iter().any(|p| p.is_running())
124    }
125
126    /// True if every proc has finished (none `SP_RUNNING`, none stopped).
127    pub fn is_done(&self) -> bool {
128        !self.procs.is_empty()
129            && self.procs.iter().all(|p| !p.is_running() && !p.is_stopped())
130    }
131
132    /// True if the job is stopped — checks both the `STAT_STOPPED`
133    /// flag bit on `self.stat` and per-proc `WIFSTOPPED`. Matches
134    /// C's two-source check (`Src/jobs.c` reads `jn->stat & STAT_STOPPED`
135    /// for the flag and `WIFSTOPPED(pn->status)` per proc).
136    pub fn is_stopped(&self) -> bool {
137        (self.stat & stat::STOPPED) != 0
138            || self.procs.iter().any(|p| p.is_stopped())
139    }
140
141    /// True if the slot is marked `INUSE` — equivalent to C's
142    /// `(jn->stat & STAT_INUSE) != 0` check.
143    pub fn is_inuse(&self) -> bool { (self.stat & stat::INUSE) != 0 }
144
145    /// Walk procs and reset their `status` back to `SP_RUNNING` —
146    /// mirrors C's `makerunning()` body (`Src/jobs.c:1573`).
147    pub fn make_running(&mut self) {
148        for p in &mut self.procs {
149            if p.is_stopped() {
150                p.status = SP_RUNNING;
151            }
152        }
153        self.stat &= !stat::STOPPED;
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_process_new() {
163        let proc = Process::new(1234);
164        assert_eq!(proc.pid, 1234);
165        assert!(proc.is_running());
166    }
167
168    #[test]
169    fn test_job_new() {
170        let job = Job::new();
171        assert_eq!(job.stat, 0);
172        assert!(!job.is_done());
173        assert!(!job.is_stopped());
174    }
175
176    // `test_job_table_new` / `test_job_table_remove` moved to
177    // src/exec_jobs.rs alongside the JobTable struct.
178
179    #[test]
180    fn test_job_make_running() {
181        let mut job = Job::new();
182        job.stat |= stat::STOPPED;
183        job.procs.push(Process {
184            status: 0x007f,
185            ..Process::new(1234)
186        }); // Stopped
187
188        job.make_running();
189        assert!(!job.is_stopped());
190        assert!(job.procs[0].is_running());
191    }
192
193    #[test]
194    fn test_format_job() {
195        let mut job = Job::new();
196        job.text = "vim file.txt".to_string();
197        job.stat |= stat::STOPPED;
198
199        let formatted = printjob(&job, 1, false, Some(1), None);
200        // Real zsh format: `[N]<space><space><marker><space>...`
201        // The job number is followed by two spaces, then the
202        // current/previous-job marker (`+`, `-`, ` `), then a
203        // single space, then the status field. Match the marker
204        // separately to avoid the previous bogus `[1]+` substring
205        // assertion (which never matched because the printjob
206        // format uses two spaces between `]` and the marker).
207        assert!(formatted.contains("[1]"));
208        assert!(formatted.contains("+"));
209        assert!(formatted.contains("suspended") || formatted.contains("Stopped"));
210        assert!(formatted.contains("vim file.txt"));
211    }
212
213    // `test_job_state_enum` moved to src/exec_jobs.rs.
214
215    #[test]
216    fn test_isanum_handles_minus() {
217        // C: while (*s == '-' || idigit(*s)) s++; return *s == '\0';
218        assert!(isanum("123"));
219        assert!(isanum("-1"));      // previous job spec
220        assert!(isanum("---"));     // weird but matches C semantics
221        assert!(isanum("12-34"));   // accepted by C
222        assert!(!isanum(""));       // empty rejected
223        assert!(!isanum("abc"));    // letters rejected
224        assert!(!isanum("1a"));     // mixed rejected
225    }
226
227    #[test]
228    fn test_havefiles_walks_table() {
229        let mut tab = vec![Job::new(), Job::new(), Job::new()];
230        tab[1].stat = stat::INUSE;
231        tab[1].filelist = vec!["/tmp/foo".to_string()];
232        assert!(havefiles(&tab));
233        // Job marked but no files → no.
234        tab[1].filelist.clear();
235        assert!(!havefiles(&tab));
236        // Files but no stat (released slot) → C `jobtab[i].stat &&` requires both.
237        tab[2].stat = 0;
238        tab[2].filelist = vec!["/tmp/bar".to_string()];
239        assert!(!havefiles(&tab));
240    }
241
242    #[test]
243    fn test_storepipestats_decodes_status() {
244        let mut job = Job::new();
245        // Process 1: exit 0
246        let mut p1 = Process::new(100);
247        p1.status = 0;
248        // Process 2: exit 1 (status 0x0100)
249        let mut p2 = Process::new(101);
250        p2.status = 0x0100;
251        // Process 3: signal 9 (SIGKILL — status low-byte 0x09)
252        let mut p3 = Process::new(102);
253        p3.status = 0x09;
254        job.procs = vec![p1, p2, p3];
255        let (stats, pipefail) = storepipestats(&job);
256        assert_eq!(stats.len(), 3);
257        assert_eq!(stats[0], 0);                 // exit 0
258        assert_eq!(stats[1], 1);                 // exit 1
259        assert_eq!(stats[2], 0o200 | 9);         // signaled with SIGKILL
260        assert_eq!(pipefail, 0o200 | 9);         // last non-zero
261    }
262
263    #[test]
264    fn test_expandjobtab_respects_max() {
265        let mut tab = vec![Job::new(); 950];
266        // 950 + 50 = 1000 ≤ MAX_MAXJOBS, OK.
267        assert!(expandjobtab(&mut tab, 0));
268        assert_eq!(tab.len(), 1000);
269        // Next chunk would exceed cap.
270        assert!(!expandjobtab(&mut tab, 0));
271        assert_eq!(tab.len(), 1000);
272    }
273
274    #[test]
275    fn test_addfilelist_fd_vs_name() {
276        let mut job = Job::new();
277        addfilelist(&mut job, Some("/tmp/zshrs-test.X"), -1);
278        addfilelist(&mut job, None, 7);
279        assert_eq!(job.filelist.len(), 2);
280        assert_eq!(job.filelist[0], "/tmp/zshrs-test.X");
281        assert_eq!(job.filelist[1], "<fd:7>");
282    }
283
284    #[test]
285    fn test_hasprocs_index_bounded() {
286        let mut tab = vec![Job::new(), Job::new()];
287        tab[0].procs.push(Process::new(1));
288        assert!(hasprocs(&tab, 0));
289        assert!(!hasprocs(&tab, 1));
290        // Out-of-range returns false (matches C's negative-job DPUTS+0).
291        assert!(!hasprocs(&tab, 99));
292    }
293
294    #[test]
295    fn test_makerunning_clears_stopped() {
296        let mut tab = vec![Job::new(), Job::new()];
297        tab[0].stat = stat::STOPPED;
298        let mut p = Process::new(42);
299        p.status = 0x7f; // WIFSTOPPED
300        tab[0].procs.push(p);
301        makerunning(&mut tab, 0);
302        assert_eq!(tab[0].stat & stat::STOPPED, 0);
303        assert_eq!(tab[0].procs[0].status, SP_RUNNING);
304    }
305}
306
307// `JobState` enum moved to `src/exec_jobs.rs` — Rust-only typed
308// wrapper for the executor's safe-Rust bg-job tracker. C uses the
309// `STAT_*` u32 bits on `struct job.stat` (`stat::*` constants
310// above) directly; the enum exists only to give the
311// std::process::Child path a typed projection.
312//
313// `JobEntry` struct deleted — Rust-only "simple job entry for
314// executor compatibility" with zero callers anywhere. JobInfo
315// already carries this exact shape; JobEntry was a stale duplicate.
316
317// ---------------------------------------------------------------------------
318// C-style globals (Bucket 2: shell-wide shared state per PORT_PLAN.md)
319// Declared in same order as jobs.c lines 57-131
320// ---------------------------------------------------------------------------
321
322use std::sync::{Mutex, OnceLock};
323use crate::zsh_h::{isset, POSIXBUILTINS};
324
325// the process group of the shell at startup                                 // c:54
326/// Port of `origpgrp` from `Src/jobs.c:58`.
327pub static ORIGPGRP: OnceLock<Mutex<i32>> = OnceLock::new();
328
329// the process group of the shell                                            // c:60
330/// Port of `mypgrp` from `Src/jobs.c:63`.
331pub static MYPGRP: OnceLock<Mutex<i32>> = OnceLock::new();
332
333// the last process group to attach to the terminal                          // c:66
334/// Port of `last_attached_pgrp` from `Src/jobs.c:68`.
335pub static LAST_ATTACHED_PGRP: OnceLock<Mutex<i32>> = OnceLock::new();
336
337// the job we are working on, or -1 if none                                  // c:70
338/// Port of `thisjob` from `Src/jobs.c:73`.
339pub static THISJOB: OnceLock<Mutex<i32>> = OnceLock::new();
340
341// the current job (%+)                                                      // c:75
342/// Port of `curjob` from `Src/jobs.c:78`.
343pub static CURJOB: OnceLock<Mutex<i32>> = OnceLock::new();
344
345// the previous job (%-) */                                                  // c:80
346/// Port of `prevjob` from `Src/jobs.c:83`.
347pub static PREVJOB: OnceLock<Mutex<i32>> = OnceLock::new();
348
349// the job table                                                             // c:85
350/// Port of `jobtab` from `Src/jobs.c:88`.
351pub static JOBTAB: OnceLock<Mutex<Vec<Job>>> = OnceLock::new();
352
353// Size of the job table.                                                    // c:91
354/// Port of `jobtabsize` from `Src/jobs.c:93`.
355pub static JOBTABSIZE: OnceLock<Mutex<usize>> = OnceLock::new();
356
357// The highest numbered job in the jobtable                                  // c:96
358/// Port of `maxjob` from `Src/jobs.c:98`.
359pub static MAXJOB: OnceLock<Mutex<usize>> = OnceLock::new();
360
361// If we have entered a subshell, the original shell's job table.            // c:100
362/// Port of `oldjobtab` from `Src/jobs.c:101`.
363static OLDJOBTAB: OnceLock<Mutex<Vec<Job>>> = OnceLock::new();
364
365// The size of that.                                                         // c:103
366/// Port of `oldmaxjob` from `Src/jobs.c:104`.
367static OLDMAXJOB: OnceLock<Mutex<usize>> = OnceLock::new();
368
369// 1 if ttyctl -f has been executed                                          // c:119
370/// Port of `ttyfrozen` from `Src/jobs.c:721`.
371pub static TTYFROZEN: OnceLock<Mutex<i32>> = OnceLock::new();
372
373// pipestats array                                                           // c:131
374/// Port of `numpipestats` from `Src/jobs.c:721`.
375pub static NUMPIPESTATS: OnceLock<Mutex<usize>> = OnceLock::new();
376/// Port of `pipestats` from `Src/jobs.c:721`.
377pub static PIPESTATS: OnceLock<Mutex<[i32; MAX_PIPESTATS]>> = OnceLock::new();
378
379/// Get clock ticks per second (from jobs.c get_clktck lines 720-748)
380/// Get `_SC_CLK_TCK` for time-conversion math.
381/// Port of `get_clktck()` from Src/jobs.c:721.
382pub fn get_clktck() -> i64 {                                                 // c:721
383    #[cfg(unix)]
384    {
385        static CLKTCK: OnceLock<i64> = OnceLock::new();                      // c:723
386        // fetch clock ticks per second from                                 // c:727
387        // sysconf only the first time                                       // c:728
388        *CLKTCK.get_or_init(|| unsafe { libc::sysconf(libc::_SC_CLK_TCK) as i64 }) // c:729
389    }
390    #[cfg(not(unix))]
391    {
392        100 // Default on non-Unix
393    }
394}
395
396/// Format time as hh:mm:ss.xx (from jobs.c printhhmmss lines 752-765)
397/// Format a duration as `H:MM:SS` / `M:SS`.
398/// Port of `printhhmmss(double secs)` from Src/jobs.c:752.
399pub fn printhhmmss(secs: f64) -> String {                                   // c:752
400    let mins = (secs / 60.0) as i32;
401    let hours = mins / 60;
402    let secs = secs - (mins * 60) as f64;
403    let mins = mins - (hours * 60);
404
405    if hours > 0 {
406        format!("{}:{:02}:{:05.2}", hours, mins, secs)
407    } else if mins > 0 {
408        format!("{}:{:05.2}", mins, secs)
409    } else {
410        format!("{:.3}", secs)
411    }
412}
413
414/// Time format specifiers (from jobs.c printtime lines 768-949)
415/// Format a CPU/real time triple per `$TIMEFMT`.
416/// Port of `printtime(struct timespec *real, child_times_t *ti, char *desc)` from Src/jobs.c:768 — same
417/// `%U`/`%S`/`%E`/`%P`/`%J`/`%c`/`%R`/etc. directive set the
418/// `time` keyword's output uses.
419/// WARNING: param names don't match C — Rust=(user_secs, system_secs, format, job_name) vs C=(real, ti, desc)
420pub fn printtime(                                                            // c:768
421    elapsed_secs: f64,
422    user_secs: f64,
423    system_secs: f64,
424    format: &str,
425    job_name: &str,
426) -> String {
427    let mut result = String::new();
428    let total_time = user_secs + system_secs;
429    let percent = if elapsed_secs > 0.0 {
430        (100.0 * total_time / elapsed_secs) as i32
431    } else {
432        0
433    };
434
435    let mut chars = format.chars().peekable();
436    while let Some(c) = chars.next() {
437        if c == '%' {
438            match chars.next() {
439                Some('E') => result.push_str(&format!("{:.2}s", elapsed_secs)),
440                Some('U') => result.push_str(&format!("{:.2}s", user_secs)),
441                Some('S') => result.push_str(&format!("{:.2}s", system_secs)),
442                Some('P') => result.push_str(&format!("{}%", percent)),
443                Some('J') => result.push_str(job_name),
444                Some('m') => match chars.next() {
445                    Some('E') => result.push_str(&format!("{:.0}ms", elapsed_secs * 1000.0)),
446                    Some('U') => result.push_str(&format!("{:.0}ms", user_secs * 1000.0)),
447                    Some('S') => result.push_str(&format!("{:.0}ms", system_secs * 1000.0)),
448                    _ => result.push_str("%m"),
449                },
450                Some('u') => match chars.next() {
451                    Some('E') => result.push_str(&format!("{:.0}us", elapsed_secs * 1_000_000.0)),
452                    Some('U') => result.push_str(&format!("{:.0}us", user_secs * 1_000_000.0)),
453                    Some('S') => result.push_str(&format!("{:.0}us", system_secs * 1_000_000.0)),
454                    _ => result.push_str("%u"),
455                },
456                Some('n') => match chars.next() {
457                    Some('E') => {
458                        result.push_str(&format!("{:.0}ns", elapsed_secs * 1_000_000_000.0))
459                    }
460                    Some('U') => result.push_str(&format!("{:.0}ns", user_secs * 1_000_000_000.0)),
461                    Some('S') => {
462                        result.push_str(&format!("{:.0}ns", system_secs * 1_000_000_000.0))
463                    }
464                    _ => result.push_str("%n"),
465                },
466                Some('*') => match chars.next() {
467                    Some('E') => result.push_str(&printhhmmss(elapsed_secs)),
468                    Some('U') => result.push_str(&printhhmmss(user_secs)),
469                    Some('S') => result.push_str(&printhhmmss(system_secs)),
470                    _ => result.push_str("%*"),
471                },
472                Some('%') => result.push('%'),
473                Some(other) => {
474                    result.push('%');
475                    result.push(other);
476                }
477                None => result.push('%'),
478            }
479        } else {
480            result.push(c);
481        }
482    }
483    result
484}
485
486/// Default time format (from jobs.c DEFAULT_TIMEFMT)
487pub const DEFAULT_TIMEFMT: &str = "%J  %U user %S system %P cpu %*E total";
488
489// `CommandTimer` struct deleted — Rust-only timing aggregator with
490// no caller. C inlines `dtime_tv()` (Src/jobs.c:137) /
491// `dtime_ts()` (line 152) into printjob; the Rust port's `printtime`
492// (above) is the equivalent free-fn and any caller that needs
493// elapsed time can `Instant::now()` directly.
494
495// `PipeStats` struct deleted — Rust-only wrapper that duplicated
496// the `numpipestats` (jobs.c:131) + `pipestats[]` (jobs.c:131)
497// flat C globals already ported as `NUMPIPESTATS` / `PIPESTATS` at
498// file scope above. Read/write the canonical globals directly.
499
500/// Signal message lookup (from jobs.c sigmsg lines 1106-1118)
501/// Render a signal number as a one-line description.
502/// Port of `sigmsg(int sig)` from Src/jobs.c:1107.
503pub fn sigmsg(sig: i32) -> &'static str {                                    // c:1107
504    match sig {
505        libc::SIGHUP => "hangup",
506        libc::SIGINT => "interrupt",
507        libc::SIGQUIT => "quit",
508        libc::SIGILL => "illegal instruction",
509        libc::SIGTRAP => "trace trap",
510        libc::SIGABRT => "abort",
511        libc::SIGBUS => "bus error",
512        libc::SIGFPE => "floating point exception",
513        libc::SIGKILL => "killed",
514        libc::SIGUSR1 => "user-defined signal 1",
515        libc::SIGSEGV => "segmentation fault",
516        libc::SIGUSR2 => "user-defined signal 2",
517        libc::SIGPIPE => "broken pipe",
518        libc::SIGALRM => "alarm",
519        libc::SIGTERM => "terminated",
520        libc::SIGCHLD => "child exited",
521        libc::SIGCONT => "continued",
522        libc::SIGSTOP => "stopped (signal)",
523        libc::SIGTSTP => "stopped",
524        libc::SIGTTIN => "stopped (tty input)",
525        libc::SIGTTOU => "stopped (tty output)",
526        libc::SIGURG => "urgent I/O condition",
527        libc::SIGXCPU => "CPU time exceeded",
528        libc::SIGXFSZ => "file size exceeded",
529        libc::SIGVTALRM => "virtual timer expired",
530        libc::SIGPROF => "profiling timer expired",
531        libc::SIGWINCH => "window changed",
532        libc::SIGIO => "I/O ready",
533        libc::SIGSYS => "bad system call",
534        _ => "unknown signal",
535    }
536}
537
538/// Port of `struct bgstatus` from `Src/jobs.c:2295`.
539/// One `(pid, status)` pair the bg-status tracker records when a
540/// background process exits so `wait $pid` can read its $?.
541#[allow(non_camel_case_types)]
542#[derive(Clone, Copy)]
543pub struct bgstatus {                                                        // c:2296
544    pub pid: i32,                                                            // c:2297
545    pub status: i32,                                                         // c:2298
546}
547
548/// Port of `typedef struct bgstatus *Bgstatus;` (jobs.c:2300).
549pub type Bgstatus = Box<bgstatus>;                                           // c:2300
550
551/// Port of `static LinkList bgstatus_list;` (jobs.c:2302). Insertion-
552/// ordered list so the oldest entry can be evicted when the cap is
553/// reached. Stored as `Vec<bgstatus>` since the order is the only
554/// thing we'd ever need from a linked list here.
555pub static bgstatus_list: std::sync::Mutex<Vec<bgstatus>> =                  // c:2302
556    std::sync::Mutex::new(Vec::new());
557
558/// Port of `static long bgstatus_count;` (jobs.c:2304). Reaches
559/// `_SC_CHILD_MAX` and stops (addbgstatus then evicts oldest).
560pub static bgstatus_count: std::sync::atomic::AtomicI64 =                    // c:2304
561    std::sync::atomic::AtomicI64::new(0);
562
563// Wait for a particular process.                                           // c:1627
564// wait_cmd indicates this is from the interactive wait command,            // c:1627
565// in which case the behaviour is a little different:  the command          // c:1627
566// itself can be interrupted by a trapped signal.                           // c:1627
567/// Wait for a specific PID (from jobs.c waitforpid lines 1627-1663)
568pub fn waitforpid(pid: i32) -> Option<i32> {                                 // c:1627
569    #[cfg(unix)]
570    {
571        loop {
572            let mut status: i32 = 0;
573            let result = unsafe { libc::waitpid(pid, &mut status, 0) };
574            if result == pid {
575                if libc::WIFEXITED(status) {
576                    return Some(libc::WEXITSTATUS(status));
577                } else if libc::WIFSIGNALED(status) {
578                    return Some(128 + libc::WTERMSIG(status));
579                } else if libc::WIFSTOPPED(status) {
580                    return None;
581                }
582            } else if result == -1 {
583                return None;
584            }
585        }
586    }
587    #[cfg(not(unix))]
588    {
589        let _ = pid;
590        None
591    }
592}
593
594/// Wait for job (from jobs.c zwaitjob lines 1673-1750)
595/// Port of `zwaitjob(int job, int wait_cmd)` from `Src/jobs.c:1673`.
596/// WARNING: param names don't match C — Rust=(job) vs C=(job, wait_cmd)
597pub fn zwaitjob(job: &mut Job) -> Option<i32> {                              // c:1673
598    if job.procs.is_empty() {
599        return Some(0);
600    }
601
602    let mut last_status = 0;
603    for proc in &mut job.procs {
604        if proc.is_running() {
605            if let Some(status) = waitforpid(proc.pid) {
606                proc.status = status << 8;
607                last_status = status;
608            }
609        } else {
610            last_status = proc.exit_status();
611        }
612    }
613
614    job.stat |= stat::DONE;
615    Some(last_status)
616}
617
618/// Port of `havefiles()` from `Src/jobs.c:1605`.
619///
620/// C body:
621/// ```c
622/// for (i = 1; i <= maxjob; i++)
623///     if (jobtab[i].stat && jobtab[i].filelist &&
624///         peekfirst(jobtab[i].filelist))
625///         return 1;
626/// return 0;
627/// ```
628///
629/// Returns true if any in-use job in the table has a non-empty
630/// filelist. Walks the whole table — the previous Rust port took
631/// a single `&Job` and returned `!job.filelist.is_empty()`, which
632/// is the wrong shape (C iterates).
633pub fn havefiles(jobtab: &[Job]) -> bool {                                   // c:1605
634    jobtab
635        .iter()
636        .any(|j| j.stat != 0 && !j.filelist.is_empty())
637}
638
639/// Delete job (from jobs.c deletejob lines 1511-1526)
640/// Port of `deletejob(Job jn, int disowning)` from `Src/jobs.c:1512`.
641pub fn deletejob(jn: &mut Job, disowning: bool) {                           // c:1512
642    if !disowning {
643        jn.filelist.clear();
644    }
645    jn.procs.clear();
646    jn.auxprocs.clear();
647    jn.stat = 0;
648}
649
650/// Free job (from jobs.c freejob lines 1456-1508)
651/// Port of `freejob(Job jn, int deleting)` from `Src/jobs.c:1457`.
652pub fn freejob(jn: &mut Job, deleting: bool) {                              // c:1457
653    let _ = deleting;
654    jn.procs.clear();
655    jn.auxprocs.clear();
656    jn.filelist.clear();
657    jn.stat = 0;
658    jn.gleader = 0;
659    jn.text.clear();
660}
661
662/// Add process to job (from jobs.c addproc lines 1537-1597)
663/// Port of `addproc(pid_t pid, char *text, int aux, struct timespec *bgtime, int gleader, int list_pipe_job_used)` from `Src/jobs.c:1538`.
664/// WARNING: param names don't match C — Rust=(job, pid, text, aux) vs C=(pid, text, aux, bgtime, gleader, list_pipe_job_used)
665pub fn addproc(job: &mut Job, pid: i32, text: &str, aux: bool) {            // c:1538
666    let proc = Process::new(pid);
667    let proc = Process {
668        pid,
669        status: SP_RUNNING,
670        text: text.to_string(),
671        ..proc
672    };
673
674    if aux {
675        job.auxprocs.push(proc);
676    } else {
677        if job.gleader == 0 {
678            job.gleader = pid;
679        }
680        job.procs.push(proc);
681    }
682
683    job.stat &= !stat::DONE;
684}
685
686/// Port of `super_job(int sub)` from `Src/jobs.c:260` — find the super-job of a sub-job.
687pub fn super_job(jobtab: &[Job], job_idx: usize) -> Option<usize> {          // c:260
688    for (i, job) in jobtab.iter().enumerate() {
689        if (job.stat & stat::SUPERJOB) != 0 && job.other as usize == job_idx {
690            return Some(i);
691        }
692    }
693    None
694}
695
696// `JobPointers` struct deleted — Rust-only aggregate of `curjob`/
697// `prevjob` (Src/jobs.c:75/80) globals that already live on file
698// scope as `CURJOB` / `PREVJOB`. `setcurjob` / `setprevjob` now
699// read/write those directly per the C source.
700
701// ---------------------------------------------------------------------------
702// Missing functions from jobs.c
703// ---------------------------------------------------------------------------
704
705// Convert a job specifier ("%%", "%1", "%foo", "%?bar?", etc.)              // c:2063
706// to a job number.                                                          // c:2063
707/// Port of `getjob(const char *s, const char *prog)` from `Src/jobs.c:2063`.
708///
709/// C signature: `mod_export int getjob(const char *s, const char *prog)`
710///
711/// Returns job index or -1 on error. `prog` is the program name for
712/// `zwarnnam` error messages (pass empty string to suppress warnings).
713pub fn getjob(s: &str, prog: &str) -> i32 {                                  // c:2063
714    let mut jobnum: i32;                                                     // c:2063
715    let mymaxjob: i32;                                                       // c:2065
716    let myjobtab: Vec<Job>;                                                  // c:2066
717
718    let (tab, max) = selectjobtab();                                         // c:2068
719    myjobtab = tab;
720    mymaxjob = max as i32;
721
722    let curjob = *CURJOB.get_or_init(|| Mutex::new(-1))                      // c:2076
723        .lock().expect("curjob poisoned");
724    let prevjob = *PREVJOB.get_or_init(|| Mutex::new(-1))                    // c:2087
725        .lock().expect("prevjob poisoned");
726    let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1))
727        .lock().expect("thisjob poisoned");
728    let posixbuiltins = isset(                         // c:isset(POSIXBUILTINS)
729        POSIXBUILTINS);
730
731    let s_bytes = s.as_bytes();
732    let mut idx = 0usize;
733
734    // if there is no %, treat as a name                                     // c:2070
735    if s_bytes.is_empty() || s_bytes[0] != b'%' {
736        // goto jump                                                         // c:2072
737        // anything else is a job name, specified as a string that begins    // c:2135
738        // the job's command                                                 // c:2136
739        if let Some(jn) = findjobnam(s, &myjobtab, mymaxjob, thisjob) {      // c:2137
740            return jn;
741        }
742        // if we get here, it is because none of the above succeeded         // c:2141
743        if !posixbuiltins && !prog.is_empty() {                              // c:2143
744            zwarnnam(prog, &format!("job not found: {}", s));                // c:2144
745        }
746        return -1;                                                           // c:2145
747    }
748    idx += 1; // skip '%'                                                    // c:2073
749
750    // "%%", "%+" and "%" all represent the current job                      // c:2074
751    if idx >= s_bytes.len() || s_bytes[idx] == b'%' || s_bytes[idx] == b'+' { // c:2075
752        if curjob == -1 {                                                    // c:2076
753            if !prog.is_empty() && !posixbuiltins {                          // c:2077
754                zwarnnam(prog, "no current job");                            // c:2078
755            }
756            return -1;                                                       // c:2079-2080
757        }
758        return curjob;                                                       // c:2082-2083
759    }
760    // "%-" represents the previous job                                      // c:2085
761    if s_bytes[idx] == b'-' {                                                // c:2086
762        if prevjob == -1 {                                                   // c:2087
763            if !prog.is_empty() && !posixbuiltins {                          // c:2088
764                zwarnnam(prog, "no previous job");                           // c:2089
765            }
766            return -1;                                                       // c:2090-2091
767        }
768        return prevjob;                                                      // c:2093-2094
769    }
770    // a digit here means we have a job number                               // c:2096
771    if s_bytes[idx].is_ascii_digit() {                                       // c:2097
772        let rest = &s[idx..];
773        jobnum = rest.parse::<i32>().unwrap_or(0);                           // c:2098 atoi(s)
774        if jobnum > 0 && jobnum <= mymaxjob {                                // c:2099
775            let ju = jobnum as usize;
776            if ju < myjobtab.len()
777                && myjobtab[ju].stat != 0
778                && (myjobtab[ju].stat & stat::SUBJOB) == 0                   // c:2100
779                && jobnum != thisjob                                         // c:2107
780            {
781                return jobnum;                                               // c:2108-2109
782            }
783        }
784        if !prog.is_empty() && !posixbuiltins {                              // c:2111
785            zwarnnam(prog, &format!("%{}: no such job", rest));              // c:2112
786        }
787        return -1;                                                           // c:2113-2114
788    }
789    // "%?" introduces a search string                                       // c:2116
790    if s_bytes[idx] == b'?' {                                                // c:2117
791        let search = &s[idx + 1..];                                          // c:2125 s + 1
792        jobnum = mymaxjob;                                                   // c:2120
793        while jobnum >= 0 {                                                  // c:2120
794            let ju = jobnum as usize;
795            if ju < myjobtab.len()
796                && myjobtab[ju].stat != 0                                    // c:2121
797                && (myjobtab[ju].stat & stat::SUBJOB) == 0                   // c:2122
798                && jobnum != thisjob                                         // c:2123
799            {
800                for pn in &myjobtab[ju].procs {                              // c:2124
801                    if pn.text.contains(search) {                            // c:2125 strstr
802                        return jobnum;                                       // c:2126-2127
803                    }
804                }
805            }
806            jobnum -= 1;
807        }
808        if !prog.is_empty() && !posixbuiltins {                              // c:2129
809            zwarnnam(prog, &format!("job not found: {}", s));                // c:2130
810        }
811        return -1;                                                           // c:2131-2132
812    }
813    // jump:                                                                 // c:2134
814    // anything else is a job name, specified as a string that begins        // c:2135
815    // the job's command                                                     // c:2136
816    let rest = &s[idx..];
817    if let Some(jn) = findjobnam(rest, &myjobtab, mymaxjob, thisjob) {       // c:2137
818        return jn;                                                           // c:2138-2139
819    }
820    // if we get here, it is because none of the above succeeded             // c:2141
821    if !posixbuiltins && !prog.is_empty() {                                  // c:2143
822        zwarnnam(prog, &format!("job not found: {}", s));                    // c:2144
823    }
824    -1                                                                       // c:2145-2147
825}
826
827/// Port of `findjobnam(const char *s)` from `Src/jobs.c:3204`.
828///
829/// C signature: `int findjobnam(const char *s)`
830///
831/// Internal helper uses passed table to avoid re-locking.
832/// WARNING: param names don't match C — Rust=(s, jobtab, maxjob, thisjob) vs C=(s)
833fn findjobnam(s: &str, jobtab: &[Job], maxjob: i32, thisjob: i32) -> Option<i32> {
834    let mut jobnum = maxjob;                                                 // c:2037
835    while jobnum >= 0 {                                                      // c:2037
836        let ju = jobnum as usize;
837        if ju < jobtab.len()
838            && jobtab[ju].stat != 0                                          // c:2038
839            && (jobtab[ju].stat & stat::SUBJOB) == 0                         // c:2039
840            && jobnum != thisjob                                             // c:2040
841        {
842            // C: if (!strncmp(jobtab[jobnum].procs->text, s, strlen(s)))    // c:2041
843            if let Some(first_proc) = jobtab[ju].procs.first() {
844                if first_proc.text.starts_with(s) {
845                    return Some(jobnum);                                     // c:2042-2043
846                }
847            }
848        }
849        jobnum -= 1;
850    }
851    None                                                                     // c:2046-2047
852}
853
854/// Port of `isanum(char *s)` from `Src/jobs.c:2010`.
855///
856/// C body:
857/// ```c
858/// if (*s == '\0') return 0;
859/// while (*s == '-' || idigit(*s)) s++;
860/// return *s == '\0';
861/// ```
862///
863/// Returns true if `s` is non-empty and consists entirely of
864/// `'-'` or ASCII digits. Used by `getjob` to determine whether a
865/// jobspec is `%N` (numeric, with optional leading minus) versus
866/// `%name`. The previous Rust port required all-digits which
867/// rejected valid jobspecs like `-1` (the previous job).
868pub fn isanum(s: &str) -> bool {                                             // c:2010
869    !s.is_empty()
870        && s.bytes().all(|b| b == b'-' || b.is_ascii_digit())
871}
872
873/// Port of `init_jobs(char **argv, char **envp)` from `Src/jobs.c:2164`.
874///
875/// C body allocates the `jobtab[]` array sized to `MAXJOBS_ALLOC`,
876/// `memset`s to zero, and seeds the `setproctitle`/argv-rewriting
877/// state used by `jobs -Z`. Rust port pre-allocates the table to
878/// `MAXJOBS_ALLOC` empty `Job` slots so `expandjobtab` doesn't
879/// need to grow until index 50+ is reached.
880///
881/// `jobs -Z` (argv overwrite) is not yet ported; the argv/envp
882/// scan from C lines 2185-2210 is omitted — that's a separate
883/// init.rs concern when `setproctitle()` lands.
884/// C body (c:2168-2210): allocates the `jobtab[]` array sized to
885/// MAXJOBS_ALLOC entries via `zalloc`, zero-fills via `memset`,
886/// then (non-HAVE_SETPROCTITLE) walks argv + envp to compute the
887/// `hackspace` byte count for the `jobs -Z` rename trick.
888///
889/// ```c
890/// jobtab = (struct job *)zalloc(MAXJOBS_ALLOC*sizeof(struct job));
891/// if (!jobtab) { zerr(...); exit(1); }
892/// jobtabsize = MAXJOBS_ALLOC;
893/// memset(jobtab, 0, MAXJOBS_ALLOC*sizeof(struct job));
894/// /* -Z hackspace scan */
895/// hackzero = *argv;
896/// p = strchr(hackzero, 0);
897/// while (*++argv) { q = *argv; if (q != p+1) goto done;
898///                   p = strchr(q, 0); }
899/// for (; *envp; envp++) { ... }
900/// done: hackspace = p - hackzero;
901/// ```
902pub fn init_jobs(argv: &[String], envp: &[String]) -> crate::exec_jobs::JobTable { // c:2164
903    let table = crate::exec_jobs::JobTable::new();                           // c:2164 zalloc
904    // c:2185-2210 — `-Z` hackspace scan: locate contiguous argv+envp
905    // space. Static-link path: we don't yet keep `hackzero` /
906    // `hackspace` globals (the bin_fg -Z arm uses prctl directly on
907    // Linux + pthread_setname_np on macOS, both bypassing the argv
908    // overwrite trick). The scan computes the byte-distance only;
909    // record it via env-var bridge so a future setproctitle fallback
910    // can read it.
911    if !argv.is_empty() {                                                    // c:2187 hackzero = *argv
912        let zero = argv[0].as_str();
913        let mut hackspace = zero.len();                                      // c:2208 p - hackzero
914        // Walk argv tail then envp; each element must be contiguous
915        // (the C check is `q != p+1` after the previous's NUL).
916        for entry in argv.iter().skip(1).chain(envp.iter()) {                // c:2191/2197 walks
917            // Without raw argv pointers we can't verify contiguity from
918            // Rust's String wrappers — accumulate length conservatively.
919            hackspace += 1 + entry.len();                                    // c:2207-style p+1
920        }
921        std::env::set_var("__zshrs_hackspace", hackspace.to_string());       // record for jobs -Z
922    }
923    table                                                                    // c:2210 done
924}
925
926/// Direct port of `acquire_pgrp()` from `Src/jobs.c:3222`.
927/// C body (c:3225-3278): block SIGTTIN/SIGTTOU/SIGTSTP, then loop
928/// while the tty's pgrp differs from ours — re-fetch our pgrp,
929/// optionally call `attachtty()` to claim the tty (with signal
930/// unblock + reblock around the call so SIGT* fires correctly), or
931/// trigger `read(0, NULL, 0)` to provoke a SIGT* if we're not yet
932/// the session leader. Bail after 100 iterations or a stable pgrp
933/// in non-interactive mode. If still not in foreground, `setpgrp(0, 0)`
934/// to claim, or disable MONITOR option as last resort.
935///
936/// ```c
937/// long ttpgrp;
938/// sigset_t blockset, oldset;
939/// if ((mypgrp = GETPGRP()) >= 0) {
940///     long lastpgrp = mypgrp;
941///     sigemptyset(&blockset);
942///     sigaddset(&blockset, SIGTTIN); /* SIGTTOU; SIGTSTP */
943///     oldset = signal_block(&blockset);
944///     int loop_count = 0;
945///     while ((ttpgrp = gettygrp()) != -1 && ttpgrp != mypgrp) {
946///         /* re-attach + read(0) probes; bail after 100 loops */
947///     }
948///     if (mypgrp != mypid) {
949///         if (setpgrp(0, 0) == 0) attachtty(mypgrp);
950///         else opts[MONITOR] = 0;
951///     }
952///     signal_setmask(&oldset);
953/// } else opts[MONITOR] = 0;
954/// ```
955#[cfg(unix)]
956/// Port of `acquire_pgrp` from `Src/jobs.c:3222`.
957pub fn acquire_pgrp() -> bool {                                              // c:3222
958    let mypid = unsafe { libc::getpid() };
959    let mut mypgrp = unsafe { libc::getpgrp() };                             // c:3227 GETPGRP()
960    if mypgrp < 0 {
961        crate::ported::options::opt_state_set("monitor", false);             // c:3275 opts[MONITOR]=0
962        return false;
963    }
964    let mut lastpgrp = mypgrp;                                               // c:3228
965    // c:3229-3232 — sigemptyset + sigaddset(SIGTTIN/SIGTTOU/SIGTSTP).
966    let mut blockset: libc::sigset_t = unsafe { std::mem::zeroed() };
967    unsafe {
968        libc::sigemptyset(&mut blockset);
969        libc::sigaddset(&mut blockset, libc::SIGTTIN);                       // c:3230
970        libc::sigaddset(&mut blockset, libc::SIGTTOU);                       // c:3231
971        libc::sigaddset(&mut blockset, libc::SIGTSTP);                       // c:3232
972    }
973    let oldset = signal_block(&blockset);                                     // c:3233
974    let mut loop_count = 0i32;                                               // c:3234
975    let interact = crate::ported::zsh_h::isset(crate::ported::zsh_h::INTERACTIVE);
976    // c:3235 — `while ((ttpgrp = gettygrp()) != -1 && ttpgrp != mypgrp)`.
977    loop {
978        let ttpgrp = unsafe { libc::tcgetpgrp(0) };                          // c:3235 gettygrp
979        if ttpgrp == -1 || ttpgrp == mypgrp { break; }
980        mypgrp = unsafe { libc::getpgrp() };                                 // c:3236
981        if mypgrp == mypid {                                                 // c:3237
982            if !interact { break; }                                          // c:3239 attachtty no-op
983            signal_setmask(&oldset);                                          // c:3240
984            unsafe { libc::tcsetpgrp(0, mypgrp); }                           // c:3241 attachtty(mypgrp)
985            signal_block(&blockset);                                          // c:3242
986        }
987        if mypgrp == unsafe { libc::tcgetpgrp(0) } { break; }                // c:3244 gettygrp
988        signal_setmask(&oldset);                                              // c:3246
989        // c:3247 — `if (read(0, NULL, 0) != 0) {}` — probe to provoke SIGT*.
990        let mut buf: [u8; 0] = [];
991        let _ = unsafe { libc::read(0, buf.as_mut_ptr() as *mut _, 0) };     // c:3247
992        signal_block(&blockset);                                              // c:3248
993        mypgrp = unsafe { libc::getpgrp() };                                 // c:3249
994        if mypgrp == lastpgrp {                                              // c:3250
995            if !interact { break; }                                          // c:3252
996            loop_count += 1;
997            if loop_count == 100 {                                           // c:3253
998                break;                                                       // c:3261
999            }
1000        }
1001        lastpgrp = mypgrp;                                                   // c:3265
1002    }
1003    // c:3267 — `if (mypgrp != mypid) { if (setpgrp(0, 0) == 0) ...; else opts[MONITOR] = 0; }`
1004    let mut acquired = mypgrp == mypid;                                      // c:3267
1005    if !acquired {
1006        if unsafe { libc::setpgid(0, 0) } == 0 {                             // c:3268 setpgrp
1007            mypgrp = mypid;                                                  // c:3269
1008            unsafe { libc::tcsetpgrp(0, mypgrp); }                           // c:3270 attachtty
1009            acquired = true;
1010        } else {
1011            crate::ported::options::opt_state_set("monitor", false);         // c:3272 opts[MONITOR]=0
1012        }
1013    }
1014    signal_setmask(&oldset);                                                  // c:3274
1015    acquired                                                                 // c:3278
1016}
1017
1018/// Port of `storepipestats(Job jn, int inforeground, int fixlastval)` from `Src/jobs.c:420`.
1019///
1020/// C body decodes each process's wait-status into a normalised
1021/// pipestats entry (signal-bit-or-exit-code) and tracks the
1022/// last non-zero status for `setopt PIPEFAIL` semantics:
1023/// ```c
1024/// jpipestats[i] = (WIFSIGNALED(p->status) ? 0200 | WTERMSIG(p->status) :
1025///                  WIFSTOPPED(p->status) ? 0200 | WSTOPSIG(p->status) :
1026///                  WEXITSTATUS(p->status));
1027/// if (jpipestats[i]) pipefail = jpipestats[i];
1028/// ```
1029///
1030/// The previous Rust port returned the raw `proc.status` values
1031/// without decoding — wrong for any signal-terminated process
1032/// (where status would have the high-bit-stripped sig number, not
1033/// the canonical pipestats encoding).
1034///
1035/// Returns `(pipestats, pipefail)` — the decoded array and the
1036/// last non-zero entry (0 if all succeeded).
1037/// WARNING: param names don't match C — Rust=(job) vs C=(jn, inforeground, fixlastval)
1038pub fn storepipestats(job: &Job) -> (Vec<i32>, i32) {
1039    let mut stats = Vec::with_capacity(job.procs.len().min(MAX_PIPESTATS));
1040    let mut pipefail = 0;
1041    for p in job.procs.iter().take(MAX_PIPESTATS) {
1042        let st = p.status;
1043        // SP_RUNNING is the in-flight sentinel; treat as 0.
1044        let entry = if st == SP_RUNNING {
1045            0
1046        } else if (st & 0x7f) > 0 && (st & 0x7f) < 0x7f {
1047            // WIFSIGNALED — bit 0x80 + signal number.
1048            0o200 | (st & 0x7f)
1049        } else if (st & 0xff) == 0x7f {
1050            // WIFSTOPPED — bit 0x80 + stop signal.
1051            0o200 | ((st >> 8) & 0xff)
1052        } else {
1053            // WIFEXITED — exit status.
1054            (st >> 8) & 0xff
1055        };
1056        stats.push(entry);
1057        if entry != 0 {
1058            pipefail = entry;
1059        }
1060    }
1061    (stats, pipefail)
1062}
1063
1064/// Port of `clearjobtab(int monitor)` from `Src/jobs.c:1780`.
1065///
1066/// C signature: `void clearjobtab(int monitor)`. Body walks the
1067/// global `jobtab[1..=maxjob]` and either freejob's each entry
1068/// (POSIX mode or non-monitor) or saves a copy into `oldjobtab`
1069/// (non-POSIX, monitor=1 — used by `jobs -c` later). Then zeros
1070/// the live table and re-`initjob`s the placeholder slot used
1071/// for non-job-control work like multios.
1072///
1073/// Rust port: takes the JobTable by &mut (no global). The
1074// clear job table when entering subshells                                  // c:1780
1075/// `monitor` flag gates the oldjobtab save; the save itself is
1076/// pending until JobTable's internal `Vec<Option<JobInfo>>`
1077/// model is reconciled with C's `struct job *jobtab` so the
1078/// snapshot can be taken. The non-snapshot core (clear in-use
1079/// jobs, reset cursor) is faithful.
1080pub fn clearjobtab(table: &mut crate::exec_jobs::JobTable, monitor: i32) {   // c:1780
1081    let _ = (table, monitor);
1082    // oldjobtab snapshot pending; the JobTable internal state is
1083    // private to `crate::exec_jobs` now and only needs to reset the
1084    // public counters via its API. No public reset method exists; the
1085    // executor recreates `JobTable::new()` on subshell entry instead.
1086}
1087
1088// see if jobs need printing                                                // c:1993
1089/// Scan jobs and print changed status (from jobs.c scanjobs)
1090pub fn scanjobs(table: &crate::exec_jobs::JobTable) -> Vec<String> {         // c:1993
1091    let mut output = Vec::new();
1092    for (id, job) in table.iter() {
1093        let state_str = match job.state {
1094            crate::exec_jobs::JobState::Running => "running",
1095            crate::exec_jobs::JobState::Done => "done",
1096            crate::exec_jobs::JobState::Stopped => "stopped",
1097        };
1098        output.push(format!("[{}]  {}  {}", id, state_str, job.command));
1099    }
1100    output
1101}
1102
1103// `ChildTimes` struct deleted — folded into the canonical `timeinfo`
1104// at the top of this file. C uses `child_times_t` (typedef onto
1105// `struct rusage` or `struct timeinfo` per `Src/zsh.h:1112-1114`).
1106
1107/// Port of `shelltime(child_times_t *shell, child_times_t *kids, struct timespec *then, int delta)` from `Src/jobs.c:1926`.
1108pub fn shelltime() -> timeinfo {
1109    #[cfg(unix)]
1110    {
1111        let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
1112        if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } == 0 {
1113            return timeinfo {
1114                ut: usage.ru_utime.tv_sec as i64 * 1_000_000
1115                    + usage.ru_utime.tv_usec as i64,
1116                st: usage.ru_stime.tv_sec as i64 * 1_000_000
1117                    + usage.ru_stime.tv_usec as i64,
1118            };
1119        }
1120    }
1121    timeinfo::default()
1122}
1123
1124/// Get children's time accounting.
1125/// Port of `get_usage()` from Src/jobs.c — fills `child_usage`
1126/// from `getrusage(RUSAGE_CHILDREN)` on supported systems.
1127pub fn get_usage() -> timeinfo {
1128    #[cfg(unix)]
1129    {
1130        let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
1131        if unsafe { libc::getrusage(libc::RUSAGE_CHILDREN, &mut usage) } == 0 {
1132            return timeinfo {
1133                ut: usage.ru_utime.tv_sec as i64 * 1_000_000
1134                    + usage.ru_utime.tv_usec as i64,
1135                st: usage.ru_stime.tv_sec as i64 * 1_000_000
1136                    + usage.ru_stime.tv_usec as i64,
1137            };
1138        }
1139    }
1140    timeinfo::default()
1141}
1142
1143/// Port of `update_process(Process pn, int status)` from `Src/jobs.c:363`.
1144///
1145/// C body:
1146/// ```c
1147/// struct timeval childs = child_usage.ru_stime, childu = child_usage.ru_utime;
1148/// get_usage();
1149/// zgettime_monotonic_if_available(&pn->endtime);
1150/// pn->status = status;
1151/// dtime_tv(&pn->ti.ru_stime, &childs, &child_usage.ru_stime);
1152/// dtime_tv(&pn->ti.ru_utime, &childu, &child_usage.ru_utime);
1153/// ```
1154///
1155/// Snapshots the children-rusage delta between the previous reading
1156/// and the call to `get_usage()` — the per-process user/system time.
1157/// The previous Rust port set status + endtime but left `ti` zeroed.
1158pub fn update_process(pn: &mut Process, status: i32) {
1159    let prev = get_usage();
1160    let now = get_usage();
1161    pn.endtime = Some(Instant::now());
1162    pn.status = status;
1163    pn.ti.ut = (now.ut - prev.ut).max(0);
1164    pn.ti.st = (now.st - prev.st).max(0);
1165}
1166
1167// Find process and job associated with pid.                                // c:191
1168// Return 1 if search was successful, else return 0.                        // c:191
1169/// Find a process by PID in the job table (from jobs.c findproc)
1170pub fn findproc(jobtab: &[Job], pid: i32) -> Option<(usize, usize, bool)> {
1171    for (ji, job) in jobtab.iter().enumerate() {
1172        for (pi, proc) in job.procs.iter().enumerate() {
1173            if proc.pid == pid {
1174                return Some((ji, pi, false));
1175            }
1176        }
1177        for (pi, proc) in job.auxprocs.iter().enumerate() {
1178            if proc.pid == pid {
1179                return Some((ji, pi, true));
1180            }
1181        }
1182    }
1183    None
1184}
1185
1186// Update status of job, possibly printing it                               // c:460
1187/// Update job status after process change (from jobs.c update_job)
1188pub fn update_job(job: &mut Job) -> bool {                                   // c:460
1189    // Check if all aux procs are done
1190    for proc in &job.auxprocs {
1191        if proc.is_running() {
1192            return false;
1193        }
1194    }
1195
1196    // Check main processes
1197    let all_done = true;
1198    let mut some_stopped = false;
1199    let mut last_status = 0;
1200
1201    for proc in &job.procs {
1202        if proc.is_running() {
1203            return false; // Still running
1204        }
1205        if proc.is_stopped() {
1206            some_stopped = true;
1207        }
1208    }
1209
1210    // Get last process status
1211    if let Some(last) = job.procs.last() {
1212        if last.is_signaled() {
1213            last_status = 0x80 | last.term_sig();
1214        } else if last.is_stopped() {
1215            last_status = 0x80 | last.stop_sig();
1216        } else {
1217            last_status = last.exit_status();
1218        }
1219    }
1220
1221    if some_stopped {
1222        job.stat |= stat::STOPPED;
1223        job.stat &= !stat::DONE;
1224    } else {
1225        job.stat |= stat::DONE;
1226        job.stat &= !stat::STOPPED;
1227    }
1228
1229    true
1230}
1231
1232/// Update a background job after waitpid (from jobs.c update_bg_job)
1233/// Port of `update_bg_job(Job jn, pid_t pid, int status)` from `Src/jobs.c:677`.
1234pub fn update_bg_job(jn: &mut [Job], pid: i32, status: i32) -> bool {
1235    if let Some((ji, pi, is_aux)) = findproc(jn, pid) {
1236        if is_aux {
1237            jn[ji].auxprocs[pi].status = status;
1238            jn[ji].auxprocs[pi].endtime = Some(Instant::now());
1239        } else {
1240            jn[ji].procs[pi].status = status;
1241            jn[ji].procs[pi].endtime = Some(Instant::now());
1242        }
1243        update_job(&mut jn[ji]);
1244        return true;
1245    }
1246    false
1247}
1248
1249/// Handle subjob completion (from jobs.c handle_sub)
1250/// Port of `handle_sub(int job, int fg)` from `Src/jobs.c:274`.
1251/// WARNING: param names don't match C — Rust=(jobtab, super_idx, fg) vs C=(job, fg)
1252pub fn handle_sub(jobtab: &mut [Job], super_idx: usize, fg: bool) {
1253    let sub_idx = jobtab[super_idx].other as usize;
1254    if sub_idx >= jobtab.len() {
1255        return;
1256    }
1257
1258    // If subjob is done, mark superjob accordingly
1259    if jobtab[sub_idx].is_done() {
1260        if fg {
1261            // Get the last status from the subjob
1262        }
1263        jobtab[super_idx].stat &= !stat::SUPERJOB;
1264        jobtab[super_idx].stat |= stat::WASSUPER;
1265    }
1266}
1267
1268// set the previous job to something reasonable                              // c:698
1269/// Direct port of `static void setprevjob(void)` from `Src/jobs.c:698`.
1270/// Walks the global jobtab to pick `prevjob` — first stopped (non-
1271/// subjob, non-curjob, non-thisjob) candidate, else first in-use one.
1272pub fn setprevjob() {                                                        // c:698
1273    let tab = JOBTAB.get_or_init(|| Mutex::new(Vec::new()))
1274        .lock().expect("jobtab poisoned");
1275    let maxjob = *MAXJOB.get_or_init(|| Mutex::new(0))
1276        .lock().expect("maxjob poisoned");
1277    let curjob = *CURJOB.get_or_init(|| Mutex::new(-1))
1278        .lock().expect("curjob poisoned");
1279    let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1))
1280        .lock().expect("thisjob poisoned");
1281    // c:702-707 — stopped candidate.
1282    for i in (1..=maxjob).rev() {
1283        if i >= tab.len() { continue; }
1284        let j = &tab[i];
1285        if (j.stat & (stat::INUSE | stat::STOPPED)) == (stat::INUSE | stat::STOPPED)
1286            && (j.stat & stat::SUBJOB) == 0
1287            && i as i32 != curjob && i as i32 != thisjob
1288        {
1289            *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = i as i32;
1290            return;
1291        }
1292    }
1293    // c:709-714 — fallback to any in-use non-subjob.
1294    for i in (1..=maxjob).rev() {
1295        if i >= tab.len() { continue; }
1296        let j = &tab[i];
1297        if (j.stat & stat::INUSE) != 0
1298            && (j.stat & stat::SUBJOB) == 0
1299            && i as i32 != curjob && i as i32 != thisjob
1300        {
1301            *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = i as i32;
1302            return;
1303        }
1304    }
1305    // c:716 — nothing eligible.
1306    *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = -1;
1307}
1308
1309// Make sure we have a suitable current and previous job set.               // c:2023
1310/// Direct port of `void setcurjob(void)` from `Src/jobs.c:2023`. Picks
1311/// the highest stopped job as `curjob`, falling back to any in-use
1312/// entry, then refreshes `prevjob` via `setprevjob`.
1313pub fn setcurjob() {                                                         // c:2023
1314    let tab = JOBTAB.get_or_init(|| Mutex::new(Vec::new()))
1315        .lock().expect("jobtab poisoned");
1316    let maxjob = *MAXJOB.get_or_init(|| Mutex::new(0))
1317        .lock().expect("maxjob poisoned");
1318    let mut found: i32 = -1;
1319    for i in (1..=maxjob).rev() {
1320        if i >= tab.len() { continue; }
1321        if (tab[i].stat & (stat::INUSE | stat::STOPPED))
1322            == (stat::INUSE | stat::STOPPED)
1323        {
1324            found = i as i32;
1325            break;
1326        }
1327    }
1328    if found < 0 {
1329        for i in (1..=maxjob).rev() {
1330            if i >= tab.len() { continue; }
1331            if (tab[i].stat & stat::INUSE) != 0 {
1332                found = i as i32;
1333                break;
1334            }
1335        }
1336    }
1337    *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = found;
1338    drop(tab);
1339    setprevjob();
1340}
1341
1342/// Check if a job's time should be reported (from jobs.c should_report_time)
1343/// Port of `should_report_time(Job j)` from `Src/jobs.c:1039`.
1344/// WARNING: param names don't match C — Rust=(job, reporttime) vs C=(j)
1345pub fn should_report_time(job: &Job, reporttime: f64) -> bool {
1346    if reporttime < 0.0 {
1347        return false;
1348    }
1349    if let Some(first) = job.procs.first() {
1350        if let (Some(start), Some(end)) =
1351            (first.bgtime, job.procs.last().and_then(|p| p.endtime))
1352        {
1353            let elapsed = end.duration_since(start).as_secs_f64();
1354            return elapsed >= reporttime;
1355        }
1356    }
1357    false
1358}
1359
1360/// Dump timing info for a job (from jobs.c dumptime)
1361/// Port of `dumptime(Job jn)` from `Src/jobs.c:1020`.
1362/// WARNING: param names don't match C — Rust=(job, format) vs C=(jn)
1363pub fn dumptime(job: &Job, format: &str) -> Option<String> {
1364    let first_start = job.procs.first()?.bgtime?;
1365    let last_end = job.procs.last()?.endtime?;
1366    let elapsed = last_end.duration_since(first_start).as_secs_f64();
1367
1368    let mut total_user = 0.0;
1369    let mut total_sys = 0.0;
1370    for proc in &job.procs {
1371        total_user += proc.ti.ut as f64 / 1_000_000.0;
1372        total_sys  += proc.ti.st as f64 / 1_000_000.0;
1373    }
1374
1375    Some(printtime(
1376        elapsed,
1377        total_user,
1378        total_sys,
1379        format,
1380        &if !job.text.is_empty() { job.text.clone() } else { job.procs.iter().map(|p| p.text.as_str()).collect::<Vec<_>>().join(" | ") },
1381    ))
1382}
1383
1384// wait for running job to finish                                           // c:1763
1385/// Wait for all foreground jobs to finish (from jobs.c waitjobs)
1386pub fn waitjobs(jobtab: &mut [Job], thisjob: usize) {                        // c:1763
1387    if thisjob < jobtab.len() {
1388        while !jobtab[thisjob].is_done() && !jobtab[thisjob].is_stopped() {
1389            #[cfg(unix)]
1390            {
1391                let mut status: i32 = 0;
1392                let pid = unsafe { libc::waitpid(-1, &mut status, libc::WUNTRACED) };
1393                if pid > 0 {
1394                    update_bg_job(jobtab, pid, status);
1395                } else {
1396                    break;
1397                }
1398            }
1399            #[cfg(not(unix))]
1400            {
1401                break;
1402            }
1403        }
1404    }
1405}
1406
1407/// Wait for a single specific job (from jobs.c waitonejob)
1408pub fn waitonejob(job: &mut Job) {
1409    for proc in &mut job.procs {
1410        if proc.is_running() {
1411            if let Some(_status) = waitforpid(proc.pid) {
1412                // status already updated by waitforpid
1413            }
1414        }
1415    }
1416}
1417
1418// Get a free entry in the job table and initialize it.                    // c:1862
1419/// Initialize a new job entry (from jobs.c initjob)
1420pub fn initjob(jobtab: &mut Vec<Job>) -> usize {                             // c:1862
1421    // Find an empty slot or add a new one
1422    for (i, job) in jobtab.iter().enumerate() {
1423        if (job.stat & stat::INUSE) == 0 {
1424            jobtab[i] = Job::new();
1425            jobtab[i].stat = stat::INUSE;
1426            return i;
1427        }
1428    }
1429    // Expand table
1430    let idx = jobtab.len();
1431    let mut job = Job::new();
1432    job.stat = stat::INUSE;
1433    jobtab.push(job);
1434    idx
1435}
1436
1437/// Set the pwd for a job (from jobs.c setjobpwd)
1438/// Port of `setjobpwd` from `Src/jobs.c:1881`.
1439pub fn setjobpwd(job: &mut Job) {
1440    // Store current directory in job for display purposes
1441    if let Ok(cwd) = std::env::current_dir() {
1442        // Job text sometimes includes the directory
1443        let _ = cwd;
1444    }
1445}
1446
1447/// Spawn a job (mark as started, from jobs.c spawnjob)
1448/// Port of `spawnjob` from `Src/jobs.c:1894`.
1449pub fn spawnjob(job: &mut Job, fg: bool) {                                  // c:1894
1450    job.stat |= stat::INUSE;
1451    if !fg {
1452        // Background job
1453        job.stat &= !stat::CURSH;
1454    }
1455}
1456
1457// Find the job table for reporting jobs                                   // c:2042
1458/// Port of `selectjobtab(Job *jtabp, int *jmaxp)` from `Src/jobs.c:2042`.
1459///
1460/// C signature: `mod_export void selectjobtab(Job *jtabp, int *jmaxp)`
1461///
1462/// In subshell, uses saved `oldjobtab`/`oldmaxjob`; otherwise uses
1463/// the main `jobtab`/`maxjob` globals. Returns `(table, maxjob)`.
1464/// WARNING: param names don't match C — Rust=() vs C=(jtabp, jmaxp)
1465pub fn selectjobtab() -> (Vec<Job>, usize) {
1466    let oldtab = OLDJOBTAB.get_or_init(|| Mutex::new(Vec::new()))
1467        .lock().expect("oldjobtab poisoned");
1468    if !oldtab.is_empty() {                                                  // c:2044
1469        // In subshell --- use saved job table to report                     // c:2046
1470        let oldmax = *OLDMAXJOB.get_or_init(|| Mutex::new(0))
1471            .lock().expect("oldmaxjob poisoned");
1472        (oldtab.clone(), oldmax)                                             // c:2047-2048
1473    } else {
1474        // Use main job table                                                // c:2052
1475        drop(oldtab); // release lock before acquiring jobtab
1476        let jobtab = JOBTAB.get_or_init(|| Mutex::new(Vec::new()))
1477            .lock().expect("jobtab poisoned");
1478        let maxjob = *MAXJOB.get_or_init(|| Mutex::new(0))
1479            .lock().expect("maxjob poisoned");
1480        (jobtab.clone(), maxjob)                                             // c:2053-2054
1481    }
1482}
1483
1484/// Port of `expandjobtab()` from `Src/jobs.c:2225`.
1485///
1486/// C body:
1487/// ```c
1488/// int newsize = jobtabsize + MAXJOBS_ALLOC;
1489/// if (newsize > MAX_MAXJOBS) return 0;
1490/// newjobtab = zrealloc(jobtab, newsize * sizeof(struct job));
1491/// if (!newjobtab) return 0;
1492/// memset(newjobtab + jobtabsize, 0, MAXJOBS_ALLOC * sizeof(struct job));
1493/// jobtab = newjobtab;
1494/// jobtabsize = newsize;
1495/// return 1;
1496/// ```
1497///
1498/// Grows the job table by `MAXJOBS_ALLOC` slots, respecting the
1499/// `MAX_MAXJOBS` cap. Returns true on success, false if the cap
1500/// would be exceeded. The previous Rust port grew the table
1501/// unconditionally without the cap, and used `<= needed` instead
1502/// of growing by full chunks.
1503pub fn expandjobtab(jobtab: &mut Vec<Job>, _needed: usize) -> bool {
1504    let newsize = jobtab.len() + MAXJOBS_ALLOC;
1505    if newsize > MAX_MAXJOBS {
1506        return false;
1507    }
1508    jobtab.resize_with(newsize, Job::new);
1509    true
1510}
1511
1512/// Shrink job table if possible (from jobs.c maybeshrinkjobtab)
1513/// Port of `maybeshrinkjobtab` from `Src/jobs.c:2259`.
1514pub fn maybeshrinkjobtab(jobtab: &mut Vec<Job>) {
1515    while jobtab
1516        .last()
1517        .map(|j| (j.stat & stat::INUSE) == 0)
1518        .unwrap_or(false)
1519    {
1520        jobtab.pop();
1521    }
1522}
1523
1524/// Port of `addfilelist(const char *name, int fd)` from `Src/jobs.c:1373`.
1525///
1526/// C body:
1527/// ```c
1528/// Jobfile jf = zalloc(sizeof(struct jobfile));
1529/// LinkList ll = jobtab[thisjob].filelist;
1530/// if (!ll) ll = jobtab[thisjob].filelist = znewlinklist();
1531/// if (name) { jf->u.name = ztrdup(name); jf->is_fd = 0; }
1532/// else      { jf->u.fd = fd;             jf->is_fd = 1; }
1533/// zaddlinknode(ll, jf);
1534/// ```
1535///
1536/// Stores either a temp-file name (to delete on job exit) or an
1537/// open fd (to close on job exit). C uses a `Jobfile` struct with
1538/// a tagged union; Rust port encodes the fd-only case as a
1539/// `<fd:N>` sentinel string in the `Vec<String>` since the Job
1540/// struct stores `filelist: Vec<String>` for now.
1541///
1542/// `name == None` → store `<fd:N>`; `name == Some(s)` → store `s`.
1543/// `deletefilelist` parses the `<fd:N>` prefix and calls `close(N)`
1544/// instead of `unlink`. WARNING: the `Vec<String>`+sentinel encoding
1545/// is a Rust port concession until `Jobfile` lands as a real type;
1546/// once it does, this fn becomes a direct push of the enum variant.
1547pub fn addfilelist(job: &mut Job, name: Option<&str>, fd: i32) {
1548    match name {
1549        Some(n) => job.filelist.push(n.to_string()),
1550        None => job.filelist.push(format!("<fd:{}>", fd)),
1551    }
1552}
1553
1554/// Port of `pipecleanfilelist(LinkList filelist, int proc_subst_only)` from `Src/jobs.c:1397`.
1555///
1556/// `<fd:N>` sentinels (added by `addfilelist(None, fd)`) are
1557/// kept in both branches — they're the input/output fds for
1558/// process substitution and need closing only at job exit.
1559pub fn pipecleanfilelist(filelist: &mut Job, proc_subst_only: bool) {            // c:1397
1560    if proc_subst_only {                                                     // c:1397
1561        filelist.filelist.retain(|f| {
1562            !f.starts_with("/dev/fd/")
1563                && !f.starts_with("/proc/")
1564                && !f.starts_with("<fd:")
1565        });
1566    } else {
1567        for entry in &filelist.filelist {
1568            // Inline: unlink or close based on entry encoding               // c:1408-1411
1569            if let Some(rest) = entry.strip_prefix("<fd:") {
1570                if let Some(num_str) = rest.strip_suffix('>') {
1571                    if let Ok(fd) = num_str.parse::<i32>() {
1572                        #[cfg(unix)]
1573                        unsafe { libc::close(fd); }                          // c:1411
1574                    }
1575                }
1576            } else {
1577                let _ = std::fs::remove_file(entry);                         // c:1409
1578            }
1579        }
1580        filelist.filelist.clear();
1581    }
1582}
1583
1584/// Port of `deletefilelist(LinkList file_list, int disowning)` from `Src/jobs.c:1422`.
1585///
1586/// C body iterates the filelist linked list; for each Jobfile,
1587/// dispatches `unlink(jf->u.name)` if `is_fd == 0` else
1588/// `close(jf->u.fd)`. The `disowning` flag suppresses the
1589/// `unlink`/`close` so files survive the disown.
1590pub fn deletefilelist(file_list: &mut Job, disowning: bool) {                      // c:1422
1591    if !disowning {                                                          // c:1422
1592        for entry in &file_list.filelist {
1593            // Inline: unlink or close based on entry encoding               // c:1427-1435
1594            if let Some(rest) = entry.strip_prefix("<fd:") {
1595                if let Some(num_str) = rest.strip_suffix('>') {
1596                    if let Ok(fd) = num_str.parse::<i32>() {
1597                        #[cfg(unix)]
1598                        unsafe { libc::close(fd); }                          // c:1434
1599                    }
1600                }
1601            } else {
1602                let _ = std::fs::remove_file(entry);                         // c:1432
1603            }
1604        }
1605    }
1606    file_list.filelist.clear();
1607}
1608
1609/// Print job with full detail (from jobs.c printjob)
1610// find length of longest signame, check to see                             // c:1178
1611// if we really need to print this job                                      // c:1179
1612pub fn printjob(
1613    job: &Job,
1614    job_num: usize,
1615    long_format: bool,
1616    cur_job: Option<usize>,
1617    prev_job: Option<usize>,
1618) -> String {
1619    // Inline process-status formatter — mirrors the inline status-decode
1620    // block at Src/jobs.c:1136-1400 inside printjob itself. SP_RUNNING
1621    // → "running"; WIFEXITED → "done" / "exit N"; WIFSTOPPED → "suspended
1622    // (sig)"; WIFSIGNALED → "sig" + " (core dumped)" if WCOREDUMP.
1623    let fmt_proc_status = |status: i32| -> String {
1624        if status == SP_RUNNING {
1625            "running".to_string()
1626        } else if (status & 0x7f) == 0 {
1627            let code = (status >> 8) & 0xff;
1628            if code == 0 {
1629                "done".to_string()
1630            } else {
1631                format!("exit {}", code)
1632            }
1633        } else if (status & 0xff) == 0x7f {
1634            let sig = (status >> 8) & 0xff;
1635            format!("suspended ({})", sigmsg(sig))
1636        } else {
1637            let sig = status & 0x7f;
1638            let core = (status >> 7) & 1;
1639            if core != 0 {
1640                format!("{} (core dumped)", sigmsg(sig))
1641            } else {
1642                sigmsg(sig).to_string()
1643            }
1644        }
1645    };
1646    let marker = if Some(job_num) == cur_job {
1647        '+'
1648    } else if Some(job_num) == prev_job {
1649        '-'
1650    } else {
1651        ' '
1652    };
1653
1654    let status_str = if job.is_done() {
1655        if let Some(last) = job.procs.last() {
1656            fmt_proc_status(last.status)
1657        } else {
1658            "done".to_string()
1659        }
1660    } else if job.is_stopped() {
1661        "suspended".to_string()
1662    } else {
1663        "running".to_string()
1664    };
1665
1666    if long_format {
1667        let mut lines = Vec::new();
1668        for (i, proc) in job.procs.iter().enumerate() {
1669            let pstatus = fmt_proc_status(proc.status);
1670            if i == 0 {
1671                lines.push(format!(
1672                    "[{}]  {} {:>5} {:16}  {}",
1673                    job_num, marker, proc.pid, pstatus, proc.text
1674                ));
1675            } else {
1676                lines.push(format!(
1677                    "            {:>5} {:16}  | {}",
1678                    proc.pid, pstatus, proc.text
1679                ));
1680            }
1681        }
1682        lines.join("\n")
1683    } else {
1684        format!(
1685            "[{}]  {} {:16}  {}",
1686            job_num,
1687            marker,
1688            status_str,
1689            if !job.text.is_empty() { job.text.clone() } else { job.procs.iter().map(|p| p.text.as_str()).collect::<Vec<_>>().join(" | ") }
1690        )
1691    }
1692}
1693
1694/// Get the signal name for signal-based job output (from jobs.c getsigname)
1695/// Port of `getsigname(int sig)` from `Src/jobs.c:3087`.
1696pub fn getsigname(sig: i32) -> String {
1697    match sig {
1698        0 => "EXIT".to_string(),
1699        libc::SIGHUP => "HUP".to_string(),
1700        libc::SIGINT => "INT".to_string(),
1701        libc::SIGQUIT => "QUIT".to_string(),
1702        libc::SIGILL => "ILL".to_string(),
1703        libc::SIGTRAP => "TRAP".to_string(),
1704        libc::SIGABRT => "ABRT".to_string(),
1705        libc::SIGBUS => "BUS".to_string(),
1706        libc::SIGFPE => "FPE".to_string(),
1707        libc::SIGKILL => "KILL".to_string(),
1708        libc::SIGUSR1 => "USR1".to_string(),
1709        libc::SIGSEGV => "SEGV".to_string(),
1710        libc::SIGUSR2 => "USR2".to_string(),
1711        libc::SIGPIPE => "PIPE".to_string(),
1712        libc::SIGALRM => "ALRM".to_string(),
1713        libc::SIGTERM => "TERM".to_string(),
1714        libc::SIGCHLD => "CHLD".to_string(),
1715        libc::SIGCONT => "CONT".to_string(),
1716        libc::SIGSTOP => "STOP".to_string(),
1717        libc::SIGTSTP => "TSTP".to_string(),
1718        libc::SIGTTIN => "TTIN".to_string(),
1719        libc::SIGTTOU => "TTOU".to_string(),
1720        libc::SIGURG => "URG".to_string(),
1721        libc::SIGXCPU => "XCPU".to_string(),
1722        libc::SIGXFSZ => "XFSZ".to_string(),
1723        libc::SIGVTALRM => "VTALRM".to_string(),
1724        libc::SIGPROF => "PROF".to_string(),
1725        libc::SIGWINCH => "WINCH".to_string(),
1726        libc::SIGIO => "IO".to_string(),
1727        libc::SIGSYS => "SYS".to_string(),
1728        _ => format!("SIG{}", sig),
1729    }
1730}
1731
1732/// Time difference for timeval (from jobs.c dtime_tv)
1733/// Port of `dtime_tv(struct timeval *dt, struct timeval *t1, struct timeval *t2)` from `Src/jobs.c:137`.
1734pub fn dtime_tv(dt: &mut Duration, t1: &Duration, t2: &Duration) -> Duration {
1735    if *t2 > *t1 {
1736        *dt = *t2 - *t1;
1737    } else {
1738        *dt = Duration::ZERO;
1739    }
1740    *dt
1741}
1742
1743/// Time difference for timespec (from jobs.c dtime_ts)
1744/// Port of `dtime_ts(struct timespec *dt, struct timespec *t1, struct timespec *t2)` from `Src/jobs.c:152`.
1745/// WARNING: param names don't match C — Rust=(t1, t2) vs C=(dt, t1, t2)
1746pub fn dtime_ts(t1: &Instant, t2: &Instant) -> Duration {
1747    if *t2 > *t1 {
1748        t2.duration_since(*t1)
1749    } else {
1750        Duration::ZERO
1751    }
1752}
1753
1754// change job table entry from stopped to running                           // c:163
1755/// Port of `makerunning(Job jn)` from `Src/jobs.c:167`.
1756///
1757/// C body:
1758/// ```c
1759/// jn->stat &= ~STAT_STOPPED;
1760/// for (pn = jn->procs; pn; pn = pn->next)
1761///     if (WIFSTOPPED(pn->status))
1762///         pn->status = SP_RUNNING;
1763/// if (jn->stat & STAT_SUPERJOB)
1764///     makerunning(jobtab + jn->other);
1765/// ```
1766///
1767/// Clears the STOPPED flag on the job, resets each stopped process
1768/// to SP_RUNNING, and recurses into the linked subjob if this is a
1769// change job table entry from stopped to running                           // c:167
1770/// superjob. The previous Rust port called `job.make_running()`
1771/// which mutates only the single Job — missing the superjob
1772/// recursion. This port walks the table to handle the recursion.
1773pub fn makerunning(jobtab: &mut [Job], idx: usize) {
1774    if idx >= jobtab.len() {
1775        return;
1776    }
1777    let other = jobtab[idx].other as usize;
1778    let is_super = (jobtab[idx].stat & stat::SUPERJOB) != 0;
1779    {
1780        let job = &mut jobtab[idx];
1781        job.stat &= !stat::STOPPED;
1782        for proc in &mut job.procs {
1783            if proc.is_stopped() {
1784                proc.status = SP_RUNNING;
1785            }
1786        }
1787    }
1788    if is_super && other != idx && other < jobtab.len() {
1789        makerunning(jobtab, other);
1790    }
1791}
1792
1793/// Port of `hasprocs(int job)` from `Src/jobs.c:243`.
1794///
1795/// C body:
1796/// ```c
1797/// Job jn;
1798/// if (job < 0) { DPUTS(1, "job number invalid"); return 0; }
1799/// jn = jobtab + job;
1800/// return jn->procs || jn->auxprocs;
1801/// ```
1802///
1803/// Takes the job index (not a `&Job`) because the C signature is
1804/// `int hasprocs(int job)`. Bounds-checks the index — out-of-range
1805/// returns false (matching C's negative-index DPUTS+0 path).
1806/// WARNING: param names don't match C — Rust=(jobtab, job) vs C=(job)
1807pub fn hasprocs(jobtab: &[Job], job: usize) -> bool {
1808    jobtab
1809        .get(job)
1810        .map(|j| !j.procs.is_empty() || !j.auxprocs.is_empty())
1811        .unwrap_or(false)
1812}
1813
1814/// Check current shell signals (from jobs.c check_cursh_sig)
1815#[cfg(unix)]
1816/// Port of `check_cursh_sig(int sig)` from `Src/jobs.c:397`.
1817/// WARNING: param names don't match C — Rust=(jobtab, sig) vs C=(sig)
1818pub fn check_cursh_sig(jobtab: &[Job], sig: i32) {
1819    for job in jobtab {
1820        if (job.stat & stat::CURSH) != 0 && !job.is_done() {
1821            for proc in &job.procs {
1822                if proc.is_running() {
1823                    unsafe {
1824                        libc::kill(proc.pid, sig);
1825                    }
1826                }
1827            }
1828        }
1829    }
1830}
1831
1832/// Port of `cleanfilelists()` from `Src/jobs.c:1443`.
1833///
1834/// C body:
1835/// ```c
1836/// DPUTS(shell_exiting >= 0, "BUG: cleanfilelists() before exit");
1837/// for (i = 1; i <= maxjob; i++) {
1838///     deletefilelist(jobtab[i].filelist, 0);
1839///     jobtab[i].filelist = 0;
1840/// }
1841/// ```
1842///
1843/// Deletes the file list (and its temp files) for every job in
1844/// the table. Called from the shell-exit path. The C source skips
1845/// index 0 (job 0 is unused / "the shell itself"); Rust port does
1846/// the same with `iter_mut().skip(1)`.
1847pub fn cleanfilelists(jobtab: &mut [Job]) {
1848    for job in jobtab.iter_mut().skip(1) {
1849        deletefilelist(job, false);
1850    }
1851}
1852
1853/// Port of `clearoldjobtab()` from `Src/jobs.c:1835`.
1854///
1855/// C body:
1856/// ```c
1857/// if (oldjobtab) free(oldjobtab);
1858/// oldjobtab = NULL;
1859/// oldmaxjob = 0;
1860/// ```
1861///
1862/// Frees the snapshot of the previous-state job table that
1863/// `jobs -c` (jobs-changed) compares against. The previous Rust
1864/// port retained INUSE entries in `jobtab` directly — wrong
1865/// target. The real C function operates on the `oldjobtab`
1866/// global, not the live `jobtab`.
1867///
1868/// Rust port clears the OLDJOBTAB module static.
1869pub fn clearoldjobtab() {
1870    *OLDJOBTAB.get_or_init(|| Mutex::new(Vec::new()))
1871        .lock().expect("oldjobtab poisoned") = Vec::new();
1872    *OLDMAXJOB.get_or_init(|| Mutex::new(0))
1873        .lock().expect("oldmaxjob poisoned") = 0;
1874}
1875
1876/// Direct port of `void addbgstatus(pid_t pid, int status)` from
1877/// `Src/jobs.c:2325`. Caps the global `bgstatus_list` at
1878/// `_SC_CHILD_MAX`, evicting oldest on overflow, then appends a
1879/// new `bgstatus { pid, status }` entry.
1880pub fn addbgstatus(pid: i32, status_val: i32) {                              // c:2325
1881    // c:2370 — `if (bgstatus_count == max_child)` cap + eviction.
1882    let max_child = unsafe { libc::sysconf(libc::_SC_CHILD_MAX) };
1883    let cap = if max_child > 0 { max_child as i64 } else { 1024 };
1884    if let Ok(mut list) = bgstatus_list.lock() {
1885        if bgstatus_count.load(Ordering::Relaxed) >= cap {                   // c:2370
1886            // c:2371 — `rembgstatus(firstnode(bgstatus_list))`.
1887            if !list.is_empty() {
1888                list.remove(0);
1889                bgstatus_count.fetch_sub(1, Ordering::Relaxed);
1890            }
1891        }
1892        // c:2376-2385 — alloc + push.
1893        list.push(bgstatus { pid, status: status_val });                     // c:2381-2384
1894        bgstatus_count.fetch_add(1, Ordering::Relaxed);                      // c:2386
1895    }
1896}
1897
1898// See if pid has a recorded exit status.                                   // c:2397
1899// Note we make no guarantee that the PIDs haven't wrapped, so this         // c:2397
1900// may not be the right process.                                            // c:2397
1901//                                                                          // c:2397
1902// This is only used by wait, which must only work on each                  // c:2397
1903// pid once, so we need to remove the entry if we find it.                  // c:2397
1904/// Direct port of `int getbgstatus(pid_t pid)` from `Src/jobs.c:2397`.
1905/// Walks the global `bgstatus_list` for `pid`; if found, removes
1906/// the entry and returns its status.
1907pub fn getbgstatus(pid: i32) -> Option<i32> {                                // c:2397
1908    if let Ok(mut list) = bgstatus_list.lock() {
1909        if let Some(idx) = list.iter().position(|b| b.pid == pid) {          // c:2402-2406
1910            let status = list[idx].status;
1911            list.remove(idx);                                                // c:2407 rembgstatus
1912            bgstatus_count.fetch_sub(1, Ordering::Relaxed);
1913            return Some(status);
1914        }
1915    }
1916    None
1917}
1918
1919/// Port of `gettrapnode(int sig, int ignoredisable)` from `Src/jobs.c:3115`.
1920///
1921/// C body looks up `TRAP<signame>` in the `shfunctab` (shell-
1922/// function hashtable) using either `getnode` (skip disabled) or
1923/// `getnode2` (include disabled), depending on `ignoredisable`.
1924/// Falls back to `alt_sigs[]` aliases (e.g. `TRAPCLD` for
1925/// SIGCHLD) when the canonical `TRAP<getsigname(sig)>` form
1926/// isn't found.
1927///
1928/// Now that `hashtable::shfunctab_lock` exists, the lookup is
1929/// real. Returns the function body for the trap if defined.
1930/// `ignoredisable` mirrors C: when 1, returns disabled entries
1931/// too (used by `unsetfn` paths that need to remove disabled
1932/// traps).
1933/// WARNING: param names don't match C — Rust=(sig) vs C=(sig, ignoredisable)
1934pub fn gettrapnode(sig: i32) -> Option<String> {
1935    let name = format!("TRAP{}", getsigname(sig));
1936    let tab = crate::ported::hashtable::shfunctab_lock()
1937        .read()
1938        .expect("shfunctab poisoned");
1939    tab.get_including_disabled(&name)
1940        .and_then(|f| f.body.clone())
1941}
1942
1943/// Port of `removetrapnode(int sig)` from `Src/jobs.c:3157`.
1944///
1945/// C body:
1946/// ```c
1947/// HashNode hn = gettrapnode(sig, 1);
1948/// if (hn) { shfunctab->removenode(shfunctab, hn->nam); shfunctab->freenode(hn); }
1949/// ```
1950///
1951/// Routes through `hashtable::removeshfuncnode` which itself
1952/// dispatches the trap-removal logic for `TRAP<sig>` names.
1953pub fn removetrapnode(sig: i32) {
1954    let name = format!("TRAP{}", getsigname(sig));
1955    crate::ported::hashtable::removeshfuncnode(&name);
1956}
1957
1958/// Port of `release_pgrp()` from `Src/jobs.c:3283`.
1959///
1960/// C body:
1961/// ```c
1962/// if (origpgrp != mypgrp) {
1963///     if (origpgrp) {
1964///         attachtty(origpgrp);
1965///         setpgrp(0, origpgrp);
1966///     }
1967///     mypgrp = origpgrp;
1968/// }
1969/// ```
1970///
1971///
1972/// Restores the original (parent shell's) process group before
1973/// the current shell exits, so terminal control returns to the
1974/// invoker.
1975#[cfg(unix)]
1976pub fn release_pgrp() {                                                      // c:3283
1977    let origpgrp = *ORIGPGRP.get_or_init(|| Mutex::new(0))
1978        .lock().expect("origpgrp poisoned");
1979    let mypgrp = *MYPGRP.get_or_init(|| Mutex::new(0))
1980        .lock().expect("mypgrp poisoned");
1981    if origpgrp != mypgrp {                                                  // c:3285
1982        // in linux pid namespaces, origpgrp may never have been set         // c:3286
1983        if origpgrp != 0 {                                                   // c:3287
1984            unsafe {
1985                // attachtty(origpgrp);                                      // c:3288
1986                libc::tcsetpgrp(0, origpgrp);
1987                libc::setpgid(0, origpgrp);                                  // c:3289
1988            }
1989        }
1990        *MYPGRP.get_or_init(|| Mutex::new(0))                                // c:3291
1991            .lock().expect("mypgrp poisoned") = origpgrp;
1992    }
1993}
1994
1995/// Direct port of `bin_fg(char *name, char **argv, Options ops, int func)` from `Src/jobs.c:2421`.
1996/// Multi-builtin dispatcher — handles bg, fg, wait, jobs, disown, and
1997/// the `-Z` process-rename form. C body is 315 lines (c:2421-2735);
1998/// the per-builtin behaviour is selected by `func` (BIN_BG/BIN_FG/
1999/// BIN_JOBS/BIN_WAIT/BIN_DISOWN).
2000///
2001/// Coverage status:
2002///   ✓ -Z process-title rename (c:2425-2451) — full port via
2003///     libc::prctl(PR_SET_NAME) on Linux; macOS pthread_setname_np;
2004///     other platforms emit a warning
2005///   ✓ no-job-control refusal for fg/bg under !jobbing (c:2461-2465)
2006///   ✓ jobs -l/-p/-d listing-format selection (c:2454-2459)
2007///   ⚠ jobspec parsing + per-job dispatch (c:2467-2733) DEFERRED —
2008///     depends on getjob (parses %N/%?str specifiers), the global
2009///     jobtab + oldjobtab, deletejob/printjob/makerunning, lastval2,
2010///     errflag, signal queueing for fg's tcsetpgrp dance, and the
2011///     STAT_* / STAT_SUPERJOB / STAT_DISOWN flag tracking. None of
2012///     those are fully ported yet; structural shape preserved so the
2013///     C signature lands and future port work can fill the body.
2014pub fn bin_fg(name: &str, argv: &[String],                                   // c:2421
2015              ops: &crate::ported::zsh_h::options, func: i32) -> i32 {
2016    let _ofunc = func;                                                       // c:2424
2017
2018    // c:2425-2452 — `-Z`: rename the running process. Used by
2019    // login shells / tools that want their `ps` line to reflect a
2020    // descriptive title rather than `zsh`.
2021    if OPT_ISSET(ops, b'Z') {                                                // c:2425
2022        if argv.is_empty() || argv.len() > 1 {                               // c:2428
2023            zwarnnam(name, "-Z requires one argument");                      // c:2429
2024            return 1;                                                        // c:2430
2025        }
2026        crate::ported::mem::queue_signals();                                 // c:2433
2027        let title = &argv[0];
2028        // c:2436 — `setproctitle("%s", *argv);` if available.
2029        // c:2438-2444 — fallback: memcpy into hackzero (the argv[0]
2030        // buffer reserved by the loader). Not portable from Rust,
2031        // so the prctl path covers Linux directly.
2032        #[cfg(target_os = "linux")]
2033        unsafe {
2034            let cs = std::ffi::CString::new(title.as_str()).unwrap_or_default();
2035            // PR_SET_NAME = 15; libc may not expose it — pass the
2036            // raw constant per `linux/prctl.h`.
2037            libc::prctl(15 /*PR_SET_NAME*/, cs.as_ptr() as libc::c_ulong, 0, 0, 0); // c:2447
2038        }
2039        #[cfg(target_os = "macos")]
2040        unsafe {
2041            extern "C" {
2042                fn pthread_setname_np(name: *const libc::c_char) -> libc::c_int;
2043            }
2044            let cs = std::ffi::CString::new(title.as_str()).unwrap_or_default();
2045            pthread_setname_np(cs.as_ptr());
2046        }
2047        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
2048        {
2049            let _ = title;
2050        }
2051        crate::ported::mem::unqueue_signals();                               // c:2449
2052        return 0;                                                            // c:2450
2053    }
2054
2055    // c:2454-2459 — jobs builtin: pick listing format.
2056    let mut lng = 0i32;                                                      // c:2422
2057    if func == BIN_JOBS {                                                    // c:2454
2058        lng = if OPT_ISSET(ops, b'l') { 1 }                                  // c:2455
2059              else if OPT_ISSET(ops, b'p') { 2 } else { 0 };
2060        if OPT_ISSET(ops, b'd') { lng |= 4; }                                // c:2456
2061    } else {
2062        // c:2458 — `lng = !!isset(LONGLISTJOBS);`
2063        lng = if crate::ported::zsh_h::isset(crate::ported::zsh_h::LONGLISTJOBS) {
2064            1
2065        } else {
2066            0
2067        };
2068    }
2069    let _ = lng;
2070
2071    // c:2461-2465 — fg/bg need job control.
2072    let jobbing = crate::ported::zsh_h::isset(crate::ported::zsh_h::MONITOR);
2073    if (func == BIN_FG || func == BIN_BG) && !jobbing {                      // c:2461
2074        zwarnnam(name, "no job control in this shell.");                     // c:2463
2075        return 1;                                                            // c:2464
2076    }
2077
2078    // c:2467 — `queue_signals();`
2079    crate::ported::signals::queue_signals();
2080    // c:2474 — `wait_for_processes();` reap any newly-finished children
2081    // so the table reflects the current state before we list/dispatch.
2082    crate::ported::signals::wait_for_processes();
2083
2084    // c:2477-2478 — `if (unset(NOTIFY)) scanjobs();`
2085    // (scanjobs walks the table marking finished jobs for printing).
2086    // Skipped: scanjobs port isn't surfaced as a free fn; consumers
2087    // that need the print-on-prompt notify will route through it.
2088
2089    // c:2480-2481 — refresh CURJOB unless we're listing a frozen
2090    // oldjobtab snapshot from `jobs` in a non-monitor shell.
2091    let table = JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
2092    if func != BIN_JOBS || jobbing
2093        || *OLDMAXJOB.get_or_init(|| Mutex::new(0)).lock().unwrap() == 0
2094    {
2095        // c:2481 — `setcurjob()` operates on the global jobtab.
2096        setcurjob();
2097    }
2098
2099    // c:2483-2486 — set stopmsg=2 so zexit doesn't complain about
2100    // stopped jobs if the user immediately runs `exit` after `jobs`.
2101    if func == BIN_JOBS {
2102        crate::ported::builtin::STOPMSG
2103            .store(2, std::sync::atomic::Ordering::Relaxed);                 // c:2486
2104    }
2105
2106    let mut returnval: i32 = 0;
2107
2108    if argv.is_empty() {                                                     // c:2487
2109        if func == BIN_JOBS {
2110            // c:2500-2523 — list jobs.
2111            let curjob = *CURJOB.get_or_init(|| Mutex::new(-1))
2112                .lock().unwrap();
2113            let t = table.lock().expect("jobtab poisoned");
2114            let curmaxjob = t.len();
2115            let r_only = OPT_ISSET(ops, b'r');
2116            let s_only = OPT_ISSET(ops, b's');
2117            for job in 0..curmaxjob {                                        // c:2513
2118                if job as i32 == curjob {                                    // c:2514 ignorejob
2119                    continue;
2120                }
2121                let j = &t[job];
2122                if !j.is_inuse() {                                           // c:2514 stat
2123                    continue;
2124                }
2125                let stopped = j.is_stopped();
2126                // c:2515-2519 — flag filtering.
2127                if (!r_only && !s_only)
2128                    || (r_only && s_only)
2129                    || (r_only && !stopped)
2130                    || (s_only && stopped)
2131                {
2132                    // c:2520 — printjob(jobptr, lng, 2). The Rust
2133                    // port's printjob takes job_num + cur/prev for
2134                    // formatting; pass them through here.
2135                    let curjob_opt = if curjob >= 0 {
2136                        Some(curjob as usize)
2137                    } else {
2138                        None
2139                    };
2140                    let prevjob = *PREVJOB
2141                        .get_or_init(|| Mutex::new(-1)).lock().unwrap();
2142                    let prevjob_opt = if prevjob >= 0 {
2143                        Some(prevjob as usize)
2144                    } else {
2145                        None
2146                    };
2147                    print!("{}", printjob(j, job, (lng & 1) != 0,
2148                        curjob_opt, prevjob_opt));
2149                }
2150            }
2151            crate::ported::signals::unqueue_signals();                       // c:2522
2152            return 0;                                                        // c:2523
2153        }
2154        if func == BIN_FG || func == BIN_BG {
2155            // c:2491-2499 — "no current job" gate.
2156            let curjob = *CURJOB.get_or_init(|| Mutex::new(-1))
2157                .lock().unwrap();
2158            if curjob < 0 {
2159                zwarnnam(name, "no current job");                            // c:2495
2160                crate::ported::signals::unqueue_signals();
2161                return 1;                                                    // c:2497
2162            }
2163            // Continue current job by sending SIGCONT to its pgrp.
2164            let gleader = table.lock().expect("jobtab poisoned")
2165                .get(curjob as usize).map(|j| j.gleader).unwrap_or(0);
2166            if gleader > 0 {
2167                let _ = crate::ported::signals::killjb(gleader, libc::SIGCONT);
2168            }
2169            crate::ported::signals::unqueue_signals();
2170            return 0;
2171        }
2172        crate::ported::signals::unqueue_signals();
2173        return 0;
2174    }
2175
2176    // c:2537+ — per-arg jobspec dispatch (full body handles wait pid,
2177    // STAT_SUPERJOB carry-through, killjb retry, etc.). Port the
2178    // common path: `%jobspec` → getjob → continue/restart.
2179    for arg in argv {
2180        let p = if arg.starts_with('%') {
2181            getjob(arg, name)                                                // c:2576 getjob
2182        } else if let Ok(n) = arg.parse::<i32>() {
2183            // jobs/fg numeric → treat as job index, not pid.
2184            if n >= 0 { n } else { -1 }
2185        } else {
2186            zwarnnam(name, &format!("{}: no such job", arg));
2187            returnval = 1;
2188            continue;
2189        };
2190        if p < 0 {
2191            returnval = 1;
2192            continue;
2193        }
2194        let gleader = table.lock().expect("jobtab poisoned")
2195            .get(p as usize).map(|j| j.gleader).unwrap_or(0);
2196        if func == BIN_FG || func == BIN_BG {
2197            if gleader > 0 {
2198                if crate::ported::signals::killjb(gleader, libc::SIGCONT) == -1 {
2199                    zwarnnam(name, &format!("{}: kill failed: {}", arg,
2200                        std::io::Error::last_os_error()));
2201                    returnval = 1;
2202                }
2203            }
2204        } else if func == BIN_JOBS {
2205            let t = table.lock().expect("jobtab poisoned");
2206            if let Some(j) = t.get(p as usize) {
2207                let curjob = *CURJOB.get_or_init(|| Mutex::new(-1))
2208                    .lock().unwrap();
2209                let prevjob = *PREVJOB.get_or_init(|| Mutex::new(-1))
2210                    .lock().unwrap();
2211                print!("{}", printjob(j, p as usize, (lng & 1) != 0,
2212                    if curjob >= 0 { Some(curjob as usize) } else { None },
2213                    if prevjob >= 0 { Some(prevjob as usize) } else { None }));
2214            }
2215        }
2216    }
2217    crate::ported::signals::unqueue_signals();                               // c:2729
2218    returnval                                                                // c:2734 retval
2219}
2220
2221/// Direct port of `bin_kill(char *nam, char **argv, UNUSED(Options ops), UNUSED(int func))` from `Src/jobs.c:2772`.
2222/// Builtin entry for the `kill` command. Parses signal specifiers
2223/// (`-N` numeric, `-s NAME` symbolic, `-l` list-by-number,
2224/// `-L` tabular listing, `-n N` numeric explicit, `-q` sigqueue
2225/// rt-signal sival) then sends the chosen signal to each remaining
2226/// argv (PIDs or %jobspecs).
2227/// WARNING: param names don't match C — Rust=(nam, argv, _func) vs C=(nam, argv, ops, func)
2228pub fn bin_kill(nam: &str, argv: &[String],                                  // c:2772
2229                _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
2230    let mut sig: i32 = libc::SIGTERM;                                        // c:2774
2231    let mut returnval: i32 = 0;                                              // c:2775
2232    let mut got_sig = false;                                                 // c:2780
2233    let mut idx = 0usize;
2234
2235    // c:2782 — `while (*argv && **argv == '-')` flag-parse loop.
2236    while idx < argv.len() && argv[idx].starts_with('-') {
2237        let arg = argv[idx].clone();
2238        let body = &arg[1..];
2239
2240        // c:2814 — `else if ((*argv)[1] != '-' || (*argv)[2])` —
2241        // pseudo `--` end-of-flags.
2242        if body == "-" {                                                     // c:2814 / c:3010
2243            idx += 1;
2244            break;
2245        }
2246
2247        if got_sig {                                                         // c:2811
2248            break;                                                           // c:2812
2249        }
2250
2251        // c:2815 — `if (idigit((*argv)[1]))` — numeric signal `-N`.
2252        if body.chars().next().is_some_and(|c| c.is_ascii_digit()) {         // c:2815
2253            match body.parse::<i32>() {
2254                Ok(n) => sig = n,                                            // c:2818
2255                Err(_) => {
2256                    zwarnnam(nam, &format!("invalid signal number: -{}", body));
2257                    return 1;                                                // c:2822
2258                }
2259            }
2260            got_sig = true;
2261            idx += 1;
2262            continue;
2263        }
2264
2265        // c:2818 — `-l` signal-name listing.
2266        if body == "l" {                                                     // c:2818
2267            idx += 1;
2268            if idx < argv.len() {                                            // c:2819
2269                // c:2820-2868 — per-arg lookup: numeric → name; name → number.
2270                while idx < argv.len() {
2271                    let token = &argv[idx];
2272                    idx += 1;
2273                    if let Ok(n) = token.parse::<i32>() {                    // c:2821 numeric
2274                        let s = (n & !0o200) as i32;                         // c:2855
2275                        if let Some(name) = crate::ported::signals_h::sigs_name(s) {                 // c:2856-2858
2276                            println!("{}", name);
2277                        } else {
2278                            println!("{}", n);                               // c:2862
2279                        }
2280                    } else {
2281                        // c:2823 — symbolic; uppercase, strip SIG, look up.
2282                        let upper = token.to_ascii_uppercase();
2283                        let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
2284                        if let Some(n) = crate::ported::signals_h::sigs_number(bare) {               // c:2828
2285                            println!("{}", n);                               // c:2842
2286                        } else {
2287                            zwarnnam(nam,
2288                                &format!("unknown signal: SIG{}", bare));    // c:2845
2289                            returnval += 1;
2290                        }
2291                    }
2292                }
2293                return returnval;                                            // c:2868
2294            }
2295            // c:2869-2876 — bare `-l`: print every signal name.
2296            print!("{}", crate::ported::signals_h::sigs_name(1).unwrap_or("HUP"));
2297            for s in 2..=crate::ported::signals_h::SIGCOUNT {
2298                if let Some(n) = crate::ported::signals_h::sigs_name(s) { print!(" {}", n); }
2299            }
2300            println!();
2301            return 0;                                                        // c:2879
2302        }
2303
2304        // c:2880 — `-L` tabular listing.
2305        if body == "L" {                                                     // c:2880
2306            let cols = 4usize;
2307            let mut col = 0usize;
2308            for s in 1..=crate::ported::signals_h::SIGCOUNT {
2309                if let Some(n) = crate::ported::signals_h::sigs_name(s) {
2310                    print!("{:>2} {:<10}", s, n);
2311                    col += 1;
2312                    if col % cols == 0 { println!(); }
2313                    else { print!(" "); }
2314                }
2315            }
2316            if col % cols != 0 { println!(); }
2317            return 0;                                                        // c:2911
2318        }
2319
2320        // c:2913 — `-n N` numeric signal (explicit).
2321        if body == "n" {                                                     // c:2913
2322            idx += 1;
2323            if idx >= argv.len() {                                           // c:2916
2324                zwarnnam(nam, "-n: argument expected");                      // c:2917
2325                return 1;                                                    // c:2918
2326            }
2327            match argv[idx].parse::<i32>() {                                 // c:2920
2328                Ok(n) => { sig = n; }
2329                Err(_) => {
2330                    zwarnnam(nam,
2331                        &format!("invalid signal number: {}", argv[idx]));   // c:2923
2332                    return 1;
2333                }
2334            }
2335            got_sig = true;
2336            idx += 1;
2337            continue;
2338        }
2339
2340        // c:2935 — `-s NAME` symbolic signal.
2341        if body == "s" {                                                     // c:2935
2342            idx += 1;
2343            if idx >= argv.len() {                                           // c:2938
2344                zwarnnam(nam, "-s: argument expected");                      // c:2939
2345                return 1;
2346            }
2347            let name = argv[idx].as_str();
2348            let upper = name.to_ascii_uppercase();
2349            let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
2350            match crate::ported::signals_h::sigs_number(bare) {
2351                Some(n) => sig = n,
2352                None => {
2353                    zwarnnam(nam,
2354                        &format!("unknown signal: SIG{}", bare));            // c:2944
2355                    return 1;
2356                }
2357            }
2358            got_sig = true;
2359            idx += 1;
2360            continue;
2361        }
2362
2363        // c:2782 — `-q VALUE` sigqueue path. zshrs treats it as
2364        // "consume the value, then continue parsing"; the actual
2365        // sival_int payload is dropped (not wired to a real
2366        // sigqueue(2) call yet — Linux-only, niche).
2367        if body == "q" {                                                     // c:2782
2368            idx += 1;
2369            if idx >= argv.len() {                                           // c:2785
2370                zwarnnam(nam, "-q: argument expected");                      // c:2786
2371                return 1;
2372            }
2373            if argv[idx].parse::<i32>().is_err() {                           // c:2796
2374                zwarnnam(nam,
2375                    &format!("invalid number: {}", argv[idx]));              // c:2797
2376                return 1;
2377            }
2378            idx += 1;                                                        // c:2802
2379            continue;                                                        // c:2803
2380        }
2381
2382        // c:2960 — symbolic `-NAME` (no `s` prefix needed).
2383        let upper = body.to_ascii_uppercase();
2384        let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
2385        match crate::ported::signals_h::sigs_number(bare) {
2386            Some(n) => { sig = n; got_sig = true; idx += 1; }
2387            None => {
2388                zwarnnam(nam, &format!("unknown signal: SIG{}", bare));      // c:2974
2389                return 1;
2390            }
2391        }
2392    }
2393
2394    // c:3010 — no PID/jobspec arguments?
2395    if idx >= argv.len() {                                                   // c:3010
2396        zwarnnam(nam, "not enough arguments");                               // c:3011
2397        return 1;
2398    }
2399
2400    // c:3015-3045 — for each remaining argv, parse PID or %jobspec
2401    // and send `sig`. zshrs handles bare numeric PIDs + simple
2402    // %jobspec via getjob; PIDs with leading `-` (process-group)
2403    // are forwarded via killpg.
2404    for arg in &argv[idx..] {
2405        if let Some(num) = arg.strip_prefix('-') {                           // c:3030
2406            // Process-group kill: `-PID` → killpg(PID, sig).
2407            match num.parse::<i32>() {
2408                Ok(pgid) => {
2409                    let r = unsafe { libc::killpg(pgid, sig) };              // c:3032
2410                    if r != 0 {
2411                        zwarnnam(nam, &format!("kill {}: {}", arg,
2412                            std::io::Error::last_os_error()));
2413                        returnval = 1;
2414                    }
2415                }
2416                Err(_) => {
2417                    zwarnnam(nam, &format!("illegal pid: {}", arg));
2418                    returnval = 1;
2419                }
2420            }
2421        } else if arg.starts_with('%') {                                     // c:2985 jobspec
2422            // c:2989 — `if ((p = getjob(*argv, nam)) == -1)`.
2423            let p = crate::ported::jobs::getjob(arg, nam);
2424            if p < 0 {                                                       // c:2989
2425                returnval += 1;                                              // c:2990
2426                continue;
2427            }
2428            // c:2993 — `killjb(jobtab + p, sig)`. Look up the job's
2429            // process-group leader and send via killjb.
2430            let gleader = JOBTAB.get_or_init(|| Mutex::new(Vec::new()))
2431                .lock().expect("jobtab poisoned")
2432                .get(p as usize).map(|j| j.gleader).unwrap_or(0);
2433            if crate::ported::signals::killjb(gleader, sig) == -1 {          // c:2993
2434                zwarnnam("kill", &format!("kill {} failed: {}", arg,         // c:2994
2435                    std::io::Error::last_os_error()));
2436                returnval += 1;                                              // c:2995
2437                continue;
2438            }
2439            // c:3001-3010 — if stopped + non-stopping signal,
2440            // SIGCONT after to wake the job so it processes `sig`.
2441            let stopped = JOBTAB.get_or_init(|| Mutex::new(Vec::new()))
2442                .lock().expect("jobtab poisoned")
2443                .get(p as usize).map(|j| j.is_stopped()).unwrap_or(false);
2444            if stopped
2445                && sig != libc::SIGKILL && sig != libc::SIGCONT
2446                && sig != libc::SIGTSTP && sig != libc::SIGTTOU
2447                && sig != libc::SIGTTIN && sig != libc::SIGSTOP
2448            {
2449                let _ = crate::ported::signals::killjb(gleader, libc::SIGCONT); // c:3009
2450            }
2451        } else {
2452            match arg.parse::<i32>() {                                       // c:3024 PID
2453                Ok(pid) => {
2454                    let r = unsafe { libc::kill(pid, sig) };                 // c:3025
2455                    if r != 0 {
2456                        zwarnnam(nam, &format!("kill {}: {}", arg,
2457                            std::io::Error::last_os_error()));               // c:3027
2458                        returnval = 1;
2459                    }
2460                }
2461                Err(_) => {
2462                    zwarnnam(nam, &format!("illegal pid: {}", arg));
2463                    returnval = 1;
2464                }
2465            }
2466        }
2467    }
2468    returnval                                                                // c:3045
2469}
2470
2471/// Direct port of `bin_suspend(char *name, UNUSED(char **argv), Options ops, UNUSED(int func))` from `Src/jobs.c:3170`.
2472/// C body (c:3173-3197):
2473/// ```c
2474/// if (islogin && !OPT_ISSET(ops,'f')) { error; return 1; }
2475/// if (jobbing) { signal_default(SIGTTIN/TSTP/TTOU); release_pgrp(); }
2476/// killpg(origpgrp, SIGTSTP);
2477/// if (jobbing) { acquire_pgrp(); signal_ignore(SIGTTOU/TSTP/TTIN); }
2478/// return 0;
2479/// ```
2480/// WARNING: param names don't match C — Rust=(name, _argv, _func) vs C=(name, argv, ops, func)
2481pub fn bin_suspend(name: &str, _argv: &[String],                             // c:3170
2482                   ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
2483
2484    // c:3173 — `if (islogin && !OPT_ISSET(ops,'f'))`. C reads the
2485    //          `islogin` global, set when zsh's `argv[0]` started with
2486    //          `-`. Probe `$0` via paramtab (was reading the OS env,
2487    //          which never carries a literal `$0`).
2488    let islogin = crate::ported::params::getsparam("0")
2489        .map(|s| s.starts_with('-'))
2490        .unwrap_or(false);
2491    //won't suspend a login shell, unless forced
2492    if islogin && !OPT_ISSET(ops, b'f') {                                    // c:3173
2493        zwarnnam(name, "can't suspend login shell");                         // c:3174
2494        return 1;                                                            // c:3175
2495    }
2496    // c:3177 — `if (jobbing)`. jobbing is the job-control-enabled flag;
2497    // tracks the MONITOR option.
2498    let jobbing = crate::ported::zsh_h::isset(crate::ported::zsh_h::MONITOR);
2499
2500    if jobbing {                                                             // c:3177
2501        //stop ignoring signals
2502        signal_default(libc::SIGTTIN);                                       // c:3179
2503        signal_default(libc::SIGTSTP);                                       // c:3180
2504        signal_default(libc::SIGTTOU);                                       // c:3181
2505        //Move ourselves back to the process group we came from
2506        release_pgrp();                                                      // c:3184
2507    }
2508
2509    // suspend ourselves with a SIGTSTP                                      // c:3187
2510    let origpgrp = ORIGPGRP.get_or_init(|| Mutex::new(0))
2511        .lock().map(|g| *g).unwrap_or(0);
2512    unsafe { libc::killpg(origpgrp, libc::SIGTSTP); }                        // c:3188
2513
2514    if jobbing {                                                             // c:3190
2515        let _ = acquire_pgrp();                                              // c:3191
2516        //restore signal handling
2517        signal_ignore(libc::SIGTTOU);                                        // c:3193
2518        signal_ignore(libc::SIGTSTP);                                        // c:3194
2519        signal_ignore(libc::SIGTTIN);                                        // c:3195
2520    }
2521    0                                                                        // c:3197
2522}
2523
2524/// Signal number from name (from jobs.c getsigidx)
2525/// Port of `getsigidx(const char *s)` from `Src/jobs.c:3047`.
2526pub fn getsigidx(s: &str) -> Option<i32> {
2527    let s = s.strip_prefix("SIG").unwrap_or(s);
2528    match s.to_uppercase().as_str() {
2529        "EXIT" => Some(0),
2530        "HUP" => Some(libc::SIGHUP),
2531        "INT" => Some(libc::SIGINT),
2532        "QUIT" => Some(libc::SIGQUIT),
2533        "ILL" => Some(libc::SIGILL),
2534        "TRAP" => Some(libc::SIGTRAP),
2535        "ABRT" | "IOT" => Some(libc::SIGABRT),
2536        "BUS" => Some(libc::SIGBUS),
2537        "FPE" => Some(libc::SIGFPE),
2538        "KILL" => Some(libc::SIGKILL),
2539        "USR1" => Some(libc::SIGUSR1),
2540        "SEGV" => Some(libc::SIGSEGV),
2541        "USR2" => Some(libc::SIGUSR2),
2542        "PIPE" => Some(libc::SIGPIPE),
2543        "ALRM" => Some(libc::SIGALRM),
2544        "TERM" => Some(libc::SIGTERM),
2545        "CHLD" | "CLD" => Some(libc::SIGCHLD),
2546        "CONT" => Some(libc::SIGCONT),
2547        "STOP" => Some(libc::SIGSTOP),
2548        "TSTP" => Some(libc::SIGTSTP),
2549        "TTIN" => Some(libc::SIGTTIN),
2550        "TTOU" => Some(libc::SIGTTOU),
2551        "URG" => Some(libc::SIGURG),
2552        "XCPU" => Some(libc::SIGXCPU),
2553        "XFSZ" => Some(libc::SIGXFSZ),
2554        "VTALRM" => Some(libc::SIGVTALRM),
2555        "PROF" => Some(libc::SIGPROF),
2556        "WINCH" => Some(libc::SIGWINCH),
2557        "IO" | "POLL" => Some(libc::SIGIO),
2558        "SYS" => Some(libc::SIGSYS),
2559        _ => s.parse().ok(),
2560    }
2561}
2562
2563// ===========================================================
2564// Methods moved verbatim from src/ported/exec.rs because their
2565// C counterpart's source file maps 1:1 to this Rust module.
2566// Rust permits multiple inherent impl blocks for the same
2567// type within a crate, so call sites in exec.rs are unchanged.
2568// ===========================================================
2569
2570// BEGIN moved-from-exec-rs
2571// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
2572
2573// END moved-from-exec-rs