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
- Call setup_shutdown_hook exactly once, from a single composition-root/bootstrap location
- Guard the call site with an already-initialized check or make registration idempotent at the caller
- 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
- 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
- Register the hook once from a single bootstrap/composition-root location
- Make call sites idempotent (OnceLock guard) rather than relying on the error
- In tests, isolate hook registration per test process or inject a fake hook
- Never call setup from plugin/lib code that might be initialized twice
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
- Failed to get module handle: {err}
- legacy verge bridge is not managed
- application actor reply dropped
- update_systray unhandled error
- app_handle is not exist
AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08).
Data as JSON: /api/errors/2fd74f4a2d51f3be.
Report an issue: GitHub.