From b51cd2c8ff980bf0eb8a0111e008387e374d51ea Mon Sep 17 00:00:00 2001 From: Matthew Harris Glover Date: Thu, 16 Jul 2026 15:47:29 -0400 Subject: [PATCH] fix(update): reliable Linux download completion via a counting Transform The manual `body.on('data')` progress listener alongside pipeline() is a fragile pattern that can leave the download looking stuck at 100% (the completion/'downloaded' path never runs). Count bytes with a pass-through Transform inside the pipeline instead, so pipeline() owns the chain's completion and reliably reaches the swap + downloaded broadcast. Co-Authored-By: Claude Fable 5 --- src/main/update-manager.ts | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/main/update-manager.ts b/src/main/update-manager.ts index f4b78bb..0e0b94b 100644 --- a/src/main/update-manager.ts +++ b/src/main/update-manager.ts @@ -74,7 +74,7 @@ import { app, BrowserWindow } from 'electron'; import { spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -import { Readable } from 'stream'; +import { Readable, Transform } from 'stream'; import { pipeline } from 'stream/promises'; import type { UpdateInfo } from 'velopack'; import { IPC_UPDATE_EVENT_AVAILABLE, IPC_UPDATE_EVENT_DOWNLOADED, IPC_UPDATE_EVENT_PROGRESS } from './ipc-channels'; @@ -236,20 +236,27 @@ function startLinuxDownload(url: string, appImagePath: string, remoteSha: string const total = Number(dl.headers.get('content-length')) || 0; let received = 0; let lastPercent = -1; - const body = Readable.fromWeb(dl.body as Parameters[0]); - body.on('data', (chunk: Buffer) => { - received += chunk.length; - if (!total) return; - const percent = Math.floor((received / total) * 100); - // Broadcast only on whole-percent changes so we don't flood the - // renderer with an event per chunk. - if (percent !== lastPercent) { - lastPercent = percent; - downloadPercent = percent; - broadcast(IPC_UPDATE_EVENT_PROGRESS, { percent, channel: currentChannel }); - } + // Count bytes with a pass-through Transform in the pipeline rather + // than a manual 'data' listener on the source: pipeline() then owns + // the whole chain's completion + backpressure, so it reliably + // resolves at end-of-stream (a stray 'data' listener on the source + // can leave the download looking stuck at 100%). + const counter = new Transform({ + transform(chunk: Buffer, _enc, cb) { + received += chunk.length; + if (total) { + const percent = Math.floor((received / total) * 100); + if (percent !== lastPercent) { + lastPercent = percent; + downloadPercent = percent; + broadcast(IPC_UPDATE_EVENT_PROGRESS, { percent, channel: currentChannel }); + } + } + cb(null, chunk); + }, }); - await pipeline(body, fs.createWriteStream(tmpPath)); + const body = Readable.fromWeb(dl.body as Parameters[0]); + await pipeline(body, counter, fs.createWriteStream(tmpPath)); fs.chmodSync(tmpPath, 0o755); fs.renameSync(tmpPath, appImagePath); linuxDownloadedSha = remoteSha;