Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 33 additions & 16 deletions src/update/self-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((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<void>((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)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the failure path we only unlink dest, but we never close or destroy writer unless the loop reaches the done branch. If reader.read() rejects, or if writer.write() returns false and the writer errors before drain, this block can exit with a live WriteStream and an in-flight pump(). Please mirror the shutdown pattern from src/files/download.ts: tear down the writer in finally, wait for finish/error, and only then remove the temporary file.

await new Promise<void>(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<UpdateTarget> {
Expand Down