@openclaw/crabline 0.1.11 → 0.1.13

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 (214) hide show
  1. package/README.md +137 -23
  2. package/dist/src/bin/crabline.js +3 -1
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +5 -1
  5. package/dist/src/cli/program.js +281 -52
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +39 -3
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +20 -0
  10. package/dist/src/config/schema.js +163 -35
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.d.ts +1 -0
  13. package/dist/src/core/errors.js +6 -3
  14. package/dist/src/core/errors.js.map +1 -1
  15. package/dist/src/core/http-path.d.ts +1 -0
  16. package/dist/src/core/http-path.js +14 -0
  17. package/dist/src/core/http-path.js.map +1 -0
  18. package/dist/src/core/matcher.d.ts +3 -1
  19. package/dist/src/core/matcher.js +58 -2
  20. package/dist/src/core/matcher.js.map +1 -1
  21. package/dist/src/core/nonces.d.ts +2 -0
  22. package/dist/src/core/nonces.js +11 -1
  23. package/dist/src/core/nonces.js.map +1 -1
  24. package/dist/src/core/reporters.d.ts +5 -0
  25. package/dist/src/core/reporters.js +26 -2
  26. package/dist/src/core/reporters.js.map +1 -1
  27. package/dist/src/core/run.d.ts +3 -1
  28. package/dist/src/core/run.js +178 -66
  29. package/dist/src/core/run.js.map +1 -1
  30. package/dist/src/index.d.ts +12 -10
  31. package/dist/src/index.js +6 -5
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/matrix-ids.d.ts +5 -0
  34. package/dist/src/matrix-ids.js +83 -0
  35. package/dist/src/matrix-ids.js.map +1 -0
  36. package/dist/src/openclaw/artifact-generation.d.ts +7 -6
  37. package/dist/src/openclaw/artifact-generation.js +385 -41
  38. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  39. package/dist/src/openclaw/bridges/discord.d.ts +1 -0
  40. package/dist/src/openclaw/bridges/discord.js +149 -0
  41. package/dist/src/openclaw/bridges/discord.js.map +1 -0
  42. package/dist/src/openclaw/bridges/matrix.js +22 -5
  43. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  44. package/dist/src/openclaw/bridges/mattermost.js +14 -9
  45. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  46. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  47. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  48. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  49. package/dist/src/openclaw/bridges/signal.js +16 -7
  50. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  51. package/dist/src/openclaw/bridges/slack.js +117 -24
  52. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  53. package/dist/src/openclaw/bridges/telegram.js +46 -20
  54. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  55. package/dist/src/openclaw/bridges/whatsapp.js +33 -17
  56. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  57. package/dist/src/openclaw/bridges/zalo.js +9 -4
  58. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  59. package/dist/src/openclaw/outbound-contract.js +3 -0
  60. package/dist/src/openclaw/outbound-contract.js.map +1 -1
  61. package/dist/src/openclaw/private-file.d.ts +38 -3
  62. package/dist/src/openclaw/private-file.js +2008 -87
  63. package/dist/src/openclaw/private-file.js.map +1 -1
  64. package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
  65. package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
  66. package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
  67. package/dist/src/openclaw/shared.d.ts +10 -17
  68. package/dist/src/openclaw/shared.js +53 -19
  69. package/dist/src/openclaw/shared.js.map +1 -1
  70. package/dist/src/openclaw.d.ts +3 -5
  71. package/dist/src/openclaw.js +245 -60
  72. package/dist/src/openclaw.js.map +1 -1
  73. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  74. package/dist/src/platform/process-owned-lock.js +1698 -0
  75. package/dist/src/platform/process-owned-lock.js.map +1 -0
  76. package/dist/src/platform/recorder-directory.d.ts +1 -0
  77. package/dist/src/platform/recorder-directory.js +14 -0
  78. package/dist/src/platform/recorder-directory.js.map +1 -0
  79. package/dist/src/platform/windows-acl.d.ts +18 -0
  80. package/dist/src/platform/windows-acl.js +1367 -0
  81. package/dist/src/platform/windows-acl.js.map +1 -0
  82. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  83. package/dist/src/platform/windows-lock-root.js +98 -0
  84. package/dist/src/platform/windows-lock-root.js.map +1 -0
  85. package/dist/src/providers/builtin/discord.d.ts +4 -0
  86. package/dist/src/providers/builtin/discord.js +52 -9
  87. package/dist/src/providers/builtin/discord.js.map +1 -1
  88. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  89. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  90. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  91. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  92. package/dist/src/providers/builtin/feishu.js +96 -26
  93. package/dist/src/providers/builtin/feishu.js.map +1 -1
  94. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  95. package/dist/src/providers/builtin/googlechat.js +95 -26
  96. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  97. package/dist/src/providers/builtin/imessage.js +60 -17
  98. package/dist/src/providers/builtin/imessage.js.map +1 -1
  99. package/dist/src/providers/builtin/loopback.js +88 -35
  100. package/dist/src/providers/builtin/loopback.js.map +1 -1
  101. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  102. package/dist/src/providers/builtin/matrix.js +78 -21
  103. package/dist/src/providers/builtin/matrix.js.map +1 -1
  104. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  105. package/dist/src/providers/builtin/mattermost.js +138 -24
  106. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  107. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  108. package/dist/src/providers/builtin/msteams.js +118 -38
  109. package/dist/src/providers/builtin/msteams.js.map +1 -1
  110. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  111. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  112. package/dist/src/providers/builtin/script.js +1057 -146
  113. package/dist/src/providers/builtin/script.js.map +1 -1
  114. package/dist/src/providers/builtin/slack.d.ts +8 -1
  115. package/dist/src/providers/builtin/slack.js +327 -24
  116. package/dist/src/providers/builtin/slack.js.map +1 -1
  117. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  118. package/dist/src/providers/builtin/telegram.js +55 -14
  119. package/dist/src/providers/builtin/telegram.js.map +1 -1
  120. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  121. package/dist/src/providers/builtin/whatsapp.js +157 -29
  122. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  123. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  124. package/dist/src/providers/builtin/zalo.js +31 -7
  125. package/dist/src/providers/builtin/zalo.js.map +1 -1
  126. package/dist/src/providers/catalog.d.ts +1 -1
  127. package/dist/src/providers/catalog.js +1 -1
  128. package/dist/src/providers/catalog.js.map +1 -1
  129. package/dist/src/providers/local-mock.d.ts +4 -0
  130. package/dist/src/providers/local-mock.js +162 -35
  131. package/dist/src/providers/local-mock.js.map +1 -1
  132. package/dist/src/providers/native-ids.d.ts +2 -0
  133. package/dist/src/providers/native-ids.js +3 -0
  134. package/dist/src/providers/native-ids.js.map +1 -1
  135. package/dist/src/providers/recorder.d.ts +14 -3
  136. package/dist/src/providers/recorder.js +521 -41
  137. package/dist/src/providers/recorder.js.map +1 -1
  138. package/dist/src/providers/registry.d.ts +1 -0
  139. package/dist/src/providers/registry.js +65 -20
  140. package/dist/src/providers/registry.js.map +1 -1
  141. package/dist/src/providers/signed-jwt.d.ts +6 -1
  142. package/dist/src/providers/signed-jwt.js +241 -50
  143. package/dist/src/providers/signed-jwt.js.map +1 -1
  144. package/dist/src/providers/slack-ids.d.ts +1 -0
  145. package/dist/src/providers/slack-ids.js +5 -0
  146. package/dist/src/providers/slack-ids.js.map +1 -1
  147. package/dist/src/providers/target-normalizers.d.ts +9 -1
  148. package/dist/src/providers/target-normalizers.js +177 -58
  149. package/dist/src/providers/target-normalizers.js.map +1 -1
  150. package/dist/src/providers/webhook-server.d.ts +3 -0
  151. package/dist/src/providers/webhook-server.js +268 -47
  152. package/dist/src/providers/webhook-server.js.map +1 -1
  153. package/dist/src/servers/discord.d.ts +40 -0
  154. package/dist/src/servers/discord.js +842 -0
  155. package/dist/src/servers/discord.js.map +1 -0
  156. package/dist/src/servers/http.d.ts +14 -2
  157. package/dist/src/servers/http.js +229 -58
  158. package/dist/src/servers/http.js.map +1 -1
  159. package/dist/src/servers/index.d.ts +10 -4
  160. package/dist/src/servers/index.js +4 -0
  161. package/dist/src/servers/index.js.map +1 -1
  162. package/dist/src/servers/matrix.d.ts +3 -0
  163. package/dist/src/servers/matrix.js +268 -110
  164. package/dist/src/servers/matrix.js.map +1 -1
  165. package/dist/src/servers/mattermost.d.ts +5 -0
  166. package/dist/src/servers/mattermost.js +389 -91
  167. package/dist/src/servers/mattermost.js.map +1 -1
  168. package/dist/src/servers/recorder.d.ts +11 -0
  169. package/dist/src/servers/recorder.js +1072 -69
  170. package/dist/src/servers/recorder.js.map +1 -1
  171. package/dist/src/servers/signal.d.ts +1 -0
  172. package/dist/src/servers/signal.js +279 -44
  173. package/dist/src/servers/signal.js.map +1 -1
  174. package/dist/src/servers/slack.d.ts +15 -0
  175. package/dist/src/servers/slack.js +270 -54
  176. package/dist/src/servers/slack.js.map +1 -1
  177. package/dist/src/servers/telegram-identity.d.ts +6 -0
  178. package/dist/src/servers/telegram-identity.js +27 -0
  179. package/dist/src/servers/telegram-identity.js.map +1 -0
  180. package/dist/src/servers/telegram.d.ts +17 -6
  181. package/dist/src/servers/telegram.js +1218 -107
  182. package/dist/src/servers/telegram.js.map +1 -1
  183. package/dist/src/servers/webhook-target.d.ts +12 -4
  184. package/dist/src/servers/webhook-target.js +226 -28
  185. package/dist/src/servers/webhook-target.js.map +1 -1
  186. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  187. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  188. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  189. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  190. package/dist/src/servers/whatsapp-jid.js +9 -5
  191. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  192. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  193. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  194. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  195. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  196. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  197. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  198. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  199. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  200. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  201. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  202. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  203. package/dist/src/servers/whatsapp.d.ts +7 -1
  204. package/dist/src/servers/whatsapp.js +149 -25
  205. package/dist/src/servers/whatsapp.js.map +1 -1
  206. package/dist/src/servers/zalo.d.ts +1 -0
  207. package/dist/src/servers/zalo.js +193 -82
  208. package/dist/src/servers/zalo.js.map +1 -1
  209. package/docs/channel-setup.md +251 -64
  210. package/fixtures/examples/crabline.example.yaml +3 -2
  211. package/package.json +18 -10
  212. package/dist/src/openclaw/smoke-lock.js +0 -1004
  213. package/dist/src/openclaw/smoke-lock.js.map +0 -1
  214. package/fixtures/examples/openclaw-bridge.yaml +0 -125
