diff --git a/radicle-signals/src/lib.rs b/radicle-signals/src/lib.rs index 98591cd5..5cabe19f 100644 --- a/radicle-signals/src/lib.rs +++ b/radicle-signals/src/lib.rs @@ -33,6 +33,9 @@ impl TryFrom for Signal { /// Signal notifications are sent via this channel. static NOTIFY: Mutex>> = Mutex::new(None); +/// A slice of signals to handle. +const SIGNALS: &[i32] = &[libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGWINCH]; + /// Install global signal handlers. pub fn install(notify: chan::Sender) -> io::Result<()> { if let Ok(mut channel) = NOTIFY.try_lock() { @@ -54,23 +57,51 @@ pub fn install(notify: chan::Sender) -> io::Result<()> { Ok(()) } +/// Uninstall global signal handlers. +pub fn uninstall() -> io::Result<()> { + if let Ok(mut channel) = NOTIFY.try_lock() { + if channel.is_none() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "signal handler is already uninstalled", + )); + } + *channel = None; + + unsafe { _uninstall() }?; + } else { + return Err(io::Error::new( + io::ErrorKind::WouldBlock, + "unable to uninstall signal handler", + )); + } + Ok(()) +} + /// Install global signal handlers. /// /// # Safety /// /// Calls `libc` functions safely. unsafe fn _install() -> io::Result<()> { - if libc::signal(libc::SIGTERM, handler as libc::sighandler_t) == libc::SIG_ERR { - return Err(io::Error::last_os_error()); + for signal in SIGNALS { + if libc::signal(*signal, handler as libc::sighandler_t) == libc::SIG_ERR { + return Err(io::Error::last_os_error()); + } } - if libc::signal(libc::SIGINT, handler as libc::sighandler_t) == libc::SIG_ERR { - return Err(io::Error::last_os_error()); - } - if libc::signal(libc::SIGHUP, handler as libc::sighandler_t) == libc::SIG_ERR { - return Err(io::Error::last_os_error()); - } - if libc::signal(libc::SIGWINCH, handler as libc::sighandler_t) == libc::SIG_ERR { - return Err(io::Error::last_os_error()); + Ok(()) +} + +/// Uninstall global signal handlers. +/// +/// # Safety +/// +/// Calls `libc` functions safely. +unsafe fn _uninstall() -> io::Result<()> { + for signal in SIGNALS { + if libc::signal(*signal, libc::SIG_DFL) == libc::SIG_ERR { + return Err(io::Error::last_os_error()); + } } Ok(()) } diff --git a/radicle-term/src/pager.rs b/radicle-term/src/pager.rs index 9f971d3e..a783e501 100644 --- a/radicle-term/src/pager.rs +++ b/radicle-term/src/pager.rs @@ -24,6 +24,12 @@ pub enum Error { /// A pager for the given element. Re-renders the element when the terminal is resized so that /// it doesn't wrap. If the output device is not a TTY, just prints the element via /// [`Element::print`]. +/// +/// # Signal Handling +/// +/// This will install handlers for the pager until finished by the user, with there +/// being only one element handling signals at a time. If the pager cannot install +/// handlers, then it will return with an error. pub fn page(element: E) -> Result<(), Error> { let (events_tx, events_rx) = chan::unbounded(); let (signals_tx, signals_rx) = chan::unbounded(); @@ -35,9 +41,13 @@ pub fn page(element: E) -> Result<(), Error> { events_tx.send(e).ok(); } }); - thread::spawn(move || main(element, signals_rx, events_rx)) + let result = thread::spawn(move || main(element, signals_rx, events_rx)) .join() - .unwrap() + .unwrap(); + + signals::uninstall()?; + + result } fn main( diff --git a/radicle-term/src/spinner.rs b/radicle-term/src/spinner.rs index 3a6564e5..78fe712d 100644 --- a/radicle-term/src/spinner.rs +++ b/radicle-term/src/spinner.rs @@ -3,6 +3,11 @@ use std::mem::ManuallyDrop; use std::sync::{Arc, Mutex}; use std::{fmt, io, thread, time}; +use crossbeam_channel as chan; + +use radicle_signals as signals; +use signals::Signal; + use crate::io::{ERROR_PREFIX, WARNING_PREFIX}; use crate::Paint; @@ -103,10 +108,10 @@ impl Spinner { } /// Create a new spinner with the given message. Sends animation output to `stderr` and success or -/// failure messages to `stdout`. +/// failure messages to `stdout`. This function handles signals, with there being only one +/// element handling signals at a time, and is a wrapper to [`spinner_to()`]. pub fn spinner(message: impl ToString) -> Spinner { let (stdout, stderr) = (io::stdout(), io::stderr()); - if stderr.is_terminal() { spinner_to(message, stdout, stderr) } else { @@ -115,6 +120,12 @@ pub fn spinner(message: impl ToString) -> Spinner { } /// Create a new spinner with the given message, and send output to the given writers. +/// +/// # Signal Handling +/// +/// This will install handlers for the spinner until cancelled or dropped, with there +/// being only one element handling signals at a time. If the spinner cannot install +/// handlers, then it will not attempt to install handlers again, and continue running. pub fn spinner_to( message: impl ToString, mut completion: impl io::Write + Send + 'static, @@ -122,6 +133,8 @@ pub fn spinner_to( ) -> Spinner { let message = message.to_string(); let progress = Arc::new(Mutex::new(Progress::new(Paint::new(message)))); + let (sig_tx, sig_rx) = chan::unbounded(); + let sig_result = signals::install(sig_tx); let handle = thread::Builder::new() .name(String::from("spinner")) .spawn({ @@ -134,6 +147,25 @@ pub fn spinner_to( let Ok(mut progress) = progress.lock() else { break; }; + // If were unable to install handles, skip signal processing entirely. + if sig_result.is_ok() { + match sig_rx.try_recv() { + Ok(sig) if sig == Signal::Interrupt || sig == Signal::Terminate => { + write!(animation, "\r{}", termion::clear::UntilNewline).ok(); + writeln!( + completion, + "{ERROR_PREFIX} {} {}", + &progress.message, + Paint::red("") + ) + .ok(); + drop(animation); + std::process::exit(-1); + } + Ok(_) => {} + Err(_) => {} + } + } match &mut *progress { Progress { state: State::Running { cursor }, @@ -192,6 +224,9 @@ pub fn spinner_to( drop(progress); thread::sleep(DEFAULT_TICK); } + if sig_result.is_ok() { + let _ = signals::uninstall(); + } } }) // SAFETY: Only panics if the thread name contains `null` bytes, which isn't the case here.