From 5237065385de6a0aa7307c646ec07dd7f08fdfe9 Mon Sep 17 00:00:00 2001 From: lilos Date: Wed, 4 Mar 2026 13:38:09 +0300 Subject: [PATCH] fix(windows): destroy fileStream on failed download to prevent zip lock --- js/src/download.ts | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/js/src/download.ts b/js/src/download.ts index df17fe8..70223fc 100644 --- a/js/src/download.ts +++ b/js/src/download.ts @@ -89,8 +89,8 @@ export async function ensureBinary(): Promise { if (!fs.existsSync(downloadedPath)) { throw new Error( `Download completed but binary not found at expected path: ${downloadedPath}. ` + - `This may indicate a packaging issue. Please report at ` + - `https://github.com/CloakHQ/cloakbrowser/issues` + `This may indicate a packaging issue. Please report at ` + + `https://github.com/CloakHQ/cloakbrowser/issues` ); } @@ -276,6 +276,9 @@ async function downloadFile(url: string, dest: string): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS); + // Create file stream early so we can ensure cleanup on error + const fileStream = createWriteStream(dest); + try { const response = await fetch(url, { signal: controller.signal, @@ -294,7 +297,6 @@ async function downloadFile(url: string, dest: string): Promise { let downloaded = 0; let lastLoggedPct = -1; - const fileStream = createWriteStream(dest); const reader = response.body.getReader(); // Stream chunks to file with progress logging @@ -318,19 +320,32 @@ async function downloadFile(url: string, dest: string): Promise { } } - // Wait for file stream to finish + // Wait for file stream to fully close (not just finish) await new Promise((resolve, reject) => { - fileStream.end(() => resolve()); + fileStream.end(); + fileStream.on("close", () => resolve()); fileStream.on("error", reject); }); const sizeMB = Math.floor(fs.statSync(dest).size / (1024 * 1024)); 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((resolve) => { + fileStream.destroy(); + fileStream.on("close", () => resolve()); + // Safety timeout in case close never fires + setTimeout(resolve, 2000); + }); + } + throw err; } finally { clearTimeout(timeout); } } + async function extractArchive( archivePath: string, destDir: string, @@ -387,12 +402,16 @@ async function extractTar(archivePath: string, destDir: string): Promise { } async function extractZip(archivePath: string, destDir: string): Promise { - const { execFileSync } = await import("node:child_process"); - // Use system unzip — available on Windows (PowerShell), macOS, and Linux + // Brief delay to ensure OS fully releases file handles (Windows) + await new Promise(resolve => setTimeout(resolve, 500)); + 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", [ "-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 }); } else { execFileSync("unzip", ["-o", archivePath, "-d", destDir], { timeout: 120_000 }); @@ -520,7 +539,7 @@ export async function checkWrapperUpdate(): Promise { if (data.version && versionNewer(data.version, WRAPPER_VERSION)) { console.warn( `[cloakbrowser] Update available: ${WRAPPER_VERSION} → ${data.version}. ` + - `Run: npm install cloakbrowser@latest` + `Run: npm install cloakbrowser@latest` ); } } catch { @@ -567,10 +586,10 @@ async function checkAndDownloadUpdate(): Promise { function maybeTriggerUpdateCheck(): void { // Wrapper update: once per process, not rate-limited if (!wrapperUpdateChecked) { - checkWrapperUpdate().catch(() => {}); + checkWrapperUpdate().catch(() => { }); } // Binary update: rate-limited to once per hour if (!shouldCheckForUpdate()) return; - checkAndDownloadUpdate().catch(() => {}); + checkAndDownloadUpdate().catch(() => { }); }