From c848c0fce28b940d1c4ac892e46b7e25556f1a4a Mon Sep 17 00:00:00 2001 From: jasper Date: Sun, 4 Jan 2026 11:26:29 -0800 Subject: [PATCH] fix(npm): add retry logic for Windows zip extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- npm-package/scripts/postinstall.js | 65 +++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/npm-package/scripts/postinstall.js b/npm-package/scripts/postinstall.js index 7e24059e..15f032c8 100755 --- a/npm-package/scripts/postinstall.js +++ b/npm-package/scripts/postinstall.js @@ -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); }