@actiondock/core 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/app/app.d.ts +14 -1
  2. package/dist/app/app.js +88 -215
  3. package/dist/app/static-index.d.ts +39 -0
  4. package/dist/app/static-index.js +164 -0
  5. package/dist/app/types.d.ts +5 -0
  6. package/dist/catalog/action-index.js +6 -4
  7. package/dist/catalog/package-catalog.js +2 -1
  8. package/dist/doctor/checks/index.d.ts +5 -0
  9. package/dist/doctor/checks/index.js +5 -0
  10. package/dist/doctor/checks/project-locate.d.ts +6 -0
  11. package/dist/doctor/checks/project-locate.js +36 -0
  12. package/dist/doctor/checks/project.d.ts +6 -0
  13. package/dist/doctor/checks/project.js +383 -0
  14. package/dist/doctor/checks/registry.d.ts +13 -0
  15. package/dist/doctor/checks/registry.js +207 -0
  16. package/dist/doctor/checks/runtime.d.ts +13 -0
  17. package/dist/doctor/checks/runtime.js +90 -0
  18. package/dist/doctor/checks/storage.d.ts +5 -0
  19. package/dist/doctor/checks/storage.js +34 -0
  20. package/dist/doctor/context.d.ts +35 -0
  21. package/dist/doctor/context.js +11 -0
  22. package/dist/doctor/doctor.d.ts +5 -0
  23. package/dist/doctor/doctor.js +33 -602
  24. package/dist/errors.d.ts +2 -0
  25. package/dist/errors.js +2 -0
  26. package/dist/execution/service.js +14 -9
  27. package/dist/host/host.d.ts +33 -0
  28. package/dist/host/host.js +222 -362
  29. package/dist/host/routing.d.ts +92 -0
  30. package/dist/host/routing.js +238 -0
  31. package/dist/host/types.d.ts +8 -0
  32. package/dist/ipc/host.js +38 -0
  33. package/dist/platform/types.d.ts +7 -0
  34. package/dist/process/control-arbiter.d.ts +107 -0
  35. package/dist/process/control-arbiter.js +251 -0
  36. package/dist/process/diagnostics-sink.d.ts +26 -0
  37. package/dist/process/diagnostics-sink.js +35 -0
  38. package/dist/process/index.d.ts +7 -0
  39. package/dist/process/index.js +7 -0
  40. package/dist/process/input-dispatcher.d.ts +88 -0
  41. package/dist/process/input-dispatcher.js +240 -0
  42. package/dist/process/managed-record.d.ts +78 -0
  43. package/dist/process/managed-record.js +1 -0
  44. package/dist/process/process-manager.d.ts +15 -56
  45. package/dist/process/process-manager.js +119 -707
  46. package/dist/process/reservation-table.d.ts +39 -0
  47. package/dist/process/reservation-table.js +80 -0
  48. package/dist/process/run-executor.d.ts +16 -0
  49. package/dist/process/run-executor.js +155 -0
  50. package/dist/process/terminal-output-cache.d.ts +66 -0
  51. package/dist/process/terminal-output-cache.js +132 -0
  52. package/dist/profile/client-actions.d.ts +56 -0
  53. package/dist/profile/client-actions.js +215 -0
  54. package/dist/profile/client-config.d.ts +20 -0
  55. package/dist/profile/client-config.js +51 -0
  56. package/dist/profile/client-health.d.ts +14 -0
  57. package/dist/profile/client-health.js +68 -0
  58. package/dist/profile/client-playbooks.d.ts +19 -0
  59. package/dist/profile/client-playbooks.js +36 -0
  60. package/dist/profile/client-query.d.ts +30 -0
  61. package/dist/profile/client-query.js +70 -0
  62. package/dist/profile/client-runs.d.ts +42 -0
  63. package/dist/profile/client-runs.js +58 -0
  64. package/dist/profile/client-state.d.ts +56 -0
  65. package/dist/profile/client-state.js +83 -0
  66. package/dist/profile/client-transport.d.ts +61 -0
  67. package/dist/profile/client-transport.js +155 -0
  68. package/dist/profile/client.d.ts +23 -142
  69. package/dist/profile/client.js +28 -588
  70. package/dist/profile/manager.js +1 -1
  71. package/dist/project/closure.d.ts +4 -4
  72. package/dist/project/closure.js +11 -5
  73. package/dist/project/init.js +2 -2
  74. package/dist/project/loader.d.ts +0 -25
  75. package/dist/project/loader.js +1 -154
  76. package/dist/project/transactions.d.ts +12 -14
  77. package/dist/project/transactions.js +60 -836
  78. package/dist/registry/registry.js +6 -4
  79. package/dist/registry/resolve.js +1 -1
  80. package/dist/runtime/action-registry.d.ts +45 -0
  81. package/dist/runtime/action-registry.js +108 -0
  82. package/dist/runtime/clock.d.ts +5 -17
  83. package/dist/runtime/clock.js +5 -15
  84. package/dist/runtime/context.d.ts +3 -0
  85. package/dist/runtime/context.js +17 -56
  86. package/dist/runtime/index.d.ts +3 -0
  87. package/dist/runtime/index.js +3 -0
  88. package/dist/runtime/package-runner-factory.d.ts +82 -0
  89. package/dist/runtime/package-runner-factory.js +186 -0
  90. package/dist/runtime/run-persistence.d.ts +85 -0
  91. package/dist/runtime/run-persistence.js +111 -0
  92. package/dist/runtime/runner.d.ts +23 -33
  93. package/dist/runtime/runner.js +92 -286
  94. package/dist/runtime/standalone.d.ts +11 -1
  95. package/dist/runtime/standalone.js +137 -25
  96. package/dist/server/dispatcher.d.ts +8 -3
  97. package/dist/server/dispatcher.js +16 -40
  98. package/dist/server/routes/runs.js +6 -4
  99. package/dist/server/security.d.ts +1 -7
  100. package/dist/server/security.js +2 -13
  101. package/dist/storage/clock.d.ts +25 -0
  102. package/dist/storage/clock.js +23 -0
  103. package/dist/storage/data-dir-lock.d.ts +3 -36
  104. package/dist/storage/data-dir-lock.js +48 -881
  105. package/dist/storage/index.d.ts +8 -1
  106. package/dist/storage/index.js +8 -2
  107. package/dist/storage/lazy.js +1 -0
  108. package/dist/storage/lock-core.d.ts +162 -0
  109. package/dist/storage/lock-core.js +885 -0
  110. package/dist/storage/sqlite.d.ts +19 -6
  111. package/dist/storage/sqlite.js +81 -89
  112. package/dist/storage/types.d.ts +27 -5
  113. package/dist/storage/types.js +21 -0
  114. package/dist/storage/utils.d.ts +22 -0
  115. package/dist/storage/utils.js +34 -0
  116. package/dist/target/local.d.ts +1 -0
  117. package/dist/target/local.js +23 -17
  118. package/dist/target/remote-errors.d.ts +21 -0
  119. package/dist/target/remote-errors.js +51 -0
  120. package/dist/target/remote-polling.d.ts +35 -0
  121. package/dist/target/remote-polling.js +104 -0
  122. package/dist/target/remote.d.ts +4 -0
  123. package/dist/target/remote.js +108 -301
  124. package/dist/target/sse-parser.d.ts +35 -0
  125. package/dist/target/sse-parser.js +163 -0
  126. package/dist/target/target.js +2 -0
  127. package/dist/target/types.d.ts +6 -0
  128. package/dist/utils/index.d.ts +1 -0
  129. package/dist/utils/index.js +1 -0
  130. package/dist/utils/net.d.ts +10 -0
  131. package/dist/utils/net.js +16 -0
  132. package/dist/version.d.ts +1 -1
  133. package/dist/version.js +1 -1
  134. package/package.json +2 -2
