{"record":{"id":"4471a66289d2de7f","repo":"RightNow-AI/openfang","slug":"failed-to-listen-for-sigint","errorCode":null,"errorMessage":"Failed to listen for SIGINT","messagePattern":"Failed to listen for SIGINT","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/openfang-api/src/server.rs","lineNumber":956,"sourceCode":"#[cfg(not(unix))]\nfn restrict_permissions(_path: &Path) {}\n\n/// Read daemon info from the standard location.\npub fn read_daemon_info(home_dir: &Path) -> Option<DaemonInfo> {\n    let info_path = home_dir.join(\"daemon.json\");\n    let contents = std::fs::read_to_string(info_path).ok()?;\n    serde_json::from_str(&contents).ok()\n}\n\n/// Wait for an OS termination signal OR an API shutdown request.\n///\n/// On Unix: listens for SIGINT, SIGTERM, and API notify.\n/// On Windows: listens for Ctrl+C and API notify.\nasync fn shutdown_signal(api_shutdown: Arc<tokio::sync::Notify>) {\n    #[cfg(unix)]\n    {\n        use tokio::signal::unix::{signal, SignalKind};\n        let mut sigint = signal(SignalKind::interrupt()).expect(\"Failed to listen for SIGINT\");\n        let mut sigterm = signal(SignalKind::terminate()).expect(\"Failed to listen for SIGTERM\");\n\n        tokio::select! {\n            _ = sigint.recv() => {\n                info!(\"Received SIGINT (Ctrl+C), shutting down...\");\n            }\n            _ = sigterm.recv() => {\n                info!(\"Received SIGTERM, shutting down...\");\n            }\n            _ = api_shutdown.notified() => {\n                info!(\"Shutdown requested via API, shutting down...\");\n            }\n        }\n    }\n\n    #[cfg(not(unix))]\n    {\n        tokio::select! {","sourceCodeStart":938,"sourceCodeEnd":974,"githubUrl":"https://github.com/RightNow-AI/openfang/blob/acf2587e46be174c10200489c9a2d23a39a98aeb/crates/openfang-api/src/server.rs#L938-L974","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet mut sigint = signal(SignalKind::interrupt()).expect(\"Failed to listen for SIGINT\");\n// after\nlet mut sigint = signal(SignalKind::interrupt())\n    .map_err(|e| warn!(\"SIGINT handler unavailable ({e}); falling back to api shutdown only\"))\n    .unwrap_or_else(|_| {\n        // derive a never-firing stream so select! still compiles\n        Box::pin(futures::stream::pending())\n    });","handlingStrategy":"try-catch","validationCode":"// environment check before launching the daemon\n// Probe that sigaction for SIGINT is permitted (small test binary or inline check)\nif !signals_installable() { // e.g. run a probe: tokio::signal::unix::signal(SignalKind::interrupt())\n    eprintln!(\"refusing to start: SIGINT/SIGTERM handlers cannot be installed in this environment\");\n    std::process::exit(1);\n}","typeGuard":"fn signals_installable() -> bool {\n    use tokio::signal::unix::{signal, SignalKind};\n    signal(SignalKind::interrupt()).is_ok() && signal(SignalKind::terminate()).is_ok()\n}","tryCatchPattern":"match std::panic::catch_unwind(|| { /* start run_daemon */ }) {\n    Err(p) if p.downcast_ref::<String>().map_or(false, |m| m.contains(\"Failed to listen for SIGINT\")) => {\n        eprintln!(\"signal handlers blocked (container mask/seccomp?); run under an unmasked supervisor\");\n        std::process::exit(78);\n    }\n    other => other.expect(\"daemon failed\"),\n}","preventionTips":["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"],"tags":["signals","tokio","unix","shutdown","container"],"backgroundTag":"signal-handler-registration-failed","analyzedSha":"acf2587e46be174c10200489c9a2d23a39a98aeb","analyzedAt":"2026-09-02T22:42:28.464Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}