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
This commit is contained in:
Iddo Gino 2026-09-28 20:55:31 -07:00 committed by GitHub
parent 816c6dac61
commit d8e7702dc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -170,5 +170,32 @@ export function startElectron(root: string | undefined): ChildProcess {
// successful exit. Map a signal death to a non-zero status instead. // successful exit. Map a signal death to a non-zero status instead.
ps.on('close', (code, signal) => process.exit(code ?? (signal ? 1 : 0))) ps.on('close', (code, signal) => process.exit(code ?? (signal ? 1 : 0)))
forwardTerminationSignals(ps)
return 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
}
}
})
}
}