Skip to main content

uucore/features/
signals.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5
6// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf pgrp GETFD pfds revents POLLRDBAND POLLERR
7// spell-checker:ignore (vars/signals) ABRT ALRM CHLD SEGV SIGABRT SIGALRM SIGBUS SIGCHLD SIGCONT SIGDANGER SIGEMT SIGFPE SIGHUP SIGILL SIGINFO SIGINT SIGIO SIGIOT SIGKILL SIGMIGRATE SIGMSG SIGPIPE SIGPRE SIGPROF SIGPWR SIGQUIT SIGSEGV SIGSTOP SIGSYS SIGTALRM SIGTERM SIGTRAP SIGTSTP SIGTHR SIGTTIN SIGTTOU SIGURG SIGUSR SIGVIRT SIGVTALRM SIGWINCH SIGXCPU SIGXFSZ STKFLT PWR THR TSTP TTIN TTOU VIRT VTALRM XCPU XFSZ SIGCLD SIGPOLL SIGWAITING SIGAIOCANCEL SIGLWP SIGFREEZE SIGTHAW SIGCANCEL SIGLOST SIGXRES SIGJVM SIGRTMIN SIGRT SIGRTMAX TALRM AIOCANCEL XRES RTMIN RTMAX LTOSTOP
8
9//! This module provides a way to handle signals in a platform-independent way.
10//! It provides a way to convert signal names to their corresponding values and vice versa.
11//! It also provides a way to ignore the SIGINT signal and enable pipe errors.
12
13#[cfg(unix)]
14use nix::errno::Errno;
15#[cfg(any(target_os = "linux", target_os = "android"))]
16use nix::libc;
17#[cfg(unix)]
18use nix::sys::signal::{
19    SaFlags, SigAction, SigHandler, SigHandler::SigDfl, SigHandler::SigIgn, SigSet, Signal,
20    Signal::SIGINT, Signal::SIGPIPE, sigaction, signal,
21};
22
23/// The default signal value.
24pub static DEFAULT_SIGNAL: usize = 15;
25
26/*
27
28Linux Programmer's Manual
29
30 1 HUP      2 INT      3 QUIT     4 ILL      5 TRAP     6 ABRT     7 BUS
31 8 FPE      9 KILL    10 USR1    11 SEGV    12 USR2    13 PIPE    14 ALRM
3215 TERM    16 STKFLT  17 CHLD    18 CONT    19 STOP    20 TSTP    21 TTIN
3322 TTOU    23 URG     24 XCPU    25 XFSZ    26 VTALRM  27 PROF    28 WINCH
3429 POLL    30 PWR     31 SYS
35
36
37*/
38
39/// The list of all signals.
40#[cfg(any(target_os = "linux", target_os = "android", target_os = "redox"))]
41pub static ALL_SIGNALS: [&str; 32] = [
42    "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV",
43    "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU",
44    "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "POLL", "PWR", "SYS",
45];
46
47/*
48
49
50https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/signal.3.html
51
52
53No    Name         Default Action       Description
541     SIGHUP       terminate process    terminal line hangup
552     SIGINT       terminate process    interrupt program
563     SIGQUIT      create core image    quit program
574     SIGILL       create core image    illegal instruction
585     SIGTRAP      create core image    trace trap
596     SIGABRT      create core image    abort program (formerly SIGIOT)
607     SIGEMT       create core image    emulate instruction executed
618     SIGFPE       create core image    floating-point exception
629     SIGKILL      terminate process    kill program
6310    SIGBUS       create core image    bus error
6411    SIGSEGV      create core image    segmentation violation
6512    SIGSYS       create core image    non-existent system call invoked
6613    SIGPIPE      terminate process    write on a pipe with no reader
6714    SIGALRM      terminate process    real-time timer expired
6815    SIGTERM      terminate process    software termination signal
6916    SIGURG       discard signal       urgent condition present on socket
7017    SIGSTOP      stop process         stop (cannot be caught or ignored)
7118    SIGTSTP      stop process         stop signal generated from keyboard
7219    SIGCONT      discard signal       continue after stop
7320    SIGCHLD      discard signal       child status has changed
7421    SIGTTIN      stop process         background read attempted from control terminal
7522    SIGTTOU      stop process         background write attempted to control terminal
7623    SIGIO        discard signal       I/O is possible on a descriptor (see fcntl(2))
7724    SIGXCPU      terminate process    cpu time limit exceeded (see setrlimit(2))
7825    SIGXFSZ      terminate process    file size limit exceeded (see setrlimit(2))
7926    SIGVTALRM    terminate process    virtual time alarm (see setitimer(2))
8027    SIGPROF      terminate process    profiling timer alarm (see setitimer(2))
8128    SIGWINCH     discard signal       Window size change
8229    SIGINFO      discard signal       status request from keyboard
8330    SIGUSR1      terminate process    User defined signal 1
8431    SIGUSR2      terminate process    User defined signal 2
85
86*/
87
88#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
89pub static ALL_SIGNALS: [&str; 32] = [
90    "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "EMT", "FPE", "KILL", "BUS", "SEGV",
91    "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO",
92    "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "INFO", "USR1", "USR2",
93];
94
95/*
96
97     The following signals are defined in NetBSD:
98
99     SIGHUP           1     Hangup
100     SIGINT           2     Interrupt
101     SIGQUIT          3     Quit
102     SIGILL           4     Illegal instruction
103     SIGTRAP          5     Trace/BPT trap
104     SIGABRT          6     Abort trap
105     SIGEMT           7     EMT trap
106     SIGFPE           8     Floating point exception
107     SIGKILL          9     Killed
108     SIGBUS           10    Bus error
109     SIGSEGV          11    Segmentation fault
110     SIGSYS           12    Bad system call
111     SIGPIPE          13    Broken pipe
112     SIGALRM          14    Alarm clock
113     SIGTERM          15    Terminated
114     SIGURG           16    Urgent I/O condition
115     SIGSTOP          17    Suspended (signal)
116     SIGTSTP          18    Suspended
117     SIGCONT          19    Continued
118     SIGCHLD          20    Child exited, stopped or continued
119     SIGTTIN          21    Stopped (tty input)
120     SIGTTOU          22    Stopped (tty output)
121     SIGIO            23    I/O possible
122     SIGXCPU          24    CPU time limit exceeded
123     SIGXFSZ          25    File size limit exceeded
124     SIGVTALRM        26    Virtual timer expired
125     SIGPROF          27    Profiling timer expired
126     SIGWINCH         28    Window size changed
127     SIGINFO          29    Information request
128     SIGUSR1          30    User defined signal 1
129     SIGUSR2          31    User defined signal 2
130     SIGPWR           32    Power fail/restart
131*/
132
133#[cfg(target_os = "netbsd")]
134pub static ALL_SIGNALS: [&str; 33] = [
135    "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "EMT", "FPE", "KILL", "BUS", "SEGV",
136    "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO",
137    "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "INFO", "USR1", "USR2", "PWR",
138];
139
140/*
141
142     The following signals are defined in OpenBSD:
143
144     SIGHUP       terminate process    terminal line hangup
145     SIGINT       terminate process    interrupt program
146     SIGQUIT      create core image    quit program
147     SIGILL       create core image    illegal instruction
148     SIGTRAP      create core image    trace trap
149     SIGABRT      create core image    abort(3) call (formerly SIGIOT)
150     SIGEMT       create core image    emulate instruction executed
151     SIGFPE       create core image    floating-point exception
152     SIGKILL      terminate process    kill program (cannot be caught or
153                                       ignored)
154     SIGBUS       create core image    bus error
155     SIGSEGV      create core image    segmentation violation
156     SIGSYS       create core image    system call given invalid argument
157     SIGPIPE      terminate process    write on a pipe with no reader
158     SIGALRM      terminate process    real-time timer expired
159     SIGTERM      terminate process    software termination signal
160     SIGURG       discard signal       urgent condition present on socket
161     SIGSTOP      stop process         stop (cannot be caught or ignored)
162     SIGTSTP      stop process         stop signal generated from keyboard
163     SIGCONT      discard signal       continue after stop
164     SIGCHLD      discard signal       child status has changed
165     SIGTTIN      stop process         background read attempted from control
166                                       terminal
167     SIGTTOU      stop process         background write attempted to control
168                                       terminal
169     SIGIO        discard signal       I/O is possible on a descriptor (see
170                                       fcntl(2))
171     SIGXCPU      terminate process    CPU time limit exceeded (see
172                                       setrlimit(2))
173     SIGXFSZ      terminate process    file size limit exceeded (see
174                                       setrlimit(2))
175     SIGVTALRM    terminate process    virtual time alarm (see setitimer(2))
176     SIGPROF      terminate process    profiling timer alarm (see
177                                       setitimer(2))
178     SIGWINCH     discard signal       window size change
179     SIGINFO      discard signal       status request from keyboard
180     SIGUSR1      terminate process    user-defined signal 1
181     SIGUSR2      terminate process    user-defined signal 2
182     SIGTHR       discard signal       thread AST
183*/
184
185#[cfg(target_os = "openbsd")]
186pub static ALL_SIGNALS: [&str; 33] = [
187    "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "EMT", "FPE", "KILL", "BUS", "SEGV",
188    "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO",
189    "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "INFO", "USR1", "USR2", "THR",
190];
191
192/*
193     The following signals are defined in Solaris and illumos;
194     (the signals for illumos are the same as Solaris, but illumos still has SIGLWP
195     as well as the alias for SIGLWP (SIGAIOCANCEL)):
196
197     SIGHUP       1       hangup
198     SIGINT       2       interrupt (rubout)
199     SIGQUIT      3       quit (ASCII FS)
200     SIGILL       4       illegal instruction (not reset when caught)
201     SIGTRAP      5       trace trap (not reset when caught)
202     SIGIOT       6       IOT instruction
203     SIGABRT      6       used by abort, replace SIGIOT in the future
204     SIGEMT       7       EMT instruction
205     SIGFPE       8       floating point exception
206     SIGKILL      9       kill (cannot be caught or ignored)
207     SIGBUS       10      bus error
208     SIGSEGV      11      segmentation violation
209     SIGSYS       12      bad argument to system call
210     SIGPIPE      13      write on a pipe with no one to read it
211     SIGALRM      14      alarm clock
212     SIGTERM      15      software termination signal from kill
213     SIGUSR1      16      user defined signal 1
214     SIGUSR2      17      user defined signal 2
215     SIGCLD       18      child status change
216     SIGCHLD      18      child status change alias (POSIX)
217     SIGPWR       19      power-fail restart
218     SIGWINCH     20      window size change
219     SIGURG       21      urgent socket condition
220     SIGPOLL      22      pollable event occurred
221     SIGIO        SIGPOLL socket I/O possible (SIGPOLL alias)
222     SIGSTOP      23      stop (cannot be caught or ignored)
223     SIGTSTP      24      user stop requested from tty
224     SIGCONT      25      stopped process has been continued
225     SIGTTIN      26      background tty read attempted
226     SIGTTOU      27      background tty write attempted
227     SIGVTALRM    28      virtual timer expired
228     SIGPROF      29      profiling timer expired
229     SIGXCPU      30      exceeded cpu limit
230     SIGXFSZ      31      exceeded file size limit
231     SIGWAITING   32      reserved signal no longer used by threading code
232     SIGAIOCANCEL 33      reserved signal no longer used by threading code (formerly SIGLWP)
233     SIGFREEZE    34      special signal used by CPR
234     SIGTHAW      35      special signal used by CPR
235     SIGCANCEL    36      reserved signal for thread cancellation
236     SIGLOST      37      resource lost (eg, record-lock lost)
237     SIGXRES      38      resource control exceeded
238     SIGJVM1      39      reserved signal for Java Virtual Machine
239     SIGJVM2      40      reserved signal for Java Virtual Machine
240     SIGINFO      41      information request
241     SIGRTMIN     ((int)_sysconf(_SC_SIGRT_MIN)) first realtime signal
242     SIGRTMAX     ((int)_sysconf(_SC_SIGRT_MAX)) last realtime signal
243*/
244
245#[cfg(target_os = "solaris")]
246const SIGNALS_SIZE: usize = 46;
247
248#[cfg(target_os = "illumos")]
249const SIGNALS_SIZE: usize = 47;
250
251#[cfg(any(target_os = "solaris", target_os = "illumos"))]
252static ALL_SIGNALS: [&str; SIGNALS_SIZE] = [
253    "HUP",
254    "INT",
255    "QUIT",
256    "ILL",
257    "TRAP",
258    "IOT",
259    "ABRT",
260    "EMT",
261    "FPE",
262    "KILL",
263    "BUS",
264    "SEGV",
265    "SYS",
266    "PIPE",
267    "ALRM",
268    "TERM",
269    "USR1",
270    "USR2",
271    "CLD",
272    "CHLD",
273    "PWR",
274    "WINCH",
275    "URG",
276    "POLL",
277    "IO",
278    "STOP",
279    "TSTP",
280    "CONT",
281    "TTIN",
282    "TTOU",
283    "VTALRM",
284    "PROF",
285    "XCPU",
286    "XFSZ",
287    "WAITING",
288    "AIOCANCEL",
289    #[cfg(target_os = "illumos")]
290    "LWP",
291    "FREEZE",
292    "THAW",
293    "CANCEL",
294    "LOST",
295    "XRES",
296    "JVM1",
297    "JVM2",
298    "INFO",
299    "RTMIN",
300    "RTMAX",
301];
302
303/*
304   The following signals are defined in AIX:
305
306   SIGHUP     hangup, generated when terminal disconnects
307   SIGINT     interrupt, generated from terminal special char
308   SIGQUIT    quit, generated from terminal special char
309   SIGILL     illegal instruction (not reset when caught)
310   SIGTRAP    trace trap (not reset when caught)
311   SIGABRT    abort process
312   SIGEMT     EMT instruction
313   SIGFPE     floating point exception
314   SIGKILL    kill (cannot be caught or ignored)
315   SIGBUS     bus error (specification exception)
316   SIGSEGV    segmentation violation
317   SIGSYS     bad argument to system call
318   SIGPIPE    write on a pipe with no one to read it
319   SIGALRM    alarm clock timeout
320   SIGTERM    software termination signal
321   SIGURG     urgent condition on I/O channel
322   SIGSTOP    stop (cannot be caught or ignored)
323   SIGTSTP    interactive stop
324   SIGCONT    continue (cannot be caught or ignored)
325   SIGCHLD    sent to parent on child stop or exit
326   SIGTTIN    background read attempted from control terminal
327   SIGTTOU    background write attempted to control terminal
328   SIGIO      I/O possible, or completed
329   SIGXCPU    cpu time limit exceeded (see setrlimit())
330   SIGXFSZ    file size limit exceeded (see setrlimit())
331   SIGMSG     input data is in the ring buffer
332   SIGWINCH   window size changed
333   SIGPWR     power-fail restart
334   SIGUSR1    user defined signal 1
335   SIGUSR2    user defined signal 2
336   SIGPROF    profiling time alarm (see setitimer)
337   SIGDANGER  system crash imminent; free up some page space
338   SIGVTALRM  virtual time alarm (see setitimer)
339   SIGMIGRATE migrate process
340   SIGPRE     programming exception
341   SIGVIRT    AIX virtual time alarm
342   SIGTALRM   per-thread alarm clock
343*/
344#[cfg(target_os = "aix")]
345pub static ALL_SIGNALS: [&str; 37] = [
346    "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "EMT", "FPE", "KILL", "BUS", "SEGV", "SYS",
347    "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO", "XCPU",
348    "XFSZ", "MSG", "WINCH", "PWR", "USR1", "USR2", "PROF", "DANGER", "VTALRM", "MIGRATE", "PRE",
349    "VIRT", "TALRM",
350];
351
352/*
353   The following signals are defined in Cygwin
354   https://cygwin.com/cgit/newlib-cygwin/tree/winsup/cygwin/include/cygwin/signal.h
355
356   SIGHUP     1   hangup
357   SIGINT     2   interrupt
358   SIGQUIT    3   quit
359   SIGILL     4   illegal instruction (not reset when caught)
360   SIGTRAP    5   trace trap (not reset when caught)
361   SIGABRT    6   used by abort
362   SIGEMT     7   EMT instruction
363   SIGFPE     8   floating point exception
364   SIGKILL    9   kill (cannot be caught or ignored)
365   SIGBUS     10  bus error
366   SIGSEGV    11  segmentation violation
367   SIGSYS     12  bad argument to system call
368   SIGPIPE    13  write on a pipe with no one to read it
369   SIGALRM    14  alarm clock
370   SIGTERM    15  software termination signal from kill
371   SIGURG     16  urgent condition on IO channel
372   SIGSTOP    17  sendable stop signal not from tty
373   SIGTSTP    18  stop signal from tty
374   SIGCONT    19  continue a stopped process
375   SIGCHLD    20  to parent on child stop or exit
376   SIGTTIN    21  to readers pgrp upon background tty read
377   SIGTTOU    22  like TTIN for output if (tp->t_local&LTOSTOP)
378   SIGIO      23  input/output possible signal
379   SIGXCPU    24  exceeded CPU time limit
380   SIGXFSZ    25  exceeded file size limit
381   SIGVTALRM  26  virtual time alarm
382   SIGPROF    27  profiling time alarm
383   SIGWINCH   28  window changed
384   SIGLOST    29  resource lost (eg, record-lock lost)
385   SIGUSR1    30  user defined signal 1
386   SIGUSR2    31  user defined signal 2
387*/
388#[cfg(target_os = "cygwin")]
389pub static ALL_SIGNALS: [&str; 32] = [
390    "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "EMT", "FPE", "KILL", "BUS", "SEGV",
391    "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO",
392    "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "PWR", "USR1", "USR2",
393];
394
395/// Returns the signal number for a given signal name or value.
396pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option<usize> {
397    let signal_name_upcase = signal_name_or_value.to_uppercase();
398    if let Ok(value) = signal_name_upcase.parse() {
399        if is_signal(value) {
400            return Some(value);
401        }
402        return realtime_signal_bounds()
403            .filter(|&(rtmin, rtmax)| value >= rtmin && value <= rtmax)
404            .map(|_| value);
405    }
406    let signal_name = signal_name_upcase.trim_start_matches("SIG");
407
408    if let Some(pos) = ALL_SIGNALS.iter().position(|&s| s == signal_name) {
409        return Some(pos);
410    }
411
412    realtime_signal_bounds().and_then(|(rtmin, rtmax)| match signal_name {
413        "RTMIN" => Some(rtmin),
414        "RTMAX" => Some(rtmax),
415        _ => None,
416    })
417}
418
419/// Returns true if the given number is a valid signal number.
420pub fn is_signal(num: usize) -> bool {
421    num < ALL_SIGNALS.len()
422}
423
424/// Returns the signal name for a given signal value.
425pub fn signal_name_by_value(signal_value: usize) -> Option<&'static str> {
426    ALL_SIGNALS.get(signal_value).copied()
427}
428
429#[cfg(any(target_os = "linux", target_os = "android"))]
430fn realtime_signal_bounds() -> Option<(usize, usize)> {
431    let rtmin = libc::SIGRTMIN();
432    let rtmax = libc::SIGRTMAX();
433
434    (0 < rtmin && rtmin <= rtmax).then_some((rtmin as usize, rtmax as usize))
435}
436
437#[cfg(not(any(target_os = "linux", target_os = "android")))]
438fn realtime_signal_bounds() -> Option<(usize, usize)> {
439    None
440}
441
442/// Returns the largest signal number that list-style interfaces should accept.
443pub fn signal_number_upper_bound() -> usize {
444    let base = ALL_SIGNALS.len() - 1;
445
446    realtime_signal_bounds().map_or(base, |(_, rtmax)| rtmax.max(base))
447}
448
449/// Returns the signal name for list-style interfaces.
450pub fn signal_list_name_by_value(signal_value: usize) -> Option<String> {
451    if let Some(signal_name) = signal_name_by_value(signal_value) {
452        return Some(signal_name.to_string());
453    }
454
455    realtime_signal_bounds().and_then(|(rtmin, rtmax)| {
456        if signal_value == rtmin {
457            Some("RTMIN".to_string())
458        } else if signal_value == rtmax {
459            Some("RTMAX".to_string())
460        } else {
461            None
462        }
463    })
464}
465
466/// Returns the signal value for list-style interfaces.
467pub fn signal_list_value_by_name_or_number(spec: &str) -> Option<usize> {
468    let spec_upcase = spec.to_uppercase();
469
470    if let Ok(value) = spec_upcase.parse::<usize>() {
471        return (value <= signal_number_upper_bound()).then_some(value);
472    }
473
474    if let Some(value) = signal_by_name_or_value(&spec_upcase) {
475        return Some(value);
476    }
477
478    let signal_name = spec_upcase.trim_start_matches("SIG");
479    realtime_signal_bounds().and_then(|(rtmin, rtmax)| match signal_name {
480        "RTMIN" => Some(rtmin),
481        "RTMAX" => Some(rtmax),
482        _ => None,
483    })
484}
485
486/// Restores SIGPIPE to default behavior (process terminates on broken pipe).
487#[cfg(unix)]
488pub fn enable_pipe_errors() -> Result<(), Errno> {
489    // We pass the error as is, the return value would just be Ok(SigDfl), so we can safely ignore it.
490    // SAFETY: this function is safe as long as we do not use a custom SigHandler -- we use the default one.
491    unsafe { signal(SIGPIPE, SigDfl) }.map(|_| ())
492}
493
494/// Ignores SIGPIPE signal (broken pipe errors are returned instead of terminating).
495/// Use this to override the default SIGPIPE handling when you need to handle
496/// broken pipe errors gracefully (e.g., tee with --output-error).
497#[cfg(unix)]
498pub fn disable_pipe_errors() -> Result<(), Errno> {
499    // SAFETY: this function is safe as long as we do not use a custom SigHandler -- we use the default one.
500    unsafe { signal(SIGPIPE, SigIgn) }.map(|_| ())
501}
502
503/// Ignores the SIGINT signal.
504#[cfg(unix)]
505pub fn ignore_interrupts() -> Result<(), Errno> {
506    // We pass the error as is, the return value would just be Ok(SigIgn), so we can safely ignore it.
507    // SAFETY: this function is safe as long as we do not use a custom SigHandler -- we use the default one.
508    unsafe { signal(SIGINT, SigIgn) }.map(|_| ())
509}
510
511/// Installs a signal handler. The handler must be async-signal-safe.
512#[cfg(unix)]
513pub fn install_signal_handler(
514    sig: Signal,
515    handler: extern "C" fn(std::os::raw::c_int),
516) -> Result<(), Errno> {
517    let action = SigAction::new(
518        SigHandler::Handler(handler),
519        SaFlags::SA_RESTART,
520        SigSet::empty(),
521    );
522    unsafe { sigaction(sig, &action) }?;
523    Ok(())
524}
525
526// Detect closed stdin/stdout before Rust reopens them as /dev/null (see issue #2873)
527#[cfg(unix)]
528use std::sync::atomic::{AtomicBool, Ordering};
529
530#[cfg(unix)]
531static STDIN_WAS_CLOSED: AtomicBool = AtomicBool::new(false);
532#[cfg(unix)]
533static STDOUT_WAS_CLOSED: AtomicBool = AtomicBool::new(false);
534#[cfg(unix)]
535static STDERR_WAS_CLOSED: AtomicBool = AtomicBool::new(false);
536
537// SIGPIPE state capture - captures whether SIGPIPE was ignored at process startup
538#[cfg(unix)]
539static SIGPIPE_WAS_IGNORED: AtomicBool = AtomicBool::new(false);
540
541#[cfg(unix)]
542static STARTUP_STATE_WAS_CAPTURED: AtomicBool = AtomicBool::new(false);
543
544/// Captures stdio and SIGPIPE state at process initialization, before main() runs.
545///
546/// # Safety
547/// Called from `.init_array` before main(). Only reads current state.
548#[cfg(unix)]
549#[allow(clippy::missing_safety_doc)]
550pub unsafe extern "C" fn capture_startup_state() {
551    use nix::libc;
552    use std::mem::MaybeUninit;
553    use std::ptr;
554
555    // No spinlock because we're single-threaded at this point
556    if STARTUP_STATE_WAS_CAPTURED.swap(true, Ordering::Relaxed) {
557        return;
558    }
559
560    // Capture stdio state
561    unsafe {
562        STDIN_WAS_CLOSED.store(
563            libc::fcntl(libc::STDIN_FILENO, libc::F_GETFD) == -1,
564            Ordering::Relaxed,
565        );
566        STDOUT_WAS_CLOSED.store(
567            libc::fcntl(libc::STDOUT_FILENO, libc::F_GETFD) == -1,
568            Ordering::Relaxed,
569        );
570        STDERR_WAS_CLOSED.store(
571            libc::fcntl(libc::STDERR_FILENO, libc::F_GETFD) == -1,
572            Ordering::Relaxed,
573        );
574    }
575
576    // Capture SIGPIPE state
577    let mut current = MaybeUninit::<libc::sigaction>::uninit();
578    // SAFETY: sigaction with null new-action just queries current state
579    if unsafe { libc::sigaction(libc::SIGPIPE, ptr::null(), current.as_mut_ptr()) } == 0 {
580        // SAFETY: sigaction succeeded, so current is initialized
581        let ignored = unsafe { current.assume_init() }.sa_sigaction == libc::SIG_IGN;
582        SIGPIPE_WAS_IGNORED.store(ignored, Ordering::Release);
583    }
584}
585
586/// Initializes startup state capture. Call once at crate root level.
587#[macro_export]
588#[cfg(unix)]
589macro_rules! init_startup_state_capture {
590    () => {
591        #[cfg(not(target_os = "macos"))]
592        #[used]
593        #[unsafe(link_section = ".init_array")]
594        static CAPTURE_STARTUP_STATE: unsafe extern "C" fn() =
595            $crate::signals::capture_startup_state;
596
597        #[cfg(target_os = "macos")]
598        #[used]
599        #[unsafe(link_section = "__DATA,__mod_init_func")]
600        static CAPTURE_STARTUP_STATE: unsafe extern "C" fn() =
601            $crate::signals::capture_startup_state;
602    };
603}
604
605#[macro_export]
606#[cfg(not(unix))]
607macro_rules! init_startup_state_capture {
608    () => {};
609}
610
611#[cfg(unix)]
612pub fn stdin_was_closed() -> bool {
613    STDIN_WAS_CLOSED.load(Ordering::Relaxed)
614}
615
616#[cfg(not(unix))]
617pub const fn stdin_was_closed() -> bool {
618    false
619}
620
621#[cfg(unix)]
622pub fn stdout_was_closed() -> bool {
623    STDOUT_WAS_CLOSED.load(Ordering::Relaxed)
624}
625
626#[cfg(not(unix))]
627pub const fn stdout_was_closed() -> bool {
628    false
629}
630
631#[cfg(unix)]
632pub fn stderr_was_closed() -> bool {
633    STDERR_WAS_CLOSED.load(Ordering::Relaxed)
634}
635
636#[cfg(not(unix))]
637pub const fn stderr_was_closed() -> bool {
638    false
639}
640
641/// Returns whether SIGPIPE was ignored at process startup.
642#[cfg(unix)]
643pub fn sigpipe_was_ignored() -> bool {
644    SIGPIPE_WAS_IGNORED.load(Ordering::Acquire)
645}
646
647#[cfg(not(unix))]
648pub const fn sigpipe_was_ignored() -> bool {
649    false
650}
651
652#[cfg(target_os = "linux")]
653pub fn ensure_stdout_not_broken() -> std::io::Result<bool> {
654    use nix::{
655        poll::{PollFd, PollFlags, PollTimeout, poll},
656        sys::stat::{SFlag, fstat},
657    };
658    use std::io::stdout;
659    use std::os::fd::AsFd;
660
661    let out = stdout();
662
663    // First, check that stdout is a fifo and return true if it's not the case
664    let stat = fstat(out.as_fd())?;
665    if !SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO) {
666        return Ok(true);
667    }
668
669    // POLLRDBAND is the flag used by GNU tee.
670    let mut pfds = [PollFd::new(out.as_fd(), PollFlags::POLLRDBAND)];
671
672    // Then, ensure that the pipe is not broken.
673    // Use ZERO timeout to return immediately - we just want to check the current state.
674    let res = poll(&mut pfds, PollTimeout::ZERO)?;
675
676    if res > 0 {
677        // poll returned with events ready - check if POLLERR is set (pipe broken)
678        let error = pfds.iter().any(|pfd| {
679            if let Some(revents) = pfd.revents() {
680                revents.contains(PollFlags::POLLERR)
681            } else {
682                true
683            }
684        });
685        return Ok(!error);
686    }
687
688    // res == 0 means no events ready (timeout reached immediately with ZERO timeout).
689    // This means the pipe is healthy (not broken).
690    // res < 0 would be an error, but nix returns Err in that case.
691    Ok(true)
692}
693
694#[test]
695fn signal_by_value() {
696    assert_eq!(signal_by_name_or_value("0"), Some(0));
697    for (value, _signal) in ALL_SIGNALS.iter().enumerate() {
698        assert_eq!(signal_by_name_or_value(&value.to_string()), Some(value));
699    }
700}
701
702#[test]
703fn signal_by_short_name() {
704    for (value, signal) in ALL_SIGNALS.iter().enumerate() {
705        assert_eq!(signal_by_name_or_value(signal), Some(value));
706    }
707}
708
709#[test]
710fn signal_by_long_name() {
711    for (value, signal) in ALL_SIGNALS.iter().enumerate() {
712        assert_eq!(
713            signal_by_name_or_value(&format!("SIG{signal}")),
714            Some(value)
715        );
716    }
717}
718
719#[test]
720fn name() {
721    for (value, signal) in ALL_SIGNALS.iter().enumerate() {
722        assert_eq!(signal_name_by_value(value), Some(*signal));
723    }
724}
725
726#[test]
727fn list_signal_names_match_static_signal_names() {
728    for (value, signal) in ALL_SIGNALS.iter().enumerate() {
729        assert_eq!(signal_list_name_by_value(value), Some(signal.to_string()));
730    }
731}
732
733#[test]
734fn list_signal_numbers_follow_upper_bound() {
735    assert_eq!(
736        signal_list_value_by_name_or_number(&signal_number_upper_bound().to_string()),
737        Some(signal_number_upper_bound())
738    );
739    assert_eq!(
740        signal_list_value_by_name_or_number(&(signal_number_upper_bound() + 1).to_string()),
741        None
742    );
743}
744
745#[cfg(any(target_os = "linux", target_os = "android"))]
746#[test]
747fn linux_realtime_signal_upper_bound_includes_rtmax() {
748    let (_, rtmax) = realtime_signal_bounds().unwrap();
749    assert!(signal_number_upper_bound() >= rtmax);
750}
751
752#[cfg(any(target_os = "linux", target_os = "android"))]
753#[test]
754fn linux_realtime_signal_names_are_listed() {
755    let (rtmin, rtmax) = realtime_signal_bounds().unwrap();
756
757    assert_eq!(signal_list_name_by_value(rtmin), Some("RTMIN".to_string()));
758    assert_eq!(signal_list_name_by_value(rtmax), Some("RTMAX".to_string()));
759}
760
761#[cfg(any(target_os = "linux", target_os = "android"))]
762#[test]
763fn linux_realtime_signal_names_resolve_to_runtime_values() {
764    let (rtmin, rtmax) = realtime_signal_bounds().unwrap();
765
766    assert_eq!(signal_list_value_by_name_or_number("RTMIN"), Some(rtmin));
767    assert_eq!(signal_list_value_by_name_or_number("RTMAX"), Some(rtmax));
768    assert_eq!(signal_list_value_by_name_or_number("SIGRTMIN"), Some(rtmin));
769    assert_eq!(signal_list_value_by_name_or_number("SIGRTMAX"), Some(rtmax));
770}
771
772#[cfg(any(target_os = "linux", target_os = "android"))]
773#[test]
774fn linux_unnamed_signal_numbers_are_valid_for_lists() {
775    assert_eq!(signal_list_value_by_name_or_number("32"), Some(32));
776    assert_eq!(signal_list_value_by_name_or_number("33"), Some(33));
777}
778
779#[cfg(any(target_os = "linux", target_os = "android"))]
780#[test]
781fn linux_realtime_signals_resolve_by_name_or_value() {
782    let (rtmin, rtmax) = realtime_signal_bounds().unwrap();
783
784    // By name
785    assert_eq!(signal_by_name_or_value("RTMIN"), Some(rtmin));
786    assert_eq!(signal_by_name_or_value("RTMAX"), Some(rtmax));
787    assert_eq!(signal_by_name_or_value("SIGRTMIN"), Some(rtmin));
788    assert_eq!(signal_by_name_or_value("SIGRTMAX"), Some(rtmax));
789
790    // By numeric value
791    assert_eq!(signal_by_name_or_value(&rtmin.to_string()), Some(rtmin));
792    assert_eq!(signal_by_name_or_value(&rtmax.to_string()), Some(rtmax));
793}