use std::sync::Arc;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use tokio::sync::watch;
const DOUBLE_SIGINT_WINDOW: Duration = Duration::from_secs(5);
pub(crate) fn install() -> watch::Receiver<bool> {
let (sender, receiver) = watch::channel(false);
let last_signal: Arc<Mutex<Option<Instant>>> = Arc::new(Mutex::new(None));
tokio::spawn(handler_task(sender, last_signal));
receiver
}
async fn handler_task(sender: watch::Sender<bool>, last_signal: Arc<Mutex<Option<Instant>>>) {
loop {
match tokio::signal::ctrl_c().await {
Ok(()) => {
let now = Instant::now();
let mut guard = match last_signal.lock() {
Ok(g) => g,
Err(poison) => poison.into_inner(),
};
let window_secs = DOUBLE_SIGINT_WINDOW.as_secs();
if let Some(prev) = *guard {
if now.duration_since(prev) <= DOUBLE_SIGINT_WINDOW {
tracing::warn!(
event.name = "cli.sigint.hard_exit",
window_secs,
"second SIGINT received within {window_secs}s; hard-exiting with code 130",
);
std::process::exit(crate::exit::HARD_SIGINT);
}
}
*guard = Some(now);
drop(guard);
tracing::debug!(
event.name = "cli.sigint.received",
"SIGINT received; draining gracefully",
);
let _ = crate::output::write_stderr_line(&format!(
"Stopping listeners gracefully (Ctrl+C again within {window_secs}s to force exit)..."
));
if sender.send(true).is_err() {
return;
}
}
Err(e) => {
tracing::warn!(
event.name = "cli.sigint.install_failed",
error = %e,
"could not install Ctrl+C handler; graceful shutdown unavailable"
);
std::future::pending::<()>().await;
unreachable!("std::future::pending never resolves");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn double_sigint_window_is_five_seconds() {
assert_eq!(DOUBLE_SIGINT_WINDOW, Duration::from_secs(5));
}
}