use std::future::Future;
#[cfg(unix)]
use tokio::signal::unix::{Signal, SignalKind, signal};
#[cfg(unix)]
struct TerminationSignals {
interrupt: Signal,
terminate: Signal,
}
#[cfg(unix)]
impl TerminationSignals {
fn register() -> std::io::Result<Self> {
Ok(Self {
interrupt: signal(SignalKind::interrupt())?,
terminate: signal(SignalKind::terminate())?,
})
}
async fn next(&mut self) {
let received = tokio::select! {
received = self.interrupt.recv() => received,
received = self.terminate.recv() => received,
};
if received.is_none() {
std::future::pending::<()>().await;
}
}
}
#[cfg(unix)]
pub(crate) fn shutdown_signal() -> crate::Result<impl Future<Output = ()>> {
let mut signals = TerminationSignals::register()?;
Ok(async move { signals.next().await })
}
#[cfg(not(unix))]
pub(crate) fn shutdown_signal() -> crate::Result<impl Future<Output = ()>> {
Ok(async {
if let Err(e) = tokio::signal::ctrl_c().await {
tracing::warn!(target: "phoxal.runtime", error = %e, "failed to listen for ctrl-c");
}
})
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::time::Duration;
#[expect(
clippy::expect_used,
reason = "test support: a handler that will not install is the test's own failure to report"
)]
async fn resolves_on(signal: libc::c_int) {
let mut signals = TerminationSignals::register().expect("both handlers install");
assert_eq!(
unsafe { libc::raise(signal) },
0,
"raising signal {signal} at this process must succeed"
);
tokio::time::timeout(Duration::from_secs(5), signals.next())
.await
.unwrap_or_else(|_| panic!("signal {signal} must resolve the shutdown trigger"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial_test::serial(process_signals)]
async fn sigterm_triggers_shutdown() {
resolves_on(libc::SIGTERM).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial_test::serial(process_signals)]
async fn sigint_triggers_shutdown() {
resolves_on(libc::SIGINT).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial_test::serial(process_signals)]
async fn shutdown_future_registers_before_its_first_poll() {
let shutdown = shutdown_signal().expect("signal handlers install");
assert_eq!(unsafe { libc::raise(libc::SIGTERM) }, 0);
tokio::time::timeout(Duration::from_secs(5), shutdown)
.await
.expect("a startup signal must be queued before the wait is polled");
}
}