fix(desktop): bound Windows Electron quit finalization
This commit is contained in:
@@ -358,6 +358,7 @@ import {
|
||||
tagRegistrySessionResponse
|
||||
} from './profile-session-routing'
|
||||
import { createQuickEntryShortcut, quickEntryWindowBounds, sanitizeQuickEntrySettings } from './quick-entry'
|
||||
import { createQuitFinalization } from './quit-finalization'
|
||||
import { type ActiveWork, mergeActiveWork, normalizeActiveWork, quitPromptFor } from './quit-guard'
|
||||
import { backendQuitNeedsWait, createQuitTeardownCoordinator } from './quit-teardown'
|
||||
import * as remoteLifecycle from './remote-lifecycle'
|
||||
@@ -12709,6 +12710,13 @@ const backendShutdown = createBackendShutdownCoordinator(async () => {
|
||||
})
|
||||
|
||||
const quitTeardown = createQuitTeardownCoordinator(() => app.quit())
|
||||
const quitFinalization = createQuitFinalization({
|
||||
isWindows: IS_WINDOWS,
|
||||
hardExit: code => {
|
||||
rememberLog(`[quit] forcing Windows process exit after Electron quit finalization stalled`)
|
||||
app.exit(code)
|
||||
}
|
||||
})
|
||||
|
||||
async function teardownSshForQuit() {
|
||||
const scopes = [...sshConnections.keys()]
|
||||
@@ -17222,6 +17230,11 @@ app.on('before-quit', () => {
|
||||
app.on('will-quit', () => {
|
||||
sshIsolatedKeepalives.stopAll()
|
||||
destroyKeepaliveAgents()
|
||||
quitFinalization.arm()
|
||||
})
|
||||
|
||||
app.on('quit', () => {
|
||||
quitFinalization.cancel()
|
||||
})
|
||||
|
||||
// Answered synchronously so preload can publish the verdict before the
|
||||
|
||||
76
apps/desktop/electron/quit-finalization.test.ts
Normal file
76
apps/desktop/electron/quit-finalization.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { test, vi } from 'vitest'
|
||||
|
||||
import { createQuitFinalization } from './quit-finalization'
|
||||
|
||||
test('does not arm a finalization fallback outside Windows', () => {
|
||||
const schedule = vi.fn()
|
||||
const hardExit = vi.fn()
|
||||
const finalization = createQuitFinalization({ isWindows: false, schedule, hardExit })
|
||||
|
||||
finalization.arm()
|
||||
|
||||
assert.equal(schedule.mock.calls.length, 0)
|
||||
assert.equal(hardExit.mock.calls.length, 0)
|
||||
})
|
||||
|
||||
test('forces a Windows exit once the admitted quit exceeds its deadline', () => {
|
||||
let onTimeout: (() => void) | undefined
|
||||
const hardExit = vi.fn()
|
||||
|
||||
const finalization = createQuitFinalization({
|
||||
isWindows: true,
|
||||
schedule: callback => {
|
||||
onTimeout = callback
|
||||
|
||||
return 'timer'
|
||||
},
|
||||
hardExit
|
||||
})
|
||||
|
||||
finalization.arm()
|
||||
finalization.arm()
|
||||
|
||||
assert.ok(onTimeout)
|
||||
onTimeout()
|
||||
onTimeout()
|
||||
|
||||
assert.deepEqual(hardExit.mock.calls, [[0]])
|
||||
})
|
||||
|
||||
test('cancels the fallback when Electron reports a completed quit', () => {
|
||||
let onTimeout: (() => void) | undefined
|
||||
const cancel = vi.fn()
|
||||
const hardExit = vi.fn()
|
||||
|
||||
const finalization = createQuitFinalization({
|
||||
isWindows: true,
|
||||
schedule: callback => {
|
||||
onTimeout = callback
|
||||
|
||||
return 'timer'
|
||||
},
|
||||
cancel,
|
||||
hardExit
|
||||
})
|
||||
|
||||
finalization.arm()
|
||||
finalization.cancel()
|
||||
onTimeout?.()
|
||||
|
||||
assert.deepEqual(cancel.mock.calls, [['timer']])
|
||||
assert.equal(hardExit.mock.calls.length, 0)
|
||||
})
|
||||
|
||||
test('does not re-arm after finalization has been cancelled', () => {
|
||||
const schedule = vi.fn(() => 'timer')
|
||||
const hardExit = vi.fn()
|
||||
const finalization = createQuitFinalization({ isWindows: true, schedule, hardExit })
|
||||
|
||||
finalization.arm()
|
||||
finalization.cancel()
|
||||
finalization.arm()
|
||||
|
||||
assert.equal(schedule.mock.calls.length, 1)
|
||||
})
|
||||
70
apps/desktop/electron/quit-finalization.ts
Normal file
70
apps/desktop/electron/quit-finalization.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
const DEFAULT_TIMEOUT_MS = 10_000
|
||||
|
||||
type TimerHandle = unknown
|
||||
|
||||
type Schedule = (callback: () => void, timeoutMs: number) => TimerHandle
|
||||
|
||||
type Cancel = (handle: TimerHandle) => void
|
||||
|
||||
export interface QuitFinalizationOptions {
|
||||
isWindows: boolean
|
||||
hardExit: (code: number) => void
|
||||
timeoutMs?: number
|
||||
schedule?: Schedule
|
||||
cancel?: Cancel
|
||||
}
|
||||
|
||||
export interface QuitFinalization {
|
||||
arm: () => void
|
||||
cancel: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a bounded escape hatch for a Windows Electron process that has
|
||||
* entered its final quit phase but never emits the completed quit event.
|
||||
*
|
||||
* The fallback is deliberately armed only from `will-quit`, after the normal
|
||||
* before-quit teardown has been admitted. A successful `quit` event cancels it.
|
||||
*/
|
||||
export function createQuitFinalization({
|
||||
isWindows,
|
||||
hardExit,
|
||||
timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
schedule = (callback, delay) => setTimeout(callback, delay),
|
||||
cancel = handle => clearTimeout(handle as ReturnType<typeof setTimeout>)
|
||||
}: QuitFinalizationOptions): QuitFinalization {
|
||||
let timer: TimerHandle | null = null
|
||||
let finished = false
|
||||
|
||||
return {
|
||||
arm() {
|
||||
if (!isWindows || finished || timer !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
timer = schedule(() => {
|
||||
timer = null
|
||||
|
||||
if (finished) {
|
||||
return
|
||||
}
|
||||
|
||||
finished = true
|
||||
hardExit(0)
|
||||
}, timeoutMs)
|
||||
},
|
||||
|
||||
cancel() {
|
||||
if (finished) {
|
||||
return
|
||||
}
|
||||
|
||||
finished = true
|
||||
|
||||
if (timer !== null) {
|
||||
cancel(timer)
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user