You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 01:55:23