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