@evomap/evolver 1.91.2 → 1.92.1
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/README.md +13 -7
- package/index.js +150 -71
- package/package.json +2 -1
- package/src/adapters/scripts/_runtimePaths.js +10 -1
- package/src/adapters/scripts/evolver-session-end.js +10 -1
- package/src/canonicalIdentityLock.js +455 -0
- package/src/evolve/guards.js +1 -1
- package/src/evolve/pipeline/collect.js +1 -1
- package/src/evolve/pipeline/dispatch.js +1 -1
- package/src/evolve/pipeline/enrich.js +1 -1
- package/src/evolve/pipeline/hub.js +1 -1
- package/src/evolve/pipeline/select.js +1 -1
- package/src/evolve/pipeline/signals.js +1 -1
- package/src/evolve/utils.js +1 -1
- package/src/evolve.js +1 -1
- package/src/gep/a2aProtocol.js +1 -5175
- package/src/gep/antiAbuseTelemetry.js +1 -233
- package/src/gep/assetStore.js +56 -12
- package/src/gep/autoDistillConv.js +1 -205
- package/src/gep/autoDistillLlm.js +1 -315
- package/src/gep/candidateEval.js +1 -92
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -30
- package/src/gep/conversationDistiller.js +1 -270
- package/src/gep/conversationSniffer.js +1 -266
- package/src/gep/crypto.js +1 -93
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -218
- package/src/gep/envFingerprint.js +1 -118
- package/src/gep/epigenetics.js +1 -31
- package/src/gep/execBridge.js +1 -712
- package/src/gep/explore.js +1 -289
- package/src/gep/hash.js +1 -15
- package/src/gep/hubFetch.js +1 -672
- package/src/gep/hubReview.js +1 -212
- package/src/gep/hubSearch.js +1 -544
- package/src/gep/hubVerify.js +1 -306
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/memoryGraph.js +1 -1449
- package/src/gep/memoryGraphAdapter.js +1 -203
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -205
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -599
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallInject.js +1 -409
- package/src/gep/recallVerifier.js +1 -318
- package/src/gep/reflection.js +1 -1
- package/src/gep/savingsCore.js +1 -1
- package/src/gep/schemas/gene.js +2 -0
- package/src/gep/selector.js +1 -1
- package/src/gep/skillDistiller.js +1 -1294
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -136
- package/src/gep/syncAsset.js +1 -0
- package/src/gep/tokenSavings.js +1 -1
- package/src/gep/trajectoryExport.js +1 -3841
- package/src/gep/workspaceKeychain.js +1 -174
- package/src/proxy/extensions/traceControl.js +1 -123
- package/src/proxy/index.js +136 -8
- package/src/proxy/inject.js +1 -52
- package/src/proxy/lifecycle/manager.js +279 -18
- package/src/proxy/mailbox/state.js +60 -29
- package/src/proxy/trace/extractor.js +1 -2355
- package/src/proxy/trace/usage.js +1 -105
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const PRIVATE_DIR_MODE = 0o700;
|
|
9
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
10
|
+
const DEFAULT_LOCK_WAIT_MS = 10;
|
|
11
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 10_000;
|
|
12
|
+
const UNKNOWN_OWNER_STALE_MS = 60_000;
|
|
13
|
+
const heldLocks = new Map();
|
|
14
|
+
let lockWaitMs = DEFAULT_LOCK_WAIT_MS;
|
|
15
|
+
let lockTimeoutMs = DEFAULT_LOCK_TIMEOUT_MS;
|
|
16
|
+
let beforeAbandonedLockUnlinkForTesting = null;
|
|
17
|
+
let processStartIdentityReaderForTesting = null;
|
|
18
|
+
|
|
19
|
+
function sleepSync(ms) {
|
|
20
|
+
try {
|
|
21
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
22
|
+
} catch {
|
|
23
|
+
const end = Date.now() + ms;
|
|
24
|
+
while (Date.now() < end) {}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function processIsAlive(pid) {
|
|
29
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, 0);
|
|
32
|
+
return true;
|
|
33
|
+
} catch (e) {
|
|
34
|
+
if (e && e.code === 'ESRCH') return false;
|
|
35
|
+
if (e && e.code === 'EPERM') return true;
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readLinuxProcessStartIdentity(pid) {
|
|
41
|
+
let bootId;
|
|
42
|
+
let statContents;
|
|
43
|
+
try {
|
|
44
|
+
bootId = fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
|
|
45
|
+
statContents = fs.readFileSync(`/proc/${pid}/stat`, 'utf8').trim();
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (!/^[a-f0-9-]{36}$/i.test(bootId)) return null;
|
|
50
|
+
|
|
51
|
+
// A process name may contain spaces and parentheses, so fields after comm
|
|
52
|
+
// must be parsed from the final closing parenthesis. starttime is field 22.
|
|
53
|
+
const closeParen = statContents.lastIndexOf(')');
|
|
54
|
+
const openParen = statContents.indexOf('(');
|
|
55
|
+
if (openParen <= 0 || closeParen <= openParen) return null;
|
|
56
|
+
const statPid = Number(statContents.slice(0, openParen).trim());
|
|
57
|
+
if (statPid !== pid) return null;
|
|
58
|
+
const fieldsAfterComm = statContents.slice(closeParen + 1).trim().split(/\s+/);
|
|
59
|
+
const startTimeTicks = fieldsAfterComm[19];
|
|
60
|
+
if (!/^\d+$/.test(startTimeTicks || '')) return null;
|
|
61
|
+
return `linux:${bootId}:${startTimeTicks}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runDarwinIdentityCommand(file, args) {
|
|
65
|
+
const result = spawnSync(file, args, {
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
env: { ...process.env, LANG: 'C', LC_ALL: 'C', TZ: 'UTC' },
|
|
68
|
+
maxBuffer: 16 * 1024,
|
|
69
|
+
timeout: 1_000,
|
|
70
|
+
windowsHide: true,
|
|
71
|
+
});
|
|
72
|
+
if (result.error || result.status !== 0 || typeof result.stdout !== 'string') return null;
|
|
73
|
+
return result.stdout.trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readDarwinProcessStartIdentity(pid) {
|
|
77
|
+
const bootOutput = runDarwinIdentityCommand('/usr/sbin/sysctl', ['-n', 'kern.boottime']);
|
|
78
|
+
const bootMatch = /\{\s*sec\s*=\s*(\d+)\s*,\s*usec\s*=\s*(\d+)\s*\}/.exec(bootOutput || '');
|
|
79
|
+
if (!bootMatch) return null;
|
|
80
|
+
|
|
81
|
+
const processOutput = runDarwinIdentityCommand('/bin/ps', [
|
|
82
|
+
'-o', 'pid=',
|
|
83
|
+
'-o', 'lstart=',
|
|
84
|
+
'-p', String(pid),
|
|
85
|
+
]);
|
|
86
|
+
const processMatch = /^(\d+)\s+(.+)$/.exec(processOutput || '');
|
|
87
|
+
if (!processMatch || Number(processMatch[1]) !== pid) return null;
|
|
88
|
+
const startedAt = processMatch[2].trim().replace(/\s+/g, ' ');
|
|
89
|
+
if (!startedAt) return null;
|
|
90
|
+
return `darwin:${bootMatch[1]}.${bootMatch[2]}:${startedAt}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readProcessStartIdentity(pid) {
|
|
94
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
95
|
+
if (processStartIdentityReaderForTesting) {
|
|
96
|
+
const value = processStartIdentityReaderForTesting(pid);
|
|
97
|
+
return typeof value === 'string' && value ? value : null;
|
|
98
|
+
}
|
|
99
|
+
if (process.platform === 'linux') return readLinuxProcessStartIdentity(pid);
|
|
100
|
+
if (process.platform === 'darwin') return readDarwinProcessStartIdentity(pid);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function ownerIsProvablyDead(owner) {
|
|
105
|
+
const alive = processIsAlive(owner.pid);
|
|
106
|
+
if (alive === false) return true;
|
|
107
|
+
if (alive !== true || !owner.processStartIdentity) return false;
|
|
108
|
+
const currentIdentity = readProcessStartIdentity(owner.pid);
|
|
109
|
+
return Boolean(currentIdentity && currentIdentity !== owner.processStartIdentity);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readOwner(ownerFile) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
|
|
115
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
116
|
+
const pid = Number(parsed.pid);
|
|
117
|
+
const token = typeof parsed.token === 'string' ? parsed.token : '';
|
|
118
|
+
const processStartIdentity = typeof parsed.processStartIdentity === 'string'
|
|
119
|
+
? parsed.processStartIdentity
|
|
120
|
+
: '';
|
|
121
|
+
if (!Number.isInteger(pid) || pid <= 0 || !token) return null;
|
|
122
|
+
return { pid, token, processStartIdentity };
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function discoverOwner(lockDir) {
|
|
129
|
+
let names;
|
|
130
|
+
try {
|
|
131
|
+
names = fs.readdirSync(lockDir);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (names.length !== 1) return null;
|
|
136
|
+
const match = /^owner\.([a-zA-Z0-9-]+)\.json$/.exec(names[0]);
|
|
137
|
+
if (!match) return null;
|
|
138
|
+
const token = match[1];
|
|
139
|
+
const ownerFile = path.join(lockDir, names[0]);
|
|
140
|
+
let contents;
|
|
141
|
+
try {
|
|
142
|
+
contents = fs.readFileSync(ownerFile);
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const owner = readOwner(ownerFile);
|
|
147
|
+
if (!owner || owner.token !== token) {
|
|
148
|
+
return { kind: 'malformed', owner: null, ownerFile, token, contents };
|
|
149
|
+
}
|
|
150
|
+
return { kind: 'valid', owner, ownerFile, token, contents };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readLockIdentity(lockDir) {
|
|
154
|
+
try {
|
|
155
|
+
const stat = fs.statSync(lockDir, { bigint: true });
|
|
156
|
+
return {
|
|
157
|
+
dev: String(stat.dev),
|
|
158
|
+
ino: String(stat.ino),
|
|
159
|
+
mtimeNs: String(stat.mtimeNs),
|
|
160
|
+
mtimeMs: Number(stat.mtimeMs),
|
|
161
|
+
};
|
|
162
|
+
} catch (e) {
|
|
163
|
+
if (e && e.code === 'ENOENT') return null;
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function sameOwner(left, right) {
|
|
169
|
+
return Boolean(
|
|
170
|
+
left &&
|
|
171
|
+
right &&
|
|
172
|
+
left.pid === right.pid &&
|
|
173
|
+
left.token === right.token &&
|
|
174
|
+
left.processStartIdentity === right.processStartIdentity
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sameLockIdentity(left, right) {
|
|
179
|
+
return Boolean(
|
|
180
|
+
left &&
|
|
181
|
+
right &&
|
|
182
|
+
left.dev === right.dev &&
|
|
183
|
+
left.ino === right.ino &&
|
|
184
|
+
left.mtimeNs === right.mtimeNs
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function sameDiscoveredOwner(left, right) {
|
|
189
|
+
if (!left || !right || left.kind !== right.kind) return false;
|
|
190
|
+
if (left.ownerFile !== right.ownerFile || left.token !== right.token) return false;
|
|
191
|
+
if (!Buffer.isBuffer(left.contents) || !Buffer.isBuffer(right.contents)) return false;
|
|
192
|
+
if (!left.contents.equals(right.contents)) return false;
|
|
193
|
+
if (left.kind === 'valid') return sameOwner(left.owner, right.owner);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function currentAbandonedSnapshotMatches(lockDir, expected) {
|
|
198
|
+
const identity = readLockIdentity(lockDir);
|
|
199
|
+
if (!sameLockIdentity(identity, expected && expected.identity)) return false;
|
|
200
|
+
if (expected.kind === 'empty') {
|
|
201
|
+
try {
|
|
202
|
+
return fs.readdirSync(lockDir).length === 0;
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return sameDiscoveredOwner(discoverOwner(lockDir), expected);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function removeAbandonedLock(lockDir, expected) {
|
|
211
|
+
if (!expected || !currentAbandonedSnapshotMatches(lockDir, expected)) return false;
|
|
212
|
+
|
|
213
|
+
if (expected.kind === 'valid') {
|
|
214
|
+
if (!ownerIsProvablyDead(expected.owner)) return false;
|
|
215
|
+
} else if (Date.now() - expected.identity.mtimeMs <= UNKNOWN_OWNER_STALE_MS) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Reject replacements that happened after stale classification. The
|
|
220
|
+
// tokenized unlink below also protects the final post-check ABA window.
|
|
221
|
+
if (beforeAbandonedLockUnlinkForTesting) {
|
|
222
|
+
beforeAbandonedLockUnlinkForTesting({ lockDir, expected });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!currentAbandonedSnapshotMatches(lockDir, expected)) return false;
|
|
226
|
+
if (expected.kind === 'empty') {
|
|
227
|
+
try {
|
|
228
|
+
fs.rmdirSync(lockDir);
|
|
229
|
+
return true;
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return Boolean(e && e.code === 'ENOENT');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
// Tokenized owner paths are the deletion CAS. If a successor replaced the
|
|
237
|
+
// stale directory after the final check, this exact old path is absent;
|
|
238
|
+
// it can never resolve to the successor's different owner token.
|
|
239
|
+
fs.unlinkSync(expected.ownerFile);
|
|
240
|
+
} catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
const emptiedIdentity = readLockIdentity(lockDir);
|
|
244
|
+
try {
|
|
245
|
+
fs.rmdirSync(lockDir);
|
|
246
|
+
return true;
|
|
247
|
+
} catch (e) {
|
|
248
|
+
if (e && e.code === 'ENOENT') return true;
|
|
249
|
+
// If an unexpected directory entry prevented removal, restore the stale
|
|
250
|
+
// owner marker when this is still the same directory. That keeps a later
|
|
251
|
+
// safe recovery attempt possible instead of leaving an ownerless wedge.
|
|
252
|
+
try {
|
|
253
|
+
const after = readLockIdentity(lockDir);
|
|
254
|
+
if (sameLockIdentity(after, emptiedIdentity) && fs.readdirSync(lockDir).length === 0) {
|
|
255
|
+
fs.writeFileSync(expected.ownerFile, expected.contents, {
|
|
256
|
+
mode: PRIVATE_FILE_MODE,
|
|
257
|
+
flag: 'wx',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
} catch { /* the failed removal remains authoritative */ }
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function abandonedLockSnapshot(lockDir) {
|
|
266
|
+
const identity = readLockIdentity(lockDir);
|
|
267
|
+
if (!identity) return null;
|
|
268
|
+
const discovered = discoverOwner(lockDir);
|
|
269
|
+
if (!discovered) {
|
|
270
|
+
try {
|
|
271
|
+
if (fs.readdirSync(lockDir).length !== 0) return null;
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
if (Date.now() - identity.mtimeMs <= UNKNOWN_OWNER_STALE_MS) return null;
|
|
276
|
+
return { kind: 'empty', identity };
|
|
277
|
+
}
|
|
278
|
+
if (discovered.kind === 'malformed') {
|
|
279
|
+
if (Date.now() - identity.mtimeMs <= UNKNOWN_OWNER_STALE_MS) return null;
|
|
280
|
+
return { ...discovered, identity };
|
|
281
|
+
}
|
|
282
|
+
const { owner } = discovered;
|
|
283
|
+
if (!ownerIsProvablyDead(owner)) return null;
|
|
284
|
+
return { ...discovered, identity };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function prepareOwnerFile(lockDir, token) {
|
|
288
|
+
const preparedOwnerFile = `${lockDir}.owner.${token}.tmp`;
|
|
289
|
+
const processStartIdentity = readProcessStartIdentity(process.pid);
|
|
290
|
+
let descriptor = null;
|
|
291
|
+
try {
|
|
292
|
+
fs.writeFileSync(preparedOwnerFile, JSON.stringify({
|
|
293
|
+
pid: process.pid,
|
|
294
|
+
token,
|
|
295
|
+
processStartIdentity,
|
|
296
|
+
}), {
|
|
297
|
+
encoding: 'utf8',
|
|
298
|
+
mode: PRIVATE_FILE_MODE,
|
|
299
|
+
flag: 'wx',
|
|
300
|
+
});
|
|
301
|
+
descriptor = fs.openSync(preparedOwnerFile, 'r');
|
|
302
|
+
fs.fdatasyncSync(descriptor);
|
|
303
|
+
fs.closeSync(descriptor);
|
|
304
|
+
descriptor = null;
|
|
305
|
+
const owner = readOwner(preparedOwnerFile);
|
|
306
|
+
if (
|
|
307
|
+
!owner ||
|
|
308
|
+
owner.token !== token ||
|
|
309
|
+
owner.pid !== process.pid ||
|
|
310
|
+
owner.processStartIdentity !== (processStartIdentity || '')
|
|
311
|
+
) {
|
|
312
|
+
const err = new Error('failed to prepare canonical identity lock owner');
|
|
313
|
+
err.code = 'CANONICAL_IDENTITY_LOCK_OWNER_INVALID';
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
return preparedOwnerFile;
|
|
317
|
+
} catch (e) {
|
|
318
|
+
if (descriptor !== null) {
|
|
319
|
+
try { fs.closeSync(descriptor); } catch { /* preparation error remains primary */ }
|
|
320
|
+
}
|
|
321
|
+
try { fs.unlinkSync(preparedOwnerFile); } catch { /* preparation error remains primary */ }
|
|
322
|
+
throw e;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function releaseCanonicalIdentityLock(lockDir, ownerFile, token) {
|
|
327
|
+
const owner = readOwner(ownerFile);
|
|
328
|
+
if (!owner || owner.token !== token) {
|
|
329
|
+
const err = new Error('canonical identity lock ownership was lost');
|
|
330
|
+
err.code = 'CANONICAL_IDENTITY_LOCK_LOST';
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const releaseDir = `${lockDir}.release.${token}`;
|
|
335
|
+
fs.renameSync(lockDir, releaseDir);
|
|
336
|
+
const releaseOwnerFile = path.join(releaseDir, path.basename(ownerFile));
|
|
337
|
+
const movedOwner = readOwner(releaseOwnerFile);
|
|
338
|
+
if (!movedOwner || movedOwner.token !== token) {
|
|
339
|
+
try { fs.renameSync(releaseDir, lockDir); } catch { /* ownership error remains primary */ }
|
|
340
|
+
const err = new Error('canonical identity lock ownership was lost');
|
|
341
|
+
err.code = 'CANONICAL_IDENTITY_LOCK_LOST';
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
fs.unlinkSync(releaseOwnerFile);
|
|
345
|
+
fs.rmdirSync(releaseDir);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function acquireCanonicalIdentityLock(nodeIdFile) {
|
|
349
|
+
const canonicalFile = path.resolve(nodeIdFile);
|
|
350
|
+
const lockDir = `${canonicalFile}.tuple.lock`;
|
|
351
|
+
const token = `${process.pid}-${crypto.randomBytes(12).toString('hex')}`;
|
|
352
|
+
const ownerFile = path.join(lockDir, `owner.${token}.json`);
|
|
353
|
+
const startedAt = Date.now();
|
|
354
|
+
let preparedOwnerFile = null;
|
|
355
|
+
|
|
356
|
+
fs.mkdirSync(path.dirname(canonicalFile), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
357
|
+
preparedOwnerFile = prepareOwnerFile(lockDir, token);
|
|
358
|
+
try {
|
|
359
|
+
while (true) {
|
|
360
|
+
try {
|
|
361
|
+
fs.mkdirSync(lockDir, { mode: PRIVATE_DIR_MODE });
|
|
362
|
+
try {
|
|
363
|
+
fs.renameSync(preparedOwnerFile, ownerFile);
|
|
364
|
+
preparedOwnerFile = null;
|
|
365
|
+
} catch (e) {
|
|
366
|
+
try { fs.rmdirSync(lockDir); } catch { /* publication error remains primary */ }
|
|
367
|
+
throw e;
|
|
368
|
+
}
|
|
369
|
+
return function releaseLock() {
|
|
370
|
+
releaseCanonicalIdentityLock(lockDir, ownerFile, token);
|
|
371
|
+
};
|
|
372
|
+
} catch (e) {
|
|
373
|
+
if (!e || e.code !== 'EEXIST') throw e;
|
|
374
|
+
const abandoned = abandonedLockSnapshot(lockDir);
|
|
375
|
+
if (abandoned) {
|
|
376
|
+
if (removeAbandonedLock(lockDir, abandoned)) continue;
|
|
377
|
+
}
|
|
378
|
+
if (Date.now() - startedAt > lockTimeoutMs) {
|
|
379
|
+
const err = new Error('timed out waiting for canonical identity lock');
|
|
380
|
+
err.code = 'CANONICAL_IDENTITY_LOCK_TIMEOUT';
|
|
381
|
+
throw err;
|
|
382
|
+
}
|
|
383
|
+
sleepSync(lockWaitMs);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} finally {
|
|
387
|
+
if (preparedOwnerFile) {
|
|
388
|
+
try { fs.unlinkSync(preparedOwnerFile); } catch { /* acquisition error remains primary */ }
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function _setCanonicalIdentityLockTimingForTesting(options) {
|
|
394
|
+
const next = options || {};
|
|
395
|
+
lockWaitMs = Number.isFinite(next.waitMs) && next.waitMs >= 0
|
|
396
|
+
? Number(next.waitMs)
|
|
397
|
+
: DEFAULT_LOCK_WAIT_MS;
|
|
398
|
+
lockTimeoutMs = Number.isFinite(next.timeoutMs) && next.timeoutMs >= 0
|
|
399
|
+
? Number(next.timeoutMs)
|
|
400
|
+
: DEFAULT_LOCK_TIMEOUT_MS;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function _resetCanonicalIdentityLockTimingForTesting() {
|
|
404
|
+
lockWaitMs = DEFAULT_LOCK_WAIT_MS;
|
|
405
|
+
lockTimeoutMs = DEFAULT_LOCK_TIMEOUT_MS;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function _setBeforeAbandonedLockUnlinkForTesting(callback) {
|
|
409
|
+
beforeAbandonedLockUnlinkForTesting = typeof callback === 'function' ? callback : null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function _setProcessStartIdentityReaderForTesting(callback) {
|
|
413
|
+
processStartIdentityReaderForTesting = typeof callback === 'function' ? callback : null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Run a synchronous canonical identity/credential operation under the shared
|
|
418
|
+
* cross-process lock. Nested calls in the same process reuse the lock.
|
|
419
|
+
*
|
|
420
|
+
* @template T
|
|
421
|
+
* @param {string} nodeIdFile
|
|
422
|
+
* @param {() => T} operation
|
|
423
|
+
* @returns {T}
|
|
424
|
+
*/
|
|
425
|
+
function withCanonicalIdentityLock(nodeIdFile, operation) {
|
|
426
|
+
if (typeof operation !== 'function') throw new TypeError('operation must be a function');
|
|
427
|
+
const key = path.resolve(nodeIdFile);
|
|
428
|
+
const held = heldLocks.get(key);
|
|
429
|
+
if (held) {
|
|
430
|
+
held.depth += 1;
|
|
431
|
+
try {
|
|
432
|
+
return operation();
|
|
433
|
+
} finally {
|
|
434
|
+
held.depth -= 1;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const release = acquireCanonicalIdentityLock(key);
|
|
439
|
+
heldLocks.set(key, { depth: 1 });
|
|
440
|
+
try {
|
|
441
|
+
return operation();
|
|
442
|
+
} finally {
|
|
443
|
+
heldLocks.delete(key);
|
|
444
|
+
release();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
module.exports = {
|
|
449
|
+
acquireCanonicalIdentityLock,
|
|
450
|
+
withCanonicalIdentityLock,
|
|
451
|
+
_setCanonicalIdentityLockTimingForTesting,
|
|
452
|
+
_resetCanonicalIdentityLockTimingForTesting,
|
|
453
|
+
_setBeforeAbandonedLockUnlinkForTesting,
|
|
454
|
+
_setProcessStartIdentityReaderForTesting,
|
|
455
|
+
};
|