From fa691a0dc8e2ae5342812340539669f8c7665b37 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Sat, 11 Jul 2026 00:15:44 -0700 Subject: [PATCH] fix(win): bound renderer launch-failed recovery (#8234) --- .../process-gone-classification.test.ts | 9 ++-- .../process-gone-classification.ts | 7 +-- src/main/window/createMainWindow.test.ts | 43 +++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/main/crash-reporting/process-gone-classification.test.ts b/src/main/crash-reporting/process-gone-classification.test.ts index 50740f22a..78522b766 100644 --- a/src/main/crash-reporting/process-gone-classification.test.ts +++ b/src/main/crash-reporting/process-gone-classification.test.ts @@ -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', diff --git a/src/main/crash-reporting/process-gone-classification.ts b/src/main/crash-reporting/process-gone-classification.ts index c9042ede7..d3f363d13 100644 --- a/src/main/crash-reporting/process-gone-classification.ts +++ b/src/main/crash-reporting/process-gone-classification.ts @@ -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 } diff --git a/src/main/window/createMainWindow.test.ts b/src/main/window/createMainWindow.test.ts index dc278cb1f..b72be7962 100644 --- a/src/main/window/createMainWindow.test.ts +++ b/src/main/window/createMainWindow.test.ts @@ -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(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 void> = {} const webContents = {