@actiondock/core 2.0.11 → 2.2.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 (258) hide show
  1. package/README.md +61 -30
  2. package/dist/app/app.d.ts +73 -0
  3. package/dist/app/app.js +700 -0
  4. package/dist/app/index.d.ts +2 -0
  5. package/dist/app/index.js +2 -0
  6. package/dist/app/types.d.ts +283 -0
  7. package/dist/app/types.js +1 -0
  8. package/dist/catalog/action-index.d.ts +9 -0
  9. package/dist/catalog/action-index.js +93 -0
  10. package/dist/catalog/action-resolver.d.ts +20 -0
  11. package/dist/catalog/action-resolver.js +95 -0
  12. package/dist/catalog/index.d.ts +5 -0
  13. package/dist/catalog/index.js +5 -0
  14. package/dist/catalog/location-registry.d.ts +15 -0
  15. package/dist/catalog/location-registry.js +138 -0
  16. package/dist/catalog/package-catalog.d.ts +7 -0
  17. package/dist/catalog/package-catalog.js +76 -0
  18. package/dist/catalog/types.d.ts +68 -0
  19. package/dist/catalog/types.js +1 -0
  20. package/dist/doctor/doctor.d.ts +7 -0
  21. package/dist/doctor/doctor.js +613 -0
  22. package/dist/doctor/index.d.ts +2 -0
  23. package/dist/doctor/index.js +2 -0
  24. package/dist/doctor/types.d.ts +23 -0
  25. package/dist/doctor/types.js +1 -0
  26. package/dist/errors.d.ts +122 -0
  27. package/dist/errors.js +123 -0
  28. package/dist/execution/index.d.ts +2 -0
  29. package/dist/execution/index.js +2 -0
  30. package/dist/execution/service.d.ts +79 -0
  31. package/dist/execution/service.js +633 -0
  32. package/dist/execution/types.d.ts +131 -0
  33. package/dist/execution/types.js +1 -0
  34. package/dist/export/index.d.ts +1 -0
  35. package/dist/export/index.js +1 -0
  36. package/dist/export/templates.d.ts +71 -0
  37. package/dist/export/templates.js +621 -0
  38. package/dist/filter/index.d.ts +1 -0
  39. package/dist/filter/index.js +1 -0
  40. package/dist/filter/intent.d.ts +33 -0
  41. package/dist/filter/intent.js +123 -0
  42. package/dist/host/host.d.ts +55 -0
  43. package/dist/host/host.js +740 -0
  44. package/dist/host/index.d.ts +2 -0
  45. package/dist/host/index.js +2 -0
  46. package/dist/host/types.d.ts +81 -0
  47. package/dist/host/types.js +1 -0
  48. package/dist/index.d.ts +21 -0
  49. package/dist/index.js +20 -0
  50. package/dist/ipc/diagnostic.d.ts +65 -0
  51. package/dist/ipc/diagnostic.js +129 -0
  52. package/dist/ipc/host.d.ts +7 -0
  53. package/dist/ipc/host.js +93 -0
  54. package/dist/ipc/index.d.ts +4 -0
  55. package/dist/ipc/index.js +4 -0
  56. package/dist/ipc/target.d.ts +56 -0
  57. package/dist/ipc/target.js +297 -0
  58. package/dist/ipc/types.d.ts +42 -0
  59. package/dist/ipc/types.js +1 -0
  60. package/dist/platform/default.d.ts +32 -0
  61. package/dist/platform/default.js +40 -0
  62. package/dist/platform/index.d.ts +3 -0
  63. package/dist/platform/index.js +3 -0
  64. package/dist/platform/node-fs.d.ts +37 -0
  65. package/dist/platform/node-fs.js +81 -0
  66. package/dist/platform/types.d.ts +104 -0
  67. package/dist/platform/types.js +1 -0
  68. package/dist/profile/client.d.ts +162 -0
  69. package/dist/profile/client.js +467 -0
  70. package/dist/profile/index.d.ts +3 -0
  71. package/dist/profile/index.js +3 -0
  72. package/dist/profile/manager.d.ts +47 -0
  73. package/dist/profile/manager.js +294 -0
  74. package/dist/profile/types.d.ts +61 -0
  75. package/dist/profile/types.js +1 -0
  76. package/dist/project/closure.d.ts +25 -0
  77. package/dist/project/closure.js +68 -0
  78. package/dist/project/digest.d.ts +29 -0
  79. package/dist/project/digest.js +324 -0
  80. package/dist/project/index.d.ts +10 -0
  81. package/dist/project/index.js +10 -0
  82. package/dist/project/init.d.ts +26 -0
  83. package/dist/project/init.js +175 -0
  84. package/dist/project/loader.d.ts +99 -0
  85. package/dist/project/loader.js +484 -0
  86. package/dist/project/lockfile.d.ts +55 -0
  87. package/dist/project/lockfile.js +175 -0
  88. package/dist/project/manifest.d.ts +22 -0
  89. package/dist/project/manifest.js +220 -0
  90. package/dist/project/resolver.d.ts +116 -0
  91. package/dist/project/resolver.js +376 -0
  92. package/dist/project/transactions.d.ts +103 -0
  93. package/dist/project/transactions.js +1203 -0
  94. package/dist/project/types-generator.d.ts +33 -0
  95. package/dist/project/types-generator.js +168 -0
  96. package/dist/project/types.d.ts +107 -0
  97. package/dist/project/types.js +1 -0
  98. package/dist/registry/index.d.ts +5 -0
  99. package/dist/registry/index.js +5 -0
  100. package/dist/registry/lock.d.ts +27 -0
  101. package/dist/registry/lock.js +93 -0
  102. package/dist/registry/registry.d.ts +45 -0
  103. package/dist/registry/registry.js +611 -0
  104. package/dist/registry/resolve.d.ts +62 -0
  105. package/dist/registry/resolve.js +195 -0
  106. package/dist/registry/scan.d.ts +38 -0
  107. package/dist/registry/scan.js +135 -0
  108. package/dist/registry/types.d.ts +117 -0
  109. package/dist/registry/types.js +1 -0
  110. package/dist/runtime/action-collection.d.ts +27 -0
  111. package/dist/runtime/action-collection.js +57 -0
  112. package/dist/runtime/clock.d.ts +19 -0
  113. package/dist/runtime/clock.js +17 -0
  114. package/dist/runtime/context.d.ts +86 -0
  115. package/dist/runtime/context.js +242 -0
  116. package/dist/runtime/env.d.ts +56 -0
  117. package/dist/runtime/env.js +159 -0
  118. package/dist/runtime/events.d.ts +93 -0
  119. package/dist/runtime/events.js +604 -0
  120. package/dist/runtime/execution-manager.d.ts +48 -0
  121. package/dist/runtime/execution-manager.js +66 -0
  122. package/dist/runtime/index.d.ts +10 -0
  123. package/dist/runtime/index.js +10 -0
  124. package/dist/runtime/module-loader.d.ts +34 -0
  125. package/dist/runtime/module-loader.js +16 -0
  126. package/dist/runtime/process.d.ts +9 -0
  127. package/dist/runtime/process.js +146 -0
  128. package/dist/runtime/runner.d.ts +293 -0
  129. package/dist/runtime/runner.js +984 -0
  130. package/dist/runtime/standalone.d.ts +89 -0
  131. package/dist/runtime/standalone.js +536 -0
  132. package/dist/schema/validator.d.ts +28 -0
  133. package/dist/schema/validator.js +116 -0
  134. package/dist/server/body.d.ts +40 -0
  135. package/dist/server/body.js +93 -0
  136. package/dist/server/index.d.ts +5 -0
  137. package/dist/server/index.js +5 -0
  138. package/dist/server/routes/actions.d.ts +5 -0
  139. package/dist/server/routes/actions.js +298 -0
  140. package/dist/server/routes/common.d.ts +47 -0
  141. package/dist/server/routes/common.js +104 -0
  142. package/dist/server/routes/config.d.ts +6 -0
  143. package/dist/server/routes/config.js +173 -0
  144. package/dist/server/routes/doctor.d.ts +5 -0
  145. package/dist/server/routes/doctor.js +63 -0
  146. package/dist/server/routes/health.d.ts +5 -0
  147. package/dist/server/routes/health.js +35 -0
  148. package/dist/server/routes/index.d.ts +9 -0
  149. package/dist/server/routes/index.js +9 -0
  150. package/dist/server/routes/info.d.ts +7 -0
  151. package/dist/server/routes/info.js +158 -0
  152. package/dist/server/routes/playbooks.d.ts +5 -0
  153. package/dist/server/routes/playbooks.js +134 -0
  154. package/dist/server/routes/runs.d.ts +5 -0
  155. package/dist/server/routes/runs.js +343 -0
  156. package/dist/server/routes/state.d.ts +6 -0
  157. package/dist/server/routes/state.js +169 -0
  158. package/dist/server/security.d.ts +44 -0
  159. package/dist/server/security.js +101 -0
  160. package/dist/server/server.d.ts +15 -0
  161. package/dist/server/server.js +355 -0
  162. package/dist/server/types.d.ts +78 -0
  163. package/dist/server/types.js +1 -0
  164. package/dist/storage/data-dir-lock.d.ts +119 -0
  165. package/dist/storage/data-dir-lock.js +1016 -0
  166. package/dist/storage/driver.d.ts +9 -0
  167. package/dist/storage/driver.js +59 -0
  168. package/dist/storage/index.d.ts +64 -0
  169. package/dist/storage/index.js +98 -0
  170. package/dist/storage/lazy.d.ts +8 -0
  171. package/dist/storage/lazy.js +44 -0
  172. package/dist/storage/mask.d.ts +26 -0
  173. package/dist/storage/mask.js +48 -0
  174. package/dist/storage/params.d.ts +13 -0
  175. package/dist/storage/params.js +32 -0
  176. package/dist/storage/sqlite.d.ts +79 -0
  177. package/dist/storage/sqlite.js +689 -0
  178. package/dist/storage/types.d.ts +172 -0
  179. package/dist/storage/types.js +14 -0
  180. package/dist/target/index.d.ts +4 -0
  181. package/dist/target/index.js +4 -0
  182. package/dist/target/local.d.ts +52 -0
  183. package/dist/target/local.js +343 -0
  184. package/dist/target/remote.d.ts +76 -0
  185. package/dist/target/remote.js +839 -0
  186. package/dist/target/target.d.ts +6 -0
  187. package/dist/target/target.js +35 -0
  188. package/dist/target/types.d.ts +246 -0
  189. package/dist/target/types.js +34 -0
  190. package/dist/utils/index.d.ts +49 -0
  191. package/dist/utils/index.js +180 -0
  192. package/dist/version.d.ts +4 -0
  193. package/dist/version.js +4 -0
  194. package/package.json +13 -13
  195. package/src/build/index.ts +0 -1
  196. package/src/build/templates.ts +0 -59
  197. package/src/catalog/action-index.ts +0 -92
  198. package/src/catalog/action-resolver.ts +0 -120
  199. package/src/catalog/index.ts +0 -5
  200. package/src/catalog/location-registry.ts +0 -134
  201. package/src/catalog/package-catalog.ts +0 -87
  202. package/src/catalog/types.ts +0 -75
  203. package/src/doctor/doctor.ts +0 -419
  204. package/src/doctor/index.ts +0 -2
  205. package/src/doctor/types.ts +0 -25
  206. package/src/execution/index.ts +0 -2
  207. package/src/execution/service.ts +0 -322
  208. package/src/execution/types.ts +0 -73
  209. package/src/export/index.ts +0 -1
  210. package/src/export/templates.ts +0 -590
  211. package/src/filter/index.ts +0 -1
  212. package/src/filter/intent.ts +0 -154
  213. package/src/index.ts +0 -16
  214. package/src/profile/client.ts +0 -552
  215. package/src/profile/index.ts +0 -3
  216. package/src/profile/manager.ts +0 -341
  217. package/src/profile/types.ts +0 -71
  218. package/src/project/index.ts +0 -4
  219. package/src/project/init.ts +0 -242
  220. package/src/project/loader.ts +0 -479
  221. package/src/project/manifest.ts +0 -299
  222. package/src/project/types.ts +0 -141
  223. package/src/registry/index.ts +0 -2
  224. package/src/registry/registry.ts +0 -940
  225. package/src/registry/types.ts +0 -127
  226. package/src/runtime/clock.ts +0 -41
  227. package/src/runtime/context.ts +0 -259
  228. package/src/runtime/env.ts +0 -172
  229. package/src/runtime/events.ts +0 -142
  230. package/src/runtime/execution-manager.ts +0 -74
  231. package/src/runtime/index.ts +0 -10
  232. package/src/runtime/module-loader.ts +0 -67
  233. package/src/runtime/process.ts +0 -245
  234. package/src/runtime/runner.ts +0 -527
  235. package/src/runtime/standalone.ts +0 -429
  236. package/src/schema/validator.ts +0 -61
  237. package/src/server/body.ts +0 -112
  238. package/src/server/index.ts +0 -7
  239. package/src/server/routes/actions.ts +0 -262
  240. package/src/server/routes/common.ts +0 -126
  241. package/src/server/routes/config.ts +0 -147
  242. package/src/server/routes/doctor.ts +0 -29
  243. package/src/server/routes/health.ts +0 -40
  244. package/src/server/routes/index.ts +0 -9
  245. package/src/server/routes/info.ts +0 -221
  246. package/src/server/routes/playbooks.ts +0 -109
  247. package/src/server/routes/runs.ts +0 -301
  248. package/src/server/routes/state.ts +0 -133
  249. package/src/server/runtime-registry.ts +0 -115
  250. package/src/server/security.ts +0 -115
  251. package/src/server/server.ts +0 -251
  252. package/src/server/types.ts +0 -57
  253. package/src/storage/driver.ts +0 -126
  254. package/src/storage/index.ts +0 -81
  255. package/src/storage/mask.ts +0 -34
  256. package/src/storage/sqlite.ts +0 -714
  257. package/src/storage/types.ts +0 -139
  258. package/src/utils/index.ts +0 -62
