Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli: Handle ctrl+c interrupts during an active spinner thread
Merged did:key:z6Mktnv1...8DHL opened 1 year ago

This is in response to issue# d2aeb57, where the cursor is not shown when interrupting rad when a progress spinner thread is present.

This is mainly achieved by adding signal handling for spinners now using radicle-signals and crossbeam-channel. When a SIGINT signal fires off while a spinner is active, it will drop its respective HideCursor object and exit the process entirely.

Some other somewhat related changes to this patch:

  • to better pass on handling to the OS/another CLI element, radicle-signals::uninstall() is implemented and used for the spinners.
    • the CLI pager also used radicle-signals, so that was also modified to use the above
  • doc changes to the spinner/pager to include the tidbits on signal handling
3 files changed +92 -14 a831e18a 1848c2b8
modified radicle-signals/src/lib.rs
@@ -33,6 +33,9 @@ impl TryFrom<i32> for Signal {
/// Signal notifications are sent via this channel.
static NOTIFY: Mutex<Option<chan::Sender<Signal>>> = 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<Signal>) -> io::Result<()> {
    if let Ok(mut channel) = NOTIFY.try_lock() {
@@ -54,23 +57,51 @@ pub fn install(notify: chan::Sender<Signal>) -> 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());
-
    }
-
    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());
+
    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::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(())
}
modified 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<E: Element + Send + 'static>(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<E: Element + Send + 'static>(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<E: Element>(
modified 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,10 @@ 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 +149,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("<canceled>")
+
                                )
+
                                .ok();
+
                                drop(animation);
+
                                std::process::exit(-1);
+
                            }
+
                            Ok(_) => {}
+
                            Err(_) => {}
+
                        }
+
                    }
                    match &mut *progress {
                        Progress {
                            state: State::Running { cursor },
@@ -192,6 +226,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.