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.
|
/// Signal notifications are sent via this channel.
|
||||||
static NOTIFY: Mutex<Option<chan::Sender<Signal>>> = Mutex::new(None);
|
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.
|
/// Install global signal handlers.
|
||||||
pub fn install(notify: chan::Sender<Signal>) -> io::Result<()> {
|
pub fn install(notify: chan::Sender<Signal>) -> io::Result<()> {
|
||||||
if let Ok(mut channel) = NOTIFY.try_lock() {
|
if let Ok(mut channel) = NOTIFY.try_lock() {
|
||||||
|
|
@ -54,23 +57,51 @@ pub fn install(notify: chan::Sender<Signal>) -> io::Result<()> {
|
||||||
Ok(())
|
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.
|
/// Install global signal handlers.
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
///
|
///
|
||||||
/// Calls `libc` functions safely.
|
/// Calls `libc` functions safely.
|
||||||
unsafe fn _install() -> io::Result<()> {
|
unsafe fn _install() -> io::Result<()> {
|
||||||
if libc::signal(libc::SIGTERM, handler as libc::sighandler_t) == libc::SIG_ERR {
|
for signal in SIGNALS {
|
||||||
return Err(io::Error::last_os_error());
|
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 {
|
Ok(())
|
||||||
return Err(io::Error::last_os_error());
|
}
|
||||||
}
|
|
||||||
if libc::signal(libc::SIGHUP, handler as libc::sighandler_t) == libc::SIG_ERR {
|
/// Uninstall global signal handlers.
|
||||||
return Err(io::Error::last_os_error());
|
///
|
||||||
}
|
/// # Safety
|
||||||
if libc::signal(libc::SIGWINCH, handler as libc::sighandler_t) == libc::SIG_ERR {
|
///
|
||||||
return Err(io::Error::last_os_error());
|
/// 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(())
|
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
|
/// 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
|
/// it doesn't wrap. If the output device is not a TTY, just prints the element via
|
||||||
/// [`Element::print`].
|
/// [`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> {
|
pub fn page<E: Element + Send + 'static>(element: E) -> Result<(), Error> {
|
||||||
let (events_tx, events_rx) = chan::unbounded();
|
let (events_tx, events_rx) = chan::unbounded();
|
||||||
let (signals_tx, signals_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();
|
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()
|
.join()
|
||||||
.unwrap()
|
.unwrap();
|
||||||
|
|
||||||
|
signals::uninstall()?;
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main<E: Element>(
|
fn main<E: Element>(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@ use std::mem::ManuallyDrop;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::{fmt, io, thread, time};
|
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::io::{ERROR_PREFIX, WARNING_PREFIX};
|
||||||
use crate::Paint;
|
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
|
/// 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 {
|
pub fn spinner(message: impl ToString) -> Spinner {
|
||||||
let (stdout, stderr) = (io::stdout(), io::stderr());
|
let (stdout, stderr) = (io::stdout(), io::stderr());
|
||||||
|
|
||||||
if stderr.is_terminal() {
|
if stderr.is_terminal() {
|
||||||
spinner_to(message, stdout, stderr)
|
spinner_to(message, stdout, stderr)
|
||||||
} else {
|
} 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.
|
/// 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(
|
pub fn spinner_to(
|
||||||
message: impl ToString,
|
message: impl ToString,
|
||||||
mut completion: impl io::Write + Send + 'static,
|
mut completion: impl io::Write + Send + 'static,
|
||||||
|
|
@ -122,6 +133,8 @@ pub fn spinner_to(
|
||||||
) -> Spinner {
|
) -> Spinner {
|
||||||
let message = message.to_string();
|
let message = message.to_string();
|
||||||
let progress = Arc::new(Mutex::new(Progress::new(Paint::new(message))));
|
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()
|
let handle = thread::Builder::new()
|
||||||
.name(String::from("spinner"))
|
.name(String::from("spinner"))
|
||||||
.spawn({
|
.spawn({
|
||||||
|
|
@ -134,6 +147,25 @@ pub fn spinner_to(
|
||||||
let Ok(mut progress) = progress.lock() else {
|
let Ok(mut progress) = progress.lock() else {
|
||||||
break;
|
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 {
|
match &mut *progress {
|
||||||
Progress {
|
Progress {
|
||||||
state: State::Running { cursor },
|
state: State::Running { cursor },
|
||||||
|
|
@ -192,6 +224,9 @@ pub fn spinner_to(
|
||||||
drop(progress);
|
drop(progress);
|
||||||
thread::sleep(DEFAULT_TICK);
|
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.
|
// SAFETY: Only panics if the thread name contains `null` bytes, which isn't the case here.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue