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