#[cfg(unix)]
use std::future::Future;
#[cfg(unix)]
use std::io;
use tokio_util::sync::CancellationToken;
#[cfg(unix)]
pub trait SignalSource: Send + 'static {
type Signal: Send;
fn register_sigint(&self) -> io::Result<Self::Signal>;
fn register_sigterm(&self) -> io::Result<Self::Signal>;
fn next_signal(sig: &mut Self::Signal) -> impl Future<Output = Option<()>> + Send;
}
#[cfg(unix)]
pub struct TokioUnixSignalSource;
#[cfg(unix)]
impl SignalSource for TokioUnixSignalSource {
type Signal = tokio::signal::unix::Signal;
fn register_sigint(&self) -> io::Result<Self::Signal> {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
}
fn register_sigterm(&self) -> io::Result<Self::Signal> {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
}
async fn next_signal(sig: &mut Self::Signal) -> Option<()> {
sig.recv().await
}
}
#[cfg(unix)]
pub fn spawn_signal_listener(token: CancellationToken) {
spawn_signal_listener_with(token, TokioUnixSignalSource);
}
#[cfg(not(unix))]
pub(crate) fn spawn_signal_listener(token: CancellationToken) {
tokio::spawn(async move {
if let Err(e) = tokio::signal::ctrl_c().await {
tracing::warn!(error = %e, "could not install Ctrl+C handler");
return;
}
tracing::info!("received Ctrl+C; cancelling MCP server");
token.cancel();
});
}
#[cfg(unix)]
pub fn spawn_signal_listener_with<S>(token: CancellationToken, source: S)
where
S: SignalSource,
{
tokio::spawn(async move {
let mut sigint = match source.register_sigint() {
Ok(s) => s,
Err(e) => {
tracing::warn!(error = %e, "could not install SIGINT handler");
return;
}
};
let mut sigterm = match source.register_sigterm() {
Ok(s) => s,
Err(e) => {
tracing::warn!(error = %e, "could not install SIGTERM handler");
return;
}
};
tokio::select! {
_ = S::next_signal(&mut sigint) => {
tracing::info!("received SIGINT; cancelling MCP server");
}
_ = S::next_signal(&mut sigterm) => {
tracing::info!("received SIGTERM; cancelling MCP server");
}
}
token.cancel();
});
}
#[cfg(test)]
#[cfg(unix)]
mod tests {
use super::*;
struct NeverFiringSource;
impl SignalSource for NeverFiringSource {
type Signal = ();
fn register_sigint(&self) -> io::Result<Self::Signal> {
Ok(())
}
fn register_sigterm(&self) -> io::Result<Self::Signal> {
Ok(())
}
async fn next_signal(_sig: &mut Self::Signal) -> Option<()> {
std::future::pending().await
}
}
#[tokio::test]
async fn listener_terminates_cleanly_when_both_registrations_succeed() {
let token = CancellationToken::new();
spawn_signal_listener_with(token.clone(), NeverFiringSource);
tokio::task::yield_now().await;
token.cancel();
}
#[tokio::test]
async fn production_source_registers_both_handlers_cleanly() {
let source = TokioUnixSignalSource;
let _sigint = source
.register_sigint()
.expect("SIGINT registration succeeds under tokio runtime");
let _sigterm = source
.register_sigterm()
.expect("SIGTERM registration succeeds under tokio runtime");
let token = CancellationToken::new();
spawn_signal_listener_with(token.clone(), TokioUnixSignalSource);
tokio::task::yield_now().await;
token.cancel();
}
}