@@ -0,0 +1,1698 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import fs, { readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { performance } from "node:perf_hooks";
6
+ import { resolveWindowsPowerShellPath } from "./windows-acl.js";
7
+ const retainedCoordinationClaims = new Map();
8
+ const abandonedOwnerKeys = new Set();
9
+ const abandonedDirectoryIdentities = new Map();
10
+ const OWNER_FILE = "crabline-owner.json";
11
+ const ABANDONED_OWNER_PREFIX = ".crabline-abandoned-";
12
+ const ABANDONED_OWNER_NAME_PATTERN = /^\.crabline-abandoned-[0-9a-f]{64}$/u;
13
+ const MAX_OWNER_BYTES = 4096;
14
+ const MAX_PROCESS_ID = 2_147_483_647;
15
+ const IDENTITY_CACHE_MS = 1000;
16
+ const CURRENT_IDENTITY_ATTEMPTS = 3;
17
+ const COORDINATION_RELEASE_RETRY_MS = 10;
18
+ const COORDINATION_RELEASE_WAIT_MS = 5000;
19
+ const LEGACY_PROCESS_START_TOLERANCE_MS = 2000;
20
+ const OWNERLESS_LOCK_RECOVERY_MS = 10 * 60 * 1000;
21
+ const CURRENT_PROCESS_STARTED_AT_MS = Math.trunc(performance.timeOrigin);
22
+ const CURRENT_EXECUTION_IDENTITY = randomUUID();
23
+ const DARWIN_PRECISE_IDENTITY_PATTERN = /^darwin:\d+\.\d+:us:\d+$/u;
24
+ const DARWIN_COARSE_IDENTITY_PATTERN = /^darwin:\d+\.\d+:s:(\d+)$/u;
25
+ const LINUX_PID_NAMESPACE_PATTERN = /^pid:\[\d+\]$/u;
26
+ const DARWIN_MONTHS = new Map([
27
+ ["Jan", 0],
28
+ ["Feb", 1],
29
+ ["Mar", 2],
30
+ ["Apr", 3],
31
+ ["May", 4],
32
+ ["Jun", 5],
33
+ ["Jul", 6],
34
+ ["Aug", 7],
35
+ ["Sep", 8],
36
+ ["Oct", 9],
37
+ ["Nov", 10],
38
+ ["Dec", 11],
39
+ ]);
40
+ function isValidProcessId(pid) {
41
+ return Number.isSafeInteger(pid) && pid > 0 && pid <= MAX_PROCESS_ID;
42
+ }
43
+ function isProcessAlive(pid) {
44
+ if (!isValidProcessId(pid)) {
45
+ return false;
46
+ }
47
+ try {
48
+ process.kill(pid, 0);
49
+ return true;
50
+ }
51
+ catch (error) {
52
+ return error.code === "EPERM";
53
+ }
54
+ }
55
+ export function isDeadLinuxProcessState(value) {
56
+ const commandEnd = value.lastIndexOf(") ");
57
+ if (commandEnd < 0) {
58
+ return false;
59
+ }
60
+ const state = value
61
+ .slice(commandEnd + 2)
62
+ .trim()
63
+ .split(/\s+/u)[0];
64
+ return state === "Z" || state === "X" || state === "x";
65
+ }
66
+ function isDefunctProcess(pid) {
67
+ if (process.platform === "linux") {
68
+ try {
69
+ return isDeadLinuxProcessState(readFileSync(`/proc/${pid}/stat`, "utf8"));
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ }
75
+ if (process.platform === "darwin") {
76
+ const result = spawnSync("/bin/ps", ["-o", "stat=", "-p", String(pid)], {
77
+ encoding: "utf8",
78
+ env: { ...process.env, LC_ALL: "C" },
79
+ timeout: 1000,
80
+ });
81
+ return result.status === 0 && /^Z/u.test(result.stdout.trim());
82
+ }
83
+ return false;
84
+ }
85
+ function processIdentityFromLinuxStat(value, bootId) {
86
+ const normalizedBootId = bootId.trim();
87
+ if (!/^[0-9a-f-]{16,64}$/iu.test(normalizedBootId)) {
88
+ return null;
89
+ }
90
+ const commandEnd = value.lastIndexOf(") ");
91
+ if (commandEnd < 0) {
92
+ return null;
93
+ }
94
+ const startTicks = value
95
+ .slice(commandEnd + 2)
96
+ .trim()
97
+ .split(/\s+/u)[19];
98
+ return startTicks && /^\d+$/u.test(startTicks) ? `linux:${normalizedBootId}:${startTicks}` : null;
99
+ }
100
+ function processIdentityFromDarwin(processDetails, bootTime) {
101
+ const bootMatch = /\bsec = (\d+), usec = (\d+)\b/u.exec(bootTime);
102
+ const launchMatch = /^Launch Time:\s*(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\.(\d{3,6}) ([+-])(\d{2})(\d{2})$/mu.exec(processDetails);
103
+ if (!bootMatch) {
104
+ return null;
105
+ }
106
+ if (launchMatch) {
107
+ const [, year, month, day, hour, minute, second, fraction, sign, offsetHour, offsetMinute] = launchMatch;
108
+ const offsetMs = (Number(offsetHour) * 60 + Number(offsetMinute)) * 60_000 * (sign === "+" ? 1 : -1);
109
+ const fractionMicros = Number(fraction.padEnd(6, "0"));
110
+ const utcMilliseconds = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second), Math.floor(fractionMicros / 1000)) - offsetMs;
111
+ if (Number.isSafeInteger(utcMilliseconds) && utcMilliseconds > 0) {
112
+ const startedAtMicros = BigInt(utcMilliseconds) * 1000n + BigInt(fractionMicros % 1000);
113
+ return `darwin:${bootMatch[1]}.${bootMatch[2]}:us:${startedAtMicros}`;
114
+ }
115
+ }
116
+ const normalizedDetails = processDetails.trim().replace(/\s+/gu, " ");
117
+ if (normalizedDetails.length === 0 || normalizedDetails.length > 64) {
118
+ return null;
119
+ }
120
+ return `darwin:${bootMatch[1]}.${bootMatch[2]}:${normalizedDetails}`;
121
+ }
122
+ function coarseProcessIdentityFromDarwin(processDetails, bootTime) {
123
+ const bootMatch = /\bsec = (\d+), usec = (\d+)\b/u.exec(bootTime);
124
+ const startMatch = /^(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)\s+([A-Z][a-z]{2})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})\s*$/u.exec(processDetails);
125
+ const month = DARWIN_MONTHS.get(startMatch?.[1] ?? "");
126
+ if (!bootMatch || !startMatch || month === undefined) {
127
+ return null;
128
+ }
129
+ const startedAtMs = Date.UTC(Number(startMatch[6]), month, Number(startMatch[2]), Number(startMatch[3]), Number(startMatch[4]), Number(startMatch[5]));
130
+ return Number.isSafeInteger(startedAtMs) && startedAtMs > 0
131
+ ? `darwin:${bootMatch[1]}.${bootMatch[2]}:s:${Math.trunc(startedAtMs / 1000)}`
132
+ : null;
133
+ }
134
+ function readProcessIdentity(pid) {
135
+ if (process.platform === "linux") {
136
+ try {
137
+ return processIdentityFromLinuxStat(readFileSync(`/proc/${pid}/stat`, "utf8"), readFileSync("/proc/sys/kernel/random/boot_id", "utf8"));
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ if (process.platform === "darwin") {
144
+ const options = {
145
+ encoding: "utf8",
146
+ env: { ...process.env, LC_ALL: "C", TZ: "UTC" },
147
+ maxBuffer: 512 * 1024,
148
+ timeout: 1000,
149
+ };
150
+ const bootTime = spawnSync("/usr/sbin/sysctl", ["-n", "kern.boottime"], options);
151
+ const preciseDetails = spawnSync("/usr/bin/vmmap", ["-summary", String(pid)], {
152
+ ...options,
153
+ timeout: 3000,
154
+ });
155
+ const sampleDetails = preciseDetails.status === 0
156
+ ? undefined
157
+ : spawnSync("/usr/bin/sample", [String(pid), "1", "1"], {
158
+ ...options,
159
+ timeout: 3000,
160
+ });
161
+ const nativeDetails = preciseDetails.status === 0
162
+ ? preciseDetails
163
+ : sampleDetails?.status === 0
164
+ ? sampleDetails
165
+ : undefined;
166
+ const identity = bootTime.status === 0 && nativeDetails
167
+ ? processIdentityFromDarwin(nativeDetails.stdout, bootTime.stdout)
168
+ : null;
169
+ if (identity !== null && DARWIN_PRECISE_IDENTITY_PATTERN.test(identity)) {
170
+ return identity;
171
+ }
172
+ const coarseDetails = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], options);
173
+ return bootTime.status === 0 && coarseDetails.status === 0
174
+ ? coarseProcessIdentityFromDarwin(coarseDetails.stdout, bootTime.stdout)
175
+ : null;
176
+ }
177
+ if (process.platform !== "win32") {
178
+ return null;
179
+ }
180
+ let powershellPath;
181
+ try {
182
+ powershellPath = resolveWindowsPowerShellPath(process.env.SystemRoot);
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ const result = spawnSync(powershellPath, [
188
+ "-NoProfile",
189
+ "-NonInteractive",
190
+ "-Command",
191
+ `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks.ToString()`,
192
+ ], { encoding: "utf8", timeout: 1000, windowsHide: true });
193
+ const ticks = result.status === 0 ? result.stdout.trim() : "";
194
+ return /^\d+$/u.test(ticks) ? `windows:${ticks}` : null;
195
+ }
196
+ function readProcessNamespace(pid) {
197
+ if (process.platform !== "linux") {
198
+ return null;
199
+ }
200
+ try {
201
+ const namespace = fs.readlinkSync(`/proc/${pid}/ns/pid`);
202
+ return LINUX_PID_NAMESPACE_PATTERN.test(namespace) ? namespace : null;
203
+ }
204
+ catch {
205
+ return null;
206
+ }
207
+ }
208
+ function readMachineIdentity() {
209
+ if (process.platform === "linux") {
210
+ try {
211
+ const bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
212
+ return /^[0-9a-f-]{16,64}$/iu.test(bootId) ? `linux:${bootId}` : null;
213
+ }
214
+ catch {
215
+ return null;
216
+ }
217
+ }
218
+ if (process.platform === "darwin") {
219
+ const result = spawnSync("/usr/sbin/ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
220
+ encoding: "utf8",
221
+ env: { ...process.env, LC_ALL: "C" },
222
+ timeout: 1000,
223
+ });
224
+ const platformUuid = /"IOPlatformUUID"\s*=\s*"([^"]+)"/u.exec(result.stdout)?.[1];
225
+ return result.status === 0 && platformUuid ? `darwin:${platformUuid.toLowerCase()}` : null;
226
+ }
227
+ if (process.platform !== "win32") {
228
+ return null;
229
+ }
230
+ let powershellPath;
231
+ try {
232
+ powershellPath = resolveWindowsPowerShellPath(process.env.SystemRoot);
233
+ }
234
+ catch {
235
+ return null;
236
+ }
237
+ const result = spawnSync(powershellPath, [
238
+ "-NoProfile",
239
+ "-NonInteractive",
240
+ "-Command",
241
+ "(Get-ItemProperty -LiteralPath 'HKLM:\\SOFTWARE\\Microsoft\\Cryptography' -Name MachineGuid -ErrorAction Stop).MachineGuid",
242
+ ], { encoding: "utf8", timeout: 1000, windowsHide: true });
243
+ const machineGuid = result.status === 0 ? result.stdout.trim() : "";
244
+ return /^[0-9a-f-]{16,64}$/iu.test(machineGuid) ? `windows:${machineGuid.toLowerCase()}` : null;
245
+ }
246
+ let cachedLinuxClockTicks;
247
+ function processStartedAtMsFromIdentity(identity) {
248
+ const darwinStart = /^darwin:\d+\.\d+:us:(\d+)$/u.exec(identity ?? "")?.[1];
249
+ if (darwinStart) {
250
+ return Number(BigInt(darwinStart) / 1000n);
251
+ }
252
+ const coarseDarwinStart = DARWIN_COARSE_IDENTITY_PATTERN.exec(identity ?? "")?.[1];
253
+ if (coarseDarwinStart) {
254
+ return Number(coarseDarwinStart) * 1000;
255
+ }
256
+ const windowsStart = /^windows:(\d+)$/u.exec(identity ?? "")?.[1];
257
+ if (windowsStart) {
258
+ const unixEpochTicks = 621355968000000000n;
259
+ const ticks = BigInt(windowsStart);
260
+ return ticks >= unixEpochTicks ? Number((ticks - unixEpochTicks) / 10000n) : null;
261
+ }
262
+ return null;
263
+ }
264
+ function readLegacyProcessStartedAtMs(pid) {
265
+ if (process.platform === "linux") {
266
+ try {
267
+ const processStat = readFileSync(`/proc/${pid}/stat`, "utf8");
268
+ const commandEnd = processStat.lastIndexOf(") ");
269
+ const startTicks = commandEnd < 0
270
+ ? undefined
271
+ : processStat
272
+ .slice(commandEnd + 2)
273
+ .trim()
274
+ .split(/\s+/u)[19];
275
+ const bootTime = /^btime\s+(\d+)$/mu.exec(readFileSync("/proc/stat", "utf8"))?.[1];
276
+ if (!startTicks || !/^\d+$/u.test(startTicks) || !bootTime) {
277
+ return null;
278
+ }
279
+ if (cachedLinuxClockTicks === undefined) {
280
+ const result = spawnSync("/usr/bin/getconf", ["CLK_TCK"], {
281
+ encoding: "utf8",
282
+ timeout: 1000,
283
+ });
284
+ const ticks = result.status === 0 ? Number(result.stdout.trim()) : Number.NaN;
285
+ if (!Number.isSafeInteger(ticks) || ticks <= 0) {
286
+ return null;
287
+ }
288
+ cachedLinuxClockTicks = ticks;
289
+ }
290
+ return Number(bootTime) * 1000 + (Number(startTicks) * 1000) / cachedLinuxClockTicks;
291
+ }
292
+ catch {
293
+ return null;
294
+ }
295
+ }
296
+ return processStartedAtMsFromIdentity(readProcessIdentity(pid));
297
+ }
298
+ function legacyProcessStartMatches(expected, observed) {
299
+ return Math.abs(expected - observed) <= LEGACY_PROCESS_START_TOLERANCE_MS;
300
+ }
301
+ function isCoarseDarwinIdentity(identity) {
302
+ return DARWIN_COARSE_IDENTITY_PATTERN.test(identity);
303
+ }
304
+ function isExactProcessIdentity(identity) {
305
+ return (/^linux:[0-9a-f-]{16,64}:\d+$/iu.test(identity) ||
306
+ DARWIN_PRECISE_IDENTITY_PATTERN.test(identity) ||
307
+ /^windows:\d+$/u.test(identity));
308
+ }
309
+ function directoryIdentity(stats) {
310
+ return { dev: stats.dev, ino: stats.ino };
311
+ }
312
+ function directoryIdentityMatches(expected, actual) {
313
+ return expected !== undefined && expected.dev === actual.dev && expected.ino === actual.ino;
314
+ }
315
+ function lockCleanupError(message) {
316
+ return Object.assign(new Error(message), { code: "ELOCKED" });
317
+ }
318
+ function isLockArtifactName(name) {
319
+ return name === OWNER_FILE || ABANDONED_OWNER_NAME_PATTERN.test(name);
320
+ }
321
+ function verifiedLockDirectoryStats(directoryPath, expectedIdentity, expectedMtimeMs) {
322
+ let stats;
323
+ try {
324
+ stats = fs.lstatSync(directoryPath, { bigint: true });
325
+ }
326
+ catch (error) {
327
+ if (error.code === "ENOENT") {
328
+ return undefined;
329
+ }
330
+ throw error;
331
+ }
332
+ if (!stats.isDirectory() ||
333
+ stats.isSymbolicLink() ||
334
+ !directoryIdentityMatches(expectedIdentity, stats) ||
335
+ (expectedMtimeMs !== undefined && stats.mtimeMs !== expectedMtimeMs)) {
336
+ throw lockCleanupError("Recorder lock cleanup target changed.");
337
+ }
338
+ return stats;
339
+ }
340
+ function removeVerifiedLockDirectorySync(directoryPath, expectedIdentity, expectedMtimeMs, beforeRemove, afterClaim) {
341
+ if (!verifiedLockDirectoryStats(directoryPath, expectedIdentity, expectedMtimeMs)) {
342
+ return;
343
+ }
344
+ const sourcePath = String(directoryPath);
345
+ const quarantinePath = `${sourcePath}.cleanup.${randomUUID()}`;
346
+ beforeRemove?.(sourcePath);
347
+ if (!verifiedLockDirectoryStats(sourcePath, expectedIdentity, expectedMtimeMs)) {
348
+ return;
349
+ }
350
+ fs.renameSync(directoryPath, quarantinePath);
351
+ if (!verifiedLockDirectoryStats(quarantinePath, expectedIdentity, expectedMtimeMs)) {
352
+ throw lockCleanupError("Recorder lock cleanup target disappeared.");
353
+ }
354
+ afterClaim?.(quarantinePath);
355
+ try {
356
+ const entries = fs.readdirSync(quarantinePath);
357
+ if (!verifiedLockDirectoryStats(quarantinePath, expectedIdentity)) {
358
+ throw lockCleanupError("Recorder lock cleanup target changed.");
359
+ }
360
+ const artifacts = [];
361
+ for (const entry of entries) {
362
+ if (!isLockArtifactName(entry)) {
363
+ throw lockCleanupError("Recorder lock cleanup found an unexpected artifact.");
364
+ }
365
+ if (!verifiedLockDirectoryStats(quarantinePath, expectedIdentity)) {
366
+ throw lockCleanupError("Recorder lock cleanup target changed.");
367
+ }
368
+ const artifactPath = path.join(quarantinePath, entry);
369
+ const artifact = fs.lstatSync(artifactPath, { bigint: true });
370
+ if (!artifact.isFile() || artifact.isSymbolicLink()) {
371
+ throw lockCleanupError("Recorder lock cleanup found an unsafe artifact.");
372
+ }
373
+ artifacts.push(artifactPath);
374
+ }
375
+ artifacts.sort((left, right) => Number(path.basename(left) === OWNER_FILE) - Number(path.basename(right) === OWNER_FILE));
376
+ for (const artifactPath of artifacts) {
377
+ if (!verifiedLockDirectoryStats(quarantinePath, expectedIdentity)) {
378
+ throw lockCleanupError("Recorder lock cleanup target changed.");
379
+ }
380
+ const artifact = fs.lstatSync(artifactPath, { bigint: true });
381
+ if (!artifact.isFile() || artifact.isSymbolicLink()) {
382
+ throw lockCleanupError("Recorder lock cleanup artifact changed.");
383
+ }
384
+ fs.unlinkSync(artifactPath);
385
+ }
386
+ if (!verifiedLockDirectoryStats(quarantinePath, expectedIdentity)) {
387
+ throw lockCleanupError("Recorder lock cleanup target disappeared.");
388
+ }
389
+ fs.rmdirSync(quarantinePath);
390
+ }
391
+ catch (error) {
392
+ try {
393
+ if (verifiedLockDirectoryStats(quarantinePath, expectedIdentity)) {
394
+ fs.utimesSync(quarantinePath, new Date(0), new Date(0));
395
+ }
396
+ }
397
+ catch {
398
+ // Do not mutate a replacement directory after cleanup loses its identity fence.
399
+ }
400
+ throw error;
401
+ }
402
+ }
403
+ function canonicalLockDirectoryPath(directoryPath) {
404
+ const resolved = path.resolve(String(directoryPath));
405
+ let canonicalParent;
406
+ try {
407
+ canonicalParent = fs.realpathSync.native(path.dirname(resolved));
408
+ }
409
+ catch {
410
+ canonicalParent = path.dirname(resolved);
411
+ }
412
+ const canonical = path.join(canonicalParent, path.basename(resolved));
413
+ return process.platform === "win32" ? path.win32.normalize(canonical).toLowerCase() : canonical;
414
+ }
415
+ function abandonedOwnerKey(directoryPath, ownerToken) {
416
+ return `${canonicalLockDirectoryPath(directoryPath)}\0${ownerToken}`;
417
+ }
418
+ function recoveryClaimPath(directoryPath, fingerprint = "coordination") {
419
+ const canonicalDirectory = canonicalLockDirectoryPath(directoryPath);
420
+ const digest = createHash("sha256")
421
+ .update(canonicalDirectory)
422
+ .update("\0")
423
+ .update(fingerprint)
424
+ .digest("hex");
425
+ return path.join(path.dirname(canonicalDirectory), `.crabline-reclaim-${digest}`);
426
+ }
427
+ function lockOwnerMatches(left, right) {
428
+ if (!left || !right) {
429
+ return left === right;
430
+ }
431
+ return left.owner.token === right.owner.token;
432
+ }
433
+ let cachedCurrentProcessIdentity;
434
+ let cachedCurrentMachineIdentity;
435
+ function readProcessIdentityWithRetry(pid, reader) {
436
+ for (let attempt = 0; attempt < CURRENT_IDENTITY_ATTEMPTS; attempt++) {
437
+ const identity = reader(pid);
438
+ if (identity !== null) {
439
+ return identity;
440
+ }
441
+ }
442
+ return null;
443
+ }
444
+ function readProcessNamespaceWithRetry(pid) {
445
+ for (let attempt = 0; attempt < CURRENT_IDENTITY_ATTEMPTS; attempt++) {
446
+ const namespace = readProcessNamespace(pid);
447
+ if (namespace !== null) {
448
+ return namespace;
449
+ }
450
+ }
451
+ return null;
452
+ }
453
+ function parseOpenedOwner(ownerHandle) {
454
+ const stats = fs.fstatSync(ownerHandle);
455
+ if (!stats.isFile() || stats.size === 0 || stats.size > MAX_OWNER_BYTES) {
456
+ return undefined;
457
+ }
458
+ const raw = Buffer.alloc(stats.size);
459
+ let offset = 0;
460
+ while (offset < raw.byteLength) {
461
+ const bytesRead = fs.readSync(ownerHandle, raw, offset, raw.byteLength - offset, offset);
462
+ if (bytesRead === 0) {
463
+ return undefined;
464
+ }
465
+ offset += bytesRead;
466
+ }
467
+ const value = JSON.parse(raw.toString("utf8"));
468
+ if ((value.version !== 1 && value.version !== 2 && value.version !== 3 && value.version !== 4) ||
469
+ !isValidProcessId(value.pid ?? 0) ||
470
+ typeof value.token !== "string" ||
471
+ value.token.length === 0 ||
472
+ value.token.length > 128 ||
473
+ !Number.isSafeInteger(value.processStartedAtMs) ||
474
+ (value.executionIdentity !== undefined &&
475
+ value.executionIdentity !== null &&
476
+ (typeof value.executionIdentity !== "string" ||
477
+ value.executionIdentity.length === 0 ||
478
+ value.executionIdentity.length > 128)) ||
479
+ (value.machineIdentity !== undefined &&
480
+ value.machineIdentity !== null &&
481
+ (typeof value.machineIdentity !== "string" ||
482
+ value.machineIdentity.length === 0 ||
483
+ value.machineIdentity.length > 256)) ||
484
+ (value.processNamespace !== undefined &&
485
+ value.processNamespace !== null &&
486
+ (typeof value.processNamespace !== "string" ||
487
+ !LINUX_PID_NAMESPACE_PATTERN.test(value.processNamespace))) ||
488
+ (value.processIdentity !== null &&
489
+ (typeof value.processIdentity !== "string" ||
490
+ value.processIdentity.length === 0 ||
491
+ value.processIdentity.length > 256))) {
492
+ return undefined;
493
+ }
494
+ if ((value.version === 2 || value.version === 3 || value.version === 4) &&
495
+ (value.processIdentity === null ||
496
+ !isExactProcessIdentity(value.processIdentity) ||
497
+ (/^linux:/u.test(value.processIdentity) &&
498
+ (value.processNamespace === null ||
499
+ value.processNamespace === undefined ||
500
+ !LINUX_PID_NAMESPACE_PATTERN.test(value.processNamespace))))) {
501
+ return undefined;
502
+ }
503
+ if ((value.version === 3 || value.version === 4) &&
504
+ (value.machineIdentity === null ||
505
+ value.machineIdentity === undefined ||
506
+ !/^(darwin|linux|windows):.+$/u.test(value.machineIdentity))) {
507
+ return undefined;
508
+ }
509
+ if (value.version === 4 &&
510
+ (value.executionIdentity === null ||
511
+ value.executionIdentity === undefined ||
512
+ !/^[0-9a-f-]{36}$/iu.test(value.executionIdentity))) {
513
+ return undefined;
514
+ }
515
+ return {
516
+ owner: {
517
+ ...value,
518
+ executionIdentity: value.executionIdentity ?? null,
519
+ machineIdentity: value.machineIdentity ?? null,
520
+ processNamespace: value.processNamespace ?? null,
521
+ },
522
+ publishedAtMs: stats.mtimeMs,
523
+ };
524
+ }
525
+ function parseOwner(lockDirectory) {
526
+ let ownerHandle;
527
+ try {
528
+ ownerHandle = fs.openSync(path.join(String(lockDirectory), OWNER_FILE), fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | fs.constants.O_NOFOLLOW);
529
+ }
530
+ catch (error) {
531
+ return error.code === "ENOENT" ? undefined : null;
532
+ }
533
+ let parsed;
534
+ try {
535
+ parsed = parseOpenedOwner(ownerHandle);
536
+ }
537
+ catch {
538
+ parsed = undefined;
539
+ }
540
+ try {
541
+ fs.closeSync(ownerHandle);
542
+ }
543
+ catch {
544
+ return null;
545
+ }
546
+ return parsed === undefined
547
+ ? null
548
+ : {
549
+ ...parsed,
550
+ lockDirectory: String(lockDirectory),
551
+ };
552
+ }
553
+ function restoreDisplacedPublishedLockDirectory(displacedPath, lockDirectory) {
554
+ try {
555
+ const displaced = fs.lstatSync(displacedPath, { bigint: true });
556
+ const owner = parseOwner(displacedPath);
557
+ if (!displaced.isDirectory() ||
558
+ displaced.isSymbolicLink() ||
559
+ owner === null ||
560
+ owner === undefined) {
561
+ return;
562
+ }
563
+ }
564
+ catch {
565
+ return;
566
+ }
567
+ try {
568
+ fs.lstatSync(lockDirectory);
569
+ return;
570
+ }
571
+ catch (error) {
572
+ if (error.code !== "ENOENT") {
573
+ return;
574
+ }
575
+ }
576
+ try {
577
+ // The coordination claim excludes compliant publishers. If an unexpected
578
+ // published directory races this restore, its owner file makes it nonempty,
579
+ // so rename refuses to replace it and the displaced directory stays preserved.
580
+ fs.renameSync(displacedPath, lockDirectory);
581
+ }
582
+ catch {
583
+ // Preserve the displaced directory at its claim-local path if restoration races.
584
+ }
585
+ }
586
+ function renamedDirectoryMatches(claimedPath, lockDirectory, expectedIdentity) {
587
+ try {
588
+ const claimed = fs.lstatSync(claimedPath, { bigint: true });
589
+ if (claimed.isDirectory() &&
590
+ !claimed.isSymbolicLink() &&
591
+ directoryIdentityMatches(expectedIdentity, claimed)) {
592
+ return true;
593
+ }
594
+ }
595
+ catch {
596
+ // Restore or preserve any directory displaced after the final identity check.
597
+ }
598
+ restoreDisplacedPublishedLockDirectory(claimedPath, lockDirectory);
599
+ return false;
600
+ }
601
+ function abandonedOwnerMarkerPath(lockDirectory, ownerToken) {
602
+ const digest = createHash("sha256").update(ownerToken).digest("hex");
603
+ return path.join(String(lockDirectory), `${ABANDONED_OWNER_PREFIX}${digest}`);
604
+ }
605
+ function hasAbandonedOwnerMarker(candidate) {
606
+ const markerPath = abandonedOwnerMarkerPath(candidate.lockDirectory, candidate.owner.token);
607
+ let markerHandle;
608
+ try {
609
+ markerHandle = fs.openSync(markerPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
610
+ }
611
+ catch {
612
+ return false;
613
+ }
614
+ let matches = false;
615
+ try {
616
+ const stats = fs.fstatSync(markerHandle);
617
+ if (stats.isFile() && stats.size > 0 && stats.size <= 256) {
618
+ const raw = Buffer.alloc(stats.size);
619
+ let offset = 0;
620
+ while (offset < raw.byteLength) {
621
+ const bytesRead = fs.readSync(markerHandle, raw, offset, raw.byteLength - offset, offset);
622
+ if (bytesRead === 0) {
623
+ break;
624
+ }
625
+ offset += bytesRead;
626
+ }
627
+ matches = offset === raw.byteLength && raw.toString("utf8").trim() === candidate.owner.token;
628
+ }
629
+ }
630
+ catch {
631
+ matches = false;
632
+ }
633
+ try {
634
+ fs.closeSync(markerHandle);
635
+ }
636
+ catch {
637
+ return false;
638
+ }
639
+ return matches;
640
+ }
641
+ function publishAbandonedOwnerMarker(lockDirectory, ownerToken) {
642
+ const candidate = parseOwner(lockDirectory);
643
+ if (!candidate || candidate.owner.token !== ownerToken) {
644
+ return false;
645
+ }
646
+ const markerPath = abandonedOwnerMarkerPath(lockDirectory, ownerToken);
647
+ let markerHandle;
648
+ let markerIdentity;
649
+ try {
650
+ markerHandle = fs.openSync(markerPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
651
+ markerIdentity = fs.fstatSync(markerHandle, { bigint: true });
652
+ const contents = Buffer.from(`${ownerToken}\n`);
653
+ let offset = 0;
654
+ while (offset < contents.byteLength) {
655
+ const written = fs.writeSync(markerHandle, contents, offset, contents.byteLength - offset, offset);
656
+ if (written === 0) {
657
+ throw new Error("Recorder lock abandonment publication made no progress.");
658
+ }
659
+ offset += written;
660
+ }
661
+ fs.fsyncSync(markerHandle);
662
+ fs.closeSync(markerHandle);
663
+ markerHandle = undefined;
664
+ }
665
+ catch (error) {
666
+ if (error.code === "EEXIST") {
667
+ return hasAbandonedOwnerMarker(candidate);
668
+ }
669
+ if (markerIdentity !== undefined) {
670
+ try {
671
+ const current = fs.lstatSync(markerPath, { bigint: true });
672
+ if (markerIdentity.dev === current.dev && markerIdentity.ino === current.ino) {
673
+ fs.unlinkSync(markerPath);
674
+ }
675
+ }
676
+ catch {
677
+ // Preserve a replacement marker that cannot be proven to be ours.
678
+ }
679
+ }
680
+ return false;
681
+ }
682
+ finally {
683
+ if (markerHandle !== undefined) {
684
+ try {
685
+ fs.closeSync(markerHandle);
686
+ }
687
+ catch {
688
+ // The failed abandonment publication remains non-authoritative.
689
+ }
690
+ }
691
+ }
692
+ const refreshed = parseOwner(lockDirectory);
693
+ if (!refreshed || refreshed.owner.token !== ownerToken || !hasAbandonedOwnerMarker(refreshed)) {
694
+ try {
695
+ fs.unlinkSync(markerPath);
696
+ }
697
+ catch {
698
+ // A changed marker is ignored unless it matches the current owner token.
699
+ }
700
+ return false;
701
+ }
702
+ return true;
703
+ }
704
+ function hasRecoverableMalformedOwner(lockDirectory) {
705
+ let ownerHandle;
706
+ try {
707
+ ownerHandle = fs.openSync(path.join(String(lockDirectory), OWNER_FILE), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
708
+ }
709
+ catch {
710
+ return false;
711
+ }
712
+ let recoverable = false;
713
+ try {
714
+ const stats = fs.fstatSync(ownerHandle);
715
+ if (!stats.isFile() || stats.size > MAX_OWNER_BYTES) {
716
+ recoverable = false;
717
+ }
718
+ else if (stats.size === 0) {
719
+ recoverable = true;
720
+ }
721
+ else {
722
+ const raw = Buffer.alloc(stats.size);
723
+ let offset = 0;
724
+ while (offset < raw.byteLength) {
725
+ const bytesRead = fs.readSync(ownerHandle, raw, offset, raw.byteLength - offset, offset);
726
+ if (bytesRead === 0) {
727
+ recoverable = true;
728
+ break;
729
+ }
730
+ offset += bytesRead;
731
+ }
732
+ if (offset === raw.byteLength) {
733
+ JSON.parse(raw.toString("utf8"));
734
+ }
735
+ }
736
+ }
737
+ catch (error) {
738
+ recoverable = error instanceof SyntaxError;
739
+ }
740
+ try {
741
+ fs.closeSync(ownerHandle);
742
+ }
743
+ catch {
744
+ return false;
745
+ }
746
+ return recoverable;
747
+ }
748
+ function syntheticFreshStat(stats) {
749
+ const fresh = Object.assign(Object.create(Object.getPrototypeOf(stats)), stats);
750
+ Object.defineProperty(fresh, "mtime", {
751
+ configurable: true,
752
+ enumerable: true,
753
+ value: new Date(),
754
+ });
755
+ return fresh;
756
+ }
757
+ function syntheticStaleStat(stats) {
758
+ const stale = Object.assign(Object.create(Object.getPrototypeOf(stats)), stats);
759
+ Object.defineProperty(stale, "mtime", {
760
+ configurable: true,
761
+ enumerable: true,
762
+ value: new Date(0),
763
+ });
764
+ return stale;
765
+ }
766
+ function statMtimeMs(stats) {
767
+ return typeof stats.mtimeMs === "bigint" ? Number(stats.mtimeMs) : stats.mtimeMs;
768
+ }
769
+ function isRecoverableOwnerlessClaim(stats) {
770
+ return Date.now() - statMtimeMs(stats) >= OWNERLESS_LOCK_RECOVERY_MS;
771
+ }
772
+ function isRecoverableUnverifiableOwner(candidate, status) {
773
+ return (status === "superseded" &&
774
+ candidate.owner.version === 1 &&
775
+ (candidate.owner.processIdentity === null ||
776
+ isCoarseDarwinIdentity(candidate.owner.processIdentity)) &&
777
+ Date.now() - candidate.publishedAtMs >= OWNERLESS_LOCK_RECOVERY_MS);
778
+ }
779
+ function isRecoverableForeignOwner(candidate, status, stats) {
780
+ return (status === "foreign" &&
781
+ candidate.owner.version >= 2 &&
782
+ Date.now() - statMtimeMs(stats) >= OWNERLESS_LOCK_RECOVERY_MS);
783
+ }
784
+ function isRecoverableUnknownOwner(candidate, status, stats) {
785
+ return (status === "unknown" &&
786
+ candidate.owner.version >= 2 &&
787
+ candidate.owner.processIdentity !== null &&
788
+ isExactProcessIdentity(candidate.owner.processIdentity) &&
789
+ Date.now() - candidate.publishedAtMs >= OWNERLESS_LOCK_RECOVERY_MS &&
790
+ Date.now() - statMtimeMs(stats) >= OWNERLESS_LOCK_RECOVERY_MS);
791
+ }
792
+ export function createProcessOwnedLockFileSystem(options = {}) {
793
+ if (process.platform !== "linux" &&
794
+ process.platform !== "darwin" &&
795
+ process.platform !== "win32") {
796
+ return fs;
797
+ }
798
+ const token = randomUUID();
799
+ const removeLockDirectorySync = (directoryPath, expectedIdentity, expectedMtimeMs, afterClaim) => {
800
+ removeVerifiedLockDirectorySync(directoryPath, expectedIdentity, expectedMtimeMs, options.beforeDirectoryRemoval, afterClaim);
801
+ };
802
+ const identityReader = options.processIdentityReader ?? readProcessIdentity;
803
+ let currentProcessIdentity;
804
+ if (options.processIdentityReader) {
805
+ currentProcessIdentity = readProcessIdentityWithRetry(process.pid, identityReader);
806
+ }
807
+ else {
808
+ if (cachedCurrentProcessIdentity === undefined) {
809
+ const currentIdentity = readProcessIdentityWithRetry(process.pid, identityReader);
810
+ if (currentIdentity !== null) {
811
+ cachedCurrentProcessIdentity = currentIdentity;
812
+ }
813
+ }
814
+ currentProcessIdentity = cachedCurrentProcessIdentity ?? null;
815
+ }
816
+ if (currentProcessIdentity === null ||
817
+ (!isExactProcessIdentity(currentProcessIdentity) &&
818
+ !(process.platform === "darwin" && isCoarseDarwinIdentity(currentProcessIdentity)))) {
819
+ throw new Error("Recorder lock process identity is unavailable.");
820
+ }
821
+ let currentMachineIdentity;
822
+ if (options.machineIdentityReader) {
823
+ currentMachineIdentity = options.machineIdentityReader();
824
+ }
825
+ else {
826
+ if (cachedCurrentMachineIdentity === undefined) {
827
+ const machineIdentity = readMachineIdentity();
828
+ if (machineIdentity !== null) {
829
+ cachedCurrentMachineIdentity = machineIdentity;
830
+ }
831
+ }
832
+ currentMachineIdentity = cachedCurrentMachineIdentity ?? null;
833
+ }
834
+ if (currentMachineIdentity === null) {
835
+ throw new Error("Recorder lock machine identity is unavailable.");
836
+ }
837
+ const currentProcessNamespace = readProcessNamespaceWithRetry(process.pid);
838
+ if (process.platform === "linux" && currentProcessNamespace === null) {
839
+ throw new Error("Recorder lock process namespace is unavailable.");
840
+ }
841
+ const ownedDirectories = new Map();
842
+ const interruptedPublicationIdentities = new Map();
843
+ let cachedForeignIdentity;
844
+ const owner = {
845
+ executionIdentity: CURRENT_EXECUTION_IDENTITY,
846
+ machineIdentity: currentMachineIdentity,
847
+ pid: process.pid,
848
+ processIdentity: currentProcessIdentity,
849
+ processNamespace: currentProcessNamespace,
850
+ processStartedAtMs: CURRENT_PROCESS_STARTED_AT_MS,
851
+ token,
852
+ version: isExactProcessIdentity(currentProcessIdentity) ? 4 : 1,
853
+ };
854
+ const publishOwner = (directory) => {
855
+ const ownerPath = path.join(directory, OWNER_FILE);
856
+ let ownerHandle;
857
+ let openedIdentity;
858
+ try {
859
+ ownerHandle = fs.openSync(ownerPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
860
+ openedIdentity = fs.fstatSync(ownerHandle, { bigint: true });
861
+ const contents = Buffer.from(`${JSON.stringify(owner)}\n`);
862
+ let offset = 0;
863
+ while (offset < contents.byteLength) {
864
+ const written = fs.writeSync(ownerHandle, contents, offset, contents.byteLength - offset, offset);
865
+ if (written === 0) {
866
+ throw new Error("Recorder lock owner publication made no progress.");
867
+ }
868
+ offset += written;
869
+ }
870
+ fs.closeSync(ownerHandle);
871
+ ownerHandle = undefined;
872
+ }
873
+ catch (error) {
874
+ if (openedIdentity !== undefined) {
875
+ try {
876
+ const current = fs.lstatSync(ownerPath, { bigint: true });
877
+ if (openedIdentity.dev === current.dev && openedIdentity.ino === current.ino) {
878
+ fs.unlinkSync(ownerPath);
879
+ }
880
+ }
881
+ catch {
882
+ // Preserve a replacement path that cannot be proven to be ours.
883
+ }
884
+ }
885
+ throw error;
886
+ }
887
+ finally {
888
+ if (ownerHandle !== undefined) {
889
+ try {
890
+ fs.closeSync(ownerHandle);
891
+ }
892
+ catch {
893
+ // Preserve the publication error already being reported.
894
+ }
895
+ }
896
+ }
897
+ };
898
+ const abandonOwner = (directory) => {
899
+ abandonedOwnerKeys.add(abandonedOwnerKey(directory, token));
900
+ const published = publishAbandonedOwnerMarker(directory, token);
901
+ const coordinationKey = canonicalLockDirectoryPath(directory);
902
+ const ownedIdentity = ownedDirectories.get(coordinationKey);
903
+ if (!ownedIdentity) {
904
+ return published;
905
+ }
906
+ try {
907
+ const current = fs.lstatSync(directory, { bigint: true });
908
+ if (current.isDirectory() && directoryIdentityMatches(ownedIdentity, current)) {
909
+ abandonedDirectoryIdentities.set(coordinationKey, {
910
+ ...ownedIdentity,
911
+ ownerGenerationKey: token,
912
+ });
913
+ }
914
+ }
915
+ catch {
916
+ // The path no longer names the directory published by this wrapper.
917
+ }
918
+ return published;
919
+ };
920
+ const ownerStatus = (candidate, forceIdentityRefresh = false) => {
921
+ if (!candidate) {
922
+ return "unknown";
923
+ }
924
+ const claim = candidate.owner;
925
+ if (abandonedOwnerKeys.has(abandonedOwnerKey(candidate.lockDirectory, claim.token)) ||
926
+ hasAbandonedOwnerMarker(candidate)) {
927
+ return "dead";
928
+ }
929
+ if (claim.version >= 3 &&
930
+ claim.machineIdentity !== null &&
931
+ claim.machineIdentity !== currentMachineIdentity) {
932
+ return "foreign";
933
+ }
934
+ if (process.platform === "linux" &&
935
+ claim.version >= 2 &&
936
+ (claim.processNamespace === null ||
937
+ currentProcessNamespace === null ||
938
+ claim.processNamespace !== currentProcessNamespace)) {
939
+ return "foreign";
940
+ }
941
+ if (claim.version === 2 && (process.platform === "darwin" || process.platform === "win32")) {
942
+ return "foreign";
943
+ }
944
+ if (!isProcessAlive(claim.pid)) {
945
+ if (process.platform === "linux" && claim.version === 1 && claim.processNamespace === null) {
946
+ return "unknown";
947
+ }
948
+ return "dead";
949
+ }
950
+ if (isDefunctProcess(claim.pid)) {
951
+ return "dead";
952
+ }
953
+ if (claim.pid === process.pid) {
954
+ if (claim.processIdentity !== null && isExactProcessIdentity(claim.processIdentity)) {
955
+ if (claim.processIdentity !== currentProcessIdentity) {
956
+ return "dead";
957
+ }
958
+ return claim.version === 4 &&
959
+ claim.executionIdentity !== null &&
960
+ claim.executionIdentity !== CURRENT_EXECUTION_IDENTITY
961
+ ? "foreign"
962
+ : "active";
963
+ }
964
+ if (claim.processStartedAtMs !== CURRENT_PROCESS_STARTED_AT_MS) {
965
+ return "superseded";
966
+ }
967
+ return claim.executionIdentity !== null &&
968
+ claim.executionIdentity !== CURRENT_EXECUTION_IDENTITY
969
+ ? "superseded"
970
+ : "active";
971
+ }
972
+ const now = Date.now();
973
+ if (forceIdentityRefresh ||
974
+ !cachedForeignIdentity ||
975
+ cachedForeignIdentity.pid !== claim.pid ||
976
+ cachedForeignIdentity.ownerGenerationKey !== claim.token ||
977
+ now - cachedForeignIdentity.checkedAt >= IDENTITY_CACHE_MS) {
978
+ const observedIdentity = identityReader(claim.pid);
979
+ cachedForeignIdentity = {
980
+ checkedAt: now,
981
+ identity: observedIdentity,
982
+ ownerGenerationKey: claim.token,
983
+ pid: claim.pid,
984
+ startedAtMs: processStartedAtMsFromIdentity(observedIdentity) ??
985
+ readLegacyProcessStartedAtMs(claim.pid),
986
+ };
987
+ }
988
+ if (claim.processIdentity === null ||
989
+ isCoarseDarwinIdentity(claim.processIdentity) ||
990
+ !isExactProcessIdentity(claim.processIdentity)) {
991
+ if (claim.version !== 1 || cachedForeignIdentity.startedAtMs === null) {
992
+ return "unknown";
993
+ }
994
+ return legacyProcessStartMatches(claim.processStartedAtMs, cachedForeignIdentity.startedAtMs)
995
+ ? "active"
996
+ : "superseded";
997
+ }
998
+ if (cachedForeignIdentity.identity === null) {
999
+ return "unknown";
1000
+ }
1001
+ if (process.platform === "darwin" && isCoarseDarwinIdentity(cachedForeignIdentity.identity)) {
1002
+ return cachedForeignIdentity.startedAtMs === null
1003
+ ? "unknown"
1004
+ : legacyProcessStartMatches(claim.processStartedAtMs, cachedForeignIdentity.startedAtMs)
1005
+ ? "active"
1006
+ : "superseded";
1007
+ }
1008
+ return cachedForeignIdentity.identity === claim.processIdentity ? "active" : "dead";
1009
+ };
1010
+ const createRecoveryClaim = (claimPath, callback) => {
1011
+ fs.mkdir(claimPath, { mode: 0o700 }, (claimError) => {
1012
+ if (!claimError) {
1013
+ let createdIdentity;
1014
+ try {
1015
+ const created = fs.lstatSync(claimPath, { bigint: true });
1016
+ if (!created.isDirectory() || created.isSymbolicLink()) {
1017
+ throw lockCleanupError("Recorder lock recovery claim changed during creation.");
1018
+ }
1019
+ createdIdentity = directoryIdentity(created);
1020
+ publishOwner(claimPath);
1021
+ const published = verifiedLockDirectoryStats(claimPath, createdIdentity);
1022
+ if (!published) {
1023
+ throw lockCleanupError("Recorder lock recovery claim disappeared during publication.");
1024
+ }
1025
+ callback(null, {
1026
+ activeIdentity: directoryIdentity(published),
1027
+ activePath: claimPath,
1028
+ ownerGenerationKey: token,
1029
+ supersededPaths: [],
1030
+ });
1031
+ }
1032
+ catch (ownerError) {
1033
+ if (createdIdentity) {
1034
+ try {
1035
+ removeLockDirectorySync(claimPath, createdIdentity);
1036
+ }
1037
+ catch {
1038
+ // Preserve an unverified replacement instead of deleting it by pathname.
1039
+ }
1040
+ }
1041
+ callback(ownerError);
1042
+ }
1043
+ return;
1044
+ }
1045
+ if (claimError.code !== "EEXIST") {
1046
+ callback(claimError);
1047
+ return;
1048
+ }
1049
+ let existingFingerprint;
1050
+ let existingIdentity;
1051
+ let existingOwnerFingerprint;
1052
+ try {
1053
+ const initialStats = fs.lstatSync(claimPath, { bigint: true });
1054
+ if (initialStats.isDirectory() && !initialStats.isSymbolicLink()) {
1055
+ const initialIdentity = directoryIdentity(initialStats);
1056
+ const existingClaim = parseOwner(claimPath);
1057
+ const stats = verifiedLockDirectoryStats(claimPath, initialIdentity, initialStats.mtimeMs);
1058
+ if (!stats) {
1059
+ throw lockCleanupError("Recorder lock recovery claim disappeared during inspection.");
1060
+ }
1061
+ if (existingClaim) {
1062
+ const status = ownerStatus(existingClaim);
1063
+ const recoverableForeign = status === "foreign" && isRecoverableForeignOwner(existingClaim, status, stats);
1064
+ const recoverableUnverifiable = isRecoverableUnverifiableOwner(existingClaim, status);
1065
+ const recoverableUnknown = isRecoverableUnknownOwner(existingClaim, status, stats) &&
1066
+ isRecoverableUnknownOwner(existingClaim, ownerStatus(existingClaim, true), stats);
1067
+ if (status === "dead" ||
1068
+ recoverableForeign ||
1069
+ recoverableUnverifiable ||
1070
+ recoverableUnknown) {
1071
+ existingFingerprint = `owner:${existingClaim.owner.token}`;
1072
+ existingOwnerFingerprint = existingFingerprint;
1073
+ }
1074
+ }
1075
+ else if (isRecoverableOwnerlessClaim(stats)) {
1076
+ existingOwnerFingerprint = existingClaim === undefined ? "ownerless" : "malformed";
1077
+ existingFingerprint = `${existingOwnerFingerprint}:${stats.dev}:${stats.ino}:${stats.mtimeMs}`;
1078
+ }
1079
+ if (existingFingerprint) {
1080
+ existingIdentity = {
1081
+ ...directoryIdentity(stats),
1082
+ mtimeMs: stats.mtimeMs,
1083
+ };
1084
+ }
1085
+ }
1086
+ }
1087
+ catch {
1088
+ // Treat a concurrently changing claim as active.
1089
+ }
1090
+ if (!existingFingerprint || !existingIdentity || !existingOwnerFingerprint) {
1091
+ callback(Object.assign(new Error("Recorder lock recovery is already in progress."), {
1092
+ code: "ELOCKED",
1093
+ }));
1094
+ return;
1095
+ }
1096
+ const takeoverPath = recoveryClaimPath(claimPath, existingFingerprint);
1097
+ createRecoveryClaim(takeoverPath, (takeoverError, takeoverClaim) => {
1098
+ if (takeoverError || !takeoverClaim) {
1099
+ callback(takeoverError ??
1100
+ Object.assign(new Error("Recorder lock recovery claim failed."), {
1101
+ code: "ELOCKED",
1102
+ }));
1103
+ }
1104
+ else {
1105
+ callback(null, {
1106
+ activeIdentity: takeoverClaim.activeIdentity,
1107
+ activePath: takeoverClaim.activePath,
1108
+ ownerGenerationKey: takeoverClaim.ownerGenerationKey,
1109
+ supersededPaths: [
1110
+ {
1111
+ identity: existingIdentity,
1112
+ ownerFingerprint: existingOwnerFingerprint,
1113
+ path: claimPath,
1114
+ },
1115
+ ...takeoverClaim.supersededPaths,
1116
+ ],
1117
+ });
1118
+ }
1119
+ });
1120
+ });
1121
+ };
1122
+ const releaseRecoveryClaim = (directory, claim, callback) => {
1123
+ const coordinationKey = canonicalLockDirectoryPath(directory);
1124
+ // Retire nested takeover paths before the base claim so any failure leaves
1125
+ // the active claim discoverable through the original coordination path.
1126
+ const supersededPaths = [...claim.supersededPaths].reverse();
1127
+ let removedSupersededPath = false;
1128
+ const removeActiveClaim = () => {
1129
+ let activeError = null;
1130
+ try {
1131
+ removeLockDirectorySync(claim.activePath, claim.activeIdentity, undefined, (claimedPath) => {
1132
+ claim.activePath = claimedPath;
1133
+ });
1134
+ }
1135
+ catch (error) {
1136
+ activeError = error;
1137
+ }
1138
+ if (activeError && !abandonOwner(claim.activePath)) {
1139
+ retainedCoordinationClaims.set(coordinationKey, {
1140
+ claim,
1141
+ retainedByToken: token,
1142
+ });
1143
+ }
1144
+ callback();
1145
+ };
1146
+ const preserveActiveClaim = () => {
1147
+ if (!abandonOwner(claim.activePath)) {
1148
+ retainedCoordinationClaims.set(coordinationKey, {
1149
+ claim,
1150
+ retainedByToken: token,
1151
+ });
1152
+ }
1153
+ callback();
1154
+ };
1155
+ const removeSuperseded = (index) => {
1156
+ const superseded = supersededPaths[index];
1157
+ if (!superseded) {
1158
+ removeActiveClaim();
1159
+ return;
1160
+ }
1161
+ let current;
1162
+ try {
1163
+ current = verifiedLockDirectoryStats(superseded.path, superseded.identity, superseded.identity.mtimeMs);
1164
+ }
1165
+ catch {
1166
+ if (!removedSupersededPath) {
1167
+ preserveActiveClaim();
1168
+ return;
1169
+ }
1170
+ removeActiveClaim();
1171
+ return;
1172
+ }
1173
+ if (!current) {
1174
+ removedSupersededPath = true;
1175
+ removeSuperseded(index + 1);
1176
+ return;
1177
+ }
1178
+ const currentOwner = parseOwner(superseded.path);
1179
+ try {
1180
+ current = verifiedLockDirectoryStats(superseded.path, superseded.identity, superseded.identity.mtimeMs);
1181
+ }
1182
+ catch {
1183
+ if (!removedSupersededPath) {
1184
+ preserveActiveClaim();
1185
+ return;
1186
+ }
1187
+ removeActiveClaim();
1188
+ return;
1189
+ }
1190
+ if (!current) {
1191
+ removedSupersededPath = true;
1192
+ removeSuperseded(index + 1);
1193
+ return;
1194
+ }
1195
+ const currentFingerprint = currentOwner
1196
+ ? `owner:${currentOwner.owner.token}`
1197
+ : currentOwner === undefined
1198
+ ? "ownerless"
1199
+ : "malformed";
1200
+ if (currentFingerprint !== superseded.ownerFingerprint) {
1201
+ if (!removedSupersededPath) {
1202
+ preserveActiveClaim();
1203
+ return;
1204
+ }
1205
+ removeActiveClaim();
1206
+ return;
1207
+ }
1208
+ try {
1209
+ removeLockDirectorySync(superseded.path, superseded.identity, superseded.identity.mtimeMs);
1210
+ }
1211
+ catch {
1212
+ if (!removedSupersededPath) {
1213
+ preserveActiveClaim();
1214
+ return;
1215
+ }
1216
+ removeActiveClaim();
1217
+ return;
1218
+ }
1219
+ removedSupersededPath = true;
1220
+ removeSuperseded(index + 1);
1221
+ };
1222
+ removeSuperseded(0);
1223
+ };
1224
+ const acquireCoordinationClaim = (directory, callback) => {
1225
+ const coordinationKey = canonicalLockDirectoryPath(directory);
1226
+ const retained = retainedCoordinationClaims.get(coordinationKey);
1227
+ if (retained) {
1228
+ if (retained.retainedByToken !== token && !ownedDirectories.has(coordinationKey)) {
1229
+ callback(lockCleanupError("Recorder lock retained coordination claim is owned elsewhere."));
1230
+ return;
1231
+ }
1232
+ const { claim: retainedClaim } = retained;
1233
+ let retainedClaimIsOwned = false;
1234
+ try {
1235
+ const current = verifiedLockDirectoryStats(retainedClaim.activePath, retainedClaim.activeIdentity);
1236
+ const retainedOwner = current ? parseOwner(retainedClaim.activePath) : undefined;
1237
+ retainedClaimIsOwned =
1238
+ retainedOwner !== undefined &&
1239
+ retainedOwner !== null &&
1240
+ retainedOwner.owner.token === retainedClaim.ownerGenerationKey &&
1241
+ ownerStatus(retainedOwner) === "active";
1242
+ }
1243
+ catch {
1244
+ retainedClaimIsOwned = false;
1245
+ }
1246
+ if (retainedClaimIsOwned) {
1247
+ if (retainedCoordinationClaims.get(coordinationKey) === retained) {
1248
+ retainedCoordinationClaims.delete(coordinationKey);
1249
+ }
1250
+ callback(null, retainedClaim);
1251
+ return;
1252
+ }
1253
+ if (retainedCoordinationClaims.get(coordinationKey) === retained) {
1254
+ retainedCoordinationClaims.delete(coordinationKey);
1255
+ }
1256
+ }
1257
+ createRecoveryClaim(recoveryClaimPath(directory), callback);
1258
+ };
1259
+ const lockFs = Object.create(fs);
1260
+ lockFs.mkdir = ((directoryPath, callback) => {
1261
+ const directory = String(directoryPath);
1262
+ acquireCoordinationClaim(directory, (claimError, coordinationClaim) => {
1263
+ if (claimError || !coordinationClaim) {
1264
+ callback(claimError ??
1265
+ Object.assign(new Error("Recorder lock coordination failed."), {
1266
+ code: "ELOCKED",
1267
+ }));
1268
+ return;
1269
+ }
1270
+ fs.mkdir(directoryPath, (error) => {
1271
+ if (error) {
1272
+ if (error.code === "EEXIST" && coordinationClaim.supersededPaths.length > 0) {
1273
+ try {
1274
+ const existing = fs.lstatSync(directoryPath, { bigint: true });
1275
+ const candidate = parseOwner(directoryPath);
1276
+ if (existing.isDirectory() &&
1277
+ !existing.isSymbolicLink() &&
1278
+ (candidate === undefined || candidate === null)) {
1279
+ const coordinationKey = canonicalLockDirectoryPath(directory);
1280
+ interruptedPublicationIdentities.set(coordinationKey, directoryIdentity(existing));
1281
+ retainedCoordinationClaims.set(coordinationKey, {
1282
+ claim: coordinationClaim,
1283
+ retainedByToken: token,
1284
+ });
1285
+ callback(error);
1286
+ return;
1287
+ }
1288
+ }
1289
+ catch {
1290
+ // Fall through to normal cleanup for a changing target.
1291
+ }
1292
+ }
1293
+ releaseRecoveryClaim(directory, coordinationClaim, () => callback(error));
1294
+ return;
1295
+ }
1296
+ let createdIdentity;
1297
+ let ownerPublished = false;
1298
+ let publicationError = null;
1299
+ try {
1300
+ const createdDirectory = fs.lstatSync(directoryPath, { bigint: true });
1301
+ if (!createdDirectory.isDirectory() || createdDirectory.isSymbolicLink()) {
1302
+ throw new Error("Recorder lock directory changed during creation.");
1303
+ }
1304
+ const candidateIdentity = directoryIdentity(createdDirectory);
1305
+ const createdEntries = fs.readdirSync(directoryPath);
1306
+ if (createdEntries.length > 0 ||
1307
+ !verifiedLockDirectoryStats(directoryPath, candidateIdentity)) {
1308
+ throw lockCleanupError("Recorder lock directory changed during creation.");
1309
+ }
1310
+ createdIdentity = candidateIdentity;
1311
+ publishOwner(directory);
1312
+ ownerPublished = true;
1313
+ const publishedDirectory = verifiedLockDirectoryStats(directory, createdIdentity);
1314
+ if (!publishedDirectory) {
1315
+ throw new Error("Recorder lock directory changed during owner publication.");
1316
+ }
1317
+ const publishedIdentity = directoryIdentity(publishedDirectory);
1318
+ options.onDirectoryOwned?.(directory, { ...publishedIdentity });
1319
+ ownedDirectories.set(canonicalLockDirectoryPath(directory), publishedIdentity);
1320
+ interruptedPublicationIdentities.delete(canonicalLockDirectoryPath(directory));
1321
+ }
1322
+ catch (ownerError) {
1323
+ publicationError = ownerError;
1324
+ }
1325
+ if (publicationError) {
1326
+ if (ownerPublished && createdIdentity) {
1327
+ try {
1328
+ if (verifiedLockDirectoryStats(directory, createdIdentity)) {
1329
+ abandonOwner(directory);
1330
+ }
1331
+ }
1332
+ catch {
1333
+ // Preserve an unverified replacement instead of mutating it by pathname.
1334
+ }
1335
+ }
1336
+ else if (createdIdentity) {
1337
+ try {
1338
+ removeLockDirectorySync(directoryPath, createdIdentity);
1339
+ }
1340
+ catch {
1341
+ // Preserve an unverified replacement instead of deleting it by pathname.
1342
+ }
1343
+ }
1344
+ releaseRecoveryClaim(directory, coordinationClaim, () => callback(publicationError));
1345
+ return;
1346
+ }
1347
+ releaseRecoveryClaim(directory, coordinationClaim, () => callback(null));
1348
+ });
1349
+ });
1350
+ });
1351
+ lockFs.stat = ((filePath, callback) => {
1352
+ fs.stat(filePath, (error, stats) => {
1353
+ if (error) {
1354
+ callback(error);
1355
+ return;
1356
+ }
1357
+ const coordinationKey = canonicalLockDirectoryPath(filePath);
1358
+ let identityStats;
1359
+ try {
1360
+ const current = fs.lstatSync(filePath, { bigint: true });
1361
+ if (current.isDirectory() && !current.isSymbolicLink()) {
1362
+ identityStats = current;
1363
+ }
1364
+ }
1365
+ catch {
1366
+ // A changing path cannot match an identity-based recovery fence.
1367
+ }
1368
+ if (identityStats &&
1369
+ directoryIdentityMatches(abandonedDirectoryIdentities.get(coordinationKey), identityStats)) {
1370
+ callback(null, syntheticStaleStat(stats));
1371
+ return;
1372
+ }
1373
+ const candidate = parseOwner(filePath);
1374
+ const status = ownerStatus(candidate);
1375
+ if (candidate !== undefined && candidate !== null) {
1376
+ interruptedPublicationIdentities.delete(coordinationKey);
1377
+ }
1378
+ const recoverableInterruptedPublication = (candidate === undefined || candidate === null) &&
1379
+ identityStats !== undefined &&
1380
+ directoryIdentityMatches(interruptedPublicationIdentities.get(coordinationKey), identityStats) &&
1381
+ Date.now() - stats.mtimeMs >= OWNERLESS_LOCK_RECOVERY_MS;
1382
+ const recoverableAgedUnverifiableOwner = isRecoverableOwnerlessClaim(stats) &&
1383
+ (candidate === undefined || (candidate === null && hasRecoverableMalformedOwner(filePath)));
1384
+ const recoverableForeign = candidate !== undefined &&
1385
+ candidate !== null &&
1386
+ isRecoverableForeignOwner(candidate, status, stats);
1387
+ const recoverableUnknown = candidate !== undefined &&
1388
+ candidate !== null &&
1389
+ isRecoverableUnknownOwner(candidate, status, stats);
1390
+ if (candidate &&
1391
+ abandonedOwnerKeys.has(abandonedOwnerKey(candidate.lockDirectory, candidate.owner.token))) {
1392
+ callback(null, syntheticStaleStat(stats));
1393
+ return;
1394
+ }
1395
+ if (candidate && hasAbandonedOwnerMarker(candidate)) {
1396
+ callback(null, syntheticStaleStat(stats));
1397
+ return;
1398
+ }
1399
+ callback(null, candidate?.owner.token === token ||
1400
+ status === "dead" ||
1401
+ recoverableForeign ||
1402
+ recoverableUnknown ||
1403
+ recoverableInterruptedPublication ||
1404
+ recoverableAgedUnverifiableOwner ||
1405
+ (candidate !== undefined &&
1406
+ candidate !== null &&
1407
+ isRecoverableUnverifiableOwner(candidate, status))
1408
+ ? stats
1409
+ : syntheticFreshStat(stats));
1410
+ });
1411
+ });
1412
+ lockFs.rmdir = ((directoryPath, callback) => {
1413
+ const directory = String(directoryPath);
1414
+ const coordinationKey = canonicalLockDirectoryPath(directory);
1415
+ const releaseDeadline = Date.now() + COORDINATION_RELEASE_WAIT_MS;
1416
+ const acquireForRemoval = () => {
1417
+ acquireCoordinationClaim(directory, (claimError, coordinationClaim) => {
1418
+ if (claimError?.code === "ELOCKED" &&
1419
+ ownedDirectories.has(coordinationKey) &&
1420
+ Date.now() < releaseDeadline) {
1421
+ setTimeout(acquireForRemoval, COORDINATION_RELEASE_RETRY_MS);
1422
+ return;
1423
+ }
1424
+ if (claimError || !coordinationClaim) {
1425
+ if (ownedDirectories.has(coordinationKey)) {
1426
+ abandonOwner(directory);
1427
+ }
1428
+ callback(claimError ??
1429
+ Object.assign(new Error("Recorder lock coordination failed."), {
1430
+ code: "ELOCKED",
1431
+ }));
1432
+ return;
1433
+ }
1434
+ const finish = (error) => {
1435
+ releaseRecoveryClaim(directory, coordinationClaim, () => callback(error));
1436
+ };
1437
+ const candidate = parseOwner(directoryPath);
1438
+ let initialStats;
1439
+ try {
1440
+ initialStats = fs.lstatSync(directoryPath, { bigint: true });
1441
+ if (!initialStats.isDirectory() || initialStats.isSymbolicLink()) {
1442
+ throw lockCleanupError("Recorder lock recovery target is not a directory.");
1443
+ }
1444
+ }
1445
+ catch (error) {
1446
+ const pathError = error;
1447
+ finish(pathError.code === "ENOENT" && ownedDirectories.has(coordinationKey)
1448
+ ? lockCleanupError("Recorder lock owned release target disappeared.")
1449
+ : pathError);
1450
+ return;
1451
+ }
1452
+ if (candidate !== undefined && candidate !== null) {
1453
+ interruptedPublicationIdentities.delete(coordinationKey);
1454
+ }
1455
+ const ownedIdentity = ownedDirectories.get(coordinationKey);
1456
+ const ownsPublishedDirectory = directoryIdentityMatches(ownedIdentity, initialStats);
1457
+ if ((candidate?.owner.token === token && ownsPublishedDirectory) ||
1458
+ ((candidate === null || candidate === undefined) && ownsPublishedDirectory)) {
1459
+ const tombstonePath = `${coordinationClaim.activePath}.${token}.release`;
1460
+ fs.rename(directoryPath, tombstonePath, (renameError) => {
1461
+ if (renameError) {
1462
+ abandonOwner(directory);
1463
+ finish(renameError);
1464
+ return;
1465
+ }
1466
+ if (!renamedDirectoryMatches(tombstonePath, directory, directoryIdentity(initialStats))) {
1467
+ finish(lockCleanupError("Recorder lock release displaced a replacement directory."));
1468
+ return;
1469
+ }
1470
+ ownedDirectories.delete(coordinationKey);
1471
+ abandonedDirectoryIdentities.delete(coordinationKey);
1472
+ abandonedOwnerKeys.delete(abandonedOwnerKey(directory, token));
1473
+ try {
1474
+ removeLockDirectorySync(tombstonePath, directoryIdentity(initialStats));
1475
+ finish(null);
1476
+ }
1477
+ catch (error) {
1478
+ finish(error);
1479
+ }
1480
+ });
1481
+ return;
1482
+ }
1483
+ if (candidate === null && ownedDirectories.has(coordinationKey)) {
1484
+ abandonOwner(directory);
1485
+ finish(Object.assign(new Error("Recorder lock owner metadata cannot be verified for release."), {
1486
+ code: "ELOCKED",
1487
+ }));
1488
+ return;
1489
+ }
1490
+ let ownerlessStats;
1491
+ let foreignStats;
1492
+ let recoverableAbandoned = false;
1493
+ let recoverableInterruptedPublication = false;
1494
+ let recoverableAgedUnverifiableOwner = false;
1495
+ const status = ownerStatus(candidate);
1496
+ const recoverableUnverifiable = candidate !== undefined &&
1497
+ candidate !== null &&
1498
+ isRecoverableUnverifiableOwner(candidate, status);
1499
+ const recoverableForeign = candidate !== undefined &&
1500
+ candidate !== null &&
1501
+ isRecoverableForeignOwner(candidate, status, initialStats);
1502
+ const recoverableUnknown = candidate !== undefined &&
1503
+ candidate !== null &&
1504
+ isRecoverableUnknownOwner(candidate, status, initialStats);
1505
+ if (recoverableForeign) {
1506
+ foreignStats = initialStats;
1507
+ }
1508
+ if (candidate === undefined || candidate === null) {
1509
+ ownerlessStats = initialStats;
1510
+ recoverableAbandoned = directoryIdentityMatches(abandonedDirectoryIdentities.get(coordinationKey), ownerlessStats);
1511
+ recoverableInterruptedPublication =
1512
+ directoryIdentityMatches(interruptedPublicationIdentities.get(coordinationKey), ownerlessStats) && Date.now() - statMtimeMs(ownerlessStats) >= OWNERLESS_LOCK_RECOVERY_MS;
1513
+ recoverableAgedUnverifiableOwner =
1514
+ isRecoverableOwnerlessClaim(ownerlessStats) &&
1515
+ (candidate === undefined ||
1516
+ (candidate === null && hasRecoverableMalformedOwner(directoryPath)));
1517
+ }
1518
+ if (status !== "dead" &&
1519
+ !recoverableForeign &&
1520
+ !recoverableUnknown &&
1521
+ !recoverableUnverifiable &&
1522
+ !recoverableAbandoned &&
1523
+ !recoverableInterruptedPublication &&
1524
+ !recoverableAgedUnverifiableOwner) {
1525
+ finish(Object.assign(new Error("Recorder lock owner is still active or cannot be verified."), {
1526
+ code: "ELOCKED",
1527
+ }));
1528
+ return;
1529
+ }
1530
+ const refreshed = parseOwner(directoryPath);
1531
+ const refreshedStatus = ownerStatus(refreshed, recoverableUnknown);
1532
+ let refreshedStats;
1533
+ try {
1534
+ const current = fs.lstatSync(directoryPath, { bigint: true });
1535
+ if (current.isDirectory() &&
1536
+ !current.isSymbolicLink() &&
1537
+ current.dev === initialStats.dev &&
1538
+ current.ino === initialStats.ino &&
1539
+ current.mtimeMs === initialStats.mtimeMs) {
1540
+ refreshedStats = current;
1541
+ }
1542
+ }
1543
+ catch {
1544
+ // A changed recovery target fails authorization below.
1545
+ }
1546
+ let recoveryStillAuthorized = candidate !== undefined &&
1547
+ candidate !== null &&
1548
+ refreshedStats !== undefined &&
1549
+ lockOwnerMatches(candidate, refreshed) &&
1550
+ (refreshedStatus === "dead" ||
1551
+ (recoverableForeign &&
1552
+ refreshed !== undefined &&
1553
+ refreshed !== null &&
1554
+ foreignStats !== undefined &&
1555
+ refreshedStats.dev === foreignStats.dev &&
1556
+ refreshedStats.ino === foreignStats.ino &&
1557
+ refreshedStats.mtimeMs === foreignStats.mtimeMs &&
1558
+ isRecoverableForeignOwner(refreshed, refreshedStatus, refreshedStats)) ||
1559
+ (recoverableUnknown &&
1560
+ refreshed !== undefined &&
1561
+ refreshed !== null &&
1562
+ isRecoverableUnknownOwner(refreshed, refreshedStatus, refreshedStats)) ||
1563
+ (refreshed !== undefined &&
1564
+ refreshed !== null &&
1565
+ isRecoverableUnverifiableOwner(refreshed, refreshedStatus)));
1566
+ if (recoverableAbandoned && ownerlessStats) {
1567
+ try {
1568
+ recoveryStillAuthorized =
1569
+ refreshedStats !== undefined &&
1570
+ directoryIdentityMatches(abandonedDirectoryIdentities.get(coordinationKey), refreshedStats);
1571
+ }
1572
+ catch {
1573
+ recoveryStillAuthorized = false;
1574
+ }
1575
+ }
1576
+ if (recoverableInterruptedPublication && ownerlessStats) {
1577
+ try {
1578
+ recoveryStillAuthorized =
1579
+ refreshedStats !== undefined &&
1580
+ refreshed === undefined &&
1581
+ refreshedStats.dev === ownerlessStats.dev &&
1582
+ refreshedStats.ino === ownerlessStats.ino &&
1583
+ refreshedStats.mtimeMs === ownerlessStats.mtimeMs &&
1584
+ directoryIdentityMatches(interruptedPublicationIdentities.get(coordinationKey), refreshedStats) &&
1585
+ Date.now() - statMtimeMs(refreshedStats) >= OWNERLESS_LOCK_RECOVERY_MS;
1586
+ }
1587
+ catch {
1588
+ recoveryStillAuthorized = false;
1589
+ }
1590
+ }
1591
+ if (recoverableAgedUnverifiableOwner && ownerlessStats) {
1592
+ try {
1593
+ recoveryStillAuthorized =
1594
+ refreshedStats !== undefined &&
1595
+ (candidate === undefined ? refreshed === undefined : refreshed === null) &&
1596
+ (candidate !== null || hasRecoverableMalformedOwner(directoryPath)) &&
1597
+ refreshedStats.dev === ownerlessStats.dev &&
1598
+ refreshedStats.ino === ownerlessStats.ino &&
1599
+ refreshedStats.mtimeMs === ownerlessStats.mtimeMs &&
1600
+ isRecoverableOwnerlessClaim(refreshedStats);
1601
+ }
1602
+ catch {
1603
+ recoveryStillAuthorized = false;
1604
+ }
1605
+ }
1606
+ if (!recoveryStillAuthorized) {
1607
+ finish(Object.assign(new Error("Recorder lock owner changed during recovery."), {
1608
+ code: "ELOCKED",
1609
+ }));
1610
+ return;
1611
+ }
1612
+ const tombstonePath = `${coordinationClaim.activePath}.${token}.lock`;
1613
+ fs.rename(directoryPath, tombstonePath, (renameError) => {
1614
+ if (renameError) {
1615
+ finish(renameError);
1616
+ return;
1617
+ }
1618
+ if (!renamedDirectoryMatches(tombstonePath, directory, directoryIdentity(initialStats))) {
1619
+ finish(lockCleanupError("Recorder lock recovery displaced a replacement directory."));
1620
+ return;
1621
+ }
1622
+ try {
1623
+ removeLockDirectorySync(tombstonePath, directoryIdentity(initialStats));
1624
+ if (candidate) {
1625
+ abandonedOwnerKeys.delete(abandonedOwnerKey(candidate.lockDirectory, candidate.owner.token));
1626
+ }
1627
+ interruptedPublicationIdentities.delete(coordinationKey);
1628
+ const abandonedIdentity = abandonedDirectoryIdentities.get(coordinationKey);
1629
+ if (abandonedIdentity) {
1630
+ abandonedOwnerKeys.delete(abandonedOwnerKey(directory, abandonedIdentity.ownerGenerationKey));
1631
+ abandonedDirectoryIdentities.delete(coordinationKey);
1632
+ }
1633
+ retainedCoordinationClaims.set(canonicalLockDirectoryPath(directory), {
1634
+ claim: coordinationClaim,
1635
+ retainedByToken: token,
1636
+ });
1637
+ callback(null);
1638
+ }
1639
+ catch (error) {
1640
+ finish(error);
1641
+ }
1642
+ });
1643
+ });
1644
+ };
1645
+ acquireForRemoval();
1646
+ });
1647
+ lockFs.rmdirSync = ((directoryPath) => {
1648
+ const directory = String(directoryPath);
1649
+ const claimPath = recoveryClaimPath(directory);
1650
+ fs.mkdirSync(claimPath, { mode: 0o700 });
1651
+ const createdClaim = fs.lstatSync(claimPath, { bigint: true });
1652
+ if (!createdClaim.isDirectory() || createdClaim.isSymbolicLink()) {
1653
+ throw lockCleanupError("Recorder lock recovery claim changed during creation.");
1654
+ }
1655
+ const coordinationClaim = {
1656
+ activeIdentity: directoryIdentity(createdClaim),
1657
+ activePath: claimPath,
1658
+ ownerGenerationKey: token,
1659
+ supersededPaths: [],
1660
+ };
1661
+ try {
1662
+ publishOwner(claimPath);
1663
+ if (!verifiedLockDirectoryStats(claimPath, coordinationClaim.activeIdentity)) {
1664
+ throw lockCleanupError("Recorder lock recovery claim disappeared during publication.");
1665
+ }
1666
+ const candidate = parseOwner(directoryPath);
1667
+ if (candidate?.owner.token !== token) {
1668
+ throw Object.assign(new Error("Recorder lock is not owned by this process."), {
1669
+ code: "ELOCKED",
1670
+ });
1671
+ }
1672
+ const ownedIdentity = ownedDirectories.get(canonicalLockDirectoryPath(directory));
1673
+ const ownedDirectory = fs.lstatSync(directoryPath, { bigint: true });
1674
+ if (!ownedDirectory.isDirectory() ||
1675
+ ownedDirectory.isSymbolicLink() ||
1676
+ !directoryIdentityMatches(ownedIdentity, ownedDirectory)) {
1677
+ throw lockCleanupError("Recorder lock ownership changed before release.");
1678
+ }
1679
+ const tombstonePath = `${claimPath}.${token}.release`;
1680
+ fs.renameSync(directoryPath, tombstonePath);
1681
+ if (!renamedDirectoryMatches(tombstonePath, directory, directoryIdentity(ownedDirectory))) {
1682
+ throw lockCleanupError("Recorder lock release displaced a replacement directory.");
1683
+ }
1684
+ removeLockDirectorySync(tombstonePath, directoryIdentity(ownedDirectory));
1685
+ ownedDirectories.delete(canonicalLockDirectoryPath(directory));
1686
+ }
1687
+ finally {
1688
+ try {
1689
+ removeLockDirectorySync(coordinationClaim.activePath, coordinationClaim.activeIdentity);
1690
+ }
1691
+ catch {
1692
+ // Exit cleanup is best-effort; a live coordination claim is safer than an ABA race.
1693
+ }
1694
+ }
1695
+ });
1696
+ return lockFs;
1697
+ }
1698
+ //# sourceMappingURL=process-owned-lock.js.map