Merge pull request #3169 from wqpan/fix/renderer-crash-recovery
fix(electron): recover crashed renderer windows
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
|||||||
} from './linux-autostart.mjs';
|
} from './linux-autostart.mjs';
|
||||||
import { unsupportedAppSpecificOpenError, validateLocalPath } from './path-open-utils.mjs';
|
import { unsupportedAppSpecificOpenError, validateLocalPath } from './path-open-utils.mjs';
|
||||||
import { shouldAllowBrowserPanelCertificateError } from './browser-panel-security.mjs';
|
import { shouldAllowBrowserPanelCertificateError } from './browser-panel-security.mjs';
|
||||||
|
import { createRendererRecoveryPolicy } from './renderer-recovery.mjs';
|
||||||
import { mintOutsideFileGrant } from '@openchamber/web/server/lib/fs/routes.js';
|
import { mintOutsideFileGrant } from '@openchamber/web/server/lib/fs/routes.js';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
@@ -2496,6 +2497,7 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
|
|||||||
};
|
};
|
||||||
|
|
||||||
const browserWindow = new BrowserWindow(options);
|
const browserWindow = new BrowserWindow(options);
|
||||||
|
const rendererRecoveryPolicy = createRendererRecoveryPolicy();
|
||||||
browserWindow.__ocLabel = label || nextWindowLabel();
|
browserWindow.__ocLabel = label || nextWindowLabel();
|
||||||
browserWindow.__ocRuntimeConfig = { apiBaseUrl: desktopApiBaseUrl, clientToken: desktopClientToken, requestHeaders: desktopRequestHeaders };
|
browserWindow.__ocRuntimeConfig = { apiBaseUrl: desktopApiBaseUrl, clientToken: desktopClientToken, requestHeaders: desktopRequestHeaders };
|
||||||
browserWindow.__ocInitScript = buildInitScript(desktopLocalOrigin, state.bootOutcome, desktopApiBaseUrl, desktopClientToken, 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.on('zoom-changed', () => {
|
||||||
browserWindow.webContents.setZoomFactor(1);
|
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', () => {
|
browserWindow.webContents.on('dom-ready', () => {
|
||||||
if (browserWindow.__ocLabel === 'main') {
|
if (browserWindow.__ocLabel === 'main') {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user