/** * 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 };