use anyhow::{Context, Result};
static SAVED_TERMIOS_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
static SAVED_TERMIOS: std::sync::atomic::AtomicPtr<libc::termios> =
std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
static SAVED_TERMIOS_INSTALLED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
extern "C" fn terminal_restore_signal_handler(sig: libc::c_int) {
let fd = SAVED_TERMIOS_FD.load(std::sync::atomic::Ordering::Acquire);
if fd >= 0 {
let ptr = SAVED_TERMIOS.load(std::sync::atomic::Ordering::Acquire);
if !ptr.is_null() {
unsafe {
libc::tcsetattr(fd, libc::TCSANOW, ptr);
}
}
}
unsafe {
libc::raise(sig);
}
}
pub(crate) struct TerminalRawGuard {
original: nix::sys::termios::Termios,
fd: std::os::unix::io::RawFd,
prev_sigint: libc::sigaction,
prev_sigterm: libc::sigaction,
prev_sigquit: libc::sigaction,
prev_sigabrt: libc::sigaction,
prev_sigfpe: libc::sigaction,
}
impl TerminalRawGuard {
pub(crate) fn enter() -> Result<Self> {
use nix::sys::termios::{self, SetArg};
use std::os::unix::io::AsRawFd;
if SAVED_TERMIOS_INSTALLED
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::AcqRel,
std::sync::atomic::Ordering::Acquire,
)
.is_err()
{
anyhow::bail!(
"TerminalRawGuard already installed; only one raw-mode guard may be live at a time"
);
}
let fd = std::io::stdin().as_raw_fd();
let borrowed = unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd) };
let original = match termios::tcgetattr(borrowed).context("tcgetattr") {
Ok(t) => t,
Err(e) => {
SAVED_TERMIOS_INSTALLED.store(false, std::sync::atomic::Ordering::Release);
return Err(e);
}
};
let mut raw = original.clone();
termios::cfmakeraw(&mut raw);
if let Err(e) = termios::tcsetattr(borrowed, SetArg::TCSANOW, &raw).context("tcsetattr raw")
{
SAVED_TERMIOS_INSTALLED.store(false, std::sync::atomic::Ordering::Release);
return Err(e);
}
let boxed: Box<libc::termios> = Box::new(original.clone().into());
let ptr = Box::into_raw(boxed);
SAVED_TERMIOS.store(ptr, std::sync::atomic::Ordering::Release);
SAVED_TERMIOS_FD.store(fd, std::sync::atomic::Ordering::Release);
let mut prev_sigint: libc::sigaction = unsafe { std::mem::zeroed() };
let mut prev_sigterm: libc::sigaction = unsafe { std::mem::zeroed() };
let mut prev_sigquit: libc::sigaction = unsafe { std::mem::zeroed() };
let mut prev_sigabrt: libc::sigaction = unsafe { std::mem::zeroed() };
let mut prev_sigfpe: libc::sigaction = unsafe { std::mem::zeroed() };
unsafe {
let mut sa: libc::sigaction = std::mem::zeroed();
sa.sa_sigaction = terminal_restore_signal_handler as *const () as usize;
sa.sa_flags = libc::SA_RESETHAND;
libc::sigemptyset(&mut sa.sa_mask);
libc::sigaction(libc::SIGINT, &sa, &mut prev_sigint);
libc::sigaction(libc::SIGTERM, &sa, &mut prev_sigterm);
libc::sigaction(libc::SIGQUIT, &sa, &mut prev_sigquit);
libc::sigaction(libc::SIGABRT, &sa, &mut prev_sigabrt);
libc::sigaction(libc::SIGFPE, &sa, &mut prev_sigfpe);
}
Ok(Self {
original,
fd,
prev_sigint,
prev_sigterm,
prev_sigquit,
prev_sigabrt,
prev_sigfpe,
})
}
}
impl Drop for TerminalRawGuard {
fn drop(&mut self) {
SAVED_TERMIOS_FD.store(-1, std::sync::atomic::Ordering::Release);
let borrowed = unsafe { std::os::unix::io::BorrowedFd::borrow_raw(self.fd) };
let _ = nix::sys::termios::tcsetattr(
borrowed,
nix::sys::termios::SetArg::TCSANOW,
&self.original,
);
SAVED_TERMIOS.store(std::ptr::null_mut(), std::sync::atomic::Ordering::Release);
SAVED_TERMIOS_INSTALLED.store(false, std::sync::atomic::Ordering::Release);
unsafe {
libc::sigaction(libc::SIGINT, &self.prev_sigint, std::ptr::null_mut());
libc::sigaction(libc::SIGTERM, &self.prev_sigterm, std::ptr::null_mut());
libc::sigaction(libc::SIGQUIT, &self.prev_sigquit, std::ptr::null_mut());
libc::sigaction(libc::SIGABRT, &self.prev_sigabrt, std::ptr::null_mut());
libc::sigaction(libc::SIGFPE, &self.prev_sigfpe, std::ptr::null_mut());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn terminal_raw_guard_double_enter_fails() {
let mut master: libc::c_int = 0;
let mut slave: libc::c_int = 0;
let rc = unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null(),
std::ptr::null(),
)
};
assert_eq!(rc, 0, "openpty failed: {}", std::io::Error::last_os_error());
let saved_stdin = unsafe { libc::dup(0) };
assert!(saved_stdin >= 0);
assert_eq!(unsafe { libc::dup2(slave, 0) }, 0);
let first = TerminalRawGuard::enter().expect("first enter must succeed");
let second = TerminalRawGuard::enter();
let err_msg = match second {
Ok(_) => panic!("second concurrent enter must fail the INSTALLED CAS"),
Err(e) => e.to_string(),
};
assert!(
err_msg.contains("already installed"),
"error message should name the double-install condition, got: {err_msg}"
);
drop(first);
let third = TerminalRawGuard::enter()
.expect("third enter must succeed after Drop clears INSTALLED");
drop(third);
unsafe {
libc::dup2(saved_stdin, 0);
libc::close(saved_stdin);
libc::close(slave);
libc::close(master);
}
}
#[test]
fn terminal_raw_guard_enter_drop_cycle() {
let mut master: libc::c_int = 0;
let mut slave: libc::c_int = 0;
let rc = unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null(),
std::ptr::null(),
)
};
assert_eq!(rc, 0, "openpty failed: {}", std::io::Error::last_os_error());
let saved_stdin = unsafe { libc::dup(0) };
assert!(saved_stdin >= 0);
assert_eq!(unsafe { libc::dup2(slave, 0) }, 0);
for i in 0..3 {
let guard = TerminalRawGuard::enter()
.unwrap_or_else(|e| panic!("enter iteration {i} must succeed, got: {e}"));
drop(guard);
}
unsafe {
libc::dup2(saved_stdin, 0);
libc::close(saved_stdin);
libc::close(slave);
libc::close(master);
}
}
#[test]
fn terminal_raw_guard_installs_and_restores_sigabrt_handler() {
let mut master: libc::c_int = 0;
let mut slave: libc::c_int = 0;
let rc = unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null(),
std::ptr::null(),
)
};
assert_eq!(rc, 0, "openpty failed: {}", std::io::Error::last_os_error());
let saved_stdin = unsafe { libc::dup(0) };
assert!(saved_stdin >= 0);
assert_eq!(unsafe { libc::dup2(slave, 0) }, 0);
let mut pre_test: libc::sigaction = unsafe { std::mem::zeroed() };
let mut ign: libc::sigaction = unsafe { std::mem::zeroed() };
ign.sa_sigaction = libc::SIG_IGN;
unsafe {
libc::sigemptyset(&mut ign.sa_mask);
libc::sigaction(libc::SIGABRT, &ign, &mut pre_test);
}
let mut current: libc::sigaction = unsafe { std::mem::zeroed() };
unsafe {
libc::sigaction(libc::SIGABRT, std::ptr::null(), &mut current);
}
assert_eq!(
current.sa_sigaction,
libc::SIG_IGN,
"test setup: SIG_IGN sentinel must be installed before enter()",
);
let guard = TerminalRawGuard::enter().expect("enter must succeed");
unsafe {
libc::sigaction(libc::SIGABRT, std::ptr::null(), &mut current);
}
assert_ne!(
current.sa_sigaction,
libc::SIG_IGN,
"enter() must replace the SIGABRT SIG_IGN sentinel with its own handler",
);
assert_ne!(
current.sa_sigaction,
libc::SIG_DFL,
"enter() must not leave SIGABRT at SIG_DFL",
);
let expected = terminal_restore_signal_handler as *const () as usize;
assert_eq!(
current.sa_sigaction, expected,
"enter() must point SIGABRT at terminal_restore_signal_handler",
);
drop(guard);
unsafe {
libc::sigaction(libc::SIGABRT, std::ptr::null(), &mut current);
}
assert_eq!(
current.sa_sigaction,
libc::SIG_IGN,
"Drop must restore the previous SIGABRT SIG_IGN sentinel",
);
unsafe {
libc::sigaction(libc::SIGABRT, &pre_test, std::ptr::null_mut());
libc::dup2(saved_stdin, 0);
libc::close(saved_stdin);
libc::close(slave);
libc::close(master);
}
}
#[test]
fn terminal_raw_guard_installs_and_restores_sigfpe_handler() {
let mut master: libc::c_int = 0;
let mut slave: libc::c_int = 0;
let rc = unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null(),
std::ptr::null(),
)
};
assert_eq!(rc, 0, "openpty failed: {}", std::io::Error::last_os_error());
let saved_stdin = unsafe { libc::dup(0) };
assert!(saved_stdin >= 0);
assert_eq!(unsafe { libc::dup2(slave, 0) }, 0);
let mut pre_test: libc::sigaction = unsafe { std::mem::zeroed() };
let mut ign: libc::sigaction = unsafe { std::mem::zeroed() };
ign.sa_sigaction = libc::SIG_IGN;
unsafe {
libc::sigemptyset(&mut ign.sa_mask);
libc::sigaction(libc::SIGFPE, &ign, &mut pre_test);
}
let mut current: libc::sigaction = unsafe { std::mem::zeroed() };
unsafe {
libc::sigaction(libc::SIGFPE, std::ptr::null(), &mut current);
}
assert_eq!(
current.sa_sigaction,
libc::SIG_IGN,
"test setup: SIG_IGN sentinel must be installed before enter()",
);
let guard = TerminalRawGuard::enter().expect("enter must succeed");
unsafe {
libc::sigaction(libc::SIGFPE, std::ptr::null(), &mut current);
}
assert_ne!(
current.sa_sigaction,
libc::SIG_IGN,
"enter() must replace the SIGFPE SIG_IGN sentinel with its own handler",
);
assert_ne!(
current.sa_sigaction,
libc::SIG_DFL,
"enter() must not leave SIGFPE at SIG_DFL",
);
let expected = terminal_restore_signal_handler as *const () as usize;
assert_eq!(
current.sa_sigaction, expected,
"enter() must point SIGFPE at terminal_restore_signal_handler",
);
drop(guard);
unsafe {
libc::sigaction(libc::SIGFPE, std::ptr::null(), &mut current);
}
assert_eq!(
current.sa_sigaction,
libc::SIG_IGN,
"Drop must restore the previous SIGFPE SIG_IGN sentinel",
);
unsafe {
libc::sigaction(libc::SIGFPE, &pre_test, std::ptr::null_mut());
libc::dup2(saved_stdin, 0);
libc::close(saved_stdin);
libc::close(slave);
libc::close(master);
}
}
}