use crate::ui::out;
use crossterm::{cursor, execute, terminal};
use std::io::Write;
use std::sync::{Mutex, MutexGuard, Once};
enum Restore {
Screen,
Prompt(Saved),
}
#[cfg(unix)]
type Saved = Option<libc::termios>;
#[cfg(not(unix))]
struct Saved;
static NEEDS: Mutex<Option<Restore>> = Mutex::new(None);
fn needs() -> MutexGuard<'static, Option<Restore>> {
NEEDS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub struct Guard(());
impl Guard {
fn arm(restore: Restore) -> Self {
static LISTENING: Once = Once::new();
LISTENING.call_once(listen);
*needs() = Some(restore);
block_signals(true);
Guard(())
}
}
impl Drop for Guard {
fn drop(&mut self) {
block_signals(false);
*needs() = None;
}
}
pub fn protect_screen() -> Guard {
Guard::arm(Restore::Screen)
}
pub fn protect_prompt() -> Guard {
Guard::arm(Restore::Prompt(save_mode()))
}
pub fn restore_screen(out: &mut impl Write) {
let _ = terminal::disable_raw_mode();
let _ = execute!(out, cursor::Show, terminal::LeaveAlternateScreen);
}
fn restore_prompt(out: &mut impl Write, saved: &Saved) {
let _ = execute!(out, cursor::Show);
put_back_mode(saved);
}
#[cfg(unix)]
fn save_mode() -> Saved {
let mut mode = std::mem::MaybeUninit::<libc::termios>::uninit();
let read = unsafe { libc::tcgetattr(libc::STDIN_FILENO, mode.as_mut_ptr()) } == 0;
read.then(|| unsafe { mode.assume_init() })
}
#[cfg(unix)]
fn put_back_mode(saved: &Saved) {
if let Some(mode) = saved {
unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, mode) };
}
}
#[cfg(not(unix))]
fn save_mode() -> Saved {
Saved
}
#[cfg(not(unix))]
fn put_back_mode(_: &Saved) {}
fn put_right() {
match needs().as_ref() {
Some(Restore::Screen) => restore_screen(&mut out::stdout_unrecorded()),
Some(Restore::Prompt(saved)) => restore_prompt(&mut out::stderr_unrecorded(), saved),
None => {}
}
}
#[cfg(unix)]
fn block_signals(block: bool) {
unsafe {
let mut set = std::mem::MaybeUninit::<libc::sigset_t>::uninit();
libc::sigemptyset(set.as_mut_ptr());
for signal in LISTENED {
libc::sigaddset(set.as_mut_ptr(), signal);
}
let how = if block {
libc::SIG_BLOCK
} else {
libc::SIG_UNBLOCK
};
libc::pthread_sigmask(how, set.as_ptr(), std::ptr::null_mut());
}
}
#[cfg(not(unix))]
fn block_signals(_: bool) {}
#[cfg(unix)]
pub fn unblocked_in_child(command: &mut std::process::Command) -> &mut std::process::Command {
use std::os::unix::process::CommandExt;
unsafe { command.pre_exec(unblock_all) }
}
#[cfg(unix)]
fn unblock_all() -> std::io::Result<()> {
unsafe {
let mut set = std::mem::MaybeUninit::<libc::sigset_t>::uninit();
libc::sigemptyset(set.as_mut_ptr());
libc::pthread_sigmask(libc::SIG_SETMASK, set.as_ptr(), std::ptr::null_mut());
}
Ok(())
}
#[cfg(unix)]
const LISTENED: [i32; 4] = [
signal_hook::consts::SIGINT,
signal_hook::consts::SIGTERM,
signal_hook::consts::SIGHUP,
signal_hook::consts::SIGQUIT,
];
pub fn ctrl_c() {
serve(std::iter::once(2), exit_with);
}
fn exit_with(code: i32) {
#[cfg(unix)]
{
write_coverage();
let _ = signal_hook::low_level::emulate_default_handler(code - 128);
}
std::process::exit(code)
}
#[cfg(all(unix, coverage))]
fn write_coverage() {
extern "C" {
fn __llvm_profile_write_file() -> libc::c_int;
}
unsafe { __llvm_profile_write_file() };
}
#[cfg(all(unix, not(coverage)))]
fn write_coverage() {}
fn serve(signals: impl Iterator<Item = i32>, end: impl Fn(i32)) {
for signal in signals {
put_right();
end(128 + signal);
}
}
#[cfg(unix)]
fn listen() {
let _ = signal_hook::iterator::Signals::new(LISTENED)
.map(|mut signals| std::thread::spawn(move || serve(signals.forever(), exit_with)));
}
#[cfg(windows)]
fn listen() {
let _ = ctrlc::set_handler(ctrl_c);
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
#[test]
fn restoring_the_screen_shows_the_cursor_and_leaves_the_alternate_screen() {
let mut out = Vec::new();
restore_screen(&mut out);
assert_eq!(String::from_utf8(out).unwrap(), "\x1b[?25h\x1b[?1049l");
}
#[test]
fn restoring_a_prompt_shows_the_cursor() {
let mut out = Vec::new();
restore_prompt(&mut out, &save_mode());
assert_eq!(String::from_utf8(out).unwrap(), "\x1b[?25h");
}
#[test]
fn a_signal_ends_proc_with_128_plus_its_number_and_a_guard_clears_the_state() {
let ended = RefCell::new(Vec::new());
serve([1, 2, 3, 15].into_iter(), |code| {
ended.borrow_mut().push(code)
});
assert_eq!(*ended.borrow(), [129, 130, 131, 143]);
*needs() = Some(Restore::Prompt(save_mode()));
assert!(needs().is_some());
drop(Guard(()));
assert!(needs().is_none());
}
#[cfg(unix)]
#[test]
fn a_child_started_while_the_signals_are_blocked_can_still_be_ended() {
use std::os::unix::process::ExitStatusExt;
block_signals(true);
let child = unblocked_in_child(&mut std::process::Command::new("sleep"))
.arg("60")
.spawn();
let _ = unblock_all();
let mut child = child.unwrap();
unsafe { libc::kill(child.id() as i32, libc::SIGTERM) };
let start = std::time::Instant::now();
let status = child.wait().unwrap();
assert_eq!(status.signal(), Some(libc::SIGTERM));
assert!(start.elapsed() < std::time::Duration::from_secs(10));
}
}