Harden renderer startup readiness recovery

Require a bounded Ready signal after each main-document load, cancel the deadline on successful readiness or window disposal, and keep recovery limited to one reload before the branded failure surface.

Route renderer initialization failures through production startup handlers and reject malformed authenticated remote keyboard payloads without throwing or logging.
This commit is contained in:
Sucukdeluxe
2026-08-13 22:25:28 +02:00
parent d5ff45644c
commit a4d0854e76
4 changed files with 436 additions and 24 deletions
+124 -12
View File
@@ -18,13 +18,39 @@ function createStartupFailureDocument(language) {
return `<!doctype html><html lang="${german ? 'de' : 'en'}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Multi Hoster Uploader</title><style>html,body{height:100%;margin:0;background:#1f1f1f;color:#f4f4f4;font:15px system-ui,sans-serif}body{display:grid;place-items:center}.card{width:min(520px,calc(100% - 48px));padding:28px;border:1px solid #444;border-radius:12px;background:#292929;box-shadow:0 18px 50px #0008}h1{margin:0 0 12px;font-size:22px}p{margin:0 0 22px;color:#c8c8c8;line-height:1.5}button{min-height:38px;padding:0 18px;border:1px solid #555;border-radius:7px;background:#363636;color:#fff;font-weight:650;cursor:pointer}button:hover{background:#414141}</style></head><body><main class="card"><h1>${title}</h1><p>${detail}</p><button type="button" onclick="window.close()">${close}</button></main></body></html>`;
}
function createStartupRecoveryCoordinator({ load, reload, reveal, showFailure, close }) {
function createStartupRecoveryCoordinator({
load,
reload,
reveal,
showFailure,
close,
readyTimeoutMs = 15000,
scheduleReadyDeadline = setTimeout,
cancelReadyDeadline = clearTimeout
}) {
let initialLoad;
let crashReloads = 0;
let terminalFailure;
let recovery;
let readyDeadline;
let rendererDocument = 0;
let awaitingReady = false;
let stopped = false;
function clearRendererDeadline() {
if (readyDeadline !== undefined) cancelReadyDeadline(readyDeadline);
readyDeadline = undefined;
}
function abandonRendererDocument() {
awaitingReady = false;
clearRendererDeadline();
}
function endWithFailure(failure) {
if (stopped) return Promise.resolve(false);
if (!terminalFailure) {
abandonRendererDocument();
terminalFailure = (async () => {
if (typeof showFailure !== 'function') {
await close(failure);
@@ -41,28 +67,48 @@ function createStartupRecoveryCoordinator({ load, reload, reveal, showFailure, c
return terminalFailure;
}
async function recoverRenderer(phase, details) {
function recoverRenderer(phase, details) {
if (stopped) return Promise.resolve(false);
if (terminalFailure) return terminalFailure;
if (crashReloads >= 1) {
return endWithFailure({ phase, attempt: crashReloads + 1, details });
}
crashReloads++;
try {
await reload();
} catch (error) {
return endWithFailure({ phase: 'renderer-reload', attempt: crashReloads, details, error });
}
if (recovery) return recovery;
abandonRendererDocument();
const currentRecovery = (async () => {
if (crashReloads >= 1) {
return endWithFailure({ phase, attempt: crashReloads + 1, details });
}
crashReloads++;
try {
await reload();
return true;
} catch (error) {
if (stopped) return false;
return endWithFailure({ phase: 'renderer-reload', attempt: crashReloads, details, error });
}
})();
recovery = currentRecovery;
currentRecovery.then(
() => {
if (recovery === currentRecovery) recovery = undefined;
},
() => {
if (recovery === currentRecovery) recovery = undefined;
}
);
return currentRecovery;
}
return {
loadInitial(...args) {
if (stopped) return Promise.resolve(false);
if (terminalFailure) return terminalFailure;
if (!initialLoad) {
initialLoad = (async () => {
for (let attempt = 1; attempt <= 2; attempt++) {
if (stopped) return false;
try {
return await load(...args);
} catch (error) {
if (stopped) return false;
if (attempt === 2) {
await endWithFailure({ phase: 'initial-load', attempt, error });
}
@@ -78,11 +124,76 @@ function createStartupRecoveryCoordinator({ load, reload, reveal, showFailure, c
rendererInitializationFailed(details) {
return recoverRenderer('renderer-initialization', details);
},
rendererLoadStarted() {
if (stopped || terminalFailure) return false;
clearRendererDeadline();
rendererDocument++;
awaitingReady = true;
return true;
},
rendererLoaded() {
if (stopped || terminalFailure || !awaitingReady) return false;
clearRendererDeadline();
const document = rendererDocument;
readyDeadline = scheduleReadyDeadline(() => {
if (stopped || terminalFailure || !awaitingReady || rendererDocument !== document) return false;
readyDeadline = undefined;
return recoverRenderer('renderer-ready-timeout', { timeoutMs: readyTimeoutMs });
}, readyTimeoutMs);
if (readyDeadline && typeof readyDeadline.unref === 'function') readyDeadline.unref();
return true;
},
rendererReady() {
if (terminalFailure) return false;
if (stopped || terminalFailure) return false;
abandonRendererDocument();
crashReloads = 0;
reveal();
return true;
},
dispose() {
if (stopped) return false;
stopped = true;
abandonRendererDocument();
return true;
}
};
}
function createStartupRendererHandlers({ window, coordinator, onReady, onInitializationFailed }) {
let disposed = false;
function accepts(event) {
return !disposed && window && !window.isDestroyed() && event && event.sender === window.webContents;
}
return {
documentLoadStarted() {
if (disposed) return false;
return coordinator.rendererLoadStarted();
},
documentLoaded() {
if (disposed) return false;
return coordinator.rendererLoaded();
},
rendererCrashed(details) {
if (disposed) return false;
return coordinator.rendererCrashed(details);
},
rendererInitializationFailed(event, details) {
if (!accepts(event)) return false;
if (typeof onInitializationFailed === 'function') onInitializationFailed(details);
return coordinator.rendererInitializationFailed(details);
},
rendererReady(event) {
if (!accepts(event)) return false;
const ready = coordinator.rendererReady();
if (ready && typeof onReady === 'function') onReady();
return ready;
},
dispose() {
if (disposed) return false;
disposed = true;
return coordinator.dispose();
}
};
}
@@ -105,6 +216,7 @@ module.exports = {
configureStartupRenderer,
createStartupFailureDocument,
createStartupRecoveryCoordinator,
createStartupRendererHandlers,
createStartupWindow,
resolveStartupLanguage
};