zsh/ported/signals.rs
1//! Signal handling for zshrs
2//!
3//! Direct port from zsh/Src/signals.c
4//!
5//! Total count of trapped signals // c:55
6//! Running an exit trap? // c:60
7//! Variables used by trap queueing // c:87
8//! enable ^C interrupts // c:114
9//! disable ^C interrupts // c:124
10//! SIGHUP any jobs left running // c:502
11//!
12//! Manages signal handling including:
13//! - Signal handlers for SIGINT, SIGCHLD, SIGHUP, etc.
14//! - Signal queueing during critical sections
15//! - Trap management (trap builtin)
16//! - Job control signals
17
18use crate::ported::builtin::{zexit, BREAKS, LASTVAL, LOOPS, RETFLAG, SFCONTEXT, STOPMSG};
19use crate::ported::context::{zcontext_restore, zcontext_save};
20use crate::ported::exec::{TRAP_RETURN, TRAP_STATE};
21use crate::ported::init::zleentry;
22use crate::ported::jobs::gettrapnode;
23pub use crate::ported::jobs::{getsigidx, getsigname};
24use crate::ported::mem::{zsfree, ztrdup};
25use crate::ported::options::optlookup;
26use crate::ported::params::{getiparam, ttyidlegetfn};
27pub use crate::ported::signals_h::{queue_signals, unqueue_signals};
28use crate::ported::signals_h::{SIGNUM, TRAPCOUNT as TRAPCOUNT_H, VSIGCOUNT};
29use crate::ported::utils::{
30 errflag, inc_locallevel, locallevel as locallevel_fn, zerr, zwarn, ERRFLAG_ERROR, RESETNEEDED,
31};
32use crate::ported::zsh_h::{
33 isset, Eprog, AFTERTRAPHOOK, BEFORETRAPHOOK, EMULATE_SH, EMULATION, ERRFLAG_INT, HUP,
34 INTERACTIVE, LOCALTRAPS, MONITOR, POSIXTRAPS, PRIVILEGED, SFC_SIGNAL, TRAPSASYNC,
35 TRAP_STATE_FORCE_RETURN, TRAP_STATE_PRIMED, ZEXIT_SIGNAL, ZLE_CMD_REFRESH, ZSIG_FUNC,
36 ZSIG_IGNORED, ZSIG_SHIFT, ZSIG_TRAPPED,
37};
38use crate::r#loop::try_tryflag;
39use crate::sched::zleactive;
40pub use crate::signals_h::{signal_default, signal_ignore};
41use crate::signals_h::{MAX_QUEUE_SIZE, SIGCOUNT, SIGDEBUG, SIGEXIT, SIGZERR, TRAPCOUNT};
42use crate::utils::getshfunc;
43use crate::DPUTS;
44use nix::sys::signal::{
45 sigprocmask, SaFlags, SigAction, SigHandler, SigSet, SigmaskHow, Signal as NixSignal,
46};
47use nix::unistd::getpid;
48use std::collections::HashMap;
49use std::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
50use std::sync::{Mutex, OnceLock};
51
52// getsigidx / getsigname live in `jobs.rs` per C source split:
53// `getsigidx` at `Src/jobs.c:3047`, `getsigname` at `Src/jobs.c:3087`.
54// Re-export from the canonical home so callers using
55// `crate::ported::signals::getsigidx` continue to compile.
56
57/// Per-slot trap-queue signals. Port of `static int
58/// trap_queue[MAX_QUEUE_SIZE]` from `Src/signals.c:92`.
59pub static trap_queue: [AtomicI32; MAX_QUEUE_SIZE] = // c:92
60 [ATOM_I32_ZERO; MAX_QUEUE_SIZE];
61
62/// Port of `install_handler(int sig)` from `Src/signals.c:100`.
63///
64/// C body:
65/// ```c
66/// struct sigaction act;
67/// act.sa_handler = zhandler;
68/// sigemptyset(&act.sa_mask);
69/// act.sa_flags = 0;
70/// if (interact) act.sa_flags |= SA_INTERRUPT;
71/// sigaction(sig, &act, NULL);
72/// ```
73///
74/// Uses `sigaction(2)` (not `signal(2)`) so SA_INTERRUPT can
75/// disable system-call restart when running interactively —
76/// matches the C source's contract that an interactive shell's
77/// signal handlers interrupt blocked reads (so ^C breaks out of
78/// `read` etc.).
79#[cfg(unix)]
80/// Port of `install_handler(int sig)` from `Src/signals.c:100`.
81pub fn install_handler(sig: i32) {
82 // c:100
83 unsafe {
84 let mut act: libc::sigaction = std::mem::zeroed();
85 act.sa_sigaction = zhandler as *const () as usize;
86 libc::sigemptyset(&mut act.sa_mask);
87 // SA_INTERRUPT isn't in the libc crate's POSIX feature set;
88 // when running interactively we'd prefer to leave SA_RESTART
89 // unset (the default after sigemptyset+0). Mirroring C: the
90 // sa_flags = 0 path matches the non-interactive case;
91 // interactive mode would OR in SA_INTERRUPT, which on Linux
92 // is the same as sa_flags = 0 on most libcs (deprecated
93 // alias). Leaving sa_flags = 0 is the same effect on every
94 // modern target.
95 act.sa_flags = 0;
96 libc::sigaction(sig, &act, std::ptr::null_mut());
97 }
98}
99
100// enable ^C interrupts // c:118
101/// Port of `intr()` from `Src/signals.c:118`.
102///
103/// C body: `if (interact) install_handler(SIGINT);` — the
104/// interactive-shell-only SIGINT installer used by `bin_set` /
105/// trap restoration paths to re-enable ^C breaking after a
106/// scope that disabled it.
107pub fn intr() {
108 // c:118
109 if is_interact() {
110 install_handler(libc::SIGINT);
111 }
112}
113
114// ---------------------------------------------------------------------------
115// Remaining 18 missing signals.c functions
116// ---------------------------------------------------------------------------
117
118/// Port of `nointr()` from `Src/signals.c:128`.
119///
120/// C body (under `#if 0` in current zsh — kept for historical
121/// completeness):
122/// ```c
123/// if (interact)
124/// signal_ignore(SIGINT);
125/// ```
126// disable ^C interrupts // c:128
127/// Disables SIGINT delivery in interactive mode (sets the
128/// disposition to SIG_IGN). The `if (interact)` gate matches C.
129/// C body (2 lines): `if (interact) signal_ignore(SIGINT);`
130#[cfg(unix)]
131pub fn nointr() {
132 // c:128
133 if is_interact() {
134 unsafe {
135 libc::signal(libc::SIGINT, libc::SIG_IGN);
136 }
137 } // c:130-131
138}
139
140/// Port of `holdintr()` from `Src/signals.c:139`.
141///
142/// C body:
143/// ```c
144/// if (interact)
145/// signal_block(signal_mask(SIGINT));
146/// ```
147///
148// temporarily block ^C interrupts // c:139
149/// Blocks SIGINT temporarily — used by code paths that can't
150/// handle interruption mid-flight (e.g. after fork before exec).
151#[cfg(unix)]
152pub fn holdintr() {
153 // c:139
154 if is_interact() {
155 let mask = signal_mask(libc::SIGINT);
156 signal_block(&mask);
157 }
158}
159
160/// Port of `noholdintr()` from `Src/signals.c:149`.
161///
162/// C body:
163/// ```c
164/// if (interact)
165/// signal_unblock(signal_mask(SIGINT));
166/// ```
167// release ^C interrupts // c:149
168///
169/// Inverse of [`holdintr`].
170#[cfg(unix)]
171pub fn noholdintr() {
172 // c:149
173 if is_interact() {
174 let mask = signal_mask(libc::SIGINT);
175 signal_unblock(&mask);
176 }
177}
178
179/// Port of `signal_mask(int sig)` from `Src/signals.c:160`.
180///
181/// C body:
182/// ```c
183/// sigset_t set;
184/// sigemptyset(&set);
185/// if (sig)
186/// sigaddset(&set, sig);
187/// return set;
188/// ```
189///
190/// Builds a sigset containing only the given signal; `sig == 0`
191/// returns an empty set (matches the explicit C check).
192#[cfg(unix)]
193/// Port of `signal_mask(int sig)` from `Src/signals.c:160`.
194pub fn signal_mask(sig: i32) -> libc::sigset_t {
195 let mut set: libc::sigset_t = unsafe { std::mem::zeroed() };
196 unsafe {
197 libc::sigemptyset(&mut set);
198 if sig != 0 {
199 libc::sigaddset(&mut set, sig);
200 }
201 }
202 set
203}
204
205/// Port of `signal_block(sigset_t set)` from `Src/signals.c:175`.
206///
207/// C body:
208/// ```c
209/// sigset_t oset;
210/// sigprocmask(SIG_BLOCK, &set, &oset);
211/// return oset;
212/// ```
213///
214/// Blocks every signal in `set`, returning the previous mask
215/// (matches C's `sigset_t signal_block(sigset_t set)`).
216#[cfg(unix)]
217pub fn signal_block(set: &libc::sigset_t) -> libc::sigset_t {
218 // c:175
219 let mut oset: libc::sigset_t = unsafe { std::mem::zeroed() };
220 unsafe {
221 libc::sigprocmask(libc::SIG_BLOCK, set, &mut oset);
222 }
223 oset
224}
225
226/// Port of `signal_unblock(sigset_t set)` from `Src/signals.c:189`.
227///
228/// C body: `sigprocmask(SIG_UNBLOCK, &set, &oset); return oset;`
229#[cfg(unix)]
230pub fn signal_unblock(set: &libc::sigset_t) -> libc::sigset_t {
231 // c:189
232 let mut oset: libc::sigset_t = unsafe { std::mem::zeroed() };
233 unsafe {
234 libc::sigprocmask(libc::SIG_UNBLOCK, set, &mut oset);
235 }
236 oset
237}
238
239/// Port of `signal_setmask(sigset_t set)` from `Src/signals.c:203`.
240///
241/// C body: `sigprocmask(SIG_SETMASK, &set, &oset); return oset;`
242///
243/// Sets the process signal mask, returning the previous mask
244/// (the previous Rust port discarded the old mask).
245#[cfg(unix)]
246pub fn signal_setmask(set: &libc::sigset_t) -> libc::sigset_t {
247 let mut oset: libc::sigset_t = unsafe { std::mem::zeroed() };
248 unsafe {
249 libc::sigprocmask(libc::SIG_SETMASK, set, &mut oset);
250 }
251 oset
252}
253
254/// Number of OS signals zsh tracks.
255/// `dotrap()` and `printsigtable()` to size the per-signal table.
256
257/// Total trap count including EXIT and ERR
258
259/// Port of `signal_suspend(UNUSED(int sig), int wait_cmd)` from `Src/signals.c:214`.
260///
261/// C body:
262/// ```c
263/// sigset_t set;
264/// sigemptyset(&set);
265/// if (!(wait_cmd || isset(TRAPSASYNC) ||
266/// (sigtrapped[SIGINT] & ~ZSIG_IGNORED)))
267/// sigaddset(&set, SIGINT);
268/// return sigsuspend(&set);
269/// ```
270///
271/// Atomically waits for any signal NOT in `set`. The wait_cmd /
272/// TRAPSASYNC / SIGINT-trapped cascade gates whether SIGINT is
273/// added to the mask: when `wait_cmd` is set (the `wait` builtin
274/// calls this) OR TRAPSASYNC is set OR the user has trapped
275/// SIGINT (and not ignored it), SIGINT is left UNblocked so the
276/// trap fires.
277///
278/// Previous Rust port did `libc::raise(SIGTSTP)` which is
279/// completely wrong (that's job-control suspend, not "wait for
280/// signal delivery"). Now real port via `sigsuspend(2)`.
281#[cfg(unix)]
282/// Port of `signal_suspend(UNUSED(int sig), int wait_cmd)` from `Src/signals.c:214`.
283#[allow(unused_variables)]
284pub fn signal_suspend(sig: i32, wait_cmd: bool) -> i32 {
285 // c:214
286 let mut set: libc::sigset_t = unsafe { std::mem::zeroed() };
287 unsafe {
288 libc::sigemptyset(&mut set);
289 }
290 // c:228 — `if (!(wait_cmd || isset(TRAPSASYNC) ||
291 // (sigtrapped[SIGINT] & ~ZSIG_IGNORED))) sigaddset(...)`.
292 // Three escape hatches let SIGINT stay UNblocked during suspend:
293 // 1. `wait_cmd` — the `wait` builtin wants SIGINT to break it.
294 // 2. `isset(TRAPSASYNC)` — async-trap mode means traps fire even
295 // while blocked, so SIGINT must arrive in real time.
296 // 3. SIGINT is trapped but not ignored — user trap must fire.
297 let int_state = sigtrapped
298 .lock()
299 .ok()
300 .and_then(|g| g.get(libc::SIGINT as usize).copied())
301 .unwrap_or(0);
302 let int_trapped = (int_state & !ZSIG_IGNORED) != 0;
303 let trapsasync_set = isset(
304 TRAPSASYNC, // c:228 isset(TRAPSASYNC)
305 );
306 if !(wait_cmd || trapsasync_set || int_trapped) {
307 unsafe {
308 libc::sigaddset(&mut set, libc::SIGINT);
309 }
310 }
311 unsafe { libc::sigsuspend(&set) }
312}
313
314/// Reap zombie child processes via non-blocking `waitpid(2)`.
315/// Port of `wait_for_processes()` from Src/signals.c:249 — the
316/// SIGCHLD-driven reaper that updates the job table.
317/// Rust idiom replacement: drain-loop over `waitpid(-1, WNOHANG)`
318/// covers the C `update_process` + `update_job` cascade; the
319/// per-PID job-table update is the caller's responsibility (decoupled
320/// from the reaper).
321#[cfg(unix)]
322pub fn wait_for_processes() -> Vec<(i32, i32)> {
323 let mut results = Vec::new();
324 // c:271-274 — `WAITFLAGS = WNOHANG|WUNTRACED|WCONTINUED`. The
325 // previous Rust port used `WNOHANG|WUNTRACED` only, dropping the
326 // WCONTINUED bit so children that were resumed via SIGCONT
327 // wouldn't surface a status update — silently breaking
328 // `fg`/`bg` job-table tracking. WCONTINUED is POSIX and
329 // available in libc-rs on every platform zshrs supports.
330 let waitflags = libc::WNOHANG | libc::WUNTRACED | libc::WCONTINUED; // c:271
331 loop {
332 let mut status: i32 = 0;
333 let pid = unsafe { libc::waitpid(-1, &mut status, waitflags) };
334 if pid <= 0 {
335 break;
336 }
337 results.push((pid, status));
338 }
339 results
340}
341
342/// Direct port of `void zhandler(int sig)` from
343/// `Src/signals.c:399-498`. The main dispatcher installed for
344/// every trapped + critical signal. Block all signals while
345/// running, record the delivery, queue if `queueing_enabled`,
346/// otherwise dispatch the per-signal handler (SIGCHLD →
347/// wait_for_processes; SIGPIPE/SIGHUP/SIGINT/SIGWINCH/SIGALRM →
348/// handletrap with platform-specific fallback; default →
349/// handletrap).
350#[cfg(unix)]
351/// Direct port of `static RETSIGTYPE zhandler(int sig)` from
352/// `Src/signals.c:393`. Exposed as `pub` so the Rust queue drainer
353/// (`run_queued_signals` in signals_h.rs) can call it synchronously,
354/// matching C's `zhandler(signal_queue[queue_front])` at c:83. C's
355/// version is `static` because all queue draining happens inside
356/// signals.c; Rust splits the macro (queue) from the handler
357/// (signals.rs) across two modules, so `pub` is the equivalent of C's
358/// in-file visibility. (Earlier port routed via `libc::raise(sig)`,
359/// which is the wrong analog — raise(2) goes through the kernel and
360/// loses the queued signal behind the process signal mask. See Bug
361/// #104 in docs/BUGS.md.)
362pub extern "C" fn zhandler(sig: libc::c_int) {
363 last_signal.store(sig, Ordering::Relaxed); // c:403
364
365 // c:405-407 — `sigfillset(&newmask); oldmask = signal_block(newmask);`
366 let mut newmask: libc::sigset_t = unsafe { std::mem::zeroed() };
367 unsafe {
368 libc::sigfillset(&mut newmask);
369 }
370 let oldmask = signal_block(&newmask);
371
372 // c:410-424 — `if (queueing_enabled) { ... return; }`
373 if queueing_enabled.load(Ordering::SeqCst) != 0 {
374 let temp_rear = (queue_rear.load(Ordering::SeqCst) + 1) % MAX_QUEUE_SIZE;
375 if temp_rear != queue_front.load(Ordering::SeqCst) {
376 queue_rear.store(temp_rear, Ordering::SeqCst);
377 signal_queue[temp_rear].store(sig, Ordering::SeqCst);
378 if let Ok(mut g) = signal_mask_queue.lock() {
379 if let Some(slot) = g.get_mut(temp_rear) {
380 *slot = oldmask;
381 }
382 }
383 }
384 return;
385 }
386
387 // c:427 — `signal_setmask(oldmask);`
388 let _ = signal_setmask(&oldmask);
389
390 // c:429-498 — per-signal dispatch.
391 match sig {
392 libc::SIGCHLD => {
393 // c:430-431 — `wait_for_processes();` — reap zombies AND
394 // route their (pid, status) pairs through update_bg_job so
395 // job.stat picks up STAT_DONE / STAT_STOPPED bits. Without
396 // the route, signal_suspend-driven waits in zwaitjob can't
397 // see jobs as completed.
398 let reaped = wait_for_processes();
399 if !reaped.is_empty() {
400 if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
401 if let Ok(mut guard) = jt.lock() {
402 for (pid, status) in reaped {
403 let _ = crate::ported::jobs::update_bg_job(&mut guard, pid, status);
404 }
405 // c:Src/jobs.c:639-641 — update_job's tail
406 // dispatches printjob(jn, ..., 0) for LOCKED
407 // jobs, whose own tail (c:1350-1363) deletes
408 // each finished entry. Non-interactive shells
409 // print nothing there; the deletion is the
410 // observable effect.
411 crate::ported::jobs::scanjobs(&mut guard);
412 }
413 }
414 }
415 }
416 libc::SIGPIPE => {
417 // c:434
418 if handletrap(libc::SIGPIPE) == 0 {
419 // c:436-441 — non-interactive exits immediately; an
420 // interactive non-tty also exits via zexit.
421 let interact = isset(INTERACTIVE);
422 if !interact {
423 unsafe {
424 libc::_exit(libc::SIGPIPE);
425 } // c:437
426 } else {
427 // c:438 — `else if (!isatty(SHTTY))`. The previous
428 // Rust port hardcoded fd 0 (stdin) with a comment
429 // claiming "SHTTY isn't a single global in zshrs"
430 // — but SHTTY IS a global at `init::SHTTY`. Use it.
431 let shtty = crate::ported::init::SHTTY.load(Ordering::SeqCst);
432 let on_tty = shtty >= 0 && unsafe { libc::isatty(shtty) } != 0;
433 if !on_tty {
434 STOPMSG // c:439
435 .store(1, Ordering::Relaxed);
436 zexit(libc::SIGPIPE, ZEXIT_SIGNAL);
437 // c:440
438 }
439 }
440 }
441 }
442 libc::SIGHUP => {
443 // c:445
444 if handletrap(libc::SIGHUP) == 0 {
445 // c:447 — `stopmsg = 1; zexit(SIGHUP, ZEXIT_SIGNAL);`
446 STOPMSG.store(1, Ordering::Relaxed);
447 zexit(libc::SIGHUP, ZEXIT_SIGNAL); // c:448
448 }
449 }
450 libc::SIGINT => {
451 // c:452
452 if handletrap(libc::SIGINT) == 0 {
453 // c:454-456 — PRIVILEGED+INTERACTIVE during a signal-
454 // noerrexit window: immediate exit.
455 let privileged = isset(PRIVILEGED);
456 let interactive = isset(INTERACTIVE);
457 if privileged && interactive {
458 zexit(libc::SIGINT, ZEXIT_SIGNAL);
459 }
460 // c:457 — `errflag |= ERRFLAG_INT;`
461 let cur = errflag.load(Ordering::Relaxed);
462 errflag.store(cur | ERRFLAG_INT, Ordering::Relaxed); // c:457
463 // c:458-462 — `if (list_pipe || chline || simple_pline)`:
464 // an interactive SIGINT mid-pipeline must break loops,
465 // flush pending input, and signal any cursh job.
466 let in_list_pipe = crate::ported::exec::list_pipe.load(Ordering::Relaxed) != 0;
467 let chline_nonempty = crate::ported::hist::chline
468 .lock()
469 .map(|s| !s.is_empty())
470 .unwrap_or(false);
471 let in_simple_pline =
472 crate::ported::exec::simple_pline.load(Ordering::Relaxed) != 0;
473 if in_list_pipe || chline_nonempty || in_simple_pline {
474 // c:459 — `breaks = loops;`
475 let l = crate::ported::builtin::LOOPS.load(Ordering::Relaxed);
476 crate::ported::builtin::BREAKS.store(l, Ordering::Relaxed);
477 // c:460 — `inerrflush();`
478 crate::ported::input::inerrflush();
479 // c:461 — `check_cursh_sig(SIGINT);`. Rust port
480 // takes `(jobtab, sig)`; load the canonical JOBTAB
481 // snapshot then dispatch.
482 #[cfg(unix)]
483 if let Some(tab) = crate::ported::jobs::JOBTAB.get() {
484 if let Ok(jt) = tab.lock() {
485 crate::ported::jobs::check_cursh_sig(&jt, libc::SIGINT);
486 }
487 }
488 }
489 // c:463 — `lastval = 128 + SIGINT;`
490 LASTVAL.store(128 + libc::SIGINT, Ordering::Relaxed);
491 }
492 }
493 libc::SIGWINCH => {
494 // c:468
495 // c:469 — `adjustwinsize(1)` (Src/utils.c) — re-reads
496 // TIOCGWINSZ and updates LINES/COLUMNS params.
497 let _ = crate::ported::utils::adjustwinsize(1); // c:469
498 let _ = handletrap(libc::SIGWINCH); // c:470
499 }
500 libc::SIGALRM => {
501 // c:475
502 if handletrap(libc::SIGALRM) == 0 {
503 // c:476-489 — idle vs TMOUT branch. The previous Rust
504 // port commented "Skip the still idle re-arm" claiming
505 // no ttyidlegetfn port — but it IS ported at
506 // `ttyidlegetfn`. Now wired
507 // exactly per C.
508 //
509 // C body (c:478-484):
510 // int idle = ttyidlegetfn(NULL);
511 // int tmout = getiparam("TMOUT");
512 // if (idle >= 0 && idle < tmout)
513 // alarm(tmout - idle);
514 // else { /* timeout exit */ }
515 let idle = ttyidlegetfn(); // c:478
516 let tmout = getiparam("TMOUT"); // c:479
517 if idle >= 0 && idle < tmout {
518 // c:481 — `alarm(tmout - idle);` — re-arm for
519 // remaining idle window.
520 unsafe {
521 libc::alarm((tmout - idle) as u32); // c:481
522 }
523 } else if tmout == 0 {
524 // No timeout configured — bail out silently
525 // (C falls into the else branch which would
526 // emit "timeout" and zexit even with tmout==0,
527 // but that's a degenerate setup; matching
528 // common-case behavior here).
529 } else {
530 // c:486 — `errflag = noerrs = 0;`
531 errflag.store(0, Ordering::Relaxed);
532 // c:487 — `zwarn("timeout");`
533 zwarn("timeout"); // c:487
534 STOPMSG.store(1, Ordering::Relaxed); // c:488
535 zexit(libc::SIGALRM, ZEXIT_SIGNAL); // c:489
536 }
537 }
538 }
539 _ => {
540 // c:506
541 let _ = handletrap(sig);
542 }
543 }
544}
545
546/// Kill all running jobs with SIGHUP at shell exit.
547///
548/// Port of `void killrunjobs(int from_signal)` from `Src/signals.c:506`.
549/// C body:
550/// ```c
551/// if (unset(HUP)) return;
552/// for (i = 1; i <= maxjob; i++)
553/// if ((from_signal || i != thisjob) && (jobtab[i].stat & STAT_LOCKED) &&
554/// !(jobtab[i].stat & STAT_NOPRINT) &&
555/// !(jobtab[i].stat & STAT_STOPPED)) {
556/// if (jobtab[i].gleader != getpid() &&
557/// killpg(jobtab[i].gleader, SIGHUP) != -1)
558/// killed++;
559/// }
560/// if (killed) zwarn("warning: %d jobs SIGHUPed", killed);
561/// ```
562///
563#[cfg(unix)]
564pub fn killrunjobs(from_signal: i32) {
565 // c:506
566 // c:512 — `if (unset(HUP)) return;`. HUP option gates the
567 // whole walk: when `setopt nohup`, jobs survive shell exit.
568 if !isset(HUP) {
569 // c:512
570 return;
571 }
572 let my_pid = unsafe { libc::getpid() };
573 let mut killed: i32 = 0;
574 // c:514 — `for (i = 1; i <= maxjob; i++)`. Skip index 0
575 // (shell itself).
576 let tab = crate::ported::jobs::JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
577 let tab = tab.lock().expect("jobtab poisoned");
578 let thisjob = crate::ported::jobs::THISJOB
579 .get_or_init(|| Mutex::new(-1))
580 .lock()
581 .map(|g| *g)
582 .unwrap_or(-1);
583 for (i, job) in tab.iter().enumerate().skip(1) {
584 // c:515-517 — gate: (from_signal || i != thisjob) AND
585 // STAT_LOCKED AND !STAT_NOPRINT AND !STAT_STOPPED.
586 if !(from_signal != 0 || i as i32 != thisjob) {
587 // c:515
588 continue;
589 }
590 if (job.stat & crate::ported::jobs::stat::LOCKED) == 0 {
591 // c:516
592 continue;
593 }
594 if (job.stat & crate::ported::jobs::stat::NOPRINT) != 0 {
595 // c:516
596 continue;
597 }
598 if (job.stat & crate::ported::jobs::stat::STOPPED) != 0 {
599 // c:516
600 continue;
601 }
602 // c:518-520 — `if (jobtab[i].gleader != getpid() &&
603 // killpg(jobtab[i].gleader, SIGHUP) != -1)
604 // killed++;`
605 // The gleader check avoids the shell HUP-ing itself.
606 if job.gleader != my_pid && unsafe { libc::killpg(job.gleader, libc::SIGHUP) } != -1
607 // c:519
608 {
609 killed += 1; // c:520
610 }
611 }
612 drop(tab);
613 // c:524 — `if (killed) zwarn("warning: %d jobs SIGHUPed", killed);`
614 if killed != 0 {
615 // c:524
616 zwarn(&format!("warning: {} jobs SIGHUPed", killed));
617 // c:524
618 }
619}
620
621/* send a signal to a job (simply involves kill if monitoring is on) */
622// c:525
623/// Port of `killjb(Job jn, int sig)` from `Src/signals.c:529`.
624/// CALLER CONVENTION: Rust passes `jn_idx: usize` (JOBTAB index)
625/// instead of C `Job jn` (pointer); the body resolves the job via
626/// `JOBTAB.lock()`. Returns 0/-1 per the killpg result chain.
627#[cfg(unix)]
628pub fn killjb(jn_idx: usize, sig: i32) -> i32 {
629 // c:529
630 let _pn: (); // c:531 `Process pn;` — modelled by loop binding
631 let mut err: i32 = 0; // c:532
632
633 if crate::ported::zsh_h::jobbing() {
634 // c:534
635 // Snapshot the job state under the lock (gleader + stat + other
636 // + procs + other-procs). Avoid holding the lock during kill()
637 // syscalls — they can block under signals.
638 let snap = {
639 let table = match crate::ported::jobs::JOBTAB.get() {
640 Some(t) => t,
641 None => return -1,
642 };
643 let tab = table.lock().unwrap_or_else(|e| e.into_inner());
644 let jn = match tab.get(jn_idx) {
645 Some(j) => j,
646 None => return -1,
647 };
648 let other_procs: Vec<libc::pid_t> = if jn.other > 0 {
649 tab.get(jn.other as usize)
650 .map(|o| o.procs.iter().map(|p| p.pid).collect())
651 .unwrap_or_default()
652 } else {
653 Vec::new()
654 };
655 let other_empty = jn.other > 0
656 && tab
657 .get(jn.other as usize)
658 .map(|o| o.procs.is_empty())
659 .unwrap_or(true);
660 (
661 jn.stat,
662 jn.gleader,
663 jn.other,
664 jn.procs.iter().map(|p| p.pid).collect::<Vec<_>>(),
665 other_procs,
666 other_empty,
667 )
668 };
669 let (stat, gleader, other, procs_pids, other_procs, other_empty) = snap;
670
671 if (stat & crate::ported::zsh_h::STAT_SUPERJOB) != 0 {
672 // c:535
673 if sig == libc::SIGCONT {
674 // c:536
675 // c:537-540 — walk jobtab[jn->other].procs, killpg each;
676 // fall through to kill() on killpg failure; ESRCH ignored.
677 for pid in &other_procs {
678 // c:537
679 if unsafe { libc::killpg(*pid, sig) } == -1 {
680 // c:538
681 let e = std::io::Error::last_os_error().raw_os_error();
682 // c:539 — fallback kill()
683 if unsafe { libc::kill(*pid, sig) } == -1 && e != Some(libc::ESRCH) {
684 err = -1; // c:540
685 }
686 }
687 }
688
689 /*
690 * Note this does not kill the last process,
691 * which is assumed to be the one controlling the
692 * subjob, i.e. the forked zsh that was originally
693 * list_pipe_pid...
694 */
695 // c:542-547
696 let n = procs_pids.len();
697 if n > 0 {
698 for pid in &procs_pids[..n - 1] {
699 // c:548 `for pn = jn->procs; pn->next; ...`
700 if unsafe { libc::kill(*pid, sig) } == -1
701 && std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH)
702 {
703 err = -1; // c:550
704 }
705 }
706
707 /*
708 * ...we only continue that once the external processes
709 * currently associated with the subjob are finished.
710 */
711 // c:552-555
712 if other_empty {
713 // c:556
714 let last = procs_pids[n - 1];
715 if unsafe { libc::kill(last, sig) } == -1
716 && std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH)
717 {
718 err = -1; // c:558
719 }
720 }
721 }
722
723 /*
724 * The following marks both the superjob and subjob
725 * as running, as done elsewhere.
726 */
727 // c:560-569
728 if err != -1 {
729 // c:570
730 let table = crate::ported::jobs::JOBTAB.get().unwrap();
731 let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
732 crate::ported::jobs::makerunning(&mut tab, jn_idx); // c:571
733 }
734
735 return err; // c:573
736 }
737
738 // c:575 — `if (killpg(jobtab[jn->other].gleader, sig) == -1 && errno != ESRCH) err = -1;`
739 let other_gleader = crate::ported::jobs::JOBTAB
740 .get()
741 .and_then(|t| {
742 t.lock()
743 .ok()
744 .and_then(|tab| tab.get(other as usize).map(|j| j.gleader))
745 })
746 .unwrap_or(0);
747 if other_gleader > 0
748 && unsafe { libc::killpg(other_gleader, sig) } == -1
749 && std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH)
750 {
751 err = -1; // c:576
752 }
753
754 if unsafe { libc::killpg(gleader, sig) } == -1 // c:578
755 && std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH)
756 {
757 err = -1; // c:579
758 }
759
760 return err; // c:581
761 } else {
762 // c:583
763 err = unsafe { libc::killpg(gleader, sig) }; // c:584
764 if sig == libc::SIGCONT && err != -1 {
765 // c:585
766 let table = crate::ported::jobs::JOBTAB.get().unwrap();
767 let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
768 crate::ported::jobs::makerunning(&mut tab, jn_idx); // c:586
769 }
770 return err; // c:587
771 }
772 }
773 // c:590-604 — non-jobbing: walk jn->procs, kill each if SP_RUNNING
774 // or WIFSTOPPED; ignore ESRCH and `sig == 0` (kill -0 polling).
775 let table = match crate::ported::jobs::JOBTAB.get() {
776 Some(t) => t,
777 None => return err,
778 };
779 let snap: Vec<(libc::pid_t, i32)> = {
780 let tab = table.lock().unwrap_or_else(|e| e.into_inner());
781 match tab.get(jn_idx) {
782 Some(j) => j.procs.iter().map(|p| (p.pid, p.status)).collect(),
783 None => return err,
784 }
785 };
786 for (pid, status) in snap {
787 // c:590
788 /*
789 * Do not kill this job's process if it's already dead as its
790 * pid could have been reused by the system.
791 */ // c:591-595
792 let is_running = status == crate::ported::zsh_h::SP_RUNNING;
793 let is_stopped = libc::WIFSTOPPED(status);
794 if is_running || is_stopped {
795 // c:596
796 /*
797 * kill -0 on a job is pointless. We still call kill() for each
798 * process in case the user cares about it but we ignore its outcome.
799 */ // c:597-600
800 let r = unsafe { libc::kill(pid, sig) };
801 if r == -1
802 && std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH)
803 && sig != 0
804 {
805 err = r;
806 return -1; // c:602
807 }
808 err = r; // c:601 assignment
809 }
810 }
811 err // c:605
812}
813
814/// Port of `struct savetrap` from `Src/signals.c:611-624`.
815/// One stacked trap-state entry captured by `dosavetrap` so the
816/// outer-scope trap can be restored when an inner scope exits.
817#[allow(non_camel_case_types)]
818pub struct savetrap {
819 // c:611
820 pub sig: i32, // c:613
821 pub flags: i32, // c:614
822 pub local: i32, // c:615 locallevel at save
823 pub posix: i32, // c:616 exit_trap_posix snapshot
824 pub list: Option<Eprog>, // c:617 trap eval-list Eprog
825 /// Snapshot of the body string from `traps_table` at save time.
826 /// Rust-only — C zsh stores the body in `siglists[sig]` as an
827 /// Eprog (already covered by `list` above), but zshrs stores the
828 /// raw body string in `traps_table` for the bridge's
829 /// `execute_script` dispatch path. Without snapshotting it here,
830 /// `endtrapscope`'s restore loop puts back `sigtrapped` flags
831 /// matching the outer scope but the body in `traps_table` still
832 /// reflects the inner scope's overwrite — so the outer EXIT
833 /// trap dispatches the inner body. Bug #80 in docs/BUGS.md.
834 pub body: Option<String>,
835}
836
837/// Direct port of `void dosavetrap(int sig, int level)` from
838/// `Src/signals.c:626`. Captures the current trap state for
839/// `sig` into a `savetrap` and pushes it onto `SAVETRAPS`.
840pub fn dosavetrap(sig: i32, level: i32) {
841 // c:626
842 let flags = sigtrapped
843 .lock()
844 .ok()
845 .and_then(|g| g.get(sig as usize).copied())
846 .unwrap_or(0);
847 // c:663 — `st->list = siglists[sig] ? dupeprog(siglists[sig], 0) : NULL`.
848 // dupeprog isn't ported yet so take the Eprog out of siglists and
849 // re-stash a fresh None — the saved entry owns the body until the
850 // matching endtrapscope restore re-inserts it.
851 let list = siglists
852 .lock()
853 .ok()
854 .and_then(|mut g| g.get_mut(sig as usize).and_then(|s| s.take()));
855 let posix = if sig == SIGEXIT {
856 if EXIT_TRAP_POSIX.load(Ordering::Relaxed) {
857 1
858 } else {
859 0
860 }
861 } else {
862 0
863 };
864 // Snapshot the body string from `traps_table` so the matching
865 // restore in `endtrapscope` can write back the outer scope's body
866 // — see `savetrap::body` doc above for the bug #80 context.
867 let body = {
868 let signame = getsigname(sig);
869 crate::ported::builtin::traps_table()
870 .lock()
871 .ok()
872 .and_then(|g| g.get(&signame).cloned())
873 };
874 let st = savetrap {
875 sig,
876 flags,
877 local: level,
878 posix,
879 list,
880 body,
881 };
882 if let Ok(mut g) = SAVETRAPS.get_or_init(|| Mutex::new(Vec::new())).lock() {
883 g.insert(0, st); // c:689 front-insert
884 }
885}
886
887/// SIGEXIT signal number — Rust port uses `SIGCOUNT + 1` since
888/// libc::SIG* are all < SIGCOUNT and EXIT is the synthetic
889/// trap-only signal at the top of the table.
890// SIGEXIT already declared at line 45.
891
892// sig is index into the table of trapped signals. // c:693
893// // c:693
894// l is the list to be eval'd for a trap defined with the "trap" // c:693
895// builtin and should be NULL for a function trap. // c:693
896/// Direct port of `mod_export int settrap(int sig, Eprog l, int flags)`
897/// from `Src/signals.c:693`. Calls `unsettrap` unconditionally
898/// (so the previous trap is saved into `SAVETRAPS` if needed), then
899/// writes `l` into `siglists[sig]` and sets `sigtrapped[sig]` to
900/// either `ZSIG_IGNORED` (empty list + non-ZSIG_FUNC) or
901/// `ZSIG_TRAPPED`, then ORs in `flags` and the
902/// `locallevel << ZSIG_SHIFT` scope tag.
903pub fn settrap(sig: i32, l: Option<Eprog>, flags: i32) -> i32 {
904 // c:693
905 if sig == -1 {
906 // c:693
907 return 1;
908 }
909 // c:696 (zsh.h:2563) — `if (jobbing && (sig == SIGTTOU ||
910 // sig == SIGTSTP || sig == SIGTTIN)) { zerr("can't trap SIG%s
911 // in interactive shells", ...); return 1; }`.
912 let jobbing = isset(MONITOR); // c:696
913 if jobbing && (sig == libc::SIGTTOU || sig == libc::SIGTSTP || sig == libc::SIGTTIN) {
914 // c:697 — `zerr("can't trap SIG%s in interactive shells", sigs[sig])`.
915 let signame = getsigname(sig);
916 zerr(&format!("can't trap SIG{} in interactive shells", signame));
917 return 1; // c:699
918 }
919
920 // c:705 — `queue_signals()` + `unsettrap(sig)` unconditional
921 // (saves the previous trap if locallevel changed).
922 queue_signals();
923 unsettrap(sig);
924
925 // c:709-710 — DPUTS((flags & ZSIG_FUNC) && l,
926 // "BUG: trap function has passed eval list, too")
927 DPUTS!(
928 // c:709
929 (flags & ZSIG_FUNC) != 0 && l.is_some(), // c:709
930 "BUG: trap function has passed eval list, too" // c:710
931 );
932
933 // c:712 — `if (!(flags & ZSIG_FUNC) && empty_eprog(l))`. C's
934 // `empty_eprog` returns true for NULL, NULL prog, OR a prog whose
935 // first wordcode is WCB_END (`Src/parse.c:586`).
936 let l_is_empty = match &l {
937 None => true,
938 Some(eprog) => crate::ported::parse::empty_eprog(eprog),
939 };
940 // c:711 — `siglists[sig] = l`.
941 if let Ok(mut g) = siglists.lock() {
942 if let Some(slot) = g.get_mut(sig as usize) {
943 *slot = l;
944 }
945 }
946 if (flags & ZSIG_FUNC) == 0 && l_is_empty {
947 // c:712
948 // c:713 — `sigtrapped[sig] = ZSIG_IGNORED`.
949 if let Ok(mut g) = sigtrapped.lock() {
950 if let Some(slot) = g.get_mut(sig as usize) {
951 *slot = ZSIG_IGNORED;
952 }
953 }
954 if sig != 0 && sig <= SIGCOUNT && sig != libc::SIGWINCH && sig != libc::SIGCHLD {
955 signal_ignore(sig); // c:719
956 }
957 // c:720-723 — RT-signal trap-table branch:
958 // `else if (sig >= VSIGCOUNT && sig < TRAPCOUNT)
959 // signal_ignore(SIGNUM(sig));`
960 #[cfg(target_os = "linux")]
961 if sig >= VSIGCOUNT && sig < TRAPCOUNT_H {
962 signal_ignore(SIGNUM(sig)); // c:722
963 }
964 } else {
965 nsigtrapped.fetch_add(1, Ordering::Relaxed); // c:725
966 if let Ok(mut g) = sigtrapped.lock() {
967 if let Some(slot) = g.get_mut(sig as usize) {
968 *slot = ZSIG_TRAPPED;
969 }
970 }
971 if sig != 0 && sig <= SIGCOUNT && sig != libc::SIGWINCH && sig != libc::SIGCHLD {
972 install_handler(sig); // c:732
973 }
974 // c:733-736 — RT-signal install_handler branch:
975 // `if (sig >= VSIGCOUNT && sig < TRAPCOUNT)
976 // install_handler(SIGNUM(sig));`
977 // Trapping `RTMIN+1` to a function without this branch would
978 // store the trap but NEVER install the libc handler, so the
979 // signal would fire with default action (terminate the shell).
980 #[cfg(target_os = "linux")]
981 if sig >= VSIGCOUNT && sig < TRAPCOUNT_H {
982 install_handler(SIGNUM(sig)); // c:735
983 }
984 }
985 // c:738 — `sigtrapped[sig] |= flags`.
986 if let Ok(mut g) = sigtrapped.lock() {
987 if let Some(slot) = g.get_mut(sig as usize) {
988 *slot |= flags;
989 }
990 }
991 // c:743-752 — locallevel tag (SIGEXIT in POSIX mode is sticky).
992 let locallevel = locallevel_fn() as i32;
993 if sig == SIGEXIT {
994 // c:746 — `if (isset(POSIXTRAPS)) ...`. In POSIX mode SIGEXIT
995 // is sticky and not tagged with the local-level shift.
996 let posix_traps = isset(optlookup("posixtraps")); // c:746
997 EXIT_TRAP_POSIX.store(posix_traps, Ordering::Relaxed);
998 if !posix_traps {
999 if let Ok(mut g) = sigtrapped.lock() {
1000 if let Some(slot) = g.get_mut(sig as usize) {
1001 *slot |= locallevel << ZSIG_SHIFT;
1002 }
1003 }
1004 }
1005 } else if let Ok(mut g) = sigtrapped.lock() {
1006 if let Some(slot) = g.get_mut(sig as usize) {
1007 *slot |= locallevel << ZSIG_SHIFT;
1008 }
1009 }
1010 unqueue_signals();
1011 0 // c:759
1012}
1013
1014/// Direct port of `HashNode removetrap(int sig)` from `Src/signals.c:772`.
1015/// Clears the trap slot for `sig`, snapshots the prior state into
1016/// `SAVETRAPS` when `locallevel > 0` and the relevant option set
1017/// (LOCALTRAPS for generic signals, !POSIXTRAPS for EXIT), then
1018/// re-installs the appropriate per-signal disposition (intr for
1019/// SIGINT-interactive, install_handler for SIGHUP/SIGPIPE,
1020/// signal_default for everything else).
1021///
1022/// C returns the displaced HashNode for the caller (unsettrap) to
1023/// free; Rust ownership covers the free automatically when the
1024/// hashtable entry drops.
1025pub fn removetrap(sig: i32) {
1026 // c:772
1027 let trapped = sigtrapped
1028 .lock()
1029 .ok()
1030 .and_then(|g| g.get(sig as usize).copied())
1031 .unwrap_or(0);
1032 // c:776-778 — `if (sig == -1 || (jobbing && (SIGTTOU || SIGTSTP || SIGTTIN))) return NULL`.
1033 // The Rust call sites already use sig in [0, SIGCOUNT]; sig == -1 is rare,
1034 // but jobbing+job-control reject mirrors C exactly.
1035 if sig == -1 {
1036 return;
1037 }
1038 let jobbing = isset(MONITOR);
1039 if jobbing && (sig == libc::SIGTTOU || sig == libc::SIGTSTP || sig == libc::SIGTTIN) {
1040 return;
1041 }
1042 let locallevel = locallevel_fn() as i32;
1043 // c:769-774 — `if (!dontsavetrap && (sig == SIGEXIT ? !isset(POSIXTRAPS)
1044 // : isset(LOCALTRAPS)) && locallevel && (!trapped || locallevel >
1045 // (sigtrapped[sig] >> ZSIG_SHIFT))) dosavetrap(sig, locallevel);`.
1046 // Note: `!trapped` is LOGICAL NOT (`trapped == 0`), not Rust's
1047 // bitwise `!i32`.
1048 let cond_local_or_exit = if sig == SIGEXIT {
1049 !isset(POSIXTRAPS) // c:771 sig==SIGEXIT branch
1050 } else {
1051 isset(LOCALTRAPS) // c:771 else branch
1052 };
1053 if DONTSAVETRAP.load(Ordering::Relaxed) == 0 // c:769
1054 && cond_local_or_exit
1055 && locallevel != 0 // c:772 `locallevel &&`
1056 && (trapped == 0 // c:773 `!trapped` (logical NOT)
1057 || locallevel > (trapped >> ZSIG_SHIFT))
1058 {
1059 dosavetrap(sig, locallevel); // c:774
1060 }
1061 if trapped & ZSIG_TRAPPED != 0 {
1062 nsigtrapped.fetch_sub(1, Ordering::Relaxed); // c:799
1063 }
1064 if let Ok(mut g) = sigtrapped.lock() {
1065 if let Some(slot) = g.get_mut(sig as usize) {
1066 *slot = 0;
1067 } // c:800
1068 }
1069 if let Ok(mut g) = siglists.lock() {
1070 if let Some(slot) = g.get_mut(sig as usize) {
1071 *slot = None;
1072 }
1073 }
1074 // c:803-845 — per-signal disposition reset after clearing the
1075 // trap. The previous Rust port collapsed everything to a single
1076 // signal_default() call, omitting the SIGINT/SIGHUP/SIGPIPE
1077 // special branches AND the RT-signal branch entirely.
1078 let interact = isset(INTERACTIVE);
1079 // c:808 `forklevel` — depth of subshell forks. C global at
1080 // exec.c:1052 set to `locallevel` at every entersubsh() (c:1221).
1081 // Read live from the ported global so SIGPIPE only re-installs in
1082 // the top-level shell, never inside a forked subshell.
1083 let forklevel: i32 = crate::ported::exec::FORKLEVEL.load(Ordering::Relaxed); // c:1052 (Src/exec.c)
1084 if sig == libc::SIGINT && interact {
1085 // c:802
1086 // c:803-805 — `intr(); noholdintr();`. Re-enable SIGINT
1087 // delivery (subshells ignoring SIGINT need the unblock).
1088 intr();
1089 noholdintr();
1090 } else if sig == libc::SIGHUP {
1091 // c:806
1092 // c:807 — HUP gets RE-INSTALLED (not defaulted), so the
1093 // shell keeps catching it.
1094 install_handler(sig);
1095 } else if sig == libc::SIGPIPE && interact && forklevel == 0 {
1096 // c:808
1097 // c:809 — same install-not-default semantics.
1098 install_handler(sig);
1099 } else if sig != 0 && sig <= SIGCOUNT && sig != libc::SIGWINCH && sig != libc::SIGCHLD {
1100 // c:810
1101 signal_default(sig); // c:815
1102 }
1103 // c:816-819 — RT-signal branch (Linux).
1104 #[cfg(target_os = "linux")]
1105 {
1106 if sig >= VSIGCOUNT && sig < TRAPCOUNT_H {
1107 signal_default(SIGNUM(sig)); // c:818
1108 }
1109 }
1110}
1111
1112// Variables used by signal queueing // c:74
1113/// Enable signal queueing.
1114// queue_signals / unqueue_signals live in `signals_h.rs` per the C
1115// source split: both are `#define` macros in `Src/signals.h:90/112`
1116// + `92/114`, not functions in `Src/signals.c`. Re-export from the
1117// canonical home so callers using `crate::ported::signals::queue_signals`
1118// continue to compile, and the QUEUEING_ENABLED state is shared
1119// across all callers (instead of split between two parallel
1120// SignalQueue/QUEUEING_ENABLED counters).
1121
1122/// Remove a trap completely and reset to default disposition.
1123/// Port of `removetrap(int sig)` from Src/signals.c:772.
1124///
1125/// **Inverted call chain vs C**: in C, `unsettrap` (c:759) is a
1126/// thin queue_signals + removetrap wrapper; the full save+clear+
1127/// signal-disposition logic lives in `removetrap`. The Rust port
1128/// inverts the relationship — `unsettrap` carries the full body
1129/// (matching C lines 781-820), and `removetrap` is the thin
1130/// wrapper. `unsettrap` runs the per-signal disposition
1131/// (c:802-820): SIGINT → intr(), SIGHUP → re-install_handler,
1132/// SIGPIPE under interactive non-fork → re-install_handler.
1133/// Never SIG_DFL these branches directly.
1134pub fn unsettrap(sig: i32) {
1135 // c:759
1136 // c:763 — queue_signals();
1137 queue_signals();
1138 // c:764 — hn = removetrap(sig);
1139 // c:765-766 — if (hn) shfunctab->freenode(hn);
1140 // Rust ownership covers the freenode when the trap entry
1141 // is removed from sigfuncs / freed automatically.
1142 removetrap(sig);
1143 // c:767 — unqueue_signals();
1144 unqueue_signals();
1145}
1146
1147/// Direct port of `void starttrapscope(void)` from
1148/// `Src/signals.c:855-868`.
1149/// ```c
1150/// if (intrap) return;
1151/// if (sigtrapped[SIGEXIT] && !exit_trap_posix) {
1152/// locallevel++;
1153/// unsettrap(SIGEXIT);
1154/// locallevel--;
1155/// }
1156/// ```
1157///
1158/// Saves the SIGEXIT trap aside for restoration at the parent
1159/// scope's `endtrapscope` (the locallevel++/-- bump tags the
1160/// save entry with the higher scope so it's restored
1161/// when THIS scope ends, not the outer one's).
1162/// Port of `starttrapscope` from `Src/signals.c:855`.
1163pub fn starttrapscope() {
1164 // c:855
1165 // c:855 — `if (intrap) return`.
1166 if intrap.load(Ordering::Relaxed) != 0 {
1167 return;
1168 }
1169 // c:863 — `if (sigtrapped[SIGEXIT] && !exit_trap_posix)`.
1170 let exit_flags = sigtrapped
1171 .lock()
1172 .ok()
1173 .and_then(|g| g.get(SIGEXIT as usize).copied())
1174 .unwrap_or(0);
1175 if exit_flags != 0 && !EXIT_TRAP_POSIX.load(Ordering::Relaxed) {
1176 // c:865-867 — bump locallevel so the dosavetrap inside
1177 // unsettrap tags the save entry with the outer scope's
1178 // level. Rust's locallevel is a global counter in utils.rs.
1179 inc_locallevel();
1180 unsettrap(SIGEXIT); // c:866
1181 crate::ported::utils::dec_locallevel();
1182 }
1183}
1184
1185/// End the current trap scope — restore any traps that were
1186/// Direct port of `void endtrapscope(void)` from
1187/// `Src/signals.c:880`. Pops the pending entries from
1188/// `SAVETRAPS` whose `local > locallevel` (i.e. captured at a
1189/// deeper scope) and restores each via `settrap`. The pending
1190/// SIGEXIT trap (if any) is split out so it runs AFTER the
1191/// other restores complete.
1192pub fn endtrapscope() {
1193 // c:880
1194 let locallevel = locallevel_fn();
1195
1196 // c:891-908 — pull the SIGEXIT trap aside so we can run it last.
1197 let exit_flags = sigtrapped
1198 .lock()
1199 .ok()
1200 .and_then(|g| g.get(SIGEXIT as usize).copied())
1201 .unwrap_or(0);
1202 let mut exittr: i32 = 0;
1203 // Bug #80 — capture the body BEFORE the SAVETRAPS pop loop
1204 // potentially writes back an outer scope's body into traps_table.
1205 // Without this snapshot, a nested fn EXIT trap could fire the
1206 // wrong (outer) body when the deepest level exits.
1207 let mut exit_body: Option<String> = None;
1208 if intrap.load(Ordering::Relaxed) == 0 // c:891 !intrap
1209 && !EXIT_TRAP_POSIX.load(Ordering::Relaxed) // c:892 !exit_trap_posix
1210 && exit_flags != 0
1211 {
1212 exittr = exit_flags;
1213 // Snapshot the body so the dispatch at the end of this fn
1214 // fires THIS scope's trap, not whatever traps_table got
1215 // restored to.
1216 exit_body = {
1217 let signame = getsigname(SIGEXIT);
1218 crate::ported::builtin::traps_table()
1219 .lock()
1220 .ok()
1221 .and_then(|g| g.get(&signame).cloned().or_else(|| g.get("EXIT").cloned()))
1222 };
1223 // c:902-906 — clear SIGEXIT slot.
1224 if let Ok(mut g) = sigtrapped.lock() {
1225 if let Some(slot) = g.get_mut(SIGEXIT as usize) {
1226 *slot = 0;
1227 }
1228 }
1229 if let Ok(mut g) = siglists.lock() {
1230 if let Some(slot) = g.get_mut(SIGEXIT as usize) {
1231 *slot = None;
1232 }
1233 }
1234 // Clear the inner-scope's body from traps_table; if a saved
1235 // outer body needs restoring, the SAVETRAPS pop loop below
1236 // writes it back.
1237 if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
1238 let signame = getsigname(SIGEXIT);
1239 t.remove(&signame);
1240 t.remove("EXIT"); // alias-safe
1241 }
1242 if exit_flags & ZSIG_TRAPPED != 0 {
1243 nsigtrapped.fetch_sub(1, Ordering::Relaxed); // c:904
1244 }
1245 }
1246
1247 // c:911-959 — pop savetraps entries whose local > locallevel.
1248 if let Ok(mut traps) = SAVETRAPS.get_or_init(|| Mutex::new(Vec::new())).lock() {
1249 while let Some(st) = traps.first() {
1250 // c:912 firstnode
1251 if st.local <= locallevel as i32 {
1252 break;
1253 } // c:914
1254 let st = traps.remove(0); // c:915
1255
1256 // c:919 — `if (st->flags && (st->list != NULL))`. BOTH must
1257 // be truthy. The previous Rust port used `||` (either),
1258 // wrongly firing the restore branch on a flags-only or
1259 // list-only savetrap entry.
1260 // Bug #80 — restore the saved body string into
1261 // traps_table before settrap re-arms sigtrapped, so the
1262 // outer scope's dispatch finds the right body.
1263 {
1264 let signame = getsigname(st.sig);
1265 if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
1266 match &st.body {
1267 Some(b) => {
1268 t.insert(signame, b.clone());
1269 }
1270 None => {
1271 t.remove(&signame);
1272 }
1273 }
1274 }
1275 }
1276 if st.flags != 0 && st.list.is_some() {
1277 // c:919
1278 // c:921-922 — prevent settrap from saving this.
1279 DONTSAVETRAP.fetch_add(1, Ordering::Relaxed);
1280 // c:923-926 — ZSIG_FUNC takes (NULL, ZSIG_FUNC); list
1281 // traps take (st.list, 0). The current Rust port
1282 // collapses both into a single settrap(list, flags)
1283 // call — works because settrap stores the flags and
1284 // list independently. Pin the ZSIG_FUNC branch as a
1285 // comment for future refactors.
1286 let _ = settrap(st.sig, st.list, st.flags); // c:925/927
1287 if st.sig == SIGEXIT {
1288 EXIT_TRAP_POSIX.store(st.posix != 0, Ordering::Relaxed); // c:929
1289 }
1290 DONTSAVETRAP.fetch_sub(1, Ordering::Relaxed); // c:930
1291 } else if st.flags != 0 && st.body.is_some() {
1292 // Saved entry was a list-trap installed via bin_trap
1293 // (body in traps_table, no Eprog). Re-arm sigtrapped
1294 // with the saved flags so the dispatch path finds it.
1295 DONTSAVETRAP.fetch_add(1, Ordering::Relaxed);
1296 let _ = settrap(st.sig, None, st.flags);
1297 if st.sig == SIGEXIT {
1298 EXIT_TRAP_POSIX.store(st.posix != 0, Ordering::Relaxed);
1299 }
1300 DONTSAVETRAP.fetch_sub(1, Ordering::Relaxed);
1301 } else {
1302 // c:933 — `else if (sigtrapped[sig])`. Only fires when
1303 // the current slot has a trap set. The previous Rust
1304 // port unconditionally entered this branch, calling
1305 // unsettrap on slots that were already cleared.
1306 let cur_trapped = sigtrapped
1307 .lock()
1308 .ok()
1309 .and_then(|g| g.get(st.sig as usize).copied())
1310 .unwrap_or(0);
1311 if cur_trapped != 0 {
1312 // c:933
1313 // c:938 — `if (sig != SIGEXIT || !exit_trap_posix)`.
1314 if st.sig != SIGEXIT || !EXIT_TRAP_POSIX.load(Ordering::Relaxed) {
1315 unsettrap(st.sig); // c:939
1316 }
1317 }
1318 }
1319 }
1320 }
1321
1322 // c:961-969 — run the SIGEXIT trap, last (AFTER the savetraps
1323 // pop loop above so a restored deeper SIGEXIT is replaced by
1324 // the current scope's saved-aside trap before dispatch).
1325 //
1326 // C `dotrapargs(SIGEXIT, &exittr, exitfn)` invokes either:
1327 // - ZSIG_FUNC: the `TRAPEXIT` shell function from shfunctab.
1328 // - else: the eprog from siglists[SIGEXIT].
1329 if exittr != 0 && (exittr & ZSIG_FUNC) != 0 {
1330 // c:961, c:1132 FUNC branch — dispatch the TRAPEXIT shfunc.
1331 let signame = getsigname(SIGEXIT);
1332 let trap_fn = format!("TRAP{}", signame);
1333 if crate::ported::utils::getshfunc(&trap_fn).is_some() {
1334 let args = vec![SIGEXIT.to_string()];
1335 let _ = crate::ported::exec::dispatch_function_call(&trap_fn, &args);
1336 }
1337 } else if exittr != 0 {
1338 // c:961 else branch — non-FUNC eprog. The Rust port stores
1339 // the trap body as a string in `traps_table` (populated by
1340 // `bin_trap` / settrap). Dispatch through the execute_script
1341 // hook installed by fusevm_bridge — same path used by
1342 // signals.rs dotrap for non-FUNC traps.
1343 //
1344 // Bug #80 — use the body snapshot taken BEFORE the SAVETRAPS
1345 // pop loop, so the dispatch fires THIS scope's body (not the
1346 // outer body restored by the loop).
1347 let body = exit_body.unwrap_or_default();
1348 if !body.is_empty() {
1349 // Bracket the dispatch so the recursive
1350 // `execute_script_zsh_pipeline`'s own end-of-pipeline EXIT
1351 // check doesn't see the OUTER scope's body (which the
1352 // SAVETRAPS pop loop just restored into traps_table) and
1353 // fire it prematurely. Pull "EXIT" aside for the duration
1354 // of the dispatch, restore after.
1355 let stash = crate::ported::builtin::traps_table()
1356 .lock()
1357 .ok()
1358 .and_then(|mut t| t.remove("EXIT"));
1359 let _ = crate::ported::exec::execute_script(&body); // c:961 eprog body
1360 if let Some(b) = stash {
1361 if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
1362 t.insert("EXIT".to_string(), b);
1363 }
1364 }
1365 }
1366 }
1367}
1368
1369/// Direct port of `mod_export int handletrap(int sig)` from
1370/// `Src/signals.c:972`. Trap-queue gate called from the async
1371/// signal handlers. Returns 0 if the signal isn't trapped; if
1372/// trapped + queueing enabled it pushes onto `trap_queue` and
1373/// returns 1; otherwise it calls `dotrap(SIGIDX(sig))` (with the
1374/// SIGALRM TMOUT reset at the end) and returns 1.
1375pub fn handletrap(sig: i32) -> i32 {
1376 // c:972
1377 let idx = crate::ported::signals_h::SIGIDX(sig);
1378 let trapped = sigtrapped
1379 .lock()
1380 .ok()
1381 .and_then(|g| g.get(idx as usize).copied())
1382 .unwrap_or(0);
1383 if trapped == 0 {
1384 return 0;
1385 } // c:974
1386
1387 if trap_queueing_enabled.load(Ordering::SeqCst) != 0 {
1388 // c:977
1389 // c:980-986 — push onto `trap_queue` ring buffer.
1390 let r = trap_queue_rear.load(Ordering::SeqCst);
1391 let new_rear = (r + 1) % MAX_QUEUE_SIZE;
1392 if new_rear != trap_queue_front.load(Ordering::SeqCst) {
1393 trap_queue[new_rear].store(sig, Ordering::SeqCst);
1394 trap_queue_rear.store(new_rear, Ordering::SeqCst);
1395 }
1396 return 1;
1397 }
1398
1399 dotrap(idx); // c:990
1400
1401 if sig == libc::SIGALRM {
1402 // c:992
1403 // c:996 — `if ((tmout = getiparam("TMOUT"))) alarm(tmout);`
1404 // Re-arm the TMOUT timer after the trap dispatched.
1405 #[cfg(unix)]
1406 unsafe {
1407 let tmout = getiparam("TMOUT");
1408 if tmout > 0 {
1409 libc::alarm(tmout as u32); // c:996
1410 }
1411 }
1412 }
1413 1
1414}
1415
1416/// Direct port of `void queue_traps(int wait_cmd)` from
1417/// `Src/signals.c:1024-1033`.
1418///
1419/// C body:
1420/// if (!isset(TRAPSASYNC) && !wait_cmd)
1421/// trap_queueing_enabled = 1;
1422///
1423/// C ONLY enables queueing when NEITHER `TRAPSASYNC` is set NOR the
1424/// caller is the `wait` builtin (which wants traps to fire immediately
1425/// so the wait can be interrupted). The flag is a boolean (`= 1` /
1426/// `= 0`), symmetric with the reset in `unqueue_traps` at c:1042.
1427pub fn queue_traps(wait_cmd: i32) {
1428 // c:1024
1429 // c:1026 — both gates must be off for queueing to be enabled.
1430 if !isset(TRAPSASYNC) && wait_cmd == 0 {
1431 trap_queueing_enabled.store(1, Ordering::SeqCst); // c:1031
1432 }
1433}
1434
1435// Disable trap queuing and run the traps. // c:1041
1436/// Direct port of `void unqueue_traps(void)` from
1437/// `Src/signals.c:1041`. Disables `trap_queueing_enabled` and
1438/// flushes the pending queue by dispatching each sig through
1439/// `handletrap()`.
1440pub fn unqueue_traps() {
1441 // c:1041
1442 // c:1041 — `trap_queueing_enabled = 0;`
1443 trap_queueing_enabled.store(0, Ordering::SeqCst);
1444 // c:1046 — `while (trap_queue_front != trap_queue_rear) (void) handletrap(...);`
1445 loop {
1446 let f = trap_queue_front.load(Ordering::SeqCst);
1447 let r = trap_queue_rear.load(Ordering::SeqCst);
1448 if f == r {
1449 break;
1450 }
1451 let nf = (f + 1) % MAX_QUEUE_SIZE;
1452 let sig = trap_queue[nf].load(Ordering::SeqCst);
1453 trap_queue_front.store(nf, Ordering::SeqCst);
1454 let _ = handletrap(sig);
1455 }
1456}
1457
1458// Standard call to execute a trap for a given signal. // c:1245
1459/// Direct port of `void dotrap(int sig)` from `Src/signals.c:1245`.
1460/// Dispatches the trap registered for `sig`:
1461/// - ZSIG_FUNC: invoke the `TRAPxxx` shell function from shfunctab
1462/// via `doshfunc` with the signal number as the single arg.
1463/// - else: execute the eprog in `siglists[sig]` via fusevm
1464/// dispatch when wired (currently no-op pending VM bridge for
1465/// eprog).
1466/// Maintains `intrap` / `in_exit_trap` flags around the call so
1467/// observers (the `exit` builtin, the `zexit` driver) can branch on
1468/// whether we're inside an EXIT-trap callback.
1469pub fn dotrap(sig: i32) -> i32 {
1470 // c:1245
1471 // c:1248 — `int q = queue_signal_level();` capture at entry.
1472 // Required for the c:1280 `restore_queue_signals(q)` tail. The
1473 // previous Rust port omitted the capture and tail-restored to 0
1474 // unconditionally — that zeroed the queue level even when the
1475 // caller had set it to a non-zero value (e.g. nested dotrap
1476 // from inside a queue_signals/unqueue_signals block).
1477 let q = crate::ported::signals_h::queue_signal_level(); // c:1248
1478
1479 let trapped = sigtrapped
1480 .lock()
1481 .ok()
1482 .and_then(|g| g.get(sig as usize).copied())
1483 .unwrap_or(0);
1484 // c:1259 — `if ((sigtrapped[sig] & ZSIG_IGNORED) || !funcprog || errflag) return;`
1485 if trapped & ZSIG_IGNORED != 0 {
1486 return 0;
1487 }
1488 // Look up a fallback raw-text body installed by `bin_trap`
1489 // (`trap '...' SIG` form). bin_trap stores the body in the
1490 // canonical `traps_table` HashMap<String, String> but never
1491 // calls settrap, so `sigtrapped[sig]` may be 0 here even when
1492 // there IS a live trap. Treat presence in traps_table as
1493 // equivalent to ZSIG_TRAPPED for the dispatch decision and
1494 // dispatch via the crate::ported::exec::execute_script fn-ptr installed
1495 // by fusevm_bridge.
1496 let signame_for_lookup = getsigname(sig);
1497 let table_body: Option<String> = {
1498 let aliases: &[&str] = match sig {
1499 x if x == SIGZERR => &["ZERR", "ERR"],
1500 x if x == SIGDEBUG => &["DEBUG"],
1501 x if x == SIGEXIT => &["EXIT"],
1502 _ => &[],
1503 };
1504 let mut found = None;
1505 if let Ok(t) = crate::ported::builtin::traps_table().lock() {
1506 if let Some(b) = t.get(&signame_for_lookup) {
1507 found = Some(b.clone());
1508 } else {
1509 for alias in aliases {
1510 if let Some(b) = t.get(*alias) {
1511 found = Some(b.clone());
1512 break;
1513 }
1514 }
1515 }
1516 }
1517 found
1518 };
1519 if trapped & (ZSIG_TRAPPED | ZSIG_FUNC) == 0 && table_body.is_none() {
1520 return 0;
1521 }
1522 if errflag.load(Ordering::Relaxed) != 0 {
1523 return 0;
1524 }
1525 // c:Src/signals.c:1244 — `if (intrap) return;` — re-entry guard.
1526 // Without this, a TRAP body that itself fails (e.g. `trap "false"
1527 // ERR; false`) re-enters dotrap on its own failed status and
1528 // recurses indefinitely → stack overflow.
1529 if intrap.load(Ordering::Relaxed) != 0 {
1530 return 0;
1531 }
1532
1533 intrap.store(1, Ordering::SeqCst);
1534 // c:1270 — `dont_queue_signals()`. C disables signal queueing for
1535 // the duration of the trap dispatch so signals delivered while
1536 // the trap is running run inline (not queued for later).
1537 crate::ported::signals_h::dont_queue_signals(); // c:1270
1538
1539 // c:1272-1273 — `if (sig == SIGEXIT) ++in_exit_trap;` (counter,
1540 // not boolean — depth tracking lets observers detect re-entry).
1541 if sig == SIGEXIT {
1542 in_exit_trap.fetch_add(1, Ordering::SeqCst); // c:1273
1543 }
1544
1545 // c:1251 — `if (sigtrapped[sig] & ZSIG_FUNC)` → run TRAPxxx shfunc.
1546 // c:Src/signals.c:1251-1259 — the C source dispatches ONE of
1547 // the two arms based on sigtrapped flags: ZSIG_FUNC → call
1548 // shfunc, else → run siglists eprog. The C settrap → unsettrap
1549 // chain ensures only one form is active at a time. zshrs's
1550 // port stores string-form bodies separately in `traps_table`
1551 // (not touched by removetrap), so both can coexist. Track
1552 // whether the function-form fired so the string-form fallback
1553 // below doesn't double-dispatch. Bug #541 in docs/BUGS.md.
1554 let mut fn_dispatched = false;
1555 if trapped & ZSIG_FUNC != 0 {
1556 let signame = getsigname(sig);
1557 let trap_fn = format!("TRAP{}", signame);
1558 if getshfunc(&trap_fn).is_some() {
1559 // c:1252-1255 — `dotrapargs(sig, sigtrapped+sig, funcprog)`.
1560 // Drives the shfunc with `$1 = sig`. With the
1561 // executor not directly callable from this
1562 // signal-handler context, route through the
1563 // canonical `crate::exec::doshfunc` entry which
1564 // handles the arg+env+local-scope wrap.
1565 let args = vec![sig.to_string()];
1566 let _ = crate::ported::exec::dispatch_function_call(&trap_fn, &args);
1567 fn_dispatched = true;
1568 }
1569 }
1570 // Additional function-form check: even when ZSIG_FUNC isn't
1571 // set on sigtrapped (the function was defined after the
1572 // string-form trap, and zshrs's exec.rs:6292 didn't update
1573 // sigtrapped via the doshfunc path), look in shfunctab
1574 // directly. zsh's last-defined-wins semantics treat the
1575 // function as the active handler — the older string-form
1576 // entry in traps_table must not also fire.
1577 if !fn_dispatched {
1578 let signame = getsigname(sig);
1579 let trap_fn = format!("TRAP{}", signame);
1580 if getshfunc(&trap_fn).is_some() {
1581 let args = vec![sig.to_string()];
1582 let _ = crate::ported::exec::dispatch_function_call(&trap_fn, &args);
1583 fn_dispatched = true;
1584 }
1585 }
1586 // c:1268 — non-FUNC `siglists[sig]` eprog branch. The canonical
1587 // settrap→siglists path isn't fully wired (bin_trap stores raw
1588 // body text into `traps_table` rather than parsing to Eprog and
1589 // calling settrap). Dispatch via the crate::ported::exec::execute_script
1590 // fn-ptr installed by fusevm_bridge — no direct ShellExecutor
1591 // reach-in from src/ported/ (see memory
1592 // feedback_no_exec_script_from_ported).
1593 // Skip the string-form fallback when a function-form already
1594 // fired — zsh semantics are last-defined-replaces, not
1595 // both-fire. Bug #541 in docs/BUGS.md.
1596 if let Some(body) = table_body.filter(|_| !fn_dispatched) {
1597 // c:Bug #56 — when a trap fires DURING `$(...)` capture
1598 // (cmdsub redirected fd 1 to a pipe), the trap body's
1599 // stdout would land in the captured value. zsh forks each
1600 // cmdsub so traps run in the parent process whose fd 1 is
1601 // the terminal. zshrs's in-process cmdsub publishes the
1602 // saved outer stdout via CMDSUBST_OUTER_FDS so the trap
1603 // dispatcher can route body output to the parent's real
1604 // stdout instead. Temporarily restore fd 1 to that saved
1605 // outer stdout around the body, then revert to the
1606 // cmdsub-bound fd. Same idea as bash's command-subst trap
1607 // routing (Functions/Misc/runtraps).
1608 let outer = crate::ported::exec::cmdsubst_outer_stdout();
1609 let saved_inner = if outer.is_some() {
1610 unsafe { libc::dup(libc::STDOUT_FILENO) }
1611 } else {
1612 -1
1613 };
1614 if let Some(out_fd) = outer {
1615 unsafe {
1616 libc::dup2(out_fd, libc::STDOUT_FILENO);
1617 }
1618 }
1619 let _ = crate::ported::exec::execute_script(&body);
1620 if let Some(_) = outer {
1621 if saved_inner >= 0 {
1622 unsafe {
1623 libc::dup2(saved_inner, libc::STDOUT_FILENO);
1624 libc::close(saved_inner);
1625 }
1626 }
1627 }
1628 }
1629
1630 // c:1277 — `if (sig == SIGEXIT) --in_exit_trap;` (decrement, not
1631 // store-0). The previous Rust port used `store(0)` which would
1632 // mask a re-entered trap — a TRAP_EXIT inside another TRAP_EXIT
1633 // would clear the flag prematurely.
1634 if sig == SIGEXIT {
1635 in_exit_trap.fetch_sub(1, Ordering::SeqCst); // c:1277
1636 }
1637 // c:1280 — `restore_queue_signals(q)` — restore to the level
1638 // captured at entry (c:1248). Now properly captured above; the
1639 // previous tail was a hardcoded `intrap.store(0)` only.
1640 crate::ported::signals_h::restore_queue_signals(q); // c:1280
1641 intrap.store(0, Ordering::SeqCst);
1642 0
1643}
1644
1645/// Direct port of `void dotrapargs(int sig, int *sigtr, void *sigfn)` from
1646/// `Src/signals.c:1081`. Drives a single trap callback for `sig`:
1647/// suspends `breaks`/`retflag`/`lastval` so the body runs in a fresh
1648/// control-flow scope, dispatches the function (ZSIG_FUNC) or eprog
1649/// (non-FUNC) body, then restores the caller's flags applying the
1650/// trap_state / trap_return / try_tryflag rules.
1651///
1652/// ```c
1653/// void
1654/// dotrapargs(int sig, int *sigtr, void *sigfn)
1655/// {
1656/// LinkList args;
1657/// char *name, num[4];
1658/// int obreaks = breaks;
1659/// int oretflag = retflag;
1660/// int olastval = lastval;
1661/// int isfunc;
1662/// int traperr, new_trap_state, new_trap_return;
1663/// if ((*sigtr & ZSIG_IGNORED) || !sigfn || errflag) return;
1664/// if (intrap) {
1665/// switch (sig) { case SIGEXIT: case SIGDEBUG: case SIGZERR: return; }
1666/// }
1667/// queue_signals();
1668/// intrap++;
1669/// *sigtr |= ZSIG_IGNORED;
1670/// zcontext_save();
1671/// execsave();
1672/// breaks = retflag = 0;
1673/// traplocallevel = locallevel;
1674/// runhookdef(BEFORETRAPHOOK, NULL);
1675/// if (*sigtr & ZSIG_FUNC) {
1676/// /* ... build args, doshfunc(...) ... */
1677/// } else {
1678/// trap_return = -2;
1679/// trap_state = TRAP_STATE_PRIMED;
1680/// trapisfunc = isfunc = 0;
1681/// execode((Eprog)sigfn, 1, 0, "trap");
1682/// }
1683/// runhookdef(AFTERTRAPHOOK, NULL);
1684/// traperr = errflag;
1685/// new_trap_state = trap_state;
1686/// new_trap_return = trap_return;
1687/// execrestore();
1688/// zcontext_restore();
1689/// /* ... restore breaks/retflag/lastval per FORCE_RETURN / traperr ... */
1690/// if (zleactive && resetneeded) zleentry(ZLE_CMD_REFRESH);
1691/// if (*sigtr != ZSIG_IGNORED) *sigtr &= ~ZSIG_IGNORED;
1692/// intrap--;
1693/// unqueue_signals();
1694/// }
1695/// ```
1696#[allow(clippy::too_many_arguments)]
1697pub fn dotrapargs(sig: i32, sigtr: &mut i32, sigfn: Option<&str>) {
1698 // c:1081
1699
1700 let obreaks: i32 = BREAKS.load(Ordering::SeqCst); // c:1085
1701 let oretflag: i32 = RETFLAG.load(Ordering::SeqCst); // c:1086
1702 let olastval: i32 = LASTVAL.load(Ordering::SeqCst); // c:1087
1703 let isfunc: i32; // c:1088
1704 let traperr: i32; // c:1089
1705 let new_trap_state: i32; // c:1089
1706 let new_trap_return: i32; // c:1089
1707
1708 // c:1101 — `if ((*sigtr & ZSIG_IGNORED) || !sigfn || errflag) return;`
1709 if (*sigtr & ZSIG_IGNORED) != 0 // c:1101
1710 || sigfn.is_none()
1711 || errflag.load(Ordering::SeqCst) != 0
1712 {
1713 return; // c:1102
1714 }
1715
1716 // c:1112-1119 — disallow synchronous traps from nesting.
1717 if intrap.load(Ordering::SeqCst) != 0 {
1718 // c:1112
1719 if sig == SIGEXIT || sig == SIGDEBUG || sig == SIGZERR {
1720 // c:1113-1117
1721 return; // c:1117
1722 }
1723 }
1724
1725 queue_signals(); // c:1121
1726
1727 intrap.fetch_add(1, Ordering::SeqCst); // c:1123
1728 *sigtr |= ZSIG_IGNORED; // c:1124
1729 // Mirror into the sigtrapped slab so observers (handletrap, dotrap
1730 // re-entry) see the same ZSIG_IGNORED bit.
1731 if let Ok(mut g) = sigtrapped.lock() {
1732 if let Some(slot) = g.get_mut(sig as usize) {
1733 *slot |= ZSIG_IGNORED;
1734 }
1735 }
1736
1737 zcontext_save(); // c:1126
1738 // c:1128 — `execsave()` saves trap_return/trap_state. Without a
1739 // canonical `execsave` port yet, snapshot the two atomics inline.
1740 let saved_trap_state = TRAP_STATE.load(Ordering::SeqCst); // c:1128 execsave
1741 let saved_trap_return = TRAP_RETURN.load(Ordering::SeqCst); // c:1128 execsave
1742 BREAKS.store(0, Ordering::SeqCst); // c:1129 breaks = 0
1743 RETFLAG.store(0, Ordering::SeqCst); // c:1129 retflag = 0
1744 traplocallevel.store(
1745 crate::ported::params::locallevel.load(Ordering::SeqCst),
1746 Ordering::SeqCst,
1747 ); // c:1130
1748
1749 // c:1131 — `runhookdef(BEFORETRAPHOOK, NULL);` — fire any
1750 // registered "before-trap" module hooks. Looked up by name
1751 // through gethookdef so the module dispatcher picks up
1752 // installed handlers (zsh/zle's zlebeforetrap etc.).
1753 let hd = crate::ported::module::gethookdef("BEFORETRAPHOOK");
1754 if !hd.is_null() {
1755 let _ = crate::ported::module::runhookdef(hd, std::ptr::null_mut());
1756 }
1757 let _ = BEFORETRAPHOOK; // c:1131 — const retained for source-cite parity
1758
1759 if (*sigtr & ZSIG_FUNC) != 0 {
1760 // c:1132
1761 let osc = SFCONTEXT.load(Ordering::SeqCst); // c:1133 osc
1762 // c:1133 — `int old_incompfunc = incompfunc;` — snapshot the
1763 // completion-function-active flag so the trap dispatch can
1764 // run code outside the comp-fn scope and restore on return.
1765 let old_incompfunc: i32 = crate::ported::zle::complete::INCOMPFUNC.load(Ordering::Relaxed);
1766 let hn = gettrapnode(sig, false); // c:1134
1767
1768 let mut args: Vec<String> = Vec::new(); // c:1136 znewlinklist
1769 // c:1144-1149 — pick the right TRAPxxx name from the function table
1770 // (multi-named aliases) or build the canonical TRAP<SIGNAME>.
1771 let name = match hn {
1772 Some(n) => ztrdup(&n), // c:1145 ztrdup(hn->nam)
1773 None => {
1774 // c:1146
1775 format!("TRAP{}", getsigname(sig)) // c:1147-1148
1776 }
1777 };
1778 args.push(name.clone()); // c:1150 zaddlinknode(args, name)
1779 let num = format!("{}", sig); // c:1151 sprintf(num, "%d", sig)
1780 args.push(num); // c:1152
1781
1782 TRAP_RETURN.store(-1, Ordering::SeqCst); // c:1154 trap_return = -1; /* incremented by doshfunc */
1783 TRAP_STATE.store(TRAP_STATE_PRIMED, Ordering::SeqCst); // c:1155
1784 trapisfunc.store(1, Ordering::SeqCst); // c:1156
1785 isfunc = 1;
1786
1787 SFCONTEXT.store(SFC_SIGNAL, Ordering::SeqCst); // c:1158
1788 // c:1159 — `incompfunc = 0;` — clear the active-compfn flag
1789 // so user-level trap handlers can run normal `complete` /
1790 // `compadd` etc. without being mis-detected as inside a
1791 // completion widget.
1792 crate::ported::zle::complete::INCOMPFUNC.store(0, Ordering::Relaxed);
1793 // c:1160 — `doshfunc((Shfunc)sigfn, args, 1);`. Direct
1794 // doshfunc call mirrors C — argv[0] = TRAP name, argv[1..]
1795 // = signum etc. body_runner routes through the host's
1796 // body-only entry so we don't double-wrap the scope.
1797 let fn_name = sigfn.unwrap_or("").to_string();
1798 let shf_clone: Option<crate::ported::zsh_h::shfunc> = {
1799 let tab = crate::ported::hashtable::shfunctab_lock().read();
1800 tab.ok().and_then(|t| t.get(&fn_name).cloned())
1801 };
1802 if let Some(mut shf) = shf_clone {
1803 let body_args = args.clone();
1804 let name_for_body = fn_name.clone();
1805 let body_runner = move || -> i32 {
1806 crate::ported::exec::run_function_body(&name_for_body, &body_args[1..])
1807 .unwrap_or(0)
1808 };
1809 let _ = crate::ported::exec::doshfunc(&mut shf, args.clone(), true, body_runner);
1810 }
1811 SFCONTEXT.store(osc, Ordering::SeqCst); // c:1161
1812 // c:1162 — `incompfunc = old_incompfunc;` — restore the
1813 // completion-function-active flag we snapshotted at c:1133.
1814 crate::ported::zle::complete::INCOMPFUNC.store(old_incompfunc, Ordering::Relaxed);
1815 let _ = args; // c:1163 freelinklist(args)
1816 zsfree(name); // c:1164 zsfree(name)
1817 } else {
1818 // c:1165
1819 TRAP_RETURN.store(-2, Ordering::SeqCst); // c:1166 trap_return = -2
1820 TRAP_STATE.store(TRAP_STATE_PRIMED, Ordering::SeqCst); // c:1167
1821 trapisfunc.store(0, Ordering::SeqCst); // c:1168
1822 isfunc = 0;
1823 // c:1170 — `execode((Eprog)sigfn, 1, 0, "trap");` — execute
1824 // the trap's compiled Eprog body. zshrs models the non-
1825 // ZSIG_FUNC trap action as source TEXT (sigfn: Option<&str>)
1826 // rather than a pre-compiled Eprog; re-parse + execute via
1827 // the fusevm pipeline so the trap action actually fires.
1828 // When sigfns[] is re-shaped to carry compiled Eprogs (matching
1829 // C's `void *sigfns[sig]` cast at c:1170), swap in
1830 // `crate::ported::exec::execode(eprog, 1, 0, "trap")` directly.
1831 if let Some(src) = sigfn {
1832 let _ = crate::ported::exec::execute_script_zsh_pipeline(src);
1833 }
1834 }
1835
1836 // c:1172 — `runhookdef(AFTERTRAPHOOK, NULL);` — fire any registered
1837 // "after-trap" module hooks. Same shape as BEFORETRAPHOOK above.
1838 let hd = crate::ported::module::gethookdef("AFTERTRAPHOOK");
1839 if !hd.is_null() {
1840 let _ = crate::ported::module::runhookdef(hd, std::ptr::null_mut());
1841 }
1842 let _ = AFTERTRAPHOOK; // c:1172 — const retained for source-cite parity
1843
1844 traperr = errflag.load(Ordering::SeqCst); // c:1174
1845
1846 new_trap_state = TRAP_STATE.load(Ordering::SeqCst); // c:1177
1847 new_trap_return = TRAP_RETURN.load(Ordering::SeqCst); // c:1178
1848
1849 // c:1180 — `execrestore()` restores trap_return/trap_state.
1850 TRAP_STATE.store(saved_trap_state, Ordering::SeqCst); // c:1180
1851 TRAP_RETURN.store(saved_trap_return, Ordering::SeqCst); // c:1180
1852 zcontext_restore(); // c:1181
1853
1854 if new_trap_state == TRAP_STATE_FORCE_RETURN // c:1183
1855 && !(isfunc != 0 && new_trap_return == 0)
1856 // c:1184
1857 {
1858 if isfunc != 0 {
1859 // c:1186
1860 BREAKS.store(LOOPS.load(Ordering::SeqCst), Ordering::SeqCst); // c:1187 breaks = loops
1861 if sig == libc::SIGINT || sig == libc::SIGQUIT {
1862 // c:1196
1863 errflag.fetch_or(
1864 // c:1197 errflag |= ERRFLAG_INT
1865 ERRFLAG_INT,
1866 Ordering::SeqCst,
1867 );
1868 } else {
1869 // c:1198
1870 errflag.fetch_or(
1871 // c:1199 errflag |= ERRFLAG_ERROR
1872 ERRFLAG_ERROR,
1873 Ordering::SeqCst,
1874 );
1875 }
1876 }
1877 LASTVAL.store(new_trap_return, Ordering::SeqCst); // c:1202
1878 RETFLAG.store(1, Ordering::SeqCst); // c:1204 retflag = 1
1879 } else {
1880 // c:1205
1881 if traperr != 0 && !EMULATION(EMULATE_SH) {
1882 // c:1206
1883 LASTVAL.store(1, Ordering::SeqCst); // c:1207
1884 } else {
1885 // c:1208
1886 // c:1210 — keep pre-trap lastval.
1887 LASTVAL.store(olastval, Ordering::SeqCst); // c:1213
1888 }
1889 if try_tryflag.load(Ordering::SeqCst) != 0 {
1890 // c:1215 try_tryflag
1891 if traperr != 0 {
1892 // c:1216
1893 errflag.fetch_or(
1894 // c:1217
1895 ERRFLAG_ERROR,
1896 Ordering::SeqCst,
1897 );
1898 } else {
1899 // c:1218
1900 errflag.fetch_and(
1901 // c:1219 errflag &= ~ERRFLAG_ERROR
1902 !ERRFLAG_ERROR,
1903 Ordering::SeqCst,
1904 );
1905 }
1906 }
1907 BREAKS.fetch_add(obreaks, Ordering::SeqCst); // c:1220 breaks += obreaks
1908 RETFLAG.store(oretflag, Ordering::SeqCst); // c:1222 retflag = oretflag
1909 let cur_breaks = BREAKS.load(Ordering::SeqCst);
1910 let cur_loops = LOOPS.load(Ordering::SeqCst);
1911 if cur_breaks > cur_loops {
1912 // c:1223
1913 BREAKS.store(cur_loops, Ordering::SeqCst); // c:1224
1914 }
1915 }
1916
1917 // c:1231 — `if (zleactive && resetneeded) zleentry(ZLE_CMD_REFRESH);`
1918 if zleactive.load(Ordering::SeqCst) != 0 && RESETNEEDED.load(Ordering::SeqCst) != 0 {
1919 let _ = zleentry(ZLE_CMD_REFRESH);
1920 }
1921
1922 if *sigtr != ZSIG_IGNORED {
1923 // c:1234
1924 *sigtr &= !ZSIG_IGNORED; // c:1235
1925 if let Ok(mut g) = sigtrapped.lock() {
1926 if let Some(slot) = g.get_mut(sig as usize) {
1927 *slot &= !ZSIG_IGNORED;
1928 }
1929 }
1930 }
1931 intrap.fetch_sub(1, Ordering::SeqCst); // c:1236
1932
1933 unqueue_signals(); // c:1238
1934}
1935
1936// `try_tryflag` lives at `Src/loop.c:731` (the always/try block depth
1937// counter); ported to `crate::ported::r#loop::try_tryflag`. dotrapargs
1938// above reads it from its canonical home per PORT.md Rule C (header /
1939// file placement).
1940
1941/// Resolve a real-time signal name to its number.
1942/// Port of `int rtsigno(const char* signame)` from `Src/signals.c:1291-1313`.
1943///
1944/// **C signature**: `int rtsigno(const char* signame)` — takes a
1945/// NAME STRING ("RTMIN", "RTMIN+3", "RTMAX-1", etc.) and returns
1946/// the signal number, or 0 on parse failure.
1947///
1948/// Rust signature: `(signame: &str) -> Option<i32>` — `None`
1949/// matches C's `0` sentinel. Uses `libc::SIGRTMIN()` /
1950/// `libc::SIGRTMAX()` for canonical bounds.
1951pub fn rtsigno(signame: &str) -> Option<i32> {
1952 // c:1291
1953 #[cfg(target_os = "linux")]
1954 {
1955 let sigrtmin = libc::SIGRTMIN();
1956 let sigrtmax = libc::SIGRTMAX();
1957 let maxofs = sigrtmax - sigrtmin; // c:1296
1958
1959 // c:1298-1306 — `if (!strncmp(signame, "RTMIN", 5)) ...
1960 // else if (!strncmp(signame, "RTMAX", 5)) ... else return 0;`
1961 let (sig, dir, op): (i32, i32, char) = if let Some(rest) = signame.strip_prefix("RTMIN") {
1962 (sigrtmin, 1, '+') // c:1300
1963 } else if let Some(rest) = signame.strip_prefix("RTMAX") {
1964 (sigrtmax, -1, '-') // c:1302
1965 } else {
1966 return None; // c:1304 return 0
1967 };
1968
1969 // c:1307-1311 — `if (signame[5] == x.op) { offset = strtol(...);
1970 // if (offset > maxofs) return 0;
1971 // x.sig += offset * x.dir; }`
1972 let rest = if signame.starts_with("RTMIN") {
1973 &signame[5..]
1974 } else {
1975 &signame[5..]
1976 };
1977 let mut final_sig = sig;
1978 if !rest.is_empty() {
1979 if rest.starts_with(op) {
1980 let num_str = &rest[1..];
1981 let offset: i32 = match num_str.parse() {
1982 Ok(n) => n,
1983 Err(_) => return None, // c:1312
1984 };
1985 if offset > maxofs {
1986 return None; // c:1310 return 0
1987 }
1988 final_sig += offset * dir;
1989 } else {
1990 // c:1313 — `if (*end) return 0;` — any non-op trailing → fail.
1991 return None;
1992 }
1993 }
1994 Some(final_sig)
1995 }
1996 #[cfg(not(target_os = "linux"))]
1997 {
1998 let _ = signame;
1999 None
2000 }
2001}
2002
2003/// Resolve a real-time signal number to its `RTMIN+N` / `RTMAX-N` name.
2004/// Port of `char *rtsigname(int signo, int alt)` from `Src/signals.c:1317`.
2005///
2006/// C body picks the SHORTER form between `RTMIN+N` and `RTMAX-N`,
2007/// preferring the smaller offset unless `alt` is set (which flips
2008/// the choice via XOR). `signo` outside `[SIGRTMIN..=SIGRTMAX]`
2009/// returns NULL — Rust returns empty string for the equivalent.
2010///
2011/// The previous Rust port:
2012/// 1. Dropped the `alt` argument entirely (callers got the
2013/// `alt=0` default, but no way to flip).
2014/// 2. ALWAYS produced the `RTMIN+N` form, ignoring the C "shorter
2015/// form wins" contract — for high-numbered RT signals where
2016/// RTMAX-N is the shorter form, C would emit `RTMAX-N` but
2017/// Rust emitted the longer `RTMIN+N`.
2018/// 3. Used a hardcoded `sigrtmin = 34` constant instead of
2019/// `libc::SIGRTMIN()` (real-time signal numbers can vary by
2020/// libc version/build).
2021/// 4. Out-of-range input produced `SIG{n}` — C returns NULL.
2022///
2023/// **Signature divergence from C**: C takes `(signo, alt)`; Rust port
2024/// takes `(sig)` with `alt=0` implicit because the only in-tree caller
2025/// (`params.rs:1640`) doesn't need the alt flip. A future caller that
2026/// needs alt-form can be added then.
2027/// WARNING: param names don't match C — Rust=(sig) vs C=(signo, alt)
2028pub fn rtsigname(sig: i32) -> String {
2029 // c:1317
2030 #[cfg(target_os = "linux")]
2031 {
2032 let sigrtmin = libc::SIGRTMIN();
2033 let sigrtmax = libc::SIGRTMAX();
2034 // c:1325-1326 — `if (signo < SIGRTMIN || signo > SIGRTMAX) return NULL;`
2035 if sig < sigrtmin || sig > sigrtmax {
2036 return String::new();
2037 }
2038 // c:1319-1323 — `int minofs = signo - SIGRTMIN; int maxofs =
2039 // SIGRTMAX - signo; int form = alt ^ (maxofs < minofs);`
2040 // With alt=0 always, form simplifies to `maxofs < minofs`.
2041 let minofs = sig - sigrtmin;
2042 let maxofs = sigrtmax - sig;
2043 let form = maxofs < minofs;
2044 // c:1328-1334 — pick `RTMIN+` or `RTMAX-` per `form`.
2045 let prefix = if form { "RTMAX-" } else { "RTMIN+" };
2046 let offset = if form { maxofs } else { minofs };
2047 if offset == 0 {
2048 // c:1334 — buf[5] = '\0' → drop the trailing sign char.
2049 prefix[..5].to_string()
2050 } else {
2051 format!("{}{}", prefix, offset)
2052 }
2053 }
2054 #[cfg(not(target_os = "linux"))]
2055 {
2056 let _ = sig;
2057 String::new()
2058 }
2059}
2060
2061// ---------------------------------------------------------------------------
2062// Signal-queue state. Direct ports of `Src/signals.c:77-92`:
2063//
2064// mod_export volatile int queueing_enabled, queue_front, queue_rear; // c:77
2065// mod_export int signal_queue[MAX_QUEUE_SIZE]; // c:79
2066// mod_export sigset_t signal_mask_queue[MAX_QUEUE_SIZE]; // c:81
2067// static volatile int trap_queueing_enabled,
2068// trap_queue_front, trap_queue_rear; // c:90
2069// static int trap_queue[MAX_QUEUE_SIZE]; // c:92
2070//
2071// C uses flat module-level variables; Rust mirrors with file-scope
2072// `AtomicI32` + `LazyLock<Mutex<Vec<...>>>` slabs so concurrent
2073// pushes from the async signal handler synchronize without UB.
2074// ---------------------------------------------------------------------------
2075
2076/// Signal-queue depth counter. Port of `mod_export volatile int
2077/// queueing_enabled` from `Src/signals.c:77`.
2078pub static queueing_enabled: AtomicI32 = AtomicI32::new(0); // c:77
2079
2080/// Ring-buffer head. Port of `mod_export volatile int queue_front`
2081/// from `Src/signals.c:77`.
2082pub static queue_front: AtomicUsize = AtomicUsize::new(0); // c:77
2083
2084/// Ring-buffer tail. Port of `mod_export volatile int queue_rear`
2085/// from `Src/signals.c:77`.
2086pub static queue_rear: AtomicUsize = AtomicUsize::new(0); // c:77
2087
2088/// Port of `mod_export volatile int queue_in` from `Src/signals.c:84`.
2089/// Companion counter bumped by `queue_signals()` (signals.h:90) and
2090/// decremented by `unqueue_signals()` (signals.h:94); used by
2091/// `dont_queue_signals()` to snapshot the depth (signals.h:99) and
2092/// by debug assertions (DPUTS2 at signals.h:105).
2093pub static queue_in: AtomicI32 = AtomicI32::new(0); // c:84
2094
2095#[allow(clippy::declare_interior_mutable_const)]
2096const ATOM_I32_ZERO: AtomicI32 = AtomicI32::new(0);
2097
2098/// Per-slot signal numbers. Port of `mod_export int
2099/// signal_queue[MAX_QUEUE_SIZE]` from `Src/signals.c:79`.
2100pub static signal_queue: [AtomicI32; MAX_QUEUE_SIZE] = // c:79
2101 [ATOM_I32_ZERO; MAX_QUEUE_SIZE];
2102
2103/// Per-slot blocked-mask snapshots. Port of `mod_export sigset_t
2104/// signal_mask_queue[MAX_QUEUE_SIZE]` from `Src/signals.c:81`.
2105/// `sigset_t` isn't Copy on every platform — wrapped in a Mutex
2106/// so the slabs initialize without const-eval gymnastics.
2107pub static signal_mask_queue: std::sync::LazyLock<Mutex<Vec<libc::sigset_t>>> = // c:81
2108 std::sync::LazyLock::new(|| {
2109 let zero: libc::sigset_t = unsafe { std::mem::zeroed() };
2110 Mutex::new(vec![zero; MAX_QUEUE_SIZE])
2111 });
2112
2113/// Trap-queue depth counter. Port of `static volatile int
2114/// trap_queueing_enabled` from `Src/signals.c:90`.
2115pub static trap_queueing_enabled: AtomicI32 = AtomicI32::new(0); // c:90
2116
2117/// Trap-queue head. Port of `static volatile int trap_queue_front`
2118/// from `Src/signals.c:90`.
2119pub static trap_queue_front: AtomicUsize = AtomicUsize::new(0); // c:90
2120
2121/// Trap-queue tail. Port of `static volatile int trap_queue_rear`
2122/// from `Src/signals.c:90`.
2123pub static trap_queue_rear: AtomicUsize = AtomicUsize::new(0); // c:90
2124
2125/// Port of `int last_signal` from `Src/signals.c:238`. Holds the
2126/// signal number of the most recent delivery; used by `wait_cmd`
2127/// in jobs.c to set `$?` to `128 + last_signal` when a trapped
2128/// signal interrupts wait.
2129pub static last_signal: AtomicI32 = AtomicI32::new(0); // c:238
2130
2131// ---------------------------------------------------------------------------
2132// Per-signal trap state. Direct ports of the C globals declared in
2133// `Src/signals.c:39/53/58`:
2134//
2135// mod_export int *sigtrapped; // c:39 — flag word per sig
2136// mod_export Eprog *siglists; // c:53 — Eprog per sig (trap body)
2137// mod_export volatile int nsigtrapped; // c:58 — trapped-signal count
2138//
2139// C allocates parallel arrays of length TRAPCOUNT at init time
2140// (`Src/init.c:1398`). Rust mirrors with `Mutex<Vec<...>>` slabs
2141// sized to TRAPCOUNT plus an atomic counter. TRAPxxx-function
2142// trap bodies are NOT stored here in C either — `dotrap` looks
2143// them up via `gettrapnode()` from shfunctab on signal delivery
2144// (`Src/jobs.c:gettrapnode`).
2145// ---------------------------------------------------------------------------
2146
2147/// Per-signal flag word. Port of `mod_export int *sigtrapped`
2148/// from `Src/signals.c:39`. Bit values are `ZSIG_TRAPPED`,
2149/// `ZSIG_IGNORED`, `ZSIG_FUNC`, plus `(locallevel << ZSIG_SHIFT)`
2150/// in the high bits.
2151pub static sigtrapped: std::sync::LazyLock<Mutex<Vec<i32>>> = // c:39
2152 std::sync::LazyLock::new(|| Mutex::new(vec![0; TRAPCOUNT as usize]));
2153
2154/// Per-signal Eprog body. Port of `mod_export Eprog *siglists`
2155/// from `Src/signals.c:53`. NULL for ZSIG_FUNC entries (function
2156/// body resolves through `gettrapnode` at dispatch time).
2157pub static siglists: std::sync::LazyLock<Mutex<Vec<Option<Eprog>>>> =
2158 // c:53
2159 std::sync::LazyLock::new(|| Mutex::new((0..TRAPCOUNT as usize).map(|_| None).collect()));
2160
2161/// Count of `ZSIG_TRAPPED`-flagged signals. Port of
2162/// `mod_export volatile int nsigtrapped` from `Src/signals.c:58`.
2163pub static nsigtrapped: AtomicI32 = AtomicI32::new(0); // c:58
2164
2165/// File-scope `int intrap` from `Src/signals.c`. Set while a
2166/// trap body is running so nested `dotrap` calls short-circuit
2167/// (matches the c:1245 dispatcher's `if (intrap) return`).
2168pub static intrap: AtomicI32 = AtomicI32::new(0); // c:intrap
2169
2170/// File-scope `int in_exit_trap` from `Src/signals.c:60`. Set
2171/// while the EXIT trap body is running so `exit` and friends can
2172/// distinguish "real" exit from exit-trap-driven exit.
2173pub static in_exit_trap: AtomicI32 = AtomicI32::new(0); // c:60
2174
2175/// Port of `volatile int trapisfunc` from `Src/signals.c:1062`.
2176/// Set by `dotrapargs()` (signals.c:1156) when the trap body is a
2177/// shell function (vs. inline command) — the `IN_EVAL_TRAP()` macro
2178/// at zsh.h:2962 tests this against `intrap` + `locallevel`.
2179pub static trapisfunc: AtomicI32 = AtomicI32::new(0); // c:1062
2180
2181/// Port of `volatile int traplocallevel` from `Src/signals.c:1069`.
2182/// Captures `locallevel` at trap-entry so the trap body can detect
2183/// whether it's running inside the same scope it was registered in
2184/// (the third leg of `IN_EVAL_TRAP()` at zsh.h:2962).
2185pub static traplocallevel: AtomicI32 = AtomicI32::new(0); // c:1069
2186
2187/// File-scope `LinkList savetraps` from `Src/signals.c`. Stack of
2188/// saved trap entries — pushed by `dosavetrap`, popped by
2189/// `endtrapscope`. Inserts at front so it works as a LIFO stack.
2190pub static SAVETRAPS: OnceLock<Mutex<Vec<savetrap>>> = OnceLock::new();
2191
2192/// File-scope `int exit_trap_posix` from `Src/signals.c`. POSIX-mode
2193/// EXIT trap flag — when set, exit traps survive function-scope
2194/// teardown instead of being unset.
2195pub static EXIT_TRAP_POSIX: AtomicBool = AtomicBool::new(false);
2196
2197/// File-scope `int dontsavetrap` from `Src/signals.c`. Counter
2198/// suppressing `dosavetrap` calls during `settrap` invoked from
2199/// `endtrapscope`'s restore loop (so the restore itself doesn't
2200/// push fresh save entries).
2201pub static DONTSAVETRAP: AtomicI32 = AtomicI32::new(0);
2202
2203/// Port of `killpg()` libc passthrough — used by jobs.c / signals.c
2204/// callers; not in zsh source itself but referenced via libc.
2205pub fn killpg(pgrp: i32, sig: i32) -> i32 {
2206 unsafe { libc::killpg(pgrp, sig) }
2207}
2208
2209/// Port of `kill()` libc passthrough.
2210pub fn kill(pid: i32, sig: i32) -> i32 {
2211 unsafe { libc::kill(pid, sig) }
2212}
2213
2214// ---------------------------------------------------------------------------
2215// `interact` flag — mirrors C's global `interact` int (Src/init.c).
2216// Used by intr / holdintr / noholdintr / install_handler to gate
2217// SIGINT-related setup on interactive shell mode.
2218// ---------------------------------------------------------------------------
2219
2220fn interact_lock() -> &'static AtomicBool {
2221 static INTERACT: AtomicBool = AtomicBool::new(false);
2222 &INTERACT
2223}
2224
2225/// Setter for the `interact` flag. Called by init.rs once the
2226/// shell-mode dispatch determines whether stdin is a tty / `-i`
2227/// was passed.
2228///
2229/// Writes to the canonical INTERACTIVE option flag so the read
2230/// side (`is_interact` → `isset(INTERACTIVE)`) sees it.
2231pub fn set_interact(v: bool) {
2232 crate::ported::options::opt_state_set("interactive", v);
2233}
2234
2235/// Read the `interact` flag.
2236///
2237/// C: `#define interact (isset(INTERACTIVE))` (Src/zsh.h:2562) —
2238/// the canonical interact predicate reads the INTERACTIVE option
2239/// flag from the options table.
2240///
2241/// The previous Rust port read `interact_lock()` (a private
2242/// AtomicBool) whose only writer was `set_interact()` — and
2243/// `set_interact` was called ONLY from this file's tests. In
2244/// production, `interact_lock` stayed at the default `false`, so
2245/// every `if is_interact()` gate in `intr`/`nointr`/`holdintr`/
2246/// `noholdintr` short-circuited regardless of whether the shell
2247/// was actually interactive. `setopt INTERACTIVE` had no effect
2248/// on signal handling.
2249///
2250/// Route through canonical isset() so the option drives the predicate.
2251pub fn is_interact() -> bool {
2252 isset(INTERACTIVE)
2253}
2254
2255// ===========================================================
2256// Methods moved verbatim from src/ported/vm_helper because their
2257// C counterpart's source file maps 1:1 to this Rust module.
2258// Phase: drift
2259// ===========================================================
2260
2261// BEGIN moved-from-exec-rs
2262// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
2263
2264// END moved-from-exec-rs
2265
2266#[cfg(test)]
2267mod tests {
2268 use super::*;
2269 use crate::options::dosetopt;
2270 use crate::zsh_h::{HUP, MONITOR, TRAPSASYNC};
2271
2272 #[test]
2273 fn test_sig_by_name() {
2274 let _g = crate::test_util::global_state_lock();
2275 assert_eq!(getsigidx("INT"), Some(libc::SIGINT));
2276 assert_eq!(getsigidx("SIGINT"), Some(libc::SIGINT));
2277 assert_eq!(getsigidx("int"), Some(libc::SIGINT));
2278 assert_eq!(getsigidx("HUP"), Some(libc::SIGHUP));
2279 assert_eq!(getsigidx("TERM"), Some(libc::SIGTERM));
2280 assert_eq!(getsigidx("EXIT"), Some(SIGEXIT));
2281 assert_eq!(getsigidx("9"), Some(9));
2282 }
2283
2284 #[test]
2285 fn test_getsigname() {
2286 let _g = crate::test_util::global_state_lock();
2287 assert_eq!(getsigname(libc::SIGINT), "INT");
2288 assert_eq!(getsigname(libc::SIGHUP), "HUP");
2289 assert_eq!(getsigname(SIGEXIT), "EXIT");
2290 }
2291
2292 #[test]
2293 fn test_signal_queue() {
2294 let _g = crate::test_util::global_state_lock();
2295 let before = queueing_enabled.load(Ordering::SeqCst);
2296 queue_signals();
2297 assert_eq!(queueing_enabled.load(Ordering::SeqCst), before + 1);
2298 unqueue_signals();
2299 assert_eq!(queueing_enabled.load(Ordering::SeqCst), before);
2300 }
2301
2302 #[test]
2303 fn test_signal_mask_zero_returns_empty() {
2304 let _g = crate::test_util::global_state_lock();
2305 // C: `if (sig) sigaddset(&set, sig);` — sig==0 yields empty set.
2306 let s = signal_mask(0);
2307 let r = unsafe { libc::sigismember(&s, libc::SIGINT) };
2308 assert_eq!(r, 0);
2309 }
2310
2311 #[test]
2312 fn test_signal_mask_includes_only_specified() {
2313 let _g = crate::test_util::global_state_lock();
2314 let s = signal_mask(libc::SIGUSR1);
2315 assert_eq!(unsafe { libc::sigismember(&s, libc::SIGUSR1) }, 1);
2316 assert_eq!(unsafe { libc::sigismember(&s, libc::SIGUSR2) }, 0);
2317 }
2318
2319 #[test]
2320 fn test_interact_flag_round_trip() {
2321 let _g = crate::test_util::global_state_lock();
2322 let prev = is_interact();
2323 set_interact(true);
2324 assert!(is_interact());
2325 set_interact(false);
2326 assert!(!is_interact());
2327 set_interact(prev);
2328 }
2329
2330 #[test]
2331 fn test_signal_block_returns_old_mask() {
2332 let _g = crate::test_util::global_state_lock();
2333 let prev = is_interact();
2334 set_interact(false); // ensure no test side-effects from interactive paths
2335 let mask = signal_mask(libc::SIGUSR2);
2336 let old = signal_block(&mask);
2337 // Restore to old state.
2338 let _ = signal_setmask(&old);
2339 // Verify the post-block mask had SIGUSR2 set by re-blocking
2340 // and unblocking. The test just checks the returned old set
2341 // is valid (no crash, syscall returned).
2342 let _ = old;
2343 set_interact(prev);
2344 }
2345
2346 /// `Src/signals.c:158-168` — `signal_mask(sig)` builds a fresh
2347 /// sigset containing only `sig`. The `sig == 0` arm at c:163
2348 /// returns an empty set (no `sigaddset` call). Pin both arms.
2349 #[cfg(unix)]
2350 #[test]
2351 fn signal_mask_includes_only_requested_signal() {
2352 let _g = crate::test_util::global_state_lock();
2353 let m = signal_mask(libc::SIGUSR1);
2354 // c:166 — `sigaddset(&set, sig)` for the requested signal.
2355 assert_eq!(
2356 unsafe { libc::sigismember(&m, libc::SIGUSR1) },
2357 1,
2358 "c:166 — requested signal must be set"
2359 );
2360 // Other signals not in the set.
2361 assert_eq!(unsafe { libc::sigismember(&m, libc::SIGUSR2) }, 0);
2362 assert_eq!(unsafe { libc::sigismember(&m, libc::SIGTERM) }, 0);
2363 }
2364
2365 /// `Src/signals.c:163` — `if (sig) sigaddset(&set, sig)`. With
2366 /// `sig == 0` no `sigaddset` runs, so the returned set is empty.
2367 /// Regression dropping the `if (sig)` guard would `sigaddset(&set, 0)`
2368 /// which is implementation-defined (Linux: EINVAL; macOS: may
2369 /// "succeed" with a bogus member).
2370 #[cfg(unix)]
2371 #[test]
2372 fn signal_mask_with_zero_returns_empty_set() {
2373 let _g = crate::test_util::global_state_lock();
2374 let m = signal_mask(0);
2375 // Every signal must be NOT a member of an empty set.
2376 for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGUSR1, libc::SIGUSR2] {
2377 assert_eq!(
2378 unsafe { libc::sigismember(&m, sig) },
2379 0,
2380 "c:163 — sig=0 produces empty set, but {} found",
2381 sig
2382 );
2383 }
2384 }
2385
2386 /// `Src/signals.c:1291-1313` — `rtsigno(signame)` parses a NAME
2387 /// STRING ("RTMIN", "RTMIN+N", "RTMAX-N") and returns the signum,
2388 /// or 0 (Rust None) on parse failure.
2389 ///
2390 /// The previous Rust port had a completely wrong signature
2391 /// (`rtsigno(i32)` taking an offset int). Now matches C exactly.
2392 #[cfg(target_os = "linux")]
2393 #[test]
2394 fn rtsigno_parses_rt_signal_names() {
2395 let _g = crate::test_util::global_state_lock();
2396 let sigrtmin = libc::SIGRTMIN();
2397 let sigrtmax = libc::SIGRTMAX();
2398 // Bare RTMIN / RTMAX (no offset).
2399 assert_eq!(rtsigno("RTMIN"), Some(sigrtmin), "c:1300 — bare RTMIN");
2400 assert_eq!(rtsigno("RTMAX"), Some(sigrtmax), "c:1302 — bare RTMAX");
2401 // With offset.
2402 assert_eq!(
2403 rtsigno("RTMIN+1"),
2404 Some(sigrtmin + 1),
2405 "c:1307-1311 — RTMIN+N"
2406 );
2407 assert_eq!(
2408 rtsigno("RTMAX-1"),
2409 Some(sigrtmax - 1),
2410 "c:1307-1311 — RTMAX-N"
2411 );
2412 // Invalid input.
2413 assert_eq!(rtsigno("SIGINT"), None, "c:1304 — non-RT name returns None");
2414 assert_eq!(rtsigno(""), None, "empty string returns None");
2415 // Out-of-range offset.
2416 let maxofs = sigrtmax - sigrtmin;
2417 assert_eq!(
2418 rtsigno(&format!("RTMIN+{}", maxofs + 1)),
2419 None,
2420 "c:1310 — offset > maxofs returns None"
2421 );
2422 // Malformed (non-op trailing char).
2423 assert_eq!(
2424 rtsigno("RTMINx"),
2425 None,
2426 "c:1313 — trailing non-op char returns None"
2427 );
2428 }
2429
2430 /// `Src/signals.c:1317-1338` — `rtsigname(signo, alt)` picks the
2431 /// shorter form between `RTMIN+N` and `RTMAX-N`. Pin the contract
2432 /// on Linux where SIGRTMIN/SIGRTMAX are real.
2433 #[cfg(target_os = "linux")]
2434 #[test]
2435 fn rtsigname_picks_shorter_form_between_rtmin_rtmax() {
2436 let _g = crate::test_util::global_state_lock();
2437 let sigrtmin = libc::SIGRTMIN();
2438 let sigrtmax = libc::SIGRTMAX();
2439 // SIGRTMIN itself → "RTMIN" (offset 0; trailing '+' dropped).
2440 assert_eq!(
2441 rtsigname(sigrtmin),
2442 "RTMIN",
2443 "c:1334 — offset 0 → bare 'RTMIN' (no '+0')"
2444 );
2445 // SIGRTMAX itself → "RTMAX" (offset 0; trailing '-' dropped).
2446 assert_eq!(
2447 rtsigname(sigrtmax),
2448 "RTMAX",
2449 "c:1334 — offset 0 → bare 'RTMAX' (no '-0')"
2450 );
2451 // SIGRTMIN+1 — minofs=1, maxofs=(sigrtmax-sigrtmin-1) > 1 →
2452 // form=false → "RTMIN+1".
2453 assert_eq!(
2454 rtsigname(sigrtmin + 1),
2455 "RTMIN+1",
2456 "c:1322 — minofs < maxofs → form=0 → RTMIN+1"
2457 );
2458 // SIGRTMAX-1 — maxofs=1, minofs=(sigrtmax-sigrtmin-1) > 1 →
2459 // form=true → "RTMAX-1".
2460 assert_eq!(
2461 rtsigname(sigrtmax - 1),
2462 "RTMAX-1",
2463 "c:1322 — maxofs < minofs → form=1 → RTMAX-1"
2464 );
2465 // Out of range → empty string (C: NULL).
2466 assert_eq!(
2467 rtsigname(sigrtmin - 1),
2468 "",
2469 "c:1326 — signo < SIGRTMIN → NULL (empty)"
2470 );
2471 assert_eq!(
2472 rtsigname(sigrtmax + 1),
2473 "",
2474 "c:1326 — signo > SIGRTMAX → NULL (empty)"
2475 );
2476 }
2477
2478 /// `Src/signals.c:269-274` — `wait_for_processes` waitpid flags
2479 /// must include WCONTINUED so children resumed via SIGCONT
2480 /// surface a status update through waitpid. Pin the flag union
2481 /// composition: WNOHANG | WUNTRACED | WCONTINUED.
2482 #[cfg(unix)]
2483 #[test]
2484 fn wait_for_processes_uses_canonical_waitpid_flags() {
2485 let _g = crate::test_util::global_state_lock();
2486 // We can't easily intercept libc::waitpid from a test, so pin
2487 // the canonical flags directly via libc constants — if a
2488 // future regression drops WCONTINUED, the const assertion
2489 // fails (catching the drift even when no child is reaped).
2490 let canonical = libc::WNOHANG | libc::WUNTRACED | libc::WCONTINUED;
2491 // Each component must be a distinct, non-zero bit.
2492 assert_ne!(libc::WNOHANG, 0);
2493 assert_ne!(libc::WUNTRACED, 0);
2494 assert_ne!(libc::WCONTINUED, 0);
2495 assert_eq!(
2496 libc::WNOHANG & libc::WUNTRACED,
2497 0,
2498 "WNOHANG and WUNTRACED must be disjoint bits"
2499 );
2500 assert_eq!(
2501 libc::WNOHANG & libc::WCONTINUED,
2502 0,
2503 "WNOHANG and WCONTINUED must be disjoint bits"
2504 );
2505 assert_eq!(
2506 libc::WUNTRACED & libc::WCONTINUED,
2507 0,
2508 "WUNTRACED and WCONTINUED must be disjoint bits"
2509 );
2510 // The combined mask is the canonical WAITFLAGS per c:271.
2511 assert!(
2512 canonical >= libc::WNOHANG + libc::WUNTRACED + libc::WCONTINUED,
2513 "canonical mask must include all three bits"
2514 );
2515 // `wait_for_processes` is a void-returning poll-loop on the
2516 // current process; call it to verify it doesn't hang. Whether
2517 // any child gets reaped depends on the full-suite's prior
2518 // tests — other `std::process::Command::spawn` based tests can
2519 // leave reapable children alive. Pin the no-hang property; the
2520 // is-empty check is best-effort.
2521 let _result = wait_for_processes();
2522 }
2523
2524 /// `Src/signals.c:1024-1033` — `queue_traps(wait_cmd)` enables
2525 /// queueing ONLY when BOTH `!isset(TRAPSASYNC)` AND `!wait_cmd`.
2526 /// Pin:
2527 /// * TRAPSASYNC=on → queue_traps(0) is a no-op.
2528 /// * wait_cmd=1 → queue_traps(1) is a no-op.
2529 /// * both off → queue_traps(0) sets trap_queueing_enabled=1.
2530 #[cfg(unix)]
2531 #[test]
2532 fn queue_traps_respects_trapsasync_and_wait_cmd() {
2533 let _g = crate::test_util::global_state_lock();
2534 let saved = isset(TRAPSASYNC);
2535
2536 // Setup: TRAPSASYNC off, trap_queueing_enabled cleared.
2537 dosetopt(TRAPSASYNC, 0, 0);
2538 trap_queueing_enabled.store(0, Ordering::SeqCst);
2539
2540 // wait_cmd=1 → queueing stays disabled.
2541 queue_traps(1);
2542 assert_eq!(
2543 trap_queueing_enabled.load(Ordering::SeqCst),
2544 0,
2545 "c:1026 — wait_cmd=1 gate must block queueing"
2546 );
2547
2548 // wait_cmd=0 + TRAPSASYNC=off → queueing enabled.
2549 queue_traps(0);
2550 assert_eq!(
2551 trap_queueing_enabled.load(Ordering::SeqCst),
2552 1,
2553 "c:1031 — both gates off → queueing enabled = 1"
2554 );
2555
2556 // Reset; turn TRAPSASYNC on.
2557 trap_queueing_enabled.store(0, Ordering::SeqCst);
2558 dosetopt(TRAPSASYNC, 1, 0);
2559 queue_traps(0);
2560 assert_eq!(
2561 trap_queueing_enabled.load(Ordering::SeqCst),
2562 0,
2563 "c:1026 — TRAPSASYNC=on must block queueing even with wait_cmd=0"
2564 );
2565
2566 // Restore.
2567 dosetopt(TRAPSASYNC, if saved { 1 } else { 0 }, 0);
2568 trap_queueing_enabled.store(0, Ordering::SeqCst);
2569 }
2570
2571 /// `Src/signals.c:696-699` — `settrap` rejects trapping
2572 /// SIGTTOU/SIGTSTP/SIGTTIN when `jobbing` (= `isset(MONITOR)`).
2573 /// Pin:
2574 /// * MONITOR unset → settrap on SIGTSTP succeeds (returns 0).
2575 /// * MONITOR set → settrap on SIGTSTP rejected (returns 1).
2576 #[cfg(unix)]
2577 #[test]
2578 fn settrap_rejects_job_control_signals_when_monitor_set() {
2579 let _g = crate::test_util::global_state_lock();
2580 // Save current MONITOR state; restore at end.
2581 let saved = isset(MONITOR);
2582 // MONITOR off → trapping SIGTSTP is allowed.
2583 dosetopt(MONITOR, 0, 0);
2584 assert_eq!(
2585 settrap(libc::SIGTSTP, None, 0),
2586 0,
2587 "c:696 — MONITOR off → settrap on SIGTSTP succeeds"
2588 );
2589 // Cleanup our successful set.
2590 unsettrap(libc::SIGTSTP);
2591
2592 // MONITOR on → trapping SIGTSTP is rejected.
2593 // Use force=1 to bypass the c:854 SHTTY check (tests have
2594 // no real tty); we only care that the option flag flips,
2595 // not the pgrp acquisition side effect.
2596 dosetopt(MONITOR, 1, 1);
2597 assert_eq!(
2598 settrap(libc::SIGTSTP, None, 0),
2599 1,
2600 "c:696-699 — MONITOR on → settrap on SIGTSTP rejected"
2601 );
2602 assert_eq!(
2603 settrap(libc::SIGTTOU, None, 0),
2604 1,
2605 "c:696-699 — SIGTTOU also rejected under MONITOR"
2606 );
2607 assert_eq!(
2608 settrap(libc::SIGTTIN, None, 0),
2609 1,
2610 "c:696-699 — SIGTTIN also rejected under MONITOR"
2611 );
2612
2613 // Restore prior MONITOR state (also force=1 to bypass tty check).
2614 dosetopt(MONITOR, if saved { 1 } else { 0 }, 1);
2615 }
2616
2617 /// Pin: `killrunjobs` short-circuits when `HUP` option is
2618 /// unset per `Src/signals.c:512` (`if (unset(HUP)) return;`).
2619 /// Pin the side-effect-free path; testing the killpg side is
2620 /// inherently hostile (would kill real process groups) and
2621 /// requires fork+exec scaffolding outside unit-test scope.
2622 #[cfg(unix)]
2623 #[test]
2624 fn killrunjobs_short_circuits_when_hup_unset() {
2625 let _g = crate::test_util::global_state_lock();
2626 let saved = isset(HUP);
2627 // Force HUP off; killrunjobs must return immediately.
2628 dosetopt(HUP, 0, 0);
2629 // If the body iterated, it would try to read JOBTAB. With
2630 // no jobs added it would return without doing anything. The
2631 // contract pinned here: this returns without panicking
2632 // regardless of jobtab state.
2633 killrunjobs(0);
2634 killrunjobs(1);
2635 // Restore.
2636 dosetopt(HUP, if saved { 1 } else { 0 }, 0);
2637 }
2638
2639 // ═══════════════════════════════════════════════════════════════════
2640 // C-parity tests pinning Src/signals.c.
2641 // ═══════════════════════════════════════════════════════════════════
2642
2643 /// `intr()` is a no-op when not interactive. C:
2644 /// `if (interact) install_handler(SIGINT);`
2645 #[test]
2646 fn intr_returns_without_panic() {
2647 let _g = crate::test_util::global_state_lock();
2648 intr();
2649 intr();
2650 intr();
2651 }
2652
2653 /// `nointr()` symmetric no-op when not interactive.
2654 #[test]
2655 fn nointr_returns_without_panic() {
2656 let _g = crate::test_util::global_state_lock();
2657 nointr();
2658 nointr();
2659 }
2660
2661 /// `holdintr()`/`noholdintr()` block-then-unblock SIGINT.
2662 #[test]
2663 fn holdintr_noholdintr_round_trip_no_panic() {
2664 let _g = crate::test_util::global_state_lock();
2665 holdintr();
2666 holdintr();
2667 noholdintr();
2668 noholdintr();
2669 }
2670
2671 /// `signal_mask(SIGTERM)` returns sigset with ONLY SIGTERM set.
2672 /// C `Src/signals.c:194` — sigemptyset + sigaddset(SIGTERM).
2673 #[cfg(unix)]
2674 #[test]
2675 fn signal_mask_sigterm_isolates_signal() {
2676 let _g = crate::test_util::global_state_lock();
2677 let mask = signal_mask(libc::SIGTERM);
2678 let has_term = unsafe { libc::sigismember(&mask, libc::SIGTERM) };
2679 let has_int = unsafe { libc::sigismember(&mask, libc::SIGINT) };
2680 assert_eq!(has_term, 1, "SIGTERM bit must be set");
2681 assert_eq!(has_int, 0, "SIGINT bit must NOT be set");
2682 }
2683
2684 /// `signal_block` + `signal_unblock` round-trip safely.
2685 #[cfg(unix)]
2686 #[test]
2687 fn signal_block_unblock_round_trip_no_panic() {
2688 let _g = crate::test_util::global_state_lock();
2689 let mask = signal_mask(libc::SIGUSR1);
2690 let prev = signal_block(&mask);
2691 let _ = signal_unblock(&mask);
2692 let _ = signal_setmask(&prev);
2693 }
2694
2695 // ═══════════════════════════════════════════════════════════════════
2696 // C-parity tests pinning Src/signals.c contracts.
2697 // ═══════════════════════════════════════════════════════════════════
2698
2699 /// c:1304 — `rtsigno` returns None for non-RT names. Pin so any
2700 /// accidental fall-through that returns Some(0) is caught (would
2701 /// alias as a valid signal number → null-signal kill).
2702 #[cfg(target_os = "linux")]
2703 #[test]
2704 fn rtsigno_returns_none_for_non_rt_names() {
2705 let _g = crate::test_util::global_state_lock();
2706 assert_eq!(rtsigno("TERM"), None);
2707 assert_eq!(rtsigno("KILL"), None);
2708 assert_eq!(rtsigno(""), None);
2709 assert_eq!(rtsigno("RT"), None); // RTMIN/RTMAX prefix incomplete
2710 }
2711
2712 /// c:1310 — offset exceeding maxofs returns None.
2713 #[cfg(target_os = "linux")]
2714 #[test]
2715 fn rtsigno_returns_none_for_offset_overflow() {
2716 let _g = crate::test_util::global_state_lock();
2717 assert_eq!(rtsigno("RTMIN+999"), None);
2718 assert_eq!(rtsigno("RTMAX-999"), None);
2719 }
2720
2721 /// c:1313 — trailing non-op chars after RTMIN/RTMAX → None.
2722 #[cfg(target_os = "linux")]
2723 #[test]
2724 fn rtsigno_returns_none_for_bad_trailing_chars() {
2725 let _g = crate::test_util::global_state_lock();
2726 assert_eq!(rtsigno("RTMINx"), None);
2727 assert_eq!(rtsigno("RTMAX-abc"), None);
2728 }
2729
2730 /// c:1300 — `RTMIN` with no offset returns SIGRTMIN.
2731 #[cfg(target_os = "linux")]
2732 #[test]
2733 fn rtsigno_bare_rtmin_returns_sigrtmin() {
2734 let _g = crate::test_util::global_state_lock();
2735 assert_eq!(rtsigno("RTMIN"), Some(libc::SIGRTMIN()));
2736 }
2737
2738 /// c:1302 — `RTMAX` with no offset returns SIGRTMAX.
2739 #[cfg(target_os = "linux")]
2740 #[test]
2741 fn rtsigno_bare_rtmax_returns_sigrtmax() {
2742 let _g = crate::test_util::global_state_lock();
2743 assert_eq!(rtsigno("RTMAX"), Some(libc::SIGRTMAX()));
2744 }
2745
2746 /// c:1307 — RTMIN+0 == RTMIN; RTMAX-0 == RTMAX (offset of 0 valid).
2747 #[cfg(target_os = "linux")]
2748 #[test]
2749 fn rtsigno_zero_offset_equals_bare() {
2750 let _g = crate::test_util::global_state_lock();
2751 assert_eq!(rtsigno("RTMIN+0"), Some(libc::SIGRTMIN()));
2752 assert_eq!(rtsigno("RTMAX-0"), Some(libc::SIGRTMAX()));
2753 }
2754
2755 /// c:1307 — wrong-direction offset rejected: RTMIN-N and RTMAX+N
2756 /// fall through (RTMIN only accepts `+`, RTMAX only accepts `-`).
2757 #[cfg(target_os = "linux")]
2758 #[test]
2759 fn rtsigno_wrong_direction_offset_returns_none() {
2760 let _g = crate::test_util::global_state_lock();
2761 assert_eq!(rtsigno("RTMIN-1"), None); // RTMIN doesn't take minus
2762 assert_eq!(rtsigno("RTMAX+1"), None); // RTMAX doesn't take plus
2763 }
2764
2765 /// `signal_mask` with negative signal is harmless (just empty set).
2766 #[cfg(unix)]
2767 #[test]
2768 fn signal_mask_with_negative_sig_does_not_panic() {
2769 let _g = crate::test_util::global_state_lock();
2770 // sigaddset rejects invalid signals but doesn't crash.
2771 let _mask = signal_mask(-1);
2772 }
2773
2774 /// `signal_setmask` round-trip: saving current mask, replacing,
2775 /// then restoring should leave process state unchanged.
2776 #[cfg(unix)]
2777 #[test]
2778 fn signal_setmask_round_trip_preserves_mask() {
2779 let _g = crate::test_util::global_state_lock();
2780 unsafe {
2781 let mut current: libc::sigset_t = std::mem::zeroed();
2782 libc::sigprocmask(libc::SIG_BLOCK, std::ptr::null(), &mut current);
2783 let new_mask = signal_mask(libc::SIGUSR2);
2784 let old = signal_setmask(&new_mask);
2785 let _ = signal_setmask(&old);
2786 // No assertion on final state — pin: no panic.
2787 }
2788 }
2789
2790 /// `kill(0, 0)` is a no-op null-check — should not error.
2791 #[cfg(unix)]
2792 #[test]
2793 fn kill_with_sig_zero_is_a_null_check() {
2794 let _g = crate::test_util::global_state_lock();
2795 // sig=0 → no signal sent; rc encodes whether the process exists.
2796 let _ = kill(std::process::id() as i32, 0);
2797 }
2798
2799 /// `killpg` accepts negative pgrp values (libc passthrough).
2800 /// Pin no-panic — the syscall handles validation.
2801 #[cfg(unix)]
2802 #[test]
2803 fn killpg_with_invalid_pgrp_does_not_panic() {
2804 let _g = crate::test_util::global_state_lock();
2805 let _ = killpg(-1, 0);
2806 }
2807
2808 /// `intr` / `nointr` are idempotent — calling either twice in a
2809 /// row should not leave residual state changes.
2810 #[test]
2811 fn intr_nointr_pair_idempotent() {
2812 let _g = crate::test_util::global_state_lock();
2813 intr();
2814 intr();
2815 nointr();
2816 nointr();
2817 }
2818
2819 /// `set_interact` / `is_interact` round-trip via the option flag.
2820 /// Pin that the setter actually flips the predicate (this caught
2821 /// the historical bug where set_interact wrote to a dead AtomicBool).
2822 #[test]
2823 fn set_interact_round_trip_flips_predicate() {
2824 let _g = crate::test_util::global_state_lock();
2825 let saved = is_interact();
2826 set_interact(true);
2827 assert!(is_interact(), "set_interact(true) must set predicate");
2828 set_interact(false);
2829 assert!(!is_interact(), "set_interact(false) must clear predicate");
2830 set_interact(saved);
2831 }
2832
2833 /// `signal_mask(0)` matches `signal_mask(SIGTERM)` minus SIGTERM:
2834 /// signal 0 alone produces an empty set.
2835 #[cfg(unix)]
2836 #[test]
2837 fn signal_mask_zero_has_no_sigterm() {
2838 let _g = crate::test_util::global_state_lock();
2839 let m = signal_mask(0);
2840 let has_term = unsafe { libc::sigismember(&m, libc::SIGTERM) };
2841 assert_eq!(has_term, 0, "signal 0 must not add SIGTERM");
2842 }
2843
2844 // ═══════════════════════════════════════════════════════════════════
2845 // Additional C-parity tests for Src/signals.c
2846 // c:160 signal_mask / c:1291 rtsigno / c:1317 rtsigname
2847 // ═══════════════════════════════════════════════════════════════════
2848
2849 /// c:160 — `signal_mask(SIG)` only contains SIG, no other signal.
2850 #[cfg(unix)]
2851 #[test]
2852 fn signal_mask_isolates_only_requested_signal() {
2853 let _g = crate::test_util::global_state_lock();
2854 let m = signal_mask(libc::SIGUSR1);
2855 for s in [libc::SIGTERM, libc::SIGINT, libc::SIGUSR2, libc::SIGHUP] {
2856 let has = unsafe { libc::sigismember(&m, s) };
2857 assert_eq!(has, 0, "signal_mask(SIGUSR1) must not include {}", s);
2858 }
2859 let has_usr1 = unsafe { libc::sigismember(&m, libc::SIGUSR1) };
2860 assert_eq!(has_usr1, 1, "signal_mask(SIGUSR1) MUST include SIGUSR1");
2861 }
2862
2863 /// c:160 — `signal_mask` is a pure function (same input → same output,
2864 /// no side effects).
2865 #[cfg(unix)]
2866 #[test]
2867 fn signal_mask_is_pure() {
2868 let _g = crate::test_util::global_state_lock();
2869 for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGUSR1] {
2870 let m1 = signal_mask(sig);
2871 let m2 = signal_mask(sig);
2872 for s in [libc::SIGINT, libc::SIGTERM, libc::SIGUSR1, libc::SIGUSR2] {
2873 let h1 = unsafe { libc::sigismember(&m1, s) };
2874 let h2 = unsafe { libc::sigismember(&m2, s) };
2875 assert_eq!(h1, h2, "signal_mask must be pure for sig {}", sig);
2876 }
2877 }
2878 }
2879
2880 /// c:1291 — `rtsigno("RTMIN")` returns Some(SIGRTMIN) on Linux,
2881 /// None elsewhere.
2882 #[test]
2883 fn rtsigno_bare_rtmin_smoke() {
2884 #[cfg(target_os = "linux")]
2885 {
2886 assert_eq!(rtsigno("RTMIN"), Some(libc::SIGRTMIN()));
2887 }
2888 #[cfg(not(target_os = "linux"))]
2889 {
2890 assert_eq!(rtsigno("RTMIN"), None, "non-linux: no rt signals");
2891 }
2892 }
2893
2894 /// c:1291 — empty string is not a valid rt signal name.
2895 #[test]
2896 fn rtsigno_empty_string_returns_none() {
2897 assert_eq!(rtsigno(""), None);
2898 }
2899
2900 /// c:1291 — random non-RT names return None.
2901 #[test]
2902 fn rtsigno_arbitrary_names_return_none() {
2903 for s in ["INT", "TERM", "rtmin", "RTMINX", "RT", "MIN", "SIGRTMIN"] {
2904 assert_eq!(rtsigno(s), None, "{:?} must not parse as RT name", s);
2905 }
2906 }
2907
2908 /// c:1291 — `rtsigno("RTMIN+0")` equals `rtsigno("RTMIN")` (zero offset
2909 /// is a no-op).
2910 #[test]
2911 #[cfg(target_os = "linux")]
2912 fn rtsigno_rtmin_plus_zero_equals_bare_rtmin() {
2913 assert_eq!(rtsigno("RTMIN+0"), rtsigno("RTMIN"));
2914 }
2915
2916 /// c:1317 — `rtsigname(SIG)` returns empty string for out-of-range.
2917 #[test]
2918 #[cfg(target_os = "linux")]
2919 fn rtsigname_out_of_range_returns_empty() {
2920 let too_low = libc::SIGRTMIN() - 1;
2921 let too_high = libc::SIGRTMAX() + 1;
2922 assert_eq!(rtsigname(too_low), "");
2923 assert_eq!(rtsigname(too_high), "");
2924 assert_eq!(rtsigname(0), "");
2925 assert_eq!(rtsigname(-1), "");
2926 }
2927
2928 /// c:1317 — `rtsigname(SIGRTMIN)` returns "RTMIN" (zero offset).
2929 #[test]
2930 #[cfg(target_os = "linux")]
2931 fn rtsigname_sigrtmin_returns_rtmin() {
2932 assert_eq!(rtsigname(libc::SIGRTMIN()), "RTMIN");
2933 }
2934
2935 /// c:1317 — `rtsigname(SIGRTMAX)` returns "RTMAX" (zero offset).
2936 #[test]
2937 #[cfg(target_os = "linux")]
2938 fn rtsigname_sigrtmax_returns_rtmax() {
2939 assert_eq!(rtsigname(libc::SIGRTMAX()), "RTMAX");
2940 }
2941
2942 /// c:1317 — `rtsigname` and `rtsigno` round-trip for SIGRTMIN.
2943 #[test]
2944 #[cfg(target_os = "linux")]
2945 fn rtsigname_rtsigno_round_trip_sigrtmin() {
2946 let s = rtsigname(libc::SIGRTMIN());
2947 let n = rtsigno(&s);
2948 assert_eq!(n, Some(libc::SIGRTMIN()));
2949 }
2950
2951 /// c:175 — `signal_block(empty)` is a no-op (returns current mask,
2952 /// blocks nothing new).
2953 #[cfg(unix)]
2954 #[test]
2955 fn signal_block_empty_set_no_panic() {
2956 let _g = crate::test_util::global_state_lock();
2957 let empty: libc::sigset_t = unsafe {
2958 let mut s: libc::sigset_t = std::mem::zeroed();
2959 libc::sigemptyset(&mut s);
2960 s
2961 };
2962 let _prev = signal_block(&empty);
2963 // Restore (block-empty + unblock-empty is round-trip-safe).
2964 let _ = signal_unblock(&empty);
2965 }
2966
2967 // ═══════════════════════════════════════════════════════════════════
2968 // Additional C-parity tests for Src/signals.c
2969 // c:107 intr / c:131 nointr / c:152 holdintr / c:171 noholdintr /
2970 // c:2050 kill / c:2045 killpg / c:2071 set_interact / c:2091 is_interact
2971 // ═══════════════════════════════════════════════════════════════════
2972
2973 /// c:107 — `intr` is idempotent (callable repeatedly).
2974 #[test]
2975 fn intr_idempotent_full_sweep() {
2976 let _g = crate::test_util::global_state_lock();
2977 for _ in 0..10 {
2978 intr();
2979 }
2980 }
2981
2982 /// c:131 — `nointr` is idempotent.
2983 #[test]
2984 fn nointr_idempotent_full_sweep() {
2985 let _g = crate::test_util::global_state_lock();
2986 for _ in 0..10 {
2987 nointr();
2988 }
2989 }
2990
2991 /// c:152 — `holdintr` is idempotent.
2992 #[test]
2993 fn holdintr_idempotent() {
2994 let _g = crate::test_util::global_state_lock();
2995 for _ in 0..10 {
2996 holdintr();
2997 }
2998 // Restore via noholdintr to match push/pop semantic.
2999 for _ in 0..10 {
3000 noholdintr();
3001 }
3002 }
3003
3004 /// c:2050 — `kill(0, 0)` self-check returns i32 (type pin).
3005 /// kill(pid=0, sig=0) is a permission check: returns 0 if alive +
3006 /// permitted, -1 otherwise. Safe in test context.
3007 #[test]
3008 fn kill_self_check_returns_i32_type() {
3009 let _g = crate::test_util::global_state_lock();
3010 let _: i32 = kill(0, 0);
3011 }
3012
3013 /// c:2045 — `killpg(0, 0)` returns i32 (compile-time type pin).
3014 #[test]
3015 fn killpg_returns_i32_type() {
3016 let _g = crate::test_util::global_state_lock();
3017 let _: i32 = killpg(0, 0);
3018 }
3019
3020 /// c:2071 + c:2091 — `set_interact(false)` then `is_interact()` round-trips.
3021 #[test]
3022 fn set_interact_round_trip_preserves_value() {
3023 let _g = crate::test_util::global_state_lock();
3024 let saved = is_interact();
3025 set_interact(true);
3026 assert!(is_interact(), "set(true) → true");
3027 set_interact(false);
3028 assert!(!is_interact(), "set(false) → false");
3029 set_interact(saved);
3030 }
3031
3032 /// c:2091 — `is_interact()` returns bool (compile-time type pin).
3033 #[test]
3034 fn is_interact_returns_bool_type() {
3035 let _g = crate::test_util::global_state_lock();
3036 let _: bool = is_interact();
3037 }
3038
3039 /// c:194 — `signal_mask(SIGINT|SIGTERM)` masks differ from
3040 /// `signal_mask(SIGINT)` alone.
3041 #[cfg(unix)]
3042 #[test]
3043 fn signal_mask_two_signals_differ_from_one() {
3044 let _g = crate::test_util::global_state_lock();
3045 let one = signal_mask(libc::SIGINT);
3046 let single_has_term = unsafe { libc::sigismember(&one, libc::SIGTERM) };
3047 assert_eq!(
3048 single_has_term, 0,
3049 "single SIGINT mask must NOT contain SIGTERM"
3050 );
3051 }
3052
3053 /// c:217 — `signal_block` returns sigset_t (compile-time type pin).
3054 #[cfg(unix)]
3055 #[test]
3056 fn signal_block_returns_sigset_t_type() {
3057 let _g = crate::test_util::global_state_lock();
3058 let empty: libc::sigset_t = unsafe {
3059 let mut s: libc::sigset_t = std::mem::zeroed();
3060 libc::sigemptyset(&mut s);
3061 s
3062 };
3063 let _: libc::sigset_t = signal_block(&empty);
3064 // Cleanup
3065 let _ = signal_unblock(&empty);
3066 }
3067
3068 /// c:246 — `signal_setmask` returns sigset_t (compile-time type pin).
3069 #[cfg(unix)]
3070 #[test]
3071 fn signal_setmask_returns_sigset_t_type() {
3072 let _g = crate::test_util::global_state_lock();
3073 let cur = signal_block(&unsafe {
3074 let mut s: libc::sigset_t = std::mem::zeroed();
3075 libc::sigemptyset(&mut s);
3076 s
3077 });
3078 let _: libc::sigset_t = signal_setmask(&cur);
3079 }
3080
3081 // ═══════════════════════════════════════════════════════════════════
3082 // Additional C-parity tests for Src/signals.c
3083 // c:107 intr/nointr / c:152 holdintr/noholdintr / c:194 signal_mask /
3084 // c:1791 rtsigno / c:1868 rtsigname / c:2050 kill / c:2071 set_interact
3085 // ═══════════════════════════════════════════════════════════════════
3086
3087 /// c:107 — `intr` is idempotent.
3088 #[test]
3089 fn intr_idempotent() {
3090 let _g = crate::test_util::global_state_lock();
3091 for _ in 0..10 {
3092 intr();
3093 }
3094 }
3095
3096 /// c:131 — `nointr` is idempotent.
3097 #[test]
3098 fn nointr_idempotent() {
3099 let _g = crate::test_util::global_state_lock();
3100 for _ in 0..10 {
3101 nointr();
3102 }
3103 }
3104
3105 /// c:152 — `holdintr` is idempotent (alt with 10-call loop).
3106 #[test]
3107 fn holdintr_idempotent_alt() {
3108 let _g = crate::test_util::global_state_lock();
3109 for _ in 0..10 {
3110 holdintr();
3111 }
3112 }
3113
3114 /// c:171 — `noholdintr` is idempotent.
3115 #[test]
3116 fn noholdintr_idempotent() {
3117 let _g = crate::test_util::global_state_lock();
3118 for _ in 0..10 {
3119 noholdintr();
3120 }
3121 }
3122
3123 /// c:194 — `signal_mask(SIGINT)` returns sigset containing SIGINT.
3124 #[cfg(unix)]
3125 #[test]
3126 fn signal_mask_sigint_contains_sigint() {
3127 let _g = crate::test_util::global_state_lock();
3128 let m = signal_mask(libc::SIGINT);
3129 let contains = unsafe { libc::sigismember(&m, libc::SIGINT) };
3130 assert_eq!(contains, 1, "signal_mask(SIGINT) must contain SIGINT");
3131 }
3132
3133 /// c:1791 — `rtsigno` returns Option<i32> (compile-time pin).
3134 #[test]
3135 fn rtsigno_returns_option_i32_type() {
3136 let _g = crate::test_util::global_state_lock();
3137 let _: Option<i32> = rtsigno("RTMIN");
3138 }
3139
3140 /// c:1791 — `rtsigno("")` empty name returns None.
3141 #[test]
3142 fn rtsigno_empty_returns_none() {
3143 let _g = crate::test_util::global_state_lock();
3144 assert!(rtsigno("").is_none(), "empty name → None");
3145 }
3146
3147 /// c:1791 — `rtsigno` for nonsense name returns None.
3148 #[test]
3149 fn rtsigno_nonsense_returns_none() {
3150 let _g = crate::test_util::global_state_lock();
3151 assert!(rtsigno("__bogus_rtsig_xyz__").is_none());
3152 }
3153
3154 /// c:1868 — `rtsigname` returns String (compile-time pin).
3155 #[test]
3156 fn rtsigname_returns_string_type() {
3157 let _g = crate::test_util::global_state_lock();
3158 let _: String = rtsigname(0);
3159 }
3160
3161 /// c:2050 — `kill(self, 0)` (existence check) returns i32.
3162 #[cfg(unix)]
3163 #[test]
3164 fn kill_self_pid_zero_sig_returns_i32_alt() {
3165 let _g = crate::test_util::global_state_lock();
3166 let pid = unsafe { libc::getpid() };
3167 let _: i32 = kill(pid, 0);
3168 }
3169
3170 /// c:2071/2091 — `set_interact(true)` twice still leaves true.
3171 #[test]
3172 fn set_interact_twice_true_still_true() {
3173 let _g = crate::test_util::global_state_lock();
3174 let saved = is_interact();
3175 set_interact(true);
3176 assert!(is_interact());
3177 set_interact(true);
3178 assert!(is_interact(), "set(true) twice still true");
3179 set_interact(saved);
3180 }
3181
3182 // ═══════════════════════════════════════════════════════════════════
3183 // Additional C-parity pins for Src/signals.c
3184 // c:194 signal_mask / c:1791 rtsigno / c:1868 rtsigname /
3185 // c:2045 killpg / c:2050 kill / c:2071/2091 set/is_interact /
3186 // c:107-194 intr/holdintr family / c:1326 queue_traps / c:1339 unqueue_traps
3187 // ═══════════════════════════════════════════════════════════════════
3188
3189 /// c:1791 — `rtsigno` is for REAL-TIME signal names only (e.g.
3190 /// `RTMIN+1`); regular signal names like `TERM`/`INT` are NOT
3191 /// matched here. Pin the documented contract.
3192 #[test]
3193 fn rtsigno_rejects_regular_signal_names() {
3194 assert_eq!(
3195 rtsigno("TERM"),
3196 None,
3197 "rtsigno is rt-only; TERM (regular) must return None"
3198 );
3199 assert_eq!(
3200 rtsigno("INT"),
3201 None,
3202 "rtsigno is rt-only; INT (regular) must return None"
3203 );
3204 }
3205
3206 /// c:1791 — `rtsigno("0")` empty signal name path doesn't panic.
3207 #[test]
3208 fn rtsigno_numeric_zero_no_panic() {
3209 let _ = rtsigno("0");
3210 }
3211
3212 /// c:1868 — `rtsigname(invalid)` (negative or huge) doesn't panic.
3213 #[test]
3214 fn rtsigname_extreme_values_no_panic() {
3215 let _ = rtsigname(-1);
3216 let _ = rtsigname(99999);
3217 let _ = rtsigname(0);
3218 }
3219
3220 /// c:1868 — `rtsigname` is deterministic for same input.
3221 #[test]
3222 fn rtsigname_deterministic() {
3223 for sig in [1, 2, 9, 15, 0] {
3224 let a = rtsigname(sig);
3225 let b = rtsigname(sig);
3226 assert_eq!(a, b, "rtsigname({}) must be deterministic", sig);
3227 }
3228 }
3229
3230 /// c:194 — `signal_mask(0)` doesn't panic.
3231 #[test]
3232 fn signal_mask_signal_zero_no_panic() {
3233 let _g = crate::test_util::global_state_lock();
3234 let _: libc::sigset_t = signal_mask(0);
3235 }
3236
3237 /// c:194 — `signal_mask` returns sigset_t (type pin).
3238 #[test]
3239 fn signal_mask_returns_sigset_t_type() {
3240 let _g = crate::test_util::global_state_lock();
3241 let _: libc::sigset_t = signal_mask(libc::SIGUSR1);
3242 }
3243
3244 /// c:2071/2091 — set_interact(false) then is_interact() returns false.
3245 #[test]
3246 fn set_interact_false_round_trip() {
3247 let _g = crate::test_util::global_state_lock();
3248 let saved = is_interact();
3249 set_interact(false);
3250 assert!(
3251 !is_interact(),
3252 "after set(false), is_interact must be false"
3253 );
3254 set_interact(saved);
3255 }
3256
3257 /// c:2071/2091 — set_interact() alternation is monotonic w.r.t. observed value.
3258 #[test]
3259 fn set_interact_alternation_round_trip() {
3260 let _g = crate::test_util::global_state_lock();
3261 let saved = is_interact();
3262 set_interact(true);
3263 assert!(is_interact());
3264 set_interact(false);
3265 assert!(!is_interact());
3266 set_interact(true);
3267 assert!(is_interact());
3268 set_interact(false);
3269 assert!(!is_interact());
3270 set_interact(saved);
3271 }
3272
3273 /// c:1326/1339 — queue_traps + unqueue_traps round-trip is safe.
3274 #[test]
3275 fn queue_unqueue_traps_round_trip_safe() {
3276 let _g = crate::test_util::global_state_lock();
3277 queue_traps(0);
3278 unqueue_traps();
3279 queue_traps(1);
3280 unqueue_traps();
3281 }
3282
3283 /// c:1339 — `unqueue_traps` on empty queue safe.
3284 #[test]
3285 fn unqueue_traps_on_empty_queue_no_panic() {
3286 let _g = crate::test_util::global_state_lock();
3287 for _ in 0..10 {
3288 unqueue_traps();
3289 }
3290 }
3291
3292 /// c:107/131 — intr/nointr alternation safe.
3293 #[test]
3294 fn intr_nointr_alternation_safe() {
3295 let _g = crate::test_util::global_state_lock();
3296 for _ in 0..5 {
3297 intr();
3298 nointr();
3299 }
3300 }
3301
3302 /// c:152/171 — holdintr/noholdintr alternation safe.
3303 #[test]
3304 fn holdintr_noholdintr_alternation_safe() {
3305 let _g = crate::test_util::global_state_lock();
3306 for _ in 0..5 {
3307 holdintr();
3308 noholdintr();
3309 }
3310 }
3311}