file-lock.js 2.28 KB
/**
 * file-lock.js — 基于 .lock 文件的 Advisory Lock
 *
 * 用于保护模板关键文件(config.json, SKILL.md)的并发写入。
 * 使用 O_EXCL 原子操作创建锁文件,自动检测和清理过期锁。
 *
 * 使用方式:
 *   const { withFileLock } = require('./utils/file-lock');
 *   await withFileLock('/path/to/config.json', async () => {
 *     // 安全写入 config.json
 *   });
 */

const fs = require('fs');

const LOCK_TIMEOUT = 30_000;   // 30s 锁超期
const RETRY_INTERVAL = 200;    // 200ms 重试间隔
const MAX_WAIT = 10_000;       // 最多等待 10s

/**
 * 在文件锁保护下执行 fn
 * @param {string} filePath - 要保护的文件路径
 * @param {Function} fn - 在锁保护下执行的异步函数
 * @returns {Promise<*>} fn 的返回值
 */
async function withFileLock(filePath, fn) {
  const lockPath = filePath + '.lock';
  const deadline = Date.now() + MAX_WAIT;

  // 自旋等待获取锁
  while (Date.now() < deadline) {
    try {
      // O_EXCL: 如果文件已存在则失败(原子操作)
      fs.writeFileSync(lockPath, JSON.stringify({
        pid: process.pid,
        ts: Date.now(),
        file: filePath,
      }), { flag: 'wx' });

      // 获取锁成功
      try {
        return await fn();
      } finally {
        try { fs.unlinkSync(lockPath); } catch {}
      }
    } catch (e) {
      if (e.code === 'EEXIST') {
        // 锁已存在 — 检查是否过期
        try {
          const lockInfo = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
          if (Date.now() - lockInfo.ts > LOCK_TIMEOUT) {
            // 过期锁,强制移除
            console.warn(`[FileLock] ⚠️ 清理过期锁: ${lockPath} (age: ${((Date.now() - lockInfo.ts) / 1000).toFixed(0)}s)`);
            try { fs.unlinkSync(lockPath); } catch {}
            continue;
          }
        } catch {
          // 锁文件被并发删除或损坏,重试
        }

        await new Promise(r => setTimeout(r, RETRY_INTERVAL));
      } else {
        throw e;
      }
    }
  }

  // 超时 — 强制移除锁文件并执行(降级策略,不死等)
  console.error(`[FileLock] ❌ 获取文件锁超时 (${MAX_WAIT}ms): ${filePath},强制执行`);
  try { fs.unlinkSync(lockPath); } catch {}
  return await fn();
}

module.exports = { withFileLock };