mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
fix(windows): destroy fileStream on failed download to prevent zip lock
This commit is contained in:
+27
-8
@@ -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 });
|
||||||
@@ -567,10 +586,10 @@ async function checkAndDownloadUpdate(): Promise<void> {
|
|||||||
function maybeTriggerUpdateCheck(): void {
|
function maybeTriggerUpdateCheck(): void {
|
||||||
// Wrapper update: once per process, not rate-limited
|
// Wrapper update: once per process, not rate-limited
|
||||||
if (!wrapperUpdateChecked) {
|
if (!wrapperUpdateChecked) {
|
||||||
checkWrapperUpdate().catch(() => {});
|
checkWrapperUpdate().catch(() => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Binary update: rate-limited to once per hour
|
// Binary update: rate-limited to once per hour
|
||||||
if (!shouldCheckForUpdate()) return;
|
if (!shouldCheckForUpdate()) return;
|
||||||
checkAndDownloadUpdate().catch(() => {});
|
checkAndDownloadUpdate().catch(() => { });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user