fix(win): bound renderer launch-failed recovery (#8234)

This commit is contained in:
OrcaWin 2026-07-11 00:15:44 -07:00 committed by GitHub
parent 76c0365110
commit fa691a0dc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 53 additions and 6 deletions

View File

@ -366,19 +366,22 @@ describe('shouldRecoverRendererAfterProcessGone', () => {
).toBe(false)
})
it('does not recover renderer startup and security launch failures', () => {
it('recovers transient renderer launch failures', () => {
expect(
shouldRecoverRendererAfterProcessGone({
reason: 'launch-failed',
expectedTeardown: 'none'
})
).toBe(false)
).toBe(true)
expect(
shouldRecoverRendererAfterProcessGone({
reason: 'launch-failed',
expectedTeardown: 'renderer-reload'
})
).toBe(false)
).toBe(true)
})
it('does not recover renderer integrity failures', () => {
expect(
shouldRecoverRendererAfterProcessGone({
reason: 'integrity-failure',

View File

@ -11,7 +11,7 @@ const RECOVERABLE_UTILITY_SERVICE_NAMES = new Set([
'video_capture.mojom.VideoCaptureService'
])
const RECOVERABLE_CHILD_PROCESS_REASONS = new Set(['abnormal-exit', 'crashed', 'killed'])
const NON_RECOVERABLE_RENDERER_REASONS = new Set(['integrity-failure', 'launch-failed'])
const NON_RECOVERABLE_RENDERER_REASONS = new Set(['integrity-failure'])
function isWindowsControlTerminationExitCode(exitCode: number | null): boolean {
if (exitCode === null) {
@ -95,8 +95,9 @@ export function shouldRecoverRendererAfterProcessGone({
if (expectedTeardown === 'app-shutdown') {
return false
}
// Why: these mean Chromium could not start or trust the renderer process;
// retrying the same BrowserWindow load can loop indefinitely on Windows.
// Why: an integrity failure means Chromium cannot trust the renderer, so a
// reload cannot safely recover it. Launch failures can be transient and are
// bounded by the caller's renderer-recovery circuit breaker.
if (NON_RECOVERABLE_RENDERER_REASONS.has(reason)) {
return false
}

View File

@ -62,6 +62,7 @@ vi.mock('../browser/browser-manager', () => ({
import { createMainWindow, loadMainWindow } from './createMainWindow'
import { ipcMain } from 'electron'
import { shouldRecoverRendererAfterProcessGone } from '../crash-reporting/process-gone-classification'
function withPlatform<T>(platform: NodeJS.Platform, run: () => T): T {
const original = process.platform
@ -2703,6 +2704,48 @@ describe('createMainWindow', () => {
consoleError.mockRestore()
})
it('bounds renderer launch-failed recovery with the crash-loop breaker', () => {
vi.useFakeTimers()
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
const onRendererRecoveryExhausted = vi.fn()
const { browserWindowInstance, windowHandlers } = createRendererRecoveryWindowHarness()
try {
createMainWindow(null, {
onRendererRecoveryExhausted,
shouldRecoverRenderer: (details) =>
shouldRecoverRendererAfterProcessGone({
reason: details.reason,
expectedTeardown: 'none'
})
})
const details = {
reason: 'launch-failed',
exitCode: 18
} as Electron.RenderProcessGoneDetails
const driveLaunchFailure = (): void => {
windowHandlers['render-process-gone']?.({} as never, details)
vi.advanceTimersByTime(250)
}
driveLaunchFailure()
driveLaunchFailure()
driveLaunchFailure()
expect(browserWindowInstance.loadFile).toHaveBeenCalledTimes(4)
driveLaunchFailure()
expect(browserWindowInstance.loadFile).toHaveBeenCalledTimes(4)
expect(onRendererRecoveryExhausted).toHaveBeenCalledOnce()
expect(onRendererRecoveryExhausted).toHaveBeenCalledWith(
expect.objectContaining({ details, recentRecoveryCount: 3 })
)
} finally {
consoleError.mockRestore()
}
})
function createStartupRevealWindowFixture() {
const windowHandlers: Record<string, (...args: any[]) => void> = {}
const webContents = {