@evomap/evolver-core 2.0.0-beta.2 → 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 +171 -26
- package/dist/algo/capabilityCandidates.js +10 -0
- package/dist/algo/conversationSniffer.js +25 -8
- package/dist/algo/cycleEngine.d.ts +63 -5
- package/dist/algo/cycleEngine.js +320 -42
- package/dist/algo/cycleFailureClassifier.d.ts +1 -1
- 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 +36 -3
- package/dist/algo/geneHealth.js +47 -4
- package/dist/algo/geneIntake.d.ts +46 -1
- package/dist/algo/geneIntake.js +121 -10
- package/dist/algo/geneSelection.d.ts +126 -6
- package/dist/algo/geneSelection.js +479 -36
- package/dist/algo/index.d.ts +6 -1
- package/dist/algo/index.js +6 -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/memoryGraph.d.ts +62 -0
- package/dist/algo/memoryGraph.js +86 -0
- package/dist/algo/orchestrator.d.ts +17 -1
- package/dist/algo/orchestrator.js +30 -4
- package/dist/algo/publishEligibility.d.ts +34 -0
- package/dist/algo/publishEligibility.js +52 -0
- 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.d.ts +28 -0
- package/dist/assetstore/assetSidecarRecords.js +384 -0
- package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
- package/dist/assetstore/assetSidecarRecovery.js +288 -0
- package/dist/assetstore/assetStoreHealth.d.ts +81 -0
- package/dist/assetstore/assetStoreHealth.js +319 -0
- package/dist/assetstore/assetStoreLayout.d.ts +2 -0
- package/dist/assetstore/assetStoreLayout.js +6 -0
- package/dist/assetstore/assetStoreStorage.d.ts +42 -0
- package/dist/assetstore/assetStoreStorage.js +336 -0
- package/dist/assetstore/assetSyncLedger.d.ts +91 -1
- package/dist/assetstore/assetSyncLedger.js +718 -59
- package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
- package/dist/assetstore/foreignJsonlSource.js +150 -0
- package/dist/assetstore/index.d.ts +5 -0
- package/dist/assetstore/index.js +5 -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 +12 -1
- package/dist/assetstore/localJsonl.js +251 -42
- package/dist/assetstore/provenance.d.ts +93 -4
- package/dist/assetstore/provenance.js +368 -84
- package/dist/assetstore/provider.d.ts +63 -0
- package/dist/assetstore/provider.js +97 -6
- package/dist/assetstore/reviewFilter.d.ts +19 -1
- package/dist/assetstore/reviewFilter.js +39 -1
- package/dist/assetstore/reviewLedger.d.ts +8 -2
- package/dist/assetstore/reviewLedger.js +71 -45
- 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 +3 -1
- package/dist/benchmark/index.js +3 -1
- package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
- package/dist/benchmark/selectionFlatAbstention.js +481 -0
- package/dist/benchmark/triggerShift.d.ts +62 -0
- package/dist/benchmark/triggerShift.js +106 -0
- package/dist/bootstrap/envFingerprint.d.ts +9 -0
- package/dist/bootstrap/envFingerprint.js +5 -0
- package/dist/bootstrap/index.d.ts +3 -1
- package/dist/bootstrap/index.js +3 -1
- package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
- package/dist/bootstrap/lifecycleBootstrap.js +433 -0
- package/dist/bootstrap/v1EnvCompat.d.ts +113 -0
- package/dist/bootstrap/v1EnvCompat.js +303 -0
- 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 +15 -0
- package/dist/events/paths.d.ts +12 -10
- package/dist/events/paths.js +24 -20
- package/dist/events/public.d.ts +2 -2
- package/dist/events/public.js +2 -2
- package/dist/events/reports.d.ts +2 -0
- package/dist/events/reports.js +4 -0
- package/dist/exec/autoExec.d.ts +68 -4
- package/dist/exec/autoExec.js +392 -33
- package/dist/exec/autonomousCycle.d.ts +31 -4
- package/dist/exec/autonomousCycle.js +71 -13
- package/dist/exec/claudeBridge.d.ts +80 -15
- package/dist/exec/claudeBridge.js +845 -76
- 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/openPrRegistry.d.ts +8 -2
- package/dist/exec/openPrRegistry.js +32 -22
- package/dist/exec/prompt.js +14 -1
- package/dist/exec/proofOfWork.d.ts +1 -1
- package/dist/exec/proofOfWork.js +2 -2
- package/dist/exec/runnerRegistry.d.ts +153 -36
- package/dist/exec/runnerRegistry.js +848 -65
- package/dist/exec/selfPr.js +1 -7
- package/dist/feedback/envelope.d.ts +61 -0
- package/dist/feedback/envelope.js +168 -0
- package/dist/feedback/index.d.ts +1 -0
- package/dist/feedback/index.js +1 -0
- package/dist/hooks/hooks.js +1 -0
- package/dist/hub/assetCallLog.d.ts +35 -1
- package/dist/hub/assetCallLog.js +124 -1
- package/dist/hub/bindings.d.ts +8 -1
- package/dist/hub/bindings.js +29 -8
- package/dist/hub/capability.d.ts +130 -4
- package/dist/hub/conversationDistiller.d.ts +19 -0
- package/dist/hub/conversationDistiller.js +115 -37
- package/dist/hub/fake.d.ts +3 -2
- package/dist/hub/fake.js +2 -1
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/hub/questionGenerator.d.ts +5 -1
- package/dist/hub/questionGenerator.js +8 -6
- package/dist/hub/recipeCompose.d.ts +27 -0
- package/dist/hub/recipeCompose.js +90 -0
- package/dist/hub/sanitize.js +122 -7
- package/dist/index.d.ts +6 -1
- package/dist/index.js +7 -1
- package/dist/issueReporter/index.d.ts +156 -0
- package/dist/issueReporter/index.js +1688 -0
- package/dist/mailbox/dispatch.d.ts +1 -1
- package/dist/mailbox/dispatch.js +22 -6
- package/dist/mailbox/envelope.d.ts +7 -1
- package/dist/mailbox/envelope.js +9 -2
- package/dist/mailbox/ipcServer.d.ts +12 -2
- package/dist/mailbox/ipcServer.js +183 -13
- package/dist/mailbox/store.d.ts +89 -3
- package/dist/mailbox/store.js +895 -41
- 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/savingsCore.js +1 -2
- package/dist/ops/selfUpdate.d.ts +18 -1
- package/dist/ops/selfUpdate.js +88 -23
- package/dist/ops/valueOutreach.d.ts +3 -1
- package/dist/ops/valueOutreach.js +5 -1
- package/dist/personality/schema.d.ts +24 -24
- 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/schema/signal.d.ts +3 -3
- package/dist/schema/signal.js +1 -1
- package/dist/shadow/shadowHub.js +1 -0
- package/dist/signals/curriculum.d.ts +55 -0
- package/dist/signals/curriculum.js +202 -0
- package/dist/signals/cycleHistoryFromEvents.js +17 -8
- package/dist/signals/expand.d.ts +15 -1
- package/dist/signals/expand.js +169 -1
- package/dist/signals/extractor.d.ts +2 -2
- package/dist/signals/extractor.js +31 -6
- package/dist/signals/index.d.ts +4 -1
- package/dist/signals/index.js +4 -1
- package/dist/signals/metaSignals.d.ts +4 -0
- package/dist/signals/metaSignals.js +42 -0
- package/dist/signals/scopeVocabulary.d.ts +75 -0
- package/dist/signals/scopeVocabulary.js +91 -0
- package/dist/signals/signalGate.js +1 -1
- package/dist/signals/taskDomain.d.ts +22 -0
- package/dist/signals/taskDomain.js +43 -0
- package/dist/strategy/constraintAblation.d.ts +64 -0
- package/dist/strategy/constraintAblation.js +2820 -0
- package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
- package/dist/strategy/constraintAblationPredicates.js +339 -0
- package/dist/strategy/index.d.ts +2 -1
- package/dist/strategy/index.js +2 -1
- package/dist/trace/index.d.ts +3 -1
- package/dist/trace/index.js +3 -1
- package/dist/trace/learningTrace.d.ts +216 -0
- package/dist/trace/learningTrace.js +298 -0
- package/dist/trace/proxyTurns.d.ts +31 -0
- package/dist/trace/proxyTurns.js +137 -0
- package/dist/trace/trajectory.d.ts +8 -0
- package/dist/trace/trajectory.js +14 -2
- package/dist/util/fetchPort.d.ts +1 -0
- package/dist/util/fetchPort.js +11 -0
- package/dist/util/fileLock.d.ts +113 -7
- package/dist/util/fileLock.js +1035 -80
- package/dist/util/index.d.ts +3 -1
- package/dist/util/index.js +2 -1
- package/dist/verify/index.d.ts +2 -1
- package/dist/verify/index.js +1 -1
- package/dist/verify/sandboxRunner.d.ts +30 -0
- package/dist/verify/sandboxRunner.js +370 -27
- package/dist/verify/sandboxedValidation.d.ts +16 -2
- package/dist/verify/sandboxedValidation.js +200 -22
- package/dist/verify/validation.d.ts +35 -4
- package/dist/verify/validation.js +164 -16
- 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/dist/workflow/dsl.d.ts +24 -3
- package/dist/workflow/dsl.js +4 -0
- package/dist/workflow/engine.d.ts +5 -1
- package/dist/workflow/engine.js +3 -0
- package/dist/workflow/index.d.ts +3 -1
- package/dist/workflow/index.js +3 -1
- package/dist/workflow/runtime.d.ts +110 -0
- package/dist/workflow/runtime.js +1298 -0
- package/dist/workflow/stateStore.d.ts +172 -0
- package/dist/workflow/stateStore.js +1044 -0
- package/package.json +12 -4
package/dist/util/fileLock.js
CHANGED
|
@@ -1,33 +1,325 @@
|
|
|
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
|
}
|
|
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';
|
|
7
12
|
const ownedLocks = new Map();
|
|
13
|
+
const malformedLockObservations = new Map();
|
|
14
|
+
const observedProcessIdentities = new Map();
|
|
15
|
+
let currentProcessIdentity;
|
|
16
|
+
let fileLockTestHooks = {};
|
|
8
17
|
export class LockTimeoutError extends Error {
|
|
9
18
|
code = 'LOCK_TIMEOUT';
|
|
10
|
-
constructor(
|
|
11
|
-
super(
|
|
19
|
+
constructor(_lockPath) {
|
|
20
|
+
super('获取文件锁超时');
|
|
12
21
|
this.name = 'LockTimeoutError';
|
|
13
22
|
}
|
|
14
23
|
}
|
|
24
|
+
export class UnsafeLockPathError extends Error {
|
|
25
|
+
reason;
|
|
26
|
+
code = 'UNSAFE_LOCK_PATH';
|
|
27
|
+
constructor(reason) {
|
|
28
|
+
super(`不安全的文件锁路径: ${reason}`);
|
|
29
|
+
this.reason = reason;
|
|
30
|
+
this.name = 'UnsafeLockPathError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export const MAX_LOCK_OWNER_BYTES = 4096;
|
|
15
34
|
function lockKey(lockPath) {
|
|
16
35
|
return resolve(lockPath);
|
|
17
36
|
}
|
|
18
37
|
function newOwner() {
|
|
19
|
-
|
|
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 };
|
|
20
42
|
}
|
|
21
43
|
function serializeOwner(owner) {
|
|
22
|
-
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
|
+
};
|
|
23
57
|
}
|
|
24
58
|
function isErrno(error, code) {
|
|
25
59
|
return typeof error === 'object' && error !== null && error.code === code;
|
|
26
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
|
+
}
|
|
240
|
+
function noFollowFlag() {
|
|
241
|
+
return constants['O_NOFOLLOW'] ?? 0;
|
|
242
|
+
}
|
|
243
|
+
function assertRegularOwnerStat(stat) {
|
|
244
|
+
if (stat.isSymbolicLink())
|
|
245
|
+
throw new UnsafeLockPathError('symlink');
|
|
246
|
+
if (!stat.isFile())
|
|
247
|
+
throw new UnsafeLockPathError('not_regular_file');
|
|
248
|
+
if (stat.size > BigInt(MAX_LOCK_OWNER_BYTES))
|
|
249
|
+
throw new UnsafeLockPathError('owner_too_large');
|
|
250
|
+
}
|
|
251
|
+
function currentOwnerStat(path) {
|
|
252
|
+
try {
|
|
253
|
+
const stat = lstatSync(path, { bigint: true });
|
|
254
|
+
assertRegularOwnerStat(stat);
|
|
255
|
+
return stat;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (isErrno(error, 'ENOENT'))
|
|
259
|
+
return null;
|
|
260
|
+
if (isErrno(error, 'EACCES') || isErrno(error, 'EPERM')) {
|
|
261
|
+
throw new UnsafeLockPathError('permission_denied');
|
|
262
|
+
}
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function readOwnerFileBounded(path) {
|
|
267
|
+
const before = currentOwnerStat(path);
|
|
268
|
+
if (before === null)
|
|
269
|
+
return null;
|
|
270
|
+
let fd;
|
|
271
|
+
try {
|
|
272
|
+
fd = openSync(path, constants.O_RDONLY | noFollowFlag());
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (isErrno(error, 'ENOENT'))
|
|
276
|
+
return null;
|
|
277
|
+
if (isErrno(error, 'ELOOP'))
|
|
278
|
+
throw new UnsafeLockPathError('symlink');
|
|
279
|
+
if (isErrno(error, 'EACCES') || isErrno(error, 'EPERM')) {
|
|
280
|
+
throw new UnsafeLockPathError('permission_denied');
|
|
281
|
+
}
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
286
|
+
assertRegularOwnerStat(opened);
|
|
287
|
+
const current = currentOwnerStat(path);
|
|
288
|
+
if (current === null || current.dev !== opened.dev || current.ino !== opened.ino) {
|
|
289
|
+
throw new UnsafeLockPathError('path_changed');
|
|
290
|
+
}
|
|
291
|
+
const buffer = Buffer.alloc(MAX_LOCK_OWNER_BYTES + 1);
|
|
292
|
+
let total = 0;
|
|
293
|
+
while (total < buffer.length) {
|
|
294
|
+
const read = readSync(fd, buffer, total, buffer.length - total, total);
|
|
295
|
+
if (read === 0)
|
|
296
|
+
break;
|
|
297
|
+
total += read;
|
|
298
|
+
}
|
|
299
|
+
if (total > MAX_LOCK_OWNER_BYTES)
|
|
300
|
+
throw new UnsafeLockPathError('owner_too_large');
|
|
301
|
+
const settled = fstatSync(fd, { bigint: true });
|
|
302
|
+
assertRegularOwnerStat(settled);
|
|
303
|
+
const settledPath = currentOwnerStat(path);
|
|
304
|
+
if (settledPath === null
|
|
305
|
+
|| settledPath.dev !== settled.dev
|
|
306
|
+
|| settledPath.ino !== settled.ino
|
|
307
|
+
|| settledPath.size !== settled.size
|
|
308
|
+
|| settledPath.mtimeNs !== settled.mtimeNs) {
|
|
309
|
+
throw new UnsafeLockPathError('path_changed');
|
|
310
|
+
}
|
|
311
|
+
return { raw: buffer.subarray(0, total).toString('utf8'), stat: settled };
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
closeSync(fd);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
27
317
|
/** True if pid is a live process (cross-platform via signal 0; EPERM = exists but owned by another user). */
|
|
28
318
|
function pidAlive(pid) {
|
|
29
319
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
30
320
|
return false;
|
|
321
|
+
if (fileLockTestHooks.pidAlive)
|
|
322
|
+
return fileLockTestHooks.pidAlive(pid);
|
|
31
323
|
try {
|
|
32
324
|
process.kill(pid, 0);
|
|
33
325
|
return true;
|
|
@@ -36,20 +328,57 @@ function pidAlive(pid) {
|
|
|
36
328
|
return e.code === 'EPERM';
|
|
37
329
|
}
|
|
38
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
|
+
}
|
|
39
359
|
function parseJsonOwner(raw) {
|
|
40
360
|
try {
|
|
41
361
|
const value = JSON.parse(raw);
|
|
42
|
-
if (typeof value !== 'object' || value === null)
|
|
362
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
43
363
|
return null;
|
|
44
364
|
const record = value;
|
|
45
|
-
if (typeof record
|
|
46
|
-
|| !Number.isInteger(record
|
|
47
|
-
|| record
|
|
48
|
-
|| typeof record
|
|
49
|
-
|| 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) {
|
|
50
370
|
return null;
|
|
51
371
|
}
|
|
52
|
-
|
|
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;
|
|
53
382
|
}
|
|
54
383
|
catch (error) {
|
|
55
384
|
if (error instanceof SyntaxError)
|
|
@@ -62,7 +391,7 @@ function parseLegacyOwner(raw) {
|
|
|
62
391
|
if (first === undefined)
|
|
63
392
|
return null;
|
|
64
393
|
const pid = Number(first);
|
|
65
|
-
return Number.isInteger(pid) && pid > 0 ? { pid, token: null } : null;
|
|
394
|
+
return Number.isInteger(pid) && pid > 0 ? { pid, token: null, processStart: null } : null;
|
|
66
395
|
}
|
|
67
396
|
function parseOwner(raw) {
|
|
68
397
|
const trimmed = raw.trim();
|
|
@@ -70,19 +399,160 @@ function parseOwner(raw) {
|
|
|
70
399
|
return null;
|
|
71
400
|
return parseJsonOwner(trimmed) ?? parseLegacyOwner(trimmed);
|
|
72
401
|
}
|
|
73
|
-
|
|
74
|
-
|
|
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. */
|
|
415
|
+
function lockSnapshot(lockPath) {
|
|
75
416
|
try {
|
|
76
|
-
return
|
|
417
|
+
return readLockSnapshot(lockPath);
|
|
77
418
|
}
|
|
78
419
|
catch (error) {
|
|
79
|
-
|
|
420
|
+
// A valid owner can release and another waiter can acquire between lstat/open/fstat. Treat that snapshot as
|
|
421
|
+
// transiently unavailable: callers remain conservative (no steal/delete) and the acquire loop retries.
|
|
422
|
+
if (error instanceof UnsafeLockPathError && error.reason === 'path_changed')
|
|
80
423
|
return null;
|
|
81
424
|
throw error;
|
|
82
425
|
}
|
|
83
426
|
}
|
|
427
|
+
function sameInode(left, right) {
|
|
428
|
+
return left !== null && right !== null && left.dev === right.dev && left.ino === right.ino;
|
|
429
|
+
}
|
|
430
|
+
function sameSnapshot(left, right) {
|
|
431
|
+
return sameInode(left, right)
|
|
432
|
+
&& left?.size === right?.size
|
|
433
|
+
&& left?.mtimeNs === right?.mtimeNs;
|
|
434
|
+
}
|
|
435
|
+
function forgetMalformedObservation(lockPath) {
|
|
436
|
+
malformedLockObservations.delete(lockKey(lockPath));
|
|
437
|
+
}
|
|
438
|
+
function reclaimableSnapshot(lockPath, snapshot) {
|
|
439
|
+
if (snapshot.owner !== null) {
|
|
440
|
+
forgetMalformedObservation(lockPath);
|
|
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);
|
|
450
|
+
}
|
|
451
|
+
const key = lockKey(lockPath);
|
|
452
|
+
const now = process.hrtime.bigint();
|
|
453
|
+
const observed = malformedLockObservations.get(key);
|
|
454
|
+
if (observed === undefined || !sameSnapshot(observed.snapshot, snapshot)) {
|
|
455
|
+
malformedLockObservations.set(key, { snapshot, firstSeenAtNs: now });
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
return now - observed.firstSeenAtNs >= MALFORMED_LOCK_GRACE_NS;
|
|
459
|
+
}
|
|
84
460
|
function sameOwner(parsed, owner) {
|
|
85
|
-
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);
|
|
86
556
|
}
|
|
87
557
|
function sidecarPath(lockPath, kind) {
|
|
88
558
|
return join(dirname(lockPath), `.${basename(lockPath)}.${kind}.${process.pid}.${randomUUID()}`);
|
|
@@ -99,129 +569,614 @@ function removeFileIfExists(path) {
|
|
|
99
569
|
throw error;
|
|
100
570
|
}
|
|
101
571
|
}
|
|
102
|
-
function
|
|
572
|
+
function pathExists(path) {
|
|
573
|
+
return existsSync(path);
|
|
574
|
+
}
|
|
575
|
+
function restoreMovedFile(lockPath, movedPath, moved) {
|
|
103
576
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return
|
|
577
|
+
linkSync(movedPath, lockPath);
|
|
578
|
+
removeFileIfExists(movedPath);
|
|
579
|
+
forgetMalformedObservation(lockPath);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
if (!isErrno(error, 'EEXIST'))
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
const current = lockSnapshot(lockPath);
|
|
587
|
+
if (sameInode(current, moved)) {
|
|
588
|
+
removeFileIfExists(movedPath);
|
|
589
|
+
forgetMalformedObservation(lockPath);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
throw new Error(`文件锁在安全回收期间被替换,已保留候选文件: ${movedPath}`);
|
|
593
|
+
}
|
|
594
|
+
function removeUnchangedLock(lockPath, expected, kind, onCleanupError) {
|
|
595
|
+
const movedPath = sidecarPath(lockPath, kind);
|
|
596
|
+
try {
|
|
597
|
+
renameSync(lockPath, movedPath);
|
|
108
598
|
}
|
|
109
599
|
catch (error) {
|
|
110
|
-
if (isErrno(error, '
|
|
600
|
+
if (isErrno(error, 'ENOENT'))
|
|
111
601
|
return false;
|
|
112
602
|
throw error;
|
|
113
603
|
}
|
|
604
|
+
const moved = lockSnapshot(movedPath);
|
|
605
|
+
if (!sameSnapshot(moved, expected)) {
|
|
606
|
+
if (moved !== null)
|
|
607
|
+
restoreMovedFile(lockPath, movedPath, moved);
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
removeFileIfExists(movedPath);
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
if (onCleanupError === undefined)
|
|
615
|
+
throw error;
|
|
616
|
+
onCleanupError(error);
|
|
617
|
+
}
|
|
618
|
+
forgetMalformedObservation(lockPath);
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
function removeFileIfOwned(lockPath, owner, onCleanupError) {
|
|
622
|
+
const snapshot = lockSnapshot(lockPath);
|
|
623
|
+
if (snapshot === null || !sameOwner(snapshot.owner, owner))
|
|
624
|
+
return false;
|
|
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;
|
|
722
|
+
}
|
|
723
|
+
function createLockFile(lockPath, owner, trackOwnership) {
|
|
724
|
+
const guardPath = mutationGuardPath(lockPath);
|
|
725
|
+
if (trackOwnership && pathExists(guardPath))
|
|
726
|
+
return false;
|
|
727
|
+
if (trackOwnership && pathExists(guardianPath(lockPath)))
|
|
728
|
+
return false;
|
|
729
|
+
if (pathExists(lockPath))
|
|
730
|
+
return false;
|
|
731
|
+
for (let permissionAttempt = 0; permissionAttempt < 2; permissionAttempt++) {
|
|
732
|
+
try {
|
|
733
|
+
if (!publishOwnerFile(lockPath, owner))
|
|
734
|
+
return false;
|
|
735
|
+
if (trackOwnership && pathExists(guardPath)) {
|
|
736
|
+
removeFileIfOwned(lockPath, owner);
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
if (trackOwnership)
|
|
740
|
+
ownedLocks.set(lockKey(lockPath), { owner, releasePending: false });
|
|
741
|
+
forgetMalformedObservation(lockPath);
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
if (isErrno(error, 'EEXIST'))
|
|
746
|
+
return false;
|
|
747
|
+
if (isErrno(error, 'EACCES') || isErrno(error, 'EPERM')) {
|
|
748
|
+
// Windows can report EPERM instead of EEXIST during contention. A regular owner is busy; if the owner
|
|
749
|
+
// disappeared before lstat, retry create once to distinguish that release race from a persistent ACL
|
|
750
|
+
// failure. Unsafe/inaccessible paths still throw from currentOwnerStat and remain fail closed.
|
|
751
|
+
if (currentOwnerStat(lockPath) !== null)
|
|
752
|
+
return false;
|
|
753
|
+
if (permissionAttempt === 0)
|
|
754
|
+
continue;
|
|
755
|
+
throw new UnsafeLockPathError('permission_denied');
|
|
756
|
+
}
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
throw new UnsafeLockPathError('permission_denied');
|
|
114
761
|
}
|
|
115
762
|
function tryAcquireMutationGuard(lockPath) {
|
|
116
763
|
const guardPath = mutationGuardPath(lockPath);
|
|
117
764
|
const guard = newOwner();
|
|
118
765
|
if (createLockFile(guardPath, guard, false))
|
|
119
766
|
return guard;
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
767
|
+
const snapshot = lockSnapshot(guardPath);
|
|
768
|
+
if (snapshot === null || !reclaimableSnapshot(guardPath, snapshot))
|
|
769
|
+
return null;
|
|
770
|
+
if (!removeUnchangedLock(guardPath, snapshot, 'stale'))
|
|
122
771
|
return null;
|
|
123
|
-
|
|
772
|
+
return createLockFile(guardPath, guard, false) ? guard : null;
|
|
773
|
+
}
|
|
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;
|
|
124
782
|
try {
|
|
125
|
-
|
|
783
|
+
operationResult = operation();
|
|
784
|
+
operationCompleted = true;
|
|
126
785
|
}
|
|
127
786
|
catch (error) {
|
|
128
|
-
|
|
129
|
-
return null;
|
|
130
|
-
throw error;
|
|
787
|
+
operationError = error;
|
|
131
788
|
}
|
|
132
|
-
|
|
133
|
-
return createLockFile(guardPath, guard, false) ? guard : null;
|
|
134
|
-
}
|
|
135
|
-
function releaseMutationGuard(lockPath, guard) {
|
|
136
|
-
const guardPath = mutationGuardPath(lockPath);
|
|
137
|
-
if (!sameOwner(lockOwner(guardPath), guard))
|
|
138
|
-
return;
|
|
139
|
-
const releasedPath = sidecarPath(guardPath, 'released');
|
|
789
|
+
let releaseError;
|
|
140
790
|
try {
|
|
141
|
-
|
|
791
|
+
if (!releaseMutationGuard(lockPath, guard, onCleanupError)) {
|
|
792
|
+
releaseError = new UnsafeLockPathError('path_changed');
|
|
793
|
+
}
|
|
142
794
|
}
|
|
143
795
|
catch (error) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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;
|
|
147
803
|
}
|
|
148
|
-
|
|
804
|
+
if (releaseError !== undefined)
|
|
805
|
+
throw releaseError;
|
|
806
|
+
return operationResult;
|
|
149
807
|
}
|
|
150
808
|
function reclaimStaleLock(lockPath) {
|
|
151
809
|
const guard = tryAcquireMutationGuard(lockPath);
|
|
152
810
|
if (guard === null)
|
|
153
811
|
return false;
|
|
154
812
|
try {
|
|
155
|
-
const
|
|
156
|
-
if (
|
|
813
|
+
const snapshot = lockSnapshot(lockPath);
|
|
814
|
+
if (snapshot === null || !reclaimableSnapshot(lockPath, snapshot))
|
|
157
815
|
return false;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
816
|
+
return removeUnchangedLock(lockPath, snapshot, 'stale');
|
|
817
|
+
}
|
|
818
|
+
finally {
|
|
819
|
+
releaseMutationGuard(lockPath, guard);
|
|
820
|
+
}
|
|
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))
|
|
164
833
|
return false;
|
|
165
|
-
|
|
834
|
+
fileLockTestHooks.beforeGuardianReclaim?.(receiptPath, lockPath);
|
|
835
|
+
return removeUnchangedLock(receiptPath, observed.snapshot, 'guardian-malformed');
|
|
166
836
|
}
|
|
167
|
-
|
|
168
|
-
|
|
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');
|
|
169
849
|
}
|
|
170
850
|
finally {
|
|
171
851
|
releaseMutationGuard(lockPath, guard);
|
|
172
852
|
}
|
|
173
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
|
+
}
|
|
878
|
+
/**
|
|
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
|
+
}
|
|
174
989
|
/**
|
|
175
|
-
*
|
|
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.
|
|
176
1049
|
*
|
|
177
1050
|
* The lock file records the owner pid and token. If a waiter finds the lock held by a pid that is no longer
|
|
178
1051
|
* alive (the owner crashed without releaseLock), it reclaims the stale lock instead of spinning
|
|
179
1052
|
* until timeout — otherwise one crashed process would deadlock every future writer until the file
|
|
180
|
-
* is removed by hand.
|
|
181
|
-
*
|
|
182
|
-
*
|
|
1053
|
+
* is removed by hand. Empty or truncated locks are reclaimed only after the same inode and contents
|
|
1054
|
+
* remain malformed for a grace period, so a live creator can finish publishing its owner payload.
|
|
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
|
|
1058
|
+
* process's own) is never stolen.
|
|
183
1059
|
*
|
|
184
1060
|
* NOTE: still synchronous (blocks the event loop while waiting) by design — it guards short
|
|
185
1061
|
* synchronous critical sections (append-only writes).
|
|
186
1062
|
*/
|
|
187
1063
|
export function acquireLock(lockPath, opts = {}) {
|
|
1064
|
+
const pending = ownedLocks.get(lockKey(lockPath));
|
|
1065
|
+
if (pending?.releasePending) {
|
|
1066
|
+
const released = releaseLock(lockPath);
|
|
1067
|
+
if (!released.released)
|
|
1068
|
+
throw new LockReleaseError(released.reason);
|
|
1069
|
+
}
|
|
188
1070
|
const maxTries = opts.maxTries ?? 300;
|
|
189
1071
|
const waitMs = opts.waitMs ?? 10;
|
|
190
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
|
+
}
|
|
191
1087
|
const owner = newOwner();
|
|
192
1088
|
if (createLockFile(lockPath, owner, true)) {
|
|
193
|
-
return;
|
|
1089
|
+
return publicOwnerRecord(owner);
|
|
194
1090
|
}
|
|
195
|
-
const current =
|
|
196
|
-
if (current !== null &&
|
|
1091
|
+
const current = lockSnapshot(lockPath);
|
|
1092
|
+
if (current !== null && reclaimableSnapshot(lockPath, current)) {
|
|
197
1093
|
if (!reclaimStaleLock(lockPath))
|
|
198
1094
|
syncSleep(waitMs);
|
|
199
1095
|
continue;
|
|
200
1096
|
}
|
|
1097
|
+
if (current === null && pathExists(mutationGuardPath(lockPath))) {
|
|
1098
|
+
const guard = tryAcquireMutationGuard(lockPath);
|
|
1099
|
+
if (guard !== null)
|
|
1100
|
+
releaseMutationGuard(lockPath, guard);
|
|
1101
|
+
}
|
|
201
1102
|
syncSleep(waitMs);
|
|
202
1103
|
}
|
|
203
|
-
throw new LockTimeoutError(
|
|
1104
|
+
throw new LockTimeoutError();
|
|
204
1105
|
}
|
|
205
1106
|
export function releaseLock(lockPath) {
|
|
206
1107
|
const key = lockKey(lockPath);
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
return;
|
|
1108
|
+
const owned = ownedLocks.get(key);
|
|
1109
|
+
if (owned === undefined)
|
|
1110
|
+
return { released: true, reason: 'not_owned' };
|
|
1111
|
+
const failed = (reason) => {
|
|
1112
|
+
owned.releasePending = true;
|
|
1113
|
+
return { released: false, reason };
|
|
1114
|
+
};
|
|
1115
|
+
let guard;
|
|
210
1116
|
try {
|
|
211
|
-
|
|
212
|
-
return;
|
|
213
|
-
const releasedPath = sidecarPath(lockPath, 'released');
|
|
214
|
-
try {
|
|
215
|
-
renameSync(lockPath, releasedPath);
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
if (isErrno(error, 'ENOENT'))
|
|
219
|
-
return;
|
|
220
|
-
throw error;
|
|
221
|
-
}
|
|
222
|
-
removeFileIfExists(releasedPath);
|
|
1117
|
+
guard = tryAcquireMutationGuard(lockPath);
|
|
223
1118
|
}
|
|
224
|
-
|
|
1119
|
+
catch (error) {
|
|
1120
|
+
return failed(error instanceof UnsafeLockPathError ? error.reason : 'release_failed');
|
|
1121
|
+
}
|
|
1122
|
+
if (guard === null)
|
|
1123
|
+
return failed('release_failed');
|
|
1124
|
+
let cleanupFailed = false;
|
|
1125
|
+
let forgetOwnership = false;
|
|
1126
|
+
let result;
|
|
1127
|
+
try {
|
|
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; });
|
|
1164
|
+
}
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
return failed(error instanceof UnsafeLockPathError ? error.reason : 'release_failed');
|
|
1167
|
+
}
|
|
1168
|
+
if (result.released && forgetOwnership)
|
|
225
1169
|
ownedLocks.delete(key);
|
|
1170
|
+
return result.released && cleanupFailed
|
|
1171
|
+
? { released: true, reason: 'released_with_cleanup_error' }
|
|
1172
|
+
: result;
|
|
1173
|
+
}
|
|
1174
|
+
export class LockReleaseError extends Error {
|
|
1175
|
+
reason;
|
|
1176
|
+
code = 'LOCK_RELEASE_FAILED';
|
|
1177
|
+
constructor(reason) {
|
|
1178
|
+
super(`文件锁释放未完成: ${reason}`);
|
|
1179
|
+
this.reason = reason;
|
|
1180
|
+
this.name = 'LockReleaseError';
|
|
226
1181
|
}
|
|
227
1182
|
}
|