Skip to main content

hotl_platform/console/
unix.rs

1//! `termios` + `sigaction`, in async-signal-safe calls only.
2
3use super::{ConsoleControl, HandlerContract};
4use std::io;
5use std::sync::atomic::{AtomicUsize, Ordering};
6
7#[derive(Debug, Clone, Copy, Default)]
8pub struct UnixConsoleControl;
9
10impl UnixConsoleControl {
11    pub const fn new() -> Self {
12        Self
13    }
14}
15
16impl crate::sealed::Sealed for UnixConsoleControl {}
17
18/// The cooked modes, `Copy` so the caller can hold them in a `OnceLock` rather
19/// than leaking a `Box` for the handler to read.
20#[derive(Clone, Copy)]
21pub struct UnixModes(libc::termios);
22
23// SAFETY: `termios` is a plain repr(C) struct of integers with no interior
24// pointers; sharing a copy across threads is sound.
25unsafe impl Send for UnixModes {}
26unsafe impl Sync for UnixModes {}
27
28/// The callback, as a plain function pointer so the handler allocates nothing.
29/// `usize` because `AtomicPtr` would need a concrete pointee type.
30static ON_INTERRUPT: AtomicUsize = AtomicUsize::new(0);
31
32/// The signals that kill a foreground TUI outright. `SIGQUIT` is left alone: it
33/// is the deliberate "core-dump this" escape hatch.
34const TRAPPED: [libc::c_int; 3] = [libc::SIGINT, libc::SIGTERM, libc::SIGHUP];
35
36extern "C" fn on_signal(signal: libc::c_int) {
37    let f = ON_INTERRUPT.load(Ordering::SeqCst);
38    if f != 0 {
39        // SAFETY: only ever stored from `trap`, and only ever a `fn()`.
40        let f: fn() = unsafe { std::mem::transmute::<usize, fn()>(f) };
41        f();
42    }
43    // SAFETY: `_exit` is async-signal-safe; `exit` is not.
44    unsafe { libc::_exit(128 + signal) };
45}
46
47impl ConsoleControl for UnixConsoleControl {
48    type Saved = UnixModes;
49
50    const HANDLER_CONTRACT: HandlerContract = HandlerContract::AsyncSignalSafe;
51
52    fn capture(&self) -> io::Result<Self::Saved> {
53        // SAFETY: `termios` is a plain repr(C) struct; all-zero is valid.
54        let mut modes: libc::termios = unsafe { std::mem::zeroed() };
55        // SAFETY: a live out-param and a standard fd.
56        if unsafe { libc::tcgetattr(libc::STDIN_FILENO, &mut modes) } != 0 {
57            return Err(io::Error::last_os_error());
58        }
59        Ok(UnixModes(modes))
60    }
61
62    fn restore(&self, saved: &Self::Saved) -> io::Result<()> {
63        // SAFETY: `tcsetattr` is async-signal-safe, which is what lets this be
64        // called from the handler.
65        if unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &saved.0) } != 0 {
66            return Err(io::Error::last_os_error());
67        }
68        Ok(())
69    }
70
71    fn write_raw(&self, bytes: &[u8]) {
72        // SAFETY: `write(2)` is async-signal-safe. A short or failed write has
73        // nothing to report from a signal context.
74        unsafe {
75            libc::write(libc::STDOUT_FILENO, bytes.as_ptr().cast(), bytes.len());
76        }
77    }
78
79    fn trap(&self, on_interrupt: fn()) -> io::Result<()> {
80        ON_INTERRUPT.store(on_interrupt as usize, Ordering::SeqCst);
81        for signal in TRAPPED {
82            // SAFETY: installing a handler that touches only async-signal-safe
83            // calls, per `HANDLER_CONTRACT`. Handlers are reset across `exec`,
84            // so spawned tools still get the default disposition.
85            unsafe {
86                let mut action: libc::sigaction = std::mem::zeroed();
87                action.sa_sigaction = on_signal as *const () as libc::sighandler_t;
88                libc::sigemptyset(&mut action.sa_mask);
89                action.sa_flags = libc::SA_RESTART;
90                if libc::sigaction(signal, &action, std::ptr::null_mut()) != 0 {
91                    return Err(io::Error::last_os_error());
92                }
93            }
94        }
95        Ok(())
96    }
97
98    fn interrupt_exit_code(&self) -> i32 {
99        128 + libc::SIGINT
100    }
101}