use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::OnceLock;
use std::thread::ThreadId;
use crossterm::cursor::Show;
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
static RESTORED: AtomicBool = AtomicBool::new(true);
static UI_THREAD: OnceLock<ThreadId> = OnceLock::new();
pub struct Session(());
impl Session {
pub fn enter() -> io::Result<Self> {
install_panic_hook();
#[cfg(unix)]
signals::install();
enable_raw_mode()?;
RESTORED.store(false, Ordering::SeqCst);
let session = Session(());
execute!(io::stdout(), EnterAlternateScreen)?;
Ok(session)
}
}
impl Drop for Session {
fn drop(&mut self) {
restore();
}
}
fn restore() {
if RESTORED.swap(true, Ordering::SeqCst) {
return;
}
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen, Show);
}
fn install_panic_hook() {
if UI_THREAD.set(std::thread::current().id()).is_err() {
return;
}
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if UI_THREAD.get() == Some(&std::thread::current().id()) {
restore();
}
previous(info);
}));
}
pub fn stop_signal() -> Option<i32> {
#[cfg(unix)]
{
signals::caught()
}
#[cfg(not(unix))]
{
None
}
}
#[cfg(unix)]
mod signals {
use std::sync::atomic::{AtomicI32, Ordering};
use std::time::Duration;
const STOP: [libc::c_int; 4] = [libc::SIGTERM, libc::SIGINT, libc::SIGHUP, libc::SIGQUIT];
const GRACE: Duration = Duration::from_secs(1);
static CAUGHT: AtomicI32 = AtomicI32::new(0);
static WAKE: AtomicI32 = AtomicI32::new(-1);
pub(super) fn caught() -> Option<i32> {
match CAUGHT.load(Ordering::SeqCst) {
0 => None,
sig => Some(sig),
}
}
extern "C" fn on_signal(sig: libc::c_int) {
let _ = CAUGHT.compare_exchange(0, sig, Ordering::SeqCst, Ordering::SeqCst);
let fd = WAKE.load(Ordering::SeqCst);
if fd >= 0 {
let byte = 1u8;
unsafe { libc::write(fd, (&byte as *const u8).cast(), 1) };
}
}
pub(super) fn install() {
if WAKE.load(Ordering::SeqCst) >= 0 {
return;
}
let mut fds = [-1 as libc::c_int; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
return;
}
for fd in fds {
unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) };
}
let [read_end, write_end] = fds;
let spawned = std::thread::Builder::new()
.name("signal-watchdog".into())
.spawn(move || watchdog(read_end));
if spawned.is_err() {
unsafe {
libc::close(read_end);
libc::close(write_end);
}
return;
}
WAKE.store(write_end, Ordering::SeqCst);
for sig in STOP {
unsafe {
let mut action: libc::sigaction = std::mem::zeroed();
action.sa_sigaction = on_signal as *const () as usize;
action.sa_flags = libc::SA_RESTART;
libc::sigemptyset(&mut action.sa_mask);
libc::sigaction(sig, &action, std::ptr::null_mut());
}
}
}
fn watchdog(fd: libc::c_int) {
let mut byte = 0u8;
loop {
let n = unsafe { libc::read(fd, (&mut byte as *mut u8).cast(), 1) };
if n == 1 {
break;
}
if n < 0 && std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted {
continue;
}
return;
}
std::thread::sleep(GRACE);
if !super::RESTORED.swap(true, Ordering::SeqCst) {
let _ = crossterm::terminal::disable_raw_mode();
let leave = b"\x1b[?1049l\x1b[?25h";
unsafe { libc::write(1, leave.as_ptr().cast(), leave.len()) };
}
let sig = caught().unwrap_or(libc::SIGTERM);
unsafe { libc::_exit(128 + sig) };
}
}