googleworkspace/cli · error

failed to listen for SIGINT

Error message

failed to listen for SIGINT

What it means

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.

Source

Thrown at crates/google-workspace-cli/src/helpers/mod.rs:63

/// loop iterations.
pub(crate) async fn shutdown_signal() {
    use std::sync::OnceLock;
    use tokio::sync::Notify;

    static NOTIFY: OnceLock<std::sync::Arc<Notify>> = OnceLock::new();

    let notify = NOTIFY.get_or_init(|| {
        let n = std::sync::Arc::new(Notify::new());
        let n2 = n.clone();
        tokio::spawn(async move {
            #[cfg(unix)]
            {
                use tokio::signal::unix::{signal, SignalKind};
                match signal(SignalKind::terminate()) {
                    Ok(mut sigterm) => {
                        tokio::select! {
                            res = tokio::signal::ctrl_c() => {
                                res.expect("failed to listen for SIGINT");
                            }
                            Some(_) = sigterm.recv() => {}
                        }
                    }
                    Err(e) => {
                        eprintln!(
                            "warning: could not register SIGTERM handler: {e}. \
                             Listening for Ctrl+C only."
                        );
                        tokio::signal::ctrl_c()
                            .await
                            .expect("failed to listen for SIGINT");
                    }
                }
            }
            #[cfg(not(unix))]
            {
                tokio::signal::ctrl_c()

View on GitHub (pinned to a3768d0e82)

Solutions

  1. 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
  2. In containers/sandboxes, allow the rt_sigaction syscall and use a standard init (docker --init, tini) so signal dispositions are sane
  3. 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
  4. 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

Example fix

// before
res = tokio::signal::ctrl_c() => {
    res.expect("failed to listen for SIGINT");
}

// after
res = tokio::signal::ctrl_c() => {
    if let Err(e) = res {
        eprintln!("warning: SIGINT listener unavailable: {e}");
        n2.notify_waiters(); // let shutdown_signal() return instead of hanging
    }
}
Defensive patterns

Strategy: validation

Validate before calling

// Reset an inherited SIG_IGN disposition before entering the tokio runtime,
// so ctrl_c()/SIGTERM registration cannot fail later.
#[cfg(unix)]
fn ensure_default_signal_dispositions() {
    unsafe {
        libc::signal(libc::SIGINT, libc::SIG_DFL);
        libc::signal(libc::SIGTERM, libc::SIG_DFL);
    }
}

fn main() {
    #[cfg(unix)]
    ensure_default_signal_dispositions();
    // then build the runtime and run the CLI
}

Try / catch

// shutdown_signal() itself cannot error; guard the wrapper so a dead
// listener task cannot hang the loop forever:
tokio::select! {
    _ = shutdown_signal() => { /* graceful exit */ }
    _ = tokio::time::sleep(std::time::Duration::from_secs(max_runtime)) => {
        eprintln!("shutdown signal never arrived; exiting on timeout");
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of googleworkspace/cli@a3768d0e82 (2026-08-16). Data as JSON: /api/errors/1b95bed5644f534c. Report an issue: GitHub.