@evomap/evolver-core 2.0.0-beta.19 → 2.0.0-beta.22
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.
- package/assets/gep/genes.jsonl +5 -5
- package/dist/algo/bans.d.ts +10 -1
- package/dist/algo/bans.js +56 -6
- package/dist/algo/candidateAssembly.d.ts +25 -2
- package/dist/algo/candidateAssembly.js +143 -20
- package/dist/algo/capabilityCandidates.js +10 -0
- package/dist/algo/cycleEngine.d.ts +40 -5
- package/dist/algo/cycleEngine.js +277 -37
- package/dist/algo/evolutionEvent.d.ts +26 -0
- package/dist/algo/evolutionEvent.js +32 -0
- package/dist/algo/exploration.d.ts +7 -0
- package/dist/algo/exploration.js +16 -3
- package/dist/algo/geneHealth.d.ts +34 -1
- package/dist/algo/geneHealth.js +42 -0
- package/dist/algo/geneIntake.d.ts +46 -1
- package/dist/algo/geneIntake.js +121 -10
- package/dist/algo/geneSelection.d.ts +115 -6
- package/dist/algo/geneSelection.js +459 -32
- package/dist/algo/index.d.ts +4 -1
- package/dist/algo/index.js +4 -1
- package/dist/algo/kautoProjection.d.ts +41 -0
- package/dist/algo/kautoProjection.js +95 -0
- package/dist/algo/kautoValidator.d.ts +68 -0
- package/dist/algo/kautoValidator.js +256 -0
- package/dist/algo/orchestrator.d.ts +14 -1
- package/dist/algo/orchestrator.js +9 -2
- package/dist/algo/solidify.d.ts +11 -2
- package/dist/algo/solidify.js +37 -7
- package/dist/algo/ucb1.d.ts +53 -0
- package/dist/algo/ucb1.js +156 -0
- package/dist/assetrepair/hubRejection.d.ts +12 -0
- package/dist/assetrepair/hubRejection.js +109 -0
- package/dist/assetrepair/index.d.ts +2 -0
- package/dist/assetrepair/index.js +2 -0
- package/dist/assetrepair/repair.d.ts +33 -0
- package/dist/assetrepair/repair.js +155 -0
- package/dist/assetstore/assetSidecarRecords.js +5 -1
- package/dist/assetstore/assetStoreHealth.d.ts +2 -3
- package/dist/assetstore/assetStoreHealth.js +4 -10
- package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
- package/dist/assetstore/foreignJsonlSource.js +150 -0
- package/dist/assetstore/index.d.ts +3 -0
- package/dist/assetstore/index.js +3 -0
- package/dist/assetstore/learningHistory.js +3 -3
- package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
- package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
- package/dist/assetstore/localJsonl.d.ts +8 -0
- package/dist/assetstore/localJsonl.js +128 -3
- package/dist/assetstore/provenance.d.ts +38 -3
- package/dist/assetstore/provenance.js +103 -45
- package/dist/assetstore/provider.d.ts +4 -0
- package/dist/assetstore/reviewFilter.d.ts +19 -1
- package/dist/assetstore/reviewFilter.js +36 -0
- package/dist/assetstore/unionReadStore.d.ts +25 -0
- package/dist/assetstore/unionReadStore.js +119 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +4 -3
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +4 -3
- package/dist/benchmark/index.d.ts +2 -1
- package/dist/benchmark/index.js +2 -1
- package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
- package/dist/benchmark/selectionFlatAbstention.js +481 -0
- package/dist/bootstrap/envFingerprint.d.ts +9 -0
- package/dist/bootstrap/envFingerprint.js +5 -0
- package/dist/bootstrap/index.d.ts +2 -1
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
- package/dist/bootstrap/lifecycleBootstrap.js +433 -0
- package/dist/bootstrap/v1EnvCompat.d.ts +3 -0
- package/dist/bootstrap/v1EnvCompat.js +48 -1
- package/dist/events/eventArchive.d.ts +2 -0
- package/dist/events/eventArchive.js +13 -3
- package/dist/events/eventSchema.d.ts +7 -7
- package/dist/events/eventStore.d.ts +2 -0
- package/dist/events/eventStore.js +5 -1
- package/dist/events/ingest.d.ts +2 -1
- package/dist/events/ingest.js +6 -0
- package/dist/exec/autoExec.d.ts +30 -3
- package/dist/exec/autoExec.js +294 -29
- package/dist/exec/autonomousCycle.d.ts +7 -0
- package/dist/exec/autonomousCycle.js +2 -0
- package/dist/exec/claudeBridge.d.ts +28 -6
- package/dist/exec/claudeBridge.js +411 -258
- package/dist/exec/executionBinding.d.ts +414 -0
- package/dist/exec/executionBinding.js +588 -0
- package/dist/exec/index.d.ts +1 -0
- package/dist/exec/index.js +1 -0
- package/dist/exec/proofOfWork.d.ts +1 -1
- package/dist/exec/proofOfWork.js +2 -2
- package/dist/exec/runnerRegistry.d.ts +5 -0
- package/dist/exec/runnerRegistry.js +54 -14
- package/dist/hub/capability.d.ts +40 -1
- package/dist/hub/conversationDistiller.d.ts +19 -0
- package/dist/hub/conversationDistiller.js +115 -37
- package/dist/hub/fake.d.ts +1 -0
- package/dist/hub/fake.js +1 -0
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/hub/recipeCompose.d.ts +27 -0
- package/dist/hub/recipeCompose.js +90 -0
- package/dist/hub/sanitize.js +119 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mailbox/ipcServer.d.ts +2 -0
- package/dist/mailbox/ipcServer.js +20 -0
- package/dist/mailbox/store.d.ts +52 -2
- package/dist/mailbox/store.js +546 -81
- package/dist/modelCompatibility.d.ts +164 -0
- package/dist/modelCompatibility.js +309 -0
- package/dist/observers/valueDigestObserver.d.ts +9 -0
- package/dist/observers/valueDigestObserver.js +35 -2
- package/dist/ops/cleanup.js +1 -1
- package/dist/ops/evolutionGraphProjection.d.ts +20 -0
- package/dist/ops/evolutionGraphProjection.js +315 -0
- package/dist/ops/index.d.ts +2 -1
- package/dist/ops/index.js +2 -1
- package/dist/ops/selfUpdate.d.ts +8 -0
- package/dist/ops/selfUpdate.js +24 -8
- package/dist/ops/valueOutreach.d.ts +3 -1
- package/dist/ops/valueOutreach.js +5 -1
- package/dist/personality/schema.d.ts +18 -18
- package/dist/schema/evolutionGraph.d.ts +784 -0
- package/dist/schema/evolutionGraph.js +187 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/proofOfWork.d.ts +125 -6
- package/dist/schema/proofOfWork.js +102 -4
- package/dist/shadow/shadowHub.js +1 -0
- package/dist/signals/expand.d.ts +15 -1
- package/dist/signals/expand.js +148 -0
- package/dist/signals/index.d.ts +3 -1
- package/dist/signals/index.js +3 -1
- package/dist/signals/scopeVocabulary.d.ts +75 -0
- package/dist/signals/scopeVocabulary.js +91 -0
- package/dist/signals/taskDomain.d.ts +22 -0
- package/dist/signals/taskDomain.js +43 -0
- package/dist/trace/learningTrace.d.ts +7 -0
- package/dist/trace/learningTrace.js +22 -0
- package/dist/trace/trajectory.d.ts +8 -0
- package/dist/trace/trajectory.js +14 -2
- package/dist/util/fileLock.d.ts +92 -5
- package/dist/util/fileLock.js +809 -70
- package/dist/util/index.d.ts +2 -1
- package/dist/util/index.js +1 -1
- package/dist/verify/index.d.ts +2 -1
- package/dist/verify/index.js +1 -1
- package/dist/verify/sandboxRunner.d.ts +2 -0
- package/dist/verify/sandboxRunner.js +154 -10
- package/dist/verify/sandboxedValidation.d.ts +7 -2
- package/dist/verify/sandboxedValidation.js +102 -24
- package/dist/verify/validation.d.ts +24 -3
- package/dist/verify/validation.js +141 -24
- package/dist/wire/geneHints.d.ts +88 -16
- package/dist/wire/geneHints.js +124 -15
- package/dist/wire/index.d.ts +8 -3
- package/dist/wire/index.js +2 -2
- package/dist/wire/schemaGate.d.ts +21 -0
- package/dist/wire/schemaGate.js +119 -10
- package/package.json +5 -4
package/dist/util/fileLock.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { closeSync, constants, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, openSync, readSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { basename, dirname, join, resolve, win32 } from 'node:path';
|
|
4
5
|
export function syncSleep(ms) {
|
|
5
6
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
6
7
|
}
|
|
7
8
|
const MALFORMED_LOCK_GRACE_NS = 1000000000n;
|
|
9
|
+
const OBSERVED_PROCESS_IDENTITY_CACHE_NS = 5000000000n;
|
|
10
|
+
const PROCESS_IDENTITY_TIMEOUT_MS = 15_000;
|
|
11
|
+
const HOST_WINDOWS_SYSTEM_ROOT = process.env['SystemRoot']?.trim() || 'C:\\Windows';
|
|
8
12
|
const ownedLocks = new Map();
|
|
9
13
|
const malformedLockObservations = new Map();
|
|
14
|
+
const observedProcessIdentities = new Map();
|
|
15
|
+
let currentProcessIdentity;
|
|
16
|
+
let fileLockTestHooks = {};
|
|
10
17
|
export class LockTimeoutError extends Error {
|
|
11
18
|
code = 'LOCK_TIMEOUT';
|
|
12
19
|
constructor(_lockPath) {
|
|
@@ -28,14 +35,208 @@ function lockKey(lockPath) {
|
|
|
28
35
|
return resolve(lockPath);
|
|
29
36
|
}
|
|
30
37
|
function newOwner() {
|
|
31
|
-
|
|
38
|
+
const processStart = processStartIdentity(process.pid);
|
|
39
|
+
if (processStart === null)
|
|
40
|
+
throw new UnsafeLockPathError('process_identity_unavailable');
|
|
41
|
+
return { pid: process.pid, token: randomUUID(), processStart };
|
|
32
42
|
}
|
|
33
43
|
function serializeOwner(owner) {
|
|
34
|
-
return `${JSON.stringify({
|
|
44
|
+
return `${JSON.stringify({
|
|
45
|
+
v: 2,
|
|
46
|
+
pid: owner.pid,
|
|
47
|
+
token: owner.token,
|
|
48
|
+
processStart: owner.processStart,
|
|
49
|
+
})}\n`;
|
|
50
|
+
}
|
|
51
|
+
function publicOwnerRecord(owner) {
|
|
52
|
+
return {
|
|
53
|
+
pid: owner.pid,
|
|
54
|
+
token: owner.token,
|
|
55
|
+
processStartIdentity: { ...owner.processStart },
|
|
56
|
+
};
|
|
35
57
|
}
|
|
36
58
|
function isErrno(error, code) {
|
|
37
59
|
return typeof error === 'object' && error !== null && error.code === code;
|
|
38
60
|
}
|
|
61
|
+
function validPositiveIntegerText(value) {
|
|
62
|
+
return /^[1-9]\d*$/.test(value);
|
|
63
|
+
}
|
|
64
|
+
function validIdentityText(value, maxLength) {
|
|
65
|
+
if (value.length === 0 || value.length > maxLength)
|
|
66
|
+
return false;
|
|
67
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
68
|
+
const code = value.charCodeAt(index);
|
|
69
|
+
if (code <= 0x1f || code === 0x7f)
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
let cachedLinuxBootId;
|
|
75
|
+
function linuxBootId() {
|
|
76
|
+
if (cachedLinuxBootId !== undefined)
|
|
77
|
+
return cachedLinuxBootId;
|
|
78
|
+
try {
|
|
79
|
+
const value = readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim().toLowerCase();
|
|
80
|
+
cachedLinuxBootId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(value)
|
|
81
|
+
? value
|
|
82
|
+
: null;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
cachedLinuxBootId = null;
|
|
86
|
+
}
|
|
87
|
+
return cachedLinuxBootId;
|
|
88
|
+
}
|
|
89
|
+
function linuxProcessStartTicks(pid) {
|
|
90
|
+
try {
|
|
91
|
+
const raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
92
|
+
const commandEnd = raw.lastIndexOf(')');
|
|
93
|
+
if (commandEnd < 0)
|
|
94
|
+
return null;
|
|
95
|
+
const fields = raw.slice(commandEnd + 1).trim().split(/\s+/);
|
|
96
|
+
const startTicks = fields[19];
|
|
97
|
+
return startTicks !== undefined && validPositiveIntegerText(startTicks) ? startTicks : null;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function linuxProcessStartIdentity(pid) {
|
|
104
|
+
const first = linuxProcessStartTicks(pid);
|
|
105
|
+
const bootId = linuxBootId();
|
|
106
|
+
const second = linuxProcessStartTicks(pid);
|
|
107
|
+
return first !== null && first === second && bootId !== null
|
|
108
|
+
? { source: 'linux-proc', bootId, startTicks: first }
|
|
109
|
+
: null;
|
|
110
|
+
}
|
|
111
|
+
function trustedWindowsPowerShell() {
|
|
112
|
+
if (!win32.isAbsolute(HOST_WINDOWS_SYSTEM_ROOT)
|
|
113
|
+
|| !validIdentityText(HOST_WINDOWS_SYSTEM_ROOT, 1024))
|
|
114
|
+
return null;
|
|
115
|
+
const executable = win32.join(HOST_WINDOWS_SYSTEM_ROOT, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
116
|
+
return existsSync(executable) ? executable : null;
|
|
117
|
+
}
|
|
118
|
+
function windowsProcessStartIdentity(pid) {
|
|
119
|
+
const powershell = trustedWindowsPowerShell();
|
|
120
|
+
if (powershell === null)
|
|
121
|
+
return null;
|
|
122
|
+
const script = [
|
|
123
|
+
`$process = Get-Process -Id ${pid} -ErrorAction Stop`,
|
|
124
|
+
'$ticks = $process.StartTime.ToUniversalTime().Ticks',
|
|
125
|
+
'[Console]::Out.Write($ticks.ToString([Globalization.CultureInfo]::InvariantCulture))',
|
|
126
|
+
].join('; ');
|
|
127
|
+
try {
|
|
128
|
+
const startTimeTicks = execFileSync(powershell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {
|
|
129
|
+
encoding: 'utf8',
|
|
130
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
131
|
+
timeout: PROCESS_IDENTITY_TIMEOUT_MS,
|
|
132
|
+
windowsHide: true,
|
|
133
|
+
}).trim();
|
|
134
|
+
return validPositiveIntegerText(startTimeTicks)
|
|
135
|
+
? { source: 'windows-powershell', startTimeTicks }
|
|
136
|
+
: null;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function darwinProcessStartIdentity(pid) {
|
|
143
|
+
try {
|
|
144
|
+
const startTime = execFileSync('/bin/ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
145
|
+
encoding: 'utf8',
|
|
146
|
+
env: { ...process.env, LANG: 'C', LC_ALL: 'C' },
|
|
147
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
148
|
+
timeout: PROCESS_IDENTITY_TIMEOUT_MS,
|
|
149
|
+
}).trim().replace(/\s+/g, ' ');
|
|
150
|
+
return validIdentityText(startTime, 128)
|
|
151
|
+
? { source: 'darwin-ps', startTime }
|
|
152
|
+
: null;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function nativeProcessStartIdentity(pid) {
|
|
159
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
160
|
+
return null;
|
|
161
|
+
if (process.platform === 'linux')
|
|
162
|
+
return linuxProcessStartIdentity(pid);
|
|
163
|
+
if (process.platform === 'win32')
|
|
164
|
+
return windowsProcessStartIdentity(pid);
|
|
165
|
+
if (process.platform === 'darwin')
|
|
166
|
+
return darwinProcessStartIdentity(pid);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
function processStartIdentity(pid, claimedIdentity) {
|
|
170
|
+
if (pid === process.pid) {
|
|
171
|
+
if (currentProcessIdentity === undefined) {
|
|
172
|
+
const resolved = (fileLockTestHooks.processStartIdentity ?? nativeProcessStartIdentity)(pid);
|
|
173
|
+
if (resolved !== null)
|
|
174
|
+
currentProcessIdentity = resolved;
|
|
175
|
+
return resolved;
|
|
176
|
+
}
|
|
177
|
+
return currentProcessIdentity;
|
|
178
|
+
}
|
|
179
|
+
const now = process.hrtime.bigint();
|
|
180
|
+
const cached = observedProcessIdentities.get(pid);
|
|
181
|
+
if (cached !== undefined
|
|
182
|
+
&& claimedIdentity !== undefined
|
|
183
|
+
&& sameProcessStartIdentity(cached.claimedIdentity, claimedIdentity)
|
|
184
|
+
&& now < cached.expiresAtNs) {
|
|
185
|
+
return cached.identity;
|
|
186
|
+
}
|
|
187
|
+
const identity = (fileLockTestHooks.processStartIdentity ?? nativeProcessStartIdentity)(pid);
|
|
188
|
+
if (claimedIdentity !== undefined) {
|
|
189
|
+
observedProcessIdentities.set(pid, {
|
|
190
|
+
claimedIdentity,
|
|
191
|
+
identity,
|
|
192
|
+
expiresAtNs: now + OBSERVED_PROCESS_IDENTITY_CACHE_NS,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return identity;
|
|
196
|
+
}
|
|
197
|
+
function sameProcessStartIdentity(left, right) {
|
|
198
|
+
if (left.source !== right.source)
|
|
199
|
+
return false;
|
|
200
|
+
if (left.source === 'linux-proc' && right.source === 'linux-proc') {
|
|
201
|
+
return left.bootId === right.bootId && left.startTicks === right.startTicks;
|
|
202
|
+
}
|
|
203
|
+
if (left.source === 'windows-powershell' && right.source === 'windows-powershell') {
|
|
204
|
+
return left.startTimeTicks === right.startTimeTicks;
|
|
205
|
+
}
|
|
206
|
+
return left.source === 'darwin-ps'
|
|
207
|
+
&& right.source === 'darwin-ps'
|
|
208
|
+
&& left.startTime === right.startTime;
|
|
209
|
+
}
|
|
210
|
+
/** Fresh native process-start observation. Unlike lock acquisition, this does not use caches. */
|
|
211
|
+
export function readFileLockProcessStartIdentity(pid) {
|
|
212
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
213
|
+
return null;
|
|
214
|
+
const identity = (fileLockTestHooks.processStartIdentity ?? nativeProcessStartIdentity)(pid);
|
|
215
|
+
return identity === null ? null : { ...identity };
|
|
216
|
+
}
|
|
217
|
+
export function sameFileLockProcessStartIdentity(left, right) {
|
|
218
|
+
return sameProcessStartIdentity(left, right);
|
|
219
|
+
}
|
|
220
|
+
export function parseFileLockProcessStartIdentity(value) {
|
|
221
|
+
return parseProcessStartIdentity(value) ?? undefined;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Conservative owner classification for crash recovery. PID reuse and unavailable identity
|
|
225
|
+
* remain distinct from a proven-dead PID so callers cannot reclaim ambiguous ownership.
|
|
226
|
+
*/
|
|
227
|
+
export function inspectFileLockOwnerProcess(owner) {
|
|
228
|
+
if (!pidAlive(owner.pid))
|
|
229
|
+
return 'dead';
|
|
230
|
+
const current = readFileLockProcessStartIdentity(owner.pid);
|
|
231
|
+
if (current === null)
|
|
232
|
+
return 'unverifiable';
|
|
233
|
+
return sameProcessStartIdentity(owner.processStartIdentity, current) ? 'current' : 'pid_reused';
|
|
234
|
+
}
|
|
235
|
+
export function _setFileLockTestHooksForTest(hooks = {}) {
|
|
236
|
+
fileLockTestHooks = hooks;
|
|
237
|
+
currentProcessIdentity = undefined;
|
|
238
|
+
observedProcessIdentities.clear();
|
|
239
|
+
}
|
|
39
240
|
function noFollowFlag() {
|
|
40
241
|
return constants['O_NOFOLLOW'] ?? 0;
|
|
41
242
|
}
|
|
@@ -113,13 +314,12 @@ function readOwnerFileBounded(path) {
|
|
|
113
314
|
closeSync(fd);
|
|
114
315
|
}
|
|
115
316
|
}
|
|
116
|
-
function readOwnerBounded(path) {
|
|
117
|
-
return readOwnerFileBounded(path)?.raw ?? null;
|
|
118
|
-
}
|
|
119
317
|
/** True if pid is a live process (cross-platform via signal 0; EPERM = exists but owned by another user). */
|
|
120
318
|
function pidAlive(pid) {
|
|
121
319
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
122
320
|
return false;
|
|
321
|
+
if (fileLockTestHooks.pidAlive)
|
|
322
|
+
return fileLockTestHooks.pidAlive(pid);
|
|
123
323
|
try {
|
|
124
324
|
process.kill(pid, 0);
|
|
125
325
|
return true;
|
|
@@ -128,20 +328,57 @@ function pidAlive(pid) {
|
|
|
128
328
|
return e.code === 'EPERM';
|
|
129
329
|
}
|
|
130
330
|
}
|
|
331
|
+
function parseProcessStartIdentity(value) {
|
|
332
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
333
|
+
return null;
|
|
334
|
+
const record = value;
|
|
335
|
+
if (record['source'] === 'linux-proc') {
|
|
336
|
+
const bootId = record['bootId'];
|
|
337
|
+
const startTicks = record['startTicks'];
|
|
338
|
+
return typeof bootId === 'string'
|
|
339
|
+
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(bootId)
|
|
340
|
+
&& typeof startTicks === 'string'
|
|
341
|
+
&& validPositiveIntegerText(startTicks)
|
|
342
|
+
? { source: 'linux-proc', bootId, startTicks }
|
|
343
|
+
: null;
|
|
344
|
+
}
|
|
345
|
+
if (record['source'] === 'windows-powershell') {
|
|
346
|
+
const startTimeTicks = record['startTimeTicks'];
|
|
347
|
+
return typeof startTimeTicks === 'string' && validPositiveIntegerText(startTimeTicks)
|
|
348
|
+
? { source: 'windows-powershell', startTimeTicks }
|
|
349
|
+
: null;
|
|
350
|
+
}
|
|
351
|
+
if (record['source'] === 'darwin-ps') {
|
|
352
|
+
const startTime = record['startTime'];
|
|
353
|
+
return typeof startTime === 'string' && validIdentityText(startTime, 128)
|
|
354
|
+
? { source: 'darwin-ps', startTime }
|
|
355
|
+
: null;
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
131
359
|
function parseJsonOwner(raw) {
|
|
132
360
|
try {
|
|
133
361
|
const value = JSON.parse(raw);
|
|
134
|
-
if (typeof value !== 'object' || value === null)
|
|
362
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
135
363
|
return null;
|
|
136
364
|
const record = value;
|
|
137
|
-
if (typeof record
|
|
138
|
-
|| !Number.isInteger(record
|
|
139
|
-
|| record
|
|
140
|
-
|| typeof record
|
|
141
|
-
|| record
|
|
365
|
+
if (typeof record['pid'] !== 'number'
|
|
366
|
+
|| !Number.isInteger(record['pid'])
|
|
367
|
+
|| record['pid'] <= 0
|
|
368
|
+
|| typeof record['token'] !== 'string'
|
|
369
|
+
|| record['token'].length === 0) {
|
|
142
370
|
return null;
|
|
143
371
|
}
|
|
144
|
-
|
|
372
|
+
if (record['v'] === 2) {
|
|
373
|
+
const processStart = parseProcessStartIdentity(record['processStart']);
|
|
374
|
+
return processStart === null
|
|
375
|
+
? null
|
|
376
|
+
: { pid: record['pid'], token: record['token'], processStart };
|
|
377
|
+
}
|
|
378
|
+
if (record['v'] === 1 || record['v'] === undefined) {
|
|
379
|
+
return { pid: record['pid'], token: record['token'], processStart: null };
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
145
382
|
}
|
|
146
383
|
catch (error) {
|
|
147
384
|
if (error instanceof SyntaxError)
|
|
@@ -154,7 +391,7 @@ function parseLegacyOwner(raw) {
|
|
|
154
391
|
if (first === undefined)
|
|
155
392
|
return null;
|
|
156
393
|
const pid = Number(first);
|
|
157
|
-
return Number.isInteger(pid) && pid > 0 ? { pid, token: null } : null;
|
|
394
|
+
return Number.isInteger(pid) && pid > 0 ? { pid, token: null, processStart: null } : null;
|
|
158
395
|
}
|
|
159
396
|
function parseOwner(raw) {
|
|
160
397
|
const trimmed = raw.trim();
|
|
@@ -162,19 +399,22 @@ function parseOwner(raw) {
|
|
|
162
399
|
return null;
|
|
163
400
|
return parseJsonOwner(trimmed) ?? parseLegacyOwner(trimmed);
|
|
164
401
|
}
|
|
165
|
-
|
|
402
|
+
function readLockSnapshot(lockPath) {
|
|
403
|
+
const read = readOwnerFileBounded(lockPath);
|
|
404
|
+
if (read === null)
|
|
405
|
+
return null;
|
|
406
|
+
return {
|
|
407
|
+
owner: parseOwner(read.raw),
|
|
408
|
+
dev: read.stat.dev,
|
|
409
|
+
ino: read.stat.ino,
|
|
410
|
+
size: read.stat.size,
|
|
411
|
+
mtimeNs: read.stat.mtimeNs,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/** Snapshot of the lock inode and its owner payload; null if the path disappeared or changed during inspection. */
|
|
166
415
|
function lockSnapshot(lockPath) {
|
|
167
416
|
try {
|
|
168
|
-
|
|
169
|
-
if (read === null)
|
|
170
|
-
return null;
|
|
171
|
-
return {
|
|
172
|
-
owner: parseOwner(read.raw),
|
|
173
|
-
dev: read.stat.dev,
|
|
174
|
-
ino: read.stat.ino,
|
|
175
|
-
size: read.stat.size,
|
|
176
|
-
mtimeNs: read.stat.mtimeNs,
|
|
177
|
-
};
|
|
417
|
+
return readLockSnapshot(lockPath);
|
|
178
418
|
}
|
|
179
419
|
catch (error) {
|
|
180
420
|
// A valid owner can release and another waiter can acquire between lstat/open/fstat. Treat that snapshot as
|
|
@@ -198,7 +438,15 @@ function forgetMalformedObservation(lockPath) {
|
|
|
198
438
|
function reclaimableSnapshot(lockPath, snapshot) {
|
|
199
439
|
if (snapshot.owner !== null) {
|
|
200
440
|
forgetMalformedObservation(lockPath);
|
|
201
|
-
|
|
441
|
+
if (!pidAlive(snapshot.owner.pid)) {
|
|
442
|
+
observedProcessIdentities.delete(snapshot.owner.pid);
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
if (snapshot.owner.processStart === null)
|
|
446
|
+
return false;
|
|
447
|
+
const liveProcessStart = processStartIdentity(snapshot.owner.pid, snapshot.owner.processStart);
|
|
448
|
+
return liveProcessStart !== null
|
|
449
|
+
&& !sameProcessStartIdentity(snapshot.owner.processStart, liveProcessStart);
|
|
202
450
|
}
|
|
203
451
|
const key = lockKey(lockPath);
|
|
204
452
|
const now = process.hrtime.bigint();
|
|
@@ -210,7 +458,101 @@ function reclaimableSnapshot(lockPath, snapshot) {
|
|
|
210
458
|
return now - observed.firstSeenAtNs >= MALFORMED_LOCK_GRACE_NS;
|
|
211
459
|
}
|
|
212
460
|
function sameOwner(parsed, owner) {
|
|
213
|
-
return parsed?.pid === owner.pid
|
|
461
|
+
return parsed?.pid === owner.pid
|
|
462
|
+
&& parsed.token === owner.token
|
|
463
|
+
&& parsed.processStart !== null
|
|
464
|
+
&& sameProcessStartIdentity(parsed.processStart, owner.processStart);
|
|
465
|
+
}
|
|
466
|
+
function guardianPath(lockPath) {
|
|
467
|
+
return join(dirname(lockPath), `.${basename(lockPath)}.guardian`);
|
|
468
|
+
}
|
|
469
|
+
function serializedOwnerRecord(owner) {
|
|
470
|
+
return {
|
|
471
|
+
pid: owner.pid,
|
|
472
|
+
token: owner.token,
|
|
473
|
+
processStart: owner.processStart,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function parseGuardianOwner(value) {
|
|
477
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
478
|
+
return null;
|
|
479
|
+
const record = value;
|
|
480
|
+
const keys = Object.keys(record).sort();
|
|
481
|
+
if (keys.length !== 3
|
|
482
|
+
|| keys[0] !== 'pid'
|
|
483
|
+
|| keys[1] !== 'processStart'
|
|
484
|
+
|| keys[2] !== 'token'
|
|
485
|
+
|| !Number.isSafeInteger(record['pid'])
|
|
486
|
+
|| record['pid'] <= 0
|
|
487
|
+
|| typeof record['token'] !== 'string'
|
|
488
|
+
|| record['token'].length === 0
|
|
489
|
+
|| record['token'].length > 128)
|
|
490
|
+
return null;
|
|
491
|
+
const processStart = parseProcessStartIdentity(record['processStart']);
|
|
492
|
+
return processStart === null
|
|
493
|
+
? null
|
|
494
|
+
: { pid: record['pid'], token: record['token'], processStart };
|
|
495
|
+
}
|
|
496
|
+
function parseLockGuardian(raw) {
|
|
497
|
+
try {
|
|
498
|
+
const value = JSON.parse(raw);
|
|
499
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
500
|
+
return null;
|
|
501
|
+
const record = value;
|
|
502
|
+
const keys = Object.keys(record).sort();
|
|
503
|
+
if (keys.length !== 3 || keys[0] !== 'from' || keys[1] !== 'guardian'
|
|
504
|
+
|| keys[2] !== 'v' || record['v'] !== 1)
|
|
505
|
+
return null;
|
|
506
|
+
const from = parseGuardianOwner(record['from']);
|
|
507
|
+
const guardian = parseGuardianOwner(record['guardian']);
|
|
508
|
+
return from && guardian ? { from, guardian } : null;
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
if (error instanceof SyntaxError)
|
|
512
|
+
return null;
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function serializeLockGuardian(record) {
|
|
517
|
+
return `${JSON.stringify({
|
|
518
|
+
v: 1,
|
|
519
|
+
from: serializedOwnerRecord(record.from),
|
|
520
|
+
guardian: serializedOwnerRecord(record.guardian),
|
|
521
|
+
})}\n`;
|
|
522
|
+
}
|
|
523
|
+
function readLockGuardianSnapshot(lockPath) {
|
|
524
|
+
const read = readOwnerFileBounded(guardianPath(lockPath));
|
|
525
|
+
if (read === null)
|
|
526
|
+
return null;
|
|
527
|
+
return {
|
|
528
|
+
record: parseLockGuardian(read.raw),
|
|
529
|
+
snapshot: {
|
|
530
|
+
owner: null,
|
|
531
|
+
dev: read.stat.dev,
|
|
532
|
+
ino: read.stat.ino,
|
|
533
|
+
size: read.stat.size,
|
|
534
|
+
mtimeNs: read.stat.mtimeNs,
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function guardianReclaimable(record) {
|
|
539
|
+
if (!pidAlive(record.guardian.pid)) {
|
|
540
|
+
observedProcessIdentities.delete(record.guardian.pid);
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
const current = processStartIdentity(record.guardian.pid, record.guardian.processStart);
|
|
544
|
+
return current !== null && !sameProcessStartIdentity(record.guardian.processStart, current);
|
|
545
|
+
}
|
|
546
|
+
function sameGuardianRecord(left, right) {
|
|
547
|
+
return sameOwner({
|
|
548
|
+
pid: left.from.pid,
|
|
549
|
+
token: left.from.token,
|
|
550
|
+
processStart: left.from.processStart,
|
|
551
|
+
}, right.from) && sameOwner({
|
|
552
|
+
pid: left.guardian.pid,
|
|
553
|
+
token: left.guardian.token,
|
|
554
|
+
processStart: left.guardian.processStart,
|
|
555
|
+
}, right.guardian);
|
|
214
556
|
}
|
|
215
557
|
function sidecarPath(lockPath, kind) {
|
|
216
558
|
return join(dirname(lockPath), `.${basename(lockPath)}.${kind}.${process.pid}.${randomUUID()}`);
|
|
@@ -249,7 +591,7 @@ function restoreMovedFile(lockPath, movedPath, moved) {
|
|
|
249
591
|
}
|
|
250
592
|
throw new Error(`文件锁在安全回收期间被替换,已保留候选文件: ${movedPath}`);
|
|
251
593
|
}
|
|
252
|
-
function removeUnchangedLock(lockPath, expected, kind) {
|
|
594
|
+
function removeUnchangedLock(lockPath, expected, kind, onCleanupError) {
|
|
253
595
|
const movedPath = sidecarPath(lockPath, kind);
|
|
254
596
|
try {
|
|
255
597
|
renameSync(lockPath, movedPath);
|
|
@@ -265,23 +607,131 @@ function removeUnchangedLock(lockPath, expected, kind) {
|
|
|
265
607
|
restoreMovedFile(lockPath, movedPath, moved);
|
|
266
608
|
return false;
|
|
267
609
|
}
|
|
268
|
-
|
|
610
|
+
try {
|
|
611
|
+
removeFileIfExists(movedPath);
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
if (onCleanupError === undefined)
|
|
615
|
+
throw error;
|
|
616
|
+
onCleanupError(error);
|
|
617
|
+
}
|
|
269
618
|
forgetMalformedObservation(lockPath);
|
|
270
619
|
return true;
|
|
271
620
|
}
|
|
272
|
-
function removeFileIfOwned(lockPath, owner) {
|
|
621
|
+
function removeFileIfOwned(lockPath, owner, onCleanupError) {
|
|
273
622
|
const snapshot = lockSnapshot(lockPath);
|
|
274
623
|
if (snapshot === null || !sameOwner(snapshot.owner, owner))
|
|
275
624
|
return false;
|
|
276
|
-
return removeUnchangedLock(lockPath, snapshot, 'released');
|
|
625
|
+
return removeUnchangedLock(lockPath, snapshot, 'released', onCleanupError);
|
|
626
|
+
}
|
|
627
|
+
function publishOwnerFile(lockPath, owner) {
|
|
628
|
+
const temporaryPath = sidecarPath(lockPath, 'publishing');
|
|
629
|
+
let descriptor;
|
|
630
|
+
let published = false;
|
|
631
|
+
let publicationResult = false;
|
|
632
|
+
let failure;
|
|
633
|
+
try {
|
|
634
|
+
descriptor = openSync(temporaryPath, 'wx', 0o600);
|
|
635
|
+
writeFileSync(descriptor, serializeOwner(owner), { encoding: 'utf8' });
|
|
636
|
+
fsyncSync(descriptor);
|
|
637
|
+
closeSync(descriptor);
|
|
638
|
+
descriptor = undefined;
|
|
639
|
+
fileLockTestHooks.beforeOwnerPublish?.(temporaryPath, lockPath);
|
|
640
|
+
try {
|
|
641
|
+
linkSync(temporaryPath, lockPath);
|
|
642
|
+
published = true;
|
|
643
|
+
publicationResult = true;
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
if (!isErrno(error, 'EEXIST'))
|
|
647
|
+
failure = error;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
failure = error;
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
if (descriptor !== undefined) {
|
|
655
|
+
try {
|
|
656
|
+
closeSync(descriptor);
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
failure ??= error;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
unlinkSync(temporaryPath);
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
// Once linked, the final owner is valid and must be tracked even if the temporary name cannot be removed.
|
|
667
|
+
if (!published && !isErrno(error, 'ENOENT'))
|
|
668
|
+
failure ??= error;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (failure !== undefined)
|
|
672
|
+
throw failure;
|
|
673
|
+
return publicationResult;
|
|
674
|
+
}
|
|
675
|
+
function publishGuardianFile(lockPath, record) {
|
|
676
|
+
const finalPath = guardianPath(lockPath);
|
|
677
|
+
const temporaryPath = sidecarPath(finalPath, 'publishing');
|
|
678
|
+
let descriptor;
|
|
679
|
+
let published = false;
|
|
680
|
+
let publicationResult = false;
|
|
681
|
+
let failure;
|
|
682
|
+
try {
|
|
683
|
+
descriptor = openSync(temporaryPath, 'wx', 0o600);
|
|
684
|
+
writeFileSync(descriptor, serializeLockGuardian(record), { encoding: 'utf8' });
|
|
685
|
+
fsyncSync(descriptor);
|
|
686
|
+
closeSync(descriptor);
|
|
687
|
+
descriptor = undefined;
|
|
688
|
+
fileLockTestHooks.beforeGuardianPublish?.(temporaryPath, finalPath);
|
|
689
|
+
try {
|
|
690
|
+
linkSync(temporaryPath, finalPath);
|
|
691
|
+
published = true;
|
|
692
|
+
publicationResult = true;
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
if (!isErrno(error, 'EEXIST'))
|
|
696
|
+
failure = error;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
failure = error;
|
|
701
|
+
}
|
|
702
|
+
finally {
|
|
703
|
+
if (descriptor !== undefined) {
|
|
704
|
+
try {
|
|
705
|
+
closeSync(descriptor);
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
failure ??= error;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
unlinkSync(temporaryPath);
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
if (!published && !isErrno(error, 'ENOENT'))
|
|
716
|
+
failure ??= error;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (failure !== undefined)
|
|
720
|
+
throw failure;
|
|
721
|
+
return publicationResult;
|
|
277
722
|
}
|
|
278
723
|
function createLockFile(lockPath, owner, trackOwnership) {
|
|
279
724
|
const guardPath = mutationGuardPath(lockPath);
|
|
280
725
|
if (trackOwnership && pathExists(guardPath))
|
|
281
726
|
return false;
|
|
727
|
+
if (trackOwnership && pathExists(guardianPath(lockPath)))
|
|
728
|
+
return false;
|
|
729
|
+
if (pathExists(lockPath))
|
|
730
|
+
return false;
|
|
282
731
|
for (let permissionAttempt = 0; permissionAttempt < 2; permissionAttempt++) {
|
|
283
732
|
try {
|
|
284
|
-
|
|
733
|
+
if (!publishOwnerFile(lockPath, owner))
|
|
734
|
+
return false;
|
|
285
735
|
if (trackOwnership && pathExists(guardPath)) {
|
|
286
736
|
removeFileIfOwned(lockPath, owner);
|
|
287
737
|
return false;
|
|
@@ -321,8 +771,39 @@ function tryAcquireMutationGuard(lockPath) {
|
|
|
321
771
|
return null;
|
|
322
772
|
return createLockFile(guardPath, guard, false) ? guard : null;
|
|
323
773
|
}
|
|
324
|
-
function releaseMutationGuard(lockPath, guard) {
|
|
325
|
-
|
|
774
|
+
function releaseMutationGuard(lockPath, guard, onCleanupError) {
|
|
775
|
+
fileLockTestHooks.beforeMutationGuardRelease?.(mutationGuardPath(lockPath), lockPath);
|
|
776
|
+
return removeFileIfOwned(mutationGuardPath(lockPath), guard, onCleanupError);
|
|
777
|
+
}
|
|
778
|
+
function withMutationGuardRelease(lockPath, guard, operation, onCleanupError) {
|
|
779
|
+
let operationCompleted = false;
|
|
780
|
+
let operationResult;
|
|
781
|
+
let operationError;
|
|
782
|
+
try {
|
|
783
|
+
operationResult = operation();
|
|
784
|
+
operationCompleted = true;
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
operationError = error;
|
|
788
|
+
}
|
|
789
|
+
let releaseError;
|
|
790
|
+
try {
|
|
791
|
+
if (!releaseMutationGuard(lockPath, guard, onCleanupError)) {
|
|
792
|
+
releaseError = new UnsafeLockPathError('path_changed');
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
releaseError = error;
|
|
797
|
+
}
|
|
798
|
+
if (!operationCompleted) {
|
|
799
|
+
if (releaseError !== undefined) {
|
|
800
|
+
throw new AggregateError([operationError, releaseError], 'file lock mutation failed and mutation-guard release is unconfirmed');
|
|
801
|
+
}
|
|
802
|
+
throw operationError;
|
|
803
|
+
}
|
|
804
|
+
if (releaseError !== undefined)
|
|
805
|
+
throw releaseError;
|
|
806
|
+
return operationResult;
|
|
326
807
|
}
|
|
327
808
|
function reclaimStaleLock(lockPath) {
|
|
328
809
|
const guard = tryAcquireMutationGuard(lockPath);
|
|
@@ -338,16 +819,242 @@ function reclaimStaleLock(lockPath) {
|
|
|
338
819
|
releaseMutationGuard(lockPath, guard);
|
|
339
820
|
}
|
|
340
821
|
}
|
|
822
|
+
function reclaimStaleGuardian(lockPath) {
|
|
823
|
+
const guard = tryAcquireMutationGuard(lockPath);
|
|
824
|
+
if (guard === null)
|
|
825
|
+
return false;
|
|
826
|
+
try {
|
|
827
|
+
const observed = readLockGuardianSnapshot(lockPath);
|
|
828
|
+
if (observed === null)
|
|
829
|
+
return false;
|
|
830
|
+
const receiptPath = guardianPath(lockPath);
|
|
831
|
+
if (observed.record === null) {
|
|
832
|
+
if (!reclaimableSnapshot(receiptPath, observed.snapshot))
|
|
833
|
+
return false;
|
|
834
|
+
fileLockTestHooks.beforeGuardianReclaim?.(receiptPath, lockPath);
|
|
835
|
+
return removeUnchangedLock(receiptPath, observed.snapshot, 'guardian-malformed');
|
|
836
|
+
}
|
|
837
|
+
if (!guardianReclaimable(observed.record))
|
|
838
|
+
return false;
|
|
839
|
+
fileLockTestHooks.beforeGuardianReclaim?.(guardianPath(lockPath), lockPath);
|
|
840
|
+
const current = lockSnapshot(lockPath);
|
|
841
|
+
const locallyRetained = observed.record.from.pid === process.pid
|
|
842
|
+
&& ownedLocks.get(lockKey(lockPath)) === undefined;
|
|
843
|
+
if (current !== null
|
|
844
|
+
&& sameOwner(current.owner, observed.record.from)
|
|
845
|
+
&& (locallyRetained || reclaimableSnapshot(lockPath, current))) {
|
|
846
|
+
removeUnchangedLock(lockPath, current, 'guardian-owner-stale');
|
|
847
|
+
}
|
|
848
|
+
return removeUnchangedLock(receiptPath, observed.snapshot, 'guardian-stale');
|
|
849
|
+
}
|
|
850
|
+
finally {
|
|
851
|
+
releaseMutationGuard(lockPath, guard);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function samePublicOwner(left, right) {
|
|
855
|
+
return left.pid === right.pid
|
|
856
|
+
&& left.token === right.token
|
|
857
|
+
&& sameProcessStartIdentity(left.processStartIdentity, right.processStart);
|
|
858
|
+
}
|
|
859
|
+
function observeTransferTarget(pid) {
|
|
860
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || !pidAlive(pid)) {
|
|
861
|
+
return { reason: 'target_process_dead' };
|
|
862
|
+
}
|
|
863
|
+
const first = readFileLockProcessStartIdentity(pid);
|
|
864
|
+
if (first === null)
|
|
865
|
+
return { reason: 'target_process_unverifiable' };
|
|
866
|
+
if (!pidAlive(pid))
|
|
867
|
+
return { reason: 'target_process_dead' };
|
|
868
|
+
const second = readFileLockProcessStartIdentity(pid);
|
|
869
|
+
if (second === null)
|
|
870
|
+
return { reason: 'target_process_unverifiable' };
|
|
871
|
+
if (!sameProcessStartIdentity(first, second)) {
|
|
872
|
+
return { reason: 'target_process_pid_reused' };
|
|
873
|
+
}
|
|
874
|
+
if (!pidAlive(pid))
|
|
875
|
+
return { reason: 'target_process_dead' };
|
|
876
|
+
return { owner: { pid, token: randomUUID(), processStart: second } };
|
|
877
|
+
}
|
|
341
878
|
/**
|
|
342
|
-
*
|
|
879
|
+
* Pre-arm an acquired lock with the exact child PID generation while retaining local ownership.
|
|
880
|
+
* A controller can therefore keep mutating its journal, while a hard controller crash cannot make
|
|
881
|
+
* the primary owner reclaimable until the child exits.
|
|
882
|
+
*/
|
|
883
|
+
export function attachLockGuardianToProcess(lockPath, expectedOwner, targetPid) {
|
|
884
|
+
const key = lockKey(lockPath);
|
|
885
|
+
const owned = ownedLocks.get(key);
|
|
886
|
+
if (owned === undefined)
|
|
887
|
+
return { attached: false, reason: 'not_owned' };
|
|
888
|
+
if (owned.releasePending || !samePublicOwner(expectedOwner, owned.owner)) {
|
|
889
|
+
return { attached: false, reason: 'ownership_changed' };
|
|
890
|
+
}
|
|
891
|
+
const guard = tryAcquireMutationGuard(lockPath);
|
|
892
|
+
if (guard === null)
|
|
893
|
+
return { attached: false, reason: 'mutation_busy' };
|
|
894
|
+
const guarded = withMutationGuardRelease(lockPath, guard, () => {
|
|
895
|
+
const current = readLockSnapshot(lockPath);
|
|
896
|
+
if (current === null || !sameOwner(current.owner, owned.owner)) {
|
|
897
|
+
return { result: { attached: false, reason: 'ownership_changed' } };
|
|
898
|
+
}
|
|
899
|
+
if (owned.guardian !== undefined || readLockGuardianSnapshot(lockPath) !== null) {
|
|
900
|
+
return { result: { attached: false, reason: 'guardian_exists' } };
|
|
901
|
+
}
|
|
902
|
+
const target = observeTransferTarget(targetPid);
|
|
903
|
+
if ('reason' in target) {
|
|
904
|
+
return { result: { attached: false, reason: target.reason } };
|
|
905
|
+
}
|
|
906
|
+
const record = {
|
|
907
|
+
from: owned.owner,
|
|
908
|
+
guardian: target.owner,
|
|
909
|
+
};
|
|
910
|
+
if (!publishGuardianFile(lockPath, record)) {
|
|
911
|
+
return { result: { attached: false, reason: 'guardian_exists' } };
|
|
912
|
+
}
|
|
913
|
+
const publishedGuardian = readLockGuardianSnapshot(lockPath);
|
|
914
|
+
if (publishedGuardian?.record === null || publishedGuardian === null
|
|
915
|
+
|| !sameGuardianRecord(publishedGuardian.record, record)) {
|
|
916
|
+
return { result: { attached: false, reason: 'path_changed' } };
|
|
917
|
+
}
|
|
918
|
+
fileLockTestHooks.afterGuardianPublish?.(guardianPath(lockPath), lockPath);
|
|
919
|
+
const settledGuardian = readLockGuardianSnapshot(lockPath);
|
|
920
|
+
const settled = lockSnapshot(lockPath);
|
|
921
|
+
const guardianUnchanged = settledGuardian !== null
|
|
922
|
+
&& sameSnapshot(publishedGuardian.snapshot, settledGuardian.snapshot)
|
|
923
|
+
&& settledGuardian.record !== null
|
|
924
|
+
&& sameGuardianRecord(settledGuardian.record, record);
|
|
925
|
+
const ownerUnchanged = settled !== null
|
|
926
|
+
&& sameSnapshot(current, settled)
|
|
927
|
+
&& sameOwner(settled.owner, owned.owner);
|
|
928
|
+
if (!guardianUnchanged || !ownerUnchanged) {
|
|
929
|
+
if (guardianUnchanged) {
|
|
930
|
+
removeUnchangedLock(guardianPath(lockPath), settledGuardian.snapshot, 'guardian-transfer-aborted');
|
|
931
|
+
}
|
|
932
|
+
if (!ownerUnchanged)
|
|
933
|
+
ownedLocks.delete(key);
|
|
934
|
+
return { result: { attached: false, reason: 'path_changed' } };
|
|
935
|
+
}
|
|
936
|
+
forgetMalformedObservation(lockPath);
|
|
937
|
+
return {
|
|
938
|
+
result: {
|
|
939
|
+
attached: true,
|
|
940
|
+
reason: 'attached',
|
|
941
|
+
guardian: publicOwnerRecord(target.owner),
|
|
942
|
+
},
|
|
943
|
+
guardian: record,
|
|
944
|
+
};
|
|
945
|
+
});
|
|
946
|
+
if (guarded.guardian !== undefined) {
|
|
947
|
+
owned.guardian = guarded.guardian;
|
|
948
|
+
}
|
|
949
|
+
return guarded.result;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Drop local ownership only after an already-published guardian has been revalidated exactly.
|
|
953
|
+
* No file mutation is needed: the durable guardian receipt is the crash-safe owner binding.
|
|
954
|
+
*/
|
|
955
|
+
export function retainLockGuardianForProcess(lockPath, expectedOwner, expectedGuardian) {
|
|
956
|
+
const key = lockKey(lockPath);
|
|
957
|
+
const owned = ownedLocks.get(key);
|
|
958
|
+
if (owned === undefined)
|
|
959
|
+
return { retained: false, reason: 'not_owned' };
|
|
960
|
+
if (owned.releasePending || !samePublicOwner(expectedOwner, owned.owner)) {
|
|
961
|
+
return { retained: false, reason: 'ownership_changed' };
|
|
962
|
+
}
|
|
963
|
+
if (owned.guardian === undefined)
|
|
964
|
+
return { retained: false, reason: 'guardian_missing' };
|
|
965
|
+
if (!samePublicOwner(expectedGuardian, owned.guardian.guardian)) {
|
|
966
|
+
return { retained: false, reason: 'ownership_changed' };
|
|
967
|
+
}
|
|
968
|
+
const guardian = owned.guardian;
|
|
969
|
+
const guard = tryAcquireMutationGuard(lockPath);
|
|
970
|
+
if (guard === null)
|
|
971
|
+
return { retained: false, reason: 'mutation_busy' };
|
|
972
|
+
const result = withMutationGuardRelease(lockPath, guard, () => {
|
|
973
|
+
const current = readLockSnapshot(lockPath);
|
|
974
|
+
const receipt = readLockGuardianSnapshot(lockPath);
|
|
975
|
+
if (current === null || !sameOwner(current.owner, owned.owner)) {
|
|
976
|
+
return { retained: false, reason: 'ownership_changed' };
|
|
977
|
+
}
|
|
978
|
+
if (receipt?.record === null || receipt === null
|
|
979
|
+
|| !sameGuardianRecord(receipt.record, guardian)) {
|
|
980
|
+
return { retained: false, reason: 'path_changed' };
|
|
981
|
+
}
|
|
982
|
+
return { retained: true, reason: 'retained' };
|
|
983
|
+
});
|
|
984
|
+
if (result.retained) {
|
|
985
|
+
ownedLocks.delete(key);
|
|
986
|
+
}
|
|
987
|
+
return result;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Remove an exact attached guardian after the child has definitely exited, retaining the primary
|
|
991
|
+
* owner so rollback or another launch can continue under the same lifecycle lease.
|
|
992
|
+
*/
|
|
993
|
+
export function clearLockGuardianForProcess(lockPath, expectedOwner, expectedGuardian) {
|
|
994
|
+
const key = lockKey(lockPath);
|
|
995
|
+
const owned = ownedLocks.get(key);
|
|
996
|
+
if (owned === undefined)
|
|
997
|
+
return { cleared: false, reason: 'not_owned' };
|
|
998
|
+
if (owned.releasePending || !samePublicOwner(expectedOwner, owned.owner)) {
|
|
999
|
+
return { cleared: false, reason: 'ownership_changed' };
|
|
1000
|
+
}
|
|
1001
|
+
if (owned.guardian === undefined
|
|
1002
|
+
|| !samePublicOwner(expectedGuardian, owned.guardian.guardian)) {
|
|
1003
|
+
return { cleared: false, reason: 'ownership_changed' };
|
|
1004
|
+
}
|
|
1005
|
+
const guardian = owned.guardian;
|
|
1006
|
+
const guard = tryAcquireMutationGuard(lockPath);
|
|
1007
|
+
if (guard === null)
|
|
1008
|
+
return { cleared: false, reason: 'mutation_busy' };
|
|
1009
|
+
const result = withMutationGuardRelease(lockPath, guard, () => {
|
|
1010
|
+
const current = readLockSnapshot(lockPath);
|
|
1011
|
+
if (current === null || !sameOwner(current.owner, owned.owner)) {
|
|
1012
|
+
return { cleared: false, reason: 'ownership_changed' };
|
|
1013
|
+
}
|
|
1014
|
+
const receipt = readLockGuardianSnapshot(lockPath);
|
|
1015
|
+
if (receipt === null) {
|
|
1016
|
+
return { cleared: true, reason: 'missing' };
|
|
1017
|
+
}
|
|
1018
|
+
if (receipt.record === null || !sameGuardianRecord(receipt.record, guardian)) {
|
|
1019
|
+
return { cleared: false, reason: 'path_changed' };
|
|
1020
|
+
}
|
|
1021
|
+
if (!removeUnchangedLock(guardianPath(lockPath), receipt.snapshot, 'guardian-cleared')) {
|
|
1022
|
+
return { cleared: false, reason: 'path_changed' };
|
|
1023
|
+
}
|
|
1024
|
+
return { cleared: true, reason: 'cleared' };
|
|
1025
|
+
});
|
|
1026
|
+
if (result.cleared) {
|
|
1027
|
+
delete owned.guardian;
|
|
1028
|
+
}
|
|
1029
|
+
return result;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Atomically bind an acquired lock to a still-running child and relinquish local ownership.
|
|
1033
|
+
*/
|
|
1034
|
+
export function transferLockOwnershipToProcess(lockPath, expectedOwner, targetPid) {
|
|
1035
|
+
const attached = attachLockGuardianToProcess(lockPath, expectedOwner, targetPid);
|
|
1036
|
+
if (!attached.attached)
|
|
1037
|
+
return { transferred: false, reason: attached.reason };
|
|
1038
|
+
const retained = retainLockGuardianForProcess(lockPath, expectedOwner, attached.guardian);
|
|
1039
|
+
if (!retained.retained)
|
|
1040
|
+
return { transferred: false, reason: retained.reason };
|
|
1041
|
+
return {
|
|
1042
|
+
transferred: true,
|
|
1043
|
+
reason: 'transferred',
|
|
1044
|
+
guardian: attached.guardian,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Cross-process file lock with crashed-owner recovery.
|
|
343
1049
|
*
|
|
344
1050
|
* The lock file records the owner pid and token. If a waiter finds the lock held by a pid that is no longer
|
|
345
1051
|
* alive (the owner crashed without releaseLock), it reclaims the stale lock instead of spinning
|
|
346
1052
|
* until timeout — otherwise one crashed process would deadlock every future writer until the file
|
|
347
1053
|
* is removed by hand. Empty or truncated locks are reclaimed only after the same inode and contents
|
|
348
1054
|
* remain malformed for a grace period, so a live creator can finish publishing its owner payload.
|
|
349
|
-
* Acquisition
|
|
350
|
-
* guard, then verifies the inode snapshot before
|
|
1055
|
+
* Acquisition publishes a fully synced temporary owner with an atomic no-replace hardlink, and stale
|
|
1056
|
+
* reclaim moves the old lock aside under a mutation guard, then verifies the inode snapshot before
|
|
1057
|
+
* deletion. A live owner's lock (including this
|
|
351
1058
|
* process's own) is never stolen.
|
|
352
1059
|
*
|
|
353
1060
|
* NOTE: still synchronous (blocks the event loop while waiting) by design — it guards short
|
|
@@ -363,9 +1070,23 @@ export function acquireLock(lockPath, opts = {}) {
|
|
|
363
1070
|
const maxTries = opts.maxTries ?? 300;
|
|
364
1071
|
const waitMs = opts.waitMs ?? 10;
|
|
365
1072
|
for (let i = 0; i < maxTries; i++) {
|
|
1073
|
+
const guardian = readLockGuardianSnapshot(lockPath);
|
|
1074
|
+
if (guardian !== null) {
|
|
1075
|
+
const reclaimable = guardian.record === null
|
|
1076
|
+
? reclaimableSnapshot(guardianPath(lockPath), guardian.snapshot)
|
|
1077
|
+
: guardianReclaimable(guardian.record);
|
|
1078
|
+
if (!reclaimable) {
|
|
1079
|
+
syncSleep(waitMs);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if (!reclaimStaleGuardian(lockPath)) {
|
|
1083
|
+
syncSleep(waitMs);
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
366
1087
|
const owner = newOwner();
|
|
367
1088
|
if (createLockFile(lockPath, owner, true)) {
|
|
368
|
-
return;
|
|
1089
|
+
return publicOwnerRecord(owner);
|
|
369
1090
|
}
|
|
370
1091
|
const current = lockSnapshot(lockPath);
|
|
371
1092
|
if (current !== null && reclaimableSnapshot(lockPath, current)) {
|
|
@@ -391,46 +1112,64 @@ export function releaseLock(lockPath) {
|
|
|
391
1112
|
owned.releasePending = true;
|
|
392
1113
|
return { released: false, reason };
|
|
393
1114
|
};
|
|
394
|
-
|
|
395
|
-
// for acquire retries. Here, an inconclusive read marks a pending release so the next acquire can retry or
|
|
396
|
-
// surface LOCK_RELEASE_FAILED instead of silently timing out behind this process's own PID.
|
|
397
|
-
let raw;
|
|
1115
|
+
let guard;
|
|
398
1116
|
try {
|
|
399
|
-
|
|
1117
|
+
guard = tryAcquireMutationGuard(lockPath);
|
|
400
1118
|
}
|
|
401
1119
|
catch (error) {
|
|
402
1120
|
return failed(error instanceof UnsafeLockPathError ? error.reason : 'release_failed');
|
|
403
1121
|
}
|
|
404
|
-
if (
|
|
405
|
-
ownedLocks.delete(key);
|
|
406
|
-
return { released: true, reason: 'missing' };
|
|
407
|
-
}
|
|
408
|
-
const current = parseOwner(raw);
|
|
409
|
-
if (current === null)
|
|
410
|
-
return failed('invalid_owner');
|
|
411
|
-
if (!sameOwner(current, owned.owner)) {
|
|
412
|
-
ownedLocks.delete(key);
|
|
413
|
-
return { released: true, reason: 'ownership_changed' };
|
|
414
|
-
}
|
|
415
|
-
const releasedPath = sidecarPath(lockPath, 'released');
|
|
416
|
-
try {
|
|
417
|
-
renameSync(lockPath, releasedPath);
|
|
418
|
-
}
|
|
419
|
-
catch (error) {
|
|
420
|
-
if (isErrno(error, 'ENOENT')) {
|
|
421
|
-
ownedLocks.delete(key);
|
|
422
|
-
return { released: true, reason: 'missing' };
|
|
423
|
-
}
|
|
1122
|
+
if (guard === null)
|
|
424
1123
|
return failed('release_failed');
|
|
425
|
-
|
|
426
|
-
|
|
1124
|
+
let cleanupFailed = false;
|
|
1125
|
+
let forgetOwnership = false;
|
|
1126
|
+
let result;
|
|
427
1127
|
try {
|
|
428
|
-
|
|
1128
|
+
result = withMutationGuardRelease(lockPath, guard, () => {
|
|
1129
|
+
// Release must use exact snapshots. Inconclusive reads leave a pending release instead of
|
|
1130
|
+
// silently unlocking a live guarded child.
|
|
1131
|
+
const current = readLockSnapshot(lockPath);
|
|
1132
|
+
if (current === null) {
|
|
1133
|
+
if (owned.guardian !== undefined)
|
|
1134
|
+
return failed('ownership_changed');
|
|
1135
|
+
forgetOwnership = true;
|
|
1136
|
+
return { released: true, reason: 'missing' };
|
|
1137
|
+
}
|
|
1138
|
+
if (current.owner === null)
|
|
1139
|
+
return failed('invalid_owner');
|
|
1140
|
+
if (!sameOwner(current.owner, owned.owner)) {
|
|
1141
|
+
forgetOwnership = true;
|
|
1142
|
+
return { released: true, reason: 'ownership_changed' };
|
|
1143
|
+
}
|
|
1144
|
+
const receipt = readLockGuardianSnapshot(lockPath);
|
|
1145
|
+
if (owned.guardian !== undefined) {
|
|
1146
|
+
if (receipt !== null) {
|
|
1147
|
+
if (receipt.record === null || !sameGuardianRecord(receipt.record, owned.guardian)) {
|
|
1148
|
+
return failed('path_changed');
|
|
1149
|
+
}
|
|
1150
|
+
if (!removeUnchangedLock(guardianPath(lockPath), receipt.snapshot, 'guardian-released', () => { cleanupFailed = true; })) {
|
|
1151
|
+
return failed('path_changed');
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
else if (receipt !== null) {
|
|
1156
|
+
return failed('path_changed');
|
|
1157
|
+
}
|
|
1158
|
+
if (!removeUnchangedLock(lockPath, current, 'released', () => { cleanupFailed = true; })) {
|
|
1159
|
+
return failed('path_changed');
|
|
1160
|
+
}
|
|
1161
|
+
forgetOwnership = true;
|
|
1162
|
+
return { released: true, reason: 'released' };
|
|
1163
|
+
}, () => { cleanupFailed = true; });
|
|
429
1164
|
}
|
|
430
|
-
catch {
|
|
431
|
-
return
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
return failed(error instanceof UnsafeLockPathError ? error.reason : 'release_failed');
|
|
432
1167
|
}
|
|
433
|
-
|
|
1168
|
+
if (result.released && forgetOwnership)
|
|
1169
|
+
ownedLocks.delete(key);
|
|
1170
|
+
return result.released && cleanupFailed
|
|
1171
|
+
? { released: true, reason: 'released_with_cleanup_error' }
|
|
1172
|
+
: result;
|
|
434
1173
|
}
|
|
435
1174
|
export class LockReleaseError extends Error {
|
|
436
1175
|
reason;
|