From f7162326e294632f56c75bb1ff11a78e75ade0d7 Mon Sep 17 00:00:00 2001 From: Iddo Gino Date: Mon, 20 Apr 2026 16:33:34 -0700 Subject: [PATCH] fix(dev): forward termination signals to Electron child Spawn the Electron binary directly in dev/preview mode but forward SIGINT/SIGTERM/SIGHUP from the electron-vite parent to the child, matching Electron's own CLI wrapper (node_modules/electron/cli.js). Without this, Ctrl-C sends SIGINT to the foreground process group once: the Node parent exits immediately (no handler) and the Electron child gets only a single SIGINT. On macOS, an Electron app whose before-quit handler calls preventDefault() and later calls app.quit() can stall in AppKit's event loop in that path. Under a direct 'electron .' launch the CLI shim forwards SIGINT as well, so Electron gets it twice and exits cleanly. Matching that behavior here fixes the hang. Fixes #899 --- src/electron.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/electron.ts b/src/electron.ts index 5704504..2bb8bed 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -161,5 +161,32 @@ export function startElectron(root: string | undefined): ChildProcess { const ps = spawn(electronPath, [entry].concat(args), { stdio: 'inherit' }) ps.on('close', process.exit) + forwardTerminationSignals(ps) + return ps } + +let signalHandlersInstalled = false +let currentElectronPs: ChildProcess | undefined + +// Forward termination signals to the Electron child, matching electron's own +// CLI wrapper (node_modules/electron/cli.js). Without this, Ctrl-C kills the +// parent immediately and the Electron child can get stuck on macOS when a +// before-quit handler calls preventDefault then later app.quit(). See #899. +function forwardTerminationSignals(ps: ChildProcess): void { + currentElectronPs = ps + if (signalHandlersInstalled) return + signalHandlersInstalled = true + for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) { + process.on(signal, () => { + const target = currentElectronPs + if (target && target.exitCode === null && !target.killed) { + try { + target.kill(signal) + } catch { + // already gone + } + } + }) + } +}