From 990d4c4ffade23be6ab533aa41a4cf3e90f60032 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:01:07 +0200 Subject: [PATCH] fix: render update download progress incrementally Use the verified manifest size for percentage calculations, suppress duplicate percentage events, and yield between buffered download updates so Electron can paint intermediate progress before completion. Add a regression test that proves renderer-observable progress during an immediately buffered download. --- lib/updater.js | 10 ++++-- tests/updater-version.test.js | 65 +++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/lib/updater.js b/lib/updater.js index 0135a20..365a8cc 100644 --- a/lib/updater.js +++ b/lib/updater.js @@ -281,8 +281,9 @@ async function prepareUpdate(onProgress, options = {}) { throw new Error(`Download fehlgeschlagen: HTTP ${res.status}`); } - const totalBytes = check.assetSize || 0; + const totalBytes = manifest.size; let downloadedBytes = 0; + let lastReportedPercent = -1; const chunks = []; const DOWNLOAD_STALL_MS = 45000; @@ -309,13 +310,16 @@ async function prepareUpdate(onProgress, options = {}) { if (done) break; chunks.push(value); downloadedBytes += value.length; - if (onProgress) { + const percent = Math.max(0, Math.min(100, Math.floor((downloadedBytes / totalBytes) * 100))); + if (onProgress && percent !== lastReportedPercent) { + lastReportedPercent = percent; onProgress({ stage: 'downloading', - percent: totalBytes > 0 ? Math.round((downloadedBytes / totalBytes) * 100) : 0, + percent, bytesDownloaded: downloadedBytes, bytesTotal: totalBytes }); + await new Promise(resolve => setImmediate(resolve)); } } diff --git a/tests/updater-version.test.js b/tests/updater-version.test.js index 91e0eba..eefba8b 100644 --- a/tests/updater-version.test.js +++ b/tests/updater-version.test.js @@ -87,6 +87,71 @@ test('update preparation writes a verified installer without launching it', asyn } }); +test('buffered installer downloads yield between progress updates so the renderer can paint', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mhu-updater-progress-test-')); + const installer = Buffer.alloc(128 * 1024, 0); + installer[0] = 0x4d; + installer[1] = 0x5a; + const chunks = [ + installer.subarray(0, 32 * 1024), + installer.subarray(32 * 1024, 64 * 1024), + installer.subarray(64 * 1024, 96 * 1024), + installer.subarray(96 * 1024) + ]; + const progress = []; + let readerIndex = 0; + let preparationFinished = false; + let rendererObservedProgressBeforeFinish = false; + let rendererObservationScheduled = false; + + try { + await prepareUpdate(value => { + progress.push(value); + if (value.stage !== 'downloading' || rendererObservationScheduled) return; + rendererObservationScheduled = true; + setImmediate(() => { + rendererObservedProgressBeforeFinish = !preparationFinished; + }); + }, { + checkResult: { + available: true, + assetUrl: 'https://update.invalid/setup.exe', + assetName: 'setup.exe', + remoteVersion: '2.2.0', + latestYmlUrl: 'https://update.invalid/latest.yml' + }, + tempDir, + fetchImpl: async url => url.endsWith('latest.yml') + ? { + ok: true, + status: 200, + text: async () => `version: 2.2.0\npath: setup.exe\nsha512: ${crypto.createHash('sha512').update(installer).digest('base64')}\nsize: ${installer.length}\n` + } + : { + ok: true, + status: 200, + body: { + getReader: () => ({ + read: async () => readerIndex < chunks.length + ? { done: false, value: chunks[readerIndex++] } + : { done: true } + }) + } + } + }); + preparationFinished = true; + await new Promise(resolve => setImmediate(resolve)); + + assert.equal(rendererObservedProgressBeforeFinish, true); + assert.deepEqual( + progress.filter(value => value.stage === 'downloading').map(value => value.percent), + [25, 50, 75, 100] + ); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + test('update preparation refreshes a cached release before downloading the installer', async () => { const updaterPath = require.resolve('../lib/updater'); const originalLoad = Module._load;