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

  1. 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)
  2. Inspect the container/unit security config: remove signal masking in Docker/K8s or adjust systemd SignalMask/RestrictSIGNALS settings
  3. 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
  4. 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
  5. 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

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


AI-assisted analysis of RightNow-AI/openfang@acf2587e46 (2026-09-02). Data as JSON: /api/errors/4471a66289d2de7f. Report an issue: GitHub.