@@ -0,0 +1,1016 @@
1
+ import fs, { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { hostname } from "node:os";
3
+ import { dirname, join } from "node:path";
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
+ }
602
+ /**
603
+ * 安全释放主锁。
604
+ * 仅当锁目录中持有当前 lockToken / sessionToken 时才删除,杜绝删除他人新锁。
605
+ */
606
+ 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
+ }
666
+ }
667
+ /**
668
+ * 数据目录排他文件锁管理器。
669
+ * 负责在数据目录下维护 .actiondock.data.lock 排他文件锁,记录宿主进程与子进程运行状态,
670
+ * 防止多个无协调宿主并发冲突,并在非正常退出时提供故障恢复检测。
671
+ */
672
+ export class DataDirLock {
673
+ lockDirPath;
674
+ info;
675
+ released = false;
676
+ constructor(lockDirPath, info) {
677
+ this.lockDirPath = lockDirPath;
678
+ this.info = info;
679
+ }
680
+ /**
681
+ * 获取当前排他锁元数据信息。
682
+ */
683
+ get lockInfo() {
684
+ return this.info;
685
+ }
686
+ /**
687
+ * 检查当前锁是否已被释放。
688
+ */
689
+ get isReleased() {
690
+ return this.released;
691
+ }
692
+ /**
693
+ * 登记受管子进程标识符。
694
+ */
695
+ registerChildPid(pid) {
696
+ if (this.released)
697
+ return;
698
+ if (!this.info.childPids) {
699
+ this.info.childPids = [];
700
+ }
701
+ if (!this.info.childPids.includes(pid)) {
702
+ this.info.childPids.push(pid);
703
+ this.flush();
704
+ }
705
+ }
706
+ /**
707
+ * 注销受管子进程标识符。
708
+ */
709
+ unregisterChildPid(pid) {
710
+ if (this.released)
711
+ return;
712
+ if (this.info.childPids) {
713
+ this.info.childPids = this.info.childPids.filter((p) => p !== pid);
714
+ this.flush();
715
+ }
716
+ }
717
+ /**
718
+ * 将当前锁元数据刷新持久化至磁盘文件。
719
+ * 先写入临时文件再通过 renameSync 原子替换,消除 truncate 空文件窗口。
720
+ */
721
+ flush() {
722
+ try {
723
+ let isDir = false;
724
+ try {
725
+ isDir = existsSync(this.lockDirPath) && statSync(this.lockDirPath).isDirectory();
726
+ }
727
+ catch {
728
+ isDir = false;
729
+ }
730
+ const content = JSON.stringify(this.info, null, 2);
731
+ if (isDir) {
732
+ const metaPath = join(this.lockDirPath, "metadata.json");
733
+ const tmpPath = join(this.lockDirPath, `metadata.json.tmp.${process.pid}.${randomUUID().slice(0, 8)}`);
734
+ writeFileSync(tmpPath, content, { mode: 0o600 });
735
+ renameSync(tmpPath, metaPath);
736
+ }
737
+ else {
738
+ // 兼容单文件模式
739
+ const tmpPath = `${this.lockDirPath}.tmp.${process.pid}.${randomUUID().slice(0, 8)}`;
740
+ writeFileSync(tmpPath, content, { mode: 0o600 });
741
+ renameSync(tmpPath, this.lockDirPath);
742
+ }
743
+ }
744
+ catch {
745
+ // 忽略刷新写入异常
746
+ }
747
+ }
748
+ /**
749
+ * 释放排他锁并移除锁目录或文件。
750
+ */
751
+ release() {
752
+ if (this.released)
753
+ return;
754
+ this.released = true;
755
+ safeReleaseLock(this.lockDirPath, this.info.sessionToken, this.info.lockToken);
756
+ cleanStaleQuarantines(dirname(this.lockDirPath), ".actiondock.data.lock");
757
+ }
758
+ /**
759
+ * 尝试获取指定数据目录的排他锁。
760
+ *
761
+ * 仲裁规则:
762
+ * - 引入所有竞争者均遵守的原子 reclaim guard 机制(lockDirPath.reclaim),每个 guard 具备全局唯一 guardToken。
763
+ * - 基于原子目录创建 mkdirSync(lockDirPath, { mode: 0o700 }) 确立所有权,并在其下存放 metadata.json。
764
+ * - 兼容已有常规文件锁(如老版本或测试 mock 场景)。
765
+ * - 若检测到 reclaim guard 存在且处于宽限期内或持有者存活,必须等待,禁止抢先创建主锁。
766
+ * - 若锁目录已存在,通过宽限期机制防止将并发写入中的元数据误判为锁死亡。
767
+ * - 若主进程仍处于存活状态,抛出 DATA_DIR_IN_USE 错误拒绝并发启动。
768
+ * - 若主进程已退出但仍有子进程存活,抛出 DATA_DIR_RECOVERY_REQUIRED 错误。
769
+ * - 当识别到主锁为陈旧锁时,竞争者必须先原子竞争获取 reclaim guard。
770
+ * - 仅成功获取 reclaim guard 的唯一胜利者获准执行:复核主锁陈旧性 -> 验证并清理陈旧主锁 -> 原子创建新主锁并写入自身元数据 -> 清理 reclaim guard。
771
+ * - 竞争失败者等待并 continue 重试;若 reclaim guard 持有者意外崩溃,其他竞争者在超过宽限期且 PID 已死后通过 safeRemoveStaleReclaimGuard 核对 guardToken 并清理。
772
+ * - 严禁在获取新主锁遇到 EEXIST 时盲目 rmSync;后检发现他人活跃 guard 时,仅通过 safeRollbackLock 核对自身 sessionToken 回滚自身锁。
773
+ *
774
+ * @param dataDir 目标数据存储目录物理绝对路径
775
+ * @param options 锁配置参数
776
+ */
777
+ static acquire(dataDir, options = {}) {
778
+ const timeoutMs = options.acquireTimeoutMs ?? 5000;
779
+ const deadline = Date.now() + timeoutMs;
780
+ if (!existsSync(dataDir)) {
781
+ mkdirSync(dataDir, { recursive: true, mode: 0o700 });
782
+ }
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();
788
+ const token = options.sessionToken || randomUUID();
789
+ const lockToken = randomUUID();
790
+ const newLockInfo = {
791
+ pid: currentPid,
792
+ hostname: currentHost,
793
+ sessionToken: token,
794
+ lockToken,
795
+ createdAt: new Date().toISOString(),
796
+ childPids: [],
797
+ hostSessionId: options.hostSessionId,
798
+ };
799
+ 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;
835
+ }
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
+ }
1011
+ }
1012
+ throw err;
1013
+ }
1014
+ }
1015
+ }
1016
+ }