fix(windows): destroy fileStream on failed download to prevent zip lock

This commit is contained in:
lilos
2026-03-04 13:38:09 +03:00
parent ed0ecf0e48
commit 5237065385
+25 -6
View File
@@ -276,6 +276,9 @@ async function downloadFile(url: string, dest: string): Promise<void> {
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS); const timeout = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
// Create file stream early so we can ensure cleanup on error
const fileStream = createWriteStream(dest);
try { try {
const response = await fetch(url, { const response = await fetch(url, {
signal: controller.signal, signal: controller.signal,
@@ -294,7 +297,6 @@ async function downloadFile(url: string, dest: string): Promise<void> {
let downloaded = 0; let downloaded = 0;
let lastLoggedPct = -1; let lastLoggedPct = -1;
const fileStream = createWriteStream(dest);
const reader = response.body.getReader(); const reader = response.body.getReader();
// Stream chunks to file with progress logging // Stream chunks to file with progress logging
@@ -318,19 +320,32 @@ async function downloadFile(url: string, dest: string): Promise<void> {
} }
} }
// Wait for file stream to finish // Wait for file stream to fully close (not just finish)
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
fileStream.end(() => resolve()); fileStream.end();
fileStream.on("close", () => resolve());
fileStream.on("error", reject); fileStream.on("error", reject);
}); });
const sizeMB = Math.floor(fs.statSync(dest).size / (1024 * 1024)); const sizeMB = Math.floor(fs.statSync(dest).size / (1024 * 1024));
console.log(`[cloakbrowser] Download complete: ${sizeMB} MB`); console.log(`[cloakbrowser] Download complete: ${sizeMB} MB`);
} catch (err) {
// Ensure file stream is destroyed on error to release the handle
if (!fileStream.destroyed) {
await new Promise<void>((resolve) => {
fileStream.destroy();
fileStream.on("close", () => resolve());
// Safety timeout in case close never fires
setTimeout(resolve, 2000);
});
}
throw err;
} finally { } finally {
clearTimeout(timeout); clearTimeout(timeout);
} }
} }
async function extractArchive( async function extractArchive(
archivePath: string, archivePath: string,
destDir: string, destDir: string,
@@ -387,12 +402,16 @@ async function extractTar(archivePath: string, destDir: string): Promise<void> {
} }
async function extractZip(archivePath: string, destDir: string): Promise<void> { async function extractZip(archivePath: string, destDir: string): Promise<void> {
const { execFileSync } = await import("node:child_process"); // Brief delay to ensure OS fully releases file handles (Windows)
// Use system unzip — available on Windows (PowerShell), macOS, and Linux await new Promise(resolve => setTimeout(resolve, 500));
if (process.platform === "win32") { if (process.platform === "win32") {
// PowerShell 5.1's Expand-Archive uses .NET FileStream which can conflict
// with recently-closed Node.js file handles. Use ZipFile API directly.
execFileSync("powershell", [ execFileSync("powershell", [
"-NoProfile", "-Command", "-NoProfile", "-Command",
`Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`, `Add-Type -AssemblyName System.IO.Compression.FileSystem; ` +
`[System.IO.Compression.ZipFile]::ExtractToDirectory('${archivePath}', '${destDir}')`,
], { timeout: 120_000 }); ], { timeout: 120_000 });
} else { } else {
execFileSync("unzip", ["-o", archivePath, "-d", destDir], { timeout: 120_000 }); execFileSync("unzip", ["-o", archivePath, "-d", destDir], { timeout: 120_000 });