VS Code扩展激活后大文件下载的多窗口冲突问题求助
解决VS Code扩展多实例下载冲突与异常锁残留问题
问题背景
开发VS Code扩展时,需下载大文件才能完成激活,已实现断点续传逻辑,但多窗口同时启动时会导致文件损坏。尝试用.lock文件做互斥控制,却遇到两个核心问题:
- VS Code异常关闭后,
.lock文件残留,重启后无法触发续传 deactivate()中删除.lock文件的逻辑未生效
可行解决方案
1. 改用排他式文件锁替代简单存在性检查
单纯检查文件存在无法处理异常残留的锁,改用fs.open的wx标志(排他创建,文件已存在则报错),确保只有第一个实例能获取锁:
async function acquireLock(lockPath: string): Promise<fs.promises.FileHandle | null> { try { // 排他创建锁文件,成功则持有文件句柄(防止其他实例误删) const handle = await fs.promises.open(lockPath, 'wx'); // 写入初始心跳时间 await handle.writeFile(JSON.stringify({ lastHeartbeat: Date.now() })); return handle; } catch (err) { // 锁已存在,返回null表示需要等待 return null; } }
2. 为锁文件添加心跳机制处理异常残留
持有锁的实例定期更新锁文件的心跳时间,等待的实例检查心跳是否超时(比如10秒),超时则判定锁已失效,主动清理并尝试重新获取:
// 持有锁的实例启动心跳 function startLockHeartbeat(lockHandle: fs.promises.FileHandle): NodeJS.Timeout { return setInterval(async () => { await lockHandle.writeFile(JSON.stringify({ lastHeartbeat: Date.now() }), { flag: 'w' }); }, 3000); // 每3秒更新一次心跳 } // 检查锁是否有效 async function isLockValid(lockPath: string): Promise<boolean> { try { const content = await fs.promises.readFile(lockPath, 'utf-8'); const { lastHeartbeat } = JSON.parse(content); // 心跳超过10秒未更新,判定锁失效 return Date.now() - lastHeartbeat < 10000; } catch { // 锁文件不存在或损坏,直接判定无效 return false; } }
3. 修复锁释放逻辑:不依赖deactivate(),改用finally兜底
VS Code的deactivate()仅在正常关闭时触发,异常崩溃时不会执行。因此必须在下载流程的finally块中释放锁,同时保留deactivate()的处理作为补充:
async function safeDownload(url: string, path: string, progressFilePath: string, lockPath: string) { let lockHandle: fs.promises.FileHandle | null = null; let heartbeatInterval: NodeJS.Timeout | null = null; try { // 尝试获取锁 lockHandle = await acquireLock(lockPath); if (!lockHandle) { // 等待锁释放或失效 await waitForValidLockRelease(lockPath, path); return; } // 启动心跳 heartbeatInterval = startLockHeartbeat(lockHandle); // 读取已有进度 let progress = 0; if (fs.existsSync(progressFilePath)) { const progressData = JSON.parse(await fs.promises.readFile(progressFilePath, 'utf-8')); progress = progressData.progress || 0; } // 执行下载逻辑 await downloadFile(url, path, progress, progressFilePath); // 下载完成后删除进度文件 if (fs.existsSync(progressFilePath)) { await fs.promises.unlink(progressFilePath); } } catch (err) { // 下载失败,清理已下载文件 if (fs.existsSync(path)) { await fs.promises.unlink(path); } throw err; } finally { // 无论成功失败,都释放锁 if (heartbeatInterval) clearInterval(heartbeatInterval); if (lockHandle) { await lockHandle.close(); await fs.promises.unlink(lockPath).catch(() => {}); } } } // 等待锁释放的逻辑 async function waitForValidLockRelease(lockPath: string, targetPath: string) { return new Promise<void>((resolve) => { const checkInterval = setInterval(async () => { // 检查目标文件是否已下载完成,或锁已失效 const fileExists = fs.existsSync(targetPath); const lockValid = await isLockValid(lockPath); if (fileExists || !lockValid) { clearInterval(checkInterval); // 如果锁失效,主动清理残留锁 if (!lockValid && fs.existsSync(lockPath)) { await fs.promises.unlink(lockPath).catch(() => {}); } resolve(); } }, 1000); }); }
4. 调整原下载函数为异步风格(适配新逻辑)
将原回调式的downloadFile改为异步,避免阻塞事件循环:
async function downloadFile(url: string, path: string, progress: number, progressFilePath: string) { return new Promise<void>((resolve, reject) => { const file = fs.createWriteStream(path, { flags: 'a' }); https.get(url, { headers: { Range: `bytes=${progress}-` } }, (response) => { const totalLength = parseInt(response.headers['content-length']!, 10); let downloaded = progress; response.on('data', async (chunk) => { downloaded += chunk.length; // 异步写入进度,避免阻塞 await fs.promises.writeFile(progressFilePath, JSON.stringify({ progress: downloaded })); }); response.pipe(file); response.on('end', () => { file.on('finish', resolve); }); }).on('error', (err) => { file.destroy(); fs.promises.unlink(path).catch(() => {}); reject(err); }); }); }
5. 优化deactivate()逻辑
虽然不能依赖它,但仍需补充锁清理,确保正常关闭时释放:
// 需全局存储lockHandle和lockPath,确保deactivate能访问到 let globalLockHandle: fs.promises.FileHandle | null = null; const globalLockPath = '/path/to/your/lock/file'; export async function deactivate(): Promise<void> { if (globalLockHandle) { await globalLockHandle.close(); await fs.promises.unlink(globalLockPath).catch(() => {}); } }
关键说明
- 排他锁+心跳机制解决了异常关闭后的锁残留问题,确保锁的有效性
finally块保证无论下载成功/失败/正常关闭都能释放锁- 异步写入进度和锁文件,避免阻塞VS Code的事件循环
内容的提问来源于stack exchange,提问作者Anastasiia Solop
相关产品推荐
相关产品推荐

