fix: close orphaned dev Electron app after Ctrl+C (#498)

This commit is contained in:
Jinwoo Hong 2026-04-11 18:50:21 -04:00 committed by GitHub
parent aeb8002df7
commit f43bf4f90e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 30 additions and 2 deletions

View File

@ -10,6 +10,7 @@ vi.mock('electron', () => {
paths.set(name, value)
}),
quit: vi.fn(),
exit: vi.fn(),
commandLine: {
appendSwitch: vi.fn()
}
@ -50,6 +51,7 @@ describe('installDevParentDisconnectQuit', () => {
const { app } = await import('electron')
const { installDevParentDisconnectQuit } = await import('./configure-process')
vi.useFakeTimers()
const originalSend = process.send
const originalOnce = process.once.bind(process)
const disconnectHandlers: (() => void)[] = []
@ -74,6 +76,10 @@ describe('installDevParentDisconnectQuit', () => {
expect(disconnectHandlers).toHaveLength(1)
disconnectHandlers[0]()
expect(app.quit).toHaveBeenCalledTimes(1)
expect(app.exit).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(3000)
expect(app.exit).toHaveBeenCalledWith(0)
})
it('does not register the disconnect hook outside dev ipc launches', async () => {
@ -104,6 +110,7 @@ describe('installDevParentWatchdog', () => {
vi.useFakeTimers()
vi.mocked(app.quit).mockClear()
vi.mocked(app.exit).mockClear()
let parentExists = true
vi.spyOn(process, 'kill').mockImplementation(((
@ -132,6 +139,10 @@ describe('installDevParentWatchdog', () => {
parentExists = false
await vi.advanceTimersByTimeAsync(1000)
expect(app.quit).toHaveBeenCalledTimes(1)
expect(app.exit).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(3000)
expect(app.exit).toHaveBeenCalledWith(0)
} finally {
if (originalPpid) {
Object.defineProperty(process, 'ppid', originalPpid)

View File

@ -1,6 +1,23 @@
import { app } from 'electron'
import { join } from 'path'
const DEV_PARENT_SHUTDOWN_GRACE_MS = 3000
function requestDevParentShutdown(): void {
app.quit()
const forceExitTimer = setTimeout(() => {
// Why: in dev, losing the supervising parent means this Electron process is
// already orphaned from the terminal session. We try app.quit() first so
// normal cleanup still runs, but fall back to app.exit() when macOS quit
// handlers or window-close guards stall and would otherwise leave Orca
// hanging after Ctrl+C ends `pnpm dev`.
app.exit(0)
}, DEV_PARENT_SHUTDOWN_GRACE_MS)
forceExitTimer.unref()
}
export function installUncaughtPipeErrorGuard(): void {
process.on('uncaughtException', (error) => {
if (
@ -66,7 +83,7 @@ export function installDevParentDisconnectQuit(isDev: boolean): void {
// without terminating the app window, so in dev we quit explicitly when the
// supervising IPC channel disconnects instead of leaving a stray Electron app.
process.once('disconnect', () => {
app.quit()
requestDevParentShutdown()
})
}
@ -106,7 +123,7 @@ export function installDevParentWatchdog(isDev: boolean): void {
// the dev runner while leaving Orca open. Watching the original parent PID
// keeps dev shutdown coupled to the terminal session without affecting the
// packaged app, which is not supervised by electron-vite.
app.quit()
requestDevParentShutdown()
}
}, 1000)