RightNow-AI/openfang · critical
Failed to listen for SIGINT
Error message
Failed to listen for SIGINT
What it means
During server startup the shutdown_signal task registers handlers for SIGINT (and SIGTERM) via tokio::signal::unix::signal, which panics with this .expect() message if the OS refuses to install the signal handler. This happens when the process has no permission to set a signal disposition for that signal or the runtime environment does not support it.
Source
Thrown at crates/openfang-api/src/server.rs:956
#[cfg(not(unix))]
fn restrict_permissions(_path: &Path) {}
/// Read daemon info from the standard location.
pub fn read_daemon_info(home_dir: &Path) -> Option<DaemonInfo> {
let info_path = home_dir.join("daemon.json");
let contents = std::fs::read_to_string(info_path).ok()?;
serde_json::from_str(&contents).ok()
}
/// Wait for an OS termination signal OR an API shutdown request.
///
/// On Unix: listens for SIGINT, SIGTERM, and API notify.
/// On Windows: listens for Ctrl+C and API notify.
async fn shutdown_signal(api_shutdown: Arc<tokio::sync::Notify>) {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
let mut sigint = signal(SignalKind::interrupt()).expect("Failed to listen for SIGINT");
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to listen for SIGTERM");
tokio::select! {
_ = sigint.recv() => {
info!("Received SIGINT (Ctrl+C), shutting down...");
}
_ = sigterm.recv() => {
info!("Received SIGTERM, shutting down...");
}
_ = api_shutdown.notified() => {
info!("Shutdown requested via API, shutting down...");
}
}
}
#[cfg(not(unix))]
{
tokio::select! {View on GitHub (pinned to acf2587e46)
Solutions
- Unblock/unmask SIGINT and SIGTERM for the process: check container signal masks, seccomp profiles, and any parent that blocked signals before exec (use a shell wrapper that resets the signal mask, e.g. exec your binary)
- Inspect the container/unit security config: remove signal masking in Docker/K8s or adjust systemd SignalMask/RestrictSIGNALS settings
- Avoid running the server as PID 1 with a masked SIGINT: use a supervisor (tini, dumb-init, or the runtime's init) as PID 1 and run the server as a child
- If the environment genuinely cannot install handlers, patch shutdown_signal to handle the registration error gracefully (log and rely on the api_shutdown Notify path) instead of .expect() panicking
- Verify with a quick test binary (tokio::signal::unix::signal(SignalKind::interrupt())) that signal registration works in the target environment before deploying
Example fix
// before
let mut sigint = signal(SignalKind::interrupt()).expect("Failed to listen for SIGINT");
// after
let mut sigint = signal(SignalKind::interrupt())
.map_err(|e| warn!("SIGINT handler unavailable ({e}); falling back to api shutdown only"))
.unwrap_or_else(|_| {
// derive a never-firing stream so select! still compiles
Box::pin(futures::stream::pending())
}); Defensive patterns
Strategy: try-catch
Validate before calling
// environment check before launching the daemon
// Probe that sigaction for SIGINT is permitted (small test binary or inline check)
if !signals_installable() { // e.g. run a probe: tokio::signal::unix::signal(SignalKind::interrupt())
eprintln!("refusing to start: SIGINT/SIGTERM handlers cannot be installed in this environment");
std::process::exit(1);
} Type guard
fn signals_installable() -> bool {
use tokio::signal::unix::{signal, SignalKind};
signal(SignalKind::interrupt()).is_ok() && signal(SignalKind::terminate()).is_ok()
} Try / catch
match std::panic::catch_unwind(|| { /* start run_daemon */ }) {
Err(p) if p.downcast_ref::<String>().map_or(false, |m| m.contains("Failed to listen for SIGINT")) => {
eprintln!("signal handlers blocked (container mask/seccomp?); run under an unmasked supervisor");
std::process::exit(78);
}
other => other.expect("daemon failed"),
} Prevention
- Never run the server as PID 1 with masked signals; use tini/dumb-init or the runtime's init as PID 1
- Check container signal masks (docker inspect MaskedPaths / signal config) and seccomp profiles before deploy
- Ensure the parent process does not block SIGINT/SIGTERM across exec
- Keep a graceful-shutdown fallback path (api_shutdown Notify / HTTP /health shutdown endpoint)
- Probe signal handler installation in CI on the same runtime image used in production
When it happens
Trigger: Running the API server in an environment where SIGINT/SIGTERM handling is blocked or already masked: a PID 1 process in a container with signals ignored/masked, a restricted seccomp/AppArmor profile, running as an unprivileged user in a hardened sandbox where sigaction is denied, or signals blocked by the parent before exec.
Common situations: Docker/Kubernetes containers that mask SIGINT in the container config (docker run --stop-signal mismatch or masked paths), CI runners with restricted syscall filters, systemd units with RestrictAddressFamilies/SignalMask settings, running under WSL or minimal distros with unusual signal setups.
Related errors
- Failed to listen for SIGTERM
- Failed to create Tokio runtime
- Failed to create tokio runtime for embedded server
- Failed to set listener to non-blocking
- Failed to convert std TcpListener to tokio
AI-assisted analysis of RightNow-AI/openfang@acf2587e46 (2026-09-02).
Data as JSON: /api/errors/4471a66289d2de7f.
Report an issue: GitHub.