From d8e7702dc5dac1dae9c0aa74bcd970779fa11764 Mon Sep 17 00:00:00 2001 From: Iddo Gino Date: Mon, 28 Sep 2026 20:55:31 -0700 Subject: [PATCH] fix(dev): forward termination signals to Electron child (#900) 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 43d0482..3698879 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -170,5 +170,32 @@ export function startElectron(root: string | undefined): ChildProcess { // successful exit. Map a signal death to a non-zero status instead. ps.on('close', (code, signal) => process.exit(code ?? (signal ? 1 : 0))) + 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 + } + } + }) + } +}