use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
pub const SIGINT_NUM: i32 = 2;
#[allow(
dead_code,
reason = "consumed by spawn_unix_listener; tests use it cross-platform"
)]
pub const SIGHUP_NUM: i32 = 1;
pub const SIGTERM_NUM: i32 = 15;
#[must_use]
pub const fn exit_code_from_signal(signum: Option<i32>) -> i32 {
match signum {
None => 0,
Some(s) => 128 + s,
}
}
#[allow(
dead_code,
reason = "called by spawn_unix_listener; tests use it cross-platform"
)]
#[must_use]
pub const fn signal_name(signum: i32) -> &'static str {
match signum {
SIGHUP_NUM => "SIGHUP",
SIGINT_NUM => "SIGINT",
SIGTERM_NUM => "SIGTERM",
_ => "unknown",
}
}
pub struct SignalListener {
received: Arc<AtomicI32>,
}
impl SignalListener {
pub fn install(cancel: CancellationToken) -> std::io::Result<Self> {
let received = Arc::new(AtomicI32::new(0));
#[cfg(unix)]
install_unix(&received, &cancel)?;
#[cfg(not(unix))]
install_windows(&received, &cancel)?;
drop(cancel);
Ok(Self { received })
}
#[must_use]
pub fn received(&self) -> Option<i32> {
let n = self.received.load(Ordering::SeqCst);
if n == 0 {
None
} else {
Some(n)
}
}
#[must_use]
pub fn exit_code(&self) -> i32 {
exit_code_from_signal(self.received())
}
}
#[cfg(unix)]
fn install_unix(received: &Arc<AtomicI32>, cancel: &CancellationToken) -> std::io::Result<()> {
use tokio::signal::unix::SignalKind;
spawn_unix_listener(SignalKind::interrupt(), SIGINT_NUM, received, cancel)?;
spawn_unix_listener(SignalKind::terminate(), SIGTERM_NUM, received, cancel)?;
spawn_unix_listener(SignalKind::hangup(), SIGHUP_NUM, received, cancel)?;
spawn_winch_logger()?;
Ok(())
}
#[cfg(unix)]
fn spawn_unix_listener(
kind: tokio::signal::unix::SignalKind,
signum: i32,
received: &Arc<AtomicI32>,
cancel: &CancellationToken,
) -> std::io::Result<()> {
let mut sig = tokio::signal::unix::signal(kind)?;
let received = Arc::clone(received);
let cancel = cancel.clone();
tokio::spawn(async move {
loop {
if sig.recv().await.is_none() {
break;
}
let _ = received.compare_exchange(0, signum, Ordering::SeqCst, Ordering::SeqCst);
tracing::info!(signal = signal_name(signum), "received signal");
cancel.cancel();
}
});
Ok(())
}
#[cfg(unix)]
fn spawn_winch_logger() -> std::io::Result<()> {
let mut sig = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::window_change())?;
tokio::spawn(async move {
while sig.recv().await.is_some() {
tracing::debug!("SIGWINCH (terminal resize)");
}
});
Ok(())
}
#[cfg(not(unix))]
fn install_windows(received: &Arc<AtomicI32>, cancel: &CancellationToken) -> std::io::Result<()> {
use tokio::signal::windows::{ctrl_break, ctrl_c};
let mut intr = ctrl_c()?;
{
let received = Arc::clone(received);
let cancel = cancel.clone();
tokio::spawn(async move {
while intr.recv().await.is_some() {
let _ =
received.compare_exchange(0, SIGINT_NUM, Ordering::SeqCst, Ordering::SeqCst);
tracing::info!(signal = "Ctrl-C", "received signal");
cancel.cancel();
}
});
}
let mut brk = ctrl_break()?;
{
let received = Arc::clone(received);
let cancel = cancel.clone();
tokio::spawn(async move {
while brk.recv().await.is_some() {
let _ =
received.compare_exchange(0, SIGTERM_NUM, Ordering::SeqCst, Ordering::SeqCst);
tracing::info!(signal = "Ctrl-Break", "received signal");
cancel.cancel();
}
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::Ordering;
#[test]
fn no_signal_means_exit_code_zero() {
assert_eq!(exit_code_from_signal(None), 0);
}
#[test]
fn sigint_produces_exit_code_130() {
assert_eq!(exit_code_from_signal(Some(SIGINT_NUM)), 130);
}
#[test]
fn sigterm_produces_exit_code_143() {
assert_eq!(exit_code_from_signal(Some(SIGTERM_NUM)), 143);
}
#[test]
fn sighup_produces_exit_code_129() {
assert_eq!(exit_code_from_signal(Some(SIGHUP_NUM)), 129);
}
#[test]
fn signum_constants_match_posix() {
assert_eq!(SIGHUP_NUM, 1);
assert_eq!(SIGINT_NUM, 2);
assert_eq!(SIGTERM_NUM, 15);
}
#[test]
fn signal_name_covers_handled_signals() {
assert_eq!(signal_name(SIGINT_NUM), "SIGINT");
assert_eq!(signal_name(SIGTERM_NUM), "SIGTERM");
assert_eq!(signal_name(SIGHUP_NUM), "SIGHUP");
assert_eq!(signal_name(99), "unknown");
}
#[tokio::test]
async fn install_returns_listener_with_no_signal_received() {
let cancel = CancellationToken::new();
let listener = SignalListener::install(cancel).expect("install");
assert_eq!(listener.received(), None);
assert_eq!(listener.exit_code(), 0);
}
#[test]
fn received_and_exit_code_project_from_atomic() {
let received = Arc::new(AtomicI32::new(0));
let listener = SignalListener {
received: received.clone(),
};
assert_eq!(listener.received(), None);
assert_eq!(listener.exit_code(), 0);
received.store(SIGTERM_NUM, Ordering::SeqCst);
assert_eq!(listener.received(), Some(SIGTERM_NUM));
assert_eq!(listener.exit_code(), 143);
}
}