@@ -1,668 +1,16 @@
1
- import fs, { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
1
+ import { existsSync, mkdirSync, renameSync, statSync, writeFileSync } from "node:fs";
2
2
  import { hostname } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { randomUUID } from "node:crypto";
5
- /**
6
- * 同步休眠指定毫秒数。
7
- */
8
- function sleepSync(ms) {
9
- try {
10
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
11
- }
12
- catch {
13
- const end = Date.now() + ms;
14
- while (Date.now() < end) {
15
- // 降级忙等待
16
- }
17
- }
18
- }
19
- /**
20
- * 获取排他锁路径最近一次修改时间戳。
21
- */
22
- function getLockMtimeMs(lockPath, isDir) {
23
- try {
24
- const stat = statSync(lockPath);
25
- let mtime = Math.max(stat.mtimeMs, stat.ctimeMs ?? 0);
26
- if (isDir) {
27
- const metaPath = join(lockPath, "metadata.json");
28
- if (existsSync(metaPath)) {
29
- const metaStat = statSync(metaPath);
30
- mtime = Math.max(mtime, metaStat.mtimeMs, metaStat.ctimeMs ?? 0);
31
- }
32
- }
33
- return mtime;
34
- }
35
- catch {
36
- return Date.now();
37
- }
38
- }
39
- /**
40
- * 读取排他锁元数据并执行宽限期检测。
41
- */
42
- function readLockWithGracePeriod(lockPath, gracePeriodMs = 3000, deadline) {
43
- if (!existsSync(lockPath)) {
44
- return { exists: false, isDir: false, inGracePeriod: false };
45
- }
46
- let isDir = false;
47
- try {
48
- isDir = statSync(lockPath).isDirectory();
49
- }
50
- catch {
51
- return { exists: false, isDir: false, inGracePeriod: false };
52
- }
53
- const metaPath = isDir ? join(lockPath, "metadata.json") : lockPath;
54
- const tryParse = () => {
55
- try {
56
- if (existsSync(metaPath)) {
57
- const raw = readFileSync(metaPath, "utf8");
58
- if (raw.trim().length > 0) {
59
- const parsed = JSON.parse(raw);
60
- if (parsed && typeof parsed.pid === "number") {
61
- return parsed;
62
- }
63
- }
64
- }
65
- }
66
- catch {
67
- // 损坏或并发写入中
68
- }
69
- return undefined;
70
- };
71
- const initialInfo = tryParse();
72
- if (initialInfo) {
73
- return { exists: true, isDir, info: initialInfo, inGracePeriod: false };
74
- }
75
- // 元数据缺失或不可解析,检查修改时间是否在宽限期内
76
- const mtime = getLockMtimeMs(lockPath, isDir);
77
- const age = Date.now() - mtime;
78
- if (age < gracePeriodMs) {
79
- // 处于宽限期内,说明并发所有者可能正在写入,进行有限次重试等待(严格受限于 deadline)
80
- const maxRetries = 20;
81
- for (let i = 0; i < maxRetries; i++) {
82
- if (deadline !== undefined && Date.now() >= deadline) {
83
- break;
84
- }
85
- const remaining = deadline !== undefined ? deadline - Date.now() : 50;
86
- if (remaining <= 0) {
87
- break;
88
- }
89
- sleepSync(Math.min(50, remaining));
90
- const retriedInfo = tryParse();
91
- if (retriedInfo) {
92
- return { exists: true, isDir, info: retriedInfo, inGracePeriod: false };
93
- }
94
- if (!existsSync(lockPath)) {
95
- return { exists: false, isDir: false, inGracePeriod: false };
96
- }
97
- }
98
- const currentAge = Date.now() - getLockMtimeMs(lockPath, isDir);
99
- if (currentAge < gracePeriodMs) {
100
- return { exists: true, isDir, inGracePeriod: true };
101
- }
102
- }
103
- return { exists: true, isDir, inGracePeriod: false };
104
- }
105
- /**
106
- * 检查目标进程是否处于存活状态。
107
- *
108
- * @param pid 待检测的进程标识符
109
- */
110
- export function isProcessAlive(pid) {
111
- if (typeof pid !== "number" || isNaN(pid) || pid <= 0) {
112
- return false;
113
- }
114
- try {
115
- process.kill(pid, 0);
116
- return true;
117
- }
118
- catch (err) {
119
- return Boolean(err && err.code === "EPERM");
120
- }
121
- }
122
- /**
123
- * 检查接管守卫(reclaim guard)目录状态。
124
- *
125
- * - 若目录不存在:返回非活跃。
126
- * - 若处于宽限期内或持有者存活:返回活跃,禁止抢先创建主锁。
127
- * - 若持有者已死亡且超过宽限期:返回陈旧,可供安全清理。
128
- */
129
- function checkReclaimGuard(reclaimPath, isAliveFn, gracePeriodMs = 1000) {
130
- if (!existsSync(reclaimPath)) {
131
- return { exists: false, active: false, isStale: false };
132
- }
133
- let isDir = false;
134
- try {
135
- isDir = statSync(reclaimPath).isDirectory();
136
- }
137
- catch {
138
- return { exists: false, active: false, isStale: false };
139
- }
140
- const metaPath = isDir ? join(reclaimPath, "metadata.json") : reclaimPath;
141
- const tryParse = () => {
142
- try {
143
- if (existsSync(metaPath)) {
144
- const raw = readFileSync(metaPath, "utf8");
145
- if (raw.trim().length > 0) {
146
- const parsed = JSON.parse(raw);
147
- if (parsed && typeof parsed.pid === "number") {
148
- return parsed;
149
- }
150
- }
151
- }
152
- }
153
- catch { }
154
- return undefined;
155
- };
156
- const info = tryParse();
157
- if (info && typeof info.pid === "number") {
158
- const guardToken = typeof info.guardToken === "string" ? info.guardToken : undefined;
159
- const createdAt = typeof info.createdAt === "number" ? info.createdAt : getLockMtimeMs(reclaimPath, isDir);
160
- const age = Date.now() - createdAt;
161
- if (isAliveFn(info.pid)) {
162
- return { exists: true, active: true, isStale: false, holderPid: info.pid, guardToken };
163
- }
164
- if (age < gracePeriodMs) {
165
- return { exists: true, active: true, isStale: false, holderPid: info.pid, guardToken };
166
- }
167
- return { exists: true, active: false, isStale: true, holderPid: info.pid, guardToken };
168
- }
169
- const mtime = getLockMtimeMs(reclaimPath, isDir);
170
- const age = Date.now() - mtime;
171
- if (age < gracePeriodMs) {
172
- return { exists: true, active: true, isStale: false };
173
- }
174
- return { exists: true, active: false, isStale: true };
175
- }
176
- /**
177
- * 尝试原子获取接管守卫(reclaim guard)。
178
- * 基于 mkdirSync 原子创建目录并写入自身元数据与唯一 guardToken。
179
- */
180
- function tryAcquireReclaimGuard(reclaimPath, pid, guardToken) {
181
- try {
182
- mkdirSync(reclaimPath, { mode: 0o700 });
183
- const metaPath = join(reclaimPath, "metadata.json");
184
- const tmpPath = join(reclaimPath, `metadata.json.tmp.${pid}.${randomUUID().slice(0, 8)}`);
185
- writeFileSync(tmpPath, JSON.stringify({ pid, guardToken, createdAt: Date.now() }, null, 2), { mode: 0o600 });
186
- renameSync(tmpPath, metaPath);
187
- return true;
188
- }
189
- catch {
190
- return false;
191
- }
192
- }
193
- /**
194
- * 安全清理陈旧接管守卫(reclaim guard)。
195
- * 采用原子重命名检疫并严格核对 guardToken 与持有者存活状态,确保仅删除目标陈旧 guard,严禁误删活跃守卫或并发新守卫。
196
- *
197
- * @param reclaimPath 目标守卫路径
198
- * @param expectedGuardToken 预期持有的守卫令牌(必填,拒绝未指定令牌的盲目清理)
199
- */
200
- export function safeRemoveStaleReclaimGuard(reclaimPath, expectedGuardToken) {
201
- if (!expectedGuardToken || typeof expectedGuardToken !== "string")
202
- return;
203
- if (!existsSync(reclaimPath))
204
- return;
205
- let isDir = false;
206
- try {
207
- isDir = statSync(reclaimPath).isDirectory();
208
- }
209
- catch {
210
- return;
211
- }
212
- const metaPath = isDir ? join(reclaimPath, "metadata.json") : reclaimPath;
213
- if (!existsSync(metaPath)) {
214
- return;
215
- }
216
- try {
217
- const metaStat = statSync(metaPath);
218
- const mtimeMs = metaStat.mtimeMs;
219
- const raw = readFileSync(metaPath, "utf8");
220
- if (raw.trim().length === 0) {
221
- return;
222
- }
223
- const parsed = JSON.parse(raw);
224
- if (parsed?.guardToken !== expectedGuardToken) {
225
- return;
226
- }
227
- // 自身验证 fail-closed:
228
- // 若不是当前进程自身清理自身守卫(即 parsed.pid !== process.pid):
229
- // 1. 验证持有者 PID 是否存活,若仍存活则判定守卫仍处于活跃状态,严禁删除!
230
- // 2. 验证是否处于创建宽限期内(1000ms),若在宽限期内严禁删除!
231
- if (parsed?.pid !== process.pid) {
232
- if (typeof parsed?.pid === "number" && isProcessAlive(parsed.pid)) {
233
- return;
234
- }
235
- if (Date.now() - mtimeMs < 1000) {
236
- return;
237
- }
238
- }
239
- }
240
- catch {
241
- return;
242
- }
243
- const quarantinePath = `${reclaimPath}.quarantine.${process.pid}.${Date.now()}.${randomUUID().slice(0, 8)}`;
244
- try {
245
- fs.renameSync(reclaimPath, quarantinePath);
246
- }
247
- catch {
248
- return;
249
- }
250
- try {
251
- const quarantinedMetaPath = isDir ? join(quarantinePath, "metadata.json") : quarantinePath;
252
- let actualGuardToken;
253
- let actualPid;
254
- if (existsSync(quarantinedMetaPath)) {
255
- try {
256
- const raw = readFileSync(quarantinedMetaPath, "utf8");
257
- if (raw.trim().length > 0) {
258
- const parsed = JSON.parse(raw);
259
- actualGuardToken = parsed?.guardToken;
260
- actualPid = typeof parsed?.pid === "number" ? parsed.pid : undefined;
261
- }
262
- }
263
- catch { }
264
- }
265
- // 后置校验:必须再次核对 actualGuardToken 与 expectedGuardToken 一致,
266
- // 且若持有者非自身,必须确保 actualPid 确已死亡。
267
- if (expectedGuardToken !== actualGuardToken ||
268
- (actualPid !== undefined && actualPid !== process.pid && isProcessAlive(actualPid))) {
269
- // 并非此前检查的目标陈旧 guard(已被并发者替换或持有者存活)
270
- // 保持隔离状态,转换为 .orphan 脱离态,脱离创建者 Host PID 的存活保护,由 GC 基于规范路径凭据与时间清理
271
- const orphanPath = `${reclaimPath}.orphan.${Date.now()}.${randomUUID().slice(0, 8)}`;
272
- try {
273
- fs.renameSync(quarantinePath, orphanPath);
274
- }
275
- catch { }
276
- return;
277
- }
278
- // 确认正是目标陈旧 guard(或自身持有的 guard),安全清理
279
- fs.rmSync(quarantinePath, { recursive: true, force: true });
280
- }
281
- catch {
282
- // 发生异常,保持隔离状态,转换为 .orphan 脱离态
283
- const orphanPath = `${reclaimPath}.orphan.${Date.now()}.${randomUUID().slice(0, 8)}`;
284
- try {
285
- fs.renameSync(quarantinePath, orphanPath);
286
- }
287
- catch { }
288
- }
289
- }
290
- /**
291
- * 从隔离或回滚临时目录名称中解析操作者 PID 与隔离生成时间戳。
292
- * 命名规范:*.(quarantine|rollback|release).<pid>.<timestamp>.<uuid>
293
- */
294
- export function parseQuarantineTimestamp(entryName) {
295
- const orphanMatch = entryName.match(/\.orphan\.(\d+)(?:\.|$)/);
296
- if (orphanMatch && orphanMatch[1]) {
297
- const ts = parseInt(orphanMatch[1], 10);
298
- return {
299
- timestamp: !Number.isNaN(ts) && ts > 0 ? ts : undefined,
300
- };
301
- }
302
- const match = entryName.match(/\.(?:quarantine|rollback|release)\.(\d+)\.(\d+)(?:\.|$)/);
303
- if (match && match[1] && match[2]) {
304
- const pid = parseInt(match[1], 10);
305
- const ts = parseInt(match[2], 10);
306
- return {
307
- operatorPid: !Number.isNaN(pid) && pid > 0 ? pid : undefined,
308
- timestamp: !Number.isNaN(ts) && ts > 0 ? ts : undefined,
309
- };
310
- }
311
- const fallbackMatch = entryName.match(/\.(?:quarantine|rollback|release)\.(\d+)(?:\.|$)/);
312
- if (fallbackMatch && fallbackMatch[1]) {
313
- const ts = parseInt(fallbackMatch[1], 10);
314
- return {
315
- timestamp: !Number.isNaN(ts) && ts > 0 ? ts : undefined,
316
- };
317
- }
318
- return {};
319
- }
320
- /**
321
- * 读取目标规范路径(主锁目录或 reclaim 目录)中记录的当前有效令牌凭据。
322
- */
323
- function readCanonicalToken(targetPath) {
324
- try {
325
- if (!existsSync(targetPath)) {
326
- return undefined;
327
- }
328
- let isDir = false;
329
- try {
330
- isDir = statSync(targetPath).isDirectory();
331
- }
332
- catch {
333
- return undefined;
334
- }
335
- const metaPath = isDir ? join(targetPath, "metadata.json") : targetPath;
336
- if (!existsSync(metaPath)) {
337
- return undefined;
338
- }
339
- const raw = readFileSync(metaPath, "utf8");
340
- if (raw.trim().length === 0) {
341
- return undefined;
342
- }
343
- const parsed = JSON.parse(raw);
344
- if (typeof parsed?.guardToken === "string" && parsed.guardToken.length > 0) {
345
- return parsed.guardToken;
346
- }
347
- if (typeof parsed?.lockToken === "string" && parsed.lockToken.length > 0) {
348
- return parsed.lockToken;
349
- }
350
- return undefined;
351
- }
352
- catch {
353
- return undefined;
354
- }
355
- }
356
- /**
357
- * 清理过期的隔离目录(GC 回收机制)。
358
- * 实施分流存活校验,防止误删活跃持锁者与回滚中目录,同时防止孤儿目录无限泄漏:
359
- * - 共同前置条件:存活年龄必须超过 maxAgeMs(默认 10000ms),未超期前严禁清理(保持宽限期)。
360
- * - 活跃隔离目录(quarantine / rollback / release):若 operatorPid 存活或内部 metadata.json 记录的 pid 与 childPids 存活,跳过不予清理。
361
- * - 孤儿脱离目录(orphan):内部旧 metadata.pid 不再作为续命依据;提取其内部 orphanToken,仅当当前规范主路径(主锁目录与 reclaim 目录)依然持有该凭证时保守跳过;规范路径不存在或已变更凭据时直接安全清理。
362
- */
363
- export function cleanStaleQuarantines(parentDir, basePrefix, maxAgeMs = 10000, deadline) {
364
- try {
365
- if (!existsSync(parentDir))
366
- return;
367
- const entries = readdirSync(parentDir);
368
- const now = Date.now();
369
- const canonicalPrimaryLockPath = basePrefix.endsWith(".reclaim")
370
- ? join(parentDir, basePrefix.slice(0, -".reclaim".length))
371
- : join(parentDir, basePrefix);
372
- const canonicalReclaimPath = `${canonicalPrimaryLockPath}.reclaim`;
373
- for (const entry of entries) {
374
- if (deadline !== undefined && Date.now() >= deadline) {
375
- break;
376
- }
377
- if (!entry.startsWith(basePrefix))
378
- continue;
379
- if (!entry.includes(".quarantine.") &&
380
- !entry.includes(".rollback.") &&
381
- !entry.includes(".release.") &&
382
- !entry.includes(".orphan.")) {
383
- continue;
384
- }
385
- const fullPath = join(parentDir, entry);
386
- try {
387
- const { operatorPid, timestamp } = parseQuarantineTimestamp(entry);
388
- // a. 共同前置条件:存活年龄必须超过 maxAgeMs(默认 10000ms),未超期前严禁清理(保持宽限期)
389
- let age;
390
- if (timestamp !== undefined) {
391
- age = now - timestamp;
392
- }
393
- else {
394
- const stat = statSync(fullPath);
395
- age = now - stat.mtimeMs;
396
- }
397
- if (age <= maxAgeMs) {
398
- continue;
399
- }
400
- const isOrphan = entry.includes(".orphan.");
401
- if (!isOrphan) {
402
- // b. 若不是 .orphan.(即活跃的 quarantine / rollback / release 目录):
403
- // - 若 operatorPid 存活,continue;
404
- if (operatorPid !== undefined && isProcessAlive(operatorPid)) {
405
- continue;
406
- }
407
- // - 若内部 metadata.json 记录的 pid 或 childPids 存活,continue;
408
- let isDir = false;
409
- try {
410
- isDir = statSync(fullPath).isDirectory();
411
- }
412
- catch {
413
- continue;
414
- }
415
- const metaPath = isDir ? join(fullPath, "metadata.json") : fullPath;
416
- if (existsSync(metaPath)) {
417
- try {
418
- const raw = readFileSync(metaPath, "utf8");
419
- if (raw.trim().length > 0) {
420
- const meta = JSON.parse(raw);
421
- if (typeof meta?.pid === "number" && isProcessAlive(meta.pid)) {
422
- continue;
423
- }
424
- if (Array.isArray(meta?.childPids) &&
425
- meta.childPids.some((childPid) => typeof childPid === "number" && isProcessAlive(childPid))) {
426
- continue;
427
- }
428
- }
429
- }
430
- catch {
431
- // 元数据损坏或不可读,不视为存在存活持有者
432
- }
433
- }
434
- rmSync(fullPath, { recursive: true, force: true });
435
- continue;
436
- }
437
- // c. 若是 .orphan. 目录(已脱离规范路径):
438
- // 严禁再用内部旧 metadata.pid 存活为由无限续命!
439
- // 提取 orphan 内部记录的 orphanToken(guardToken ?? lockToken)
440
- const orphanToken = readCanonicalToken(fullPath);
441
- // 核对当前 canonical 规范主路径(主锁目录与 reclaim 目录)的当前凭证:
442
- // 仅当当前规范路径依然持有该 orphanToken 时保守跳过;若规范路径不存在或已切换为其他 token,直接执行 rmSync 安全清理!
443
- const primaryToken = readCanonicalToken(canonicalPrimaryLockPath);
444
- const reclaimToken = readCanonicalToken(canonicalReclaimPath);
445
- if (orphanToken !== undefined &&
446
- (orphanToken === primaryToken || orphanToken === reclaimToken)) {
447
- continue;
448
- }
449
- rmSync(fullPath, { recursive: true, force: true });
450
- }
451
- catch { }
452
- }
453
- }
454
- catch { }
455
- }
456
- /**
457
- * 安全回滚当前进程创建的主锁。
458
- * 优先核对 lockToken,回退核对 sessionToken,防止误删接管者或并发新锁。
459
- */
460
- export function safeRollbackLock(lockPath, expectedSessionToken, expectedLockToken) {
461
- if (!existsSync(lockPath))
462
- return;
463
- const quarantinePath = `${lockPath}.rollback.${process.pid}.${Date.now()}.${randomUUID().slice(0, 8)}`;
464
- try {
465
- renameSync(lockPath, quarantinePath);
466
- }
467
- catch {
468
- return;
469
- }
470
- try {
471
- let isDir = false;
472
- try {
473
- isDir = statSync(quarantinePath).isDirectory();
474
- }
475
- catch {
476
- isDir = false;
477
- }
478
- const metaPath = isDir ? join(quarantinePath, "metadata.json") : quarantinePath;
479
- let actualSessionToken;
480
- let actualLockToken;
481
- if (existsSync(metaPath)) {
482
- try {
483
- const raw = readFileSync(metaPath, "utf8");
484
- const parsed = JSON.parse(raw);
485
- actualSessionToken = parsed?.sessionToken;
486
- actualLockToken = parsed?.lockToken;
487
- }
488
- catch { }
489
- }
490
- const isMatch = expectedLockToken
491
- ? (actualLockToken === expectedLockToken && actualSessionToken === expectedSessionToken)
492
- : (actualSessionToken === expectedSessionToken);
493
- if (!isMatch) {
494
- // 并非自身刚才创建的锁目录,立即恢复原位!
495
- try {
496
- renameSync(quarantinePath, lockPath);
497
- }
498
- catch {
499
- try {
500
- renameSync(quarantinePath, `${lockPath}.orphan.${Date.now()}.${randomUUID().slice(0, 8)}`);
501
- }
502
- catch { }
503
- }
504
- return;
505
- }
506
- rmSync(quarantinePath, { recursive: true, force: true });
507
- }
508
- catch {
509
- try {
510
- renameSync(quarantinePath, lockPath);
511
- }
512
- catch {
513
- try {
514
- renameSync(quarantinePath, `${lockPath}.orphan.${Date.now()}.${randomUUID().slice(0, 8)}`);
515
- }
516
- catch { }
517
- }
518
- }
519
- }
520
- /**
521
- * 安全隔离并清理陈旧主锁。
522
- * 隔离后复核元数据,确保仅删除目标陈旧锁,若已被更新或仍有存活所有者则恢复原位。
523
- */
524
- function safeQuarantineStaleLock(lockPath, expectedSessionToken, expectedLockToken) {
525
- if (!existsSync(lockPath))
526
- return true;
527
- const quarantinePath = `${lockPath}.quarantine.${process.pid}.${Date.now()}.${randomUUID().slice(0, 8)}`;
528
- try {
529
- renameSync(lockPath, quarantinePath);
530
- }
531
- catch {
532
- return false;
533
- }
534
- try {
535
- let isDir = false;
536
- try {
537
- isDir = statSync(quarantinePath).isDirectory();
538
- }
539
- catch {
540
- isDir = false;
541
- }
542
- const metaPath = isDir ? join(quarantinePath, "metadata.json") : quarantinePath;
543
- let actualSessionToken;
544
- let actualLockToken;
545
- let actualPid;
546
- if (existsSync(metaPath)) {
547
- try {
548
- const raw = readFileSync(metaPath, "utf8");
549
- const parsed = JSON.parse(raw);
550
- actualSessionToken = parsed?.sessionToken;
551
- actualLockToken = parsed?.lockToken;
552
- actualPid = parsed?.pid;
553
- }
554
- catch { }
555
- }
556
- const tokenMismatch = expectedLockToken
557
- ? (actualLockToken !== expectedLockToken || (expectedSessionToken && actualSessionToken !== expectedSessionToken))
558
- : ((expectedSessionToken && actualSessionToken !== expectedSessionToken) || (!expectedSessionToken && actualSessionToken));
559
- if (tokenMismatch) {
560
- // 锁已被其他竞争者接管并写入新 token,绝不可删除!立即恢复原位
561
- try {
562
- renameSync(quarantinePath, lockPath);
563
- }
564
- catch {
565
- try {
566
- renameSync(quarantinePath, `${lockPath}.orphan.${Date.now()}.${randomUUID().slice(0, 8)}`);
567
- }
568
- catch { }
569
- }
570
- return false;
571
- }
572
- if (typeof actualPid === "number" && isProcessAlive(actualPid)) {
573
- // 持有者实际仍存活,恢复原位
574
- try {
575
- renameSync(quarantinePath, lockPath);
576
- }
577
- catch {
578
- try {
579
- renameSync(quarantinePath, `${lockPath}.orphan.${Date.now()}.${randomUUID().slice(0, 8)}`);
580
- }
581
- catch { }
582
- }
583
- return false;
584
- }
585
- // 确认正是目标陈旧锁,安全删除
586
- rmSync(quarantinePath, { recursive: true, force: true });
587
- return true;
588
- }
589
- catch {
590
- try {
591
- renameSync(quarantinePath, lockPath);
592
- }
593
- catch {
594
- try {
595
- renameSync(quarantinePath, `${lockPath}.orphan.${Date.now()}.${randomUUID().slice(0, 8)}`);
596
- }
597
- catch { }
598
- }
599
- return false;
600
- }
601
- }
5
+ import { acquireDirectoryLock, cleanStaleQuarantines, isProcessAlive, parseQuarantineTimestamp, safeReleaseLock as coreSafeReleaseLock, safeRemoveStaleReclaimGuard, safeRollbackLock, } from "./lock-core.js";
6
+ const LOCK_DIR_NAME = ".actiondock.data.lock";
7
+ export { isProcessAlive, parseQuarantineTimestamp, cleanStaleQuarantines, safeRemoveStaleReclaimGuard, safeRollbackLock, };
602
8
  /**
603
9
  * 安全释放主锁。
604
10
  * 仅当锁目录中持有当前 lockToken / sessionToken 时才删除,杜绝删除他人新锁。
605
11
  */
606
12
  export function safeReleaseLock(lockPath, expectedSessionToken, expectedLockToken) {
607
- if (!existsSync(lockPath))
608
- return;
609
- const quarantinePath = `${lockPath}.release.${process.pid}.${Date.now()}.${randomUUID().slice(0, 8)}`;
610
- try {
611
- renameSync(lockPath, quarantinePath);
612
- }
613
- catch {
614
- return;
615
- }
616
- try {
617
- let isDir = false;
618
- try {
619
- isDir = statSync(quarantinePath).isDirectory();
620
- }
621
- catch {
622
- isDir = false;
623
- }
624
- const metaPath = isDir ? join(quarantinePath, "metadata.json") : quarantinePath;
625
- let actualSessionToken;
626
- let actualLockToken;
627
- if (existsSync(metaPath)) {
628
- try {
629
- const raw = readFileSync(metaPath, "utf8");
630
- const parsed = JSON.parse(raw);
631
- actualSessionToken = parsed?.sessionToken;
632
- actualLockToken = parsed?.lockToken;
633
- }
634
- catch { }
635
- }
636
- const isMatch = expectedLockToken
637
- ? (actualLockToken === expectedLockToken && actualSessionToken === expectedSessionToken)
638
- : (actualSessionToken === expectedSessionToken);
639
- if (isMatch) {
640
- rmSync(quarantinePath, { recursive: true, force: true });
641
- }
642
- else {
643
- // 并非当前会话持有的锁,恢复原位
644
- try {
645
- renameSync(quarantinePath, lockPath);
646
- }
647
- catch {
648
- try {
649
- renameSync(quarantinePath, `${lockPath}.orphan.${Date.now()}.${randomUUID().slice(0, 8)}`);
650
- }
651
- catch { }
652
- }
653
- }
654
- }
655
- catch {
656
- try {
657
- renameSync(quarantinePath, lockPath);
658
- }
659
- catch {
660
- try {
661
- renameSync(quarantinePath, `${lockPath}.orphan.${Date.now()}.${randomUUID().slice(0, 8)}`);
662
- }
663
- catch { }
664
- }
665
- }
13
+ coreSafeReleaseLock(lockPath, expectedSessionToken, expectedLockToken);
666
14
  }
667
15
  /**
668
16
  * 数据目录排他文件锁管理器。
@@ -752,13 +100,13 @@ export class DataDirLock {
752
100
  if (this.released)
753
101
  return;
754
102
  this.released = true;
755
- safeReleaseLock(this.lockDirPath, this.info.sessionToken, this.info.lockToken);
756
- cleanStaleQuarantines(dirname(this.lockDirPath), ".actiondock.data.lock");
103
+ coreSafeReleaseLock(this.lockDirPath, this.info.sessionToken, this.info.lockToken);
104
+ cleanStaleQuarantines(dirname(this.lockDirPath), LOCK_DIR_NAME);
757
105
  }
758
106
  /**
759
107
  * 尝试获取指定数据目录的排他锁。
760
108
  *
761
- * 仲裁规则:
109
+ * 仲裁规则由目录锁内核(lock-core)统一承载:
762
110
  * - 引入所有竞争者均遵守的原子 reclaim guard 机制(lockDirPath.reclaim),每个 guard 具备全局唯一 guardToken。
763
111
  * - 基于原子目录创建 mkdirSync(lockDirPath, { mode: 0o700 }) 确立所有权,并在其下存放 metadata.json。
764
112
  * - 兼容已有常规文件锁(如老版本或测试 mock 场景)。
@@ -776,20 +124,15 @@ export class DataDirLock {
776
124
  */
777
125
  static acquire(dataDir, options = {}) {
778
126
  const timeoutMs = options.acquireTimeoutMs ?? 5000;
779
- const deadline = Date.now() + timeoutMs;
780
127
  if (!existsSync(dataDir)) {
781
128
  mkdirSync(dataDir, { recursive: true, mode: 0o700 });
782
129
  }
783
- cleanStaleQuarantines(dataDir, ".actiondock.data.lock", 10000, deadline);
784
- const lockDirPath = join(dataDir, ".actiondock.data.lock");
785
- const reclaimDirPath = `${lockDirPath}.reclaim`;
786
- const currentPid = process.pid;
787
- const currentHost = hostname();
130
+ const lockDirPath = join(dataDir, LOCK_DIR_NAME);
788
131
  const token = options.sessionToken || randomUUID();
789
132
  const lockToken = randomUUID();
790
133
  const newLockInfo = {
791
- pid: currentPid,
792
- hostname: currentHost,
134
+ pid: process.pid,
135
+ hostname: hostname(),
793
136
  sessionToken: token,
794
137
  lockToken,
795
138
  createdAt: new Date().toISOString(),
@@ -797,220 +140,44 @@ export class DataDirLock {
797
140
  hostSessionId: options.hostSessionId,
798
141
  };
799
142
  const content = JSON.stringify(newLockInfo, null, 2);
800
- while (true) {
801
- // 1. 当竞争者尝试创建新主锁时,若检测到 reclaim 目录存在且处于宽限期内或持有者存活,必须等待,禁止在他人正在接管/验证期间抢先创建主锁
802
- const reclaimState = checkReclaimGuard(reclaimDirPath, isProcessAlive, 1000);
803
- if (reclaimState.active) {
804
- if (Date.now() >= deadline) {
805
- const timeoutErr = new Error(`DATA_DIR_IN_USE: Timeout waiting for active reclaim guard (PID ${reclaimState.holderPid ?? "unknown"}) on data directory '${dataDir}' after ${timeoutMs}ms`);
806
- timeoutErr.code = "DATA_DIR_IN_USE";
807
- throw timeoutErr;
808
- }
809
- sleepSync(Math.min(50, Math.max(1, deadline - Date.now())));
810
- continue;
811
- }
812
- if (reclaimState.isStale && reclaimState.guardToken) {
813
- // 若 reclaim guard 持有者意外崩溃,其他竞争者核对此前检查的 guardToken 安全清理
814
- safeRemoveStaleReclaimGuard(reclaimDirPath, reclaimState.guardToken);
815
- }
816
- // 2. 尝试常规获取新主锁
817
- try {
818
- mkdirSync(lockDirPath, { mode: 0o700 });
819
- const metaPath = join(lockDirPath, "metadata.json");
820
- const tmpPath = join(lockDirPath, `metadata.json.tmp.${currentPid}.${randomUUID().slice(0, 8)}`);
821
- writeFileSync(tmpPath, content, { mode: 0o600 });
822
- renameSync(tmpPath, metaPath);
823
- // 再次确认在此窗口期内是否有他人持有活跃 reclaim guard
824
- const postCheck = checkReclaimGuard(reclaimDirPath, isProcessAlive, 1000);
825
- if (postCheck.active && postCheck.holderPid !== currentPid) {
826
- // 仅当锁目录中包含自身创建的 sessionToken 与 lockToken 时安全回滚,杜绝误删他人新锁
827
- safeRollbackLock(lockDirPath, newLockInfo.sessionToken, newLockInfo.lockToken);
828
- if (Date.now() >= deadline) {
829
- const timeoutErr = new Error(`DATA_DIR_IN_USE: Timeout acquiring lock on data directory '${dataDir}' due to active reclaim guard (PID ${postCheck.holderPid}) after ${timeoutMs}ms`);
830
- timeoutErr.code = "DATA_DIR_IN_USE";
831
- throw timeoutErr;
832
- }
833
- sleepSync(Math.min(50, Math.max(1, deadline - Date.now())));
834
- continue;
143
+ return acquireDirectoryLock({
144
+ lockPath: lockDirPath,
145
+ parentDir: dataDir,
146
+ basePrefix: LOCK_DIR_NAME,
147
+ metadataContent: content,
148
+ sessionToken: token,
149
+ lockToken,
150
+ acquireTimeoutMs: options.acquireTimeoutMs,
151
+ createLockError(message) {
152
+ const timeoutErr = new Error(message);
153
+ timeoutErr.code = "DATA_DIR_IN_USE";
154
+ return timeoutErr;
155
+ },
156
+ messages: {
157
+ timeoutWaitingReclaimGuard: (holderPid) => `DATA_DIR_IN_USE: Timeout waiting for active reclaim guard (PID ${holderPid ?? "unknown"}) on data directory '${dataDir}' after ${timeoutMs}ms`,
158
+ timeoutAcquireBlockedByGuard: (holderPid) => `DATA_DIR_IN_USE: Timeout acquiring lock on data directory '${dataDir}' due to active reclaim guard (PID ${holderPid}) after ${timeoutMs}ms`,
159
+ timeoutAcquire: () => `DATA_DIR_IN_USE: Timeout acquiring lock on data directory '${dataDir}' after ${timeoutMs}ms`,
160
+ timeoutGracePeriod: () => `DATA_DIR_IN_USE: Timeout waiting for lock grace period on data directory '${dataDir}' after ${timeoutMs}ms`,
161
+ timeoutGuardContention: () => `DATA_DIR_IN_USE: Timeout contending for reclaim guard on data directory '${dataDir}' after ${timeoutMs}ms`,
162
+ timeoutReclaimStale: () => `DATA_DIR_IN_USE: Timeout reclaiming stale lock on data directory '${dataDir}' after ${timeoutMs}ms`,
163
+ timeoutCreate: () => `DATA_DIR_IN_USE: Timeout creating primary lock on data directory '${dataDir}' after ${timeoutMs}ms`,
164
+ },
165
+ assertStaleHolderReclaimable(info) {
166
+ const holderPid = info.pid;
167
+ if (isProcessAlive(holderPid)) {
168
+ const inUseErr = new Error(`DATA_DIR_IN_USE: Data directory '${dataDir}' is in use by another active Host process (PID ${holderPid})`);
169
+ inUseErr.code = "DATA_DIR_IN_USE";
170
+ throw inUseErr;
835
171
  }
836
- return new DataDirLock(lockDirPath, newLockInfo);
837
- }
838
- catch (err) {
839
- if (err && (err.code === "EEXIST" || err.code === "ENOENT")) {
840
- const lockState = readLockWithGracePeriod(lockDirPath, 3000, deadline);
841
- if (!lockState.exists) {
842
- if (Date.now() >= deadline) {
843
- const timeoutErr = new Error(`DATA_DIR_IN_USE: Timeout acquiring lock on data directory '${dataDir}' after ${timeoutMs}ms`);
844
- timeoutErr.code = "DATA_DIR_IN_USE";
845
- throw timeoutErr;
846
- }
847
- continue;
848
- }
849
- if (lockState.inGracePeriod) {
850
- if (Date.now() >= deadline) {
851
- const timeoutErr = new Error(`DATA_DIR_IN_USE: Timeout waiting for lock grace period on data directory '${dataDir}' after ${timeoutMs}ms`);
852
- timeoutErr.code = "DATA_DIR_IN_USE";
853
- throw timeoutErr;
854
- }
855
- sleepSync(Math.min(50, Math.max(1, deadline - Date.now())));
856
- continue;
857
- }
858
- if (lockState.info && typeof lockState.info.pid === "number") {
859
- const parentAlive = isProcessAlive(lockState.info.pid);
860
- if (parentAlive) {
861
- const inUseErr = new Error(`DATA_DIR_IN_USE: Data directory '${dataDir}' is in use by another active Host process (PID ${lockState.info.pid})`);
862
- inUseErr.code = "DATA_DIR_IN_USE";
863
- throw inUseErr;
864
- }
865
- // 主进程已死亡,检查关联子进程存活状态
866
- const activeChildren = (lockState.info.childPids || []).filter((childPid) => isProcessAlive(childPid));
867
- if (activeChildren.length > 0) {
868
- const recoveryErr = new Error(`DATA_DIR_RECOVERY_REQUIRED: Data directory recovery required for '${dataDir}': previous host (PID ${lockState.info.pid}) exited but child processes (${activeChildren.join(", ")}) are still running`);
869
- recoveryErr.code = "DATA_DIR_RECOVERY_REQUIRED";
870
- throw recoveryErr;
871
- }
872
- }
873
- const staleSessionToken = lockState.info?.sessionToken;
874
- const staleLockToken = lockState.info?.lockToken;
875
- const guardToken = randomUUID();
876
- // 当识别到主锁为陈旧锁时,竞争者必须先原子竞争获取 reclaim guard
877
- const acquiredReclaim = tryAcquireReclaimGuard(reclaimDirPath, currentPid, guardToken);
878
- if (!acquiredReclaim) {
879
- const currentReclaim = checkReclaimGuard(reclaimDirPath, isProcessAlive, 1000);
880
- if (currentReclaim.isStale && currentReclaim.guardToken) {
881
- safeRemoveStaleReclaimGuard(reclaimDirPath, currentReclaim.guardToken);
882
- }
883
- if (Date.now() >= deadline) {
884
- const timeoutErr = new Error(`DATA_DIR_IN_USE: Timeout contending for reclaim guard on data directory '${dataDir}' after ${timeoutMs}ms`);
885
- timeoutErr.code = "DATA_DIR_IN_USE";
886
- throw timeoutErr;
887
- }
888
- sleepSync(Math.min(50, Math.max(1, deadline - Date.now())));
889
- continue;
890
- }
891
- // 仅成功获取 reclaim guard 的唯一胜利者获准执行:
892
- // 复核主锁陈旧性 -> 隔离/清理陈旧锁 -> 原子创建新主锁并写入自身元数据 -> 清理 reclaim guard
893
- try {
894
- const verifyReclaimOwnership = () => {
895
- const ownGuard = checkReclaimGuard(reclaimDirPath, isProcessAlive, 1000);
896
- return Boolean(ownGuard.active &&
897
- ownGuard.holderPid === currentPid &&
898
- ownGuard.guardToken === guardToken);
899
- };
900
- // 自检校验自身守卫:验证自身 guardToken 依然有效且持有者为自身 PID
901
- // 若已被抢占或不匹配,立即退出当前接管并 continue 重试,杜绝在守卫已失窃的情况下操作主锁
902
- if (!verifyReclaimOwnership()) {
903
- if (Date.now() >= deadline) {
904
- const timeoutErr = new Error(`DATA_DIR_IN_USE: Timeout acquiring lock on data directory '${dataDir}' after ${timeoutMs}ms`);
905
- timeoutErr.code = "DATA_DIR_IN_USE";
906
- throw timeoutErr;
907
- }
908
- sleepSync(Math.min(50, Math.max(1, deadline - Date.now())));
909
- continue;
910
- }
911
- // 1. 复核主锁陈旧性
912
- const recheckState = readLockWithGracePeriod(lockDirPath, 1000, deadline);
913
- if (recheckState.exists) {
914
- if (recheckState.inGracePeriod) {
915
- if (Date.now() >= deadline) {
916
- const timeoutErr = new Error(`DATA_DIR_IN_USE: Timeout waiting for lock grace period on data directory '${dataDir}' after ${timeoutMs}ms`);
917
- timeoutErr.code = "DATA_DIR_IN_USE";
918
- throw timeoutErr;
919
- }
920
- sleepSync(Math.min(50, Math.max(1, deadline - Date.now())));
921
- continue;
922
- }
923
- if (recheckState.info &&
924
- typeof recheckState.info.pid === "number" &&
925
- isProcessAlive(recheckState.info.pid)) {
926
- const inUseErr = new Error(`DATA_DIR_IN_USE: Data directory '${dataDir}' is in use by another active Host process (PID ${recheckState.info.pid})`);
927
- inUseErr.code = "DATA_DIR_IN_USE";
928
- throw inUseErr;
929
- }
930
- const activeChildren = (recheckState.info?.childPids || []).filter((childPid) => isProcessAlive(childPid));
931
- if (activeChildren.length > 0) {
932
- const recoveryErr = new Error(`DATA_DIR_RECOVERY_REQUIRED: Data directory recovery required for '${dataDir}': previous host (PID ${recheckState.info?.pid}) exited but child processes (${activeChildren.join(", ")}) are still running`);
933
- recoveryErr.code = "DATA_DIR_RECOVERY_REQUIRED";
934
- throw recoveryErr;
935
- }
936
- // 2. 隔离并核验清理陈旧主锁(验证 sessionToken 一致,若已被他人占用则恢复原位)
937
- const cleaned = safeQuarantineStaleLock(lockDirPath, recheckState.info?.sessionToken ?? staleSessionToken, recheckState.info?.lockToken ?? staleLockToken);
938
- if (!cleaned) {
939
- if (Date.now() >= deadline) {
940
- const timeoutErr = new Error(`DATA_DIR_IN_USE: Timeout reclaiming stale lock on data directory '${dataDir}' after ${timeoutMs}ms`);
941
- timeoutErr.code = "DATA_DIR_IN_USE";
942
- throw timeoutErr;
943
- }
944
- continue;
945
- }
946
- }
947
- // 后置复核 1:在清理陈旧主锁之后、创建新主锁之前,再次核验 reclaim guard 所有权
948
- // 若守卫在此期间失窃,严禁继续创建新主锁,立即退出重试
949
- if (!verifyReclaimOwnership()) {
950
- if (Date.now() >= deadline) {
951
- const timeoutErr = new Error(`DATA_DIR_IN_USE: Timeout acquiring lock on data directory '${dataDir}' after ${timeoutMs}ms`);
952
- timeoutErr.code = "DATA_DIR_IN_USE";
953
- throw timeoutErr;
954
- }
955
- sleepSync(Math.min(50, Math.max(1, deadline - Date.now())));
956
- continue;
957
- }
958
- // 3. 原子创建新主锁并写入自身元数据(严格遵守 deadline,并在每次重试检查时间窗口)
959
- let created = false;
960
- for (let attempt = 0; attempt < 40; attempt++) {
961
- try {
962
- mkdirSync(lockDirPath, { mode: 0o700 });
963
- created = true;
964
- break;
965
- }
966
- catch (createErr) {
967
- if (createErr?.code === "EEXIST") {
968
- if (Date.now() >= deadline) {
969
- break;
970
- }
971
- const remaining = deadline - Date.now();
972
- if (remaining <= 0) {
973
- break;
974
- }
975
- sleepSync(Math.min(25, remaining));
976
- continue;
977
- }
978
- throw createErr;
979
- }
980
- }
981
- if (!created) {
982
- if (Date.now() >= deadline) {
983
- const timeoutErr = new Error(`DATA_DIR_IN_USE: Timeout creating primary lock on data directory '${dataDir}' after ${timeoutMs}ms`);
984
- timeoutErr.code = "DATA_DIR_IN_USE";
985
- throw timeoutErr;
986
- }
987
- continue;
988
- }
989
- const metaPath = join(lockDirPath, "metadata.json");
990
- const tmpPath = join(lockDirPath, `metadata.json.tmp.${currentPid}.${randomUUID().slice(0, 8)}`);
991
- writeFileSync(tmpPath, content, { mode: 0o600 });
992
- renameSync(tmpPath, metaPath);
993
- // 后置复核 2:新主锁创建完成后、返回之前再次核验 reclaim guard 所有权
994
- // 若守卫在创建新主锁期间失窃,说明存在并发仲裁漂移,严禁生效并安全回滚自身主锁
995
- if (!verifyReclaimOwnership()) {
996
- safeRollbackLock(lockDirPath, newLockInfo.sessionToken, newLockInfo.lockToken);
997
- if (Date.now() >= deadline) {
998
- const timeoutErr = new Error(`DATA_DIR_IN_USE: Timeout acquiring lock on data directory '${dataDir}' after ${timeoutMs}ms`);
999
- timeoutErr.code = "DATA_DIR_IN_USE";
1000
- throw timeoutErr;
1001
- }
1002
- sleepSync(Math.min(50, Math.max(1, deadline - Date.now())));
1003
- continue;
1004
- }
1005
- return new DataDirLock(lockDirPath, newLockInfo);
1006
- }
1007
- finally {
1008
- // 4. 清理自身持有的 reclaim guard(必须核对自身 guardToken)
1009
- safeRemoveStaleReclaimGuard(reclaimDirPath, guardToken);
1010
- }
172
+ // 主进程已死亡,检查关联子进程存活状态
173
+ const activeChildren = (info.childPids || []).filter((childPid) => isProcessAlive(childPid));
174
+ if (activeChildren.length > 0) {
175
+ const recoveryErr = new Error(`DATA_DIR_RECOVERY_REQUIRED: Data directory recovery required for '${dataDir}': previous host (PID ${holderPid}) exited but child processes (${activeChildren.join(", ")}) are still running`);
176
+ recoveryErr.code = "DATA_DIR_RECOVERY_REQUIRED";
177
+ throw recoveryErr;
1011
178
  }
1012
- throw err;
1013
- }
1014
- }
179
+ },
180
+ onAcquired: () => new DataDirLock(lockDirPath, newLockInfo),
181
+ });
1015
182
  }
1016
183
  }