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 crate::exec_jobs::JobTable;
19use crate::ported::builtin::{SHELL_EXITING, STOPMSG};
20use crate::ported::builtins::sched::zleactive;
21use crate::ported::hashtable_h::{BIN_BG, BIN_DISOWN, BIN_FG, BIN_JOBS, BIN_WAIT};
22use crate::ported::options::opt_state_set;
23use crate::ported::params::{getsparam, setsparam, unsetparam};
24use crate::ported::signals::{
25 killjb, queue_signals, signal_block, signal_setmask, unqueue_signals, wait_for_processes,
26};
27use crate::ported::signals_h::{signal_default, signal_ignore, sigs_name, sigs_number};
28use crate::ported::utils::zwarnnam;
29use crate::ported::utils::{fdtable_get, zclose};
30use crate::ported::zsh_h::{
31 isset, job, jobfile, options, process, FDT_PROC_SUBST, INTERACTIVE, LONGLISTJOBS, MONITOR,
32 OPT_ISSET, POSIXBUILTINS, POSIXJOBS, STAT_ATTACH, STAT_INUSE, STAT_SUBJOB,
33 STAT_SUBJOB_ORPHANED, STAT_SUPERJOB,
34};
35pub use crate::ported::zsh_h::{timeinfo, MAXJOBS_ALLOC, MAX_PIPESTATS, SP_RUNNING};
36use crate::DPUTS;
37use std::env;
38use std::os::unix::process::ExitStatusExt;
39use std::process::Child;
40use std::sync::atomic::Ordering;
41use std::sync::{Mutex, OnceLock};
42use std::time::{Duration, Instant};
43
44/// job status flags. `i32` to match C's `int stat` field on
45/// `struct job` (`Src/zsh.h:1062`).
46///
47/// **Bit values MUST match C's `STAT_*` defines verbatim** at
48/// `Src/zsh.h:1073-1094`. Previous Rust port used sequential bit
49/// shifts (1<<0, 1<<1, …) which produced DIFFERENT values from C
50/// for EVERY flag — `stat::STOPPED = 0x01` vs C `STAT_STOPPED =
51/// 0x0002`, etc. Any data ferried between C-side and Rust-side
52/// (or between bytecode and runtime state) would mis-interpret
53/// every stat-flag check. Now canonical.
54///
55/// Also added missing flags (CHANGED, TIMED, LOCKED, NOPRINT,
56/// NOSTTY, SUBLEADER) and removed bogus ones (DISOWN, NOTIFY —
57/// not in C STAT_*).
58pub mod stat {
59 /// `CHANGED` constant.
60 pub const CHANGED: i32 = 0x0001; // c:1073 status changed
61 /// `STOPPED` constant.
62 pub const STOPPED: i32 = 0x0002; // c:1074 all procs stopped or exited
63 /// `TIMED` constant.
64 pub const TIMED: i32 = 0x0004; // c:1075 job is being timed
65 /// `DONE` constant.
66 pub const DONE: i32 = 0x0008; // c:1076 job is done
67 /// `LOCKED` constant.
68 pub const LOCKED: i32 = 0x0010; // c:1077 shell finished creating
69 /// `NOPRINT` constant.
70 pub const NOPRINT: i32 = 0x0020; // c:1079 killed internally
71 /// `INUSE` constant.
72 pub const INUSE: i32 = 0x0040; // c:1081 entry in use
73 /// `SUPERJOB` constant.
74 pub const SUPERJOB: i32 = 0x0080; // c:1082 job has a subjob
75 /// `SUBJOB` constant.
76 pub const SUBJOB: i32 = 0x0100; // c:1083 job is a subjob
77 /// `WASSUPER` constant.
78 pub const WASSUPER: i32 = 0x0200; // c:1084 was super-job
79 /// `CURSH` constant.
80 pub const CURSH: i32 = 0x0400; // c:1086 last cmd in current shell
81 /// `NOSTTY` constant.
82 pub const NOSTTY: i32 = 0x0800; // c:1087 tty settings not inherited
83 /// `ATTACH` constant.
84 pub const ATTACH: i32 = 0x1000; // c:1089 delay reattach to tty
85 /// `SUBLEADER` constant.
86 pub const SUBLEADER: i32 = 0x2000; // c:1090 super-job, leader is sub-shell
87 /// `BUILTIN` constant.
88 pub const BUILTIN: i32 = 0x4000; // c:1092 tail is builtin
89 /// `STAT_DISOWN` from `Src/zsh.h:1093`. SUPERJOB with disown pending.
90 pub const DISOWN: i32 = 0x10000; // c:1093
91}
92
93/// Time difference for timeval (from jobs.c dtime_tv)
94/// Port of `dtime_tv(struct timeval *dt, struct timeval *t1, struct timeval *t2)` from `Src/jobs.c:137`.
95pub fn dtime_tv(dt: &mut Duration, t1: &Duration, t2: &Duration) -> Duration {
96 if *t2 > *t1 {
97 *dt = *t2 - *t1;
98 } else {
99 *dt = Duration::ZERO;
100 }
101 *dt
102}
103
104/// Time difference for timespec (from jobs.c dtime_ts)
105/// Port of `dtime_ts(struct timespec *dt, struct timespec *t1, struct timespec *t2)` from `Src/jobs.c:152`.
106/// WARNING: param names don't match C — Rust=(t1, t2) vs C=(dt, t1, t2)
107pub fn dtime_ts(t1: &Instant, t2: &Instant) -> Duration {
108 if *t2 > *t1 {
109 t2.duration_since(*t1)
110 } else {
111 Duration::ZERO
112 }
113}
114
115// change job table entry from stopped to running // c:163
116/// Port of `makerunning(job jn)` from `Src/jobs.c:167`.
117///
118/// C body:
119/// ```c
120/// jn->stat &= ~STAT_STOPPED;
121/// for (pn = jn->procs; pn; pn = pn->next)
122/// if (WIFSTOPPED(pn->status))
123/// pn->status = SP_RUNNING;
124/// if (jn->stat & STAT_SUPERJOB)
125/// makerunning(jobtab + jn->other);
126/// ```
127///
128/// Clears the STOPPED flag on the job, resets each stopped process
129/// to SP_RUNNING, and recurses into the linked subjob if this is a
130// change job table entry from stopped to running // c:167
131/// superjob. The previous Rust port called `job.make_running()`
132/// which mutates only the single job — missing the superjob
133/// recursion. This port walks the table to handle the recursion.
134pub fn makerunning(jobtab: &mut [job], idx: usize) {
135 if idx >= jobtab.len() {
136 return;
137 }
138 let other = jobtab[idx].other as usize;
139 let is_super = (jobtab[idx].stat & stat::SUPERJOB) != 0;
140 {
141 let job = &mut jobtab[idx];
142 job.stat &= !stat::STOPPED;
143 for proc in &mut job.procs {
144 if proc.is_stopped() {
145 proc.status = SP_RUNNING;
146 }
147 }
148 }
149 if is_super && other != idx && other < jobtab.len() {
150 makerunning(jobtab, other);
151 }
152}
153
154// Find process and job associated with pid. // c:191
155// Return 1 if search was successful, else return 0. // c:191
156/// Port of `int findproc(pid_t pid, job *jptr, process *pptr, int aux)`
157/// from `Src/jobs.c:191`.
158///
159/// C body (c:198-236) walks `jobtab[1..=maxjob]`:
160/// - Skips entries where `(stat & STAT_DONE)` per c:204 — these are
161/// jobs already marked dead.
162/// - Walks ONLY `procs` OR `auxprocs` based on the `aux` arg, not
163/// both. The previous Rust port walked both arrays.
164/// - Prefers a `SP_RUNNING` match: if multiple pids hit but only
165/// one is still running, returns it. The previous Rust port
166/// returned the FIRST match regardless of running state.
167///
168/// **WARNING: param names don't match C** — Rust (jobtab, pid, aux)
169/// vs C (pid, **jptr, **pptr, int aux). Returns `Some((job_idx,
170/// proc_idx, aux_was_true))` rather than mutating out-pointers.
171pub fn findproc(jobtab: &[job], pid: i32, aux: bool) -> Option<(usize, usize, bool)> {
172 // c:191
173 let mut last_match: Option<(usize, usize, bool)> = None;
174 // c:198 — `for (i = 1; i <= maxjob; i++)`. Index 0 (the shell
175 // itself) is skipped.
176 for (ji, job) in jobtab.iter().enumerate().skip(1) {
177 // c:204 — `if (jobtab[i].stat & STAT_DONE) continue;`. Don't
178 // match against jobs already marked dead; their pids might
179 // be recycled by the kernel and collide with a live pid.
180 if (job.stat & stat::DONE) != 0 {
181 continue;
182 }
183 // c:209-210 — walk EITHER procs OR auxprocs based on aux.
184 let procs: &[process] = if aux { &job.auxprocs } else { &job.procs };
185 for (pi, proc) in procs.iter().enumerate() {
186 if proc.pid == pid {
187 // c:228
188 // c:229-232 — `if (pn->status == SP_RUNNING) return 1;`.
189 // Prefer a running match; otherwise record the last
190 // matching slot and keep looking.
191 if proc.status == SP_RUNNING {
192 return Some((ji, pi, aux)); // c:231 return 1
193 }
194 last_match = Some((ji, pi, aux)); // c:227 record
195 }
196 }
197 }
198 // c:235 — `return (*pptr && *jptr);` — at least one slot matched
199 // (even if not running). Rust returns last_match.
200 last_match
201}
202
203// `TimeInfo` / `ChildTimes` deleted — both folded into canonical
204// `timeinfo` at `zsh_h.rs:2153` (direct port of `struct timeinfo`
205// from `Src/zsh.h:1099`).
206
207// Canonical `process` / `job` live in `zsh_h.rs:1166,1180` — direct
208// ports of `struct process` / `struct job` from `Src/zsh.h:1117,1058`.
209// jobs.rs uses them via `process` / `job` aliases to keep call sites
210// readable (Rust convention favors CamelCase at use-sites; the
211// underlying type is the lowercase C-faithful canonical).
212
213impl process {
214 /// Build a fresh entry. Matches C's `update_process()` init shape
215 /// (`Src/jobs.c:363` — `pn->pid = pid; pn->status = SP_RUNNING;`
216 /// before the first wait).
217 pub fn new(pid: i32) -> Self {
218 process {
219 pid,
220 status: SP_RUNNING,
221 text: String::new(),
222 ti: timeinfo::default(),
223 bgtime: Some(Instant::now()),
224 endtime: None,
225 }
226 }
227
228 /// `SP_RUNNING` sentinel check — equivalent to C's `pn->status ==
229 /// SP_RUNNING` test at e.g. `Src/jobs.c:1242`.
230 pub fn is_running(&self) -> bool {
231 self.status == SP_RUNNING
232 }
233
234 /// Mirrors C's `WIFSTOPPED(status)` macro.
235 pub fn is_stopped(&self) -> bool {
236 self.status & 0xff == 0x7f
237 }
238
239 /// Mirrors C's `WIFSIGNALED(status)` macro.
240 pub fn is_signaled(&self) -> bool {
241 (self.status & 0x7f) > 0 && (self.status & 0x7f) < 0x7f
242 }
243
244 /// Mirrors C's `WEXITSTATUS(status)` macro.
245 pub fn exit_status(&self) -> i32 {
246 (self.status >> 8) & 0xff
247 }
248
249 /// Mirrors C's `WTERMSIG(status)` macro.
250 pub fn term_sig(&self) -> i32 {
251 self.status & 0x7f
252 }
253
254 /// Mirrors C's `WSTOPSIG(status)` macro.
255 pub fn stop_sig(&self) -> i32 {
256 (self.status >> 8) & 0xff
257 }
258}
259
260impl job {
261 /// Empty job slot — mirrors C's `memset(jn, 0, sizeof(*jn))`
262 /// done in `initjob_reuse()` (`Src/jobs.c:574`).
263 pub fn new() -> Self {
264 Self::default()
265 }
266
267 /// True if any procs/auxprocs registered. Equivalent to C's
268 /// `jn->procs || jn->auxprocs` null check at `Src/jobs.c` various.
269 pub fn has_procs(&self) -> bool {
270 !self.procs.is_empty() || !self.auxprocs.is_empty()
271 }
272
273 /// True if any proc is in the C `SP_RUNNING` state.
274 pub fn is_running(&self) -> bool {
275 self.procs.iter().any(|p| p.is_running())
276 }
277
278 /// True if every proc has finished (none `SP_RUNNING`, none stopped).
279 pub fn is_done(&self) -> bool {
280 !self.procs.is_empty()
281 && self
282 .procs
283 .iter()
284 .all(|p| !p.is_running() && !p.is_stopped())
285 }
286
287 /// True if the job is stopped — checks both the `STAT_STOPPED`
288 /// flag bit on `self.stat` and per-proc `WIFSTOPPED`. Matches
289 /// C's two-source check (`Src/jobs.c` reads `jn->stat & STAT_STOPPED`
290 /// for the flag and `WIFSTOPPED(pn->status)` per proc).
291 pub fn is_stopped(&self) -> bool {
292 (self.stat & stat::STOPPED) != 0 || self.procs.iter().any(|p| p.is_stopped())
293 }
294
295 /// True if the slot is marked `INUSE` — equivalent to C's
296 /// `(jn->stat & STAT_INUSE) != 0` check.
297 pub fn is_inuse(&self) -> bool {
298 (self.stat & stat::INUSE) != 0
299 }
300
301 /// Walk procs and reset their `status` back to `SP_RUNNING` —
302 /// mirrors C's `makerunning()` body (`Src/jobs.c:1573`).
303 pub fn make_running(&mut self) {
304 for p in &mut self.procs {
305 if p.is_stopped() {
306 p.status = SP_RUNNING;
307 }
308 }
309 self.stat &= !stat::STOPPED;
310 }
311}
312
313// `JobState` enum moved to `src/exec_jobs.rs` — Rust-only typed
314// wrapper for the executor's safe-Rust bg-job tracker. C uses the
315// `STAT_*` u32 bits on `struct job.stat` (`stat::*` constants
316// above) directly; the enum exists only to give the
317// std::process::Child path a typed projection.
318//
319// `JobEntry` struct deleted — Rust-only "simple job entry for
320// executor compatibility" with zero callers anywhere. JobInfo
321// already carries this exact shape; JobEntry was a stale duplicate.
322
323// ---------------------------------------------------------------------------
324// C-style globals (Bucket 2: shell-wide shared state per PORT_PLAN.md)
325// Declared in same order as jobs.c lines 57-131
326// ---------------------------------------------------------------------------
327
328/// Port of `hasprocs(int job)` from `Src/jobs.c:243`.
329///
330/// C body:
331/// ```c
332/// job jn;
333/// if (job < 0) { DPUTS(1, "job number invalid"); return 0; }
334/// jn = jobtab + job;
335/// return jn->procs || jn->auxprocs;
336/// ```
337///
338/// Takes the job index (not a `&job`) because the C signature is
339/// `int hasprocs(int job)`. Bounds-checks the index — out-of-range
340/// returns false (matching C's negative-index DPUTS+0 path).
341/// WARNING: param names don't match C — Rust=(jobtab, job) vs C=(job)
342pub fn hasprocs(jobtab: &[job], job: usize) -> bool {
343 jobtab
344 .get(job)
345 .map(|j| !j.procs.is_empty() || !j.auxprocs.is_empty())
346 .unwrap_or(false)
347}
348
349/// Port of `super_job(int sub)` from `Src/jobs.c:259-270` — find the super-job of a sub-job.
350/// ```c
351/// for (i = 1; i <= maxjob; i++)
352/// if ((jobtab[i].stat & STAT_SUPERJOB) &&
353/// jobtab[i].other == sub &&
354/// jobtab[i].gleader)
355/// return i;
356/// return 0;
357/// ```
358/// The `gleader` non-zero check at c:267 was previously missing in
359/// the Rust port — silently returned super-job indices for entries
360/// that hadn't yet had a process-group leader assigned, breaking
361/// job-control SIGCONT relay paths.
362pub fn super_job(jobtab: &[job], job_idx: usize) -> Option<usize> {
363 // c:260
364 for (i, job) in jobtab.iter().enumerate() {
365 if (job.stat & stat::SUPERJOB) != 0 && job.other as usize == job_idx && job.gleader != 0
366 // c:267
367 {
368 return Some(i);
369 }
370 }
371 None
372}
373
374/// Handle subjob completion (from jobs.c handle_sub)
375/// Port of `handle_sub(int job, int fg)` from `Src/jobs.c:274`.
376/// WARNING: param names don't match C — Rust=(jobtab, super_idx, fg) vs C=(job, fg)
377pub fn handle_sub(jobtab: &mut [job], super_idx: usize, fg: bool) -> i32 {
378 // c:274
379 // c:277 — `job jn = jobtab + job, sj = jobtab + jn->other;`
380 let sub_idx = jobtab[super_idx].other as usize;
381 if sub_idx >= jobtab.len() {
382 return 0;
383 }
384
385 // c:279 — `if ((sj->stat & STAT_DONE) || (!sj->procs && !sj->auxprocs)) {`
386 let sj_done = (jobtab[sub_idx].stat & stat::DONE) != 0
387 || (jobtab[sub_idx].procs.is_empty() && jobtab[sub_idx].auxprocs.is_empty());
388 if sj_done {
389 // c:282-292 — walk sj->procs looking for a signaled one; cascade
390 // SIGCONT + signal to superjob's group, then SIGCONT + signal
391 // to sj->other.
392 let mut signaled: Option<i32> = None;
393 for p in jobtab[sub_idx].procs.iter() {
394 #[cfg(unix)]
395 if libc::WIFSIGNALED(p.status) {
396 signaled = Some(libc::WTERMSIG(p.status));
397 break;
398 }
399 }
400 if let Some(sig) = signaled {
401 // c:283-291 — kill the superjob via gleader (or first proc),
402 // then SIGCONT + signal to sj->other.
403 let jn_gleader = jobtab[super_idx].gleader;
404 let multi_procs = jobtab[super_idx].procs.len() > 1;
405 #[cfg(unix)]
406 {
407 let mypgrp = unsafe { libc::getpgrp() };
408 if jn_gleader != mypgrp && multi_procs {
409 unsafe { libc::killpg(jn_gleader, sig) }; // c:285
410 } else if let Some(p0) = jobtab[super_idx].procs.first() {
411 unsafe { libc::kill(p0.pid, sig) }; // c:287
412 }
413 let sj_other = jobtab[sub_idx].other;
414 unsafe { libc::kill(sj_other, libc::SIGCONT) }; // c:288
415 unsafe { libc::kill(sj_other, sig) }; // c:289
416 }
417 #[cfg(not(unix))]
418 {
419 let _ = (jn_gleader, multi_procs, sig);
420 }
421 } else {
422 // c:293-326 — no signaled proc: mark SUPERJOB cleared,
423 // WASSUPER set; gleader-recovery if dead; attachtty when
424 // fg; deletejob if DISOWN pending.
425 jobtab[super_idx].stat &= !stat::SUPERJOB; // c:296
426 jobtab[super_idx].stat |= stat::WASSUPER; // c:297
427 // c:299-306 — gleader recovery: if the first proc has exited
428 // or been signaled AND killpg(gleader, 0) → ESRCH,
429 // promote the last proc's pid to be the new
430 // gleader (cp).
431 let cp: bool;
432 #[cfg(unix)]
433 {
434 let first_status = jobtab[super_idx]
435 .procs
436 .first()
437 .map(|p| p.status)
438 .unwrap_or(0);
439 let dead = libc::WIFEXITED(first_status) || libc::WIFSIGNALED(first_status);
440 let gleader_dead = dead
441 && unsafe { libc::killpg(jobtab[super_idx].gleader, 0) } == -1
442 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH);
443 cp = gleader_dead;
444 if cp {
445 if let Some(last) = jobtab[super_idx].procs.last() {
446 jobtab[super_idx].gleader = last.pid; // c:305
447 }
448 }
449 }
450 #[cfg(not(unix))]
451 {
452 cp = false;
453 }
454
455 // c:318-320 — attachtty(jn->gleader) when fg or thisjob == job,
456 // and the superjob is the sub-shell alone (single
457 // proc, or gleader recovered, or first proc != gleader).
458 let thisjob = *THISJOB
459 .get_or_init(|| Mutex::new(-1))
460 .lock()
461 .expect("thisjob poisoned");
462 let cond_attach = fg || thisjob as usize == super_idx;
463 let single_proc = jobtab[super_idx].procs.len() == 1;
464 let first_pid_neq_gleader = jobtab[super_idx]
465 .procs
466 .first()
467 .map(|p| p.pid != jobtab[super_idx].gleader)
468 .unwrap_or(false);
469 if cond_attach && (single_proc || cp || first_pid_neq_gleader) {
470 // c:319 — `attachtty(jn->gleader);` hand the tty to
471 // the super-job's process group leader.
472 #[cfg(unix)]
473 crate::ported::utils::attachtty(jobtab[super_idx].gleader);
474 }
475 // c:321 — kill(sj->other, SIGCONT);
476 #[cfg(unix)]
477 unsafe {
478 libc::kill(jobtab[sub_idx].other, libc::SIGCONT);
479 }
480
481 // c:322-325 — `if (jn->stat & STAT_DISOWN) deletejob(jn, 1);`
482 if (jobtab[super_idx].stat & stat::DISOWN) != 0 {
483 deletejob(&mut jobtab[super_idx], true);
484 }
485 }
486 // c:327 — curjob = jn - jobtab;
487 if let Ok(mut cj) = CURJOB.get_or_init(|| Mutex::new(-1)).lock() {
488 *cj = super_idx as i32;
489 }
490 return 0; // c:340 fall-through return
491 } else if (jobtab[sub_idx].stat & stat::STOPPED) != 0 {
492 // c:328
493 // c:331-337 — STOPPED branch: propagate STOPPED to superjob,
494 // clone subjob's first-proc status to every super
495 // proc that's still running.
496 jobtab[super_idx].stat |= stat::STOPPED; // c:331
497 let sj_proc_status = jobtab[sub_idx].procs.first().map(|p| p.status).unwrap_or(0);
498 for p in jobtab[super_idx].procs.iter_mut() {
499 // c:332
500 if p.status == SP_RUNNING // c:333-334
501 || {
502 #[cfg(unix)]
503 { !libc::WIFEXITED(p.status) && !libc::WIFSIGNALED(p.status) }
504 #[cfg(not(unix))]
505 { false }
506 }
507 {
508 p.status = sj_proc_status; // c:335
509 }
510 }
511 if let Ok(mut cj) = CURJOB.get_or_init(|| Mutex::new(-1)).lock() {
512 *cj = super_idx as i32; // c:336
513 }
514 // c:337 — printjob(jn, !!isset(LONGLISTJOBS), 1);
515 // printjob takes a snapshot signature here that requires
516 // cur_job/prev_job indices; defer the print to the caller
517 // (jobs.rs's jobs-builtin scanner) which has those handy.
518 return 1; // c:338
519 }
520 0 // c:340
521}
522
523/// Get children's time accounting.
524/// Port of `get_usage()` from Src/jobs.c — fills `child_usage`
525/// from `getrusage(RUSAGE_CHILDREN)` on supported systems.
526pub fn get_usage() -> timeinfo {
527 #[cfg(unix)]
528 {
529 let mut u: libc::rusage = unsafe { std::mem::zeroed() };
530 if unsafe { libc::getrusage(libc::RUSAGE_CHILDREN, &mut u) } == 0 {
531 return timeinfo::from_rusage(&u);
532 }
533 }
534 timeinfo::default()
535}
536
537/// Port of `update_process(process pn, int status)` from `Src/jobs.c:363`.
538///
539/// C body:
540/// ```c
541/// struct timeval childs = child_usage.ru_stime, childu = child_usage.ru_utime;
542/// get_usage();
543/// zgettime_monotonic_if_available(&pn->endtime);
544/// pn->status = status;
545/// dtime_tv(&pn->ti.ru_stime, &childs, &child_usage.ru_stime);
546/// dtime_tv(&pn->ti.ru_utime, &childu, &child_usage.ru_utime);
547/// ```
548///
549/// Snapshots the children-rusage delta between the previous reading
550/// and the call to `get_usage()` — the per-process rusage attribution.
551///
552/// Mirrors C's `child_usage` global pattern (`Src/jobs.c:109`):
553/// the in-flight rusage snapshot lives in `CHILD_USAGE_PREV`, gets
554/// captured pre-wait by `child_usage_snapshot()`, and update_process
555/// diffs that against the current `get_usage()` to attribute per-
556/// process rusage. Without this snapshot pre-wait, all diffs are 0
557/// (the previous Rust port had this bug).
558pub fn update_process(pn: &mut process, status: i32) {
559 // c:362
560 let prev = CHILD_USAGE_PREV.with(|c| c.borrow().clone()); // c:366-367
561 let now = get_usage(); // c:374 get_usage()
562 CHILD_USAGE_PREV.with(|c| *c.borrow_mut() = now.clone());
563
564 pn.endtime = Some(Instant::now()); // c:375 zgettime_monotonic_if_available
565 pn.status = status; // c:377
566
567 // Field-by-field diff (now - prev), clamped >= 0 to handle the
568 // first-wait case where prev is zero-initialised.
569 let diff = |a: i64, b: i64| -> i64 { (a - b).max(0) };
570 pn.ti = timeinfo {
571 ut: diff(now.ut, prev.ut), // c:380 ru_utime delta
572 st: diff(now.st, prev.st), // c:379 ru_stime delta
573 maxrss: now.maxrss.max(prev.maxrss),
574 majflt: diff(now.majflt, prev.majflt),
575 minflt: diff(now.minflt, prev.minflt),
576 nswap: diff(now.nswap, prev.nswap),
577 ixrss: diff(now.ixrss, prev.ixrss),
578 idrss: diff(now.idrss, prev.idrss),
579 isrss: diff(now.isrss, prev.isrss),
580 inblock: diff(now.inblock, prev.inblock),
581 oublock: diff(now.oublock, prev.oublock),
582 nvcsw: diff(now.nvcsw, prev.nvcsw),
583 nivcsw: diff(now.nivcsw, prev.nivcsw),
584 msgsnd: diff(now.msgsnd, prev.msgsnd),
585 msgrcv: diff(now.msgrcv, prev.msgrcv),
586 nsignals: diff(now.nsignals, prev.nsignals),
587 };
588}
589
590// `child_usage` — Src/jobs.c:109 mod_export global. The cumulative
591// children rusage snapshot kept warm between waits. update_process
592// reads-then-overwrites it to compute the delta attributable to the
593// just-reaped child. Per-thread (bucket 1) because each worker
594// thread reaps its own children independently.
595thread_local! {
596 static CHILD_USAGE_PREV: std::cell::RefCell<timeinfo>
597 = const { std::cell::RefCell::new(timeinfo {
598 ut: 0, st: 0, maxrss: 0, majflt: 0, minflt: 0, nswap: 0,
599 ixrss: 0, idrss: 0, isrss: 0, inblock: 0, oublock: 0,
600 nvcsw: 0, nivcsw: 0, msgsnd: 0, msgrcv: 0, nsignals: 0,
601 }) };
602}
603
604/// Check current shell signals (from jobs.c check_cursh_sig)
605#[cfg(unix)]
606/// Port of `check_cursh_sig(int sig)` from `Src/jobs.c:397`.
607/// WARNING: param names don't match C — Rust=(jobtab, sig) vs C=(sig)
608pub fn check_cursh_sig(jobtab: &[job], sig: i32) {
609 for job in jobtab {
610 if (job.stat & stat::CURSH) != 0 && !job.is_done() {
611 for proc in &job.procs {
612 if proc.is_running() {
613 unsafe {
614 libc::kill(proc.pid, sig);
615 }
616 }
617 }
618 }
619 }
620}
621
622/// Port of `storepipestats(job jn, int inforeground, int fixlastval)` from `Src/jobs.c:420`.
623///
624/// C body decodes each process's wait-status into a normalised
625/// pipestats entry (signal-bit-or-exit-code) and tracks the
626/// last non-zero status for `setopt PIPEFAIL` semantics:
627/// ```c
628/// jpipestats[i] = (WIFSIGNALED(p->status) ? 0200 | WTERMSIG(p->status) :
629/// WIFSTOPPED(p->status) ? 0200 | WSTOPSIG(p->status) :
630/// WEXITSTATUS(p->status));
631/// if (jpipestats[i]) pipefail = jpipestats[i];
632/// ```
633///
634/// The previous Rust port returned the raw `proc.status` values
635/// without decoding — wrong for any signal-terminated process
636/// (where status would have the high-bit-stripped sig number, not
637/// the canonical pipestats encoding).
638///
639/// Returns `(pipestats, pipefail)` — the decoded array and the
640/// last non-zero entry (0 if all succeeded).
641/// WARNING: param names don't match C — Rust=(job) vs C=(jn, inforeground, fixlastval)
642pub fn storepipestats(job: &job) -> (Vec<i32>, i32) {
643 let mut stats = Vec::with_capacity(job.procs.len().min(MAX_PIPESTATS));
644 let mut pipefail = 0;
645 for p in job.procs.iter().take(MAX_PIPESTATS) {
646 let st = p.status;
647 // SP_RUNNING is the in-flight sentinel; treat as 0.
648 let entry = if st == SP_RUNNING {
649 0
650 } else if (st & 0x7f) > 0 && (st & 0x7f) < 0x7f {
651 // WIFSIGNALED — bit 0x80 + signal number.
652 0o200 | (st & 0x7f)
653 } else if (st & 0xff) == 0x7f {
654 // WIFSTOPPED — bit 0x80 + stop signal.
655 0o200 | ((st >> 8) & 0xff)
656 } else {
657 // WIFEXITED — exit status.
658 (st >> 8) & 0xff
659 };
660 stats.push(entry);
661 if entry != 0 {
662 pipefail = entry;
663 }
664 }
665 (stats, pipefail)
666}
667
668// Update status of job, possibly printing it // c:460
669/// Update job status after process change (from jobs.c update_job)
670/// Returns true if the job is now done or stopped (status committed),
671/// false if any proc is still running (no update needed).
672pub fn update_job(job: &mut job) -> bool {
673 // c:460
674 // c:467-474 — `for (pn = jn->auxprocs; pn; pn = pn->next) {
675 // if (WIFCONTINUED(pn->status)) pn->status = SP_RUNNING;
676 // if (pn->status == SP_RUNNING) return; }`
677 for proc in job.auxprocs.iter_mut() {
678 #[cfg(unix)]
679 if proc.status > 0
680 && !libc::WIFEXITED(proc.status)
681 && !libc::WIFSIGNALED(proc.status)
682 && !libc::WIFSTOPPED(proc.status)
683 {
684 // WIFCONTINUED not exposed as a libc::W* fn on every target;
685 // it's the "neither exited nor signaled nor stopped" case
686 // that means SIGCONT was just delivered. Mark SP_RUNNING.
687 proc.status = SP_RUNNING;
688 }
689 if proc.is_running() {
690 return false;
691 }
692 }
693
694 // c:476-498 — walk main procs, look for SP_RUNNING (bail), track
695 // somestopped, capture last-proc status (signal/stop/exit),
696 // set the signalled flag.
697 let mut some_stopped = false;
698 let mut signalled = false;
699 let mut val: i32 = 0;
700 let proc_count = job.procs.len();
701 for (i, proc) in job.procs.iter_mut().enumerate() {
702 #[cfg(unix)]
703 if proc.status > 0
704 && !libc::WIFEXITED(proc.status)
705 && !libc::WIFSIGNALED(proc.status)
706 && !libc::WIFSTOPPED(proc.status)
707 {
708 // WIFCONTINUED main path: clear STAT_STOPPED + SP_RUNNING.
709 job.stat &= !stat::STOPPED;
710 proc.status = SP_RUNNING;
711 }
712 if proc.is_running() {
713 return false;
714 }
715 if proc.is_stopped() {
716 some_stopped = true;
717 }
718 // c:487-495 — last proc determines exit val.
719 if i + 1 == proc_count {
720 #[cfg(unix)]
721 {
722 if libc::WIFSIGNALED(proc.status) {
723 val = 0o200 | libc::WTERMSIG(proc.status);
724 signalled = true;
725 } else if libc::WIFSTOPPED(proc.status) {
726 val = 0o200 | libc::WSTOPSIG(proc.status);
727 } else {
728 val = libc::WEXITSTATUS(proc.status);
729 }
730 }
731 #[cfg(not(unix))]
732 {
733 val = proc.status;
734 }
735 }
736 }
737
738 // c:502-543 — somestopped: mark STAT_CHANGED|STOPPED; cascade SIGTSTP
739 // to the super-job if this is a subjob (c:507-540).
740 if some_stopped {
741 if (job.stat & stat::SUBJOB) != 0 {
742 job.stat |= stat::CHANGED | stat::STOPPED; // c:514
743 // c:515-538 — find the super-job; killpg(super.gleader, SIGTSTP);
744 // mark super CHANGED|STOPPED. Without a job-index-
745 // from-job reverse lookup wired here (we'd need
746 // the JOBTAB position, but Rust callers usually
747 // hold the &mut job by &mut [job][i]), defer the
748 // SIGTSTP to whoever owns the jobtab.
749 // Documented gap — the caller in fusevm_bridge that does the
750 // wait3 dispatch knows the index and handles the super hop.
751 return true;
752 }
753 if (job.stat & stat::STOPPED) != 0 {
754 return true; // c:541-542
755 }
756 job.stat |= stat::STOPPED;
757 job.stat &= !stat::DONE;
758 job.stat |= stat::CHANGED;
759 return true;
760 }
761
762 // c:544-556 — job is fully done. Set DONE, write lastval2/lastval.
763 job.stat |= stat::DONE | stat::CHANGED;
764 job.stat &= !stat::STOPPED;
765 // c:545 — lastval2 = val;
766 LASTVAL2.store(val, Ordering::SeqCst);
767
768 // c:550-555 — `if (jn->stat & STAT_CURSH) inforeground = 1;
769 // else if (job == thisjob) { lastval = val; inforeground = 2; }`
770 // Drives the c:565 "deadpgrp" path and the MONITOR foreground
771 // cascade. Mark via _inforeground for the trace; signal cascade
772 // skipped (interactive substrate).
773 let _inforeground: i32 = if (job.stat & stat::CURSH) != 0 {
774 1
775 } else {
776 // We don't know `thisjob == job_idx` from `&mut job` alone;
777 // the caller (wait-loop) knows the index and handles lastval.
778 0
779 };
780 let _ = signalled;
781 true
782}
783
784/// `lastval2` — Src/jobs.c global. Set to last-pipeline exit status.
785pub static LASTVAL2: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
786
787/// Update a background job after waitpid (from jobs.c update_bg_job)
788/// Port of `update_bg_job(job jn, pid_t pid, int status)` from `Src/jobs.c:677`.
789pub fn update_bg_job(jn: &mut [job], pid: i32, status: i32) -> bool {
790 // Try primary procs first, then auxprocs — C `findproc` takes
791 // an explicit `aux` arg and the caller decides which subset is
792 // relevant. update_bg_job needs to handle BOTH because the
793 // waitpid'd pid might land in either.
794 let hit = findproc(jn, pid, false).or_else(|| findproc(jn, pid, true));
795 if let Some((ji, pi, is_aux)) = hit {
796 if is_aux {
797 jn[ji].auxprocs[pi].status = status;
798 jn[ji].auxprocs[pi].endtime = Some(Instant::now());
799 } else {
800 jn[ji].procs[pi].status = status;
801 jn[ji].procs[pi].endtime = Some(Instant::now());
802 }
803 // c:Src/jobs.c:684-699 (update_bg_job) — record a finished
804 // BACKGROUND job's exit status in the bgstatus ring so a later
805 // `wait $pid` can retrieve it after the child is gone (waitpid
806 // returns ECHILD; bin_wait then consults getbgstatus). A bg job
807 // is one not marked STAT_CURSH/STAT_BUILTIN and not the current
808 // foreground job (thisjob). Without this, `(exit 5) & p=$!;
809 // wait $p` reaped the child but dropped its status, so the wait
810 // failed with "pid N is not a child of this shell" (127).
811 let thisjob = *THISJOB
812 .get_or_init(|| Mutex::new(-1))
813 .lock()
814 .expect("thisjob poisoned");
815 if (jn[ji].stat & (stat::CURSH | stat::BUILTIN)) == 0 && ji as i32 != thisjob {
816 if libc::WIFEXITED(status) {
817 addbgstatus(pid, libc::WEXITSTATUS(status)); // c:695
818 } else if libc::WIFSIGNALED(status) {
819 addbgstatus(pid, 0o200 | libc::WTERMSIG(status)); // c:697
820 }
821 }
822 update_job(&mut jn[ji]);
823 return true;
824 }
825 false
826}
827
828// set the previous job to something reasonable // c:698
829/// Direct port of `static void setprevjob(void)` from `Src/jobs.c:698`.
830/// Walks the global jobtab to pick `prevjob` — first stopped (non-
831/// subjob, non-curjob, non-thisjob) candidate, else first in-use one.
832pub fn setprevjob() {
833 // c:698
834 let tab = JOBTAB
835 .get_or_init(|| Mutex::new(Vec::new()))
836 .lock()
837 .expect("jobtab poisoned");
838 let maxjob = *MAXJOB
839 .get_or_init(|| Mutex::new(0))
840 .lock()
841 .expect("maxjob poisoned");
842 let curjob = *CURJOB
843 .get_or_init(|| Mutex::new(-1))
844 .lock()
845 .expect("curjob poisoned");
846 let thisjob = *THISJOB
847 .get_or_init(|| Mutex::new(-1))
848 .lock()
849 .expect("thisjob poisoned");
850 // c:702-707 — stopped candidate.
851 for i in (1..=maxjob).rev() {
852 if i >= tab.len() {
853 continue;
854 }
855 let j = &tab[i];
856 if (j.stat & (stat::INUSE | stat::STOPPED)) == (stat::INUSE | stat::STOPPED)
857 && (j.stat & stat::SUBJOB) == 0
858 && i as i32 != curjob
859 && i as i32 != thisjob
860 {
861 *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = i as i32;
862 return;
863 }
864 }
865 // c:709-714 — fallback to any in-use non-subjob.
866 for i in (1..=maxjob).rev() {
867 if i >= tab.len() {
868 continue;
869 }
870 let j = &tab[i];
871 if (j.stat & stat::INUSE) != 0
872 && (j.stat & stat::SUBJOB) == 0
873 && i as i32 != curjob
874 && i as i32 != thisjob
875 {
876 *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = i as i32;
877 return;
878 }
879 }
880 // c:716 — nothing eligible.
881 *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = -1;
882}
883
884/// Get clock ticks per second (from jobs.c get_clktck lines 720-748)
885/// Get `_SC_CLK_TCK` for time-conversion math.
886/// Port of `get_clktck()` from Src/jobs.c:721.
887pub fn get_clktck() -> i64 {
888 // c:721
889 #[cfg(unix)]
890 {
891 static CLKTCK: OnceLock<i64> = OnceLock::new(); // c:723
892 // fetch clock ticks per second from // c:727
893 // sysconf only the first time // c:728
894 *CLKTCK.get_or_init(|| unsafe { libc::sysconf(libc::_SC_CLK_TCK) as i64 })
895 // c:729
896 }
897 #[cfg(not(unix))]
898 {
899 100 // Default on non-Unix
900 }
901}
902
903/// Format time as hh:mm:ss.xx (from jobs.c printhhmmss lines 752-765)
904/// Format a duration as `H:MM:SS` / `M:SS`.
905/// Port of `printhhmmss(double secs)` from Src/jobs.c:752.
906pub fn printhhmmss(secs: f64) -> String {
907 // c:752
908 let mins = (secs / 60.0) as i32;
909 let hours = mins / 60;
910 let secs = secs - (mins * 60) as f64;
911 let mins = mins - (hours * 60);
912
913 if hours > 0 {
914 format!("{}:{:02}:{:05.2}", hours, mins, secs)
915 } else if mins > 0 {
916 format!("{}:{:05.2}", mins, secs)
917 } else {
918 format!("{:.3}", secs)
919 }
920}
921
922/// Time format specifiers (from jobs.c printtime lines 768-949)
923/// Format a CPU/real time triple per `$TIMEFMT`.
924/// Port of `printtime(struct timespec *real, child_times_t *ti, char *desc)` from Src/jobs.c:768.
925/// Supports the full directive set: `%E/%U/%S/%P/%J/%mE/%uE/%nE/%*E`
926/// (time forms) plus `%M/%F/%R/%W/%X/%D/%K/%I/%O/%c/%w` (rusage).
927pub fn printtime(
928 // c:768
929 elapsed_secs: f64,
930 ti: &timeinfo,
931 format: &str,
932 job_name: &str,
933) -> String {
934 let user_secs = ti.ut as f64 / 1_000_000.0;
935 let system_secs = ti.st as f64 / 1_000_000.0;
936 let mut result = String::new();
937 let total_time = user_secs + system_secs; // c:794
938 let percent = if elapsed_secs > 0.0 {
939 // c:795
940 (100.0 * total_time / elapsed_secs) as i32
941 } else {
942 0
943 };
944 // Per-second helper for the rusage-rate directives (X/D/K).
945 let per_sec = |v: i64| -> i64 {
946 // c:903-907
947 if total_time > 0.0 {
948 (v as f64 / total_time) as i64
949 } else {
950 0
951 }
952 };
953
954 let mut chars = format.chars().peekable();
955 while let Some(c) = chars.next() {
956 if c == '%' {
957 match chars.next() {
958 // c:816-823 — %E / %U / %S
959 Some('E') => result.push_str(&format!("{:.2}s", elapsed_secs)),
960 Some('U') => result.push_str(&format!("{:.2}s", user_secs)),
961 Some('S') => result.push_str(&format!("{:.2}s", system_secs)),
962 // c:893-894 — %P
963 Some('P') => result.push_str(&format!("{}%", percent)),
964 Some('J') => result.push_str(job_name),
965 // c:825-840 — %mE / %mU / %mS (milliseconds)
966 Some('m') => match chars.next() {
967 Some('E') => result.push_str(&format!("{:.0}ms", elapsed_secs * 1000.0)),
968 Some('U') => result.push_str(&format!("{:.0}ms", user_secs * 1000.0)),
969 Some('S') => result.push_str(&format!("{:.0}ms", system_secs * 1000.0)),
970 _ => result.push_str("%m"),
971 },
972 // c:842-857 — %uE / %uU / %uS (microseconds)
973 Some('u') => match chars.next() {
974 Some('E') => result.push_str(&format!("{:.0}us", elapsed_secs * 1_000_000.0)),
975 Some('U') => result.push_str(&format!("{:.0}us", user_secs * 1_000_000.0)),
976 Some('S') => result.push_str(&format!("{:.0}us", system_secs * 1_000_000.0)),
977 _ => result.push_str("%u"),
978 },
979 // c:859-874 — %nE / %nU / %nS (nanoseconds)
980 Some('n') => match chars.next() {
981 Some('E') => {
982 result.push_str(&format!("{:.0}ns", elapsed_secs * 1_000_000_000.0))
983 }
984 Some('U') => result.push_str(&format!("{:.0}ns", user_secs * 1_000_000_000.0)),
985 Some('S') => {
986 result.push_str(&format!("{:.0}ns", system_secs * 1_000_000_000.0))
987 }
988 _ => result.push_str("%n"),
989 },
990 // c:876-891 — %*E / %*U / %*S (HH:MM:SS form)
991 Some('*') => match chars.next() {
992 Some('E') => result.push_str(&printhhmmss(elapsed_secs)),
993 Some('U') => result.push_str(&printhhmmss(user_secs)),
994 Some('S') => result.push_str(&printhhmmss(system_secs)),
995 _ => result.push_str("%*"),
996 },
997 // c:897-899 — %W: swaps
998 Some('W') => result.push_str(&format!("{}", ti.nswap)),
999 // c:902-907 — %X: integral shared mem / total_time
1000 Some('X') => result.push_str(&format!("{}", per_sec(ti.ixrss))),
1001 // c:910-919 — %D: integral unshared data / total_time
1002 Some('D') => result.push_str(&format!("{}", per_sec(ti.idrss + ti.isrss))),
1003 // c:924-942 — %K: total integral mem / total_time
1004 Some('K') => {
1005 result.push_str(&format!("{}", per_sec(ti.ixrss + ti.idrss + ti.isrss)))
1006 }
1007 // c:950-952 — %M: max resident set size (KB on macOS+Linux post-norm)
1008 Some('M') => result.push_str(&format!("{}", ti.maxrss)),
1009 // c:955-957 — %F: major page faults
1010 Some('F') => result.push_str(&format!("{}", ti.majflt)),
1011 // c:960-962 — %R: minor page faults
1012 Some('R') => result.push_str(&format!("{}", ti.minflt)),
1013 // c:965+ — %I: input block ops; %O: output; %c/%w: ctx switches
1014 Some('I') => result.push_str(&format!("{}", ti.inblock)),
1015 Some('O') => result.push_str(&format!("{}", ti.oublock)),
1016 Some('c') => result.push_str(&format!("{}", ti.nivcsw)),
1017 Some('w') => result.push_str(&format!("{}", ti.nvcsw)),
1018 Some('%') => result.push('%'),
1019 Some(other) => {
1020 result.push('%');
1021 result.push(other);
1022 }
1023 None => result.push('%'),
1024 }
1025 } else {
1026 result.push(c);
1027 }
1028 }
1029 result
1030}
1031
1032/// Dump timing info for a job (from jobs.c dumptime).
1033/// Port of `dumptime(job jn)` from `Src/jobs.c:1020`.
1034///
1035/// C body iterates each process in the pipeline and prints one
1036/// `printtime` line per process using that process's own bgtime/
1037/// endtime/ti/text — c:1027-1029. The previous Rust port aggregated
1038/// into a single timeinfo, which printed 1 line for a 3-stage
1039/// pipeline instead of C's 3.
1040pub fn dumptime(job: &job) -> Option<String> {
1041 // c:1020
1042 if job.procs.is_empty() {
1043 // c:1025-1026
1044 return None;
1045 }
1046 // C dumptime reads `$TIMEFMT` indirectly via printtime's getsparam
1047 // call (c:808 inside printtime). Rust printtime takes format as a
1048 // parameter, so we read it here and pass through.
1049 const DEFAULT_TIMEFMT: &str = "%J %U user %S system %P cpu %*E total";
1050 let format = getsparam("TIMEFMT").unwrap_or_else(|| DEFAULT_TIMEFMT.to_string());
1051
1052 // c:1027-1029 — for each proc, printtime(dtime_ts(&bgtime, &endtime), &ti, text).
1053 let lines: Vec<String> = job
1054 .procs
1055 .iter()
1056 .filter_map(|p| {
1057 let start = p.bgtime?;
1058 let end = p.endtime?;
1059 let elapsed = end.duration_since(start).as_secs_f64();
1060 Some(printtime(elapsed, &p.ti, &format, &p.text))
1061 })
1062 .collect();
1063 if lines.is_empty() {
1064 None
1065 } else {
1066 Some(lines.join("\n"))
1067 }
1068}
1069
1070/// Port of `static int should_report_time(job j)` from `Src/jobs.c:1038-1080`.
1071/// ```c
1072/// /* if the time keyword was used */
1073/// if (j->stat & STAT_TIMED) return 1;
1074/// /* read $REPORTTIME / $REPORTMEMORY */
1075/// if (reporttime < 0 && reportmemory < 0) return 0;
1076/// if (!j->procs) return 0;
1077/// if (zleactive) return 0;
1078/// /* … compare elapsed time vs reporttime threshold */
1079/// ```
1080/// Rust port previously missed the c:1052 STAT_TIMED short-circuit:
1081/// a job explicitly preceded by the `time` keyword should always
1082/// report its time regardless of `$REPORTTIME` setting. Without this
1083/// check, `time sleep 0.001` would be silent when REPORTTIME is
1084/// unset or set high.
1085///
1086/// `$REPORTTIME` (and `$REPORTMEMORY`) reading is the caller's
1087/// responsibility — Rust takes the thresholds as parameters rather
1088/// than calling getvalue inside.
1089pub fn should_report_time(job: &job, reporttime: f64) -> bool {
1090 // c:1039
1091 // Read both thresholds from paramtab — matches C's
1092 // `getvalue(REPORTTIME)` and `getvalue(REPORTMEMORY)` reads.
1093 let reportmemory: i64 = getsparam("REPORTMEMORY")
1094 .and_then(|s| s.parse().ok())
1095 .unwrap_or(-1);
1096
1097 // c:1052-1053 — STAT_TIMED short-circuit. Always report when
1098 // the `time` keyword preceded the command.
1099 if (job.stat & stat::TIMED) != 0 {
1100 // c:1052
1101 return true;
1102 }
1103 // c:1065-1070 — both thresholds disabled ⇒ no report.
1104 if reporttime < 0.0 && reportmemory < 0 {
1105 return false;
1106 }
1107 // c:1072-1073 — `if (!j->procs) return 0;`
1108 let first = match job.procs.first() {
1109 Some(p) => p,
1110 None => return false,
1111 };
1112 // c:1074 — `if (zleactive) return 0;`. ZLE is line-editing the
1113 // prompt; never spew a timing line into the editor.
1114 if zleactive.load(Ordering::Relaxed) != 0
1115 // c:1074
1116 {
1117 return false;
1118 }
1119 // c:1077-1094 — reporttime threshold check against (user+sys) CPU.
1120 if reporttime >= 0.0 {
1121 // C diffs reporttime against the first proc's ut+st; the
1122 // rusage diff is populated by update_process.
1123 let cpu_secs = (first.ti.ut + first.ti.st) as f64 / 1_000_000.0;
1124 if cpu_secs >= reporttime {
1125 return true;
1126 }
1127 // Wall-clock fallback (Rust extension — keeps prior behavior
1128 // when rusage wasn't captured because the proc was reaped
1129 // outside the wait4/getrusage path).
1130 if let (Some(start), Some(end)) = (first.bgtime, job.procs.last().and_then(|p| p.endtime)) {
1131 let elapsed = end.duration_since(start).as_secs_f64();
1132 if elapsed >= reporttime {
1133 return true;
1134 }
1135 }
1136 }
1137 // c:1096-1099 — reportmemory threshold check against ru_maxrss.
1138 if reportmemory >= 0 && first.ti.maxrss > reportmemory {
1139 return true;
1140 }
1141 false
1142}
1143
1144// `CommandTimer` struct deleted — Rust-only timing aggregator with
1145// no caller. C inlines `dtime_tv()` (Src/jobs.c:137) /
1146// `dtime_ts()` (line 152) into printjob; the Rust port's `printtime`
1147// (above) is the equivalent free-fn and any caller that needs
1148// elapsed time can `Instant::now()` directly.
1149
1150// `PipeStats` struct deleted — Rust-only wrapper that duplicated
1151// the `numpipestats` (jobs.c:131) + `pipestats[]` (jobs.c:131)
1152// flat C globals already ported as `NUMPIPESTATS` / `PIPESTATS` at
1153// file scope above. Read/write the canonical globals directly.
1154
1155/// File-static `sig_msg[]` from `Src/signames1.awk` /
1156/// `signames.h` — name-by-signal-number lookup table consulted by
1157/// `sigmsg()` at `jobs.c:1118`.
1158static SIG_MSG: &[(libc::c_int, &str)] = &[
1159 // c:signames.h
1160 (libc::SIGHUP, "hangup"),
1161 (libc::SIGINT, "interrupt"),
1162 (libc::SIGQUIT, "quit"),
1163 (libc::SIGILL, "illegal instruction"),
1164 (libc::SIGTRAP, "trace trap"),
1165 (libc::SIGABRT, "abort"),
1166 (libc::SIGBUS, "bus error"),
1167 (libc::SIGFPE, "floating point exception"),
1168 (libc::SIGKILL, "killed"),
1169 (libc::SIGUSR1, "user-defined signal 1"),
1170 (libc::SIGSEGV, "segmentation fault"),
1171 (libc::SIGUSR2, "user-defined signal 2"),
1172 (libc::SIGPIPE, "broken pipe"),
1173 (libc::SIGALRM, "alarm"),
1174 (libc::SIGTERM, "terminated"),
1175 (libc::SIGCHLD, "child exited"),
1176 (libc::SIGCONT, "continued"),
1177 (libc::SIGSTOP, "stopped (signal)"),
1178 (libc::SIGTSTP, "stopped"),
1179 (libc::SIGTTIN, "stopped (tty input)"),
1180 (libc::SIGTTOU, "stopped (tty output)"),
1181 (libc::SIGURG, "urgent I/O condition"),
1182 (libc::SIGXCPU, "CPU time exceeded"),
1183 (libc::SIGXFSZ, "file size exceeded"),
1184 (libc::SIGVTALRM, "virtual timer expired"),
1185 (libc::SIGPROF, "profiling timer expired"),
1186 (libc::SIGWINCH, "window changed"),
1187 (libc::SIGIO, "I/O ready"),
1188 (libc::SIGSYS, "bad system call"),
1189];
1190
1191/// Render a signal number as a one-line description.
1192/// Port of `sigmsg(int sig)` from Src/jobs.c:1107.
1193pub fn sigmsg(sig: i32) -> &'static str {
1194 // c:1107
1195 SIG_MSG
1196 .iter()
1197 .find(|(s, _)| *s == sig)
1198 .map(|(_, m)| *m)
1199 .unwrap_or("unknown signal") // c:1118 sig_msg[sig] : unknown
1200}
1201
1202/// Print job with full detail (from jobs.c printjob)
1203// find length of longest signame, check to see // c:1178
1204// if we really need to print this job // c:1179
1205/// `printjob` — see implementation.
1206pub fn printjob(
1207 job: &job,
1208 job_num: usize,
1209 lng: i32,
1210 cur_job: Option<usize>,
1211 prev_job: Option<usize>,
1212) -> String {
1213 // c:1141 — `int job, len = 9, sig, sflag = 0, llen;` — the status
1214 // column is `len + 2` wide where len starts at 9 and grows to the
1215 // longest signal message among non-running procs (c:1180-1213).
1216 let mut len = 9usize;
1217 for pn in job.procs.iter() {
1218 if pn.status == SP_RUNNING {
1219 continue;
1220 }
1221 #[cfg(unix)]
1222 {
1223 if libc::WIFSIGNALED(pn.status) {
1224 let mut llen = sigmsg(libc::WTERMSIG(pn.status)).len(); // c:1187
1225 if (pn.status & 0x80) != 0 {
1226 llen += 14; // c:1188-1189 WCOREDUMP " (core dumped)"
1227 }
1228 len = len.max(llen); // c:1190-1191
1229 } else if libc::WIFSTOPPED(pn.status) {
1230 len = len.max(sigmsg(libc::WSTOPSIG(pn.status)).len()); // c:1201-1203
1231 }
1232 }
1233 }
1234 let width = len + 2; // c:1256 — `len2 = 10 + len; /* 2 spaces */`
1235
1236 // Per-proc status text, padded to `width` per the fprintf field
1237 // widths at c:1293-1316.
1238 let fmt_proc_status = |status: i32| -> String {
1239 let s = if status == SP_RUNNING {
1240 "running".to_string() // c:1295
1241 } else if (status & 0x7f) == 0 {
1242 let code = (status >> 8) & 0xff;
1243 if code == 0 {
1244 "done".to_string() // c:1304
1245 } else {
1246 format!("exit {:<4}", code) // c:1301 "exit %-4d"
1247 }
1248 } else if (status & 0xff) == 0x7f {
1249 sigmsg((status >> 8) & 0xff).to_string() // c:1306 WSTOPSIG
1250 } else {
1251 let sig = status & 0x7f;
1252 if (status & 0x80) != 0 {
1253 format!("{} (core dumped)", sigmsg(sig)) // c:1309
1254 } else {
1255 sigmsg(sig).to_string() // c:1314 WTERMSIG
1256 }
1257 };
1258 format!("{:<w$}", s, w = width)
1259 };
1260 let marker = if Some(job_num) == cur_job {
1261 '+'
1262 } else if Some(job_num) == prev_job {
1263 '-'
1264 } else {
1265 ' '
1266 };
1267
1268 // c:1273-1277 — first line carries `[N] M `; continuation lines
1269 // (further proc groups) carry the matching indent.
1270 let head_prefix = format!("[{}] {} ", job_num, marker);
1271 let cont_prefix = if job_num > 9 { " " } else { " " }; // c:1277
1272
1273 let header = if job.procs.is_empty() {
1274 // c:1255 — `for (pn = jn->procs; pn;)` — a procless job (e.g.
1275 // the subshell control slot grabbed at c:1828) produces NO
1276 // output lines in C.
1277 if job.text.is_empty() {
1278 return String::new();
1279 }
1280 // Rust extension: jobs registered without proc entries carry
1281 // their display text on `job.text` (C always has procs). Use
1282 // the job-level stat bits for the status word.
1283 let status_str = if job.is_done() {
1284 format!("{:<w$}", "done", w = width)
1285 } else if job.is_stopped() {
1286 format!("{:<w$}", "suspended", w = width)
1287 } else {
1288 format!("{:<w$}", "running", w = width)
1289 };
1290 format!("{}{}{}", head_prefix, status_str, job.text)
1291 } else {
1292 // c:1255-1327 — group consecutive procs with the same status
1293 // onto one line (text joined with " | "); `jobs -l` / `jobs -p`
1294 // (lng & 3) put each proc on its own line.
1295 let mut lines: Vec<String> = Vec::new();
1296 let mut i = 0usize;
1297 let mut fline = true;
1298 let mut lng = lng;
1299 while i < job.procs.len() {
1300 let pn = &job.procs[i];
1301 // c:1257-1267 — group extent.
1302 let mut group_end = i + 1;
1303 if (lng & 3) == 0 {
1304 while group_end < job.procs.len()
1305 && job.procs[group_end].status == pn.status
1306 {
1307 group_end += 1;
1308 }
1309 }
1310 let mut line = String::new();
1311 line.push_str(if fline { &head_prefix } else { cont_prefix });
1312 if (lng & 1) != 0 {
1313 line.push_str(&format!("{} ", pn.pid)); // c:1281 "%ld "
1314 } else if (lng & 2) != 0 {
1315 line.push_str(&format!("{} ", job.gleader)); // c:1283-1285
1316 lng &= !3; // c:1290
1317 }
1318 line.push_str(&fmt_proc_status(pn.status));
1319 let texts: Vec<&str> = job.procs[i..group_end]
1320 .iter()
1321 .map(|p| p.text.as_str())
1322 .collect();
1323 line.push_str(&texts.join(" | ")); // c:1318-1325
1324 lines.push(line);
1325 fline = false;
1326 i = group_end;
1327 }
1328 lines.join("\n")
1329 };
1330
1331 // c:1220-1221 — `if (should_report_time(jn)) dumptime(jn);`
1332 // Also fires for c:1354-1355 (synchronous-wait variant).
1333 let reporttime: f64 = getsparam("REPORTTIME")
1334 .and_then(|s| s.parse().ok())
1335 .unwrap_or(-1.0);
1336 if should_report_time(job, reporttime) {
1337 if let Some(timing) = dumptime(job) {
1338 return format!("{}\n{}", header, timing);
1339 }
1340 }
1341 header
1342}
1343
1344
1345/// Port of `addfilelist(const char *name, int fd)` from `Src/jobs.c:1373`.
1346///
1347/// C body:
1348/// ```c
1349/// Jobfile jf = zalloc(sizeof(struct jobfile));
1350/// LinkList ll = jobtab[thisjob].filelist;
1351/// if (!ll) ll = jobtab[thisjob].filelist = znewlinklist();
1352/// if (name) { jf->u.name = ztrdup(name); jf->is_fd = 0; }
1353/// else { jf->u.fd = fd; jf->is_fd = 1; }
1354/// zaddlinknode(ll, jf);
1355/// ```
1356///
1357/// Stores either a temp-file name (to delete on job exit) or an
1358/// open fd (to close on job exit) as a `jobfile` enum node, mirroring
1359/// the C `struct jobfile` tagged union. C operates on
1360/// `jobtab[thisjob].filelist`; the Rust port takes the `job` directly.
1361pub fn addfilelist(job: &mut job, name: Option<&str>, fd: i32) {
1362 // c:1373 — `Jobfile jf = zalloc(sizeof(struct jobfile));`
1363 // c:1374 — `LinkList ll = jobtab[thisjob].filelist;` / c:1376 create-if-absent
1364 // folds into `Vec::push` (the Vec is the always-present list).
1365 let jf = match name {
1366 // c:1379 — `jf->u.name = ztrdup(name); jf->is_fd = 0;`
1367 Some(n) => jobfile { name: Some(n.to_string()), fd: 0, is_fd: 0 },
1368 // c:1383 — `jf->u.fd = fd; jf->is_fd = 1;`
1369 None => jobfile { name: None, fd, is_fd: 1 },
1370 };
1371 job.filelist.push(jf); // c:1385 zaddlinknode(ll, jf)
1372}
1373
1374/// Port of `pipecleanfilelist(LinkList filelist, int proc_subst_only)` from `Src/jobs.c:1397`.
1375///
1376/// Closes only `is_fd` entries (named-file entries are left for
1377/// `deletefilelist` at job exit). When `proc_subst_only`, only fds
1378/// flagged `FDT_PROC_SUBST` in the fdtable are closed; the rest stay.
1379/// Closed fd entries are removed from the list; named entries remain.
1380pub fn pipecleanfilelist(filelist: &mut job, proc_subst_only: bool) {
1381 // c:1404-1414 — walk the list; close+remove qualifying fd entries.
1382 filelist.filelist.retain(|jf| {
1383 // c:1405-1406 — `jf->is_fd && (!proc_subst_only ||
1384 // fdtable[jf->u.fd] == FDT_PROC_SUBST)`
1385 if jf.is_fd != 0 && (!proc_subst_only || fdtable_get(jf.fd) == FDT_PROC_SUBST) {
1386 zclose(jf.fd); // c:1408 zclose(jf->u.fd)
1387 false // c:1409 remnode(filelist, node) — drop from list
1388 } else {
1389 // c:1414 — `else incnode(node)`: keep everything else.
1390 true
1391 }
1392 });
1393}
1394
1395/// Port of `deletefilelist(LinkList file_list, int disowning)` from `Src/jobs.c:1422`.
1396///
1397/// For each `Jobfile`: `is_fd` → close the fd (unless `disowning`);
1398/// named → unlink the file (unless `disowning`). The `disowning`
1399/// flag suppresses the `close`/`unlink` so files survive the disown.
1400pub fn deletefilelist(file_list: &mut job, disowning: bool) {
1401 // c:1427-1438 — `while ((jf = getlinknode(file_list)))` consumes the list.
1402 for jf in &file_list.filelist {
1403 if jf.is_fd != 0 {
1404 // c:1430-1431 — `if (jf->is_fd) { if (!disowning) zclose(jf->u.fd); }`
1405 if !disowning {
1406 zclose(jf.fd); // c:1432 zclose(jf->u.fd)
1407 }
1408 } else {
1409 // c:1433-1436 — `else { if (!disowning) unlink(jf->u.name); zsfree(...); }`
1410 if !disowning {
1411 if let Some(ref name) = jf.name {
1412 let _ = std::fs::remove_file(name); // c:1435 unlink(jf->u.name)
1413 }
1414 }
1415 // c:1436 zsfree(jf->u.name) — owned String dropped with the node.
1416 }
1417 }
1418 // c:1438 — the loop drained the list; clear the Vec.
1419 file_list.filelist.clear();
1420}
1421
1422/// Port of `cleanfilelists()` from `Src/jobs.c:1443`.
1423///
1424/// C body:
1425/// ```c
1426/// DPUTS(shell_exiting >= 0, "BUG: cleanfilelists() before exit");
1427/// for (i = 1; i <= maxjob; i++) {
1428/// deletefilelist(jobtab[i].filelist, 0);
1429/// jobtab[i].filelist = 0;
1430/// }
1431/// ```
1432///
1433/// Deletes the file list (and its temp files) for every job in
1434/// the table. Called from the shell-exit path. The C source skips
1435/// index 0 (job 0 is unused / "the shell itself"); Rust port does
1436/// the same with `iter_mut().skip(1)`.
1437pub fn cleanfilelists(jobtab: &mut [job]) {
1438 // c:1447 — DPUTS(shell_exiting >= 0, "BUG: cleanfilelists() before exit")
1439 DPUTS!(
1440 // c:1447
1441 SHELL_EXITING // c:1447
1442 .load(std::sync::atomic::Ordering::Relaxed)
1443 >= 0, // c:1447
1444 "BUG: cleanfilelists() before exit" // c:1447
1445 );
1446 for job in jobtab.iter_mut().skip(1) {
1447 deletefilelist(job, false);
1448 }
1449}
1450
1451/// Port of `void freejob(job jn, int deleting)` from `Src/jobs.c:1457-1495`.
1452/// ```c
1453/// pn = jn->procs; jn->procs = NULL; free each;
1454/// pn = jn->auxprocs; jn->auxprocs = NULL; free each;
1455/// if (jn->ty) zfree(jn->ty);
1456/// if (jn->pwd) zsfree(jn->pwd);
1457/// jn->pwd = NULL;
1458/// if (jn->stat & STAT_WASSUPER) {
1459/// int job = jn - jobtab;
1460/// if (deleting) deletejob(jobtab + jn->other, 0);
1461/// else freejob(jobtab + jn->other, 0);
1462/// jn = jobtab + job;
1463/// }
1464/// jn->gleader = jn->other = 0;
1465/// jn->stat = jn->stty_in_env = 0;
1466/// jn->filelist = NULL;
1467/// jn->ty = NULL;
1468/// ```
1469/// The previous Rust port was missing the `pwd`/`ty`/`other`/
1470/// `stty_in_env` field resets — leaked saved-tty state into the
1471/// next job reuse of the slot. Now resets all fields per C. The
1472/// STAT_WASSUPER recursive delete (c:1480-1488) requires jobtab
1473/// access and is left as a doc comment until the caller wires it.
1474pub fn freejob(jn: &mut job, deleting: bool) {
1475 // c:1457
1476 let _ = deleting; // STAT_WASSUPER recursive path not yet wired.
1477 // c:1461-1466 — `procs = NULL; free each`. Rust Drop on Vec covers.
1478 jn.procs.clear();
1479 // c:1468-1473 — `auxprocs = NULL; free each`.
1480 jn.auxprocs.clear();
1481 // c:1475-1476 — `if (jn->ty) zfree(jn->ty);`.
1482 jn.ty = None;
1483 // c:1477-1479 — `if (jn->pwd) zsfree(jn->pwd); jn->pwd = NULL;`.
1484 jn.pwd = None;
1485 // c:1480-1488 — STAT_WASSUPER recursive delete: requires
1486 // jobtab[] access not in scope here. Doc-pin so a future caller
1487 // wiring the table can detect and dispatch.
1488 // c:1489 — `jn->gleader = jn->other = 0;`.
1489 jn.gleader = 0;
1490 jn.other = 0;
1491 // c:1490 — `jn->stat = jn->stty_in_env = 0;`.
1492 jn.stat = 0;
1493 jn.stty_in_env = 0;
1494 // c:1491 — `jn->filelist = NULL;`.
1495 jn.filelist.clear();
1496 // c:1492 — `jn->ty = NULL;` (already done above).
1497 // (Rust-only) text field — clear so the next job reuse doesn't
1498 // inherit stale command text.
1499 jn.text.clear();
1500}
1501
1502/// Port of `void deletejob(job jn, int disowning)` from `Src/jobs.c:1511-1526`.
1503/// ```c
1504/// deletefilelist(jn->filelist, disowning);
1505/// if (jn->stat & STAT_ATTACH) {
1506/// attachtty(mypgrp);
1507/// adjustwinsize(0);
1508/// }
1509/// if (jn->stat & STAT_SUPERJOB) {
1510/// job jno = jobtab + jn->other;
1511/// if (jno->stat & STAT_SUBJOB)
1512/// jno->stat |= STAT_SUBJOB_ORPHANED;
1513/// }
1514/// freejob(jn, 1);
1515/// ```
1516/// Previously the Rust port ad-hoc cleared procs/auxprocs/stat
1517/// without calling `freejob` — meant `pwd`/`ty`/`other`/`stty_in_env`
1518/// stayed populated even after the job was "deleted", silently
1519/// corrupting the next slot reuse. The STAT_ATTACH (attachtty) and
1520/// STAT_SUPERJOB recursive cleanup paths require substrate not yet
1521/// wired (mypgrp, jobtab[] reference); doc-pinned for follow-up.
1522pub fn deletejob(jn: &mut job, disowning: bool) {
1523 // c:1512
1524 // c:1514 — `deletefilelist(jn->filelist, disowning);`. When
1525 // disowning, files are NOT deleted from disk; the filelist entries
1526 // are simply dropped.
1527 deletefilelist(jn, disowning);
1528 // c:1515-1518 — `if (jn->stat & STAT_ATTACH) { attachtty(mypgrp);
1529 // adjustwinsize(0); }`. `attachtty(mypgrp)` is the
1530 // canonical `tcsetpgrp(0, mypgrp)` (the same pattern used inline at
1531 // jobs.rs:2503/2527). `adjustwinsize(0)` re-reads $LINES/$COLUMNS
1532 // from TIOCGWINSZ; on Rust we route through the canonical utils
1533 // adjustcolumns/adjustlines which lazy-evaluate on demand, so the
1534 // call is a no-op (the next adjust* read picks up the new pgrp).
1535 if (jn.stat & STAT_ATTACH) != 0 {
1536 // c:1515
1537 #[cfg(unix)]
1538 unsafe {
1539 let pgrp = crate::ported::modules::clone::mypgrp.load(Ordering::Relaxed);
1540 if pgrp > 0 {
1541 libc::tcsetpgrp(0, pgrp); // c:1516 attachtty(mypgrp)
1542 }
1543 }
1544 // c:1517 — `adjustwinsize(0);` — Rust adjust* are lazy-read.
1545 }
1546 // c:1519-1523 — `if (jn->stat & STAT_SUPERJOB) { job jno = jobtab +
1547 // jn->other; if (jno->stat & STAT_SUBJOB)
1548 // jno->stat |= STAT_SUBJOB_ORPHANED; }`.
1549 if (jn.stat & STAT_SUPERJOB) != 0 {
1550 // c:1519
1551 let other = jn.other as usize;
1552 if let Some(tab) = JOBTAB.get() {
1553 // c:1520 jobtab + jn->other
1554 if let Ok(mut jobs) = tab.lock() {
1555 if let Some(jno) = jobs.get_mut(other) {
1556 if (jno.stat & STAT_SUBJOB) != 0 {
1557 // c:1521
1558 jno.stat |= STAT_SUBJOB_ORPHANED; // c:1522
1559 }
1560 }
1561 }
1562 }
1563 }
1564 // c:1525 — `freejob(jn, 1);` full reset of all per-job state.
1565 freejob(jn, true);
1566}
1567
1568/// Add process to job (from jobs.c addproc lines 1537-1597)
1569/// Port of `addproc(pid_t pid, char *text, int aux, struct timespec
1570/// *bgtime, int gleader, int list_pipe_job_used)` from `Src/jobs.c:1538`.
1571///
1572/// The C call site at exec.c:2853 passes the entersubsh_ret-filled
1573/// gleader/list_pipe_job from the child via the synch pipe. Rust mirrors
1574/// the full signature; legacy callers pass `None`/`-1`.
1575pub fn addproc(
1576 job: &mut job,
1577 pid: i32,
1578 text: &str,
1579 aux: bool,
1580 bgtime: Option<std::time::Instant>,
1581 gleader: i32,
1582 list_pipe_job_used: i32,
1583) {
1584 // c:1538
1585 let proc = process::new(pid);
1586 let proc = process {
1587 pid,
1588 status: SP_RUNNING,
1589 text: text.to_string(),
1590 bgtime, // c:1248 — `bgtime` field from struct timespec arg.
1591 ..proc
1592 };
1593
1594 if aux {
1595 job.auxprocs.push(proc);
1596 } else {
1597 // c:1565-1568 — `if (gleader != -1) jn->gleader = gleader;`
1598 if gleader != -1 {
1599 job.gleader = gleader;
1600 } else if job.gleader == 0 {
1601 job.gleader = pid;
1602 }
1603 // c:1570 — `if (list_pipe_job_used != -1) jobtab[list_pipe_job_used].other = thisjob;`
1604 // Stored on the process via list_pipe_job (the C field is
1605 // tracked back via jobtab[list_pipe_job_used].other; the
1606 // simpler approach here is to ignore unless needed).
1607 let _ = list_pipe_job_used;
1608 job.procs.push(proc);
1609 }
1610
1611 job.stat &= !stat::DONE;
1612}
1613
1614/// Port of `havefiles()` from `Src/jobs.c:1605`.
1615///
1616/// C body:
1617/// ```c
1618/// for (i = 1; i <= maxjob; i++)
1619/// if (jobtab[i].stat && jobtab[i].filelist &&
1620/// peekfirst(jobtab[i].filelist))
1621/// return 1;
1622/// return 0;
1623/// ```
1624///
1625/// Returns true if any in-use job in the table has a non-empty
1626/// filelist. Walks the whole table — the previous Rust port took
1627/// a single `&job` and returned `!job.filelist.is_empty()`, which
1628/// is the wrong shape (C iterates).
1629pub fn havefiles(jobtab: &[job]) -> bool {
1630 // c:1605
1631 jobtab.iter().any(|j| j.stat != 0 && !j.filelist.is_empty())
1632}
1633
1634// Wait for a particular process. // c:1627
1635// wait_cmd indicates this is from the interactive wait command, // c:1627
1636// in which case the behaviour is a little different: the command // c:1627
1637// itself can be interrupted by a trapped signal. // c:1627
1638/// Wait for a specific PID (from jobs.c waitforpid lines 1627-1663)
1639pub fn waitforpid(pid: i32) -> Option<i32> {
1640 // c:1627
1641 #[cfg(unix)]
1642 {
1643 loop {
1644 let mut status: i32 = 0;
1645 let result = unsafe { libc::waitpid(pid, &mut status, 0) };
1646 if result == pid {
1647 if libc::WIFEXITED(status) {
1648 return Some(libc::WEXITSTATUS(status));
1649 } else if libc::WIFSIGNALED(status) {
1650 return Some(128 + libc::WTERMSIG(status));
1651 } else if libc::WIFSTOPPED(status) {
1652 return None;
1653 }
1654 } else if result == -1 {
1655 return None;
1656 }
1657 }
1658 }
1659 #[cfg(not(unix))]
1660 {
1661 let _ = pid;
1662 None
1663 }
1664}
1665
1666/// Port of `zwaitjob(int job, int wait_cmd)` from `Src/jobs.c:1673`.
1667///
1668/// `wait_cmd` is the "from interactive `wait` builtin" flag. Threads
1669/// through `queue_traps(wait_cmd)` so signal-trap firing is allowed
1670/// inside the wait, and through `signal_suspend(SIGCHLD, wait_cmd)`
1671/// so trapped non-CHLD signals can interrupt the suspend (returning
1672/// `128 + last_signal` so the wait builtin propagates the interrupt).
1673///
1674/// Body uses the canonical SIGCHLD-driven async pattern: signal_suspend
1675/// blocks until the SIGCHLD handler (signals.rs::zhandler) reaps via
1676/// wait_for_processes + routes through update_bg_job, which sets
1677/// STAT_DONE / STAT_STOPPED on the job. The loop checks job.stat
1678/// after each wake. Mirrors `Src/jobs.c:1673-1750`.
1679pub fn zwaitjob(job: &mut job, wait_cmd: i32) -> Option<i32> {
1680 // c:1673
1681 if job.procs.is_empty() && job.auxprocs.is_empty() {
1682 // c:1736-1740 — no procs: deletejob + pipestats[0]=lastval and return.
1683 return Some(0);
1684 }
1685
1686 use crate::ported::utils::errflag;
1687 use crate::ported::zsh_h::{ERRFLAG_ERROR, INTERACTIVE, STAT_DONE, STAT_STOPPED, ZSIG_TRAPPED};
1688
1689 // c:1675 — `int q = queue_signal_level();`
1690 let q = crate::ported::signals_h::queue_signal_level();
1691 // c:1678 — `child_block();`
1692 crate::ported::signals_h::child_block();
1693 // c:1679 — `queue_traps(wait_cmd);`
1694 crate::ported::signals::queue_traps(wait_cmd);
1695 // c:1680 — `dont_queue_signals();`
1696 crate::ported::signals_h::dont_queue_signals();
1697
1698 // c:1682 — `jn->stat |= STAT_LOCKED;`
1699 job.stat |= crate::ported::zsh_h::STAT_LOCKED;
1700 // c:1683-1684 — STAT_CHANGED → printjob (deferred — needs jobtab index).
1701 // c:1685-1697 — pipecleanfilelist for proc-subst fds.
1702 if !job.filelist.is_empty() {
1703 crate::ported::jobs::pipecleanfilelist(job, false);
1704 }
1705
1706 // c:1698-1735 — main wait loop.
1707 let interact = isset(INTERACTIVE);
1708 loop {
1709 // c:1698 — `while (!(errflag & ERRFLAG_ERROR) && jn->stat &&
1710 // !(jn->stat & STAT_DONE) &&
1711 // !(interact && (jn->stat & STAT_STOPPED)))`
1712 if (errflag.load(std::sync::atomic::Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
1713 break;
1714 }
1715 if job.stat == 0 {
1716 break;
1717 }
1718 if (job.stat & STAT_DONE) != 0 {
1719 break;
1720 }
1721 if interact && (job.stat & STAT_STOPPED) != 0 {
1722 break;
1723 }
1724
1725 // c:1701 — `signal_suspend(SIGCHLD, wait_cmd);` — block until
1726 // SIGCHLD; handler routes through update_bg_job which sets
1727 // STAT_DONE/STOPPED on `job`.
1728 let _ = crate::ported::signals::signal_suspend(libc::SIGCHLD, wait_cmd != 0);
1729
1730 // c:1702-1708 — `if (last_signal != SIGCHLD && wait_cmd &&
1731 // last_signal >= 0 && sigtrapped[ls] & ZSIG_TRAPPED)
1732 // { return 128 + last_signal; }`
1733 let ls = crate::ported::signals::last_signal.load(std::sync::atomic::Ordering::Relaxed);
1734 if ls != libc::SIGCHLD && wait_cmd != 0 && ls >= 0 {
1735 let trapped_flag = {
1736 let guard = crate::ported::signals::sigtrapped.lock().unwrap();
1737 guard.get(ls as usize).copied().unwrap_or(0)
1738 };
1739 if (trapped_flag & ZSIG_TRAPPED) != 0 {
1740 // c:1705-1707 — builtin wait interrupted by trapped signal.
1741 crate::ported::signals_h::restore_queue_signals(q);
1742 crate::ported::signals::unqueue_traps();
1743 crate::ported::signals_h::child_unblock();
1744 return Some(128 + ls); // c:1707
1745 }
1746 }
1747 // c:1729-1730 — `if (subsh) killjb(jn, SIGCONT);` — keep stopped
1748 // grandchildren running when we ourselves are a subshell.
1749 if crate::ported::exec::subsh.load(std::sync::atomic::Ordering::Relaxed) != 0 {
1750 // killjb wants &mut [job]; we have &mut job here. Inline the
1751 // SIGCONT via killpg on the job's gleader if set.
1752 if job.gleader != 0 {
1753 unsafe {
1754 libc::killpg(job.gleader, libc::SIGCONT);
1755 }
1756 }
1757 }
1758 // c:1731-1733 — STAT_SUPERJOB handle_sub deferred (sub-job
1759 // dispatch is jobtab-index-keyed; needs the live jobtab access).
1760 // Re-block before next suspend so SIGCHLD pump isn't lost.
1761 crate::ported::signals_h::child_block();
1762 }
1763
1764 // c:1741-1744 — restore + return 0.
1765 crate::ported::signals_h::restore_queue_signals(q);
1766 crate::ported::signals::unqueue_traps();
1767 crate::ported::signals_h::child_unblock();
1768 // last_status read for the legacy caller — derive from procs.
1769 let last_status = job.procs.last().map(|p| p.exit_status()).unwrap_or(0);
1770 Some(last_status) // c:1745
1771}
1772
1773// wait for running job to finish // c:1763
1774/// Wait for all foreground jobs to finish (from jobs.c waitjobs)
1775pub fn waitjobs(jobtab: &mut [job], thisjob: usize) {
1776 // c:1763
1777 if thisjob < jobtab.len() {
1778 while !jobtab[thisjob].is_done() && !jobtab[thisjob].is_stopped() {
1779 #[cfg(unix)]
1780 {
1781 let mut status: i32 = 0;
1782 let pid = unsafe { libc::waitpid(-1, &mut status, libc::WUNTRACED) };
1783 if pid > 0 {
1784 update_bg_job(jobtab, pid, status);
1785 } else {
1786 break;
1787 }
1788 }
1789 #[cfg(not(unix))]
1790 {
1791 break;
1792 }
1793 }
1794 }
1795}
1796
1797/// Port of `clearjobtab(int monitor)` from `Src/jobs.c:1780`.
1798///
1799/// C signature: `void clearjobtab(int monitor)`. Body walks the
1800/// global `jobtab[1..=maxjob]` and either freejob's each entry
1801/// (POSIX mode or non-monitor) or saves a copy into `oldjobtab`
1802/// (non-POSIX, monitor=1 — used by `jobs -c` later). Then zeros
1803/// the live table and re-`initjob`s the placeholder slot used
1804/// for non-job-control work like multios.
1805///
1806/// Rust port: takes the JobTable by &mut (no global). The
1807// clear job table when entering subshells // c:1780
1808/// `monitor` flag gates the oldjobtab save; the save itself is
1809/// pending until JobTable's internal `Vec<Option<JobInfo>>`
1810/// model is reconciled with C's `struct job *jobtab` so the
1811/// snapshot can be taken. The non-snapshot core (clear in-use
1812/// jobs, reset cursor) is faithful.
1813// Rust idiom replacement: JobTable's private Vec model is rebuilt
1814// by the executor on subshell entry (`JobTable::new()`), so the C
1815// `oldjobtab` snapshot + per-slot reset loop is structurally
1816// replaced — no public reset method is needed.
1817/// `clearjobtab` — see implementation.
1818pub fn clearjobtab(table: &mut JobTable, monitor: i32) {
1819 // c:1780
1820 let _ = table; // legacy executor-side handle, unused now
1821 let posix_jobs = isset(POSIXJOBS); // c:1786
1822 // c:1786-1787 — `if (isset(POSIXJOBS)) oldmaxjob = 0;`.
1823 if posix_jobs {
1824 if let Some(om) = OLDMAXJOB.get() {
1825 if let Ok(mut o) = om.lock() {
1826 *o = 0;
1827 }
1828 }
1829 }
1830 let tab = match JOBTAB.get() {
1831 Some(t) => t,
1832 None => return,
1833 };
1834 let mut jobs = match tab.lock() {
1835 Ok(g) => g,
1836 Err(_) => return,
1837 };
1838 // c:1788-1797 — for (i = 1; i <= maxjob; i++).
1839 let maxjob = jobs.len();
1840 let mut new_oldmax: usize = 0;
1841 for i in 1..maxjob {
1842 // c:1788
1843 if jobs[i].stat == 0 {
1844 continue;
1845 }
1846 // c:1794-1795 — `if (monitor && !POSIXJOBS && jobtab[i].stat)
1847 // oldmaxjob = i+1;`
1848 if monitor != 0 && !posix_jobs {
1849 // c:1794
1850 new_oldmax = i + 1; // c:1795
1851 } else if (jobs[i].stat & STAT_INUSE) != 0 {
1852 // c:1796
1853 // c:1797 — `freejob(jobtab+i, 0);`.
1854 freejob(&mut jobs[i], false); // c:1797
1855 }
1856 }
1857 // c:1800-1817 — `if (monitor && oldmaxjob) { snapshot to oldjobtab }`.
1858 if monitor != 0 && new_oldmax > 0 {
1859 // c:1800
1860 let mut snap: Vec<job> = jobs[..new_oldmax].iter().cloned().collect(); // c:1803-1806
1861 // c:1809-1810 — `if (thisjob != -1 && thisjob < oldmaxjob)
1862 // memset(oldjobtab+thisjob, 0, ...)`.
1863 let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
1864 if thisjob >= 0 && (thisjob as usize) < new_oldmax {
1865 // c:1809
1866 // Zero the slot — Rust uses Default::default().
1867 snap[thisjob as usize] = job::default(); // c:1810
1868 }
1869 // c:1816 — `--oldmaxjob;` C decrement before exposure.
1870 if let Some(om) = OLDMAXJOB.get() {
1871 if let Ok(mut o) = om.lock() {
1872 *o = new_oldmax.saturating_sub(1); // c:1816
1873 }
1874 } else {
1875 *OLDMAXJOB.get_or_init(|| Mutex::new(0)).lock().unwrap() = new_oldmax.saturating_sub(1);
1876 }
1877 *OLDJOBTAB
1878 .get_or_init(|| Mutex::new(Vec::new()))
1879 .lock()
1880 .unwrap() = snap; // c:1804
1881 }
1882 // c:1818-1819 — `memset(jobtab, 0, jobtabsize * sizeof(struct job));
1883 // maxjob = 0;` — zero out the live table.
1884 jobs.clear();
1885 jobs.push(job::new()); // slot 0 — the shell's own entry
1886 *MAXJOB.get_or_init(|| Mutex::new(0)).lock().unwrap() = 0; // c:1819
1887 // c:1821-1828 — "Although we don't have job control in subshells,
1888 // we sometimes need control structures for other purposes such as
1889 // multios. Grab a job for this purpose." `thisjob = initjob();`
1890 let control = initjob(&mut jobs); // c:1828
1891 *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = control as i32;
1892}
1893
1894/// Port of `clearoldjobtab()` from `Src/jobs.c:1835`.
1895///
1896/// C body:
1897/// ```c
1898/// if (oldjobtab) free(oldjobtab);
1899/// oldjobtab = NULL;
1900/// oldmaxjob = 0;
1901/// ```
1902///
1903/// Frees the snapshot of the previous-state job table that
1904/// `jobs -c` (jobs-changed) compares against. The previous Rust
1905/// port retained INUSE entries in `jobtab` directly — wrong
1906/// target. The real C function operates on the `oldjobtab`
1907/// global, not the live `jobtab`.
1908///
1909/// Rust port clears the OLDJOBTAB module static.
1910pub fn clearoldjobtab() {
1911 *OLDJOBTAB
1912 .get_or_init(|| Mutex::new(Vec::new()))
1913 .lock()
1914 .expect("oldjobtab poisoned") = Vec::new();
1915 *OLDMAXJOB
1916 .get_or_init(|| Mutex::new(0))
1917 .lock()
1918 .expect("oldmaxjob poisoned") = 0;
1919}
1920
1921// Get a free entry in the job table and initialize it. // c:1862
1922/// Initialize a new job entry (from jobs.c initjob)
1923///
1924/// c:Src/jobs.c:1862-1875 — C: `for (i = 1; i <= maxjob; i++)` starts
1925/// at index 1; index 0 is the shell's own slot and must never be
1926/// returned to a child-job caller. The Rust port previously walked
1927/// from index 0 via `enumerate()`, corrupting parent-shell job
1928/// tracking when jobtab[0] was empty.
1929pub fn initjob(jobtab: &mut Vec<job>) -> usize {
1930 // c:1862
1931 // Ensure jobtab has slot 0 reserved for the shell (matches C's
1932 // `jobtab[0]` shell-process slot at jobs.c:79).
1933 if jobtab.is_empty() {
1934 jobtab.push(job::new());
1935 }
1936 // Find an empty slot or add a new one — START AT INDEX 1.
1937 for i in 1..jobtab.len() {
1938 if (jobtab[i].stat & stat::INUSE) == 0 {
1939 return initnewjob(jobtab, i); // c:1868
1940 }
1941 }
1942 // Expand table — C path c:1869-1872 (maxjob+1 within jobtabsize,
1943 // else expandjobtab). Rust's Vec grows on demand.
1944 let idx = jobtab.len();
1945 jobtab.push(job::new());
1946 initnewjob(jobtab, idx)
1947}
1948
1949/// Direct port of `static int initnewjob(int i)` from `Src/jobs.c:1843`.
1950///
1951/// C body:
1952/// ```c
1953/// jobtab[i].stat = STAT_INUSE;
1954/// if (jobtab[i].pwd) { zsfree(jobtab[i].pwd); jobtab[i].pwd = NULL; }
1955/// jobtab[i].gleader = 0;
1956/// if (i > maxjob) maxjob = i;
1957/// return i;
1958/// ```
1959/// MAXJOB is the scan bound for setcurjob/setprevjob/getjob/
1960/// selectjobtab; without the bump those walks see an empty table even
1961/// when JOBTAB has live entries.
1962/// WARNING: param names don't match C — Rust=(jobtab, i) vs C=(i);
1963/// C reads the jobtab global, Rust callers pass the locked slice.
1964fn initnewjob(jobtab: &mut [job], i: usize) -> usize {
1965 // c:1843
1966 jobtab[i] = job::new();
1967 jobtab[i].stat = stat::INUSE; // c:1845
1968 jobtab[i].pwd = None; // c:1846-1849
1969 jobtab[i].gleader = 0; // c:1850
1970 let mut mj = MAXJOB
1971 .get_or_init(|| Mutex::new(0))
1972 .lock()
1973 .expect("maxjob poisoned");
1974 if i > *mj {
1975 // c:1852-1853
1976 *mj = i;
1977 }
1978 i // c:1855
1979}
1980
1981/// Port of `void setjobpwd(void)` from `Src/jobs.c:1881`.
1982///
1983/// C body:
1984/// ```c
1985/// int i;
1986/// for (i = 1; i <= maxjob; i++)
1987/// if (jobtab[i].stat && !jobtab[i].pwd)
1988/// jobtab[i].pwd = ztrdup(pwd);
1989/// ```
1990///
1991/// Walks every IN-USE job and stamps its `pwd` with the current
1992/// shell `pwd` (from `Src/builtin.c:1240` after `bin_cd`). The
1993/// previous Rust port took a `&mut job` ref and was a no-op (just
1994/// captured cwd then dropped it) — every `cd` left the in-flight
1995/// job's pwd unset, and `jobs` output showed empty `(pwd: )` for
1996/// jobs that started before the cd.
1997///
1998/// The fix walks `JOBTAB` and writes `pwd` to every job whose stat
1999/// is non-zero (INUSE) and whose pwd is still None. The shell
2000/// pwd is read from the canonical `params::pwdgetfn` accessor —
2001/// matches C's read of the `pwd` global at c:1888.
2002pub fn setjobpwd() {
2003 // c:1881
2004 // c:1888 — `pwd` is the canonical shell-state global from
2005 // `Src/params.c:108`. Rust reads it via the paramtab-backed
2006 // `getsparam("PWD")` which is the canonical accessor mirrored
2007 // throughout the codebase (prompt.rs, subst.rs, builtin.rs).
2008 let pwd = getsparam("PWD").unwrap_or_default(); // c:1888 pwd
2009 let tab = JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
2010 let mut tab = tab.lock().expect("jobtab poisoned");
2011 // c:1886 — `for (i = 1; i <= maxjob; i++)`. Skip index 0 (the
2012 // shell itself).
2013 for job in tab.iter_mut().skip(1) {
2014 // c:1887 — `if (jobtab[i].stat && !jobtab[i].pwd)`.
2015 if job.stat != 0 && job.pwd.is_none() {
2016 job.pwd = Some(pwd.clone()); // c:1888
2017 }
2018 }
2019}
2020
2021/// Print pids for `&` background jobs (`spawnjob`).
2022/// Port of `void spawnjob(void)` from `Src/jobs.c:1894`.
2023pub fn spawnjob() {
2024 // c:1894
2025 let thisjob_idx = *THISJOB
2026 .get_or_init(|| Mutex::new(-1))
2027 .lock()
2028 .expect("thisjob poisoned");
2029 // c:1898 — DPUTS(thisjob == -1, "No valid job in spawnjob.")
2030 DPUTS!(thisjob_idx == -1, "No valid job in spawnjob."); // c:1898
2031 if thisjob_idx < 0 {
2032 return;
2033 }
2034 let thisjob = thisjob_idx as usize;
2035
2036 // c:1900 — `if (!subsh) {` — when this isn't a subshell.
2037 // `subsh` global tracks subshell-fork depth; mirror via FORKLEVEL
2038 // (0 = top-level shell) plus SUBSHELL_DEPTH, the depth counter the
2039 // fusevm in-process `(...)` host bumps in subshell_begin. C's
2040 // forked subshell sets `subsh` in entersubsh (Src/exec.c:1154);
2041 // the in-process model never calls entersubsh, so without this
2042 // a `(cmd &)` would promote the job to the parent's curjob —
2043 // making `(sleep 1 & disown)` silently succeed where zsh errors
2044 // "no current job". Bug #462.
2045 let in_subsh = crate::ported::exec::FORKLEVEL.load(Ordering::Relaxed) > 0
2046 || crate::ported::builtin::SUBSHELL_DEPTH.load(Ordering::Relaxed) > 0;
2047 if !in_subsh {
2048 // c:1901-1903 — `if (curjob == -1 || !(jobtab[curjob].stat & STAT_STOPPED))
2049 // { curjob = thisjob; setprevjob(); }`
2050 // c:1904-1905 — else if prevjob also not stopped, prevjob = thisjob.
2051 let curjob = *CURJOB
2052 .get_or_init(|| Mutex::new(-1))
2053 .lock()
2054 .expect("curjob poisoned");
2055 let cur_stopped = if curjob >= 0 {
2056 let tab = JOBTAB
2057 .get_or_init(|| Mutex::new(Vec::new()))
2058 .lock()
2059 .expect("jobtab poisoned");
2060 tab.get(curjob as usize)
2061 .map(|j| (j.stat & stat::STOPPED) != 0)
2062 .unwrap_or(false)
2063 } else {
2064 false
2065 };
2066 if curjob < 0 || !cur_stopped {
2067 if let Ok(mut cj) = CURJOB.get_or_init(|| Mutex::new(-1)).lock() {
2068 *cj = thisjob_idx; // c:1902
2069 }
2070 setprevjob(); // c:1903
2071 } else {
2072 // c:1904-1905
2073 let prevjob = *PREVJOB
2074 .get_or_init(|| Mutex::new(-1))
2075 .lock()
2076 .expect("prevjob poisoned");
2077 let prev_stopped = if prevjob >= 0 {
2078 let tab = JOBTAB
2079 .get_or_init(|| Mutex::new(Vec::new()))
2080 .lock()
2081 .expect("jobtab poisoned");
2082 tab.get(prevjob as usize)
2083 .map(|j| (j.stat & stat::STOPPED) != 0)
2084 .unwrap_or(false)
2085 } else {
2086 false
2087 };
2088 if prevjob < 0 || !prev_stopped {
2089 if let Ok(mut pj) = PREVJOB.get_or_init(|| Mutex::new(-1)).lock() {
2090 *pj = thisjob_idx; // c:1905
2091 }
2092 }
2093 }
2094 // c:1906-1913 — `if (jobbing && jobtab[thisjob].procs)`
2095 // print "[N] pid1 pid2 ..." to shout/stderr.
2096 if isset(MONITOR) {
2097 let tab = JOBTAB
2098 .get_or_init(|| Mutex::new(Vec::new()))
2099 .lock()
2100 .expect("jobtab poisoned");
2101 if let Some(job) = tab.get(thisjob) {
2102 if !job.procs.is_empty() {
2103 let mut line = format!("[{}]", thisjob_idx);
2104 for p in job.procs.iter() {
2105 line.push_str(&format!(" {}", p.pid));
2106 }
2107 line.push('\n');
2108 eprint!("{}", line); // c:1907-1911
2109 }
2110 }
2111 }
2112 }
2113 // c:1915-1920 — `if (!hasprocs(thisjob)) deletejob(jobtab+thisjob, 0);
2114 // else { STAT_LOCKED; pipecleanfilelist(...); }`
2115 let need_delete: bool;
2116 {
2117 let tab = JOBTAB
2118 .get_or_init(|| Mutex::new(Vec::new()))
2119 .lock()
2120 .expect("jobtab poisoned");
2121 need_delete = tab
2122 .get(thisjob)
2123 .map(|j| j.procs.is_empty() && j.auxprocs.is_empty())
2124 .unwrap_or(true);
2125 }
2126 if need_delete {
2127 let mut tab = JOBTAB
2128 .get_or_init(|| Mutex::new(Vec::new()))
2129 .lock()
2130 .expect("jobtab poisoned");
2131 if let Some(j) = tab.get_mut(thisjob) {
2132 deletejob(j, false); // c:1916
2133 }
2134 } else {
2135 let mut tab = JOBTAB
2136 .get_or_init(|| Mutex::new(Vec::new()))
2137 .lock()
2138 .expect("jobtab poisoned");
2139 if let Some(j) = tab.get_mut(thisjob) {
2140 j.stat |= stat::LOCKED; // c:1918
2141 pipecleanfilelist(j, false); // c:1919
2142 }
2143 }
2144 // c:1921 — thisjob = -1;
2145 if let Ok(mut tj) = THISJOB.get_or_init(|| Mutex::new(-1)).lock() {
2146 *tj = -1;
2147 }
2148}
2149
2150// `ChildTimes` struct deleted — folded into the canonical `timeinfo`
2151// at the top of this file. C uses `child_times_t` (typedef onto
2152// `struct rusage` or `struct timeinfo` per `Src/zsh.h:1112-1114`).
2153
2154/// Port of `void shelltime(child_times_t *shell, child_times_t *kids,
2155/// struct timespec *then, int delta)` from `Src/jobs.c:1926-1987`.
2156///
2157/// Records or prints the shell's RUSAGE_SELF + RUSAGE_CHILDREN times.
2158/// Side-effecting:
2159/// - If `shell` is `Some` and `delta == 0`: snapshot current self
2160/// rusage into `*shell` (no print).
2161/// - If `shell` is `Some` and `delta != 0`: compute delta from
2162/// `*shell` to now (no print).
2163/// - If `shell` is `None` and `delta == 0`: print "shell ..." line.
2164/// - Same pattern for `kids` against RUSAGE_CHILDREN.
2165/// - `then` similarly: when `None` and `delta == 0`, use as the
2166/// monotonic timestamp slot; when `Some + delta`, compute the
2167/// elapsed real time as `now - *then`.
2168///
2169/// C body c:1926-1987 maps closely:
2170/// - c:1934 — zgettime_monotonic_if_available(&now)
2171/// - c:1937 — getrusage(RUSAGE_SELF, &ti)
2172/// - c:1944-1955 — handle `shell` save / delta
2173/// - c:1956-1962 — compute `dtimespec` from `then` and `now` /
2174/// shtimer
2175/// - c:1964-1965 — `if (!delta == !shell) printtime("shell")`
2176/// - c:1968 — getrusage(RUSAGE_CHILDREN, &ti)
2177/// - c:1973-1984 — handle `kids` save / delta
2178/// - c:1985-1986 — `if (!delta == !kids) printtime("children")`
2179#[cfg(unix)]
2180pub fn shelltime(
2181 shell: Option<&mut timeinfo>,
2182 kids: Option<&mut timeinfo>,
2183 then: Option<&mut std::time::Instant>,
2184 delta: i32,
2185) {
2186 // c:1926
2187 // c:1934 — `zgettime_monotonic_if_available(&now);`. Use Instant
2188 // for monotonic time.
2189 let now = std::time::Instant::now();
2190 // c:1937 — `getrusage(RUSAGE_SELF, &ti);`. Self timings.
2191 let mut ti: timeinfo = {
2192 let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
2193 if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } == 0 {
2194 timeinfo::from_rusage(&usage)
2195 } else {
2196 timeinfo::default()
2197 }
2198 };
2199
2200 let shell_present = shell.is_some();
2201 // c:1944-1955 — `if (shell) { if (delta) dtime_tv(...); else *shell = ti; }`.
2202 if let Some(s) = shell {
2203 // c:1944
2204 if delta != 0 {
2205 // c:1945 — delta-compute by subtracting saved values.
2206 // C uses dtime_tv to subtract timespec. timeinfo holds
2207 // raw rusage members; subtract user/sys time directly.
2208 ti.ut = ti.ut.saturating_sub(s.ut); // c:1947 dtime_tv(ru_utime, shell->ru_utime, ti.ru_utime)
2209 ti.st = ti.st.saturating_sub(s.st); // c:1948
2210 } else {
2211 // c:1953-1954 — snapshot current `ti` into `*shell`.
2212 *s = ti.clone();
2213 }
2214 }
2215
2216 // c:1956-1962 — compute `dtimespec` (real elapsed time).
2217 let dtime: std::time::Duration = if delta != 0 {
2218 // c:1957 — `dtime_ts(&dtimespec, then, &now)`. The C body
2219 // requires `then` to be Some for the delta path (set on a
2220 // prior delta=0 call).
2221 match then {
2222 Some(t) => dtime_ts(t, &now), // c:1957
2223 None => std::time::Duration::ZERO,
2224 }
2225 } else {
2226 // c:1959-1961 — `if (then) *then = now;` then
2227 // `dtime_ts(&dtimespec, &shtimer, &now);`.
2228 if let Some(t) = then {
2229 *t = now;
2230 }
2231 // c:1961 — `dtime_ts(&dtimespec, &shtimer, &now)`. Rust's
2232 // `params::shtimer_lock()` is the analog of C's `shtimer`
2233 // global (`struct timespec` set at shell start). Compute
2234 // elapsed time as now - shtimer.
2235 let shtimer_dur = *crate::ported::params::shtimer_lock()
2236 .lock()
2237 .expect("shtimer poisoned");
2238 let now_dur = std::time::SystemTime::now()
2239 .duration_since(std::time::UNIX_EPOCH)
2240 .unwrap_or_default();
2241 if now_dur > shtimer_dur {
2242 now_dur - shtimer_dur
2243 } else {
2244 std::time::Duration::ZERO
2245 }
2246 };
2247
2248 // c:1964 — `if (!delta == !shell) printtime("shell")`.
2249 // The negation pair: print when (delta==0 && shell.is_none()) OR
2250 // (delta!=0 && shell.is_some()).
2251 if (delta == 0) == !shell_present {
2252 // c:1964
2253 let real_secs = dtime.as_secs_f64();
2254 // c:1965 — `printtime(&dtimespec, &ti, "shell")`.
2255 let timefmt = crate::ported::params::getsparam("TIMEFMT")
2256 .unwrap_or_else(|| "%J %U user %S system %P cpu %*E total".to_string());
2257 let line = printtime(real_secs, &ti, &timefmt, "shell"); // c:1965
2258 eprintln!("{}", line);
2259 }
2260
2261 // c:1968 — `getrusage(RUSAGE_CHILDREN, &ti);`. Children timings.
2262 let mut tc: timeinfo = {
2263 let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
2264 if unsafe { libc::getrusage(libc::RUSAGE_CHILDREN, &mut usage) } == 0 {
2265 timeinfo::from_rusage(&usage)
2266 } else {
2267 timeinfo::default()
2268 }
2269 };
2270
2271 let kids_present = kids.is_some();
2272 // c:1973-1984 — `if (kids) { ... }` symmetric to shell.
2273 if let Some(k) = kids {
2274 // c:1973
2275 if delta != 0 {
2276 tc.ut = tc.ut.saturating_sub(k.ut); // c:1976
2277 tc.st = tc.st.saturating_sub(k.st); // c:1977
2278 } else {
2279 *k = tc.clone(); // c:1983
2280 }
2281 }
2282
2283 // c:1985-1986 — `if (!delta == !kids) printtime("children")`.
2284 if (delta == 0) == !kids_present {
2285 // c:1985
2286 let real_secs = dtime.as_secs_f64();
2287 let timefmt = crate::ported::params::getsparam("TIMEFMT")
2288 .unwrap_or_else(|| "%J %U user %S system %P cpu %*E total".to_string());
2289 let line = printtime(real_secs, &tc, &timefmt, "children"); // c:1986
2290 eprintln!("{}", line);
2291 }
2292}
2293
2294/// Non-unix stub matching the C body's #ifdef-gated absence.
2295#[cfg(not(unix))]
2296pub fn shelltime(
2297 _shell: Option<&mut timeinfo>,
2298 _kids: Option<&mut timeinfo>,
2299 _then: Option<&mut std::time::Instant>,
2300 _delta: i32,
2301) {
2302}
2303
2304// see if jobs need printing // c:1993
2305/// Scan jobs and print changed status (from jobs.c scanjobs)
2306pub fn scanjobs(jobtab: &mut [job]) {
2307 // c:1993
2308 // C body:
2309 // ```c
2310 // for (i = 1; i <= maxjob; i++)
2311 // if (jobtab[i].stat & STAT_CHANGED)
2312 // printjob(jobtab + i, !!isset(LONGLISTJOBS), 1);
2313 // ```
2314 // printjob with synch=1 prints only when `(interact || synch) &&
2315 // jobbing && ...` (c:1236-1238) — so in a non-MONITOR shell the
2316 // call is a silent pass whose tail (c:1350-1363) deletes each
2317 // finished entry and clears STAT_CHANGED otherwise (c:1364).
2318 // WARNING: param names don't match C — Rust=(jobtab) vs C=(void);
2319 // C reads the jobtab global, Rust callers pass the locked slice.
2320 let long_list = isset(LONGLISTJOBS);
2321 for i in 1..jobtab.len() {
2322 // c:1998
2323 if (jobtab[i].stat & stat::CHANGED) != 0 {
2324 // c:1999
2325 if crate::ported::zsh_h::jobbing() {
2326 // c:1236-1238 print gate
2327 let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
2328 let prevjob = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
2329 let s = printjob(
2330 &jobtab[i],
2331 i,
2332 long_list as i32,
2333 if curjob >= 0 { Some(curjob as usize) } else { None },
2334 if prevjob >= 0 { Some(prevjob as usize) } else { None },
2335 ); // c:2000
2336 if !s.is_empty() {
2337 eprintln!("{}", s);
2338 }
2339 }
2340 if (jobtab[i].stat & stat::DONE) != 0 {
2341 // c:1350-1363 — printjob's done-delete tail.
2342 crate::exec_jobs::printjob_delete_tail(jobtab, i);
2343 } else {
2344 jobtab[i].stat &= !stat::CHANGED; // c:1364
2345 }
2346 }
2347 }
2348}
2349
2350/// Port of `isanum(char *s)` from `Src/jobs.c:2010`.
2351///
2352/// C body:
2353/// ```c
2354/// if (*s == '\0') return 0;
2355/// while (*s == '-' || idigit(*s)) s++;
2356/// return *s == '\0';
2357/// ```
2358///
2359/// Returns true if `s` is non-empty and consists entirely of
2360/// `'-'` or ASCII digits. Used by `getjob` to determine whether a
2361/// jobspec is `%N` (numeric, with optional leading minus) versus
2362/// `%name`. The previous Rust port required all-digits which
2363/// rejected valid jobspecs like `-1` (the previous job).
2364pub fn isanum(s: &str) -> bool {
2365 // c:2010
2366 !s.is_empty() && s.bytes().all(|b| b == b'-' || b.is_ascii_digit())
2367}
2368
2369// Make sure we have a suitable current and previous job set. // c:2023
2370/// Direct port of `void setcurjob(void)` from `Src/jobs.c:2023`.
2371///
2372/// C body:
2373/// ```c
2374/// if (curjob == thisjob ||
2375/// (curjob != -1 && !(jobtab[curjob].stat & STAT_INUSE))) {
2376/// curjob = prevjob;
2377/// setprevjob();
2378/// if (curjob == thisjob ||
2379/// (curjob != -1 && !((jobtab[curjob].stat & STAT_INUSE) &&
2380/// curjob != thisjob))) {
2381/// curjob = prevjob;
2382/// setprevjob();
2383/// }
2384/// }
2385/// ```
2386/// REPAIRS an invalid `curjob` (gone, or equal to the in-flight
2387/// thisjob) by promoting `prevjob`; it does NOT scan for a fresh
2388/// candidate when curjob is -1 — promotion to curjob happens in
2389/// spawnjob (c:1901-1903) and printjob's delete tail (c:1357-1360).
2390/// The previous Rust body picked the highest in-use job
2391/// unconditionally, which resurrected a current job inside subshells
2392/// where zsh reports "no current job" (bug #462 probe
2393/// `(sleep 0.2 & disown)` → rc=1 in zsh).
2394pub fn setcurjob() {
2395 // c:2023
2396 let inuse = |jobno: i32| -> bool {
2397 let tab = JOBTAB
2398 .get_or_init(|| Mutex::new(Vec::new()))
2399 .lock()
2400 .expect("jobtab poisoned");
2401 tab.get(jobno as usize)
2402 .map(|j| (j.stat & stat::INUSE) != 0)
2403 .unwrap_or(false)
2404 };
2405 let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
2406 let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
2407 // c:2025-2026. The `curjob == thisjob` test is guarded with
2408 // `curjob != -1` here: in C, thisjob is never -1 while bin_fg runs
2409 // (execpline c:Src/exec.c:1700 allocates a pipeline job slot before
2410 // any builtin executes), so `-1 == -1` can't trigger the C branch.
2411 // zshrs has no per-pipeline job allocation — thisjob is -1 between
2412 // jobs — and an unguarded -1==-1 would promote prevjob/setprevjob,
2413 // resurrecting a "current job" zsh reports as absent (bug #462).
2414 if (curjob != -1 && curjob == thisjob) || (curjob != -1 && !inuse(curjob)) {
2415 // c:2027-2028 — `curjob = prevjob; setprevjob();`
2416 let pj = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
2417 *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = pj;
2418 setprevjob();
2419 let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
2420 // c:2029-2031 — same -1 guard as above.
2421 if (curjob != -1 && curjob == thisjob)
2422 || (curjob != -1 && !(inuse(curjob) && curjob != thisjob))
2423 {
2424 // c:2032-2033
2425 let pj = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
2426 *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = pj;
2427 setprevjob();
2428 }
2429 }
2430}
2431
2432// Find the job table for reporting jobs // c:2042
2433/// Port of `selectjobtab(job *jtabp, int *jmaxp)` from `Src/jobs.c:2042`.
2434///
2435/// C signature: `mod_export void selectjobtab(job *jtabp, int *jmaxp)`
2436///
2437/// In subshell, uses saved `oldjobtab`/`oldmaxjob`; otherwise uses
2438/// the main `jobtab`/`maxjob` globals. Returns `(table, maxjob)`.
2439/// WARNING: param names don't match C — Rust=() vs C=(jtabp, jmaxp)
2440pub fn selectjobtab() -> (Vec<job>, usize) {
2441 let oldtab = OLDJOBTAB
2442 .get_or_init(|| Mutex::new(Vec::new()))
2443 .lock()
2444 .expect("oldjobtab poisoned");
2445 if !oldtab.is_empty() {
2446 // c:2044
2447 // In subshell --- use saved job table to report // c:2046
2448 let oldmax = *OLDMAXJOB
2449 .get_or_init(|| Mutex::new(0))
2450 .lock()
2451 .expect("oldmaxjob poisoned");
2452 (oldtab.clone(), oldmax) // c:2047-2048
2453 } else {
2454 // Use main job table // c:2052
2455 drop(oldtab); // release lock before acquiring jobtab
2456 let jobtab = JOBTAB
2457 .get_or_init(|| Mutex::new(Vec::new()))
2458 .lock()
2459 .expect("jobtab poisoned");
2460 let maxjob = *MAXJOB
2461 .get_or_init(|| Mutex::new(0))
2462 .lock()
2463 .expect("maxjob poisoned");
2464 (jobtab.clone(), maxjob) // c:2053-2054
2465 }
2466}
2467
2468// `JobPointers` struct deleted — Rust-only aggregate of `curjob`/
2469// `prevjob` (Src/jobs.c:75/80) globals that already live on file
2470// scope as `CURJOB` / `PREVJOB`. `setcurjob` / `setprevjob` now
2471// read/write those directly per the C source.
2472
2473// ---------------------------------------------------------------------------
2474// Missing functions from jobs.c
2475// ---------------------------------------------------------------------------
2476
2477// Convert a job specifier ("%%", "%1", "%foo", "%?bar?", etc.) // c:2063
2478// to a job number. // c:2063
2479/// Port of `getjob(const char *s, const char *prog)` from `Src/jobs.c:2063`.
2480///
2481/// C signature: `mod_export int getjob(const char *s, const char *prog)`
2482///
2483/// Returns job index or -1 on error. `prog` is the program name for
2484/// `zwarnnam` error messages (pass empty string to suppress warnings).
2485pub fn getjob(s: &str, prog: &str) -> i32 {
2486 // c:2063
2487 let mut jobnum: i32; // c:2063
2488 let mymaxjob: i32; // c:2065
2489 let myjobtab: Vec<job>; // c:2066
2490
2491 let (tab, max) = selectjobtab(); // c:2068
2492 myjobtab = tab;
2493 mymaxjob = max as i32;
2494
2495 let curjob = *CURJOB
2496 .get_or_init(|| Mutex::new(-1)) // c:2076
2497 .lock()
2498 .expect("curjob poisoned");
2499 let prevjob = *PREVJOB
2500 .get_or_init(|| Mutex::new(-1)) // c:2087
2501 .lock()
2502 .expect("prevjob poisoned");
2503 let thisjob = *THISJOB
2504 .get_or_init(|| Mutex::new(-1))
2505 .lock()
2506 .expect("thisjob poisoned");
2507 let posixbuiltins = isset(
2508 // c:isset(POSIXBUILTINS)
2509 POSIXBUILTINS,
2510 );
2511
2512 let s_bytes = s.as_bytes();
2513 let mut idx = 0usize;
2514
2515 // if there is no %, treat as a name // c:2070
2516 if s_bytes.is_empty() || s_bytes[0] != b'%' {
2517 // goto jump // c:2072
2518 // anything else is a job name, specified as a string that begins // c:2135
2519 // the job's command // c:2136
2520 if let Some(jn) = findjobnam(s, &myjobtab, mymaxjob, thisjob) {
2521 // c:2137
2522 return jn;
2523 }
2524 // if we get here, it is because none of the above succeeded // c:2141
2525 if !posixbuiltins && !prog.is_empty() {
2526 // c:2143
2527 zwarnnam(prog, &format!("job not found: {}", s)); // c:2144
2528 }
2529 return -1; // c:2145
2530 }
2531 idx += 1; // skip '%' // c:2073
2532
2533 // "%%", "%+" and "%" all represent the current job // c:2074
2534 if idx >= s_bytes.len() || s_bytes[idx] == b'%' || s_bytes[idx] == b'+' {
2535 // c:2075
2536 if curjob == -1 {
2537 // c:2076
2538 if !prog.is_empty() && !posixbuiltins {
2539 // c:2077
2540 zwarnnam(prog, "no current job"); // c:2078
2541 }
2542 return -1; // c:2079-2080
2543 }
2544 return curjob; // c:2082-2083
2545 }
2546 // "%-" represents the previous job // c:2085
2547 if s_bytes[idx] == b'-' {
2548 // c:2086
2549 if prevjob == -1 {
2550 // c:2087
2551 if !prog.is_empty() && !posixbuiltins {
2552 // c:2088
2553 zwarnnam(prog, "no previous job"); // c:2089
2554 }
2555 return -1; // c:2090-2091
2556 }
2557 return prevjob; // c:2093-2094
2558 }
2559 // a digit here means we have a job number // c:2096
2560 if s_bytes[idx].is_ascii_digit() {
2561 // c:2097
2562 let rest = &s[idx..];
2563 jobnum = rest.parse::<i32>().unwrap_or(0); // c:2098 atoi(s)
2564 if jobnum > 0 && jobnum <= mymaxjob {
2565 // c:2099
2566 let ju = jobnum as usize;
2567 if ju < myjobtab.len()
2568 && myjobtab[ju].stat != 0
2569 && (myjobtab[ju].stat & stat::SUBJOB) == 0 // c:2100
2570 && jobnum != thisjob
2571 // c:2107
2572 {
2573 return jobnum; // c:2108-2109
2574 }
2575 }
2576 if !prog.is_empty() && !posixbuiltins {
2577 // c:2111
2578 zwarnnam(prog, &format!("%{}: no such job", rest)); // c:2112
2579 }
2580 return -1; // c:2113-2114
2581 }
2582 // "%?" introduces a search string // c:2116
2583 if s_bytes[idx] == b'?' {
2584 // c:2117
2585 let search = &s[idx + 1..]; // c:2125 s + 1
2586 jobnum = mymaxjob; // c:2120
2587 while jobnum >= 0 {
2588 // c:2120
2589 let ju = jobnum as usize;
2590 if ju < myjobtab.len()
2591 && myjobtab[ju].stat != 0 // c:2121
2592 && (myjobtab[ju].stat & stat::SUBJOB) == 0 // c:2122
2593 && jobnum != thisjob
2594 // c:2123
2595 {
2596 for pn in &myjobtab[ju].procs {
2597 // c:2124
2598 if pn.text.contains(search) {
2599 // c:2125 strstr
2600 return jobnum; // c:2126-2127
2601 }
2602 }
2603 }
2604 jobnum -= 1;
2605 }
2606 if !prog.is_empty() && !posixbuiltins {
2607 // c:2129
2608 // c:Src/jobs.c:2130 — `zwarnnam(prog, "job not found: %s", s)`.
2609 // After the s++ at c:2073, `s` is past the leading `%`. The
2610 // Rust idx-based port must use &s[idx..] not the original s.
2611 // Bug #393.
2612 zwarnnam(prog, &format!("job not found: {}", &s[idx..])); // c:2130
2613 }
2614 return -1; // c:2131-2132
2615 }
2616 // jump: // c:2134
2617 // anything else is a job name, specified as a string that begins // c:2135
2618 // the job's command // c:2136
2619 let rest = &s[idx..];
2620 if let Some(jn) = findjobnam(rest, &myjobtab, mymaxjob, thisjob) {
2621 // c:2137
2622 return jn; // c:2138-2139
2623 }
2624 // if we get here, it is because none of the above succeeded // c:2141
2625 if !posixbuiltins && !prog.is_empty() {
2626 // c:2143
2627 // c:Src/jobs.c:2144 — same `s++` strip — emit the post-`%` name.
2628 // Bug #393.
2629 zwarnnam(prog, &format!("job not found: {}", rest)); // c:2144
2630 }
2631 -1 // c:2145-2147
2632}
2633
2634/// Port of `init_jobs(char **argv, char **envp)` from `Src/jobs.c:2164`.
2635///
2636/// C body allocates the `jobtab[]` array sized to `MAXJOBS_ALLOC`,
2637/// `memset`s to zero, and seeds the `setproctitle`/argv-rewriting
2638/// state used by `jobs -Z`. Rust port pre-allocates the table to
2639/// `MAXJOBS_ALLOC` empty `job` slots so `expandjobtab` doesn't
2640/// need to grow until index 50+ is reached.
2641///
2642/// `jobs -Z` (argv overwrite) is not yet ported; the argv/envp
2643/// scan from C lines 2185-2210 is omitted — that's a separate
2644/// init.rs concern when `setproctitle()` lands.
2645/// C body (c:2168-2210): allocates the `jobtab[]` array sized to
2646/// MAXJOBS_ALLOC entries via `zalloc`, zero-fills via `memset`,
2647/// then (non-HAVE_SETPROCTITLE) walks argv + envp to compute the
2648/// `hackspace` byte count for the `jobs -Z` rename trick.
2649///
2650/// ```c
2651/// jobtab = (struct job *)zalloc(MAXJOBS_ALLOC*sizeof(struct job));
2652/// if (!jobtab) { zerr(...); exit(1); }
2653/// jobtabsize = MAXJOBS_ALLOC;
2654/// memset(jobtab, 0, MAXJOBS_ALLOC*sizeof(struct job));
2655/// /* -Z hackspace scan */
2656/// hackzero = *argv;
2657/// p = strchr(hackzero, 0);
2658/// while (*++argv) { q = *argv; if (q != p+1) goto done;
2659/// p = strchr(q, 0); }
2660/// for (; *envp; envp++) { ... }
2661/// done: hackspace = p - hackzero;
2662/// ```
2663pub fn init_jobs(argv: &[String], envp: &[String]) -> JobTable {
2664 // c:2164
2665 let table = JobTable::new(); // c:2164 zalloc
2666 // c:2185-2210 — `-Z` hackspace scan: locate contiguous argv+envp
2667 // space. Static-link path: we don't yet keep `hackzero` /
2668 // `hackspace` globals (the bin_fg -Z arm uses prctl directly on
2669 // Linux + pthread_setname_np on macOS, both bypassing the argv
2670 // overwrite trick). The scan computes the byte-distance only;
2671 // record it via env-var bridge so a future setproctitle fallback
2672 // can read it.
2673 if !argv.is_empty() {
2674 // c:2187 hackzero = *argv
2675 let zero = argv[0].as_str();
2676 let mut hackspace = zero.len(); // c:2208 p - hackzero
2677 // Walk argv tail then envp; each element must be contiguous
2678 // (the C check is `q != p+1` after the previous's NUL).
2679 for entry in argv.iter().skip(1).chain(envp.iter()) {
2680 // c:2191/2197 walks
2681 // Without raw argv pointers we can't verify contiguity from
2682 // Rust's String wrappers — accumulate length conservatively.
2683 hackspace += 1 + entry.len(); // c:2207-style p+1
2684 }
2685 env::set_var("__zshrs_hackspace", hackspace.to_string()); // record for jobs -Z
2686 }
2687 table // c:2210 done
2688}
2689
2690/// Hard upper bound on job-table growth.
2691/// Port of `MAX_MAXJOBS` from `Src/jobs.c:2221`.
2692pub const MAX_MAXJOBS: usize = 1000;
2693
2694/// Port of `expandjobtab()` from `Src/jobs.c:2225`.
2695///
2696/// C body:
2697/// ```c
2698/// int newsize = jobtabsize + MAXJOBS_ALLOC;
2699/// if (newsize > MAX_MAXJOBS) return 0;
2700/// newjobtab = zrealloc(jobtab, newsize * sizeof(struct job));
2701/// if (!newjobtab) return 0;
2702/// memset(newjobtab + jobtabsize, 0, MAXJOBS_ALLOC * sizeof(struct job));
2703/// jobtab = newjobtab;
2704/// jobtabsize = newsize;
2705/// return 1;
2706/// ```
2707///
2708/// Grows the job table by `MAXJOBS_ALLOC` slots, respecting the
2709/// `MAX_MAXJOBS` cap. Returns true on success, false if the cap
2710/// would be exceeded. The previous Rust port grew the table
2711/// unconditionally without the cap, and used `<= needed` instead
2712/// of growing by full chunks.
2713pub fn expandjobtab(jobtab: &mut Vec<job>, _needed: usize) -> bool {
2714 let newsize = jobtab.len() + MAXJOBS_ALLOC;
2715 if newsize > MAX_MAXJOBS {
2716 return false;
2717 }
2718 jobtab.resize_with(newsize, job::new);
2719 true
2720}
2721
2722/// Shrink job table if possible (from jobs.c maybeshrinkjobtab)
2723/// Port of `maybeshrinkjobtab` from `Src/jobs.c:2259`.
2724pub fn maybeshrinkjobtab(jobtab: &mut Vec<job>) {
2725 while jobtab
2726 .last()
2727 .map(|j| (j.stat & stat::INUSE) == 0)
2728 .unwrap_or(false)
2729 {
2730 jobtab.pop();
2731 }
2732}
2733
2734/// Port of `struct bgstatus` from `Src/jobs.c:2295`.
2735/// One `(pid, status)` pair the bg-status tracker records when a
2736/// background process exits so `wait $pid` can read its $?.
2737#[allow(non_camel_case_types)]
2738#[derive(Clone, Copy)]
2739pub struct bgstatus {
2740 // c:2296
2741 pub pid: i32, // c:2297
2742 pub status: i32, // c:2298
2743}
2744
2745/// Port of `typedef struct bgstatus *Bgstatus;` (jobs.c:2300).
2746pub type Bgstatus = Box<bgstatus>; // c:2300
2747
2748/// Port of `static LinkList bgstatus_list;` (jobs.c:2302). Insertion-
2749/// ordered list so the oldest entry can be evicted when the cap is
2750/// reached. Stored as `Vec<bgstatus>` since the order is the only
2751/// thing we'd ever need from a linked list here.
2752pub static bgstatus_list: Mutex<Vec<bgstatus>> = // c:2302
2753 Mutex::new(Vec::new());
2754
2755/// Port of `static long bgstatus_count;` (jobs.c:2304). Reaches
2756/// `_SC_CHILD_MAX` and stops (addbgstatus then evicts oldest).
2757pub static bgstatus_count: std::sync::atomic::AtomicI64 = // c:2304
2758 std::sync::atomic::AtomicI64::new(0);
2759
2760/// Direct port of `void addbgstatus(pid_t pid, int status)` from
2761/// `Src/jobs.c:2325`. Caps the global `bgstatus_list` at
2762/// `_SC_CHILD_MAX`, evicting oldest on overflow, then appends a
2763/// new `bgstatus { pid, status }` entry.
2764pub fn addbgstatus(pid: i32, status_val: i32) {
2765 // c:2325
2766 // c:2370 — `if (bgstatus_count == max_child)` cap + eviction.
2767 let max_child = unsafe { libc::sysconf(libc::_SC_CHILD_MAX) };
2768 let cap = if max_child > 0 {
2769 max_child as i64
2770 } else {
2771 1024
2772 };
2773 if let Ok(mut list) = bgstatus_list.lock() {
2774 if bgstatus_count.load(Ordering::Relaxed) >= cap {
2775 // c:2370
2776 // c:2371 — `rembgstatus(firstnode(bgstatus_list))`.
2777 if !list.is_empty() {
2778 list.remove(0);
2779 bgstatus_count.fetch_sub(1, Ordering::Relaxed);
2780 }
2781 }
2782 // c:2376-2385 — alloc + push.
2783 list.push(bgstatus {
2784 pid,
2785 status: status_val,
2786 }); // c:2381-2384
2787 bgstatus_count.fetch_add(1, Ordering::Relaxed); // c:2386
2788 }
2789}
2790
2791/// Direct port of `bin_fg(char *name, char **argv, Options ops, int func)` from `Src/jobs.c:2421`.
2792/// Multi-builtin dispatcher — handles bg, fg, wait, jobs, disown, and
2793/// the `-Z` process-rename form. C body is 315 lines (c:2421-2735);
2794/// the per-builtin behaviour is selected by `func` (BIN_BG/BIN_FG/
2795/// BIN_JOBS/BIN_WAIT/BIN_DISOWN).
2796///
2797/// Coverage status:
2798/// ✓ -Z process-title rename (c:2425-2451) — full port via
2799/// libc::prctl(PR_SET_NAME) on Linux; macOS pthread_setname_np;
2800/// other platforms emit a warning
2801/// ✓ no-job-control refusal for fg/bg under !jobbing (c:2461-2465)
2802/// ✓ jobs -l/-p/-d listing-format selection (c:2454-2459)
2803/// ⚠ jobspec parsing + per-job dispatch (c:2467-2733) DEFERRED —
2804/// depends on getjob (parses %N/%?str specifiers), the global
2805/// jobtab + oldjobtab, deletejob/printjob/makerunning, lastval2,
2806/// errflag, signal queueing for fg's tcsetpgrp dance, and the
2807/// STAT_* / STAT_SUPERJOB / STAT_DISOWN flag tracking. None of
2808/// those are fully ported yet; structural shape preserved so the
2809/// C signature lands and future port work can fill the body.
2810pub fn bin_fg(
2811 name: &str,
2812 argv: &[String], // c:2421
2813 ops: &options,
2814 func: i32,
2815) -> i32 {
2816 let _ofunc = func; // c:2424
2817
2818 // c:2425-2452 — `-Z`: rename the running process. Used by
2819 // login shells / tools that want their `ps` line to reflect a
2820 // descriptive title rather than `zsh`.
2821 if OPT_ISSET(ops, b'Z') {
2822 // c:2425
2823 if argv.is_empty() || argv.len() > 1 {
2824 // c:2428
2825 zwarnnam(name, "-Z requires one argument"); // c:2429
2826 return 1; // c:2430
2827 }
2828 queue_signals(); // c:2433
2829 let title = &argv[0];
2830 // c:2436 — `setproctitle("%s", *argv);` if available.
2831 // c:2438-2444 — fallback: memcpy into hackzero (the argv[0]
2832 // buffer reserved by the loader). Not portable from Rust,
2833 // so the prctl path covers Linux directly.
2834 #[cfg(target_os = "linux")]
2835 unsafe {
2836 let cs = std::ffi::CString::new(title.as_str()).unwrap_or_default();
2837 // PR_SET_NAME = 15; libc may not expose it — pass the
2838 // raw constant per `linux/prctl.h`.
2839 libc::prctl(
2840 15, /*PR_SET_NAME*/
2841 cs.as_ptr() as libc::c_ulong,
2842 0,
2843 0,
2844 0,
2845 ); // c:2447
2846 }
2847 #[cfg(target_os = "macos")]
2848 unsafe {
2849 extern "C" {
2850 fn pthread_setname_np(name: *const libc::c_char) -> libc::c_int;
2851 }
2852 let cs = std::ffi::CString::new(title.as_str()).unwrap_or_default();
2853 pthread_setname_np(cs.as_ptr());
2854 }
2855 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
2856 {
2857 let _ = title;
2858 }
2859 unqueue_signals(); // c:2449
2860 return 0; // c:2450
2861 }
2862
2863 // c:2454-2459 — jobs builtin: pick listing format.
2864 let mut lng = 0i32; // c:2422
2865 if func == BIN_JOBS {
2866 // c:2454
2867 lng = if OPT_ISSET(ops, b'l') {
2868 1
2869 }
2870 // c:2455
2871 else if OPT_ISSET(ops, b'p') {
2872 2
2873 } else {
2874 0
2875 };
2876 if OPT_ISSET(ops, b'd') {
2877 lng |= 4;
2878 } // c:2456
2879 } else {
2880 // c:2458 — `lng = !!isset(LONGLISTJOBS);`
2881 lng = if isset(LONGLISTJOBS) { 1 } else { 0 };
2882 }
2883 let _ = lng;
2884
2885 // c:2461-2465 — fg/bg need job control.
2886 let jobbing = isset(MONITOR);
2887 if (func == BIN_FG || func == BIN_BG) && !jobbing {
2888 // c:2461
2889 zwarnnam(name, "no job control in this shell."); // c:2463
2890 return 1; // c:2464
2891 }
2892
2893 // c:2467 — `queue_signals();`
2894 queue_signals();
2895 let table = JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
2896 // c:2474 — `wait_for_processes();` reap any newly-finished children
2897 // so the table reflects the current state before we list/dispatch.
2898 // C's wait_for_processes (Src/signals.c:249) routes each reaped
2899 // (pid, status) through update_bg_job internally; the Rust port
2900 // returns the pairs and leaves the routing to the caller. Then run
2901 // the update_job→printjob done-delete chain (Src/jobs.c:639-641 →
2902 // 1350-1363) so finished jobs leave the table before we list.
2903 {
2904 let reaped = wait_for_processes();
2905 let mut tab = table.lock().expect("jobtab poisoned");
2906 for (pid, status) in reaped {
2907 update_bg_job(&mut tab, pid, status);
2908 }
2909 scanjobs(&mut tab);
2910 }
2911
2912 // c:2477-2478 — `if (unset(NOTIFY)) scanjobs();`. (The routing
2913 // block above already swept STAT_CHANGED entries; this re-walk is
2914 // the C-shaped call and is idempotent.)
2915 if !crate::ported::zsh_h::isset(crate::ported::zsh_h::NOTIFY) {
2916 if let Some(jt) = JOBTAB.get() {
2917 let mut guard = jt.lock().unwrap();
2918 scanjobs(&mut guard); // c:2478
2919 }
2920 }
2921
2922 // c:2480-2481 — refresh CURJOB unless we're listing a frozen
2923 // oldjobtab snapshot from `jobs` in a non-monitor shell.
2924 if func != BIN_JOBS || jobbing || *OLDMAXJOB.get_or_init(|| Mutex::new(0)).lock().unwrap() == 0
2925 {
2926 // c:2481 — `setcurjob()` operates on the global jobtab.
2927 setcurjob();
2928 }
2929
2930 // c:2483-2486 — set stopmsg=2 so zexit doesn't complain about
2931 // stopped jobs if the user immediately runs `exit` after `jobs`.
2932 if func == BIN_JOBS {
2933 STOPMSG.store(2, Ordering::Relaxed);
2934 // c:2486
2935 }
2936
2937 let mut returnval: i32 = 0;
2938
2939 if argv.is_empty() {
2940 // c:2487
2941 if func == BIN_JOBS {
2942 // c:2500-2523 — list jobs. `ignorejob = thisjob` (c:2512)
2943 // — the C loop skips the job slot the shell is currently
2944 // building (the foreground job), NOT curjob. Skipping
2945 // curjob would hide every freshly-backgrounded job, since
2946 // spawnjob promotes it to curjob (c:1901-1903).
2947 let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
2948 let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
2949 let t = table.lock().expect("jobtab poisoned");
2950 let curmaxjob = t.len();
2951 let r_only = OPT_ISSET(ops, b'r');
2952 let s_only = OPT_ISSET(ops, b's');
2953 for job in 0..curmaxjob {
2954 // c:2513
2955 if job as i32 == thisjob {
2956 // c:2514 ignorejob
2957 continue;
2958 }
2959 let j = &t[job];
2960 if !j.is_inuse() {
2961 // c:2514 stat
2962 continue;
2963 }
2964 let stopped = j.is_stopped();
2965 // c:2515-2519 — flag filtering.
2966 if (!r_only && !s_only)
2967 || (r_only && s_only)
2968 || (r_only && !stopped)
2969 || (s_only && stopped)
2970 {
2971 // c:2520 — printjob(jobptr, lng, 2). The Rust
2972 // port's printjob takes job_num + cur/prev for
2973 // formatting; pass them through here.
2974 let curjob_opt = if curjob >= 0 {
2975 Some(curjob as usize)
2976 } else {
2977 None
2978 };
2979 let prevjob = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
2980 let prevjob_opt = if prevjob >= 0 {
2981 Some(prevjob as usize)
2982 } else {
2983 None
2984 };
2985 let s = printjob(j, job, lng, curjob_opt, prevjob_opt);
2986 if !s.is_empty() {
2987 println!("{}", s);
2988 }
2989 }
2990 }
2991 unqueue_signals(); // c:2522
2992 return 0; // c:2523
2993 }
2994 if func == BIN_FG || func == BIN_BG || func == BIN_DISOWN {
2995 // c:2491-2499 — "no current job" gate. C body covers BIN_FG/
2996 // BIN_BG/BIN_DISOWN equivalently — disown with no args
2997 // defaults to the current job (`firstjob = curjob`), which
2998 // must exist (and be printable) or the builtin errors out.
2999 let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
3000 let cur_noprint = curjob >= 0
3001 && table
3002 .lock()
3003 .expect("jobtab poisoned")
3004 .get(curjob as usize)
3005 .map(|j| (j.stat & stat::NOPRINT) != 0)
3006 .unwrap_or(true);
3007 if curjob < 0 || cur_noprint {
3008 // c:2494
3009 zwarnnam(name, "no current job"); // c:2495
3010 unqueue_signals();
3011 return 1; // c:2497
3012 }
3013 if func == BIN_DISOWN {
3014 // c:2498 firstjob = curjob → loop BIN_DISOWN arm c:2729
3015 // `deletejob(jobtab + job, 1)` — drop the entry without
3016 // killing/ waiting on the process.
3017 let mut tab = table.lock().expect("jobtab poisoned");
3018 if let Some(j) = tab.get_mut(curjob as usize) {
3019 deletejob(j, true); // c:2729
3020 }
3021 drop(tab);
3022 // The deleted job was curjob — re-pick (printjob's
3023 // shuffle shape, c:1357-1362).
3024 let pj = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
3025 *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = pj;
3026 setprevjob();
3027 unqueue_signals();
3028 return 0;
3029 }
3030 // Continue current job by sending SIGCONT via killjb(Job, sig).
3031 if curjob >= 0 {
3032 let _ = killjb(curjob as usize, libc::SIGCONT);
3033 }
3034 unqueue_signals();
3035 return 0;
3036 }
3037 if func == BIN_WAIT {
3038 // c:Src/jobs.c bin_fg BIN_WAIT branch — `wait` with no
3039 // args blocks until ALL active background jobs complete.
3040 // Loop waitpid(-1) draining children; ECHILD ends the loop.
3041 #[cfg(unix)]
3042 loop {
3043 let mut status: libc::c_int = 0;
3044 let pid = unsafe { libc::waitpid(-1, &mut status, 0) };
3045 if pid > 0 {
3046 if let Ok(mut tab) = table.lock() {
3047 update_bg_job(&mut tab, pid, status);
3048 }
3049 // c:Src/jobs.c:644-645 — `if (sigtrapped[SIGCHLD]
3050 // && job != thisjob) dotrap(SIGCHLD);`. C zsh's
3051 // canonical site for the SIGCHLD-trap dispatch
3052 // sits in update_job, gated on the job index NOT
3053 // matching the foreground job. The Rust update_job
3054 // port doesn't have the job index, and findproc
3055 // can miss the pid when the bg-job procs vec
3056 // wasn't populated by the spawn site — so the
3057 // dispatch never fires through that path.
3058 // bin_wait's reaper loop already has the pid and
3059 // runs only for `wait` (which by definition is
3060 // waiting on background jobs, so the "job !=
3061 // thisjob" condition is always true here). Fire
3062 // the trap from this site so function-form
3063 // TRAPCHLD() {…} and string-form `trap '…' CHLD`
3064 // both reach userspace. Bug #531 in docs/BUGS.md.
3065 let chld_trapped = crate::ported::signals::sigtrapped
3066 .lock()
3067 .ok()
3068 .and_then(|g| g.get(libc::SIGCHLD as usize).copied())
3069 .unwrap_or(0);
3070 let chld_string_trap = crate::ported::builtin::traps_table()
3071 .lock()
3072 .ok()
3073 .map(|t| t.contains_key("CHLD") || t.contains_key("SIGCHLD"))
3074 .unwrap_or(false);
3075 if chld_trapped != 0 || chld_string_trap {
3076 crate::ported::signals::dotrap(libc::SIGCHLD);
3077 }
3078 } else {
3079 break;
3080 }
3081 }
3082 // c:639-641 → c:1350-1363 — every job we just reaped went
3083 // through update_job (STAT_DONE|STAT_CHANGED); run the
3084 // printjob done-delete chain so the table is empty after
3085 // `wait`, matching C where the SIGCHLD-driven printjob
3086 // deletes each finished entry.
3087 if let Ok(mut tab) = table.lock() {
3088 scanjobs(&mut tab);
3089 }
3090 unqueue_signals();
3091 return 0;
3092 }
3093 unqueue_signals();
3094 return 0;
3095 }
3096
3097 // c:2537+ — per-arg jobspec dispatch (full body handles wait pid,
3098 // STAT_SUPERJOB carry-through, killjb retry, etc.). Port the
3099 // common path: jobspec → getjob → per-func switch (c:2598-2731).
3100 for arg in argv {
3101 if func == BIN_WAIT && isanum(arg) {
3102 // c:2541-2575 — `wait PID` waits for an arbitrary PID via
3103 // waitpid(); if not a child of this shell, C falls back to
3104 // getbgstatus (the reaped-status ring) and only then emits
3105 // "pid %d is not a child of this shell" with exit 127.
3106 if let Ok(pid) = arg.parse::<i32>() {
3107 let mut status: libc::c_int = 0;
3108 let r = unsafe { libc::waitpid(pid, &mut status, 0) };
3109 if r == -1 {
3110 let err = std::io::Error::last_os_error();
3111 if err.raw_os_error() == Some(libc::ECHILD) {
3112 // c:2566-2570 — getbgstatus fallback before
3113 // the diagnostic.
3114 if let Some(bg) = getbgstatus(pid) {
3115 returnval = bg;
3116 } else {
3117 zwarnnam(
3118 name,
3119 &format!("pid {} is not a child of this shell", pid),
3120 );
3121 returnval = 127;
3122 }
3123 } else {
3124 returnval = 1;
3125 }
3126 } else {
3127 // c:1748-1750 waitforpid semantics — exit status or
3128 // 128+sig. Route the status into the canonical
3129 // jobtab so the job entry is marked done + deleted
3130 // (C's SIGCHLD handler chain does this while
3131 // waitforpid suspends).
3132 if libc::WIFEXITED(status) {
3133 returnval = libc::WEXITSTATUS(status);
3134 } else if libc::WIFSIGNALED(status) {
3135 returnval = 128 + libc::WTERMSIG(status);
3136 }
3137 if let Ok(mut tab) = table.lock() {
3138 update_bg_job(&mut tab, pid, status);
3139 scanjobs(&mut tab);
3140 }
3141 }
3142 }
3143 continue; // c:2574
3144 }
3145 // c:2576 — `job = (*argv) ? getjob(*argv, name) : firstjob;`
3146 // EVERY non-pid arg goes through getjob — a bare numeric like
3147 // `jobs 1` is a job NAME (findjobnam) in zsh, not an index
3148 // (verified: zsh -fc 'sleep 5 & jobs 1' → "job not found: 1"
3149 // rc=127).
3150 let p = getjob(arg, name);
3151 if p < 0 {
3152 // c:2578-2581 — `if (job == -1) { retval = 127; break; }`.
3153 // getjob already emitted the diagnostic. Bug #393.
3154 returnval = 127;
3155 break;
3156 }
3157 // c:2583-2592 — STAT_INUSE / STAT_NOPRINT recheck.
3158 let jstat = table
3159 .lock()
3160 .expect("jobtab poisoned")
3161 .get(p as usize)
3162 .map(|j| j.stat)
3163 .unwrap_or(0);
3164 if (jstat & stat::INUSE) == 0 || (jstat & stat::NOPRINT) != 0 {
3165 if !isset(POSIXBUILTINS) {
3166 zwarnnam(name, &format!("{}: no such job", arg)); // c:2587
3167 }
3168 unqueue_signals(); // c:2588
3169 return 127; // c:2589
3170 }
3171 if func == BIN_FG || func == BIN_BG {
3172 if killjb(p as usize, libc::SIGCONT) == -1 {
3173 zwarnnam(
3174 name,
3175 &format!("{}: kill failed: {}", arg, std::io::Error::last_os_error()),
3176 );
3177 returnval = 1;
3178 }
3179 } else if func == BIN_WAIT {
3180 // c:2655-2659 — `retval = zwaitjob(job, 1); if (!retval)
3181 // retval = lastval2;`. The Rust zwaitjob takes `&mut job`
3182 // and suspends on SIGCHLD; holding the JOBTAB lock across
3183 // the suspend would deadlock against the handler's own
3184 // lock, so wait proc-by-proc with a blocking waitpid and
3185 // route each status through update_bg_job — the same
3186 // chain C's SIGCHLD handler drives while zwaitjob
3187 // suspends (Src/signals.c:249 → jobs.c:460).
3188 loop {
3189 let next_pid = {
3190 let tab = table.lock().expect("jobtab poisoned");
3191 match tab.get(p as usize) {
3192 Some(j) if (j.stat & stat::INUSE) != 0 && !j.is_done() => j
3193 .procs
3194 .iter()
3195 .chain(j.auxprocs.iter())
3196 .find(|pr| pr.status == SP_RUNNING)
3197 .map(|pr| pr.pid),
3198 _ => None,
3199 }
3200 };
3201 let pid = match next_pid {
3202 Some(pid) => pid,
3203 None => break,
3204 };
3205 let mut status: libc::c_int = 0;
3206 let r = unsafe { libc::waitpid(pid, &mut status, 0) };
3207 let mut tab = table.lock().expect("jobtab poisoned");
3208 if r == pid {
3209 update_bg_job(&mut tab, pid, status);
3210 } else {
3211 // ECHILD — already reaped elsewhere; mark via
3212 // update_job so the loop terminates.
3213 if let Some(j) = tab.get_mut(p as usize) {
3214 for pr in j.procs.iter_mut().chain(j.auxprocs.iter_mut()) {
3215 if pr.pid == pid && pr.status == SP_RUNNING {
3216 pr.status = 0;
3217 }
3218 }
3219 update_job(j);
3220 }
3221 }
3222 }
3223 // c:2656-2657 — `if (!retval) retval = lastval2;`
3224 returnval = LASTVAL2.load(Ordering::SeqCst);
3225 // c:1350-1363 via the suspended-handler printjob — the
3226 // finished entry leaves the table before wait returns
3227 // (zsh: a second `wait %1` errors "no such job").
3228 if let Ok(mut tab) = table.lock() {
3229 crate::exec_jobs::printjob_delete_tail(&mut tab, p as usize);
3230 }
3231 } else if func == BIN_JOBS {
3232 let t = table.lock().expect("jobtab poisoned");
3233 if let Some(j) = t.get(p as usize) {
3234 let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
3235 let prevjob = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
3236 let s = printjob(
3237 j,
3238 p as usize,
3239 lng,
3240 if curjob >= 0 {
3241 Some(curjob as usize)
3242 } else {
3243 None
3244 },
3245 if prevjob >= 0 {
3246 Some(prevjob as usize)
3247 } else {
3248 None
3249 },
3250 );
3251 if !s.is_empty() {
3252 println!("{}", s);
3253 }
3254 }
3255 } else if func == BIN_DISOWN {
3256 // c:2695-2727 — stopped-job warning, then c:2729
3257 // `deletejob(jobtab + job, 1)`.
3258 let mut tab = table.lock().expect("jobtab poisoned");
3259 if let Some(j) = tab.get_mut(p as usize) {
3260 if (j.stat & stat::STOPPED) != 0 {
3261 // c:2703-2705 — `sprintf(buf, " -%d", jobtab[job].gleader)`.
3262 zwarnnam(
3263 name,
3264 &format!(
3265 "warning: job is suspended, use `kill -CONT -{}' to resume",
3266 j.gleader
3267 ),
3268 ); // c:2717-2721
3269 }
3270 deletejob(j, true); // c:2729
3271 }
3272 drop(tab);
3273 // curjob/prevjob re-pick if we just disowned one of them.
3274 let cj = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
3275 if cj == p {
3276 let pj = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
3277 *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = pj;
3278 }
3279 setprevjob();
3280 }
3281 }
3282 unqueue_signals(); // c:2733
3283 returnval // c:2734 retval
3284}
3285
3286/// Direct port of `bin_kill(char *nam, char **argv, UNUSED(Options ops), UNUSED(int func))` from `Src/jobs.c:2772`.
3287/// Builtin entry for the `kill` command. Parses signal specifiers
3288/// (`-N` numeric, `-s NAME` symbolic, `-l` list-by-number,
3289/// `-L` tabular listing, `-n N` numeric explicit, `-q` sigqueue
3290/// rt-signal sival) then sends the chosen signal to each remaining
3291/// argv (PIDs or %jobspecs).
3292/// WARNING: param names don't match C — Rust=(nam, argv, _func) vs C=(nam, argv, ops, func)
3293pub fn bin_kill(
3294 nam: &str,
3295 argv: &[String], // c:2772
3296 _ops: &options,
3297 _func: i32,
3298) -> i32 {
3299 let mut sig: i32 = libc::SIGTERM; // c:2774
3300 let mut returnval: i32 = 0; // c:2775
3301 let mut got_sig = false; // c:2780
3302 let mut idx = 0usize;
3303
3304 // c:2782 — `while (*argv && **argv == '-')` flag-parse loop.
3305 while idx < argv.len() && argv[idx].starts_with('-') {
3306 let arg = argv[idx].clone();
3307 let body = &arg[1..];
3308
3309 // c:2814 — `else if ((*argv)[1] != '-' || (*argv)[2])` —
3310 // pseudo `--` end-of-flags.
3311 if body == "-" {
3312 // c:2814 / c:3010
3313 idx += 1;
3314 break;
3315 }
3316
3317 if got_sig {
3318 // c:2811
3319 break; // c:2812
3320 }
3321
3322 // c:2815 — `if (idigit((*argv)[1]))` — numeric signal `-N`.
3323 if body.chars().next().is_some_and(|c| c.is_ascii_digit()) {
3324 // c:2815
3325 match body.parse::<i32>() {
3326 Ok(n) => sig = n, // c:2818
3327 Err(_) => {
3328 zwarnnam(nam, &format!("invalid signal number: -{}", body));
3329 return 1; // c:2822
3330 }
3331 }
3332 got_sig = true;
3333 idx += 1;
3334 continue;
3335 }
3336
3337 // c:2818 — `-l` signal-name listing.
3338 if body == "l" {
3339 // c:2818
3340 idx += 1;
3341 if idx < argv.len() {
3342 // c:2819
3343 // c:2820-2868 — per-arg lookup: numeric → name; name → number.
3344 while idx < argv.len() {
3345 let token = &argv[idx];
3346 idx += 1;
3347 if let Ok(n) = token.parse::<i32>() {
3348 // c:2821 numeric
3349 let s = (n & !0o200) as i32; // c:2855
3350 if let Some(name) = sigs_name(s) {
3351 // c:2856-2858
3352 println!("{}", name);
3353 } else {
3354 println!("{}", n); // c:2862
3355 }
3356 } else {
3357 // c:2820-2823 — `zstrtol` parses leading
3358 // `-`/`+` as sign + digits. For `-X` (sign
3359 // consumed, no digit), signame points PAST
3360 // the `-` so the diagnostic emits `SIGX` not
3361 // `SIG-X`. C's flow then takes the `else`
3362 // branch at c:2849-2852 which ALWAYS emits
3363 // unknown without re-looking-up — verified vs
3364 // /opt/homebrew/bin/zsh: `kill -l -TERM`
3365 // emits "unknown signal: SIGTERM" rc=1 even
3366 // though TERM IS a valid signal name. Mirror
3367 // that: when token has a leading `-`/`+`,
3368 // skip the lookup and emit unknown directly.
3369 let sign_stripped = token
3370 .strip_prefix('-')
3371 .or_else(|| token.strip_prefix('+'));
3372 if let Some(stripped) = sign_stripped {
3373 let upper = stripped.to_ascii_uppercase();
3374 let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
3375 zwarnnam(nam, &format!("unknown signal: SIG{}", bare)); // c:2851
3376 returnval += 1;
3377 } else {
3378 let upper = token.to_ascii_uppercase();
3379 let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
3380 if let Some(n) = sigs_number(bare) {
3381 // c:2828
3382 println!("{}", n); // c:2842
3383 } else {
3384 zwarnnam(nam, &format!("unknown signal: SIG{}", bare)); // c:2845
3385 returnval += 1;
3386 }
3387 }
3388 }
3389 }
3390 return returnval; // c:2868
3391 }
3392 // c:2869-2876 — bare `-l`: print every signal name.
3393 print!("{}", sigs_name(1).unwrap_or("HUP"));
3394 for s in 2..=crate::ported::signals_h::SIGCOUNT {
3395 if let Some(n) = sigs_name(s) {
3396 print!(" {}", n);
3397 }
3398 }
3399 println!();
3400 return 0; // c:2879
3401 }
3402
3403 // c:2880 — `-L` tabular listing.
3404 if body == "L" {
3405 // c:2880
3406 let cols = 4usize;
3407 let mut col = 0usize;
3408 for s in 1..=crate::ported::signals_h::SIGCOUNT {
3409 if let Some(n) = sigs_name(s) {
3410 print!("{:>2} {:<10}", s, n);
3411 col += 1;
3412 if col % cols == 0 {
3413 println!();
3414 } else {
3415 print!(" ");
3416 }
3417 }
3418 }
3419 if col % cols != 0 {
3420 println!();
3421 }
3422 return 0; // c:2911
3423 }
3424
3425 // c:2913 — `-n N` numeric signal (explicit).
3426 if body == "n" {
3427 // c:2913
3428 idx += 1;
3429 if idx >= argv.len() {
3430 // c:2916
3431 zwarnnam(nam, "-n: argument expected"); // c:2917
3432 return 1; // c:2918
3433 }
3434 match argv[idx].parse::<i32>() {
3435 // c:2920
3436 Ok(n) => {
3437 sig = n;
3438 }
3439 Err(_) => {
3440 zwarnnam(nam, &format!("invalid signal number: {}", argv[idx])); // c:2923
3441 return 1;
3442 }
3443 }
3444 got_sig = true;
3445 idx += 1;
3446 continue;
3447 }
3448
3449 // c:2935 — `-s NAME` symbolic signal.
3450 if body == "s" {
3451 // c:2935
3452 idx += 1;
3453 if idx >= argv.len() {
3454 // c:2938
3455 zwarnnam(nam, "-s: argument expected"); // c:2939
3456 return 1;
3457 }
3458 let name = argv[idx].as_str();
3459 // c:Src/jobs.c — empty signal-name after `-s` emits
3460 // `-: signal name expected` rc=1 (verified vs
3461 // /opt/homebrew/bin/zsh: `kill -s "" 1` →
3462 // "zsh:kill:1: -: signal name expected" rc=1).
3463 if name.is_empty() {
3464 zwarnnam(nam, "-: signal name expected");
3465 return 1;
3466 }
3467 let upper = name.to_ascii_uppercase();
3468 let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
3469 match sigs_number(bare) {
3470 Some(n) => sig = n,
3471 None => {
3472 zwarnnam(nam, &format!("unknown signal: SIG{}", bare)); // c:2944
3473 return 1;
3474 }
3475 }
3476 got_sig = true;
3477 idx += 1;
3478 continue;
3479 }
3480
3481 // c:2782 — `-q VALUE` sigqueue path. zshrs treats it as
3482 // "consume the value, then continue parsing"; the actual
3483 // sival_int payload is dropped (not wired to a real
3484 // sigqueue(2) call yet — Linux-only, niche).
3485 if body == "q" {
3486 // c:2782
3487 idx += 1;
3488 if idx >= argv.len() {
3489 // c:2785
3490 zwarnnam(nam, "-q: argument expected"); // c:2786
3491 return 1;
3492 }
3493 if argv[idx].parse::<i32>().is_err() {
3494 // c:2796
3495 zwarnnam(nam, &format!("invalid number: {}", argv[idx])); // c:2797
3496 return 1;
3497 }
3498 idx += 1; // c:2802
3499 continue; // c:2803
3500 }
3501
3502 // c:2960 — symbolic `-NAME` (no `s` prefix needed).
3503 let upper = body.to_ascii_uppercase();
3504 let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
3505 match sigs_number(bare) {
3506 Some(n) => {
3507 sig = n;
3508 got_sig = true;
3509 idx += 1;
3510 }
3511 None => {
3512 zwarnnam(nam, &format!("unknown signal: SIG{}", bare)); // c:2974
3513 // c:Src/jobs.c — when `-NAME` lookup fails AND there's
3514 // at least one positional remaining, zsh emits the
3515 // follow-up hint `type kill -L for a list of signals`
3516 // rc=1. The bundled C source uses capital `-L` (the
3517 // tabular listing flag added in zsh 5.9.x-dev). Older
3518 // /bin/zsh 5.9 shows lowercase `-l`; the bundled
3519 // source AND /opt/homebrew/bin/zsh 5.9.1+ use `-L`.
3520 zwarnnam(nam, "type kill -L for a list of signals");
3521 return 1;
3522 }
3523 }
3524 }
3525
3526 // c:3010 — no PID/jobspec arguments?
3527 if idx >= argv.len() {
3528 // c:3010
3529 zwarnnam(nam, "not enough arguments"); // c:3011
3530 return 1;
3531 }
3532
3533 // c:3015-3045 — for each remaining argv, parse PID or %jobspec
3534 // and send `sig`. zshrs handles bare numeric PIDs + simple
3535 // %jobspec via getjob; PIDs with leading `-` (process-group)
3536 // are forwarded via killpg.
3537 for arg in &argv[idx..] {
3538 if let Some(num) = arg.strip_prefix('-') {
3539 // c:3030
3540 // process-group kill: `-PID` → killpg(PID, sig).
3541 match num.parse::<i32>() {
3542 Ok(pgid) => {
3543 let r = unsafe { libc::killpg(pgid, sig) }; // c:3032
3544 if r != 0 {
3545 // c:Src/jobs.c:2994/3022 — `zwarnnam("kill",
3546 // "kill %s failed: %e", *argv, errno)`. `%e`
3547 // is C's strerror-with-lowercased-first-char
3548 // formatter (Src/utils.c:362-368, except for
3549 // EIO). Mirror via the existing
3550 // compat::strerror port to avoid leaking
3551 // Rust's `(os error N)` suffix. Bug #491.
3552 let errno = std::io::Error::last_os_error()
3553 .raw_os_error()
3554 .unwrap_or(libc::EINVAL);
3555 let mut errmsg = crate::ported::compat::strerror(errno);
3556 if errno != libc::EIO {
3557 if let Some(c) = errmsg.chars().next() {
3558 errmsg = format!(
3559 "{}{}",
3560 c.to_ascii_lowercase(),
3561 &errmsg[c.len_utf8()..]
3562 );
3563 }
3564 }
3565 zwarnnam(nam, &format!("kill {} failed: {}", arg, errmsg));
3566 returnval = 1;
3567 }
3568 }
3569 Err(_) => {
3570 zwarnnam(nam, &format!("illegal pid: {}", arg));
3571 returnval = 1;
3572 }
3573 }
3574 } else if arg.starts_with('%') {
3575 // c:2985 jobspec
3576 // c:2989 — `if ((p = getjob(*argv, nam)) == -1)`.
3577 let p = getjob(arg, nam);
3578 if p < 0 {
3579 // c:2989
3580 returnval += 1; // c:2990
3581 continue;
3582 }
3583 // c:2993 — `killjb(jobtab + p, sig)`.
3584 if killjb(p as usize, sig) == -1 {
3585 // c:2993
3586 zwarnnam(
3587 "kill",
3588 &format!(
3589 "kill {} failed: {}",
3590 arg, // c:2994
3591 std::io::Error::last_os_error()
3592 ),
3593 );
3594 returnval += 1; // c:2995
3595 continue;
3596 }
3597 // c:3001-3010 — if stopped + non-stopping signal,
3598 // SIGCONT after to wake the job so it processes `sig`.
3599 let stopped = JOBTAB
3600 .get_or_init(|| Mutex::new(Vec::new()))
3601 .lock()
3602 .expect("jobtab poisoned")
3603 .get(p as usize)
3604 .map(|j| j.is_stopped())
3605 .unwrap_or(false);
3606 if stopped
3607 && sig != libc::SIGKILL
3608 && sig != libc::SIGCONT
3609 && sig != libc::SIGTSTP
3610 && sig != libc::SIGTTOU
3611 && sig != libc::SIGTTIN
3612 && sig != libc::SIGSTOP
3613 {
3614 let _ = killjb(p as usize, libc::SIGCONT); // c:3009
3615 }
3616 } else {
3617 match arg.parse::<i32>() {
3618 // c:3024 PID
3619 Ok(pid) => {
3620 let r = unsafe { libc::kill(pid, sig) }; // c:3025
3621 if r != 0 {
3622 // c:Src/jobs.c:2994/3022 — `zwarnnam("kill",
3623 // "kill %s failed: %e", *argv, errno)`. `%e`
3624 // is C's strerror-with-lowercased-first-char
3625 // formatter (Src/utils.c:362-368, except for
3626 // EIO). Mirror via the existing
3627 // compat::strerror port to avoid leaking
3628 // Rust's `(os error N)` suffix. Bug #491.
3629 let errno = std::io::Error::last_os_error()
3630 .raw_os_error()
3631 .unwrap_or(libc::EINVAL);
3632 let mut errmsg = crate::ported::compat::strerror(errno);
3633 if errno != libc::EIO {
3634 if let Some(c) = errmsg.chars().next() {
3635 errmsg = format!(
3636 "{}{}",
3637 c.to_ascii_lowercase(),
3638 &errmsg[c.len_utf8()..]
3639 );
3640 }
3641 }
3642 zwarnnam(nam, &format!("kill {} failed: {}", arg, errmsg)); // c:3027
3643 returnval = 1;
3644 }
3645 }
3646 Err(_) => {
3647 zwarnnam(nam, &format!("illegal pid: {}", arg));
3648 returnval = 1;
3649 }
3650 }
3651 }
3652 }
3653 returnval // c:3045
3654}
3655
3656/// Signal number from name (from jobs.c getsigidx)
3657/// Port of `int getsigidx(const char *s)` from `Src/jobs.c:3047`.
3658///
3659/// **C semantics** (c:3050-3081):
3660/// 1. Try atoi(s). If first char is digit AND value in
3661/// `[0, VSIGCOUNT)` OR in `[SIGRTMIN..=SIGRTMAX]`, return SIGIDX(x).
3662/// 2. Strip "SIG" prefix.
3663/// 3. Walk `sigs[]` table (case-sensitive strcmp).
3664/// 4. Walk `alt_sigs[]` table for aliases (IOT, CLD, IO/POLL).
3665/// 5. Try `rtsigno(s)` for "RTMIN+N"/"RTMAX-N" forms.
3666/// 6. Return -1 (Rust returns None).
3667///
3668/// **Rust port divergences (documented Rust-port adaptations)**:
3669/// * Case-insensitive match (`to_uppercase()`) vs C's strcmp.
3670/// Rust adaptation: users often write `int` / `Int` / `INT`.
3671/// * Numeric path bounds-checks against VSIGCOUNT and the RT range
3672/// per c:3056-3058. Previously the Rust port accepted ANY
3673/// parse-able number including out-of-range values like "9999"
3674/// where C returns -1.
3675/// Build the `$signals` special-array contents: zsh's PM_ARRAY at
3676/// Src/Modules/parameter.c indexes signal names 1-based with slot
3677/// 1 = "EXIT", 2 = "HUP", 3 = "INT", … up to SIGCOUNT real signals
3678/// plus the two virtual slots (SIGZERR, SIGDEBUG) — but the canonical
3679/// `$signals` array only carries the real OS signals (no virtual
3680/// entries). Used by `arrays_get("signals")` in the subst path.
3681pub fn sig_names_for_signals_param() -> Vec<String> {
3682 let mut out: Vec<String> = Vec::with_capacity(crate::ported::signals_h::SIGCOUNT as usize + 1);
3683 // Slot 0 → "EXIT".
3684 if let Some(n) = crate::ported::signals_h::sigs_name(0) {
3685 out.push(n.to_string());
3686 }
3687 // Slots 1..=SIGCOUNT → real signal names (HUP, INT, QUIT, …).
3688 for s in 1..=crate::ported::signals_h::SIGCOUNT {
3689 if let Some(n) = crate::ported::signals_h::sigs_name(s) {
3690 out.push(n.to_string());
3691 }
3692 }
3693 // Virtual signals ZERR / DEBUG occupy the tail (SIGCOUNT+1,
3694 // SIGCOUNT+2) per c:Src/signames.c — zsh exposes them in
3695 // `$signals` after the real OS signals.
3696 if let Some(n) = crate::ported::signals_h::sigs_name(crate::ported::signals_h::SIGZERR) {
3697 out.push(n.to_string());
3698 }
3699 if let Some(n) = crate::ported::signals_h::sigs_name(crate::ported::signals_h::SIGDEBUG) {
3700 out.push(n.to_string());
3701 }
3702 out
3703}
3704/// `getsigidx` — see implementation.
3705pub fn getsigidx(s: &str) -> Option<i32> {
3706 // c:3052-3058 — numeric-input branch: bounded by VSIGCOUNT + RT range.
3707 if let Some(first) = s.chars().next() {
3708 if first.is_ascii_digit() {
3709 if let Ok(x) = s.parse::<i32>() {
3710 let vsig = crate::ported::signals_h::VSIGCOUNT;
3711 if x >= 0 && x < vsig {
3712 return Some(x); // c:3058 SIGIDX(x) = x in standard range
3713 }
3714 #[cfg(target_os = "linux")]
3715 {
3716 // `libc::SIGRTMIN()` / `SIGRTMAX()` are `extern "C" fn`
3717 // (NOT `unsafe`) on Linux — they're glibc functions
3718 // that read runtime values. The unsafe block was a
3719 // copy-paste leftover from when these were macros.
3720 let sigrtmin = libc::SIGRTMIN();
3721 let sigrtmax = libc::SIGRTMAX();
3722 if x >= sigrtmin && x <= sigrtmax {
3723 return Some(crate::ported::signals_h::SIGIDX(x)); // c:3058
3724 }
3725 }
3726 // c:3081 — out-of-range numeric input returns -1 (None).
3727 return None;
3728 }
3729 }
3730 }
3731 let s = s.strip_prefix("SIG").unwrap_or(s);
3732 match s.to_uppercase().as_str() {
3733 "EXIT" => Some(0),
3734 // c:Src/signames.c:62-98 + jobs.c:2761 — zsh-internal virtual
3735 // signals: ZERR/DEBUG are SIGCOUNT+1 / SIGCOUNT+2; ERR aliases
3736 // ZERR when SIGERR isn't OS-defined (the common POSIX case
3737 // since most kernels don't ship a SIGERR signal).
3738 "ZERR" | "ERR" => Some(crate::ported::signals_h::SIGZERR),
3739 "DEBUG" => Some(crate::ported::signals_h::SIGDEBUG),
3740 "HUP" => Some(libc::SIGHUP),
3741 "INT" => Some(libc::SIGINT),
3742 "QUIT" => Some(libc::SIGQUIT),
3743 "ILL" => Some(libc::SIGILL),
3744 "TRAP" => Some(libc::SIGTRAP),
3745 "ABRT" | "IOT" => Some(libc::SIGABRT),
3746 "BUS" => Some(libc::SIGBUS),
3747 "FPE" => Some(libc::SIGFPE),
3748 "KILL" => Some(libc::SIGKILL),
3749 "USR1" => Some(libc::SIGUSR1),
3750 "SEGV" => Some(libc::SIGSEGV),
3751 "USR2" => Some(libc::SIGUSR2),
3752 "PIPE" => Some(libc::SIGPIPE),
3753 "ALRM" => Some(libc::SIGALRM),
3754 "TERM" => Some(libc::SIGTERM),
3755 "CHLD" | "CLD" => Some(libc::SIGCHLD),
3756 "CONT" => Some(libc::SIGCONT),
3757 "STOP" => Some(libc::SIGSTOP),
3758 "TSTP" => Some(libc::SIGTSTP),
3759 "TTIN" => Some(libc::SIGTTIN),
3760 "TTOU" => Some(libc::SIGTTOU),
3761 "URG" => Some(libc::SIGURG),
3762 "XCPU" => Some(libc::SIGXCPU),
3763 "XFSZ" => Some(libc::SIGXFSZ),
3764 "VTALRM" => Some(libc::SIGVTALRM),
3765 "PROF" => Some(libc::SIGPROF),
3766 "WINCH" => Some(libc::SIGWINCH),
3767 "IO" | "POLL" => Some(libc::SIGIO),
3768 "SYS" => Some(libc::SIGSYS),
3769 _ => {
3770 // c:3075-3078 — `if ((x = rtsigno(s))) return SIGIDX(x);`
3771 // Parse "RTMIN+N" / "RTMAX-N" via the canonical helper
3772 // and convert the resulting signum to its trap-table
3773 // index via SIGIDX.
3774 #[cfg(target_os = "linux")]
3775 {
3776 if let Some(signum) = crate::ported::signals::rtsigno(s) {
3777 // c:3075
3778 return Some(crate::ported::signals_h::SIGIDX(signum)); // c:3076
3779 }
3780 }
3781 None // c:3081 return -1
3782 }
3783 }
3784}
3785
3786/// Get the signal name for signal-based job output (from jobs.c getsigname)
3787/// Port of `getsigname(int sig)` from `Src/jobs.c:3087`.
3788pub fn getsigname(sig: i32) -> String {
3789 // c:Src/signames.c — virtual signal names. SIGZERR/SIGDEBUG sit
3790 // PAST the libc kernel-signal range (SIGCOUNT+1/+2) and have no
3791 // libc constant; match them explicitly so the dotrap dispatcher
3792 // can build `TRAPZERR` / `TRAPDEBUG` instead of `TRAPSIG32`/`SIG33`.
3793 // Bug #389.
3794 if sig == crate::ported::signals_h::SIGZERR {
3795 return "ZERR".to_string();
3796 }
3797 if sig == crate::ported::signals_h::SIGDEBUG {
3798 return "DEBUG".to_string();
3799 }
3800 match sig {
3801 0 => "EXIT".to_string(),
3802 libc::SIGHUP => "HUP".to_string(),
3803 libc::SIGINT => "INT".to_string(),
3804 libc::SIGQUIT => "QUIT".to_string(),
3805 libc::SIGILL => "ILL".to_string(),
3806 libc::SIGTRAP => "TRAP".to_string(),
3807 libc::SIGABRT => "ABRT".to_string(),
3808 libc::SIGBUS => "BUS".to_string(),
3809 libc::SIGFPE => "FPE".to_string(),
3810 libc::SIGKILL => "KILL".to_string(),
3811 libc::SIGUSR1 => "USR1".to_string(),
3812 libc::SIGSEGV => "SEGV".to_string(),
3813 libc::SIGUSR2 => "USR2".to_string(),
3814 libc::SIGPIPE => "PIPE".to_string(),
3815 libc::SIGALRM => "ALRM".to_string(),
3816 libc::SIGTERM => "TERM".to_string(),
3817 libc::SIGCHLD => "CHLD".to_string(),
3818 libc::SIGCONT => "CONT".to_string(),
3819 libc::SIGSTOP => "STOP".to_string(),
3820 libc::SIGTSTP => "TSTP".to_string(),
3821 libc::SIGTTIN => "TTIN".to_string(),
3822 libc::SIGTTOU => "TTOU".to_string(),
3823 libc::SIGURG => "URG".to_string(),
3824 libc::SIGXCPU => "XCPU".to_string(),
3825 libc::SIGXFSZ => "XFSZ".to_string(),
3826 libc::SIGVTALRM => "VTALRM".to_string(),
3827 libc::SIGPROF => "PROF".to_string(),
3828 libc::SIGWINCH => "WINCH".to_string(),
3829 libc::SIGIO => "IO".to_string(),
3830 libc::SIGSYS => "SYS".to_string(),
3831 _ => {
3832 // c:3099-3101 — `if (sig >= VSIGCOUNT) return rtsigname(SIGNUM(sig), 0);`
3833 // RT-signal range (Linux SIGRTMIN..SIGRTMAX) maps to
3834 // "RTMIN+N"/"RTMAX-N" via the canonical rtsigname helper.
3835 // The previous Rust port emitted `SIG{sig}` for every
3836 // unknown signal — losing the RT-signal naming entirely.
3837 #[cfg(target_os = "linux")]
3838 {
3839 // glibc `SIGRTMIN()`/`SIGRTMAX()` are safe extern ported.
3840 let sigrtmin = libc::SIGRTMIN();
3841 let sigrtmax = libc::SIGRTMAX();
3842 if sig >= sigrtmin && sig <= sigrtmax {
3843 // c:3100
3844 let nm = crate::ported::signals::rtsigname(sig); // c:3101 rtsigname(SIGNUM(sig), 0)
3845 if !nm.is_empty() {
3846 return nm;
3847 }
3848 }
3849 }
3850 format!("SIG{}", sig)
3851 }
3852 }
3853}
3854
3855/// Port of `gettrapnode(int sig, int ignoredisable)` from `Src/jobs.c:3115`.
3856///
3857/// C body looks up `TRAP<signame>` in the `shfunctab` (shell-
3858/// function hashtable) using either `getnode` (skip disabled) or
3859/// `getnode2` (include disabled), depending on `ignoredisable`.
3860/// Falls back to `alt_sigs[]` aliases (e.g. `TRAPCLD` for
3861/// SIGCHLD) when the canonical `TRAP<getsigname(sig)>` form
3862/// isn't found.
3863///
3864/// Returns the matched node's NAME (mirroring C's `hn->nam`
3865/// usage at every caller), or `None` if no trap is registered
3866/// under any canonical or alt name for this signal.
3867pub fn gettrapnode(sig: i32, ignoredisable: bool) -> Option<String> {
3868 // c:3115
3869 // c:3117 — char fname[20];
3870 // c:3119 — HashNode (*getptr)(HashTable ht, const char *name);
3871 // c:3121-3124 — getptr = ignoredisable ? getnode2 : getnode;
3872 let tab = crate::ported::hashtable::shfunctab_lock()
3873 .read()
3874 .expect("shfunctab poisoned");
3875 let getptr = |name: &str| -> Option<String> {
3876 let hit = if ignoredisable {
3877 tab.get_including_disabled(name) // c:3122 getnode2
3878 } else {
3879 tab.get(name) // c:3124 getnode
3880 };
3881 hit.map(|f| f.node.nam.clone())
3882 };
3883 // c:3131 — sprintf(fname, "TRAP%s", sigs[sig]);
3884 let fname = format!("TRAP{}", getsigname(sig));
3885 // c:3132 — if ((hn = getptr(shfunctab, fname))) return hn;
3886 if let Some(n) = getptr(&fname) {
3887 return Some(n);
3888 }
3889 // c:3142-3148 — for (i = 0; alt_sigs[i].name; i++)
3890 // if (alt_sigs[i].num == sig) {
3891 // sprintf(fname, "TRAP%s", alt_sigs[i].name);
3892 // if ((hn = getptr(shfunctab, fname))) return hn;
3893 // }
3894 for (alt_name, alt_num) in crate::ported::signals_h::ALT_SIGS.iter() {
3895 if *alt_num == sig {
3896 let fname = format!("TRAP{}", alt_name);
3897 if let Some(n) = getptr(&fname) {
3898 return Some(n);
3899 }
3900 }
3901 }
3902 // c:3150 — return NULL;
3903 None
3904}
3905
3906/// Port of `removetrapnode(int sig)` from `Src/jobs.c:3157`.
3907///
3908/// C body:
3909/// ```c
3910/// HashNode hn = gettrapnode(sig, 1);
3911/// if (hn) { shfunctab->removenode(shfunctab, hn->nam); shfunctab->freenode(hn); }
3912/// ```
3913///
3914/// Routes through `hashtable::removeshfuncnode` which itself
3915/// dispatches the trap-removal logic for `TRAP<sig>` names.
3916pub fn removetrapnode(sig: i32) {
3917 let name = format!("TRAP{}", getsigname(sig));
3918 crate::ported::hashtable::removeshfuncnode(&name);
3919}
3920
3921/// Direct port of `bin_suspend(char *name, UNUSED(char **argv), Options ops, UNUSED(int func))` from `Src/jobs.c:3170`.
3922/// C body (c:3173-3197):
3923/// ```c
3924/// if (islogin && !OPT_ISSET(ops,'f')) { error; return 1; }
3925/// if (jobbing) { signal_default(SIGTTIN/TSTP/TTOU); release_pgrp(); }
3926/// killpg(origpgrp, SIGTSTP);
3927/// if (jobbing) { acquire_pgrp(); signal_ignore(SIGTTOU/TSTP/TTIN); }
3928/// return 0;
3929/// ```
3930/// WARNING: param names don't match C — Rust=(name, _argv, _func) vs C=(name, argv, ops, func)
3931pub fn bin_suspend(
3932 name: &str,
3933 _argv: &[String], // c:3170
3934 ops: &options,
3935 _func: i32,
3936) -> i32 {
3937 // c:3173 — `if (islogin && !OPT_ISSET(ops,'f'))`. C reads the
3938 // `islogin` global, set when zsh's `argv[0]` started with
3939 // `-`. Probe `$0` via paramtab (was reading the OS env,
3940 // which never carries a literal `$0`).
3941 let islogin = getsparam("0").map(|s| s.starts_with('-')).unwrap_or(false);
3942 //won't suspend a login shell, unless forced
3943 if islogin && !OPT_ISSET(ops, b'f') {
3944 // c:3173
3945 zwarnnam(name, "can't suspend login shell"); // c:3174
3946 return 1; // c:3175
3947 }
3948 // c:3177 — `if (jobbing)`. jobbing is the job-control-enabled flag;
3949 // tracks the MONITOR option.
3950 let jobbing = isset(MONITOR);
3951
3952 if jobbing {
3953 // c:3177
3954 //stop ignoring signals
3955 signal_default(libc::SIGTTIN); // c:3179
3956 signal_default(libc::SIGTSTP); // c:3180
3957 signal_default(libc::SIGTTOU); // c:3181
3958 //Move ourselves back to the process group we came from
3959 release_pgrp(); // c:3184
3960 }
3961
3962 // suspend ourselves with a SIGTSTP // c:3187
3963 let origpgrp = ORIGPGRP
3964 .get_or_init(|| Mutex::new(0))
3965 .lock()
3966 .map(|g| *g)
3967 .unwrap_or(0);
3968 unsafe {
3969 libc::killpg(origpgrp, libc::SIGTSTP);
3970 } // c:3188
3971
3972 if jobbing {
3973 // c:3190
3974 let _ = acquire_pgrp(); // c:3191
3975 //restore signal handling
3976 signal_ignore(libc::SIGTTOU); // c:3193
3977 signal_ignore(libc::SIGTSTP); // c:3194
3978 signal_ignore(libc::SIGTTIN); // c:3195
3979 }
3980 0 // c:3197
3981}
3982
3983/// Port of `findjobnam(const char *s)` from `Src/jobs.c:3204`.
3984///
3985/// C signature: `int findjobnam(const char *s)`
3986///
3987/// Internal helper uses passed table to avoid re-locking.
3988/// WARNING: param names don't match C — Rust=(s, jobtab, maxjob, thisjob) vs C=(s)
3989pub(crate) fn findjobnam(s: &str, jobtab: &[job], maxjob: i32, thisjob: i32) -> Option<i32> {
3990 let mut jobnum = maxjob; // c:2037
3991 while jobnum >= 0 {
3992 // c:2037
3993 let ju = jobnum as usize;
3994 if ju < jobtab.len()
3995 && jobtab[ju].stat != 0 // c:2038
3996 && (jobtab[ju].stat & stat::SUBJOB) == 0 // c:2039
3997 && jobnum != thisjob
3998 // c:2040
3999 {
4000 // C: if (!strncmp(jobtab[jobnum].procs->text, s, strlen(s))) // c:2041
4001 if let Some(first_proc) = jobtab[ju].procs.first() {
4002 if first_proc.text.starts_with(s) {
4003 return Some(jobnum); // c:2042-2043
4004 }
4005 }
4006 }
4007 jobnum -= 1;
4008 }
4009 None // c:2046-2047
4010}
4011
4012/// Direct port of `acquire_pgrp()` from `Src/jobs.c:3222`.
4013/// C body (c:3225-3278): block SIGTTIN/SIGTTOU/SIGTSTP, then loop
4014/// while the tty's pgrp differs from ours — re-fetch our pgrp,
4015/// optionally call `attachtty()` to claim the tty (with signal
4016/// unblock + reblock around the call so SIGT* fires correctly), or
4017/// trigger `read(0, NULL, 0)` to provoke a SIGT* if we're not yet
4018/// the session leader. Bail after 100 iterations or a stable pgrp
4019/// in non-interactive mode. If still not in foreground, `setpgrp(0, 0)`
4020/// to claim, or disable MONITOR option as last resort.
4021///
4022/// ```c
4023/// long ttpgrp;
4024/// sigset_t blockset, oldset;
4025/// if ((mypgrp = GETPGRP()) >= 0) {
4026/// long lastpgrp = mypgrp;
4027/// sigemptyset(&blockset);
4028/// sigaddset(&blockset, SIGTTIN); /* SIGTTOU; SIGTSTP */
4029/// oldset = signal_block(&blockset);
4030/// int loop_count = 0;
4031/// while ((ttpgrp = gettygrp()) != -1 && ttpgrp != mypgrp) {
4032/// /* re-attach + read(0) probes; bail after 100 loops */
4033/// }
4034/// if (mypgrp != mypid) {
4035/// if (setpgrp(0, 0) == 0) attachtty(mypgrp);
4036/// else opts[MONITOR] = 0;
4037/// }
4038/// signal_setmask(&oldset);
4039/// } else opts[MONITOR] = 0;
4040/// ```
4041#[cfg(unix)]
4042/// Port of `acquire_pgrp` from `Src/jobs.c:3222`.
4043pub fn acquire_pgrp() -> bool {
4044 // c:3222
4045 let mypid = unsafe { libc::getpid() };
4046 // C `mypgrp` is a SINGLE global written all through acquire_pgrp and
4047 // read by attachtty / getquery / the history tty-reclaim. zshrs split
4048 // it into TWO globals — clone::mypgrp (AtomicI32) and jobs::MYPGRP
4049 // (OnceLock) — so the single C `mypgrp = …` must update BOTH (same
4050 // paired-global rule as lexstop / strin). Without this, acquire_pgrp
4051 // left both at 0 and the first `attachtty(mypgrp)` ran
4052 // `tcsetpgrp(tty, 0)` → EPERM ("can't set tty pgrp") on an interactive
4053 // shell. Closure (not a `fn` item) so the src/ported port-gate is fine.
4054 let sync_mypgrp = |v: i32| {
4055 crate::ported::modules::clone::mypgrp.store(v, Ordering::Relaxed);
4056 *MYPGRP.get_or_init(|| Mutex::new(0)).lock().unwrap() = v;
4057 };
4058 let mut mypgrp = unsafe { libc::getpgrp() }; // c:3227 GETPGRP()
4059 sync_mypgrp(mypgrp); // c:3227 — `mypgrp = GETPGRP()` (global)
4060 if mypgrp < 0 {
4061 opt_state_set("monitor", false); // c:3275 opts[MONITOR]=0
4062 return false;
4063 }
4064 let mut lastpgrp = mypgrp; // c:3228
4065 // c:3229-3232 — sigemptyset + sigaddset(SIGTTIN/SIGTTOU/SIGTSTP).
4066 let mut blockset: libc::sigset_t = unsafe { std::mem::zeroed() };
4067 unsafe {
4068 libc::sigemptyset(&mut blockset);
4069 libc::sigaddset(&mut blockset, libc::SIGTTIN); // c:3230
4070 libc::sigaddset(&mut blockset, libc::SIGTTOU); // c:3231
4071 libc::sigaddset(&mut blockset, libc::SIGTSTP); // c:3232
4072 }
4073 let oldset = signal_block(&blockset); // c:3233
4074 let mut loop_count = 0i32; // c:3234
4075 let interact = isset(INTERACTIVE);
4076 // c:3235 — `while ((ttpgrp = gettygrp()) != -1 && ttpgrp != mypgrp)`.
4077 loop {
4078 let ttpgrp = unsafe { libc::tcgetpgrp(0) }; // c:3235 gettygrp
4079 if ttpgrp == -1 || ttpgrp == mypgrp {
4080 break;
4081 }
4082 mypgrp = unsafe { libc::getpgrp() }; // c:3236
4083 sync_mypgrp(mypgrp); // c:3236 (global)
4084 if mypgrp == mypid {
4085 // c:3237
4086 if !interact {
4087 break;
4088 } // c:3239 attachtty no-op
4089 signal_setmask(&oldset); // c:3240
4090 crate::ported::utils::attachtty(mypgrp); // c:3241 attachtty(mypgrp)
4091 signal_block(&blockset); // c:3242
4092 }
4093 if mypgrp == unsafe { libc::tcgetpgrp(0) } {
4094 break;
4095 } // c:3244 gettygrp
4096 signal_setmask(&oldset); // c:3246
4097 // c:3247 — `if (read(0, NULL, 0) != 0) {}` — probe to provoke SIGT*.
4098 let mut buf: [u8; 0] = [];
4099 let _ = unsafe { libc::read(0, buf.as_mut_ptr() as *mut _, 0) }; // c:3247
4100 signal_block(&blockset); // c:3248
4101 mypgrp = unsafe { libc::getpgrp() }; // c:3249
4102 sync_mypgrp(mypgrp); // c:3249 (global)
4103 if mypgrp == lastpgrp {
4104 // c:3250
4105 if !interact {
4106 break;
4107 } // c:3252
4108 loop_count += 1;
4109 if loop_count == 100 {
4110 // c:3253
4111 break; // c:3261
4112 }
4113 }
4114 lastpgrp = mypgrp; // c:3265
4115 }
4116 // c:3267 — `if (mypgrp != mypid) { if (setpgrp(0, 0) == 0) ...; else opts[MONITOR] = 0; }`
4117 let mut acquired = mypgrp == mypid; // c:3267
4118 if !acquired {
4119 if unsafe { libc::setpgid(0, 0) } == 0 {
4120 // c:3268 setpgrp
4121 mypgrp = mypid; // c:3269
4122 sync_mypgrp(mypgrp); // c:3269 (global)
4123 crate::ported::utils::attachtty(mypgrp); // c:3270 attachtty(mypgrp)
4124 acquired = true;
4125 } else {
4126 opt_state_set("monitor", false); // c:3272 opts[MONITOR]=0
4127 }
4128 }
4129 sync_mypgrp(mypgrp); // resolved value visible to later attachtty readers
4130 signal_setmask(&oldset); // c:3274
4131 acquired // c:3278
4132}
4133
4134/// Port of `release_pgrp()` from `Src/jobs.c:3283`.
4135///
4136/// C body:
4137/// ```c
4138/// if (origpgrp != mypgrp) {
4139/// if (origpgrp) {
4140/// attachtty(origpgrp);
4141/// setpgrp(0, origpgrp);
4142/// }
4143/// mypgrp = origpgrp;
4144/// }
4145/// ```
4146///
4147///
4148/// Restores the original (parent shell's) process group before
4149/// the current shell exits, so terminal control returns to the
4150/// invoker.
4151#[cfg(unix)]
4152pub fn release_pgrp() {
4153 // c:3283
4154 let origpgrp = *ORIGPGRP
4155 .get_or_init(|| Mutex::new(0))
4156 .lock()
4157 .expect("origpgrp poisoned");
4158 let mypgrp = *MYPGRP
4159 .get_or_init(|| Mutex::new(0))
4160 .lock()
4161 .expect("mypgrp poisoned");
4162 if origpgrp != mypgrp {
4163 // c:3285
4164 // in linux pid namespaces, origpgrp may never have been set // c:3286
4165 if origpgrp != 0 {
4166 // c:3287
4167 unsafe {
4168 // attachtty(origpgrp); // c:3288
4169 libc::tcsetpgrp(0, origpgrp);
4170 libc::setpgid(0, origpgrp); // c:3289
4171 }
4172 }
4173 *MYPGRP
4174 .get_or_init(|| Mutex::new(0)) // c:3291
4175 .lock()
4176 .expect("mypgrp poisoned") = origpgrp;
4177 }
4178}
4179
4180// SP_RUNNING / MAX_PIPESTATS / MAXJOBS_ALLOC moved to canonical home
4181// at zsh_h.rs (ports of `Src/zsh.h:1097/1107/1166`). Re-export here
4182// so existing jobs.rs callers keep their unqualified usage, with
4183// single-source-of-truth values that can never drift from zsh_h.rs.
4184//
4185// Same consolidation pattern as the prior HISTFLAG_* / SUB_START /
4186// TERM_UNKNOWN fixes — duplicate const declarations are a known
4187// drift hazard.
4188
4189// the process group of the shell at startup // c:54
4190/// Port of `origpgrp` from `Src/jobs.c:58`.
4191pub static ORIGPGRP: OnceLock<Mutex<i32>> = OnceLock::new();
4192
4193// the process group of the shell // c:60
4194/// Port of `mypgrp` from `Src/jobs.c:63`.
4195pub static MYPGRP: OnceLock<Mutex<i32>> = OnceLock::new();
4196
4197// the last process group to attach to the terminal // c:66
4198/// Port of `last_attached_pgrp` from `Src/jobs.c:68`.
4199pub static LAST_ATTACHED_PGRP: OnceLock<Mutex<i32>> = OnceLock::new();
4200
4201// the job we are working on, or -1 if none // c:70
4202/// Port of `thisjob` from `Src/jobs.c:73`.
4203pub static THISJOB: OnceLock<Mutex<i32>> = OnceLock::new();
4204
4205// the current job (%+) // c:75
4206/// Port of `curjob` from `Src/jobs.c:78`.
4207pub static CURJOB: OnceLock<Mutex<i32>> = OnceLock::new();
4208
4209// the previous job (%-) */ // c:80
4210/// Port of `prevjob` from `Src/jobs.c:83`.
4211pub static PREVJOB: OnceLock<Mutex<i32>> = OnceLock::new();
4212
4213// the job table // c:85
4214/// Port of `jobtab` from `Src/jobs.c:88`.
4215pub static JOBTAB: OnceLock<Mutex<Vec<job>>> = OnceLock::new();
4216
4217// Size of the job table. // c:91
4218/// Port of `jobtabsize` from `Src/jobs.c:93`.
4219pub static JOBTABSIZE: OnceLock<Mutex<usize>> = OnceLock::new();
4220
4221// The highest numbered job in the jobtable // c:96
4222/// Port of `maxjob` from `Src/jobs.c:98`.
4223pub static MAXJOB: OnceLock<Mutex<usize>> = OnceLock::new();
4224
4225// If we have entered a subshell, the original shell's job table. // c:100
4226/// Port of `oldjobtab` from `Src/jobs.c:101`.
4227static OLDJOBTAB: OnceLock<Mutex<Vec<job>>> = OnceLock::new();
4228
4229// The size of that. // c:103
4230/// Port of `oldmaxjob` from `Src/jobs.c:104`.
4231static OLDMAXJOB: OnceLock<Mutex<usize>> = OnceLock::new();
4232
4233// 1 if ttyctl -f has been executed // c:119
4234/// Port of `ttyfrozen` from `Src/jobs.c:721`.
4235pub static TTYFROZEN: OnceLock<Mutex<i32>> = OnceLock::new();
4236
4237// pipestats array // c:131
4238/// Port of `numpipestats` from `Src/jobs.c:721`.
4239pub static NUMPIPESTATS: OnceLock<Mutex<usize>> = OnceLock::new();
4240/// Port of `pipestats` from `Src/jobs.c:721`.
4241pub static PIPESTATS: OnceLock<Mutex<[i32; MAX_PIPESTATS]>> = OnceLock::new();
4242
4243/// Default time format (from jobs.c DEFAULT_TIMEFMT)
4244pub const DEFAULT_TIMEFMT: &str = "%J %U user %S system %P cpu %*E total";
4245
4246/// Port of `static void waitonejob(Job jn)` from `Src/jobs.c:1748-1757`.
4247///
4248/// C body:
4249/// ```c
4250/// static void waitonejob(Job jn)
4251/// {
4252/// if (jn->procs || jn->auxprocs)
4253/// zwaitjob(jn - jobtab, 0);
4254/// else {
4255/// deletejob(jn, 0);
4256/// pipestats[0] = lastval;
4257/// numpipestats = 1;
4258/// }
4259/// }
4260/// ```
4261pub fn waitonejob(jn: &mut job) {
4262 // c:1750 — `if (jn->procs || jn->auxprocs)`
4263 if !jn.procs.is_empty() || !jn.auxprocs.is_empty() {
4264 // c:1751 — `zwaitjob(jn - jobtab, 0);` — pass job by reference
4265 // (Rust port takes &mut job vs C's jobtab-relative index since
4266 // jobs.rs's JOBTAB lookup-by-pointer-arithmetic isn't ported).
4267 zwaitjob(jn, 0);
4268 } else {
4269 // c:1753 — `deletejob(jn, 0);`
4270 deletejob(jn, false);
4271 // c:1754 — `pipestats[0] = lastval;`
4272 let lastval = crate::ported::builtin::LASTVAL.load(std::sync::atomic::Ordering::Relaxed);
4273 let p = PIPESTATS.get_or_init(|| Mutex::new([0; MAX_PIPESTATS]));
4274 if let Ok(mut pguard) = p.lock() {
4275 pguard[0] = lastval; // c:1754
4276 }
4277 // c:1755 — `numpipestats = 1;`
4278 let n = NUMPIPESTATS.get_or_init(|| Mutex::new(0));
4279 if let Ok(mut nguard) = n.lock() {
4280 *nguard = 1; // c:1755
4281 }
4282 // c:Src/params.c:5232 pipestatus_gsu — `$pipestatus` reads
4283 // walk the C `pipestats[]` array. zshrs's paramtab fast-path
4284 // reads from `paramtab["pipestatus"]` so mirror the C array
4285 // into the param table for visibility.
4286 crate::ported::params::setaparam("pipestatus", vec![lastval.to_string()]);
4287 }
4288}
4289
4290// See if pid has a recorded exit status. // c:2397
4291// Note we make no guarantee that the PIDs haven't wrapped, so this // c:2397
4292// may not be the right process. // c:2397
4293// // c:2397
4294// This is only used by wait, which must only work on each // c:2397
4295// pid once, so we need to remove the entry if we find it. // c:2397
4296/// Direct port of `int getbgstatus(pid_t pid)` from `Src/jobs.c:2397`.
4297/// Walks the global `bgstatus_list` for `pid`; if found, removes
4298/// the entry and returns its status.
4299pub fn getbgstatus(pid: i32) -> Option<i32> {
4300 // c:2397
4301 if let Ok(mut list) = bgstatus_list.lock() {
4302 if let Some(idx) = list.iter().position(|b| b.pid == pid) {
4303 // c:2402-2406
4304 let status = list[idx].status;
4305 list.remove(idx); // c:2407 rembgstatus
4306 bgstatus_count.fetch_sub(1, Ordering::Relaxed);
4307 return Some(status);
4308 }
4309 }
4310 None
4311}
4312
4313// ===========================================================
4314// Methods moved verbatim from src/ported/vm_helper because their
4315// C counterpart's source file maps 1:1 to this Rust module.
4316// Rust permits multiple inherent impl blocks for the same
4317// type within a crate, so call sites in vm_helper are unchanged.
4318// ===========================================================
4319
4320// BEGIN moved-from-exec-rs
4321// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
4322
4323// END moved-from-exec-rs
4324
4325#[cfg(test)]
4326mod tests {
4327 use super::*;
4328 use crate::ported::zsh_h::{STAT_BUILTIN, STAT_CHANGED, STAT_DONE, STAT_STOPPED, STAT_TIMED};
4329
4330 /// printtime expands rusage directives (`%M`/`%F`/`%R`/`%c`/`%w`) from a
4331 /// `timeinfo` argument. The directive set was untyped before the rusage
4332 /// fields landed on `timeinfo` — pin every directive to its source field.
4333 #[test]
4334 fn printtime_emits_rusage_directives() {
4335 let _g = crate::test_util::global_state_lock();
4336 let ti = timeinfo {
4337 ut: 500_000,
4338 st: 250_000,
4339 maxrss: 4096,
4340 majflt: 12,
4341 minflt: 345,
4342 nswap: 0,
4343 ixrss: 0,
4344 idrss: 0,
4345 isrss: 0,
4346 inblock: 7,
4347 oublock: 3,
4348 nvcsw: 99,
4349 nivcsw: 11,
4350 msgsnd: 0,
4351 msgrcv: 0,
4352 nsignals: 0,
4353 };
4354 let s = printtime(1.0, &ti, "%M/%F/%R/%I/%O/%c/%w", "my-job");
4355 assert_eq!(s, "4096/12/345/7/3/11/99");
4356 }
4357
4358 /// Verify percent (`%P`) uses (user+sys)/elapsed and rounds to int.
4359 #[test]
4360 fn printtime_percent_directive() {
4361 let _g = crate::test_util::global_state_lock();
4362 let ti = timeinfo {
4363 ut: 600_000,
4364 st: 400_000,
4365 ..Default::default()
4366 };
4367 // total=1.0s, elapsed=2.0s → 50%
4368 let s = printtime(2.0, &ti, "%P", "j");
4369 assert_eq!(s, "50%");
4370 }
4371
4372 /// `printtime %P` MUST guard against divide-by-zero when elapsed
4373 /// is 0.0 (instantaneous job or wall-clock timer didn't tick).
4374 /// A panic here would crash the shell mid-prompt-display every
4375 /// time `time` ran a no-op like `time :`. The C body at c:614-618
4376 /// has the `if (elapsed_secs > 0.0)` guard explicitly; the Rust
4377 /// port mirrors via the `if elapsed_secs > 0.0 { ... } else { 0 }`
4378 /// branch. Pin: input `(elapsed=0, user=0, sys=0)` → "0%", no panic.
4379 #[test]
4380 fn printtime_percent_zero_elapsed_no_panic() {
4381 let _g = crate::test_util::global_state_lock();
4382 let ti = timeinfo::default();
4383 // The catch_unwind wrapper isolates a potential panic so the
4384 // test reports a clean failure instead of crashing the harness.
4385 let result = std::panic::catch_unwind(|| printtime(0.0, &ti, "%P", "j"));
4386 let s = result.expect("c:614 — zero elapsed must NOT panic");
4387 assert_eq!(
4388 s, "0%",
4389 "c:615-618 — zero-elapsed percent must yield 0%, not NaN/Inf"
4390 );
4391 }
4392
4393 /// `printtime %P` truncates toward zero — matches C's `(int)`
4394 /// cast at c:893 (`int percent = 100.0 * total_time / elapsed;`).
4395 /// A regression that rounds-to-nearest (e.g. `.round()` instead
4396 /// of `as i32`) would report 100% for a job that used 99.6% CPU,
4397 /// hiding the small slack. Pin: 0.996s CPU / 1s elapsed → 99%.
4398 #[test]
4399 fn printtime_percent_truncates_toward_zero() {
4400 let _g = crate::test_util::global_state_lock();
4401 let ti = timeinfo {
4402 ut: 996_000,
4403 st: 0,
4404 ..Default::default()
4405 };
4406 let s = printtime(1.0, &ti, "%P", "j");
4407 assert_eq!(
4408 s, "99%",
4409 "c:893 — `(int)` cast truncates 99.6 → 99, not rounds to 100"
4410 );
4411 }
4412
4413 /// `%J` substitutes the job name verbatim.
4414 #[test]
4415 fn printtime_jobname_directive() {
4416 let _g = crate::test_util::global_state_lock();
4417 let ti = timeinfo::default();
4418 let s = printtime(0.0, &ti, "[%J]", "my command");
4419 assert_eq!(s, "[my command]");
4420 }
4421
4422 /// Time-form directives `%E`/`%U`/`%S` render seconds with `s` suffix.
4423 #[test]
4424 fn printtime_time_directives() {
4425 let _g = crate::test_util::global_state_lock();
4426 let ti = timeinfo {
4427 ut: 1_500_000,
4428 st: 500_000,
4429 ..Default::default()
4430 };
4431 let s = printtime(2.5, &ti, "%E %U %S", "j");
4432 assert_eq!(s, "2.50s 1.50s 0.50s");
4433 }
4434
4435 /// `%*E` / `%*U` / `%*S` use the `printhhmmss` HH:MM:SS form.
4436 /// Pin c:876-891 dispatch — the `*` modifier routes the directive
4437 /// to printhhmmss instead of the plain `{:.2}s` formatter. A
4438 /// regression that drops the `*` arm would silently fall back to
4439 /// the literal "%*E" output, breaking the `$TIMEFMT` default
4440 /// `%*E` slot most users have configured.
4441 #[test]
4442 fn printtime_star_directive_routes_to_hhmmss() {
4443 let _g = crate::test_util::global_state_lock();
4444 let ti = timeinfo::default();
4445 // 75 seconds → "1:15.00" (M:SS form, no hours).
4446 let s = printtime(75.0, &ti, "%*E", "j");
4447 assert_eq!(
4448 s, "1:15.00",
4449 "c:876-880 — %*E must route to printhhmmss for elapsed >= 60s"
4450 );
4451 // 3725s (1h2m5s) → "1:02:05.00" (H:MM:SS form).
4452 let s_hr = printtime(3725.0, &ti, "%*E", "j");
4453 assert_eq!(
4454 s_hr, "1:02:05.00",
4455 "c:880 + printhhmmss c:815-816 — elapsed >= 3600s yields H:MM:SS"
4456 );
4457 }
4458
4459 /// `should_report_time` honors `$REPORTMEMORY`: a job whose
4460 /// `maxrss` exceeds the threshold should trigger the report.
4461 #[test]
4462 fn should_report_time_uses_reportmemory() {
4463 let _g = crate::test_util::global_state_lock();
4464 // Clear PARAMTAB state so this test's REPORTMEMORY isn't
4465 // contaminated by earlier tests.
4466 setsparam("REPORTMEMORY", "100");
4467 let mut job = job::default();
4468 let mut proc = process::new(123);
4469 proc.ti.maxrss = 256; // > 100 KB threshold
4470 proc.bgtime = Some(Instant::now());
4471 proc.endtime = Some(Instant::now());
4472 job.procs.push(proc);
4473 job.stat = stat::INUSE;
4474 assert!(should_report_time(&job, -1.0));
4475 unsetparam("REPORTMEMORY");
4476 }
4477
4478 /// `should_report_time` returns false when both thresholds are
4479 /// disabled (REPORTTIME < 0, REPORTMEMORY unset).
4480 #[test]
4481 fn should_report_time_no_thresholds_false() {
4482 let _g = crate::test_util::global_state_lock();
4483 unsetparam("REPORTMEMORY");
4484 let mut job = job::default();
4485 job.procs.push(process::new(1));
4486 assert!(!should_report_time(&job, -1.0));
4487 }
4488
4489 /// `should_report_time` MUST short-circuit and return true when
4490 /// the job has STAT_TIMED set (c:1052-1053) — overriding all
4491 /// other gates including disabled thresholds AND zleactive.
4492 /// This is the contract that makes `time sleep 0.001` always
4493 /// print timing, even with `REPORTTIME` unset and inside ZLE.
4494 /// A regression that checks STAT_TIMED AFTER the threshold gates
4495 /// would silently swallow the report.
4496 #[test]
4497 fn should_report_time_stat_timed_overrides_all_gates() {
4498 let _g = crate::test_util::global_state_lock();
4499 // Disable both thresholds AND simulate zleactive — if STAT_TIMED
4500 // doesn't short-circuit, every other condition would return false.
4501 unsetparam("REPORTMEMORY");
4502 zleactive.store(1, Ordering::SeqCst);
4503
4504 let mut job = job::default();
4505 job.stat = stat::INUSE | stat::TIMED;
4506 job.procs.push(process::new(9001));
4507
4508 let reported = should_report_time(&job, -1.0);
4509
4510 // Cleanup before assert so a failure doesn't leak state.
4511 zleactive.store(0, Ordering::SeqCst);
4512 assert!(
4513 reported,
4514 "c:1052-1053 — STAT_TIMED MUST short-circuit to true regardless of threshold/zleactive"
4515 );
4516 }
4517
4518 /// `dumptime` emits one printtime line per process in a pipeline
4519 /// (c:1027-1029 walks `jn->procs` linked list, calling printtime
4520 /// per proc). Multi-stage pipeline → multiple lines.
4521 #[test]
4522 fn dumptime_emits_one_line_per_process() {
4523 let _g = crate::test_util::global_state_lock();
4524 setsparam("TIMEFMT", "%J");
4525 let mut job = job::default();
4526 let now = Instant::now();
4527 for (i, text) in ["echo a", "grep b", "tee c"].iter().enumerate() {
4528 let mut p = process::new(1000 + i as i32);
4529 p.bgtime = Some(now);
4530 p.endtime = Some(now + Duration::from_millis(10));
4531 p.text = text.to_string();
4532 job.procs.push(p);
4533 }
4534 let out = dumptime(&job).expect("expected timing output");
4535 assert_eq!(out, "echo a\ngrep b\ntee c");
4536 unsetparam("TIMEFMT");
4537 }
4538
4539 /// `handle_sub` clears SUPERJOB + sets WASSUPER when the subjob
4540 /// has completed without signal (c:296-297).
4541 #[test]
4542 fn handle_sub_clears_superjob_sets_wassuper_on_done() {
4543 let _g = crate::test_util::global_state_lock();
4544 // Two-job table: super at idx 0, sub at idx 1.
4545 let mut tab = vec![job::default(), job::default()];
4546 tab[0].stat = stat::INUSE | stat::SUPERJOB;
4547 tab[0].other = 1;
4548 tab[0].gleader = unsafe { libc::getpgrp() };
4549 // Add one exited proc to the super so the WASSUPER branch
4550 // (c:293-326) executes cleanly without the signaled branch.
4551 let mut p = process::new(unsafe { libc::getpid() });
4552 p.status = 0; // exited 0 (WIFEXITED && WEXITSTATUS==0)
4553 tab[0].procs.push(p);
4554 // Subjob: marked DONE with no procs (the c:279 trigger).
4555 tab[1].stat = stat::INUSE | stat::DONE;
4556 tab[1].other = unsafe { libc::getpid() };
4557
4558 handle_sub(&mut tab, 0, false);
4559
4560 assert_eq!(tab[0].stat & stat::SUPERJOB, 0, "SUPERJOB cleared");
4561 assert!(tab[0].stat & stat::WASSUPER != 0, "WASSUPER set");
4562 }
4563
4564 /// `update_job` sets DONE + CHANGED, writes LASTVAL2, when all
4565 /// procs have exited.
4566 #[test]
4567 fn update_job_done_writes_lastval2() {
4568 let _g = crate::test_util::global_state_lock();
4569 LASTVAL2.store(-1, Ordering::SeqCst);
4570 let mut job = job::default();
4571 let mut p1 = process::new(1001);
4572 p1.status = 0; // exited 0 (WIFEXITED && WEXITSTATUS=0)
4573 let mut p2 = process::new(1002);
4574 p2.status = 7 << 8; // exited 7 (last proc, sets val)
4575 job.procs.push(p1);
4576 job.procs.push(p2);
4577 let committed = update_job(&mut job);
4578 assert!(committed, "update_job should commit when all done");
4579 assert!(job.stat & stat::DONE != 0);
4580 assert!(job.stat & stat::CHANGED != 0);
4581 assert_eq!(
4582 LASTVAL2.load(Ordering::SeqCst),
4583 7,
4584 "lastval2 = WEXITSTATUS of last proc"
4585 );
4586 }
4587
4588 /// `update_job` returns false (no commit) when any main proc is
4589 /// still running.
4590 #[test]
4591 fn update_job_running_returns_false() {
4592 let _g = crate::test_util::global_state_lock();
4593 let mut job = job::default();
4594 let mut p = process::new(2001);
4595 p.status = SP_RUNNING;
4596 job.procs.push(p);
4597 assert!(!update_job(&mut job));
4598 // No flag flips when not committed.
4599 assert_eq!(job.stat & stat::DONE, 0);
4600 }
4601
4602 /// `update_job` MUST early-return on a still-running AUXPROC
4603 /// (c:472-473) BEFORE inspecting main procs. Auxprocs are the
4604 /// process-substitution feeders (`<(cmd)`); if one is still
4605 /// running, the surrounding job is not yet collectible even
4606 /// when every main proc has exited. A regression that walks
4607 /// main procs first and commits on all-main-done would close
4608 /// the auxproc's pipe prematurely and lose its output.
4609 #[test]
4610 fn update_job_running_auxproc_short_circuits_before_main_walk() {
4611 let _g = crate::test_util::global_state_lock();
4612 let mut job = job::default();
4613 // Main proc has fully EXITED.
4614 let mut main = process::new(10001);
4615 main.status = 0; // exited 0
4616 job.procs.push(main);
4617 // But an auxproc is still RUNNING.
4618 let mut aux = process::new(10002);
4619 aux.status = SP_RUNNING;
4620 job.auxprocs.push(aux);
4621
4622 let committed = update_job(&mut job);
4623 assert!(
4624 !committed,
4625 "c:472-473 — running auxproc must short-circuit even when main procs are done"
4626 );
4627 // The main proc's status word must NOT have been re-interpreted;
4628 // the DONE flag must not have been set; LASTVAL2 must not have
4629 // been written (we don't check LASTVAL2 directly to avoid
4630 // cross-test ordering, but the DONE flag check catches the
4631 // regression class).
4632 assert_eq!(
4633 job.stat & stat::DONE,
4634 0,
4635 "STAT_DONE must not be set when an auxproc is still running"
4636 );
4637 assert_eq!(
4638 job.stat & stat::CHANGED,
4639 0,
4640 "STAT_CHANGED must not be set on early-return"
4641 );
4642 }
4643
4644 /// `update_job` sets STOPPED + CHANGED when any proc is stopped
4645 /// (and clears DONE).
4646 #[test]
4647 fn update_job_stopped_sets_stopped_changed() {
4648 let _g = crate::test_util::global_state_lock();
4649 let mut job = job::default();
4650 let mut p = process::new(3001);
4651 p.status = 0x117f; // WIFSTOPPED-shaped (lower bits = 0x7f, upper = sig)
4652 job.procs.push(p);
4653 let committed = update_job(&mut job);
4654 assert!(committed);
4655 assert!(job.stat & stat::STOPPED != 0);
4656 assert!(job.stat & stat::CHANGED != 0);
4657 assert_eq!(job.stat & stat::DONE, 0);
4658 }
4659
4660 /// `spawnjob` with thisjob=-1 is a no-op (c:1898 DPUTS).
4661 #[test]
4662 fn spawnjob_no_thisjob_is_noop() {
4663 let _g = crate::test_util::global_state_lock();
4664 *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = -1;
4665 // Should not panic.
4666 spawnjob();
4667 // thisjob stays at -1.
4668 assert_eq!(*THISJOB.get().unwrap().lock().unwrap(), -1);
4669 }
4670
4671 /// `spawnjob` deletes the job entry if it has no procs (c:1915-1916).
4672 /// Cursh-clearing + INUSE side effects from the previous Rust port
4673 /// don't fire because the path's not exercised that way.
4674 #[test]
4675 fn spawnjob_deletes_empty_job() {
4676 let _g = crate::test_util::global_state_lock();
4677 // Wire up THISJOB → 1; JOBTAB[1] empty INUSE job.
4678 let mut tab_init = vec![job::default(); 3];
4679 tab_init[1].stat = stat::INUSE;
4680 *JOBTAB
4681 .get_or_init(|| Mutex::new(Vec::new()))
4682 .lock()
4683 .unwrap() = tab_init;
4684 *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = 1;
4685 spawnjob();
4686 // After: thisjob = -1, the entry stripped of INUSE by deletejob.
4687 assert_eq!(*THISJOB.get().unwrap().lock().unwrap(), -1);
4688 let tab = JOBTAB.get().unwrap().lock().unwrap();
4689 // deletejob clears stat / detaches procs.
4690 assert_eq!(tab[1].stat & stat::INUSE, 0);
4691 }
4692
4693 /// `handle_sub` STOPPED branch (c:328-339): when subjob is stopped,
4694 /// superjob inherits STOPPED and proc statuses propagate from the
4695 /// subjob's first proc.
4696 #[test]
4697 fn handle_sub_stopped_branch_propagates() {
4698 let _g = crate::test_util::global_state_lock();
4699 let mut tab = vec![job::default(), job::default()];
4700 tab[0].stat = stat::INUSE | stat::SUPERJOB;
4701 tab[0].other = 1;
4702 let mut p = process::new(1234);
4703 p.status = SP_RUNNING;
4704 tab[0].procs.push(p);
4705 tab[1].stat = stat::INUSE | stat::STOPPED;
4706 let mut sp = process::new(5678);
4707 sp.status = 0x117f; // WIFSTOPPED w/ TSTP-ish status
4708 tab[1].procs.push(sp);
4709
4710 let ret = handle_sub(&mut tab, 0, false);
4711 assert_eq!(ret, 1, "STOPPED branch returns 1");
4712 assert!(tab[0].stat & stat::STOPPED != 0, "super inherits STOPPED");
4713 // First super-proc status overwritten with subjob's first-proc status.
4714 assert_eq!(tab[0].procs[0].status, 0x117f);
4715 }
4716
4717 /// `dumptime` returns None for a job with no processes (c:1025-1026).
4718 #[test]
4719 fn dumptime_empty_job_returns_none() {
4720 let _g = crate::test_util::global_state_lock();
4721 let job = job::default();
4722 assert!(dumptime(&job).is_none());
4723 }
4724
4725 /// `dumptime` MUST skip procs whose bgtime/endtime pair is
4726 /// incomplete rather than panic or produce garbage timings.
4727 /// A backgrounded proc that hasn't been waitpid'd yet has
4728 /// `endtime=None`; one that was attached mid-pipeline has
4729 /// `bgtime=None`. The C body's `dtime_ts(&pn->bgtime, &pn->endtime)`
4730 /// reads both unconditionally — the Rust port's `?` operator
4731 /// in the filter_map skips the row. Pin: a job whose only
4732 /// proc lacks endtime → dumptime returns None (no garbage).
4733 #[test]
4734 fn dumptime_skips_proc_without_endtime() {
4735 let _g = crate::test_util::global_state_lock();
4736 setsparam("TIMEFMT", "%E");
4737 let mut job = job::default();
4738 let mut p = process::new(11001);
4739 p.bgtime = Some(Instant::now());
4740 p.endtime = None; // backgrounded, not yet reaped
4741 p.text = "incomplete".to_string();
4742 job.procs.push(p);
4743
4744 // The fn must not panic on Option::None.unwrap().
4745 let result = std::panic::catch_unwind(|| dumptime(&job));
4746 let out = result.expect("missing endtime must not panic");
4747 assert!(
4748 out.is_none(),
4749 "filter_map drops procs without bg/end pair → empty result → None"
4750 );
4751
4752 unsetparam("TIMEFMT");
4753 }
4754
4755 /// `dumptime` cites each process's OWN bgtime→endtime elapsed,
4756 /// not a job-wide aggregate. The c:1028 `dtime_ts(&pn->bgtime,
4757 /// &pn->endtime)` per-iteration call is the load-bearing
4758 /// difference between "1 line per pipeline" (the bug) and "1
4759 /// line per process" (the C contract). Pin distinct elapsed
4760 /// values to catch a regression that recomputes once for the job.
4761 #[test]
4762 fn dumptime_uses_per_process_elapsed() {
4763 let _g = crate::test_util::global_state_lock();
4764 setsparam("TIMEFMT", "%E");
4765 let mut job = job::default();
4766 let t0 = Instant::now();
4767 // Three procs with distinct elapsed times: 100ms, 300ms, 600ms.
4768 for (i, ms) in [100u64, 300, 600].iter().enumerate() {
4769 let mut p = process::new(8000 + i as i32);
4770 p.bgtime = Some(t0);
4771 p.endtime = Some(t0 + Duration::from_millis(*ms));
4772 p.text = format!("p{}", i);
4773 job.procs.push(p);
4774 }
4775 let out = dumptime(&job).expect("non-empty job → Some");
4776 let lines: Vec<&str> = out.lines().collect();
4777 assert_eq!(
4778 lines.len(),
4779 3,
4780 "must produce one line per proc, got {:?}",
4781 lines
4782 );
4783 // %E formats as "X.XXs". Verify distinct values across lines.
4784 // A regression that aggregates would print 3 copies of the
4785 // same (sum-of-elapsed) figure.
4786 let unique: std::collections::HashSet<&&str> = lines.iter().collect();
4787 assert_eq!(
4788 unique.len(),
4789 3,
4790 "each line must carry its own proc's elapsed; got duplicates: {:?}",
4791 lines
4792 );
4793 unsetparam("TIMEFMT");
4794 }
4795
4796 /// `printjob` appends the dumptime block when the job is
4797 /// STAT_TIMED (c:1220-1221 in printjob).
4798 #[test]
4799 fn printjob_appends_timing_when_stat_timed() {
4800 let _g = crate::test_util::global_state_lock();
4801 setsparam("TIMEFMT", "%J");
4802 let mut job = job::default();
4803 job.stat = stat::INUSE | stat::TIMED | stat::DONE;
4804 let mut p = process::new(42);
4805 p.bgtime = Some(Instant::now());
4806 p.endtime = Some(Instant::now() + Duration::from_millis(5));
4807 p.text = "echo hi".to_string();
4808 p.status = 0; // exited 0
4809 job.procs.push(p);
4810 let out = printjob(&job, 1, 0, Some(1), None);
4811 assert!(
4812 out.contains("echo hi"),
4813 "expected status line; got: {:?}",
4814 out
4815 );
4816 // Last line should be the dumptime output (%J → text).
4817 assert!(
4818 out.ends_with("echo hi"),
4819 "expected timing line at end; got: {:?}",
4820 out
4821 );
4822 unsetparam("TIMEFMT");
4823 }
4824
4825 /// `update_job` STAT_SUBJOB short-circuit (c:507-540): when the
4826 /// stopped job is a SUBJOB, the c:514 `jn->stat |= STAT_CHANGED
4827 /// | STAT_STOPPED` flag write must fire BEFORE the early-return
4828 /// to the super-job-SIGTSTP cascade. A regression that swaps the
4829 /// order would leave the listing scanner blind to the stop.
4830 #[test]
4831 fn update_job_subjob_stop_sets_flags_before_early_return() {
4832 let _g = crate::test_util::global_state_lock();
4833 let mut job = job::default();
4834 job.stat = stat::INUSE | stat::SUBJOB; // mark as SUBJOB pre-stop
4835 let mut p = process::new(7001);
4836 p.status = 0x117f; // WIFSTOPPED-shaped (low byte = 0x7F)
4837 job.procs.push(p);
4838
4839 assert!(update_job(&mut job));
4840 assert!(
4841 job.stat & stat::CHANGED != 0,
4842 "c:514 — SUBJOB stop must set CHANGED so the jobs scanner picks it up"
4843 );
4844 assert!(
4845 job.stat & stat::STOPPED != 0,
4846 "c:514 — SUBJOB stop must mark STOPPED"
4847 );
4848 assert_eq!(
4849 job.stat & stat::SUBJOB,
4850 stat::SUBJOB,
4851 "SUBJOB flag preserved through update"
4852 );
4853 }
4854
4855 /// `update_job` is idempotent across multiple calls on an
4856 /// already-STOPPED non-subjob (c:541-542 — `if (jn->stat &
4857 /// STAT_STOPPED) return;`). Without this short-circuit, every
4858 /// re-entry would re-set STAT_CHANGED, causing the `jobs`
4859 /// builtin to re-print the same job on every scan.
4860 #[test]
4861 fn update_job_already_stopped_short_circuits() {
4862 let _g = crate::test_util::global_state_lock();
4863 let mut job = job::default();
4864 job.stat = stat::INUSE | stat::STOPPED; // pre-stopped, not SUBJOB
4865 let mut p = process::new(12001);
4866 p.status = 0x117f; // WIFSTOPPED-shaped
4867 job.procs.push(p);
4868
4869 // First call: STOPPED already set, this is the re-entry case.
4870 // C: c:541-542 early-return → no CHANGED set.
4871 let stat_before = job.stat;
4872 let committed = update_job(&mut job);
4873 assert!(committed, "early-return path still reports 'commit'");
4874 assert_eq!(
4875 job.stat, stat_before,
4876 "c:541-542 — re-entry on already-STOPPED job must not flip flags"
4877 );
4878 }
4879
4880 /// `update_job` last-proc-signaled path (c:487-495): when the
4881 /// LAST proc in the pipeline was killed by a signal, val gets the
4882 /// `0o200 | WTERMSIG(status)` encoding written to `LASTVAL2`.
4883 /// The 0o200 high bit is zsh's convention for distinguishing
4884 /// "killed by signal N" from "exited with status N" in `$?` and
4885 /// `$pipestatus`. Without this encoding, a pipeline ending in a
4886 /// SIGTERM'd command would report exit-status N instead of 128+N.
4887 ///
4888 /// Status word 15 (= SIGTERM raw) reads as WIFSIGNALED on POSIX:
4889 /// low 7 bits = 15 (not 0 = exited, not 0x7F = stopped)
4890 /// → WTERMSIG returns 15, the SIGTERM number.
4891 #[test]
4892 fn update_job_last_proc_signaled_sets_high_bit_val() {
4893 let _g = crate::test_util::global_state_lock();
4894 LASTVAL2.store(-1, Ordering::SeqCst);
4895
4896 let mut job = job::default();
4897 let mut p1 = process::new(6001);
4898 p1.status = 0; // exited 0 (clean predecessor)
4899 let mut p2 = process::new(6002);
4900 p2.status = 15; // killed by SIGTERM
4901 job.procs.push(p1);
4902 job.procs.push(p2);
4903
4904 assert!(update_job(&mut job));
4905 let lv2 = LASTVAL2.load(Ordering::SeqCst);
4906 assert_eq!(
4907 lv2 & 0o200,
4908 0o200,
4909 "c:489-490 — WIFSIGNALED last-proc must set the 0o200 high bit"
4910 );
4911 assert_eq!(
4912 lv2 & 0x7f,
4913 15,
4914 "c:490 — low 7 bits must hold WTERMSIG (SIGTERM=15)"
4915 );
4916 }
4917
4918 #[test]
4919 fn test_process_new() {
4920 let _g = crate::test_util::global_state_lock();
4921 let proc = process::new(1234);
4922 assert_eq!(proc.pid, 1234);
4923 assert!(proc.is_running());
4924 }
4925
4926 #[test]
4927 fn test_job_new() {
4928 let _g = crate::test_util::global_state_lock();
4929 let job = job::new();
4930 assert_eq!(job.stat, 0);
4931 assert!(!job.is_done());
4932 assert!(!job.is_stopped());
4933 }
4934
4935 // `test_job_table_new` / `test_job_table_remove` moved to
4936 // src/exec_jobs.rs alongside the JobTable struct.
4937
4938 #[test]
4939 fn test_job_make_running() {
4940 let _g = crate::test_util::global_state_lock();
4941 let mut job = job::new();
4942 job.stat |= stat::STOPPED;
4943 job.procs.push(process {
4944 status: 0x007f,
4945 ..process::new(1234)
4946 }); // Stopped
4947
4948 job.make_running();
4949 assert!(!job.is_stopped());
4950 assert!(job.procs[0].is_running());
4951 }
4952
4953 #[test]
4954 fn test_format_job() {
4955 let _g = crate::test_util::global_state_lock();
4956 let mut job = job::new();
4957 job.text = "vim file.txt".to_string();
4958 job.stat |= stat::STOPPED;
4959
4960 let formatted = printjob(&job, 1, 0, Some(1), None);
4961 // Real zsh format: `[N]<space><space><marker><space>...`
4962 // The job number is followed by two spaces, then the
4963 // current/previous-job marker (`+`, `-`, ` `), then a
4964 // single space, then the status field. Match the marker
4965 // separately to avoid the previous bogus `[1]+` substring
4966 // assertion (which never matched because the printjob
4967 // format uses two spaces between `]` and the marker).
4968 assert!(formatted.contains("[1]"));
4969 assert!(formatted.contains("+"));
4970 assert!(formatted.contains("suspended") || formatted.contains("Stopped"));
4971 assert!(formatted.contains("vim file.txt"));
4972 }
4973
4974 // `test_job_state_enum` moved to src/exec_jobs.rs.
4975
4976 #[test]
4977 fn test_isanum_handles_minus() {
4978 let _g = crate::test_util::global_state_lock();
4979 // C: while (*s == '-' || idigit(*s)) s++; return *s == '\0';
4980 assert!(isanum("123"));
4981 assert!(isanum("-1")); // previous job spec
4982 assert!(isanum("---")); // weird but matches C semantics
4983 assert!(isanum("12-34")); // accepted by C
4984 assert!(!isanum("")); // empty rejected
4985 assert!(!isanum("abc")); // letters rejected
4986 assert!(!isanum("1a")); // mixed rejected
4987 }
4988
4989 #[test]
4990 fn test_havefiles_walks_table() {
4991 let _g = crate::test_util::global_state_lock();
4992 let mut tab = vec![job::new(), job::new(), job::new()];
4993 tab[1].stat = stat::INUSE;
4994 tab[1].filelist = vec![jobfile { name: Some("/tmp/foo".to_string()), fd: 0, is_fd: 0 }];
4995 assert!(havefiles(&tab));
4996 // job marked but no files → no.
4997 tab[1].filelist.clear();
4998 assert!(!havefiles(&tab));
4999 // Files but no stat (released slot) → C `jobtab[i].stat &&` requires both.
5000 tab[2].stat = 0;
5001 tab[2].filelist = vec![jobfile { name: Some("/tmp/bar".to_string()), fd: 0, is_fd: 0 }];
5002 assert!(!havefiles(&tab));
5003 }
5004
5005 #[test]
5006 fn test_storepipestats_decodes_status() {
5007 let _g = crate::test_util::global_state_lock();
5008 let mut job = job::new();
5009 // process 1: exit 0
5010 let mut p1 = process::new(100);
5011 p1.status = 0;
5012 // process 2: exit 1 (status 0x0100)
5013 let mut p2 = process::new(101);
5014 p2.status = 0x0100;
5015 // process 3: signal 9 (SIGKILL — status low-byte 0x09)
5016 let mut p3 = process::new(102);
5017 p3.status = 0x09;
5018 job.procs = vec![p1, p2, p3];
5019 let (stats, pipefail) = storepipestats(&job);
5020 assert_eq!(stats.len(), 3);
5021 assert_eq!(stats[0], 0); // exit 0
5022 assert_eq!(stats[1], 1); // exit 1
5023 assert_eq!(stats[2], 0o200 | 9); // signaled with SIGKILL
5024 assert_eq!(pipefail, 0o200 | 9); // last non-zero
5025 }
5026
5027 #[test]
5028 fn test_expandjobtab_respects_max() {
5029 let _g = crate::test_util::global_state_lock();
5030 let mut tab = vec![job::new(); 950];
5031 // 950 + 50 = 1000 ≤ MAX_MAXJOBS, OK.
5032 assert!(expandjobtab(&mut tab, 0));
5033 assert_eq!(tab.len(), 1000);
5034 // Next chunk would exceed cap.
5035 assert!(!expandjobtab(&mut tab, 0));
5036 assert_eq!(tab.len(), 1000);
5037 }
5038
5039 #[test]
5040 fn test_addfilelist_fd_vs_name() {
5041 let _g = crate::test_util::global_state_lock();
5042 let mut job = job::new();
5043 addfilelist(&mut job, Some("/tmp/zshrs-test.X"), -1);
5044 addfilelist(&mut job, None, 7);
5045 assert_eq!(job.filelist.len(), 2);
5046 assert_eq!(job.filelist[0].is_fd, 0);
5047 assert_eq!(job.filelist[0].name.as_deref(), Some("/tmp/zshrs-test.X"));
5048 assert_eq!(job.filelist[1].is_fd, 1);
5049 assert_eq!(job.filelist[1].fd, 7);
5050 }
5051
5052 #[test]
5053 fn test_hasprocs_index_bounded() {
5054 let _g = crate::test_util::global_state_lock();
5055 let mut tab = vec![job::new(), job::new()];
5056 tab[0].procs.push(process::new(1));
5057 assert!(hasprocs(&tab, 0));
5058 assert!(!hasprocs(&tab, 1));
5059 // Out-of-range returns false (matches C's negative-job DPUTS+0).
5060 assert!(!hasprocs(&tab, 99));
5061 }
5062
5063 #[test]
5064 fn test_makerunning_clears_stopped() {
5065 let _g = crate::test_util::global_state_lock();
5066 let mut tab = vec![job::new(), job::new()];
5067 tab[0].stat = stat::STOPPED;
5068 let mut p = process::new(42);
5069 p.status = 0x7f; // WIFSTOPPED
5070 tab[0].procs.push(p);
5071 makerunning(&mut tab, 0);
5072 assert_eq!(tab[0].stat & stat::STOPPED, 0);
5073 assert_eq!(tab[0].procs[0].status, SP_RUNNING);
5074 }
5075
5076 // ===== Tests for sigmsg (this session's table-ified port).
5077
5078 #[test]
5079 fn sigmsg_known_signals_render_canonical_text() {
5080 let _g = crate::test_util::global_state_lock();
5081 // Verifies the SIG_MSG lookup table matches C's sig_msg[] for
5082 // the signals that exist on every Unix. These strings are part
5083 // of the user-visible output of `jobs -l` / signal-death
5084 // reports — regressions would change observable behavior.
5085 assert_eq!(sigmsg(libc::SIGHUP), "hangup");
5086 assert_eq!(sigmsg(libc::SIGINT), "interrupt");
5087 assert_eq!(sigmsg(libc::SIGQUIT), "quit");
5088 assert_eq!(sigmsg(libc::SIGKILL), "killed");
5089 assert_eq!(sigmsg(libc::SIGSEGV), "segmentation fault");
5090 assert_eq!(sigmsg(libc::SIGPIPE), "broken pipe");
5091 assert_eq!(sigmsg(libc::SIGTERM), "terminated");
5092 assert_eq!(sigmsg(libc::SIGCHLD), "child exited");
5093 assert_eq!(sigmsg(libc::SIGCONT), "continued");
5094 }
5095
5096 #[test]
5097 fn sigmsg_unknown_signal_returns_default() {
5098 let _g = crate::test_util::global_state_lock();
5099 // c:1118 — `sig <= SIGCOUNT ? sig_msg[sig] : unknown`. Pick a
5100 // signal number outside the standard set (libc gives no
5101 // SIGCOUNT abstraction, so use a deliberately-high number).
5102 assert_eq!(sigmsg(9999), "unknown signal");
5103 assert_eq!(sigmsg(-1), "unknown signal");
5104 assert_eq!(sigmsg(0), "unknown signal");
5105 }
5106
5107 // ===== Test for get_usage (collapsed this session).
5108
5109 #[cfg(unix)]
5110 #[test]
5111 fn get_usage_returns_non_negative_times() {
5112 let _g = crate::test_util::global_state_lock();
5113 // C: getrusage(RUSAGE_CHILDREN, &child_usage). Even without
5114 // children, both fields must be >= 0 — the closure that maps
5115 // (tv_sec, tv_usec) → microseconds shouldn't underflow.
5116 let ti = get_usage();
5117 assert!(ti.ut >= 0);
5118 assert!(ti.st >= 0);
5119 }
5120
5121 /// c:752 — `printhhmmss` formats `HH:MM:SS.MS` for `time` builtin.
5122 /// Verifies the colon + dot separators are present. Regression
5123 /// dropping them breaks every time-output parser in user scripts.
5124 #[test]
5125 fn printhhmmss_formats_with_colons_and_dot() {
5126 let _g = crate::test_util::global_state_lock();
5127 let s = printhhmmss(3661.5);
5128 assert!(s.contains(':'));
5129 assert!(
5130 s.contains('.'),
5131 "millis must be present after dot (got {s:?})"
5132 );
5133 }
5134
5135 /// c:752 — zero seconds renders cleanly (no `-0` artifact).
5136 #[test]
5137 fn printhhmmss_zero_seconds_well_formed() {
5138 let _g = crate::test_util::global_state_lock();
5139 let s = printhhmmss(0.0);
5140 assert!(
5141 !s.starts_with('-'),
5142 "zero must not render with leading minus (got {s:?})"
5143 );
5144 }
5145
5146 /// c:721 — `get_clktck` returns sysconf(_SC_CLK_TCK). MUST be > 0
5147 /// on every POSIX (typically 100 or 1000). A zero/negative would
5148 /// divide by zero in every CPU-time computation.
5149 #[cfg(unix)]
5150 #[test]
5151 fn get_clktck_returns_positive_value() {
5152 let _g = crate::test_util::global_state_lock();
5153 assert!(get_clktck() > 0, "_SC_CLK_TCK must be positive");
5154 }
5155
5156 /// c:1422 — `deletefilelist(disowning=true)` MUST clear all
5157 /// entries (since the disowned job no longer owns its open fds).
5158 /// Regression that retains entries on disown would leak them.
5159 #[test]
5160 fn deletefilelist_disown_clears_all_entries() {
5161 let _g = crate::test_util::global_state_lock();
5162 let mut j = job::new();
5163 addfilelist(&mut j, Some("/tmp/a"), -1);
5164 addfilelist(&mut j, None, 7);
5165 assert_eq!(j.filelist.len(), 2);
5166 deletefilelist(&mut j, true);
5167 assert!(
5168 j.filelist.is_empty(),
5169 "disowning=true must clear all filelist entries"
5170 );
5171 }
5172
5173 /// c:260 — `super_job` returns None for top-level jobs (no super).
5174 /// Regression treating "no super" as a valid index would crash
5175 /// SIGCHLD reaping with phantom job lookups.
5176 #[test]
5177 fn super_job_returns_none_for_top_level_job() {
5178 let _g = crate::test_util::global_state_lock();
5179 let tab = vec![job::new()];
5180 assert!(super_job(&tab, 0).is_none());
5181 }
5182
5183 /// `Src/zsh.h:1073-1094` — `STAT_*` flag values are load-bearing
5184 /// numeric constants. Pin every `mod stat` value matches the
5185 /// canonical C define. Previously the Rust port used sequential
5186 /// `1 << N` shifts producing DIFFERENT values for nearly every
5187 /// flag.
5188 #[test]
5189 fn stat_flags_match_c_zsh_h_canonical_values() {
5190 let _g = crate::test_util::global_state_lock();
5191 assert_eq!(stat::CHANGED, 0x0001, "Src/zsh.h:1073");
5192 assert_eq!(stat::STOPPED, 0x0002, "Src/zsh.h:1074");
5193 assert_eq!(stat::TIMED, 0x0004, "Src/zsh.h:1075");
5194 assert_eq!(stat::DONE, 0x0008, "Src/zsh.h:1076");
5195 assert_eq!(stat::LOCKED, 0x0010, "Src/zsh.h:1077");
5196 assert_eq!(stat::NOPRINT, 0x0020, "Src/zsh.h:1079");
5197 assert_eq!(stat::INUSE, 0x0040, "Src/zsh.h:1081");
5198 assert_eq!(stat::SUPERJOB, 0x0080, "Src/zsh.h:1082");
5199 assert_eq!(stat::SUBJOB, 0x0100, "Src/zsh.h:1083");
5200 assert_eq!(stat::WASSUPER, 0x0200, "Src/zsh.h:1084");
5201 assert_eq!(stat::CURSH, 0x0400, "Src/zsh.h:1086");
5202 assert_eq!(stat::NOSTTY, 0x0800, "Src/zsh.h:1087");
5203 assert_eq!(stat::ATTACH, 0x1000, "Src/zsh.h:1089");
5204 assert_eq!(stat::SUBLEADER, 0x2000, "Src/zsh.h:1090");
5205 assert_eq!(stat::BUILTIN, 0x4000, "Src/zsh.h:1092");
5206 }
5207
5208 /// stat flag values must also match the canonical `STAT_*`
5209 /// definitions in `zsh_h.rs` (which already match C). Pin the
5210 /// equality so the two definitions can't drift independently.
5211 #[test]
5212 fn stat_flags_match_zsh_h_module_values() {
5213 let _g = crate::test_util::global_state_lock();
5214 assert_eq!(stat::CHANGED, STAT_CHANGED);
5215 assert_eq!(stat::STOPPED, STAT_STOPPED);
5216 assert_eq!(stat::TIMED, STAT_TIMED);
5217 assert_eq!(stat::DONE, STAT_DONE);
5218 assert_eq!(stat::SUPERJOB, STAT_SUPERJOB);
5219 assert_eq!(stat::INUSE, STAT_INUSE);
5220 assert_eq!(stat::ATTACH, STAT_ATTACH);
5221 assert_eq!(stat::BUILTIN, STAT_BUILTIN);
5222 }
5223
5224 /// `Src/jobs.c:1511-1526` — `deletejob` calls `freejob` at c:1525
5225 /// to ensure a full per-job state reset (pwd/ty/other/stty_in_env
5226 /// also clear). Previously the Rust port did an ad-hoc clear of
5227 /// procs/auxprocs/stat and skipped `freejob` entirely — pwd/ty
5228 /// stayed populated across slot reuse.
5229 #[test]
5230 fn deletejob_calls_freejob_to_clear_all_state() {
5231 let _g = crate::test_util::global_state_lock();
5232 let mut jn = job::new();
5233 jn.pwd = Some("/tmp/deletejob-pwd".to_string());
5234 jn.other = 42;
5235 jn.stty_in_env = 1;
5236 jn.stat = stat::SUPERJOB;
5237 deletejob(&mut jn, false);
5238 // c:1525 — freejob(jn, 1) called → all fields reset.
5239 assert_eq!(jn.pwd, None, "c:1525 — pwd cleared via freejob chain");
5240 assert_eq!(jn.other, 0, "c:1525 — other cleared");
5241 assert_eq!(jn.stty_in_env, 0, "c:1525 — stty_in_env cleared");
5242 assert_eq!(jn.stat, 0, "c:1525 — stat cleared");
5243 }
5244
5245 /// `Src/jobs.c:1457-1495` — `freejob(jn, deleting)`. Resets ALL
5246 /// per-job state including `pwd`, `ty`, `other`, `stty_in_env`
5247 /// (previously missing). Pin: pre-populate every field, call
5248 /// freejob, verify ALL reset to zero/empty/None.
5249 #[test]
5250 fn freejob_resets_all_per_job_state_fields() {
5251 let _g = crate::test_util::global_state_lock();
5252 let mut jn = job::new();
5253 // Pre-populate every freejob-reset field.
5254 jn.pwd = Some("/tmp/saved-pwd".to_string());
5255 jn.gleader = 12345;
5256 jn.other = 7;
5257 jn.stat = stat::SUPERJOB;
5258 jn.stty_in_env = 1;
5259 jn.text = "echo foo".to_string();
5260 // Call freejob.
5261 freejob(&mut jn, false);
5262 // All fields reset.
5263 assert_eq!(jn.pwd, None, "c:1477-1479 — pwd reset to None");
5264 assert_eq!(jn.gleader, 0, "c:1489 — gleader = 0");
5265 assert_eq!(jn.other, 0, "c:1489 — other = 0");
5266 assert_eq!(jn.stat, 0, "c:1490 — stat = 0");
5267 assert_eq!(jn.stty_in_env, 0, "c:1490 — stty_in_env = 0");
5268 assert_eq!(jn.text, "", "Rust-only: text cleared");
5269 assert!(jn.procs.is_empty(), "c:1462 — procs cleared");
5270 assert!(jn.auxprocs.is_empty(), "c:1469 — auxprocs cleared");
5271 assert!(jn.filelist.is_empty(), "c:1491 — filelist cleared");
5272 assert!(jn.ty.is_none(), "c:1475 — ty cleared");
5273 }
5274
5275 /// `Src/jobs.c:259-270` — `super_job` requires THREE conditions:
5276 /// `STAT_SUPERJOB` bit + `other == sub` + `gleader != 0`. The
5277 /// gleader check at c:267 was previously missing in the Rust
5278 /// port. Pin all three: a job with SUPERJOB+other match but
5279 /// `gleader == 0` (not yet group-leader-assigned) must NOT be
5280 /// returned as the super-job.
5281 #[test]
5282 fn super_job_requires_nonzero_gleader() {
5283 let _g = crate::test_util::global_state_lock();
5284 let mut tab = vec![job::new(), job::new(), job::new()];
5285 // job 2 is a super-job of sub-job 1 BUT no gleader yet.
5286 tab[2].stat |= stat::SUPERJOB;
5287 tab[2].other = 1;
5288 tab[2].gleader = 0;
5289 assert!(
5290 super_job(&tab, 1).is_none(),
5291 "c:267 — gleader==0 must NOT match super_job lookup"
5292 );
5293 // Now assign gleader — super_job returns Some(2).
5294 tab[2].gleader = 12345;
5295 assert_eq!(
5296 super_job(&tab, 1),
5297 Some(2),
5298 "c:267 — gleader != 0 + other match + SUPERJOB → match"
5299 );
5300 }
5301
5302 /// c:findproc — looking up a non-existent pid returns None. A
5303 /// regression returning Some(0,0,false) would let SIGCHLD reap
5304 /// a phantom job.
5305 #[test]
5306 fn findproc_unknown_pid_returns_none() {
5307 let _g = crate::test_util::global_state_lock();
5308 let tab: Vec<job> = vec![job::new(), job::new()];
5309 assert!(findproc(&tab, 99999, false).is_none());
5310 assert!(findproc(&tab, 99999, true).is_none());
5311 }
5312
5313 /// c:findproc — finding the actual pid returns the (job_idx,
5314 /// proc_idx, is_aux) triple. Catches a regression where the
5315 /// search doesn't traverse a job's procs vec.
5316 #[test]
5317 fn findproc_known_pid_returns_correct_indices() {
5318 let _g = crate::test_util::global_state_lock();
5319 let mut tab: Vec<job> = vec![job::new(), job::new()];
5320 tab[1].stat = stat::INUSE;
5321 let mut p = process::new(12345);
5322 p.status = SP_RUNNING;
5323 tab[1].procs.push(p);
5324 // Search non-aux side — should hit.
5325 let r = findproc(&tab, 12345, false);
5326 assert!(r.is_some(), "must find the seeded pid via aux=false");
5327 let (job_idx, proc_idx, is_aux) = r.unwrap();
5328 assert_eq!(job_idx, 1);
5329 assert_eq!(proc_idx, 0);
5330 assert!(!is_aux, "primary procs vec, not auxprocs");
5331 // Search aux side — should miss (no auxprocs entries).
5332 assert!(
5333 findproc(&tab, 12345, true).is_none(),
5334 "c:209 — aux=true must NOT match a procs (non-aux) entry"
5335 );
5336 }
5337
5338 /// Pin: c:204 — `findproc` skips jobs with `STAT_DONE` set. A
5339 /// terminated pid recycled by the kernel onto a new live process
5340 /// must not match the stale STAT_DONE entry. The previous Rust
5341 /// port returned the STAT_DONE entry and SIGCHLD would have
5342 /// reaped the wrong job.
5343 #[test]
5344 fn findproc_skips_stat_done_jobs() {
5345 let _g = crate::test_util::global_state_lock();
5346 let mut tab: Vec<job> = vec![job::new(), job::new(), job::new()];
5347 // job 1: STAT_DONE with pid 7777 — must be skipped.
5348 tab[1].stat = stat::DONE | stat::INUSE;
5349 let mut p1 = process::new(7777);
5350 p1.status = 0; // exited
5351 tab[1].procs.push(p1);
5352 // job 2: live job with the SAME pid (recycled).
5353 tab[2].stat = stat::INUSE;
5354 let mut p2 = process::new(7777);
5355 p2.status = SP_RUNNING;
5356 tab[2].procs.push(p2);
5357 // Search for pid 7777 — must hit job 2, not job 1.
5358 let r = findproc(&tab, 7777, false);
5359 assert_eq!(
5360 r,
5361 Some((2, 0, false)),
5362 "c:204 — STAT_DONE entry must be skipped; live job 2 wins"
5363 );
5364 }
5365
5366 /// `Src/jobs.c:752-765` — `printhhmmss(secs)` three-branch
5367 /// decision tree:
5368 /// - hours > 0 → `H:MM:SS.xx`
5369 /// - mins > 0 → `M:SS.xx`
5370 /// - else → `S.xxx` (three-decimal precision)
5371 /// Pin each branch.
5372 #[test]
5373 fn printhhmmss_three_branch_format_dispatch() {
5374 let _g = crate::test_util::global_state_lock();
5375 // c:763 — sub-minute uses `%.3f` format.
5376 assert_eq!(printhhmmss(0.5), "0.500");
5377 assert_eq!(printhhmmss(12.345), "12.345");
5378 // c:761 — minutes branch uses `%d:%05.2f`.
5379 // 75.0s = 1m 15.0s → "1:15.00".
5380 assert_eq!(printhhmmss(75.0), "1:15.00");
5381 // 125.5s = 2m 5.5s → "2:05.50".
5382 assert_eq!(printhhmmss(125.5), "2:05.50");
5383 // c:759 — hours branch uses `%d:%02d:%05.2f`.
5384 // 3661.5s = 1h 1m 1.5s → "1:01:01.50".
5385 assert_eq!(printhhmmss(3661.5), "1:01:01.50");
5386 // Multi-hour: 7200s = 2h 0m 0s → "2:00:00.00".
5387 assert_eq!(printhhmmss(7200.0), "2:00:00.00");
5388 }
5389
5390 /// `Src/jobs.c:1107-1109` — `sigmsg(sig)` looks up signal names
5391 /// in the `sigmsg[]` table and returns a canonical message
5392 /// (e.g. "interrupt" for SIGINT). Out-of-range returns the
5393 /// default "unknown signal" message.
5394 #[test]
5395 fn sigmsg_returns_canonical_messages_for_standard_signals() {
5396 let _g = crate::test_util::global_state_lock();
5397 // SIGINT/SIGTERM are universal POSIX signals — pin their
5398 // message text exists (non-empty).
5399 let int_msg = sigmsg(libc::SIGINT);
5400 let term_msg = sigmsg(libc::SIGTERM);
5401 let kill_msg = sigmsg(libc::SIGKILL);
5402 assert!(!int_msg.is_empty());
5403 assert!(!term_msg.is_empty());
5404 assert!(!kill_msg.is_empty());
5405 // They must be distinct (no single "unknown" sentinel for all).
5406 assert_ne!(int_msg, term_msg);
5407 }
5408
5409 /// `Src/jobs.c:3052-3058` — `getsigidx` numeric-input branch
5410 /// bounds-checks against `VSIGCOUNT` and the RT-signal range.
5411 /// Previously the Rust port accepted ANY parse-able number,
5412 /// including out-of-range values like 9999 (where C returns -1).
5413 #[test]
5414 fn getsigidx_rejects_out_of_range_numeric() {
5415 let _g = crate::test_util::global_state_lock();
5416 // In-range numeric → Some.
5417 assert_eq!(getsigidx("0"), Some(0), "EXIT pseudo-signal index 0");
5418 assert_eq!(getsigidx("9"), Some(9), "SIGKILL signal number 9 → Some(9)");
5419 // Out-of-range numeric → None.
5420 assert_eq!(
5421 getsigidx("9999"),
5422 None,
5423 "c:3056 — 9999 above VSIGCOUNT and outside RT range → None"
5424 );
5425 assert_eq!(getsigidx("99999999999"), None, "c:3056 — overflow → None");
5426 }
5427
5428 /// `Src/jobs.c:3052` — non-digit-leading strings skip the numeric
5429 /// branch entirely and go to name-table lookup. "INTabc" doesn't
5430 /// match any signal name → None.
5431 #[test]
5432 fn getsigidx_non_digit_unknown_name_returns_none() {
5433 let _g = crate::test_util::global_state_lock();
5434 assert_eq!(getsigidx("DEFINITELYNOTASIGNAL"), None);
5435 assert_eq!(getsigidx(""), None, "empty string → None");
5436 }
5437
5438 /// `Src/jobs.c:3087-3107` — `getsigname(sig)` falls back to
5439 /// `rtsigname(SIGNUM(sig), 0)` for signals in `[SIGRTMIN..SIGRTMAX]`
5440 /// (Linux only). Previously the Rust port emitted `SIG{n}` for
5441 /// every unknown signal, losing the RT-signal naming entirely.
5442 #[cfg(target_os = "linux")]
5443 #[test]
5444 fn getsigname_emits_rt_form_for_rt_signal_range() {
5445 let _g = crate::test_util::global_state_lock();
5446 let sigrtmin = libc::SIGRTMIN();
5447 let sigrtmax = libc::SIGRTMAX();
5448 // SIGRTMIN → "RTMIN".
5449 assert_eq!(
5450 getsigname(sigrtmin),
5451 "RTMIN",
5452 "c:3101 — RTMIN sig → bare RTMIN"
5453 );
5454 // SIGRTMAX → "RTMAX".
5455 assert_eq!(
5456 getsigname(sigrtmax),
5457 "RTMAX",
5458 "c:3101 — RTMAX sig → bare RTMAX"
5459 );
5460 // SIGRTMIN+1 → "RTMIN+1" (shorter form per rtsigname c:1322).
5461 assert_eq!(getsigname(sigrtmin + 1), "RTMIN+1");
5462 // SIGRTMAX-1 → "RTMAX-1".
5463 assert_eq!(getsigname(sigrtmax - 1), "RTMAX-1");
5464 }
5465
5466 /// Pre-condition: standard signal names still resolve. Make sure
5467 /// the new RT-signal branch didn't break the canonical table.
5468 #[test]
5469 fn getsigname_standard_signals_unchanged() {
5470 let _g = crate::test_util::global_state_lock();
5471 assert_eq!(getsigname(libc::SIGINT), "INT");
5472 assert_eq!(getsigname(libc::SIGHUP), "HUP");
5473 assert_eq!(getsigname(libc::SIGCHLD), "CHLD");
5474 assert_eq!(getsigname(libc::SIGKILL), "KILL");
5475 // EXIT pseudo-signal at index 0.
5476 assert_eq!(getsigname(0), "EXIT");
5477 }
5478
5479 /// Serialise tests that mutate the global ZLE-active flag.
5480 static ZLEACTIVE_TEST_LOCK: Mutex<()> = Mutex::new(());
5481
5482 /// Pin: STAT_TIMED short-circuits **regardless of zleactive** per
5483 /// `Src/jobs.c:1052-1053` — the STAT_TIMED check happens BEFORE
5484 /// the zleactive gate, so explicit `time foo` always reports.
5485 #[test]
5486 fn should_report_time_stat_timed_overrides_zleactive() {
5487 let _g = crate::test_util::global_state_lock();
5488 let _g = ZLEACTIVE_TEST_LOCK.lock().unwrap();
5489 let prev = zleactive.load(Ordering::Relaxed);
5490 zleactive.store(1, Ordering::Relaxed);
5491 let mut job = job::new();
5492 job.stat |= stat::TIMED;
5493 // STAT_TIMED returns true even with zleactive=1 and no procs.
5494 assert!(should_report_time(&job, -1.0));
5495 zleactive.store(prev, Ordering::Relaxed);
5496 }
5497
5498 /// Pin: `zleactive` short-circuits per `Src/jobs.c:1074`. When
5499 /// the line editor is active, never report a timing line even
5500 /// if reporttime would otherwise trigger. Without this gate the
5501 /// timing line corrupts the active prompt.
5502 #[test]
5503 fn should_report_time_zleactive_suppresses() {
5504 let _g = crate::test_util::global_state_lock();
5505 let _g = ZLEACTIVE_TEST_LOCK.lock().unwrap();
5506 let prev = zleactive.load(Ordering::Relaxed);
5507 zleactive.store(1, Ordering::Relaxed);
5508 // Build a job with one proc that would otherwise satisfy the
5509 // elapsed-time threshold: bgtime now, endtime now + 10s,
5510 // reporttime=1s.
5511 let mut job = job::new();
5512 let now = Instant::now();
5513 let mut p = process::new(1);
5514 p.bgtime = Some(now);
5515 p.endtime = Some(now + Duration::from_secs(10));
5516 job.procs.push(p);
5517 // With zleactive=1, suppressed.
5518 assert!(!should_report_time(&job, 1.0));
5519 // With zleactive=0, fires.
5520 zleactive.store(0, Ordering::Relaxed);
5521 assert!(should_report_time(&job, 1.0));
5522 zleactive.store(prev, Ordering::Relaxed);
5523 }
5524
5525 /// Pin: reporttime<0 short-circuits per `Src/jobs.c:1065`.
5526 /// Without `$REPORTTIME` set (or with REPORTTIME<0 sentinel),
5527 /// no timing line is reported.
5528 #[test]
5529 fn should_report_time_negative_threshold_suppresses() {
5530 let _g = crate::test_util::global_state_lock();
5531 let _g = ZLEACTIVE_TEST_LOCK.lock().unwrap();
5532 let mut job = job::new();
5533 let now = Instant::now();
5534 let mut p = process::new(1);
5535 p.bgtime = Some(now);
5536 p.endtime = Some(now + Duration::from_secs(10));
5537 job.procs.push(p);
5538 assert!(!should_report_time(&job, -1.0));
5539 }
5540
5541 /// Pin: missing first proc returns 0 per `Src/jobs.c:1072`
5542 /// (`if (!j->procs) return 0`).
5543 #[test]
5544 fn should_report_time_no_procs_returns_false() {
5545 let _g = crate::test_util::global_state_lock();
5546 let _g = ZLEACTIVE_TEST_LOCK.lock().unwrap();
5547 let job = job::new(); // no procs, no STAT_TIMED
5548 assert!(!should_report_time(&job, 0.0));
5549 }
5550
5551 /// Serialise tests that mutate JOBTAB + PWD param.
5552 static JOBPWD_TEST_LOCK: Mutex<()> = Mutex::new(());
5553
5554 /// Pin: `setjobpwd()` writes `pwd` to every IN-USE job that
5555 /// doesn't already have one, per `Src/jobs.c:1886-1888`. The
5556 /// previous Rust port took a `&mut job` and was a no-op — every
5557 /// `cd` left in-flight jobs with no pwd.
5558 #[test]
5559 fn setjobpwd_stamps_pwd_on_inuse_jobs_without_one() {
5560 let _g = crate::test_util::global_state_lock();
5561 let _g = JOBPWD_TEST_LOCK.lock().unwrap();
5562 // Set PWD via the canonical paramtab path.
5563 crate::ported::params::assignsparam("PWD", "/tmp/test_setjobpwd", 0);
5564 // Reset JOBTAB: index 0 (shell itself) + 3 jobs.
5565 let tab = JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
5566 {
5567 let mut tab = tab.lock().unwrap();
5568 tab.clear();
5569 tab.push(job::new()); // index 0 — skipped
5570 // job 1: INUSE, no pwd — should get stamped.
5571 let mut j1 = job::new();
5572 j1.stat = stat::INUSE;
5573 j1.pwd = None;
5574 tab.push(j1);
5575 // job 2: INUSE, already has pwd — should be PRESERVED.
5576 let mut j2 = job::new();
5577 j2.stat = stat::INUSE;
5578 j2.pwd = Some("/preserved".to_string());
5579 tab.push(j2);
5580 // job 3: NOT in use (stat=0) — should NOT get stamped.
5581 let mut j3 = job::new();
5582 j3.stat = 0;
5583 j3.pwd = None;
5584 tab.push(j3);
5585 }
5586 setjobpwd();
5587 let tab = tab.lock().unwrap();
5588 // c:1887-1888 — IN-USE + no pwd → stamped with current pwd.
5589 assert_eq!(
5590 tab[1].pwd.as_deref(),
5591 Some("/tmp/test_setjobpwd"),
5592 "c:1888 — INUSE+no-pwd job must be stamped with PWD"
5593 );
5594 // c:1887 — IN-USE + already has pwd → preserved (the `!pwd` gate).
5595 assert_eq!(
5596 tab[2].pwd.as_deref(),
5597 Some("/preserved"),
5598 "c:1887 — existing pwd must NOT be overwritten"
5599 );
5600 // c:1887 — stat==0 (not in use) → not stamped.
5601 assert_eq!(
5602 tab[3].pwd, None,
5603 "c:1887 — non-INUSE job (stat==0) must NOT be stamped"
5604 );
5605 // Index 0 (shell itself) is skipped (c:1886 starts at i=1).
5606 assert_eq!(
5607 tab[0].pwd, None,
5608 "c:1886 — index 0 (shell) must NOT be stamped"
5609 );
5610 }
5611
5612 // ─── zsh-corpus pins for printhhmmss / sigmsg edge cases ─────────
5613
5614 /// `printhhmmss(0.0)` returns "0.000".
5615 #[test]
5616 fn jobs_corpus_printhhmmss_zero_exact() {
5617 let _g = crate::test_util::global_state_lock();
5618 assert_eq!(printhhmmss(0.0), "0.000");
5619 }
5620
5621 /// `printhhmmss(59.999)` is still in the sub-minute branch.
5622 #[test]
5623 fn jobs_corpus_printhhmmss_just_under_one_minute() {
5624 let _g = crate::test_util::global_state_lock();
5625 let s = printhhmmss(59.999);
5626 // Sub-minute → "%.3f" → "59.999" (no colons).
5627 assert!(!s.contains(':'), "sub-minute has no colon, got {s:?}");
5628 assert!(s.starts_with("59.9"));
5629 }
5630
5631 /// `printhhmmss(60.0)` enters the minutes branch → "1:00.00".
5632 #[test]
5633 fn jobs_corpus_printhhmmss_exactly_one_minute() {
5634 let _g = crate::test_util::global_state_lock();
5635 assert_eq!(printhhmmss(60.0), "1:00.00");
5636 }
5637
5638 /// `printhhmmss(3600.0)` enters the hours branch → "1:00:00.00".
5639 #[test]
5640 fn jobs_corpus_printhhmmss_exactly_one_hour() {
5641 let _g = crate::test_util::global_state_lock();
5642 assert_eq!(printhhmmss(3600.0), "1:00:00.00");
5643 }
5644
5645 /// `printhhmmss(86400.0)` → "24:00:00.00" (24h cleanly).
5646 #[test]
5647 fn jobs_corpus_printhhmmss_exactly_one_day() {
5648 let _g = crate::test_util::global_state_lock();
5649 assert_eq!(printhhmmss(86400.0), "24:00:00.00");
5650 }
5651
5652 /// `sigmsg(SIGINT)` returns "interrupt" canonically.
5653 #[test]
5654 fn jobs_corpus_sigmsg_int_is_interrupt() {
5655 let _g = crate::test_util::global_state_lock();
5656 assert_eq!(sigmsg(libc::SIGINT), "interrupt");
5657 }
5658
5659 /// `sigmsg(SIGTERM)` returns "terminated".
5660 #[test]
5661 fn jobs_corpus_sigmsg_term_is_terminated() {
5662 let _g = crate::test_util::global_state_lock();
5663 assert_eq!(sigmsg(libc::SIGTERM), "terminated");
5664 }
5665
5666 /// `sigmsg(SIGSEGV)` returns "segmentation fault".
5667 #[test]
5668 fn jobs_corpus_sigmsg_segv_is_segfault() {
5669 let _g = crate::test_util::global_state_lock();
5670 assert_eq!(sigmsg(libc::SIGSEGV), "segmentation fault");
5671 }
5672
5673 /// `sigmsg(SIGPIPE)` returns "broken pipe".
5674 #[test]
5675 fn jobs_corpus_sigmsg_pipe_is_broken_pipe() {
5676 let _g = crate::test_util::global_state_lock();
5677 assert_eq!(sigmsg(libc::SIGPIPE), "broken pipe");
5678 }
5679
5680 // ═══════════════════════════════════════════════════════════════════
5681 // C-parity tests pinning Src/jobs.c. Tests that capture KNOWN ZSHRS
5682 // BUGS use #[ignore = "ZSHRS BUG: …"].
5683 // ═══════════════════════════════════════════════════════════════════
5684
5685 /// `initjob` must SKIP index 0 (the shell itself) when picking a
5686 /// slot. C `Src/jobs.c:1865-1867`:
5687 /// `for (i = 1; i <= maxjob; i++)`
5688 /// starts at 1.
5689 /// ZSHRS BUG: Rust port at jobs.rs:1874 uses `enumerate()` starting
5690 /// at 0 — would reuse the shell's own slot if jobtab[0] is empty,
5691 /// corrupting parent-shell job tracking.
5692 #[test]
5693 fn initjob_skips_index_zero_reserved_for_shell() {
5694 let _g = crate::test_util::global_state_lock();
5695 // Fresh table with index 0 empty. C would skip it and add a
5696 // new slot at index 1. Rust off-by-one would return 0.
5697 let mut jt: Vec<job> = vec![job::new(), job::new(), job::new()];
5698 // All slots empty (stat=0).
5699 let idx = initjob(&mut jt);
5700 assert_ne!(
5701 idx, 0,
5702 "initjob must NOT return index 0 (shell slot); got {idx}"
5703 );
5704 assert!(idx >= 1, "first available slot is index >= 1");
5705 }
5706
5707 /// C `Src/jobs.c:1875` emits `zerr("job table full…")` and returns
5708 /// -1 on table-full. The Rust port has a `Vec<job>` (no fixed
5709 /// `MAXJOB` cap) so the "full" condition can't actually occur —
5710 /// the table grows on demand and a new slot is always returned.
5711 /// Pin the actual behavior: initjob on a "full" (all-INUSE) table
5712 /// expands by one and returns the new index.
5713 #[test]
5714 fn initjob_returns_negative_one_on_full_table() {
5715 let _g = crate::test_util::global_state_lock();
5716 let mut jt: Vec<job> = Vec::new();
5717 for _ in 0..4 {
5718 let mut j = job::new();
5719 j.stat = stat::INUSE;
5720 jt.push(j);
5721 }
5722 let before = jt.len();
5723 let idx = initjob(&mut jt);
5724 // Rust port grows the table — no -1 sentinel.
5725 assert_eq!(idx, before, "fresh slot at the grown end of jobtab");
5726 assert_eq!(jt.len(), before + 1, "jobtab grew by one");
5727 }
5728
5729 /// `findproc` with pid=-1 (impossible pid) returns None.
5730 /// Already covered but pin the never-match path explicitly.
5731 #[test]
5732 fn findproc_invalid_pid_returns_none() {
5733 let _g = crate::test_util::global_state_lock();
5734 let jt: Vec<job> = vec![job::new()];
5735 assert!(findproc(&jt, -1, false).is_none());
5736 }
5737
5738 /// `findproc` on empty jobtab returns None (no panic on empty
5739 /// slice; C `for (i=1; i<=maxjob; i++)` with maxjob=0 skips loop).
5740 #[test]
5741 fn findproc_empty_jobtab_no_panic() {
5742 let _g = crate::test_util::global_state_lock();
5743 let jt: Vec<job> = Vec::new();
5744 assert!(findproc(&jt, 1234, false).is_none());
5745 }
5746
5747 /// `getsigname(0)` returns "EXIT" — pseudo-signal index 0 is the
5748 /// EXIT trap target in zsh. C jobs.c:3392-3393 sigs[0]="EXIT".
5749 #[test]
5750 fn getsigname_zero_returns_exit_pseudo_signal() {
5751 let _g = crate::test_util::global_state_lock();
5752 assert_eq!(getsigname(0), "EXIT");
5753 }
5754
5755 /// `getsigname(libc::SIGHUP)` returns "HUP" without the SIG prefix.
5756 #[test]
5757 fn getsigname_sighup_returns_hup_without_prefix() {
5758 let _g = crate::test_util::global_state_lock();
5759 assert_eq!(getsigname(libc::SIGHUP), "HUP");
5760 }
5761
5762 // ═══════════════════════════════════════════════════════════════════
5763 // Additional C-parity tests for Src/jobs.c printhhmmss + sigmsg +
5764 // dtime_tv + get_clktck.
5765 // ═══════════════════════════════════════════════════════════════════
5766
5767 /// c:752 — `printhhmmss(0.0)` returns "0.000" (sub-minute fmt).
5768 #[test]
5769 fn printhhmmss_zero_returns_zero_seconds() {
5770 let _g = crate::test_util::global_state_lock();
5771 assert_eq!(printhhmmss(0.0), "0.000");
5772 }
5773
5774 /// c:752 — sub-minute time uses 3-decimal format.
5775 #[test]
5776 fn printhhmmss_sub_minute_uses_three_decimals() {
5777 let _g = crate::test_util::global_state_lock();
5778 assert_eq!(printhhmmss(5.123), "5.123");
5779 assert_eq!(printhhmmss(59.999), "59.999");
5780 }
5781
5782 /// c:752 — minute-but-not-hour uses M:SS.SS format.
5783 #[test]
5784 fn printhhmmss_minute_uses_mm_ss_format() {
5785 let _g = crate::test_util::global_state_lock();
5786 let r = printhhmmss(65.5);
5787 assert_eq!(r, "1:05.50", "1m05.50s");
5788 }
5789
5790 /// c:752 — hour+ uses H:MM:SS.SS format.
5791 #[test]
5792 fn printhhmmss_hour_uses_hh_mm_ss_format() {
5793 let _g = crate::test_util::global_state_lock();
5794 let r = printhhmmss(3725.0); // 1h 2m 5s
5795 assert_eq!(r, "1:02:05.00");
5796 }
5797
5798 /// c:752 — exactly 60 seconds crosses minute boundary.
5799 #[test]
5800 fn printhhmmss_sixty_seconds_is_one_minute() {
5801 let _g = crate::test_util::global_state_lock();
5802 let r = printhhmmss(60.0);
5803 assert_eq!(r, "1:00.00", "60s = 1m");
5804 }
5805
5806 /// c:752 — exactly 3600s crosses hour boundary.
5807 #[test]
5808 fn printhhmmss_thirty_six_hundred_seconds_is_one_hour() {
5809 let _g = crate::test_util::global_state_lock();
5810 let r = printhhmmss(3600.0);
5811 assert_eq!(r, "1:00:00.00");
5812 }
5813
5814 /// c:1107 — `sigmsg` of valid signal returns a non-default string.
5815 #[test]
5816 fn sigmsg_known_signal_returns_descriptive_string() {
5817 let _g = crate::test_util::global_state_lock();
5818 // SIGTERM, SIGSEGV, SIGINT should all have descriptive messages
5819 let term = sigmsg(libc::SIGTERM);
5820 assert_ne!(term, "unknown signal", "SIGTERM should have a message");
5821 }
5822
5823 /// c:1118 — `sigmsg(-1)` / out-of-range returns "unknown signal".
5824 #[test]
5825 fn sigmsg_unknown_signal_returns_unknown() {
5826 let _g = crate::test_util::global_state_lock();
5827 assert_eq!(sigmsg(-1), "unknown signal");
5828 assert_eq!(sigmsg(9999), "unknown signal");
5829 }
5830
5831 /// c:752 — printhhmmss is deterministic.
5832 #[test]
5833 fn printhhmmss_is_deterministic() {
5834 let _g = crate::test_util::global_state_lock();
5835 for t in &[0.0, 1.5, 60.0, 3600.0, 7200.5] {
5836 let r1 = printhhmmss(*t);
5837 let r2 = printhhmmss(*t);
5838 assert_eq!(r1, r2, "printhhmmss must be pure for {}", t);
5839 }
5840 }
5841
5842 /// `get_clktck()` returns positive — clock-ticks-per-second cannot
5843 /// be zero on any sane system.
5844 #[test]
5845 fn get_clktck_returns_positive() {
5846 let _g = crate::test_util::global_state_lock();
5847 let ck = get_clktck();
5848 assert!(ck > 0, "CLK_TCK must be positive, got {}", ck);
5849 // POSIX guarantees CLK_TCK ≥ 1; typical values are 100/250/1000.
5850 assert!(ck <= 10_000, "CLK_TCK suspiciously large: {}", ck);
5851 }
5852
5853 // ═══════════════════════════════════════════════════════════════════
5854 // Additional C-parity tests for Src/jobs.c isanum + getsigidx.
5855 // ═══════════════════════════════════════════════════════════════════
5856
5857 /// c:2010 — `isanum("")` returns false (empty not valid).
5858 #[test]
5859 fn isanum_empty_returns_false() {
5860 let _g = crate::test_util::global_state_lock();
5861 assert!(!isanum(""));
5862 }
5863
5864 /// c:2010 — all-digit string returns true.
5865 #[test]
5866 fn isanum_all_digits_returns_true() {
5867 let _g = crate::test_util::global_state_lock();
5868 assert!(isanum("123"));
5869 assert!(isanum("0"));
5870 assert!(isanum("999999"));
5871 }
5872
5873 /// c:2010 — hyphen-prefixed digits valid.
5874 #[test]
5875 fn isanum_with_hyphen_returns_true() {
5876 let _g = crate::test_util::global_state_lock();
5877 assert!(isanum("-1"));
5878 assert!(isanum("-123"));
5879 assert!(isanum("-"));
5880 }
5881
5882 /// c:2010 — alpha or non-digit chars rejected.
5883 #[test]
5884 fn isanum_rejects_alpha() {
5885 let _g = crate::test_util::global_state_lock();
5886 assert!(!isanum("abc"));
5887 assert!(!isanum("1a"));
5888 assert!(!isanum("a1"));
5889 assert!(!isanum("1 2"));
5890 assert!(!isanum("1.0"));
5891 }
5892
5893 /// c:2010 — deterministic.
5894 #[test]
5895 fn isanum_is_deterministic() {
5896 let _g = crate::test_util::global_state_lock();
5897 for s in ["", "123", "-1", "abc", "1a"] {
5898 let first = isanum(s);
5899 for _ in 0..5 {
5900 assert_eq!(isanum(s), first);
5901 }
5902 }
5903 }
5904
5905 /// c:3052 — `getsigidx("")` returns None.
5906 #[test]
5907 fn getsigidx_empty_returns_none() {
5908 let _g = crate::test_util::global_state_lock();
5909 assert!(getsigidx("").is_none());
5910 }
5911
5912 /// c:3334 — `getsigidx("EXIT")` returns Some(0).
5913 #[test]
5914 fn getsigidx_exit_returns_zero() {
5915 let _g = crate::test_util::global_state_lock();
5916 assert_eq!(getsigidx("EXIT"), Some(0));
5917 }
5918
5919 /// c:3052 — canonical POSIX signal names resolve.
5920 #[test]
5921 #[cfg(unix)]
5922 fn getsigidx_canonical_signal_names() {
5923 let _g = crate::test_util::global_state_lock();
5924 assert_eq!(getsigidx("HUP"), Some(libc::SIGHUP));
5925 assert_eq!(getsigidx("TERM"), Some(libc::SIGTERM));
5926 assert_eq!(getsigidx("INT"), Some(libc::SIGINT));
5927 assert_eq!(getsigidx("KILL"), Some(libc::SIGKILL));
5928 }
5929
5930 /// c:3332 — SIG prefix stripped transparently.
5931 #[test]
5932 #[cfg(unix)]
5933 fn getsigidx_strips_sig_prefix() {
5934 let _g = crate::test_util::global_state_lock();
5935 assert_eq!(getsigidx("HUP"), getsigidx("SIGHUP"));
5936 assert_eq!(getsigidx("TERM"), getsigidx("SIGTERM"));
5937 }
5938
5939 /// c:3333 — case-insensitive on signal name.
5940 #[test]
5941 #[cfg(unix)]
5942 fn getsigidx_case_insensitive() {
5943 let _g = crate::test_util::global_state_lock();
5944 assert_eq!(getsigidx("hup"), getsigidx("HUP"));
5945 }
5946
5947 /// c:3081 — unknown name returns None.
5948 #[test]
5949 fn getsigidx_unknown_returns_none() {
5950 let _g = crate::test_util::global_state_lock();
5951 assert!(getsigidx("NEVER_REAL_SIGNAL").is_none());
5952 }
5953
5954 /// c:3339 — ZERR and ERR both resolve to SIGZERR.
5955 #[test]
5956 fn getsigidx_zerr_and_err_alias() {
5957 let _g = crate::test_util::global_state_lock();
5958 assert_eq!(getsigidx("ZERR"), Some(crate::ported::signals_h::SIGZERR));
5959 assert_eq!(
5960 getsigidx("ERR"),
5961 Some(crate::ported::signals_h::SIGZERR),
5962 "ERR aliases ZERR"
5963 );
5964 }
5965
5966 /// c:3340 — DEBUG resolves to SIGDEBUG.
5967 #[test]
5968 fn getsigidx_debug_returns_sigdebug() {
5969 let _g = crate::test_util::global_state_lock();
5970 assert_eq!(getsigidx("DEBUG"), Some(crate::ported::signals_h::SIGDEBUG));
5971 }
5972
5973 // ═══════════════════════════════════════════════════════════════════
5974 // Additional C-parity tests for Src/jobs.c
5975 // c:93 dtime_tv / c:105 dtime_ts / c:524 get_usage / c:866 get_clktck /
5976 // c:885 printhhmmss / c:1172 sigmsg / c:340 hasprocs
5977 // ═══════════════════════════════════════════════════════════════════
5978
5979 /// c:866 — `get_clktck` returns i64 (compile-time type pin).
5980 #[test]
5981 fn get_clktck_returns_i64_type() {
5982 let _g = crate::test_util::global_state_lock();
5983 let _: i64 = get_clktck();
5984 }
5985
5986 /// c:866 — `get_clktck` returns positive value (clock ticks per sec).
5987 #[test]
5988 fn get_clktck_returns_positive_pin() {
5989 let _g = crate::test_util::global_state_lock();
5990 let tk = get_clktck();
5991 assert!(tk > 0, "clock tick rate must be > 0, got {}", tk);
5992 }
5993
5994 /// c:866 — `get_clktck` is deterministic.
5995 #[test]
5996 fn get_clktck_is_deterministic() {
5997 let _g = crate::test_util::global_state_lock();
5998 let first = get_clktck();
5999 for _ in 0..5 {
6000 assert_eq!(get_clktck(), first, "clock tick rate must be stable");
6001 }
6002 }
6003
6004 /// c:885 — `printhhmmss(0.0)` returns String (compile-time type pin).
6005 #[test]
6006 fn printhhmmss_returns_string_type() {
6007 let _: String = printhhmmss(0.0);
6008 }
6009
6010 /// c:885 — `printhhmmss` is pure for arbitrary seconds.
6011 #[test]
6012 fn printhhmmss_is_pure() {
6013 for s in [0.0, 1.0, 60.0, 3661.5, -1.0] {
6014 let first = printhhmmss(s);
6015 for _ in 0..3 {
6016 assert_eq!(printhhmmss(s), first, "printhhmmss({}) must be pure", s);
6017 }
6018 }
6019 }
6020
6021 /// c:885 — `printhhmmss(0)` produces "0.000" (sub-minute, no `:`).
6022 /// Per C body c:893-895: only adds h/m components when total exceeds them.
6023 #[test]
6024 fn printhhmmss_zero_short_form() {
6025 let s = printhhmmss(0.0);
6026 assert!(
6027 s.contains('.'),
6028 "sub-minute must use 'S.MMM' form, got {:?}",
6029 s
6030 );
6031 assert!(s.contains('0'), "must contain '0' digit, got {:?}", s);
6032 }
6033
6034 /// c:885 — `printhhmmss(>60)` adds minute colon separator.
6035 #[test]
6036 fn printhhmmss_over_minute_adds_colon() {
6037 let s = printhhmmss(125.0); // 2m 5s
6038 assert!(
6039 s.contains(':'),
6040 "over 60s must contain ':' separator, got {:?}",
6041 s
6042 );
6043 }
6044
6045 /// c:1172 — `sigmsg` returns &'static str (compile-time type pin).
6046 #[test]
6047 fn sigmsg_returns_static_str_type() {
6048 let _: &'static str = sigmsg(0);
6049 }
6050
6051 /// c:1172 — `sigmsg(N)` is pure for arbitrary signals.
6052 #[test]
6053 fn sigmsg_is_pure() {
6054 for s in [0i32, 1, 9, 15, 999] {
6055 let first = sigmsg(s);
6056 for _ in 0..3 {
6057 assert_eq!(sigmsg(s), first, "sigmsg({}) must be pure", s);
6058 }
6059 }
6060 }
6061
6062 /// c:340 — `hasprocs(empty_table, _)` returns false.
6063 #[test]
6064 fn hasprocs_empty_table_returns_false() {
6065 let empty: Vec<job> = vec![];
6066 assert!(!hasprocs(&empty, 0), "empty table → false");
6067 }
6068
6069 /// c:340 — `hasprocs` returns bool (compile-time type pin).
6070 #[test]
6071 fn hasprocs_returns_bool_type() {
6072 let empty: Vec<job> = vec![];
6073 let _: bool = hasprocs(&empty, 0);
6074 }
6075
6076 /// c:524 — `get_usage` returns timeinfo (compile-time type pin).
6077 #[test]
6078 fn get_usage_returns_timeinfo_type() {
6079 let _g = crate::test_util::global_state_lock();
6080 let _: timeinfo = get_usage();
6081 }
6082
6083 // ═══════════════════════════════════════════════════════════════════
6084 // Additional C-parity tests for Src/jobs.c
6085 // c:93 dtime_tv / c:105 dtime_ts / c:1589 havefiles / c:2209 scanjobs /
6086 // c:3876 getbgstatus / c:1599 waitforpid + edge-case pins
6087 // ═══════════════════════════════════════════════════════════════════
6088
6089 /// c:93 — `dtime_tv(t2 > t1)` returns positive diff.
6090 #[test]
6091 fn dtime_tv_positive_diff_returned() {
6092 let mut dt = Duration::ZERO;
6093 let t1 = Duration::from_secs(1);
6094 let t2 = Duration::from_secs(5);
6095 let r = dtime_tv(&mut dt, &t1, &t2);
6096 assert_eq!(r, Duration::from_secs(4), "5 - 1 = 4s");
6097 assert_eq!(dt, Duration::from_secs(4), "out param set to diff");
6098 }
6099
6100 /// c:93 — `dtime_tv(t2 <= t1)` returns ZERO (saturating).
6101 #[test]
6102 fn dtime_tv_negative_diff_saturates_to_zero() {
6103 let mut dt = Duration::from_secs(99);
6104 let t1 = Duration::from_secs(5);
6105 let t2 = Duration::from_secs(1);
6106 let r = dtime_tv(&mut dt, &t1, &t2);
6107 assert_eq!(r, Duration::ZERO, "t2 < t1 → ZERO");
6108 assert_eq!(dt, Duration::ZERO, "out param set to ZERO");
6109 }
6110
6111 /// c:93 — `dtime_tv(t2 == t1)` returns ZERO (equal saturates).
6112 #[test]
6113 fn dtime_tv_equal_returns_zero() {
6114 let mut dt = Duration::from_secs(99);
6115 let t = Duration::from_secs(5);
6116 let r = dtime_tv(&mut dt, &t, &t);
6117 assert_eq!(r, Duration::ZERO, "equal → ZERO");
6118 }
6119
6120 /// c:93 — `dtime_tv` returns Duration (compile-time type pin).
6121 #[test]
6122 fn dtime_tv_returns_duration_type() {
6123 let mut dt = Duration::ZERO;
6124 let t = Duration::from_secs(1);
6125 let _: Duration = dtime_tv(&mut dt, &t, &t);
6126 }
6127
6128 /// c:105 — `dtime_ts(t1, t2)` with t2 < t1 returns ZERO.
6129 #[test]
6130 fn dtime_ts_negative_diff_saturates_to_zero() {
6131 let t1 = Instant::now();
6132 std::thread::sleep(Duration::from_millis(1));
6133 let t2 = Instant::now();
6134 // Reverse — t1 is "later" perspective.
6135 let r = dtime_ts(&t2, &t1);
6136 assert_eq!(r, Duration::ZERO, "earlier - later = ZERO");
6137 }
6138
6139 /// c:105 — `dtime_ts` returns Duration (compile-time type pin).
6140 #[test]
6141 fn dtime_ts_returns_duration_type() {
6142 let now = Instant::now();
6143 let _: Duration = dtime_ts(&now, &now);
6144 }
6145
6146 /// c:105 — `dtime_ts(same, same)` returns ZERO.
6147 #[test]
6148 fn dtime_ts_same_instant_returns_zero() {
6149 let now = Instant::now();
6150 assert_eq!(
6151 dtime_ts(&now, &now),
6152 Duration::ZERO,
6153 "same instant → ZERO diff"
6154 );
6155 }
6156
6157 /// c:1589 — `havefiles(empty)` returns false.
6158 #[test]
6159 fn havefiles_empty_returns_false() {
6160 let empty: Vec<job> = vec![];
6161 assert!(!havefiles(&empty), "empty table has no files");
6162 }
6163
6164 /// c:1589 — `havefiles` returns bool (compile-time type pin).
6165 #[test]
6166 fn havefiles_returns_bool_type() {
6167 let empty: Vec<job> = vec![];
6168 let _: bool = havefiles(&empty);
6169 }
6170
6171 /// c:3876 — `getbgstatus(-1)` invalid pid returns Option<i32>.
6172 #[test]
6173 fn getbgstatus_returns_option_i32_type() {
6174 let _g = crate::test_util::global_state_lock();
6175 let _: Option<i32> = getbgstatus(-1);
6176 }
6177
6178 /// c:3876 — `getbgstatus(unknown)` for never-recorded pid → None.
6179 #[test]
6180 fn getbgstatus_unknown_pid_returns_none() {
6181 let _g = crate::test_util::global_state_lock();
6182 // PID 0 / negative are never recorded via addbgstatus.
6183 assert!(getbgstatus(0).is_none() || getbgstatus(0).is_some());
6184 // Real test: an arbitrary high pid we've never used.
6185 let r = getbgstatus(2147483646);
6186 assert!(r.is_none(), "never-recorded pid → None");
6187 }
6188
6189 /// c:340 — `hasprocs(table, job_index_out_of_bounds)` is safe.
6190 #[test]
6191 fn hasprocs_index_out_of_bounds_safe() {
6192 let empty: Vec<job> = vec![];
6193 for idx in [0usize, 1, 100, usize::MAX] {
6194 let _: bool = hasprocs(&empty, idx);
6195 // No panic = pass.
6196 }
6197 }
6198
6199 /// c:885 — `printhhmmss(1.0)` sub-minute formats as "S.MMM".
6200 #[test]
6201 fn printhhmmss_one_second_short_form() {
6202 let s = printhhmmss(1.0);
6203 assert!(s.contains("1."), "1.0s must contain '1.', got {:?}", s);
6204 }
6205
6206 /// c:2237 — `isanum` is pure for a sweep of inputs.
6207 #[test]
6208 fn isanum_is_pure_full_sweep() {
6209 for s in ["", "0", "123", "-5", "abc", "a1", "1a", "-", "12-34"] {
6210 let first = isanum(s);
6211 for _ in 0..3 {
6212 assert_eq!(isanum(s), first, "isanum({:?}) must be pure", s);
6213 }
6214 }
6215 }
6216}