Close startup and recovery transaction gaps
This commit is contained in:
+18
-11
@@ -41,6 +41,19 @@ function createStartupRecoveryCoordinator({ load, reload, reveal, showFailure, c
|
||||
return terminalFailure;
|
||||
}
|
||||
|
||||
async function recoverRenderer(phase, details) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loadInitial(...args) {
|
||||
if (terminalFailure) return terminalFailure;
|
||||
@@ -59,17 +72,11 @@ function createStartupRecoveryCoordinator({ load, reload, reveal, showFailure, c
|
||||
}
|
||||
return initialLoad;
|
||||
},
|
||||
async rendererCrashed(details) {
|
||||
if (terminalFailure) return terminalFailure;
|
||||
if (crashReloads >= 1) {
|
||||
return endWithFailure({ phase: 'renderer-crash', attempt: crashReloads + 1, details });
|
||||
}
|
||||
crashReloads++;
|
||||
try {
|
||||
await reload();
|
||||
} catch (error) {
|
||||
return endWithFailure({ phase: 'renderer-reload', attempt: crashReloads, details, error });
|
||||
}
|
||||
rendererCrashed(details) {
|
||||
return recoverRenderer('renderer-crash', details);
|
||||
},
|
||||
rendererInitializationFailed(details) {
|
||||
return recoverRenderer('renderer-initialization', details);
|
||||
},
|
||||
rendererReady() {
|
||||
if (terminalFailure) return false;
|
||||
|
||||
@@ -2093,17 +2093,6 @@ async function executeReservedUploadStart(payload, startLease) {
|
||||
startedAt: new Date().toISOString(),
|
||||
jobIds: tasks.map(task => task.jobId).filter(Boolean)
|
||||
};
|
||||
try {
|
||||
await configStore.saveUploadRecovery(recovery);
|
||||
} catch (error) {
|
||||
debugLog(`upload recovery state could not be saved: ${error.message}`);
|
||||
if (uploadManager === _thisManager) {
|
||||
uploadManager = null;
|
||||
globalThis._mhuUploadManagerRef = null;
|
||||
}
|
||||
return { error: 'Upload-Wiederherstellung konnte nicht gespeichert werden' };
|
||||
}
|
||||
uploadRecoveryStates.set(_thisManager, recovery);
|
||||
|
||||
// Pre-resolve a fallback for every hoster that has one. Lets the upload
|
||||
// manager break out of the retry loop after a single generic failure and
|
||||
@@ -2148,6 +2137,17 @@ async function executeReservedUploadStart(payload, startLease) {
|
||||
}
|
||||
return { error: `Quelldatei-Schutz konnte nicht vorbereitet werden: ${error.message}` };
|
||||
}
|
||||
try {
|
||||
await configStore.saveUploadRecovery(recovery);
|
||||
} catch (error) {
|
||||
debugLog(`upload recovery state could not be saved: ${error.message}`);
|
||||
if (uploadManager === _thisManager) {
|
||||
uploadManager = null;
|
||||
globalThis._mhuUploadManagerRef = null;
|
||||
}
|
||||
return { error: 'Upload-Wiederherstellung konnte nicht gespeichert werden' };
|
||||
}
|
||||
uploadRecoveryStates.set(_thisManager, recovery);
|
||||
for (const skipped of skippedJobs) sourceCleanup.markSkipped(skipped.jobId);
|
||||
_thisManager.sourceFileCleanup = sourceCleanup;
|
||||
const _producerTracker = trackUploadProducer(_thisManager);
|
||||
@@ -2991,6 +2991,14 @@ ipcMain.on('app:close-handshake-ready', (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('app:renderer-initialization-failed', (event, details) => {
|
||||
if (!mainWindow || mainWindow.isDestroyed() || event.sender !== mainWindow.webContents) return;
|
||||
const message = details && typeof details.message === 'string' ? details.message : 'Renderer initialization failed';
|
||||
_writeCrashLog('RENDERER INITIALIZATION FAILED', new Error(message), details);
|
||||
debugLog(`RENDERER INITIALIZATION FAILED: ${message}`);
|
||||
if (startupRecoveryCoordinator) void startupRecoveryCoordinator.rendererInitializationFailed(details);
|
||||
});
|
||||
|
||||
ipcMain.on('app:close-preparation-started', (event, attempt) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed() && event.sender === mainWindow.webContents && isClosePreparationActive(attempt)) {
|
||||
armCloseFlushTimer(attempt, 3000);
|
||||
|
||||
@@ -27,6 +27,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
});
|
||||
},
|
||||
signalCloseHandshakeReady: () => ipcRenderer.send('app:close-handshake-ready'),
|
||||
signalRendererInitializationFailed: (details) => ipcRenderer.send('app:renderer-initialization-failed', details),
|
||||
|
||||
// Always on top
|
||||
setAlwaysOnTop: (value) => ipcRenderer.invoke('set-always-on-top', value),
|
||||
|
||||
@@ -150,8 +150,10 @@ test('close readiness is signaled only after the renderer explicitly finishes in
|
||||
assert.deepEqual(sent, [['app:close-preparation-started', 7]]);
|
||||
|
||||
exposedApi.signalCloseHandshakeReady();
|
||||
exposedApi.signalRendererInitializationFailed({ message: 'init failed' });
|
||||
assert.deepEqual(sent, [
|
||||
['app:close-preparation-started', 7],
|
||||
['app:close-handshake-ready']
|
||||
['app:close-handshake-ready'],
|
||||
['app:renderer-initialization-failed', { message: 'init failed' }]
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ test('main process wires bounded startup recovery into real load and crash paths
|
||||
assert.match(source, /createStartupRecoveryCoordinator/);
|
||||
assert.match(source, /startupRecoveryCoordinator\.loadInitial/);
|
||||
assert.match(source, /startupRecoveryCoordinator\.rendererCrashed/);
|
||||
assert.match(source, /startupRecoveryCoordinator\.rendererInitializationFailed/);
|
||||
assert.match(source, /startupRecoveryCoordinator\.rendererReady/);
|
||||
assert.match(source, /createStartupFailureDocument/);
|
||||
});
|
||||
@@ -284,6 +285,32 @@ test('renderer crash recovery reloads once and cannot enter an infinite reload l
|
||||
assert.equal(revealCalls, 1);
|
||||
});
|
||||
|
||||
test('renderer initialization failures share the bounded recovery path', async () => {
|
||||
const failures = [];
|
||||
let reloadCalls = 0;
|
||||
const coordinator = createStartupRecoveryCoordinator({
|
||||
load() {},
|
||||
async reload() {
|
||||
reloadCalls++;
|
||||
},
|
||||
reveal() {},
|
||||
async showFailure(failure) {
|
||||
failures.push(failure);
|
||||
},
|
||||
close() {}
|
||||
});
|
||||
|
||||
await coordinator.rendererInitializationFailed({ message: 'first failure' });
|
||||
await coordinator.rendererInitializationFailed({ message: 'second failure' });
|
||||
|
||||
assert.equal(reloadCalls, 1);
|
||||
assert.deepEqual(failures, [{
|
||||
phase: 'renderer-initialization',
|
||||
attempt: 2,
|
||||
details: { message: 'second failure' }
|
||||
}]);
|
||||
});
|
||||
|
||||
test('a successful renderer ready event reveals content and resets crash recovery', async () => {
|
||||
const crashes = [
|
||||
{ reason: 'crashed', exitCode: 21 },
|
||||
|
||||
@@ -90,6 +90,7 @@ test('main and renderer keep recovery evidence until final queue persistence suc
|
||||
assert.match(startFailure, /if \(queuePersisted && terminalRecoveryPersisted\)[\s\S]*saveUploadRecovery\(null\)/);
|
||||
const startHandler = mainSource.slice(mainSource.indexOf("ipcMain.handle('start-upload'"), mainSource.indexOf("ipcMain.handle('cancel-upload'"));
|
||||
assert.match(startHandler, /await configStore\.saveUploadRecovery\(recovery\)/);
|
||||
assert.ok(startHandler.indexOf('sourceCleanup.registerGroups(sourceCleanupGroups)') < startHandler.indexOf('saveUploadRecovery(recovery)'));
|
||||
assert.match(startHandler, /catch \(error\)[\s\S]*return { error: 'Upload-Wiederherstellung konnte nicht gespeichert werden' }/);
|
||||
const addHandler = mainSource.slice(mainSource.indexOf("ipcMain.handle('add-jobs-to-batch'"), mainSource.indexOf("ipcMain.handle('finish-after-active'"));
|
||||
assert.match(addHandler, /await configStore\.saveUploadRecovery\(nextRecovery\)/);
|
||||
|
||||
Reference in New Issue
Block a user