From 7ba2c63d51c49e8ee2a917fab2c423a70218cd62 Mon Sep 17 00:00:00 2001 From: Administrator Date: Sat, 21 Mar 2026 10:20:07 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20config=20race=20condition?= =?UTF-8?q?s,=20quit=20safety,=20update=20data=20loss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Config write serialization via _writeQueue prevents concurrent read-modify-write races between settings/queue/history saves - Cancel active uploads on app quit (prevents zombie processes) - Persist queue before update install (prevents queue loss) - Sync IPC save in beforeunload (guarantees save before close) - Fix double configStore.load() call - Guard against status regression in handleProgress (done→uploading) Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/config-store.js | 40 ++++++++++++++++++++++++++-------------- main.js | 14 +++++++++++++- preload.js | 1 + renderer/app.js | 20 +++++++++++++------- 4 files changed, 53 insertions(+), 22 deletions(-) diff --git a/lib/config-store.js b/lib/config-store.js index a72aaea..78e891f 100644 --- a/lib/config-store.js +++ b/lib/config-store.js @@ -94,6 +94,7 @@ class ConfigStore { ? app.getPath('userData') : path.join(__dirname, '..'); this.filePath = path.join(dir, 'electron-config.json'); + this._writeQueue = Promise.resolve(); // Serializes all writes to prevent race conditions // Migrate config from old location if current doesn't exist if (!fs.existsSync(this.filePath) && app && app.isPackaged) { @@ -208,12 +209,19 @@ class ConfigStore { } } + _enqueueWrite(fn) { + this._writeQueue = this._writeQueue.then(fn, fn); + return this._writeQueue; + } + save(config) { - const current = this.load(); - if (config.hosters) current.hosters = config.hosters; - if (config.hosterSettings) current.hosterSettings = config.hosterSettings; - if (config.globalSettings) current.globalSettings = config.globalSettings; - return this._atomicWrite(JSON.stringify(current, null, 2)); + return this._enqueueWrite(() => { + const current = this.load(); + if (config.hosters) current.hosters = config.hosters; + if (config.hosterSettings) current.hosterSettings = config.hosterSettings; + if (config.globalSettings) current.globalSettings = config.globalSettings; + return this._atomicWrite(JSON.stringify(current, null, 2)); + }); } loadHistory() { @@ -242,18 +250,22 @@ class ConfigStore { } appendHistory(entry) { - const config = this.load(); - config.history.push(entry); - if (config.history.length > MAX_HISTORY) { - config.history = config.history.slice(-MAX_HISTORY); - } - return this._atomicWrite(JSON.stringify(config, null, 2)); + return this._enqueueWrite(() => { + const config = this.load(); + config.history.push(entry); + if (config.history.length > MAX_HISTORY) { + config.history = config.history.slice(-MAX_HISTORY); + } + return this._atomicWrite(JSON.stringify(config, null, 2)); + }); } clearHistory() { - const config = this.load(); - config.history = []; - return this._atomicWrite(JSON.stringify(config, null, 2)); + return this._enqueueWrite(() => { + const config = this.load(); + config.history = []; + return this._atomicWrite(JSON.stringify(config, null, 2)); + }); } } diff --git a/main.js b/main.js index cc61fa9..0611a01 100644 --- a/main.js +++ b/main.js @@ -502,7 +502,8 @@ app.whenReady().then(() => { // Auto-start remote server if enabled try { - const remoteConfig = configStore.load().globalSettings && configStore.load().globalSettings.remote; + const _remCfg = configStore.load(); + const remoteConfig = _remCfg.globalSettings && _remCfg.globalSettings.remote; if (remoteConfig && remoteConfig.enabled) { startRemoteServer().catch(err => { debugLog(`remote-server auto-start failed: ${err.message}`); @@ -540,6 +541,7 @@ app.on('window-all-closed', () => { }); app.on('before-quit', () => { + if (uploadManager) try { uploadManager.cancel(); } catch {} try { folderMonitor.stop(); } catch {} try { if (remoteServer) { remoteServer.stop(); remoteServer = null; } @@ -874,6 +876,16 @@ ipcMain.handle('save-global-settings', async (_event, globalSettings) => { return true; }); +// Synchronous save for beforeunload — blocks renderer until write completes +ipcMain.on('save-global-settings-sync', (event, globalSettings) => { + try { + const current = configStore.load(); + current.globalSettings = globalSettings; + fs.writeFileSync(configStore.filePath, JSON.stringify(current, null, 2)); + } catch {} + event.returnValue = true; +}); + // --- Folder Monitor --- function startFolderMonitor(settings) { try { diff --git a/preload.js b/preload.js index d50e204..e1f5def 100644 --- a/preload.js +++ b/preload.js @@ -14,6 +14,7 @@ contextBridge.exposeInMainWorld('api', { // Global settings getGlobalSettings: () => ipcRenderer.invoke('get-global-settings'), saveGlobalSettings: (settings) => ipcRenderer.invoke('save-global-settings', settings), + saveGlobalSettingsSync: (settings) => ipcRenderer.sendSync('save-global-settings-sync', settings), // Always on top setAlwaysOnTop: (value) => ipcRenderer.invoke('set-always-on-top', value), diff --git a/renderer/app.js b/renderer/app.js index 0795b0b..99a6e87 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -1487,6 +1487,9 @@ function handleProgress(data) { indexJob(job); } + // Don't regress from terminal states (stale callbacks can arrive after completion) + if (job.status === 'done' || job.status === 'skipped') return; + // Update job state job.status = data.status; job.bytesUploaded = data.bytesUploaded || 0; @@ -3041,14 +3044,16 @@ function sortHistoryRows(rows) { }); } -// Flush pending queue state on window close +// Flush pending queue state on window close (sync IPC — blocks until save completes) window.addEventListener('beforeunload', () => { - if (queuePersistTimer) { - clearTimeout(queuePersistTimer); - queuePersistTimer = null; - // Synchronous-ish: fire and forget since window is closing - persistQueueStateNow().catch(() => {}); - } + clearTimeout(queuePersistTimer); + queuePersistTimer = null; + const globalSettings = { + ...(config.globalSettings || {}), + pendingQueue: buildPersistedQueueState() + }; + config.globalSettings = globalSettings; + window.api.saveGlobalSettingsSync(globalSettings); }); // --- Setup Listeners --- @@ -3252,6 +3257,7 @@ function showUpdateBanner(info) { document.getElementById('installUpdateBtn').onclick = async () => { msg.textContent = 'Update wird heruntergeladen...'; document.getElementById('installUpdateBtn').disabled = true; + await persistQueueStateNow().catch(() => {}); // Save queue before update restart await window.api.installUpdate(); }; document.getElementById('dismissUpdateBtn').onclick = () => { banner.style.display = 'none'; };