{"record":{"id":"1b95bed5644f534c","repo":"googleworkspace/cli","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":"error","filePath":"crates/google-workspace-cli/src/helpers/mod.rs","lineNumber":63,"sourceCode":"/// loop iterations.\npub(crate) async fn shutdown_signal() {\n    use std::sync::OnceLock;\n    use tokio::sync::Notify;\n\n    static NOTIFY: OnceLock<std::sync::Arc<Notify>> = OnceLock::new();\n\n    let notify = NOTIFY.get_or_init(|| {\n        let n = std::sync::Arc::new(Notify::new());\n        let n2 = n.clone();\n        tokio::spawn(async move {\n            #[cfg(unix)]\n            {\n                use tokio::signal::unix::{signal, SignalKind};\n                match signal(SignalKind::terminate()) {\n                    Ok(mut sigterm) => {\n                        tokio::select! {\n                            res = tokio::signal::ctrl_c() => {\n                                res.expect(\"failed to listen for SIGINT\");\n                            }\n                            Some(_) = sigterm.recv() => {}\n                        }\n                    }\n                    Err(e) => {\n                        eprintln!(\n                            \"warning: could not register SIGTERM handler: {e}. \\\n                             Listening for Ctrl+C only.\"\n                        );\n                        tokio::signal::ctrl_c()\n                            .await\n                            .expect(\"failed to listen for SIGINT\");\n                    }\n                }\n            }\n            #[cfg(not(unix))]\n            {\n                tokio::signal::ctrl_c()","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/googleworkspace/cli/blob/a3768d0e82ad83cca2da97724e46bea4ff0e6dbd/crates/google-workspace-cli/src/helpers/mod.rs#L45-L81","documentation":"In shutdown_signal() (helpers/mod.rs) the SIGINT future's Result is unwrapped with .expect inside a spawned background task. tokio::signal::ctrl_c() returns Err only when the OS-level handler for SIGINT cannot be installed. The expect therefore panics inside the helper task; the Notify is never fired, so any long-running helper loop (gws gmail watch, gws events subscribe) that awaits shutdown_signal() hangs instead of exiting, and the panic may abort the process under panic=abort builds.","triggerScenarios":"Running under a supervisor that starts the process with SIGINT set to SIG_IGN (nohup, some init systems, docker-run --init edge cases, CI runners) — tokio/signal-hook refuses to install a handler over an inherited ignore disposition; a seccomp/container profile blocking the rt_sigaction syscall so handler registration fails; exotic platforms where signal(7) registration is unavailable.","commonSituations":"gws gmail watch launched via nohup or inside a wrapper script that ignores SIGINT; deployment under a process manager documented to inherit ignore dispositions; sandboxed CI executors with syscall filters; minimal containers that break signal setup.","solutions":["Ensure the parent process does not ignore SIGINT: avoid nohup-style inheritance, or reset the disposition to default at startup (unsafe { libc::signal(libc::SIGINT, libc::SIG_DFL) }) before entering the async runtime","In containers/sandboxes, allow the rt_sigaction syscall and use a standard init (docker --init, tini) so signal dispositions are sane","As a maintainer, replace .expect with a graceful branch: log a warning on Err and still call n2.notify_waiters() (or exit the process) so the CLI cannot hang; today the panic in the detached task leaves notify.notified().await pending forever","If hitting it operationally, kill the hung process with SIGKILL since neither SIGINT nor the shutdown path will complete once the listener task is dead"],"exampleFix":"// before\nres = tokio::signal::ctrl_c() => {\n    res.expect(\"failed to listen for SIGINT\");\n}\n\n// after\nres = tokio::signal::ctrl_c() => {\n    if let Err(e) = res {\n        eprintln!(\"warning: SIGINT listener unavailable: {e}\");\n        n2.notify_waiters(); // let shutdown_signal() return instead of hanging\n    }\n}","handlingStrategy":"validation","validationCode":"// Reset an inherited SIG_IGN disposition before entering the tokio runtime,\n// so ctrl_c()/SIGTERM registration cannot fail later.\n#[cfg(unix)]\nfn ensure_default_signal_dispositions() {\n    unsafe {\n        libc::signal(libc::SIGINT, libc::SIG_DFL);\n        libc::signal(libc::SIGTERM, libc::SIG_DFL);\n    }\n}\n\nfn main() {\n    #[cfg(unix)]\n    ensure_default_signal_dispositions();\n    // then build the runtime and run the CLI\n}","typeGuard":null,"tryCatchPattern":"// shutdown_signal() itself cannot error; guard the wrapper so a dead\n// listener task cannot hang the loop forever:\ntokio::select! {\n    _ = shutdown_signal() => { /* graceful exit */ }\n    _ = tokio::time::sleep(std::time::Duration::from_secs(max_runtime)) => {\n        eprintln!(\"shutdown signal never arrived; exiting on timeout\");\n    }\n}","preventionTips":["Do not launch gws long-running helpers (gmail watch, events subscribe) from wrappers that ignore SIGINT (nohup-style inheritance); prefer exec so dispositions stay default","In containers, permit signal-setup syscalls in the seccomp profile and use an init process (--init/tini)","Until the expect is replaced upstream, pair shutdown_signal() with a timeout or external supervisor stop so a panicked listener task cannot wedge the process"],"tags":["signal","unix","tokio","panic","shutdown"],"backgroundTag":"signal-handler-registration-failed","analyzedSha":"a3768d0e82ad83cca2da97724e46bea4ff0e6dbd","analyzedAt":"2026-08-16T19:51:46.516Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}