bitrouter 1.0.0-alpha.31

BitRouter: an LLM API router. CLI + assembly library.
Documentation
//! The one exit that is this process's rather than the terminal's.
//!
//! Entering and restoring the terminal live in [`bitrouter_tui::lifecycle`],
//! because they are properties of a terminal and that crate is what draws to
//! one. Signals are not: which of them exist, and what a process wants to do
//! about them, is a fact about *this* binary. So the registration stays here,
//! beside [`session`](super::session) — its only caller.

/// The signals a terminal session has to survive, registered once.
///
/// SIGINT is registered too: `kill -2` is a common supervisor default, and
/// leaving it on the default disposition means the one signal users reach for
/// most is the one that breaks their shell.
#[cfg(unix)]
struct ShutdownSignals {
    interrupt: tokio::signal::unix::Signal,
    terminate: tokio::signal::unix::Signal,
    hangup: tokio::signal::unix::Signal,
    suspend: tokio::signal::unix::Signal,
}

/// A process signal that changes terminal custody.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalSignal {
    /// End the interactive process after its ordinary cleanup path runs.
    Shutdown,
    /// Restore terminal modes, stop, then reacquire them after `SIGCONT`.
    Suspend,
}

#[cfg(unix)]
impl ShutdownSignals {
    /// Register once, before the loop.
    ///
    /// Constructing these per iteration — the obvious `select!` spelling —
    /// silently drops signals: a `Signal` only observes what arrives after it
    /// exists, and the first registration has already replaced the default
    /// disposition. A signal delivered while the loop was awaiting some other
    /// branch would then neither kill the process nor end the loop.
    fn install() -> std::io::Result<Self> {
        use tokio::signal::unix::{SignalKind, signal};
        Ok(Self {
            interrupt: signal(SignalKind::interrupt())?,
            terminate: signal(SignalKind::terminate())?,
            hangup: signal(SignalKind::hangup())?,
            suspend: signal(SignalKind::from_raw(rustix::process::Signal::TSTP.as_raw()))?,
        })
    }

    /// Resolve when any of them fires.
    async fn recv(&mut self) -> TerminalSignal {
        tokio::select! {
            _ = self.interrupt.recv() => TerminalSignal::Shutdown,
            _ = self.terminate.recv() => TerminalSignal::Shutdown,
            _ = self.hangup.recv() => TerminalSignal::Shutdown,
            _ = self.suspend.recv() => TerminalSignal::Suspend,
        }
    }
}

/// Stop this process after terminal custody has been released.
///
/// `SIGSTOP` cannot be intercepted, so execution resumes immediately after
/// this call only when the shell or supervisor delivers `SIGCONT`.
#[cfg(unix)]
pub fn suspend_current_process() -> std::io::Result<()> {
    Ok(rustix::process::kill_process(
        rustix::process::getpid(),
        rustix::process::Signal::STOP,
    )?)
}

#[cfg(not(unix))]
pub fn suspend_current_process() -> std::io::Result<()> {
    Err(std::io::Error::new(
        std::io::ErrorKind::Unsupported,
        "process suspension is unavailable on this platform",
    ))
}

/// The third exit, as a `select!` arm.
///
/// Normal teardown and a panic both run Rust code that can reach
/// [`bitrouter_tui::lifecycle::restore`]. A signal runs none: `kill`, a
/// supervisor stopping the process, or a closed terminal window sending SIGHUP
/// would otherwise end it with the terminal still in raw mode.
///
/// Registration that fails, and every platform without these signals, resolve
/// **never** rather than immediately: an arm that fired at once would end the
/// session on its first poll, which is worse than the exit it is guarding.
pub struct Shutdown {
    #[cfg(unix)]
    signals: Option<ShutdownSignals>,
}

impl Shutdown {
    /// Register once, before the loop.
    pub fn install() -> Self {
        Self {
            #[cfg(unix)]
            signals: ShutdownSignals::install().ok(),
        }
    }

    /// Resolve when one of them fires.
    pub async fn recv(&mut self) -> TerminalSignal {
        #[cfg(unix)]
        if let Some(signals) = self.signals.as_mut() {
            return signals.recv().await;
        }
        std::future::pending().await
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The signal arm must be *quiet*. It sits in a `select!` next to the
    /// futures that do the session's work, and one that resolved on its first
    /// poll would end every session the instant it started — including on the
    /// platforms and sandboxes where registration is not available at all.
    #[tokio::test]
    async fn the_shutdown_arm_does_not_fire_on_its_own() {
        let mut shutdown = Shutdown::install();
        let fired =
            tokio::time::timeout(std::time::Duration::from_millis(50), shutdown.recv()).await;
        assert!(
            fired.is_err(),
            "the shutdown arm resolved with no signal delivered"
        );
    }
}