diff --git a/fish-rust/src/common.rs b/fish-rust/src/common.rs index a582b9d62..597d949b5 100644 --- a/fish-rust/src/common.rs +++ b/fish-rust/src/common.rs @@ -947,10 +947,8 @@ pub const fn char_offset(base: char, offset: u32) -> char { } /// Exits without invoking destructors (via _exit), useful for code after fork. -fn exit_without_destructors(code: i32) -> ! { - unsafe { - libc::_exit(code); - } +pub fn exit_without_destructors(code: i32) -> ! { + unsafe { libc::_exit(code) }; } /// Save the shell mode on startup so we can restore them on exit. @@ -1594,6 +1592,7 @@ pub fn restore_term_foreground_process_group_for_exit() { // Note initial_fg_process_group == 0 is possible with Linux pid namespaces. // This is called during shutdown and from a signal handler. We don't bother to complain on // failure because doing so is unlikely to be noticed. + // Safety: All of getpgrp, signal, and tcsetpgrp are async-signal-safe. let initial_fg_process_group = INITIAL_FG_PROCESS_GROUP.load(Ordering::Relaxed); if initial_fg_process_group > 0 && initial_fg_process_group != unsafe { libc::getpgrp() } { unsafe { diff --git a/fish-rust/src/event.rs b/fish-rust/src/event.rs index b46d729a7..d4d0534d0 100644 --- a/fish-rust/src/event.rs +++ b/fish-rust/src/event.rs @@ -791,7 +791,7 @@ pub fn fire_delayed(parser: &mut parser_t) { // HACK: The only variables we change in response to a *signal* are $COLUMNS and $LINES. // Do that now. - if sig == Signal::SIGWINCH { + if sig == libc::SIGWINCH { termsize::SHARED_CONTAINER.updating(parser); } let event = Event { diff --git a/fish-rust/src/signal.rs b/fish-rust/src/signal.rs index 61f94e1d7..a499d467d 100644 --- a/fish-rust/src/signal.rs +++ b/fish-rust/src/signal.rs @@ -1,13 +1,290 @@ -use std::borrow::Cow; use std::num::NonZeroI32; -use crate::ffi; +use crate::common::{exit_without_destructors, restore_term_foreground_process_group_for_exit}; +use crate::event::{enqueue_signal, is_signal_observed}; +use crate::termsize::termsize_handle_winch; use crate::topic_monitor::{generation_t, invalid_generations, topic_monitor_principal, topic_t}; -use crate::wchar::wstr; -use crate::wchar_ffi::c_str; -use widestring::U32CStr; +use crate::wchar::{wstr, WExt, L}; +use crate::wutil::fish_wcstoi; +use crate::wutil::{wgettext, wgettext_str, wperror}; +use errno::{errno, set_errno}; +use std::sync::atomic::{AtomicI32, Ordering}; use widestring_suffix::widestrs; +/// Store the "main" pid. This allows us to reliably determine if we are in a forked child. +static MAIN_PID: AtomicI32 = AtomicI32::new(0); + +/// It's possible that we receive a signal after we have forked, but before we have reset the signal +/// handlers (or even run the pthread_atfork calls). In that event we will do something dumb like +/// swallow SIGINT. Ensure that doesn't happen. Check if we are the main fish process; if not, reset +/// and re-raise the signal. \return whether we re-raised the signal. +fn reraise_if_forked_child(sig: i32) -> bool { + // Don't use is_forked_child: it relies on atfork handlers which may have not yet run. + // Safety: getpid() is async-signal-safe. + let pid = unsafe { libc::getpid() }; + if pid == MAIN_PID.load(Ordering::Relaxed) { + return false; + } + + // Safety: signal() and raise() are async-signal-safe. + unsafe { + libc::signal(sig, libc::SIG_DFL); + libc::raise(sig); + } + true +} + +/// The cancellation signal we have received. +/// Of course this is modified from a signal handler. +static CANCELLATION_SIGNAL: AtomicI32 = AtomicI32::new(0); + +pub fn signal_clear_cancel() { + CANCELLATION_SIGNAL.store(0, Ordering::Relaxed); +} + +pub fn signal_check_cancel() -> i32 { + CANCELLATION_SIGNAL.load(Ordering::Relaxed) +} + +// Declare these as an extern C functions and call them directly, +// in case the autocxx ffi allocates or does something else signal-unfriendly. +extern "C" { + fn reader_sighup(); + fn reader_handle_sigint(); +} + +/// The single signal handler. By centralizing signal handling we ensure that we can never install +/// the "wrong" signal handler (see #5969). +extern "C" fn fish_signal_handler( + sig: i32, + _info: *mut libc::siginfo_t, + _context: *mut libc::c_void, +) { + // Ensure we preserve errno. + let saved_errno = errno(); + + // Check if we are a forked child. + if reraise_if_forked_child(sig) { + set_errno(saved_errno); + return; + } + + // Check if fish script cares about this. + let observed = is_signal_observed(sig); + if observed { + enqueue_signal(sig); + } + + // Do some signal-specific stuff. + match sig { + libc::SIGWINCH => { + // Respond to a winch signal by telling the termsize container. + termsize_handle_winch(); + } + libc::SIGHUP => { + // Exit unless the signal was trapped. + if !observed { + unsafe { reader_sighup() }; + } + topic_monitor_principal().post(topic_t::sighupint); + } + libc::SIGTERM => { + // Handle sigterm. The only thing we do is restore the front process ID, then die. + if !observed { + restore_term_foreground_process_group_for_exit(); + // Safety: signal() and raise() are async-signal-safe. + unsafe { + libc::signal(libc::SIGTERM, libc::SIG_DFL); + libc::raise(libc::SIGTERM); + } + } + } + libc::SIGINT => { + // Cancel unless the signal was trapped. + if !observed { + CANCELLATION_SIGNAL.store(libc::SIGINT, Ordering::Relaxed); + } + unsafe { reader_handle_sigint() }; + topic_monitor_principal().post(topic_t::sighupint); + } + libc::SIGCHLD => { + // A child process stopped or exited. + topic_monitor_principal().post(topic_t::sigchld); + } + libc::SIGALRM => { + // We have a sigalarm handler that does nothing. This is used in the signal torture + // test, to verify that we behave correctly when receiving lots of irrelevant signals. + } + _ => {} + } + + set_errno(saved_errno); +} + +fn signal_reset_handlers() { + let mut act: libc::sigaction = unsafe { std::mem::zeroed() }; + unsafe { libc::sigemptyset(&mut act.sa_mask) }; + act.sa_flags = 0; + act.sa_sigaction = libc::SIG_DFL; + + for data in SIGNAL_TABLE.iter() { + if data.signal == libc::SIGHUP { + let mut oact: libc::sigaction = unsafe { std::mem::zeroed() }; + unsafe { libc::sigaction(libc::SIGHUP, std::ptr::null(), &mut oact) }; + if oact.sa_sigaction == libc::SIG_IGN { + continue; + } + } + unsafe { + libc::sigaction(data.signal.code(), &act, std::ptr::null_mut()); + }; + } +} + +// Wrapper around sigaction. +fn sigaction(sig: i32, act: &libc::sigaction, oact: *mut libc::sigaction) -> libc::c_int { + // Note: historically many call sites have ignored return value of sigaction here. + unsafe { libc::sigaction(sig, act, oact) } +} + +fn set_interactive_handlers() { + let signal_handler: usize = fish_signal_handler as usize; + let mut act: libc::sigaction = unsafe { std::mem::zeroed() }; + let mut oact: libc::sigaction = unsafe { std::mem::zeroed() }; + act.sa_flags = 0; + oact.sa_flags = 0; + unsafe { libc::sigemptyset(&mut act.sa_mask) }; + + let nullptr = std::ptr::null_mut(); + + // Interactive mode. Ignore interactive signals. We are a shell, we know what is best for + // the user. + act.sa_sigaction = libc::SIG_IGN; + sigaction(libc::SIGTSTP, &act, nullptr); + sigaction(libc::SIGTTOU, &act, nullptr); + + // We don't ignore SIGTTIN because we might send it to ourself. + act.sa_sigaction = signal_handler; + act.sa_flags = libc::SA_SIGINFO; + sigaction(libc::SIGTTIN, &act, nullptr); + + // SIGTERM restores the terminal controlling process before dying. + act.sa_sigaction = signal_handler; + act.sa_flags = libc::SA_SIGINFO; + sigaction(libc::SIGTERM, &act, nullptr); + + unsafe { libc::sigaction(libc::SIGHUP, nullptr, &mut oact) }; + if oact.sa_sigaction == libc::SIG_DFL { + act.sa_sigaction = signal_handler; + act.sa_flags = libc::SA_SIGINFO; + sigaction(libc::SIGHUP, &act, nullptr); + } + + // SIGALARM as part of our signal torture test + act.sa_sigaction = signal_handler; + act.sa_flags = libc::SA_SIGINFO; + sigaction(libc::SIGALRM, &act, nullptr); + + act.sa_sigaction = signal_handler; + act.sa_flags = libc::SA_SIGINFO; + sigaction(libc::SIGWINCH, &act, nullptr); +} + +/// Sets up appropriate signal handlers. +fn signal_set_handlers(interactive: bool) { + use libc::SIG_IGN; + let nullptr = std::ptr::null_mut(); + let mut act: libc::sigaction = unsafe { std::mem::zeroed() }; + + act.sa_flags = 0; + unsafe { libc::sigemptyset(&mut act.sa_mask) }; + + // Ignore SIGPIPE. We'll detect failed writes and deal with them appropriately. We don't want + // this signal interrupting other syscalls or terminating us. + act.sa_sigaction = SIG_IGN; + sigaction(libc::SIGPIPE, &act, nullptr); + + // Ignore SIGQUIT. + act.sa_sigaction = SIG_IGN; + sigaction(libc::SIGQUIT, &act, nullptr); + + // Apply our SIGINT handler. + act.sa_sigaction = fish_signal_handler as usize; + act.sa_flags = libc::SA_SIGINFO; + sigaction(libc::SIGINT, &act, nullptr); + + // Whether or not we're interactive we want SIGCHLD to not interrupt restartable syscalls. + act.sa_sigaction = fish_signal_handler as usize; + act.sa_flags = libc::SA_SIGINFO | libc::SA_RESTART; + if sigaction(libc::SIGCHLD, &act, nullptr) != 0 { + wperror(L!("sigaction")); + exit_without_destructors(1); + } + + if interactive { + set_interactive_handlers(); + } + + if cfg!(feature = "FISH_TSAN_WORKAROUNDS") { + // Work around the following TSAN bug: + // The structure containing signal information for a thread is lazily allocated by TSAN. + // It is possible for the same thread to receive two allocations, if the signal handler + // races with other allocation paths (e.g. a blocking call). This results in the first signal + // being potentially dropped. + // The workaround is to send ourselves a SIGCHLD signal now, to force the allocation to happen. + // As no child is associated with this signal, it is OK if it is dropped, so long as the + // allocation happens. + unsafe { libc::kill(libc::getpid(), libc::SIGCHLD) }; + } +} + +pub fn signal_handle(sig: libc::c_int) { + let mut act: libc::sigaction = unsafe { std::mem::zeroed() }; + + // These should always be handled. + if sig == libc::SIGINT + || sig == libc::SIGQUIT + || sig == libc::SIGTSTP + || sig == libc::SIGTTIN + || sig == libc::SIGTTOU + || sig == libc::SIGCHLD + { + return; + } + + act.sa_flags = 0; + unsafe { libc::sigemptyset(&mut act.sa_mask) }; + act.sa_flags = libc::SA_SIGINFO; + act.sa_sigaction = fish_signal_handler as usize; + sigaction(sig, &act, std::ptr::null_mut()); +} + +pub fn get_signals_with_handlers(set: &mut libc::sigset_t) { + unsafe { libc::sigemptyset(set) }; + for data in SIGNAL_TABLE.iter() { + let mut act: libc::sigaction = unsafe { std::mem::zeroed() }; + unsafe { libc::sigaction(data.signal.code(), std::ptr::null(), &mut act) }; + // If SIGHUP is being ignored (e.g., because were were run via `nohup`) don't reset it. + // We don't special case other signals because if they're being ignored that shouldn't + // affect processes we spawn. They should get the default behavior for those signals. + if data.signal == libc::SIGHUP && act.sa_sigaction == libc::SIG_IGN { + continue; + } + if act.sa_sigaction != libc::SIG_DFL { + unsafe { libc::sigaddset(set, data.signal.code()) }; + } + } +} + +/// Ensure we did not inherit any blocked signals. See issue #3964. +pub fn signal_unblock_all() { + unsafe { + let mut iset: libc::sigset_t = std::mem::zeroed(); + libc::sigemptyset(&mut iset); + libc::sigprocmask(libc::SIG_SETMASK, &iset, std::ptr::null_mut()); + } +} + /// A Sigchecker can be used to check if a SIGINT (or SIGHUP) has been delivered. pub struct Sigchecker { topic: topic_t, @@ -47,30 +324,102 @@ impl Sigchecker { } } -#[deprecated(note = "Use [`Signal::parse()`] instead.")] -/// Get the integer signal value representing the specified signal. -pub fn wcs2sig(s: &wstr) -> Option { - let sig = ffi::wcs2sig(c_str!(s)); - - sig.0.try_into().ok() +/// Struct describing an entry for the lookup table used to convert between signal names and signal +/// ids, etc. +struct LookupEntry { + signal: Signal, + name: &'static wstr, + desc: &'static wstr, // Note: this needs to be translated via gettext before presenting it to the user. } -#[deprecated(note = "Use [`Signal::name()`] instead.")] -/// Get string representation of a signal. -pub fn sig2wcs(sig: i32) -> &'static wstr { - let s = ffi::sig2wcs(ffi::c_int(sig)); - let s = unsafe { U32CStr::from_ptr_str(s) }; - - wstr::from_ucstr(s).expect("signal name should be valid utf-32") +impl LookupEntry { + const fn new(signal: i32, name: &'static wstr, desc: &'static wstr) -> Self { + Self { + signal: Signal::new(signal), + name, + desc, + } + } } -#[deprecated(note = "Use [`Signal::desc()`] instead.")] -/// Returns a description of the specified signal. -pub fn signal_get_desc(sig: i32) -> &'static wstr { - let s = ffi::signal_get_desc(ffi::c_int(sig)); - let s = unsafe { U32CStr::from_ptr_str(s) }; +// Lookup table used to convert between signal names and signal ids, etc. +#[rustfmt::skip] +#[widestrs] +const SIGNAL_TABLE : &[LookupEntry] = &[ + LookupEntry::new(libc::SIGHUP, "SIGHUP"L, "Terminal hung up"L), + LookupEntry::new(libc::SIGINT, "SIGINT"L, "Quit request from job control (^C)"L), + LookupEntry::new(libc::SIGQUIT, "SIGQUIT"L, "Quit request from job control with core dump (^\\)"L), + LookupEntry::new(libc::SIGILL, "SIGILL"L, "Illegal instruction"L), + LookupEntry::new(libc::SIGTRAP, "SIGTRAP"L, "Trace or breakpoint trap"L), + LookupEntry::new(libc::SIGABRT, "SIGABRT"L, "Abort"L), + LookupEntry::new(libc::SIGBUS, "SIGBUS"L, "Misaligned address error"L), + LookupEntry::new(libc::SIGFPE, "SIGFPE"L, "Floating point exception"L), + LookupEntry::new(libc::SIGKILL, "SIGKILL"L, "Forced quit"L), + LookupEntry::new(libc::SIGUSR1, "SIGUSR1"L, "User defined signal 1"L), + LookupEntry::new(libc::SIGUSR2, "SIGUSR2"L, "User defined signal 2"L), + LookupEntry::new(libc::SIGSEGV, "SIGSEGV"L, "Address boundary error"L), + LookupEntry::new(libc::SIGPIPE, "SIGPIPE"L, "Broken pipe"L), + LookupEntry::new(libc::SIGALRM, "SIGALRM"L, "Timer expired"L), + LookupEntry::new(libc::SIGTERM, "SIGTERM"L, "Polite quit request"L), + LookupEntry::new(libc::SIGCHLD, "SIGCHLD"L, "Child process status changed"L), + LookupEntry::new(libc::SIGCONT, "SIGCONT"L, "Continue previously stopped process"L), + LookupEntry::new(libc::SIGSTOP, "SIGSTOP"L, "Forced stop"L), + LookupEntry::new(libc::SIGTSTP, "SIGTSTP"L, "Stop request from job control (^Z)"L), + LookupEntry::new(libc::SIGTTIN, "SIGTTIN"L, "Stop from terminal input"L), + LookupEntry::new(libc::SIGTTOU, "SIGTTOU"L, "Stop from terminal output"L), + LookupEntry::new(libc::SIGURG, "SIGURG"L, "Urgent socket condition"L), + LookupEntry::new(libc::SIGXCPU, "SIGXCPU"L, "CPU time limit exceeded"L), + LookupEntry::new(libc::SIGXFSZ, "SIGXFSZ"L, "File size limit exceeded"L), + LookupEntry::new(libc::SIGVTALRM, "SIGVTALRM"L, "Virtual timefr expired"L), + LookupEntry::new(libc::SIGPROF, "SIGPROF"L, "Profiling timer expired"L), + LookupEntry::new(libc::SIGWINCH, "SIGWINCH"L, "Window size change"L), + LookupEntry::new(libc::SIGIO, "SIGIO"L, "I/O on asynchronous file descriptor is possible"L), + LookupEntry::new(libc::SIGSYS, "SIGSYS"L, "Bad system call"L), + LookupEntry::new(libc::SIGIOT, "SIGIOT"L, "Abort (Alias for SIGABRT)"L), - wstr::from_ucstr(s).expect("signal description should be valid utf-32") + #[cfg(any(feature = "bsd", target_os = "macos"))] + LookupEntry::new(libc::SIGEMT, "SIGEMT"L, "Unused signal"L), + + #[cfg(any(feature = "bsd", target_os = "macos"))] + LookupEntry::new(libc::SIGINFO, "SIGINFO"L, "Information request"L), + + #[cfg(target_os = "linux")] + LookupEntry::new(libc::SIGSTKFLT, "SISTKFLT"L, "Stack fault"L), + + #[cfg(target_os = "linux")] + LookupEntry::new(libc::SIGIOT, "SIGIOT"L, "Abort (Alias for SIGABRT)"L), + + #[cfg(target_os = "linux")] + #[allow(deprecated)] + LookupEntry::new(libc::SIGUNUSED, "SIGUNUSED"L, "Unused signal"L), + + #[cfg(target_os = "linux")] + LookupEntry::new(libc::SIGPWR, "SIGPWR"L, "Power failure"L), + + // TODO: determine whether SIGWIND is defined on any platform. + //LookupEntry::new(libc::SIGWIND, "SIGWIND"L, "Window size change"L), +]; + +// Return true if two strings are equal, ignoring ASCII case. +fn equals_ascii_icase(left: &wstr, right: &wstr) -> bool { + if left.len() != right.len() { + return false; + } + for (lc, rc) in left.chars().zip(right.chars()) { + if lc.to_ascii_lowercase() != rc.to_ascii_lowercase() { + return false; + } + } + true +} + +/// Test if \c name is a string describing the signal named \c canonical. +fn match_signal_name(canonical: &wstr, mut name: &wstr) -> bool { + // Skip the "SIG" prefix if it exists. + if name.char_count() >= 3 && equals_ascii_icase(name.slice_to(3), L!("sig")) { + name = name.slice_from(3) + } + equals_ascii_icase(canonical.slice_from(3), name) } #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] @@ -78,71 +427,6 @@ pub fn signal_get_desc(sig: i32) -> &'static wstr { pub struct Signal(NonZeroI32); impl Signal { - pub const SIGHUP: Signal = Signal::new(libc::SIGHUP); - pub const SIGINT: Signal = Signal::new(libc::SIGINT); - pub const SIGQUIT: Signal = Signal::new(libc::SIGQUIT); - pub const SIGILL: Signal = Signal::new(libc::SIGILL); - pub const SIGTRAP: Signal = Signal::new(libc::SIGTRAP); - pub const SIGABRT: Signal = Signal::new(libc::SIGABRT); - /// Available on BSD and macOS only. - #[cfg(any(feature = "bsd", target_os = "macos"))] - pub const SIGEMT: Signal = Signal::new(libc::SIGEMT); - pub const SIGFPE: Signal = Signal::new(libc::SIGFPE); - pub const SIGKILL: Signal = Signal::new(libc::SIGKILL); - pub const SIGBUS: Signal = Signal::new(libc::SIGBUS); - pub const SIGSEGV: Signal = Signal::new(libc::SIGSEGV); - pub const SIGSYS: Signal = Signal::new(libc::SIGSYS); - pub const SIGPIPE: Signal = Signal::new(libc::SIGPIPE); - pub const SIGALRM: Signal = Signal::new(libc::SIGALRM); - pub const SIGTERM: Signal = Signal::new(libc::SIGTERM); - pub const SIGURG: Signal = Signal::new(libc::SIGURG); - pub const SIGSTOP: Signal = Signal::new(libc::SIGSTOP); - pub const SIGTSTP: Signal = Signal::new(libc::SIGTSTP); - pub const SIGCONT: Signal = Signal::new(libc::SIGCONT); - pub const SIGCHLD: Signal = Signal::new(libc::SIGCHLD); - pub const SIGTTIN: Signal = Signal::new(libc::SIGTTIN); - pub const SIGTTOU: Signal = Signal::new(libc::SIGTTOU); - pub const SIGIO: Signal = Signal::new(libc::SIGIO); - pub const SIGXCPU: Signal = Signal::new(libc::SIGXCPU); - pub const SIGXFSZ: Signal = Signal::new(libc::SIGXFSZ); - pub const SIGVTALRM: Signal = Signal::new(libc::SIGVTALRM); - pub const SIGPROF: Signal = Signal::new(libc::SIGPROF); - pub const SIGWINCH: Signal = Signal::new(libc::SIGWINCH); - /// Available on BSD and macOS only. - #[cfg(any(feature = "bsd", target_os = "macos"))] - pub const SIGINFO: Signal = Signal::new(libc::SIGINFO); - pub const SIGUSR1: Signal = Signal::new(libc::SIGUSR1); - pub const SIGUSR2: Signal = Signal::new(libc::SIGUSR2); - /// Available on BSD only. - #[cfg(any(target_os = "freebsd"))] - pub const SIGTHR: Signal = Signal::new(32); // Not exposed by libc crate - /// Available on BSD only. - #[cfg(any(target_os = "freebsd"))] - pub const SIGLIBRT: Signal = Signal::new(33); // Not exposed by libc crate - #[cfg(target_os = "linux")] - /// Available on Linux only. - pub const SIGSTKFLT: Signal = Signal::new(libc::SIGSTKFLT); - #[cfg(target_os = "linux")] - /// Available on Linux only. - pub const SIGPWR: Signal = Signal::new(libc::SIGPWR); - - // Signals aliased to other signals - /// Available on Linux only. Use [`Signal::SIGIO`] instead. - #[cfg(target_os = "linux")] - pub const SIGPOLL: Signal = Signal::SIGIO; - /// Available on Linux only. Use [`Signal::SIGSYS`] instead. - #[cfg(target_os = "linux")] - #[deprecated(note = "Use SIGSYS instead")] - pub const SIGUNUSED: Signal = Signal::SIGSYS; - /// Available on Linux only. Alias for [`Signal::SIGABRT`]. - #[cfg(target_os = "linux")] - pub const SIGIOT: Signal = Signal::SIGABRT; -} - -impl Signal { - #[widestrs] - const UNKNOWN_SIG_NAME: &'static wstr = "SIG???"L; - /// Creates a new `Signal` to represent the passed system signal code `sig`. /// Panics if `sig` is zero. pub const fn new(sig: i32) -> Self { @@ -152,166 +436,57 @@ impl Signal { } } - #[widestrs] - pub const fn name(&self) -> &'static wstr { - match *self { - Signal::SIGHUP => "SIGHUP"L, - Signal::SIGINT => "SIGINT"L, - Signal::SIGQUIT => "SIGQUIT"L, - Signal::SIGILL => "SIGILL"L, - Signal::SIGTRAP => "SIGTRAP"L, - Signal::SIGABRT => "SIGABRT"L, - #[cfg(any(feature = "bsd", target_os = "macos"))] - Signal::SIGEMT => "SIGEMT"L, - Signal::SIGFPE => "SIGFPE"L, - Signal::SIGKILL => "SIGKILL"L, - Signal::SIGBUS => "SIGBUS"L, - Signal::SIGSEGV => "SIGSEGV"L, - Signal::SIGSYS => "SIGSYS"L, - Signal::SIGPIPE => "SIGPIPE"L, - Signal::SIGALRM => "SIGALRM"L, - Signal::SIGTERM => "SIGTERM"L, - Signal::SIGURG => "SIGURG"L, - Signal::SIGSTOP => "SIGSTOP"L, - Signal::SIGTSTP => "SIGTSTP"L, - Signal::SIGCONT => "SIGCONT"L, - Signal::SIGCHLD => "SIGCHLD"L, - Signal::SIGTTIN => "SIGTTIN"L, - Signal::SIGTTOU => "SIGTTOU"L, - Signal::SIGIO => "SIGIO"L, - Signal::SIGXCPU => "SIGXCPU"L, - Signal::SIGXFSZ => "SIGXFSZ"L, - Signal::SIGVTALRM => "SIGVTALRM"L, - Signal::SIGPROF => "SIGPROF"L, - Signal::SIGWINCH => "SIGWINCH"L, - #[cfg(any(feature = "bsd", target_os = "macos"))] - Signal::SIGINFO => "SIGINFO"L, - Signal::SIGUSR1 => "SIGUSR1"L, - Signal::SIGUSR2 => "SIGUSR2"L, - #[cfg(any(target_os = "freebsd"))] - Signal::SIGTHR => "SIGTHR"L, - #[cfg(any(target_os = "freebsd"))] - Signal::SIGLIBRT => "SIGLIBRT"L, - #[cfg(target_os = "linux")] - Signal::SIGSTKFLT => "SIGSTKFLT"L, - #[cfg(target_os = "linux")] - Signal::SIGPWR => "SIGPWR"L, - Signal(_) => Self::UNKNOWN_SIG_NAME, + /// Return the LookupEntry for ourself. + fn get_lookup_entry(&self) -> Option<&'static LookupEntry> { + SIGNAL_TABLE + .iter() + .find(|entry| entry.signal == self.code()) + } + + // Previously sig2wcs(). + pub fn name(&self) -> &'static wstr { + match self.get_lookup_entry() { + Some(entry) => entry.name, + None => wgettext!("Unknown"), + } + } + + // Previously signal_get_desc(). + pub fn desc(&self) -> &'static wstr { + match self.get_lookup_entry() { + Some(entry) => wgettext_str(entry.desc), + None => wgettext!("Unknown"), } } pub fn code(&self) -> i32 { self.0.into() } - - #[widestrs] - pub const fn desc(&self) -> &'static wstr { - match *self { - Signal::SIGHUP => "Terminal hung up"L, - Signal::SIGINT => "Quit request from job control (^C)"L, - Signal::SIGQUIT => "Quit request from job control with core dump (^\\)"L, - Signal::SIGILL => "Illegal instruction"L, - Signal::SIGTRAP => "Trace or breakpoint trap"L, - Signal::SIGABRT => "Abort"L, - #[cfg(any(feature = "bsd", target_os = "macos"))] - Signal::SIGEMT => "Emulator trap"L, - Signal::SIGFPE => "Floating point exception"L, - Signal::SIGKILL => "Forced quit"L, - Signal::SIGBUS => "Misaligned address error"L, - Signal::SIGSEGV => "Address boundary error"L, - Signal::SIGSYS => "Bad system call"L, - Signal::SIGPIPE => "Broken pipe"L, - Signal::SIGALRM => "Timer expired"L, - Signal::SIGTERM => "Polite quit request"L, - Signal::SIGURG => "Urgent socket condition"L, - Signal::SIGSTOP => "Forced stop"L, - Signal::SIGTSTP => "Stop request from job control (^Z)"L, - Signal::SIGCONT => "Continue previously stopped process"L, - Signal::SIGCHLD => "Child process status changed"L, - Signal::SIGTTIN => "Stop from terminal input"L, - Signal::SIGTTOU => "Stop from terminal output"L, - Signal::SIGIO => "I/O on asynchronous file descriptior is possible"L, - Signal::SIGXCPU => "CPU time limit exceeded"L, - Signal::SIGXFSZ => "File size limit exceeded"L, - Signal::SIGVTALRM => "Virtual timer expired"L, - Signal::SIGPROF => "Profiling timer expired"L, - Signal::SIGWINCH => "Window size change"L, - #[cfg(any(feature = "bsd", target_os = "macos"))] - Signal::SIGINFO => "Information request"L, - Signal::SIGUSR1 => "User-defined signal 1"L, - Signal::SIGUSR2 => "User-defined signal 2"L, - #[cfg(any(target_os = "freebsd"))] - Signal::SIGTHR => "Thread interrupt"L, - #[cfg(any(target_os = "freebsd"))] - Signal::SIGLIBRT => "Real-time library interrupt"L, - #[cfg(target_os = "linux")] - Signal::SIGSTKFLT => "Stack fault"L, - #[cfg(target_os = "linux")] - Signal::SIGPWR => "Power failure"L, - Signal(_) => "Unknown"L, - } - } - /// Parses a string into the equivalent [`Signal`] sharing the same name. - /// /// Accepts both `SIGABC` and `ABC` to match against `Signal::SIGABC`. If the signal name is not /// recognized, `None` is returned. - pub fn parse(name: &str) -> Option { - let mut chars = name.chars(); - let name = loop { - match chars.next() { - None => break Cow::Borrowed(name), - Some(c) if !c.is_ascii() => return None, - Some(c) if !c.is_ascii_uppercase() => break Cow::Owned(name.to_ascii_uppercase()), - _ => (), - }; - }; - - let name = name.strip_prefix("SIG").unwrap_or(name.as_ref()); - match name { - "HUP" => Some(Signal::SIGHUP), - "INT" => Some(Signal::SIGINT), - "QUIT" => Some(Signal::SIGQUIT), - "ILL" => Some(Signal::SIGILL), - "TRAP" => Some(Signal::SIGTRAP), - "ABRT" => Some(Signal::SIGABRT), - #[cfg(any(feature = "bsd", target_os = "macos"))] - "EMT" => Some(Signal::SIGEMT), - "FPE" => Some(Signal::SIGFPE), - "KILL" => Some(Signal::SIGKILL), - "BUS" => Some(Signal::SIGBUS), - "SEGV" => Some(Signal::SIGSEGV), - "SYS" => Some(Signal::SIGSYS), - "PIPE" => Some(Signal::SIGPIPE), - "ALRM" => Some(Signal::SIGALRM), - "TERM" => Some(Signal::SIGTERM), - "URG" => Some(Signal::SIGURG), - "STOP" => Some(Signal::SIGSTOP), - "TSTP" => Some(Signal::SIGTSTP), - "CONT" => Some(Signal::SIGCONT), - "CHLD" => Some(Signal::SIGCHLD), - "TTIN" => Some(Signal::SIGTTIN), - "TTOU" => Some(Signal::SIGTTOU), - "IO" => Some(Signal::SIGIO), - "XCPU" => Some(Signal::SIGXCPU), - "XFSZ" => Some(Signal::SIGXFSZ), - "VTALRM" => Some(Signal::SIGVTALRM), - "PROF" => Some(Signal::SIGPROF), - "WINCH" => Some(Signal::SIGWINCH), - #[cfg(any(feature = "bsd", target_os = "macos"))] - "INFO" => Some(Signal::SIGINFO), - "USR1" => Some(Signal::SIGUSR1), - "USR2" => Some(Signal::SIGUSR2), - #[cfg(any(target_os = "freebsd"))] - "THR" => Some(Signal::SIGTHR), - #[cfg(any(target_os = "freebsd"))] - "LIBRT" => Some(Signal::SIGLIBRT), - #[cfg(target_os = "linux")] - "STKFLT" => Some(Signal::SIGSTKFLT), - #[cfg(target_os = "linux")] - "PWR" => Some(Signal::SIGPWR), - _ => None, + /// This also accepts integer codes via fish_wcstoi(). + /// Previously sig2wcs(). + pub fn parse(name: &wstr) -> Option { + for entry in SIGNAL_TABLE.iter() { + if match_signal_name(entry.name, name) { + return Some(entry.signal); + } } + + if let Ok(num) = fish_wcstoi(name) { + if num > 0 { + return Some(Signal::new(num)); + } + } + None + } +} + +// Allow signals to be compared against i32. +impl PartialEq for Signal { + fn eq(&self, other: &i32) -> bool { + self.code() == *other } } @@ -333,24 +508,32 @@ impl From for NonZeroI32 { } } -#[test] -fn signal_name() { - let sig = Signal::SIGINT; - assert_eq!(sig.name(), "SIGINT"); -} +// Need to use add_test for wgettext support. +use crate::ffi_tests::add_test; -#[test] -fn parse_signal() { - assert_eq!(Signal::parse("SIGHUP"), Some(Signal::SIGHUP)); - assert_eq!(Signal::parse("sigwinch"), Some(Signal::SIGWINCH)); - assert_eq!(Signal::parse("TSTP"), Some(Signal::SIGTSTP)); - assert_eq!(Signal::parse("TstP"), Some(Signal::SIGTSTP)); - assert_eq!(Signal::parse("sigCONT"), Some(Signal::SIGCONT)); - assert_eq!(Signal::parse("SIGFOO"), None); - assert_eq!(Signal::parse(""), None); - assert_eq!(Signal::parse("SIG"), None); - assert_eq!(Signal::parse("سلام"), None); -} +add_test!("test_signal_name", || { + let sig = Signal::new(libc::SIGINT); + assert_eq!(sig.name(), "SIGINT"); +}); + +#[rustfmt::skip] +add_test!("test_signal_parse", || { + use crate::wchar_ext::ToWString; + assert_eq!(Signal::parse(L!("SIGHUP")), Some(Signal::new(libc::SIGHUP))); + assert_eq!(Signal::parse(L!("sigwinch")), Some(Signal::new(libc::SIGWINCH))); + assert_eq!(Signal::parse(L!("TSTP")), Some(Signal::new(libc::SIGTSTP))); + assert_eq!(Signal::parse(L!("TstP")), Some(Signal::new(libc::SIGTSTP))); + assert_eq!(Signal::parse(L!("sigCONT")), Some(Signal::new(libc::SIGCONT))); + assert_eq!(Signal::parse(L!("SIGFOO")), None); + assert_eq!(Signal::parse(L!("")), None); + assert_eq!(Signal::parse(L!("SIG")), None); + assert_eq!(Signal::parse(L!("سلام")), None); + + assert_eq!(Signal::parse(&libc::SIGINT.to_wstring()), Some(Signal::new(libc::SIGINT))); + assert_eq!(Signal::parse(L!("0")), None); + assert_eq!(Signal::parse(L!("-0")), None); + assert_eq!(Signal::parse(L!("-1")), None); +}); #[test] #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] diff --git a/fish-rust/src/wchar_ext.rs b/fish-rust/src/wchar_ext.rs index b5d91ccd9..c6715f10a 100644 --- a/fish-rust/src/wchar_ext.rs +++ b/fish-rust/src/wchar_ext.rs @@ -192,6 +192,13 @@ pub trait WExt { wstr::from_char_slice(&chars[start..]) } + /// Return a char slice up to a *char index*. + /// This is different from Rust string slicing, which takes a byte index. + fn slice_to(&self, end: usize) -> &wstr { + let chars = self.as_char_slice(); + wstr::from_char_slice(&chars[..end]) + } + /// Return the number of chars. /// This is different from Rust string len, which returns the number of bytes. fn char_count(&self) -> usize { diff --git a/fish-rust/src/wutil/gettext.rs b/fish-rust/src/wutil/gettext.rs index be282224f..fd48eef65 100644 --- a/fish-rust/src/wutil/gettext.rs +++ b/fish-rust/src/wutil/gettext.rs @@ -1,6 +1,7 @@ use crate::ffi; use crate::wchar::wstr; use crate::wchar_ffi::{wchar_t, wcslen}; +use widestring::U32CString; /// Support for wgettext. @@ -12,6 +13,12 @@ pub fn wgettext_impl_do_not_use_directly(text: &[wchar_t]) -> &'static wstr { wstr::from_slice(slice).expect("Invalid UTF-32") } +/// Get a (possibly translated) string from a non-literal. +pub fn wgettext_str(s: &wstr) -> &'static wstr { + let cstr: U32CString = U32CString::from_chars_truncate(s.as_char_slice()); + wgettext_impl_do_not_use_directly(cstr.as_slice_with_nul()) +} + /// Get a (possibly translated) string from a string literal. /// This returns a &'static wstr. macro_rules! wgettext { diff --git a/fish-rust/src/wutil/mod.rs b/fish-rust/src/wutil/mod.rs index 752022f43..63e77b341 100644 --- a/fish-rust/src/wutil/mod.rs +++ b/fish-rust/src/wutil/mod.rs @@ -14,7 +14,7 @@ use crate::flog::FLOGF; use crate::wchar::{wstr, WString, L}; use crate::wchar_ext::WExt; use crate::wcstringutil::{join_strings, split_string, wcs2string_callback}; -pub(crate) use gettext::{wgettext, wgettext_fmt}; +pub(crate) use gettext::{wgettext, wgettext_fmt, wgettext_str}; use libc::{ DT_BLK, DT_CHR, DT_DIR, DT_FIFO, DT_LNK, DT_REG, DT_SOCK, EACCES, EIO, ELOOP, ENAMETOOLONG, ENODEV, ENOENT, ENOTDIR, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, diff --git a/src/reader.cpp b/src/reader.cpp index ce6fe49f5..0e25a45f6 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -1002,10 +1002,12 @@ static relaxed_atomic_t s_exit_state{exit_state_t::none}; /// This is set from a signal handler. static volatile sig_atomic_t s_sighup_received{false}; +extern "C" { void reader_sighup() { // Beware, we may be in a signal handler. s_sighup_received = true; } +} static void redirect_tty_after_sighup() { // If we have received SIGHUP, redirect the tty to avoid a user script triggering SIGTTIN or @@ -1256,8 +1258,10 @@ void reader_data_t::kill(editable_line_t *el, size_t begin_idx, size_t length, i erase_substring(el, begin_idx, length); } +extern "C" { // This is called from a signal handler! void reader_handle_sigint() { interrupted = SIGINT; } +} /// Make sure buffers are large enough to hold the current string length. void reader_data_t::command_line_changed(const editable_line_t *el) { diff --git a/src/reader.h b/src/reader.h index d6a4b35cd..4d282be28 100644 --- a/src/reader.h +++ b/src/reader.h @@ -144,7 +144,9 @@ class editable_line_t { int reader_read(parser_t &parser, int fd, const io_chain_t &io); /// Mark that we encountered SIGHUP and must (soon) exit. This is invoked from a signal handler. +extern "C" { void reader_sighup(); +} /// Initialize the reader. void reader_init(); @@ -254,7 +256,9 @@ void reader_push(parser_t &parser, const wcstring &history_name, reader_config_t void reader_pop(); /// The readers interrupt signal handler. Cancels all currently running blocks. +extern "C" { void reader_handle_sigint(); +} /// \return whether fish is currently unwinding the stack in preparation to exit. bool fish_is_unwinding_for_exit();