Merge pull request #3169 from wqpan/fix/renderer-crash-recovery

fix(electron): recover crashed renderer windows
This commit is contained in:
Bohdan Triapitsyn
2026-08-28 01:24:24 +03:00
committed by GitHub
3 changed files with 79 additions and 0 deletions
+15
View File
@@ -33,6 +33,7 @@ import {
} from './linux-autostart.mjs';
import { unsupportedAppSpecificOpenError, validateLocalPath } from './path-open-utils.mjs';
import { shouldAllowBrowserPanelCertificateError } from './browser-panel-security.mjs';
import { createRendererRecoveryPolicy } from './renderer-recovery.mjs';
import { mintOutsideFileGrant } from '@openchamber/web/server/lib/fs/routes.js';
const execFileAsync = promisify(execFile);
@@ -2496,6 +2497,7 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
};
const browserWindow = new BrowserWindow(options);
const rendererRecoveryPolicy = createRendererRecoveryPolicy();
browserWindow.__ocLabel = label || nextWindowLabel();
browserWindow.__ocRuntimeConfig = { apiBaseUrl: desktopApiBaseUrl, clientToken: desktopClientToken, requestHeaders: desktopRequestHeaders };
browserWindow.__ocInitScript = buildInitScript(desktopLocalOrigin, state.bootOutcome, desktopApiBaseUrl, desktopClientToken, desktopRequestHeaders);
@@ -2656,6 +2658,19 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
browserWindow.webContents.on('zoom-changed', () => {
browserWindow.webContents.setZoomFactor(1);
});
browserWindow.webContents.on('render-process-gone', (_event, details) => {
if (!rendererRecoveryPolicy.shouldReload(details.reason)) return;
log.warn('[electron] renderer exited unexpectedly; reloading window', {
label: browserWindow.__ocLabel,
reason: details.reason,
exitCode: details.exitCode,
});
setTimeout(() => {
if (!browserWindow.isDestroyed()) {
browserWindow.webContents.reload();
}
}, 100);
});
browserWindow.webContents.on('dom-ready', () => {
if (browserWindow.__ocLabel === 'main') {
+30
View File
@@ -0,0 +1,30 @@
const RECOVERY_WINDOW_MS = 60_000;
const MAX_RECOVERY_ATTEMPTS = 3;
const RECOVERABLE_REASONS = new Set([
'abnormal-exit',
'crashed',
'oom',
'memory-eviction',
]);
export const createRendererRecoveryPolicy = (now = Date.now) => {
let windowStartedAt = 0;
let attempts = 0;
return {
shouldReload: (reason) => {
if (!RECOVERABLE_REASONS.has(reason)) return false;
const currentTime = now();
if (currentTime - windowStartedAt >= RECOVERY_WINDOW_MS) {
windowStartedAt = currentTime;
attempts = 0;
}
if (attempts >= MAX_RECOVERY_ATTEMPTS) return false;
attempts += 1;
return true;
},
};
};
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createRendererRecoveryPolicy } from './renderer-recovery.mjs';
test('allows a bounded number of reloads for recoverable renderer failures', () => {
const policy = createRendererRecoveryPolicy(() => 1_000);
assert.equal(policy.shouldReload('crashed'), true);
assert.equal(policy.shouldReload('oom'), true);
assert.equal(policy.shouldReload('abnormal-exit'), true);
assert.equal(policy.shouldReload('memory-eviction'), false);
});
test('ignores clean and externally killed renderer exits', () => {
const policy = createRendererRecoveryPolicy(() => 1_000);
assert.equal(policy.shouldReload('clean-exit'), false);
assert.equal(policy.shouldReload('killed'), false);
assert.equal(policy.shouldReload('launch-failed'), false);
});
test('resets the recovery budget after the recovery window', () => {
let currentTime = 1_000;
const policy = createRendererRecoveryPolicy(() => currentTime);
assert.equal(policy.shouldReload('crashed'), true);
assert.equal(policy.shouldReload('crashed'), true);
assert.equal(policy.shouldReload('crashed'), true);
assert.equal(policy.shouldReload('crashed'), false);
currentTime += 60_000;
assert.equal(policy.shouldReload('crashed'), true);
});