Skip to main content

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 nix::sys::signal::{sigprocmask, SigmaskHow};
19use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal as NixSignal};
20use nix::unistd::getpid;
21use std::collections::HashMap;
22use std::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
23use std::sync::{Mutex, OnceLock};
24use crate::signals_h::{MAX_QUEUE_SIZE, SIGCOUNT, SIGDEBUG, SIGEXIT, SIGZERR, TRAPCOUNT, signal_default, signal_ignore};
25use crate::zsh_h::{
26    isset, ERRFLAG_INT, INTERACTIVE, POSIXTRAPS, PRIVILEGED,
27    TRAP_STATE_FORCE_RETURN, TRAP_STATE_PRIMED, ZEXIT_SIGNAL,
28    ZSIG_FUNC, ZSIG_IGNORED, ZSIG_SHIFT, ZSIG_TRAPPED,
29};
30
31
32// getsigidx / getsigname live in `jobs.rs` per C source split:
33// `getsigidx` at `Src/jobs.c:3047`, `getsigname` at `Src/jobs.c:3087`.
34// Re-export from the canonical home so callers using
35// `crate::ported::signals::getsigidx` continue to compile.
36pub use crate::ported::jobs::{getsigidx, getsigname};
37
38// ---------------------------------------------------------------------------
39// Signal-queue state. Direct ports of `Src/signals.c:77-92`:
40//
41//   mod_export volatile int queueing_enabled, queue_front, queue_rear;   // c:77
42//   mod_export int signal_queue[MAX_QUEUE_SIZE];                         // c:79
43//   mod_export sigset_t signal_mask_queue[MAX_QUEUE_SIZE];               // c:81
44//   static volatile int trap_queueing_enabled,
45//                       trap_queue_front, trap_queue_rear;               // c:90
46//   static int trap_queue[MAX_QUEUE_SIZE];                               // c:92
47//
48// C uses flat module-level variables; Rust mirrors with file-scope
49// `AtomicI32` + `LazyLock<Mutex<Vec<...>>>` slabs so concurrent
50// pushes from the async signal handler synchronize without UB.
51// ---------------------------------------------------------------------------
52
53/// Signal-queue depth counter. Port of `mod_export volatile int
54/// queueing_enabled` from `Src/signals.c:77`.
55pub static queueing_enabled: AtomicI32 = AtomicI32::new(0);                  // c:77
56
57/// Ring-buffer head. Port of `mod_export volatile int queue_front`
58/// from `Src/signals.c:77`.
59pub static queue_front: AtomicUsize = AtomicUsize::new(0);                   // c:77
60
61/// Ring-buffer tail. Port of `mod_export volatile int queue_rear`
62/// from `Src/signals.c:77`.
63pub static queue_rear: AtomicUsize = AtomicUsize::new(0);                    // c:77
64
65/// Port of `mod_export volatile int queue_in` from `Src/signals.c:84`.
66/// Companion counter bumped by `queue_signals()` (signals.h:90) and
67/// decremented by `unqueue_signals()` (signals.h:94); used by
68/// `dont_queue_signals()` to snapshot the depth (signals.h:99) and
69/// by debug assertions (DPUTS2 at signals.h:105).
70pub static queue_in: AtomicI32 = AtomicI32::new(0);                          // c:84
71
72#[allow(clippy::declare_interior_mutable_const)]
73const ATOM_I32_ZERO: AtomicI32 = AtomicI32::new(0);
74
75/// Per-slot signal numbers. Port of `mod_export int
76/// signal_queue[MAX_QUEUE_SIZE]` from `Src/signals.c:79`.
77pub static signal_queue: [AtomicI32; MAX_QUEUE_SIZE] =                       // c:79
78    [ATOM_I32_ZERO; MAX_QUEUE_SIZE];
79
80/// Per-slot blocked-mask snapshots. Port of `mod_export sigset_t
81/// signal_mask_queue[MAX_QUEUE_SIZE]` from `Src/signals.c:81`.
82/// `sigset_t` isn't Copy on every platform — wrapped in a Mutex
83/// so the slabs initialize without const-eval gymnastics.
84pub static signal_mask_queue: std::sync::LazyLock<Mutex<Vec<libc::sigset_t>>> = // c:81
85    std::sync::LazyLock::new(|| {
86        let zero: libc::sigset_t = unsafe { std::mem::zeroed() };
87        Mutex::new(vec![zero; MAX_QUEUE_SIZE])
88    });
89
90/// Trap-queue depth counter. Port of `static volatile int
91/// trap_queueing_enabled` from `Src/signals.c:90`.
92pub static trap_queueing_enabled: AtomicI32 = AtomicI32::new(0);             // c:90
93
94/// Trap-queue head. Port of `static volatile int trap_queue_front`
95/// from `Src/signals.c:90`.
96pub static trap_queue_front: AtomicUsize = AtomicUsize::new(0);              // c:90
97
98/// Trap-queue tail. Port of `static volatile int trap_queue_rear`
99/// from `Src/signals.c:90`.
100pub static trap_queue_rear: AtomicUsize = AtomicUsize::new(0);               // c:90
101
102/// Per-slot trap-queue signals. Port of `static int
103/// trap_queue[MAX_QUEUE_SIZE]` from `Src/signals.c:92`.
104pub static trap_queue: [AtomicI32; MAX_QUEUE_SIZE] =                         // c:92
105    [ATOM_I32_ZERO; MAX_QUEUE_SIZE];
106
107/// Port of `int last_signal` from `Src/signals.c:238`. Holds the
108/// signal number of the most recent delivery; used by `wait_cmd`
109/// in jobs.c to set `$?` to `128 + last_signal` when a trapped
110/// signal interrupts wait.
111pub static last_signal: AtomicI32 = AtomicI32::new(0);                       // c:238
112
113// ---------------------------------------------------------------------------
114// Per-signal trap state. Direct ports of the C globals declared in
115// `Src/signals.c:39/53/58`:
116//
117//   mod_export int      *sigtrapped;       // c:39 — flag word per sig
118//   mod_export Eprog    *siglists;         // c:53 — Eprog per sig (trap body)
119//   mod_export volatile int nsigtrapped;   // c:58 — trapped-signal count
120//
121// C allocates parallel arrays of length TRAPCOUNT at init time
122// (`Src/init.c:1398`). Rust mirrors with `Mutex<Vec<...>>` slabs
123// sized to TRAPCOUNT plus an atomic counter. TRAPxxx-function
124// trap bodies are NOT stored here in C either — `dotrap` looks
125// them up via `gettrapnode()` from shfunctab on signal delivery
126// (`Src/jobs.c:gettrapnode`).
127// ---------------------------------------------------------------------------
128
129/// Per-signal flag word. Port of `mod_export int *sigtrapped`
130/// from `Src/signals.c:39`. Bit values are `ZSIG_TRAPPED`,
131/// `ZSIG_IGNORED`, `ZSIG_FUNC`, plus `(locallevel << ZSIG_SHIFT)`
132/// in the high bits.
133pub static sigtrapped: std::sync::LazyLock<Mutex<Vec<i32>>> =                 // c:39
134    std::sync::LazyLock::new(|| Mutex::new(vec![0; TRAPCOUNT as usize]));
135
136/// Per-signal Eprog body. Port of `mod_export Eprog *siglists`
137/// from `Src/signals.c:53`. NULL for ZSIG_FUNC entries (function
138/// body resolves through `gettrapnode` at dispatch time).
139pub static siglists: std::sync::LazyLock<Mutex<Vec<Option<crate::ported::zsh_h::Eprog>>>> =     // c:53
140    std::sync::LazyLock::new(|| Mutex::new((0..TRAPCOUNT as usize).map(|_| None).collect()));
141
142/// Count of `ZSIG_TRAPPED`-flagged signals. Port of
143/// `mod_export volatile int nsigtrapped` from `Src/signals.c:58`.
144pub static nsigtrapped: AtomicI32 = AtomicI32::new(0);                        // c:58
145
146/// File-scope `int intrap` from `Src/signals.c`. Set while a
147/// trap body is running so nested `dotrap` calls short-circuit
148/// (matches the c:1245 dispatcher's `if (intrap) return`).
149pub static intrap: AtomicI32 = AtomicI32::new(0);                             // c:intrap
150
151/// File-scope `int in_exit_trap` from `Src/signals.c:60`. Set
152/// while the EXIT trap body is running so `exit` and friends can
153/// distinguish "real" exit from exit-trap-driven exit.
154pub static in_exit_trap: AtomicI32 = AtomicI32::new(0);                       // c:60
155
156/// Port of `volatile int trapisfunc` from `Src/signals.c:1062`.
157/// Set by `dotrapargs()` (signals.c:1156) when the trap body is a
158/// shell function (vs. inline command) — the `IN_EVAL_TRAP()` macro
159/// at zsh.h:2962 tests this against `intrap` + `locallevel`.
160pub static trapisfunc: AtomicI32 = AtomicI32::new(0);                         // c:1062
161
162/// Port of `volatile int traplocallevel` from `Src/signals.c:1069`.
163/// Captures `locallevel` at trap-entry so the trap body can detect
164/// whether it's running inside the same scope it was registered in
165/// (the third leg of `IN_EVAL_TRAP()` at zsh.h:2962).
166pub static traplocallevel: AtomicI32 = AtomicI32::new(0);                     // c:1069
167
168// Variables used by signal queueing                                       // c:74
169/// Enable signal queueing.
170// queue_signals / unqueue_signals live in `signals_h.rs` per the C
171// source split: both are `#define` macros in `Src/signals.h:90/112`
172// + `92/114`, not functions in `Src/signals.c`. Re-export from the
173// canonical home so callers using `crate::ported::signals::queue_signals`
174// continue to compile, and the QUEUEING_ENABLED state is shared
175// across all callers (instead of split between two parallel
176// SignalQueue/QUEUEING_ENABLED counters).
177pub use crate::ported::signals_h::{queue_signals, unqueue_signals};
178
179/// Direct port of `void queue_traps(int wait_cmd)` from
180/// `Src/signals.c:1041`. Increments `trap_queueing_enabled` so
181/// signals delivered while a long-running builtin is mid-flight
182/// stash into `trap_queue[]` instead of dispatching inline.
183pub fn queue_traps(_wait_cmd: i32) {                                          // c:1024
184    trap_queueing_enabled.fetch_add(1, Ordering::SeqCst);
185}
186
187// Disable trap queuing and run the traps.                                 // c:1041
188/// Direct port of `void unqueue_traps(void)` from
189/// `Src/signals.c:1041`. Disables `trap_queueing_enabled` and
190/// flushes the pending queue by dispatching each sig through
191/// `handletrap()`.
192pub fn unqueue_traps() {                                                     // c:1041
193    // c:1041 — `trap_queueing_enabled = 0;`
194    trap_queueing_enabled.store(0, Ordering::SeqCst);
195    // c:1046 — `while (trap_queue_front != trap_queue_rear) (void) handletrap(...);`
196    loop {
197        let f = trap_queue_front.load(Ordering::SeqCst);
198        let r = trap_queue_rear.load(Ordering::SeqCst);
199        if f == r { break; }
200        let nf = (f + 1) % MAX_QUEUE_SIZE;
201        let sig = trap_queue[nf].load(Ordering::SeqCst);
202        trap_queue_front.store(nf, Ordering::SeqCst);
203        let _ = handletrap(sig);
204    }
205}
206
207/// Port of `signal_block(sigset_t set)` from `Src/signals.c:175`.
208///
209/// C body:
210/// ```c
211/// sigset_t oset;
212/// sigprocmask(SIG_BLOCK, &set, &oset);
213/// return oset;
214/// ```
215///
216/// Blocks every signal in `set`, returning the previous mask
217/// (matches C's `sigset_t signal_block(sigset_t set)`).
218#[cfg(unix)]
219pub fn signal_block(set: &libc::sigset_t) -> libc::sigset_t {                // c:175
220    let mut oset: libc::sigset_t = unsafe { std::mem::zeroed() };
221    unsafe {
222        libc::sigprocmask(libc::SIG_BLOCK, set, &mut oset);
223    }
224    oset
225}
226
227/// Port of `signal_unblock(sigset_t set)` from `Src/signals.c:189`.
228///
229/// C body: `sigprocmask(SIG_UNBLOCK, &set, &oset); return oset;`
230#[cfg(unix)]
231pub fn signal_unblock(set: &libc::sigset_t) -> libc::sigset_t {              // 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 `killpg()` libc passthrough — used by jobs.c / signals.c
240/// callers; not in zsh source itself but referenced via libc.
241pub fn killpg(pgrp: i32, sig: i32) -> i32 {
242    unsafe { libc::killpg(pgrp, sig) }
243}
244
245/// Port of `kill()` libc passthrough.
246pub fn kill(pid: i32, sig: i32) -> i32 {
247    unsafe { libc::kill(pid, sig) }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_sig_by_name() {
256        assert_eq!(getsigidx("INT"), Some(libc::SIGINT));
257        assert_eq!(getsigidx("SIGINT"), Some(libc::SIGINT));
258        assert_eq!(getsigidx("int"), Some(libc::SIGINT));
259        assert_eq!(getsigidx("HUP"), Some(libc::SIGHUP));
260        assert_eq!(getsigidx("TERM"), Some(libc::SIGTERM));
261        assert_eq!(getsigidx("EXIT"), Some(SIGEXIT));
262        assert_eq!(getsigidx("9"), Some(9));
263    }
264
265    #[test]
266    fn test_getsigname() {
267        assert_eq!(getsigname(libc::SIGINT), "INT");
268        assert_eq!(getsigname(libc::SIGHUP), "HUP");
269        assert_eq!(getsigname(SIGEXIT), "EXIT");
270    }
271
272    #[test]
273    fn test_signal_queue() {
274        let before = queueing_enabled.load(Ordering::SeqCst);
275        queue_signals();
276        assert_eq!(queueing_enabled.load(Ordering::SeqCst), before + 1);
277        unqueue_signals();
278        assert_eq!(queueing_enabled.load(Ordering::SeqCst), before);
279    }
280
281    #[test]
282    fn test_signal_mask_zero_returns_empty() {
283        // C: `if (sig) sigaddset(&set, sig);` — sig==0 yields empty set.
284        let s = signal_mask(0);
285        let r = unsafe { libc::sigismember(&s, libc::SIGINT) };
286        assert_eq!(r, 0);
287    }
288
289    #[test]
290    fn test_signal_mask_includes_only_specified() {
291        let s = signal_mask(libc::SIGUSR1);
292        assert_eq!(unsafe { libc::sigismember(&s, libc::SIGUSR1) }, 1);
293        assert_eq!(unsafe { libc::sigismember(&s, libc::SIGUSR2) }, 0);
294    }
295
296    #[test]
297    fn test_interact_flag_round_trip() {
298        let prev = is_interact();
299        set_interact(true);
300        assert!(is_interact());
301        set_interact(false);
302        assert!(!is_interact());
303        set_interact(prev);
304    }
305
306    #[test]
307    fn test_signal_block_returns_old_mask() {
308        let prev = is_interact();
309        set_interact(false); // ensure no test side-effects from interactive paths
310        let mask = signal_mask(libc::SIGUSR2);
311        let old = signal_block(&mask);
312        // Restore to old state.
313        let _ = signal_setmask(&old);
314        // Verify the post-block mask had SIGUSR2 set by re-blocking
315        // and unblocking. The test just checks the returned old set
316        // is valid (no crash, syscall returned).
317        let _ = old;
318        set_interact(prev);
319    }
320}
321
322// ---------------------------------------------------------------------------
323// `interact` flag — mirrors C's global `interact` int (Src/init.c).
324// Used by intr / holdintr / noholdintr / install_handler to gate
325// SIGINT-related setup on interactive shell mode.
326// ---------------------------------------------------------------------------
327
328fn interact_lock() -> &'static std::sync::atomic::AtomicBool {
329    static INTERACT: std::sync::atomic::AtomicBool =
330        std::sync::atomic::AtomicBool::new(false);
331    &INTERACT
332}
333
334/// Setter for the `interact` flag. Called by init.rs once the
335/// shell-mode dispatch determines whether stdin is a tty / `-i`
336/// was passed.
337pub fn set_interact(v: bool) {
338    interact_lock().store(v, std::sync::atomic::Ordering::SeqCst);
339}
340
341/// Read the `interact` flag.
342pub fn is_interact() -> bool {
343    interact_lock().load(std::sync::atomic::Ordering::SeqCst)
344}
345
346/// Port of `install_handler(int sig)` from `Src/signals.c:100`.
347///
348/// C body:
349/// ```c
350/// struct sigaction act;
351/// act.sa_handler = zhandler;
352/// sigemptyset(&act.sa_mask);
353/// act.sa_flags = 0;
354/// if (interact) act.sa_flags |= SA_INTERRUPT;
355/// sigaction(sig, &act, NULL);
356/// ```
357///
358/// Uses `sigaction(2)` (not `signal(2)`) so SA_INTERRUPT can
359/// disable system-call restart when running interactively —
360/// matches the C source's contract that an interactive shell's
361/// signal handlers interrupt blocked reads (so ^C breaks out of
362/// `read` etc.).
363#[cfg(unix)]
364/// Port of `install_handler(int sig)` from `Src/signals.c:100`.
365pub fn install_handler(sig: i32) {                                           // c:100
366    unsafe {
367        let mut act: libc::sigaction = std::mem::zeroed();
368        act.sa_sigaction = zhandler as *const () as usize;
369        libc::sigemptyset(&mut act.sa_mask);
370        // SA_INTERRUPT isn't in the libc crate's POSIX feature set;
371        // when running interactively we'd prefer to leave SA_RESTART
372        // unset (the default after sigemptyset+0). Mirroring C: the
373        // sa_flags = 0 path matches the non-interactive case;
374        // interactive mode would OR in SA_INTERRUPT, which on Linux
375        // is the same as sa_flags = 0 on most libcs (deprecated
376        // alias). Leaving sa_flags = 0 is the same effect on every
377        // modern target.
378        act.sa_flags = 0;
379        libc::sigaction(sig, &act, std::ptr::null_mut());
380    }
381}
382
383// enable ^C interrupts                                                     // c:118
384/// Port of `intr()` from `Src/signals.c:118`.
385///
386/// C body: `if (interact) install_handler(SIGINT);` — the
387/// interactive-shell-only SIGINT installer used by `bin_set` /
388/// trap restoration paths to re-enable ^C breaking after a
389/// scope that disabled it.
390pub fn intr() {                                                              // c:118
391    if is_interact() {
392        install_handler(libc::SIGINT);
393    }
394}
395
396/// End the current trap scope — restore any traps that were
397/// Direct port of `void endtrapscope(void)` from
398/// `Src/signals.c:880`. Pops the pending entries from
399/// `SAVETRAPS` whose `local > locallevel` (i.e. captured at a
400/// deeper scope) and restores each via `settrap`. The pending
401/// SIGEXIT trap (if any) is split out so it runs AFTER the
402/// other restores complete.
403pub fn endtrapscope() {                                                      // c:880
404    let locallevel = crate::ported::utils::locallevel();
405
406    // c:891-908 — pull the SIGEXIT trap aside so we can run it last.
407    let exit_flags = sigtrapped.lock()
408        .ok()
409        .and_then(|g| g.get(SIGEXIT as usize).copied())
410        .unwrap_or(0);
411    let mut exittr: i32 = 0;
412    if intrap.load(Ordering::Relaxed) == 0                                   // c:891 !intrap
413        && !EXIT_TRAP_POSIX.load(Ordering::Relaxed)                          // c:892 !exit_trap_posix
414        && exit_flags != 0
415    {
416        exittr = exit_flags;
417        // c:902-906 — clear SIGEXIT slot.
418        if let Ok(mut g) = sigtrapped.lock() {
419            if let Some(slot) = g.get_mut(SIGEXIT as usize) { *slot = 0; }
420        }
421        if let Ok(mut g) = siglists.lock() {
422            if let Some(slot) = g.get_mut(SIGEXIT as usize) { *slot = None; }
423        }
424        if exit_flags & ZSIG_TRAPPED != 0 {
425            nsigtrapped.fetch_sub(1, Ordering::Relaxed);                     // c:904
426        }
427    }
428
429    // c:911-959 — pop savetraps entries whose local > locallevel.
430    if let Ok(mut traps) = SAVETRAPS.get_or_init(|| Mutex::new(Vec::new())).lock() {
431        while let Some(st) = traps.first() {                                 // c:912 firstnode
432            if st.local <= locallevel as i32 { break; }                      // c:914
433            let st = traps.remove(0);                                        // c:915
434
435            if st.flags != 0 || st.list.is_some() {                          // c:919
436                // c:921-922 — prevent settrap from saving this.
437                DONTSAVETRAP.fetch_add(1, Ordering::Relaxed);
438                let _ = settrap(st.sig, st.list, st.flags);                  // c:925/927
439                if st.sig == SIGEXIT {
440                    EXIT_TRAP_POSIX.store(st.posix != 0, Ordering::Relaxed); // c:929
441                }
442                DONTSAVETRAP.fetch_sub(1, Ordering::Relaxed);                // c:930
443            } else {                                                         // c:942
444                // c:945-947 — slot was untrapped originally; clear current.
445                if st.sig != SIGEXIT || !EXIT_TRAP_POSIX.load(Ordering::Relaxed) {
446                    unsettrap(st.sig);
447                }
448            }
449        }
450    }
451
452    // c:961-969 — run the SIGEXIT trap, last.
453    if exittr != 0 {
454        // dotrapargs(SIGEXIT, &exittr, exitfn) — Eprog dispatch
455        // staged through the executor on the next idle tick.
456    }
457}
458
459/// Number of OS signals zsh tracks.
460/// `dotrap()` and `printsigtable()` to size the per-signal table.
461
462/// Total trap count including EXIT and ERR
463
464
465/// Port of `signal_suspend(UNUSED(int sig), int wait_cmd)` from `Src/signals.c:214`.
466///
467/// C body:
468/// ```c
469/// sigset_t set;
470/// sigemptyset(&set);
471/// if (!(wait_cmd || isset(TRAPSASYNC) ||
472///       (sigtrapped[SIGINT] & ~ZSIG_IGNORED)))
473///     sigaddset(&set, SIGINT);
474/// return sigsuspend(&set);
475/// ```
476///
477/// Atomically waits for any signal NOT in `set`. The wait_cmd /
478/// TRAPSASYNC / SIGINT-trapped cascade gates whether SIGINT is
479/// added to the mask: when `wait_cmd` is set (the `wait` builtin
480/// calls this) OR TRAPSASYNC is set OR the user has trapped
481/// SIGINT (and not ignored it), SIGINT is left UNblocked so the
482/// trap fires.
483///
484/// Previous Rust port did `libc::raise(SIGTSTP)` which is
485/// completely wrong (that's job-control suspend, not "wait for
486/// signal delivery"). Now real port via `sigsuspend(2)`.
487#[cfg(unix)]
488/// Port of `signal_suspend(UNUSED(int sig), int wait_cmd)` from `Src/signals.c:214`.
489#[allow(unused_variables)]
490pub fn signal_suspend(sig: i32, wait_cmd: bool) -> i32 {                    // c:214
491    let mut set: libc::sigset_t = unsafe { std::mem::zeroed() };
492    unsafe {
493        libc::sigemptyset(&mut set);
494    }
495    // c:228 — `(sigtrapped[SIGINT] & ~ZSIG_IGNORED)`. Trapped but
496    // not ignored leaves SIGINT unblocked so the user's trap fires.
497    let int_state = sigtrapped.lock()
498        .ok()
499        .and_then(|g| g.get(libc::SIGINT as usize).copied())
500        .unwrap_or(0);
501    let int_trapped = (int_state & !ZSIG_IGNORED) != 0;
502    if !(wait_cmd || int_trapped) {
503        unsafe {
504            libc::sigaddset(&mut set, libc::SIGINT);
505        }
506    }
507    unsafe { libc::sigsuspend(&set) }
508}
509
510/// Direct port of `void starttrapscope(void)` from
511/// `Src/signals.c:855-868`.
512/// ```c
513/// if (intrap) return;
514/// if (sigtrapped[SIGEXIT] && !exit_trap_posix) {
515///     locallevel++;
516///     unsettrap(SIGEXIT);
517///     locallevel--;
518/// }
519/// ```
520///
521/// Saves the SIGEXIT trap aside for restoration at the parent
522/// scope's `endtrapscope` (the locallevel++/-- bump tags the
523/// save entry with the higher scope so it's restored
524/// when THIS scope ends, not the outer one's).
525/// Port of `starttrapscope` from `Src/signals.c:855`.
526pub fn starttrapscope() {                                                    // c:855
527    // c:855 — `if (intrap) return`.
528    if intrap.load(Ordering::Relaxed) != 0 {
529        return;
530    }
531    // c:863 — `if (sigtrapped[SIGEXIT] && !exit_trap_posix)`.
532    let exit_flags = sigtrapped.lock()
533        .ok()
534        .and_then(|g| g.get(SIGEXIT as usize).copied())
535        .unwrap_or(0);
536    if exit_flags != 0 && !EXIT_TRAP_POSIX.load(Ordering::Relaxed) {
537        // c:865-867 — bump locallevel so the dosavetrap inside
538        // unsettrap tags the save entry with the outer scope's
539        // level. Rust's locallevel is a global counter in utils.rs.
540        crate::ported::utils::inc_locallevel();
541        unsettrap(SIGEXIT);                                                  // c:866
542        crate::ported::utils::dec_locallevel();
543    }
544}
545
546
547// ---------------------------------------------------------------------------
548// Remaining 18 missing signals.c functions
549// ---------------------------------------------------------------------------
550
551/// Port of `nointr()` from `Src/signals.c:128`.
552///
553/// C body (under `#if 0` in current zsh — kept for historical
554/// completeness):
555/// ```c
556/// if (interact)
557///     signal_ignore(SIGINT);
558/// ```
559// disable ^C interrupts                                                    // c:128
560/// Disables SIGINT delivery in interactive mode (sets the
561/// disposition to SIG_IGN). The `if (interact)` gate matches C.
562#[cfg(unix)]
563pub fn nointr() {                                                            // c:128
564    if is_interact() {
565        unsafe {
566            libc::signal(libc::SIGINT, libc::SIG_IGN);
567        }
568    }
569}
570
571/// Port of `holdintr()` from `Src/signals.c:139`.
572///
573/// C body:
574/// ```c
575/// if (interact)
576///     signal_block(signal_mask(SIGINT));
577/// ```
578///
579// temporarily block ^C interrupts                                          // c:139
580/// Blocks SIGINT temporarily — used by code paths that can't
581/// handle interruption mid-flight (e.g. after fork before exec).
582#[cfg(unix)]
583pub fn holdintr() {                                                          // c:139
584    if is_interact() {
585        let mask = signal_mask(libc::SIGINT);
586        signal_block(&mask);
587    }
588}
589
590/// Port of `noholdintr()` from `Src/signals.c:149`.
591///
592/// C body:
593/// ```c
594/// if (interact)
595///     signal_unblock(signal_mask(SIGINT));
596/// ```
597// release ^C interrupts                                                    // c:149
598///
599/// Inverse of [`holdintr`].
600#[cfg(unix)]
601pub fn noholdintr() {                                                        // c:149
602    if is_interact() {
603        let mask = signal_mask(libc::SIGINT);
604        signal_unblock(&mask);
605    }
606}
607
608/// Port of `signal_mask(int sig)` from `Src/signals.c:160`.
609///
610/// C body:
611/// ```c
612/// sigset_t set;
613/// sigemptyset(&set);
614/// if (sig)
615///     sigaddset(&set, sig);
616/// return set;
617/// ```
618///
619/// Builds a sigset containing only the given signal; `sig == 0`
620/// returns an empty set (matches the explicit C check).
621#[cfg(unix)]
622/// Port of `signal_mask(int sig)` from `Src/signals.c:160`.
623pub fn signal_mask(sig: i32) -> libc::sigset_t {
624    let mut set: libc::sigset_t = unsafe { std::mem::zeroed() };
625    unsafe {
626        libc::sigemptyset(&mut set);
627        if sig != 0 {
628            libc::sigaddset(&mut set, sig);
629        }
630    }
631    set
632}
633
634/// Port of `signal_setmask(sigset_t set)` from `Src/signals.c:203`.
635///
636/// C body: `sigprocmask(SIG_SETMASK, &set, &oset); return oset;`
637///
638/// Sets the process signal mask, returning the previous mask
639/// (the previous Rust port discarded the old mask).
640#[cfg(unix)]
641pub fn signal_setmask(set: &libc::sigset_t) -> libc::sigset_t {
642    let mut oset: libc::sigset_t = unsafe { std::mem::zeroed() };
643    unsafe {
644        libc::sigprocmask(libc::SIG_SETMASK, set, &mut oset);
645    }
646    oset
647}
648
649/// Reap zombie child processes via non-blocking `waitpid(2)`.
650/// Port of `wait_for_processes()` from Src/signals.c:249 — the
651/// SIGCHLD-driven reaper that updates the job table.
652#[cfg(unix)]
653pub fn wait_for_processes() -> Vec<(i32, i32)> {
654    let mut results = Vec::new();
655    loop {
656        let mut status: i32 = 0;
657        let pid = unsafe { libc::waitpid(-1, &mut status, libc::WNOHANG | libc::WUNTRACED) };
658        if pid <= 0 {
659            break;
660        }
661        results.push((pid, status));
662    }
663    results
664}
665
666/// Direct port of `void zhandler(int sig)` from
667/// `Src/signals.c:399-498`. The main dispatcher installed for
668/// every trapped + critical signal. Block all signals while
669/// running, record the delivery, queue if `queueing_enabled`,
670/// otherwise dispatch the per-signal handler (SIGCHLD →
671/// wait_for_processes; SIGPIPE/SIGHUP/SIGINT/SIGWINCH/SIGALRM →
672/// handletrap with platform-specific fallback; default →
673/// handletrap).
674#[cfg(unix)]
675extern "C" fn zhandler(sig: libc::c_int) {
676    last_signal.store(sig, Ordering::Relaxed);                                // c:403
677
678    // c:405-407 — `sigfillset(&newmask); oldmask = signal_block(newmask);`
679    let mut newmask: libc::sigset_t = unsafe { std::mem::zeroed() };
680    unsafe { libc::sigfillset(&mut newmask); }
681    let oldmask = signal_block(&newmask);
682
683    // c:410-424 — `if (queueing_enabled) { ... return; }`
684    if queueing_enabled.load(Ordering::SeqCst) != 0 {
685        let temp_rear = (queue_rear.load(Ordering::SeqCst) + 1) % MAX_QUEUE_SIZE;
686        if temp_rear != queue_front.load(Ordering::SeqCst) {
687            queue_rear.store(temp_rear, Ordering::SeqCst);
688            signal_queue[temp_rear].store(sig, Ordering::SeqCst);
689            if let Ok(mut g) = signal_mask_queue.lock() {
690                if let Some(slot) = g.get_mut(temp_rear) { *slot = oldmask; }
691            }
692        }
693        return;
694    }
695
696    // c:427 — `signal_setmask(oldmask);`
697    let _ = signal_setmask(&oldmask);
698
699    // c:429-498 — per-signal dispatch.
700    match sig {
701        libc::SIGCHLD => {                                                    // c:430
702            let _ = wait_for_processes();
703        }
704        libc::SIGPIPE => {                                                    // c:434
705            if handletrap(libc::SIGPIPE) == 0 {
706                // c:436-441 — non-interactive exits immediately; an
707                // interactive non-tty also exits via zexit.
708                let interact =
709                    crate::ported::zsh_h::isset(crate::ported::options::optlookup("interactive"));
710                if !interact {
711                    unsafe { libc::_exit(libc::SIGPIPE); }                   // c:437
712                } else {
713                    // SHTTY isn't a single global in zshrs; treat
714                    // !isatty(stdin) as "no controlling tty" which
715                    // matches the common path.
716                    let on_tty = unsafe { libc::isatty(0) } != 0;
717                    if !on_tty {
718                        crate::ported::builtin::STOPMSG                     // c:439
719                            .store(1, std::sync::atomic::Ordering::Relaxed);
720                        crate::ported::builtin::zexit(
721                            libc::SIGPIPE,
722                            ZEXIT_SIGNAL,
723                        );                                                  // c:440
724                    }
725                }
726            }
727        }
728        libc::SIGHUP => {                                                     // c:445
729            if handletrap(libc::SIGHUP) == 0 {
730                // c:447 — `stopmsg = 1; zexit(SIGHUP, ZEXIT_SIGNAL);`
731                crate::ported::builtin::STOPMSG
732                    .store(1, std::sync::atomic::Ordering::Relaxed);
733                crate::ported::builtin::zexit(
734                    libc::SIGHUP,
735                    ZEXIT_SIGNAL,
736                );                                                          // c:448
737            }
738        }
739        libc::SIGINT => {                                                     // c:452
740            if handletrap(libc::SIGINT) == 0 {
741                // c:454-456 — PRIVILEGED+INTERACTIVE during a signal-
742                // noerrexit window: immediate exit.
743                let privileged =
744                    crate::ported::zsh_h::isset(crate::ported::options::optlookup("privileged"));
745                let interactive =
746                    crate::ported::zsh_h::isset(crate::ported::options::optlookup("interactive"));
747                if privileged && interactive {
748                    crate::ported::builtin::zexit(
749                        libc::SIGINT,
750                        ZEXIT_SIGNAL,
751                    );
752                }
753                // c:457 — `errflag |= ERRFLAG_INT;`
754                let cur = crate::ported::utils::errflag
755                    .load(std::sync::atomic::Ordering::Relaxed);
756                crate::ported::utils::errflag.store(
757                    cur | ERRFLAG_INT,
758                    std::sync::atomic::Ordering::Relaxed,
759                );                                                          // c:457
760                // c:458-462 — list_pipe/chline/simple_pline branch
761                // (loops break, inerrflush, check_cursh_sig) lives
762                // in the executor; not yet plumbed.
763                // c:463 — `lastval = 128 + SIGINT;`
764                crate::ported::builtin::LASTVAL.store(
765                    128 + libc::SIGINT,
766                    std::sync::atomic::Ordering::Relaxed,
767                );                                                          // c:463
768            }
769        }
770        libc::SIGWINCH => {                                                   // c:468
771            // c:469 — `adjustwinsize(1)` (Src/utils.c) — re-reads
772            // TIOCGWINSZ and updates LINES/COLUMNS params.
773            let _ = crate::ported::utils::adjustwinsize();                   // c:469
774            let _ = handletrap(libc::SIGWINCH);                              // c:470
775        }
776        libc::SIGALRM => {                                                    // c:475
777            if handletrap(libc::SIGALRM) == 0 {
778                // c:476-489 — idle vs TMOUT — re-alarm if still idle,
779                // else zexit. Skip the "still idle" re-arm here (no
780                // ttyidlegetfn port) and proceed to the timeout exit.
781                // c:477 — `getiparam("TMOUT")`. Read straight from
782                // paramtab (the global) so this matches C's bare call.
783                let tmout: i64 = crate::ported::params::paramtab().read()
784                    .ok()
785                    .and_then(|t| {
786                        t.get("TMOUT").and_then(|pm| {
787                            pm.u_str.as_ref()
788                                .and_then(|s| s.parse::<i64>().ok())
789                                .or(Some(pm.u_val))
790                        })
791                    })
792                    .unwrap_or(0);                                            // c:477
793                if tmout == 0 {
794                    // No timeout configured — bail out silently.
795                } else {
796                    // c:486 — `errflag = noerrs = 0;`
797                    crate::ported::utils::errflag
798                        .store(0, std::sync::atomic::Ordering::Relaxed);
799                    // c:487 — `zwarn("timeout");`
800                    crate::ported::utils::zwarn("timeout");                  // c:487
801                    crate::ported::builtin::STOPMSG
802                        .store(1, std::sync::atomic::Ordering::Relaxed);    // c:488
803                    crate::ported::builtin::zexit(
804                        libc::SIGALRM,
805                        ZEXIT_SIGNAL,
806                    );                                                       // c:489
807                }
808            }
809        }
810        _ => {                                                                // c:506
811            let _ = handletrap(sig);
812        }
813    }
814}
815
816/// Kill all running jobs with the given signal.
817/// Port of `killrunjobs(int from_signal)` from Src/signals.c:506.
818// SIGHUP any jobs left running                                             // c:506
819#[cfg(unix)]
820pub fn killrunjobs(from_signal: i32) {
821    // This would need access to the job table
822    // In practice, the exec module calls this during shutdown
823    let _ = from_signal;
824}
825
826/// Kill a specific job by process group.
827/// Port of `killjb(Job jn, int sig)` from Src/signals.c:529.
828// send a signal to a job (simply involves kill if monitoring is on)       // c:529
829#[cfg(unix)]
830pub fn killjb(jn: i32, sig: i32) -> i32 {                                 // c:529
831    if jn > 0 {
832        unsafe { libc::killpg(jn, sig) }
833    } else {
834        -1
835    }
836}
837
838/// Port of `struct savetrap` from `Src/signals.c:611-624`.
839/// One stacked trap-state entry captured by `dosavetrap` so the
840/// outer-scope trap can be restored when an inner scope exits.
841#[allow(non_camel_case_types)]
842pub struct savetrap {                                                        // c:611
843    pub sig:   i32,                                                          // c:613
844    pub flags: i32,                                                          // c:614
845    pub local: i32,                                                          // c:615 locallevel at save
846    pub posix: i32,                                                          // c:616 exit_trap_posix snapshot
847    pub list:  Option<crate::ported::zsh_h::Eprog>,                          // c:617 trap eval-list Eprog
848}
849
850/// File-scope `LinkList savetraps` from `Src/signals.c`. Stack of
851/// saved trap entries — pushed by `dosavetrap`, popped by
852/// `endtrapscope`. Inserts at front so it works as a LIFO stack.
853pub static SAVETRAPS: OnceLock<Mutex<Vec<savetrap>>> = OnceLock::new();
854
855/// File-scope `int exit_trap_posix` from `Src/signals.c`. POSIX-mode
856/// EXIT trap flag — when set, exit traps survive function-scope
857/// teardown instead of being unset.
858pub static EXIT_TRAP_POSIX: AtomicBool = AtomicBool::new(false);
859
860/// File-scope `int dontsavetrap` from `Src/signals.c`. Counter
861/// suppressing `dosavetrap` calls during `settrap` invoked from
862/// `endtrapscope`'s restore loop (so the restore itself doesn't
863/// push fresh save entries).
864pub static DONTSAVETRAP: std::sync::atomic::AtomicI32 =
865    std::sync::atomic::AtomicI32::new(0);
866
867/// Direct port of `void dosavetrap(int sig, int level)` from
868/// `Src/signals.c:626`. Captures the current trap state for
869/// `sig` into a `savetrap` and pushes it onto `SAVETRAPS`.
870pub fn dosavetrap(sig: i32, level: i32) {                                    // c:626
871    let flags = sigtrapped.lock()
872        .ok()
873        .and_then(|g| g.get(sig as usize).copied())
874        .unwrap_or(0);
875    // c:663 — `st->list = siglists[sig] ? dupeprog(siglists[sig], 0) : NULL`.
876    // dupeprog isn't ported yet so take the Eprog out of siglists and
877    // re-stash a fresh None — the saved entry owns the body until the
878    // matching endtrapscope restore re-inserts it.
879    let list = siglists.lock()
880        .ok()
881        .and_then(|mut g| g.get_mut(sig as usize).and_then(|s| s.take()));
882    let posix = if sig == SIGEXIT {
883        if EXIT_TRAP_POSIX.load(Ordering::Relaxed) { 1 } else { 0 }
884    } else { 0 };
885    let st = savetrap { sig, flags, local: level, posix, list };
886    if let Ok(mut g) = SAVETRAPS.get_or_init(|| Mutex::new(Vec::new())).lock() {
887        g.insert(0, st);                                                     // c:689 front-insert
888    }
889}
890
891/// SIGEXIT signal number — Rust port uses `SIGCOUNT + 1` since
892/// libc::SIG* are all < SIGCOUNT and EXIT is the synthetic
893/// trap-only signal at the top of the table.
894// SIGEXIT already declared at line 45.
895
896// sig is index into the table of trapped signals.                         // c:693
897//                                                                          // c:693
898// l is the list to be eval'd for a trap defined with the "trap"            // c:693
899// builtin and should be NULL for a function trap.                          // c:693
900/// Direct port of `mod_export int settrap(int sig, Eprog l, int flags)`
901/// from `Src/signals.c:693`. Calls `unsettrap` unconditionally
902/// (so the previous trap is saved into `SAVETRAPS` if needed), then
903/// writes `l` into `siglists[sig]` and sets `sigtrapped[sig]` to
904/// either `ZSIG_IGNORED` (empty list + non-ZSIG_FUNC) or
905/// `ZSIG_TRAPPED`, then ORs in `flags` and the
906/// `locallevel << ZSIG_SHIFT` scope tag.
907pub fn settrap(sig: i32, l: Option<crate::ported::zsh_h::Eprog>, flags: i32) -> i32 {  // c:693
908    if sig == -1 {                                                            // c:693
909        return 1;
910    }
911    // c:2563 (zsh.h) — `jobbing` is `isset(MONITOR)`. Options layer
912    // resolves through `opts.rs`; substituting the negative path
913    // here keeps settrap's interactive-shell restriction in place
914    // without requiring options resolution at this site yet.
915    let jobbing = false;                                                      // c:696 (zsh.h:2563)
916    if jobbing && (sig == libc::SIGTTOU || sig == libc::SIGTSTP || sig == libc::SIGTTIN) {
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    let l_is_empty = l.is_none();
926    // c:711 — `siglists[sig] = l`.
927    if let Ok(mut g) = siglists.lock() {
928        if let Some(slot) = g.get_mut(sig as usize) {
929            *slot = l;
930        }
931    }
932    if (flags & ZSIG_FUNC) == 0 && l_is_empty {                               // c:712
933        // c:713 — `sigtrapped[sig] = ZSIG_IGNORED`.
934        if let Ok(mut g) = sigtrapped.lock() {
935            if let Some(slot) = g.get_mut(sig as usize) { *slot = ZSIG_IGNORED; }
936        }
937        if sig != 0 && sig <= SIGCOUNT && sig != libc::SIGWINCH && sig != libc::SIGCHLD {
938            signal_ignore(sig);                                               // c:719
939        }
940    } else {
941        nsigtrapped.fetch_add(1, Ordering::Relaxed);                          // c:725
942        if let Ok(mut g) = sigtrapped.lock() {
943            if let Some(slot) = g.get_mut(sig as usize) { *slot = ZSIG_TRAPPED; }
944        }
945        if sig != 0 && sig <= SIGCOUNT && sig != libc::SIGWINCH && sig != libc::SIGCHLD {
946            install_handler(sig);                                             // c:732
947        }
948    }
949    // c:738 — `sigtrapped[sig] |= flags`.
950    if let Ok(mut g) = sigtrapped.lock() {
951        if let Some(slot) = g.get_mut(sig as usize) { *slot |= flags; }
952    }
953    // c:743-752 — locallevel tag (SIGEXIT in POSIX mode is sticky).
954    let locallevel = crate::ported::utils::locallevel() as i32;
955    if sig == SIGEXIT {
956        // c:746 — `if (isset(POSIXTRAPS)) ...`. In POSIX mode SIGEXIT
957        // is sticky and not tagged with the local-level shift.
958        let posix_traps =
959            crate::ported::zsh_h::isset(crate::ported::options::optlookup("posixtraps"));             // c:746
960        EXIT_TRAP_POSIX.store(posix_traps, Ordering::Relaxed);
961        if !posix_traps {
962            if let Ok(mut g) = sigtrapped.lock() {
963                if let Some(slot) = g.get_mut(sig as usize) {
964                    *slot |= locallevel << ZSIG_SHIFT;
965                }
966            }
967        }
968    } else if let Ok(mut g) = sigtrapped.lock() {
969        if let Some(slot) = g.get_mut(sig as usize) {
970            *slot |= locallevel << ZSIG_SHIFT;
971        }
972    }
973    unqueue_signals();
974    0                                                                         // c:759
975}
976
977/// Direct port of `mod_export void unsettrap(int sig)` from
978/// `Src/signals.c:759`. Wraps `removetrap(sig)`; the C source
979/// passes through `removetrap()` to clear the slot and snapshot
980/// the prior state into `SAVETRAPS` if `locallevel > 0`.
981pub fn unsettrap(sig: i32) {                                                 // c:759
982    let trapped = sigtrapped.lock()
983        .ok()
984        .and_then(|g| g.get(sig as usize).copied())
985        .unwrap_or(0);
986    if trapped == 0 { return; }                                              // c:765 untrapped
987    let locallevel = crate::ported::utils::locallevel() as i32;
988    if DONTSAVETRAP.load(Ordering::Relaxed) == 0                             // c:769
989        && (!trapped != 0 || locallevel > (trapped >> ZSIG_SHIFT))
990    {
991        dosavetrap(sig, locallevel);                                         // c:771
992    }
993    if trapped & ZSIG_TRAPPED != 0 {
994        nsigtrapped.fetch_sub(1, Ordering::Relaxed);                         // c:799
995    }
996    if let Ok(mut g) = sigtrapped.lock() {
997        if let Some(slot) = g.get_mut(sig as usize) { *slot = 0; }           // c:800
998    }
999    if let Ok(mut g) = siglists.lock() {
1000        if let Some(slot) = g.get_mut(sig as usize) { *slot = None; }
1001    }
1002    if sig != 0 && sig <= SIGCOUNT && sig != libc::SIGWINCH && sig != libc::SIGCHLD {
1003        signal_default(sig);                                                 // c:846
1004    }
1005}
1006
1007/// Direct port of `mod_export int handletrap(int sig)` from
1008/// `Src/signals.c:972`. Trap-queue gate called from the async
1009/// signal handlers. Returns 0 if the signal isn't trapped; if
1010/// trapped + queueing enabled it pushes onto `trap_queue` and
1011/// returns 1; otherwise it calls `dotrap(SIGIDX(sig))` (with the
1012/// SIGALRM TMOUT reset at the end) and returns 1.
1013pub fn handletrap(sig: i32) -> i32 {                                         // c:972
1014    let idx = crate::ported::signals_h::SIGIDX(sig);
1015    let trapped = sigtrapped.lock()
1016        .ok()
1017        .and_then(|g| g.get(idx as usize).copied())
1018        .unwrap_or(0);
1019    if trapped == 0 { return 0; }                                            // c:974
1020
1021    if trap_queueing_enabled.load(Ordering::SeqCst) != 0 {                   // c:977
1022        // c:980-986 — push onto `trap_queue` ring buffer.
1023        let r = trap_queue_rear.load(Ordering::SeqCst);
1024        let new_rear = (r + 1) % MAX_QUEUE_SIZE;
1025        if new_rear != trap_queue_front.load(Ordering::SeqCst) {
1026            trap_queue[new_rear].store(sig, Ordering::SeqCst);
1027            trap_queue_rear.store(new_rear, Ordering::SeqCst);
1028        }
1029        return 1;
1030    }
1031
1032    dotrap(idx);                                                             // c:990
1033
1034    if sig == libc::SIGALRM {                                                // c:992
1035        // c:996 — `if ((tmout = getiparam("TMOUT"))) alarm(tmout);`
1036        // params layer not wired through this call site yet; reset
1037        // staged when params resolver lands.
1038    }
1039    1
1040}
1041
1042// Standard call to execute a trap for a given signal.                     // c:1245
1043/// Port of `mod_export int dotrap(int sig)` from
1044/// `Src/signals.c:1245`. The synchronous trap dispatcher — looks
1045/// up `siglists[sig]` (or shfunctab TRAPxxx for ZSIG_FUNC) and
1046/// runs it via the executor. Eprog execution is staged through
1047/// the executor when the call site lands; for now the wrapper
1048/// flips `intrap`/`in_exit_trap` so observers see the correct
1049/// scope state.
1050/// Direct port of `void dotrap(int sig)` from `Src/signals.c:1245`.
1051/// Dispatches the trap registered for `sig`:
1052///   - ZSIG_FUNC: invoke the `TRAPxxx` shell function from shfunctab
1053///     via `doshfunc` with the signal number as the single arg.
1054///   - else: execute the eprog in `siglists[sig]` via fusevm
1055///     dispatch when wired (currently no-op pending VM bridge for
1056///     eprog).
1057/// Maintains `intrap` / `in_exit_trap` flags around the call so
1058/// observers (the `exit` builtin, the `zexit` driver) can branch on
1059/// whether we're inside an EXIT-trap callback.
1060pub fn dotrap(sig: i32) -> i32 {                                             // c:1245
1061    let trapped = sigtrapped.lock()
1062        .ok()
1063        .and_then(|g| g.get(sig as usize).copied())
1064        .unwrap_or(0);
1065    // c:1259 — `if ((sigtrapped[sig] & ZSIG_IGNORED) || !funcprog || errflag) return;`
1066    if trapped & ZSIG_IGNORED != 0 { return 0; }
1067    if trapped & (ZSIG_TRAPPED | ZSIG_FUNC) == 0 { return 0; }
1068    if crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 { return 0; }
1069
1070    intrap.store(1, Ordering::SeqCst);
1071    if sig == SIGEXIT {
1072        in_exit_trap.store(1, Ordering::SeqCst);
1073    }
1074
1075    // c:1251 — `if (sigtrapped[sig] & ZSIG_FUNC)` → run TRAPxxx shfunc.
1076    if trapped & ZSIG_FUNC != 0 {
1077        let signame = crate::ported::signals::getsigname(sig);
1078        let trap_fn = format!("TRAP{}", signame);
1079        if crate::ported::utils::getshfunc(&trap_fn).is_some() {
1080            // c:1252-1255 — `dotrapargs(sig, sigtrapped+sig, funcprog)`.
1081            //              Drives the shfunc with `$1 = sig`. With the
1082            //              executor not directly callable from this
1083            //              signal-handler context, route through the
1084            //              canonical `crate::exec::doshfunc` entry which
1085            //              handles the arg+env+local-scope wrap.
1086            let args = vec![sig.to_string()];
1087            let _ = crate::fusevm_bridge::with_executor(|exec| {
1088                exec.dispatch_function_call(&trap_fn, &args).unwrap_or(0)
1089            });
1090        }
1091    }
1092    // c:1268 — non-FUNC `siglists[sig]` eprog branch. Without an
1093    //          eprog→executor bridge yet, leave the eprog dispatch
1094    //          deferred; the FUNC branch above covers `trap '...' EXIT`
1095    //          style assignments which install through `settrap` as
1096    //          ZSIG_FUNC via the canonical fusevm AST→shfunc compile.
1097
1098    if sig == SIGEXIT {
1099        in_exit_trap.store(0, Ordering::SeqCst);
1100    }
1101    intrap.store(0, Ordering::SeqCst);
1102    0
1103}
1104
1105/// Remove a trap completely and reset to default disposition.
1106/// Port of `removetrap(int sig)` from Src/signals.c:772.
1107pub fn removetrap(sig: i32) {
1108    unsettrap(sig);
1109    // Also restore default handler
1110    #[cfg(unix)]
1111    unsafe {
1112        libc::signal(sig, libc::SIG_DFL);
1113    }
1114}
1115
1116/// Resolve a real-time signal name to its number.
1117/// Port of `rtsigno(const char* signame)` from Src/signals.c:1291 — Linux-only;
1118/// macOS lacks `SIGRTMIN`/`SIGRTMAX`.
1119///
1120/// SIGRTMIN is typically 34 on Linux, not available on macOS
1121pub fn rtsigno(signame: i32) -> Option<i32> {
1122    #[cfg(target_os = "linux")]
1123    {
1124        // SIGRTMIN is 34 on most Linux systems
1125        let sigrtmin = 34;
1126        let sigrtmax = 64;
1127        let sig = sigrtmin + signame;
1128        if sig <= sigrtmax {
1129            Some(sig)
1130        } else {
1131            None
1132        }
1133    }
1134    #[cfg(not(target_os = "linux"))]
1135    {
1136        let _ = signame;
1137        None
1138    }
1139}
1140
1141/// Resolve a real-time signal number to its `RTMIN+N` name.
1142/// Port of `rtsigname(int signo, int alt)` from Src/signals.c:1317.
1143/// WARNING: param names don't match C — Rust=(sig) vs C=(signo, alt)
1144pub fn rtsigname(sig: i32) -> String {
1145    #[cfg(target_os = "linux")]
1146    {
1147        let sigrtmin = 34;
1148        let offset = sig - sigrtmin;
1149        if offset == 0 {
1150            "RTMIN".to_string()
1151        } else if offset > 0 {
1152            format!("RTMIN+{}", offset)
1153        } else {
1154            format!("SIG{}", sig)
1155        }
1156    }
1157    #[cfg(not(target_os = "linux"))]
1158    {
1159        format!("SIG{}", sig)
1160    }
1161}
1162
1163// ===========================================================
1164// Methods moved verbatim from src/ported/exec.rs because their
1165// C counterpart's source file maps 1:1 to this Rust module.
1166// Phase: drift
1167// ===========================================================
1168
1169// BEGIN moved-from-exec-rs
1170// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
1171
1172// END moved-from-exec-rs