file-lock.js
2.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* 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 };