libnyanpasu/clash-nyanpasu · error

Shutdown hook already set

Error message

Shutdown hook already set

What it means

The app installs exactly one process-wide shutdown hook (Ctrl+C / OS signal handler backed by a dedicated thread and a stored shutdown callback). setup_shutdown_hook guards with the SHUTDOWN_HOOK_INSTANCE static (OnceCell-like) and bails if a hook was already registered, because the OS only allows one handler and the stored callback slot can only be set once.

Source

Thrown at backend/tauri/src/shutdown_hook.rs:33

        WNDCLASSEXW, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
    },
};

static SHUTDOWN_HOOK_INSTANCE: OnceCell<std::sync::mpsc::Sender<()>> = OnceCell::new();

#[atomic_enum]
#[derive(PartialEq, Eq)]
pub enum ShutdownState {
    Idle,
    CleaningUp,
    ReadyForShutdown,
}

static SHUTDOWN_STATE: AtomicShutdownState = AtomicShutdownState::new(ShutdownState::Idle);

pub fn setup_shutdown_hook(f: impl Fn() + Send + Sync + 'static) -> anyhow::Result<()> {
    if SHUTDOWN_HOOK_INSTANCE.get().is_some() {
        anyhow::bail!("Shutdown hook already set");
    }
    let (initd_tx, initd_rx) = oneshot::channel();
    let handle = std::thread::spawn(move || setup_shutdown_hook_inner(f, initd_tx));
    if let Err(oneshot::RecvError) = initd_rx.recv() {
        handle
            .join()
            .map_err(|_| anyhow::anyhow!("Failed to join the shutdown hook thread"))??;
    }
    Ok(())
}

#[allow(dead_code)]
struct WindowHandle {
    hwnd: HWND,
    h_instance: HINSTANCE,
}

impl Drop for WindowHandle {

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Call setup_shutdown_hook exactly once, from a single composition-root/bootstrap location
  2. Guard the call site with an already-initialized check or make registration idempotent at the caller
  3. In tests, run each hook setup in a separate test process, or abstract the hook behind a trait and inject a fake instead of registering the real static
  4. If re-registration is required, refactor to allow replacing the stored callback rather than calling setup again

Example fix

// before: may be called twice
setup_shutdown_hook(on_shutdown)?;
// after: idempotent registration at call site
static REGISTERED: OnceLock<()> = OnceLock::new();
if REGISTERED.set(()).is_ok() {
    setup_shutdown_hook(on_shutdown)?;
}
Defensive patterns

Strategy: try-catch

Validate before calling

fn can_register_shutdown_hook() -> bool {
    SHUTDOWN_HOOK_INSTANCE.get().is_none()
}

Try / catch

if let Err(e) = setup_shutdown_hook(on_shutdown) {
    if e.to_string().contains("already set") {
        log::debug!("shutdown hook already registered; ignoring");
    } else {
        return Err(e);
    }
}

Prevention

When it happens

Trigger: Calling setup_shutdown_hook(f) a second time in the same process — e.g. calling it in both app setup and a plugin/initialization path, or re-running setup after an app restart within the same process, or tests calling it per-test without process isolation.

Common situations: Double initialization during app bootstrap (two modules each call setup); integration tests that call setup in multiple tests within one process; conditional code paths that both end up registering the hook.

Related errors


AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/2fd74f4a2d51f3be. Report an issue: GitHub.