diff --git a/apps/desktop/scripts/dev-electron.mjs b/apps/desktop/scripts/dev-electron.mjs index 3ccebb280228ec9f4b98df1a4b28ba3dfd373a97..03ac96b5a070e5e3a12acb02693ef27bc678c672 100644 --- a/apps/desktop/scripts/dev-electron.mjs +++ b/apps/desktop/scripts/dev-electron.mjs @@ -1,3 +1,5 @@ +import { spawnSync } from "node:child_process"; + import electronmon from "electronmon"; import waitOn from "wait-on"; @@ -18,7 +20,7 @@ await waitOn({ const childEnv = { ...process.env }; delete childEnv.ELECTRON_RUN_AS_NODE; -await electronmon({ +const monitor = await electronmon({ cwd: desktopDir, args: ["dist-electron/main.js"], env: { @@ -27,3 +29,45 @@ await electronmon({ }, electronPath: resolveElectronPath(), }); + +let shuttingDown = false; + +function killChildTree(signal) { + if (process.platform === "win32") { + return; + } + + // electronmon may leave Electron orphaned when parent shutdown races; kill direct children as fallback. + spawnSync("pkill", [`-${signal}`, "-P", String(process.pid)], { stdio: "ignore" }); +} + +async function shutdown(exitCode) { + if (shuttingDown) return; + shuttingDown = true; + + try { + await Promise.race([ + monitor.destroy(), + new Promise((resolve) => setTimeout(resolve, 1_500)), + ]); + } catch { + // Best effort only; fallback process kill below handles stubborn children. + } + + killChildTree("TERM"); + setTimeout(() => { + killChildTree("KILL"); + }, 1_200).unref(); + + process.exit(exitCode); +} + +process.once("SIGINT", () => { + void shutdown(130); +}); +process.once("SIGTERM", () => { + void shutdown(143); +}); +process.once("SIGHUP", () => { + void shutdown(129); +}); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index d647bbd2e0d17da8a885c7b03ff122804faf2fdc..fcc120a460ddd758b2fd1153cb3a2577d06de29a 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -164,10 +164,10 @@ function stopBackend(): void { backendProcess = null; if (!child) return; - if (!child.killed) { + if (child.exitCode === null && child.signalCode === null) { child.kill("SIGTERM"); setTimeout(() => { - if (!child.killed) { + if (child.exitCode === null && child.signalCode === null) { child.kill("SIGKILL"); } }, 2_000).unref(); @@ -336,3 +336,19 @@ app.on("window-all-closed", () => { app.quit(); } }); + +if (process.platform !== "win32") { + process.on("SIGINT", () => { + if (isQuitting) return; + isQuitting = true; + stopBackend(); + app.quit(); + }); + + process.on("SIGTERM", () => { + if (isQuitting) return; + isQuitting = true; + stopBackend(); + app.quit(); + }); +}