diff --git a/src/update/self-update.ts b/src/update/self-update.ts index 1542b69..2cd8bf3 100644 --- a/src/update/self-update.ts +++ b/src/update/self-update.ts @@ -111,22 +111,39 @@ async function downloadFile(url: string, dest: string, onProgress?: (pct: number const writer = createWriteStream(dest); const reader = res.body!.getReader(); - await new Promise((resolve, reject) => { - writer.on('error', reject); - const pump = async () => { - try { - while (true) { - const { done, value } = await reader.read(); - if (done) { writer.end(); break; } - writer.write(value); - received += value.length; - if (onProgress && total > 0) onProgress(Math.round(received / total * 100)); - } - resolve(); - } catch (e) { reject(e); } - }; - pump(); - }); + try { + await new Promise((resolve, reject) => { + writer.on('error', reject); + // Resolve only after the writer has actually flushed and closed — + // otherwise verifySha256() can race the kernel's pagecache flush and + // produce spurious checksum mismatches on slow disks. + writer.on('finish', () => resolve()); + const pump = async () => { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { writer.end(); break; } + // Honour backpressure so we don't grow the writer's internal buffer + // unboundedly on large binaries / slow disks. + if (!writer.write(value)) { + await new Promise(r => writer.once('drain', r)); + } + received += value.length; + if (onProgress && total > 0) onProgress(Math.round(received / total * 100)); + } + } catch (e) { reject(e); } + }; + pump(); + }); + } catch (err) { + // Don't leave a half-downloaded binary in /tmp on failure. + try { (await import('fs')).unlinkSync(dest); } catch { /* best-effort */ } + throw err; + } finally { + // Always release the Web Streams reader lock — the API contract requires + // a paired acquire/release, and not doing so traps the underlying body. + reader.releaseLock(); + } } export async function resolveUpdateTarget(channel: Channel): Promise {