cli: Handle interrupts during an active spinner
This commit is contained in:
parent
a831e18a72
commit
1848c2b85d
|
|
@ -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());
|
||||
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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
|
|
|
|||
|
|
@ -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("<canceled>")
|
||||
)
|
||||
.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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue