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.
This commit is contained in:
Sucukdeluxe
2026-08-20 14:01:07 +02:00
parent 7074e6662d
commit 990d4c4ffa
2 changed files with 72 additions and 3 deletions
+7 -3
View File
@@ -281,8 +281,9 @@ async function prepareUpdate(onProgress, options = {}) {
throw new Error(`Download fehlgeschlagen: HTTP ${res.status}`); throw new Error(`Download fehlgeschlagen: HTTP ${res.status}`);
} }
const totalBytes = check.assetSize || 0; const totalBytes = manifest.size;
let downloadedBytes = 0; let downloadedBytes = 0;
let lastReportedPercent = -1;
const chunks = []; const chunks = [];
const DOWNLOAD_STALL_MS = 45000; const DOWNLOAD_STALL_MS = 45000;
@@ -309,13 +310,16 @@ async function prepareUpdate(onProgress, options = {}) {
if (done) break; if (done) break;
chunks.push(value); chunks.push(value);
downloadedBytes += value.length; 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({ onProgress({
stage: 'downloading', stage: 'downloading',
percent: totalBytes > 0 ? Math.round((downloadedBytes / totalBytes) * 100) : 0, percent,
bytesDownloaded: downloadedBytes, bytesDownloaded: downloadedBytes,
bytesTotal: totalBytes bytesTotal: totalBytes
}); });
await new Promise(resolve => setImmediate(resolve));
} }
} }
+65
View File
@@ -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 () => { test('update preparation refreshes a cached release before downloading the installer', async () => {
const updaterPath = require.resolve('../lib/updater'); const updaterPath = require.resolve('../lib/updater');
const originalLoad = Module._load; const originalLoad = Module._load;