Skip to content

Commit

Permalink
cli: Handle interrupts during an active spinner
Browse files Browse the repository at this point in the history
  • Loading branch information
RadsammyT authored and cloudhead committed Jul 18, 2024
1 parent a831e18 commit 1848c2b
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 14 deletions.
51 changes: 41 additions & 10 deletions radicle-signals/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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(())
}
Expand Down
14 changes: 12 additions & 2 deletions radicle-term/src/pager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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>(
Expand Down
39 changes: 37 additions & 2 deletions radicle-term/src/spinner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand All @@ -115,13 +120,21 @@ 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,
animation: impl io::Write + Send + 'static,
) -> 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({
Expand All @@ -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("<canceled>")
)
.ok();
drop(animation);
std::process::exit(-1);
}
Ok(_) => {}
Err(_) => {}
}
}
match &mut *progress {
Progress {
state: State::Running { cursor },
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 1848c2b

Please sign in to comment.