{"record":{"id":"2fd74f4a2d51f3be","repo":"libnyanpasu/clash-nyanpasu","slug":"shutdown-hook-already-set","errorCode":null,"errorMessage":"Shutdown hook already set","messagePattern":"Shutdown hook already set","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/tauri/src/shutdown_hook.rs","lineNumber":33,"sourceCode":"        WNDCLASSEXW, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,\n    },\n};\n\nstatic SHUTDOWN_HOOK_INSTANCE: OnceCell<std::sync::mpsc::Sender<()>> = OnceCell::new();\n\n#[atomic_enum]\n#[derive(PartialEq, Eq)]\npub enum ShutdownState {\n    Idle,\n    CleaningUp,\n    ReadyForShutdown,\n}\n\nstatic SHUTDOWN_STATE: AtomicShutdownState = AtomicShutdownState::new(ShutdownState::Idle);\n\npub fn setup_shutdown_hook(f: impl Fn() + Send + Sync + 'static) -> anyhow::Result<()> {\n    if SHUTDOWN_HOOK_INSTANCE.get().is_some() {\n        anyhow::bail!(\"Shutdown hook already set\");\n    }\n    let (initd_tx, initd_rx) = oneshot::channel();\n    let handle = std::thread::spawn(move || setup_shutdown_hook_inner(f, initd_tx));\n    if let Err(oneshot::RecvError) = initd_rx.recv() {\n        handle\n            .join()\n            .map_err(|_| anyhow::anyhow!(\"Failed to join the shutdown hook thread\"))??;\n    }\n    Ok(())\n}\n\n#[allow(dead_code)]\nstruct WindowHandle {\n    hwnd: HWND,\n    h_instance: HINSTANCE,\n}\n\nimpl Drop for WindowHandle {","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/libnyanpasu/clash-nyanpasu/blob/f7dbce2997c633e484f54788035e770b3ee99773/backend/tauri/src/shutdown_hook.rs#L15-L51","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: may be called twice\nsetup_shutdown_hook(on_shutdown)?;\n// after: idempotent registration at call site\nstatic REGISTERED: OnceLock<()> = OnceLock::new();\nif REGISTERED.set(()).is_ok() {\n    setup_shutdown_hook(on_shutdown)?;\n}","handlingStrategy":"try-catch","validationCode":"fn can_register_shutdown_hook() -> bool {\n    SHUTDOWN_HOOK_INSTANCE.get().is_none()\n}","typeGuard":null,"tryCatchPattern":"if let Err(e) = setup_shutdown_hook(on_shutdown) {\n    if e.to_string().contains(\"already set\") {\n        log::debug!(\"shutdown hook already registered; ignoring\");\n    } else {\n        return Err(e);\n    }\n}","preventionTips":["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"],"tags":["lifecycle","initialization","shutdown","singleton"],"backgroundTag":"module-init-failed","analyzedSha":"f7dbce2997c633e484f54788035e770b3ee99773","analyzedAt":"2026-09-08T01:24:59.197Z","contentChangedAt":"2026-09-08T01:24:59.197Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}