From 0f57aef7c7b188c0f8a33ac005af04baa567c1f2 Mon Sep 17 00:00:00 2001 From: Administrator Date: Mon, 8 Jun 2026 21:28:12 +0200 Subject: [PATCH] fix(stability): wrap hot timers/callbacks in try/catch, safeSend, updater waits for batch --- lib/updater.js | 15 +++++- lib/upload-manager.js | 82 +++++++++++++++-------------- main.js | 116 +++++++++++++++++++++--------------------- renderer/app.js | 16 ++++++ 4 files changed, 131 insertions(+), 98 deletions(-) diff --git a/lib/updater.js b/lib/updater.js index 3f9b725..f935a4e 100644 --- a/lib/updater.js +++ b/lib/updater.js @@ -233,7 +233,20 @@ async function installUpdate(onProgress) { // Stage: done if (onProgress) onProgress({ stage: 'done', percent: 100 }); - setTimeout(() => app.quit(), 900); + const _doQuit = () => setTimeout(() => app.quit(), 900); + const _getActive = () => { + try { return globalThis._mhuUploadManagerRef && globalThis._mhuUploadManagerRef.getActiveJobCount ? globalThis._mhuUploadManagerRef.getActiveJobCount() : 0; } + catch { return 0; } + }; + if (_getActive() > 0) { + const POLL_MS = 3000; + const poller = setInterval(() => { + if (_getActive() === 0) { clearInterval(poller); _doQuit(); } + }, POLL_MS); + setTimeout(() => { try { clearInterval(poller); } catch {} _doQuit(); }, 30 * 60 * 1000); + } else { + _doQuit(); + } } catch (err) { if (onProgress) onProgress({ stage: 'error', error: err.message }); diff --git a/lib/upload-manager.js b/lib/upload-manager.js index 8562aaf..ce80b08 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -558,14 +558,16 @@ class UploadManager extends EventEmitter { speedAbort = new AbortController(); uploadSignalBundle = this._combineManySignals([signal, speedAbort.signal]); speedMonitor = setInterval(() => { - if (currentSpeedKbs > 0 && currentSpeedKbs < settings.restartBelowKbs) { - if (!lowSpeedSince) lowSpeedSince = Date.now(); - if (Date.now() - lowSpeedSince > 6000) { - speedAbort.abort(); + try { + if (currentSpeedKbs > 0 && currentSpeedKbs < settings.restartBelowKbs) { + if (!lowSpeedSince) lowSpeedSince = Date.now(); + if (Date.now() - lowSpeedSince > 6000) { + speedAbort.abort(); + } + } else { + lowSpeedSince = 0; } - } else { - lowSpeedSince = 0; - } + } catch (e) { this._rotLog('speed-monitor-error', { jobId, error: e && e.message }); } }, 2000); } @@ -579,41 +581,42 @@ class UploadManager extends EventEmitter { const PROGRESS_EMIT_INTERVAL = 250; // ms – throttle UI updates const progressCb = (bytesUploaded, bytesTotal) => { - const now = Date.now(); - const elapsed = Math.round((now - jobStart) / 1000); - const timeDelta = (now - lastSpeedTime) / 1000; - if (timeDelta >= 1) { - const bytesDelta = bytesUploaded - lastBytes; - currentSpeedKbs = Math.round(bytesDelta / timeDelta / 1024); - lastBytes = bytesUploaded; - lastSpeedTime = now; - } + try { + const now = Date.now(); + const elapsed = Math.round((now - jobStart) / 1000); + const timeDelta = (now - lastSpeedTime) / 1000; + if (Number.isFinite(timeDelta) && timeDelta >= 1) { + const bytesDelta = bytesUploaded - lastBytes; + currentSpeedKbs = Math.round(bytesDelta / timeDelta / 1024); + lastBytes = bytesUploaded; + lastSpeedTime = now; + } - activeEntry.speedKbs = currentSpeedKbs; - activeEntry.bytesUploaded = bytesUploaded; + activeEntry.speedKbs = currentSpeedKbs; + activeEntry.bytesUploaded = bytesUploaded; - // Throttle progress emissions to reduce IPC + rendering overhead - if (now - lastEmitTime < PROGRESS_EMIT_INTERVAL) return; - lastEmitTime = now; + if (now - lastEmitTime < PROGRESS_EMIT_INTERVAL) return; + lastEmitTime = now; - const remaining = currentSpeedKbs > 0 - ? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024)) - : 0; + const remaining = currentSpeedKbs > 0 + ? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024)) + : 0; - this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId, - jobId, - status: 'uploading', - progress: bytesTotal > 0 ? Math.min(1, bytesUploaded / bytesTotal) : 0, - bytesUploaded, - bytesTotal, - speedKbs: currentSpeedKbs, - elapsed, - remaining, - error: null, - result: null, - attempt, - maxAttempts - }); + this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId, + jobId, + status: 'uploading', + progress: bytesTotal > 0 ? Math.min(1, bytesUploaded / bytesTotal) : 0, + bytesUploaded, + bytesTotal, + speedKbs: currentSpeedKbs, + elapsed, + remaining, + error: null, + result: null, + attempt, + maxAttempts + }); + } catch { /* progress callbacks must never throw — swallowing is correct, the stream keeps going */ } }; const result = await this._executeUpload(task, progressCb, uploadSignalBundle.signal, throttle); @@ -995,7 +998,7 @@ class UploadManager extends EventEmitter { _startStatsTimer() { if (this.statsInterval) clearInterval(this.statsInterval); this.statsInterval = setInterval(() => { - // Single pass over active jobs instead of two. + try { let globalSpeedKbs = 0; let activeCount = 0; let inProgressBytes = 0; @@ -1015,6 +1018,7 @@ class UploadManager extends EventEmitter { activeJobs: activeCount, pendingJobs: Object.values(this.semaphores).reduce((sum, semaphore) => sum + semaphore.pending, 0) }); + } catch { /* never let a stats tick crash the timer + caller */ } }, 1000); } diff --git a/main.js b/main.js index de00f21..7672d77 100644 --- a/main.js +++ b/main.js @@ -208,6 +208,7 @@ function getAllLogPaths() { debug: debugPath, accountRotation: rot, doodstreamDebug: path.join(dir, 'doodstream-debug.log'), + crashLog: path.join(dir, 'crash.log'), logDir: dir }; } @@ -227,6 +228,17 @@ function rotLog(msg, ts) { } catch {} } +function safeSend(channel, data) { + if (!mainWindow || mainWindow.isDestroyed()) return false; + try { + safeSend(channel, data); + return true; + } catch (err) { + debugLog(`safeSend(${channel}) failed: ${err && err.message ? err.message : err}`); + return false; + } +} + function _writeCrashLog(prefix, err, extra) { try { const ts = new Date().toISOString(); @@ -471,9 +483,7 @@ function _flushUploadLog() { // next session writes here directly (no more fallback ladder) and // the Settings input reflects reality. _persistFallbackLogPath(target.path); - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('upload-log-fallback', { fallbackPath: target.path }); - } + safeSend('upload-log-fallback', { fallbackPath: target.path }); } if (_uploadLogBuffer.length && !_uploadLogFlushTimer) setImmediate(_flushUploadLog); }); @@ -501,9 +511,7 @@ function _persistFallbackLogPath(workingPath) { cfg.globalSettings = gs; configStore.save({ globalSettings: gs }).catch(() => {}); _invalidateUploadLogTargetCache(); - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('log-path-auto-updated', { logFilePath: toSave }); - } + safeSend('log-path-auto-updated', { logFilePath: toSave }); } catch (err) { debugLog(`persist fallback logpath failed: ${err.message}`); } @@ -1125,7 +1133,7 @@ app.whenReady().then(() => { logInfo(`update-check: available=${result && result.available}, remote=${result && result.remoteVersion}`); logDebug(`update-check result: ${JSON.stringify(result)}`); if (result && result.available && mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('app:update-available', result); + safeSend('app:update-available', result); } } catch (err) { logError('update-check failed', err); @@ -1208,11 +1216,9 @@ ipcMain.handle('save-config', async (_event, config) => { rotLog(`main: config-updated → late fallback ${fallback.id} for ${hoster} (was stuck on ${failedAccountId})`); uploadManager.switchAccount(hoster, fallback); _sessionAccountOverrides.set(hoster, fallback); - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('account-switched', { + safeSend('account-switched', { hoster, fromAccountId: failedAccountId, toAccountId: fallback.id }); - } } } } catch (err) { @@ -1456,6 +1462,7 @@ ipcMain.handle('start-upload', (_event, payload) => { // Pass hoster settings to the upload manager uploadManager = new UploadManager(config.hosterSettings || {}, config.globalSettings || {}); + globalThis._mhuUploadManagerRef = uploadManager; const _progressByJob = new Map(); const _progressTerminalQueue = []; @@ -1473,7 +1480,7 @@ ipcMain.handle('start-upload', (_event, payload) => { const batch = _progressTerminalQueue.splice(0); for (const v of _progressByJob.values()) batch.push(v); _progressByJob.clear(); - if (batch.length) mainWindow.webContents.send('upload-progress-batch', batch); + if (batch.length) safeSend('upload-progress-batch', batch); }, PROGRESS_BATCH_INTERVAL_MS); } @@ -1511,16 +1518,16 @@ ipcMain.handle('start-upload', (_event, payload) => { }); uploadManager.on('stats', (data) => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('upload-stats', data); - } - // Update tray tooltip with upload progress - if (data.state === 'uploading' && data.activeJobs > 0) { - const speedMb = ((data.globalSpeedKbs || 0) / 1024).toFixed(1); - updateTrayTooltip(`Upload: ${data.activeJobs} aktiv - ${speedMb} MB/s`); - } else { - updateTrayTooltip('Multi-Hoster-Upload'); - } + try { + if (!data || typeof data !== 'object') return; + safeSend('upload-stats', data); + if (data.state === 'uploading' && data.activeJobs > 0) { + const speedMb = ((Number(data.globalSpeedKbs) || 0) / 1024).toFixed(1); + updateTrayTooltip(`Upload: ${data.activeJobs} aktiv - ${speedMb} MB/s`); + } else { + updateTrayTooltip('Multi-Hoster-Upload'); + } + } catch (e) { debugLog(`stats listener error: ${e && e.message}`); } }); uploadManager.on('account-failed', ({ hoster, accountId }) => { @@ -1533,9 +1540,7 @@ ipcMain.handle('start-upload', (_event, payload) => { rotLog(`main: account-failed ${hoster} ${accountId} → resolved fallback ${fallback.id}`); uploadManager.switchAccount(hoster, fallback); _sessionAccountOverrides.set(hoster, fallback); - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('account-switched', { hoster, fromAccountId: accountId, toAccountId: fallback.id }); - } + safeSend('account-switched', { hoster, fromAccountId: accountId, toAccountId: fallback.id }); } else { rotLog(`main: account-failed ${hoster} ${accountId} → NO fallback available (end of chain)`); } @@ -1551,17 +1556,25 @@ ipcMain.handle('start-upload', (_event, payload) => { 'doodstream-via-web' ]); uploadManager.on('rot-log', (entry) => { - const { ts, event, ...rest } = entry; - const pairs = Object.entries(rest) - .map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`) - .join(' '); - rotLog(`[${event}] ${pairs}`, ts); - if (entry.jobId) { - _appendJobLog(entry.jobId, { ts: ts || Date.now(), kind: 'rot', event, ...rest }); - } - if (mainWindow && !mainWindow.isDestroyed() && ROT_LOG_RENDERER_EVENTS.has(event)) { - mainWindow.webContents.send('account-rotation-log', entry); - } + try { + if (!entry || typeof entry !== 'object') return; + const { ts, event, ...rest } = entry; + const pairs = Object.entries(rest) + .map(([k, v]) => { + let sv; + try { sv = typeof v === 'string' ? v : JSON.stringify(v); } + catch { sv = ''; } + return `${k}=${sv}`; + }) + .join(' '); + rotLog(`[${event}] ${pairs}`, ts); + if (entry.jobId) { + _appendJobLog(entry.jobId, { ts: ts || Date.now(), kind: 'rot', event, ...rest }); + } + if (ROT_LOG_RENDERER_EVENTS.has(event)) { + safeSend('account-rotation-log', entry); + } + } catch (e) { debugLog(`rot-log listener error: ${e && e.message}`); } }); // Capture the manager identity at listener-registration time so the post- @@ -1578,13 +1591,11 @@ ipcMain.handle('start-upload', (_event, payload) => { try { await configStore.appendHistory(summary); } catch (err) { debugLog(`appendHistory failed: ${err.message}`); } - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('upload-batch-done', summary); - } + safeSend('upload-batch-done', summary); // Shutdown after finish handleShutdownAfterFinish(); - if (uploadManager === _thisManager) uploadManager = null; + if (uploadManager === _thisManager) { uploadManager = null; globalThis._mhuUploadManagerRef = null; } else debugLog('batch-done: skipping uploadManager null-out — a newer manager replaced this one mid-await'); }); @@ -1600,8 +1611,7 @@ ipcMain.handle('start-upload', (_event, payload) => { }).catch((err) => { debugLog(`startBatch REJECTED: ${err && err.stack ? err.stack : err}`); // Forward error to renderer as batch-done with failure - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('upload-batch-done', { + safeSend('upload-batch-done', { id: 'error', timestamp: new Date().toISOString(), total: tasks.length, @@ -1610,7 +1620,6 @@ ipcMain.handle('start-upload', (_event, payload) => { files: [], error: err ? err.message : 'Unbekannter Fehler' }); - } }); }); @@ -1797,6 +1806,7 @@ ipcMain.handle('create-support-bundle', async () => { { label: 'debug.log (last 5 MB)', path: paths.debug, maxBytes: 5 * 1024 * 1024 }, { label: 'account-rotation.log (last 2 MB)', path: paths.accountRotation, maxBytes: 2 * 1024 * 1024 }, { label: 'doodstream-debug.log (last 2 MB)', path: paths.doodstreamDebug, maxBytes: 2 * 1024 * 1024 }, + { label: 'crash.log', path: path.join(paths.logDir || path.dirname(paths.debug), 'crash.log'), maxBytes: 1 * 1024 * 1024 }, { label: 'fileuploader.log (last 1 MB)', path: paths.fileuploader, maxBytes: 1 * 1024 * 1024 } ] }); @@ -2011,13 +2021,9 @@ ipcMain.handle('app:check-updates', async () => { ipcMain.handle('app:install-update', () => { installUpdate((progress) => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('app:update-progress', progress); - } + safeSend('app:update-progress', progress); }).catch((err) => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('app:update-progress', { stage: 'error', error: err.message }); - } + safeSend('app:update-progress', { stage: 'error', error: err.message }); }); return { started: true }; }); @@ -2096,9 +2102,7 @@ function startFolderMonitor(settings) { folderMonitor.removeAllListeners(); folderMonitor.on('new-files', (files) => { debugLog(`folder-monitor: ${files.length} new file(s)`); - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('folder-monitor:new-files', files); - } + safeSend('folder-monitor:new-files', files); }); folderMonitor.on('error', (err) => { debugLog(`folder-monitor error: ${err.message}`); @@ -2342,9 +2346,7 @@ ipcMain.handle('remote:get-capture-source-id', async () => { // IPC: Client count updates from capture window ipcMain.on('remote:client-count', (_event, count) => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('remote:client-count', count); - } + safeSend('remote:client-count', count); }); // IPC: Remote settings @@ -2449,7 +2451,7 @@ ipcMain.on('drop-target:files', (_event, paths) => { mainWindow.show(); mainWindow.focus(); } - mainWindow.webContents.send('drop-target:files', paths); + safeSend('drop-target:files', paths); } }); @@ -2486,9 +2488,7 @@ function handleShutdownAfterFinish() { const { exec } = require('child_process'); // Notify renderer - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('shutdown-countdown', { mode: shutdownMode, seconds: 60 }); - } + safeSend('shutdown-countdown', { mode: shutdownMode, seconds: 60 }); // Clear any previous countdown to prevent orphaned timers if (shutdownTimer) clearTimeout(shutdownTimer); diff --git a/renderer/app.js b/renderer/app.js index 52e0d20..0c9d76e 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -1896,6 +1896,14 @@ async function cancelUpload() { // --- Progress handling --- function handleProgress(data) { + try { + if (!data || typeof data !== 'object') return; + _handleProgressImpl(data); + } catch (err) { + if (window.api && window.api.debugLog) window.api.debugLog(`handleProgress error: ${err && err.stack ? err.stack : err}`); + } +} +function _handleProgressImpl(data) { let job = data.jobId ? _jobIndexById.get(data.jobId) : null; if (!job && data.uploadId) job = _jobIndexByUploadId.get(data.uploadId); if (!job) { @@ -2157,6 +2165,14 @@ function _retryFailedFromBuckets(buckets, transientOnly) { } function handleStats(data) { + try { + if (!data || typeof data !== 'object') return; + _handleStatsImpl(data); + } catch (err) { + if (window.api && window.api.debugLog) window.api.debugLog(`handleStats error: ${err && err.stack ? err.stack : err}`); + } +} +function _handleStatsImpl(data) { lastUploadStats = { state: data.state || 'idle', globalSpeedKbs: data.globalSpeedKbs || 0,