pub struct ShutdownSignal {
#[cfg(unix)]
sigterm: Option<tokio::signal::unix::Signal>,
#[cfg(unix)]
sigint: Option<tokio::signal::unix::Signal>,
#[cfg(windows)]
ctrl_c: Option<tokio::signal::windows::CtrlC>,
}
impl ShutdownSignal {
pub fn new() -> Self {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
let sigterm = match signal(SignalKind::terminate()) {
Ok(sigterm) => Some(sigterm),
Err(e) => {
tracing::warn!(
"SIGTERM handler registration failed ({e}), SIGTERM will not be caught"
);
None
}
};
let sigint = match signal(SignalKind::interrupt()) {
Ok(sigint) => Some(sigint),
Err(e) => {
tracing::warn!(
"SIGINT handler registration failed ({e}), SIGINT will not be caught"
);
None
}
};
Self { sigterm, sigint }
}
#[cfg(windows)]
{
let ctrl_c = match tokio::signal::windows::ctrl_c() {
Ok(ctrl_c) => Some(ctrl_c),
Err(e) => {
tracing::warn!("Ctrl-C handler registration failed ({e})");
None
}
};
Self { ctrl_c }
}
#[cfg(not(any(unix, windows)))]
{
Self {}
}
}
pub async fn recv(&mut self) {
#[cfg(unix)]
{
match (self.sigterm.as_mut(), self.sigint.as_mut()) {
(Some(sigterm), Some(sigint)) => {
tokio::select! {
_ = sigterm.recv() => {},
_ = sigint.recv() => {},
}
}
(Some(sigterm), None) => {
sigterm.recv().await;
}
(None, Some(sigint)) => {
sigint.recv().await;
}
(None, None) => {
let _ = tokio::signal::ctrl_c().await;
}
}
}
#[cfg(windows)]
{
match self.ctrl_c.as_mut() {
Some(ctrl_c) => {
ctrl_c.recv().await;
}
None => {
let _ = tokio::signal::ctrl_c().await;
}
}
}
#[cfg(not(any(unix, windows)))]
{
let _ = tokio::signal::ctrl_c().await;
}
}
}
#[cfg(test)]
#[cfg(unix)]
mod tests {
use super::ShutdownSignal;
#[tokio::test]
async fn test_fresh_shutdown_signal_still_receives_sigterm_after_prior_instance_dropped() {
let earlier = ShutdownSignal::new();
drop(earlier);
let mut cleanup_signal = ShutdownSignal::new();
let pid = std::process::id();
let signal_sender = tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let status = std::process::Command::new("kill")
.arg("-TERM")
.arg(pid.to_string())
.status()
.unwrap();
assert!(status.success(), "`kill -TERM {pid}` must succeed");
});
let result =
tokio::time::timeout(std::time::Duration::from_secs(5), cleanup_signal.recv()).await;
signal_sender.await.unwrap();
assert!(
result.is_ok(),
"a freshly constructed ShutdownSignal must still receive a real SIGTERM sent after \
an earlier instance was dropped — this is the exact mechanism crate::shutdown's \
cleanup-window force-exit task depends on"
);
}
}