fix(npm): add retry logic for Windows zip extraction

On Windows, the downloaded ZIP file may remain locked by antivirus
software or Node.js file handle release delays. This causes
Expand-Archive to fail with "being used by another process" errors.

Added exponential backoff retry logic (5 attempts, 500ms-8s delays)
that detects file lock errors and retries the extraction. Non-lock
errors still fail immediately.

Fixes GH#889 (bd-5dlz)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jasper
2026-01-04 11:26:29 -08:00
committed by Steve Yegge
parent df66ecfe9e
commit c848c0fce2

View File

@@ -120,28 +120,53 @@ function extractTarGz(tarGzPath, destDir, binaryName) {
}
}
// Extract zip file (for Windows)
function extractZip(zipPath, destDir, binaryName) {
// Sleep helper for retry logic
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Extract zip file (for Windows) with retry logic
async function extractZip(zipPath, destDir, binaryName) {
console.log(`Extracting ${zipPath}...`);
try {
// Use unzip command or powershell on Windows
if (os.platform() === 'win32') {
execSync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`, { stdio: 'inherit' });
} else {
execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' });
const maxRetries = 5;
const baseDelayMs = 500;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Use unzip command or powershell on Windows
if (os.platform() === 'win32') {
execSync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`, { stdio: 'inherit' });
} else {
execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' });
}
// The binary should now be in destDir
const extractedBinary = path.join(destDir, binaryName);
if (!fs.existsSync(extractedBinary)) {
throw new Error(`Binary not found after extraction: ${extractedBinary}`);
}
console.log(`Binary extracted to: ${extractedBinary}`);
return; // Success
} catch (err) {
const isFileLockError = err.message && (
err.message.includes('being used by another process') ||
err.message.includes('Access is denied') ||
err.message.includes('cannot access the file')
);
if (isFileLockError && attempt < maxRetries) {
const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
console.log(`File may be locked (attempt ${attempt}/${maxRetries}). Retrying in ${delayMs}ms...`);
await sleep(delayMs);
} else if (attempt === maxRetries) {
throw new Error(`Failed to extract archive after ${maxRetries} attempts: ${err.message}`);
} else {
throw new Error(`Failed to extract archive: ${err.message}`);
}
}
// The binary should now be in destDir
const extractedBinary = path.join(destDir, binaryName);
if (!fs.existsSync(extractedBinary)) {
throw new Error(`Binary not found after extraction: ${extractedBinary}`);
}
console.log(`Binary extracted to: ${extractedBinary}`);
} catch (err) {
throw new Error(`Failed to extract archive: ${err.message}`);
}
}
@@ -175,7 +200,7 @@ async function install() {
// Extract the archive based on platform
if (platformName === 'windows') {
extractZip(archivePath, binDir, binaryName);
await extractZip(archivePath, binDir, binaryName);
} else {
extractTarGz(archivePath, binDir, binaryName);
}