wendkeep 0.61.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.en.md +18 -7
- package/README.md +18 -7
- package/docs/en/commands/operating-profiles.md +26 -4
- package/docs/pt-BR/commands/operating-profiles.md +26 -3
- package/hooks/locale.mjs +1 -73
- package/hooks/sensors-core.mjs +1 -102
- package/hooks/vault-runtime-store.mjs +1 -558
- package/package.json +4 -2
- package/packages/harness/package.json +2 -1
- package/packages/harness/src/flow-store.mjs +558 -0
- package/packages/harness/src/index.mjs +3 -0
- package/packages/harness/src/operating-profile.mjs +133 -0
- package/packages/harness/src/sensors-core.mjs +102 -0
- package/packages/vault/src/index.mjs +1 -0
- package/packages/vault/src/locale.mjs +74 -0
- package/packages/vault/src/vault-path-safety.mjs +321 -152
- package/src/operating-profile.mjs +1 -133
|
@@ -12,6 +12,12 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
|
12
12
|
|
|
13
13
|
export const VAULT_LOCK_BUSY = Symbol('wendkeep:vault-lock-busy');
|
|
14
14
|
export const VAULT_LOCK_OWNER_FILE = '.owner.json';
|
|
15
|
+
const VAULT_PATH_FAILURE = Symbol('wendkeep:vault-path-failure');
|
|
16
|
+
const VAULT_PATH_CREATED = Symbol('wendkeep:vault-path-created');
|
|
17
|
+
const VAULT_LOCK_RETRY_DEADLINE = Symbol('wendkeep:vault-lock-retry-deadline');
|
|
18
|
+
const VAULT_LOCK_TOPOLOGY_RETRIES = 3;
|
|
19
|
+
const VAULT_LOCK_TOPOLOGY_RETRY_MS = 10;
|
|
20
|
+
const VAULT_LOCK_RELEASE_RETRY_WINDOW_MS = 50;
|
|
15
21
|
|
|
16
22
|
function pathKey(value) {
|
|
17
23
|
const normalized = resolve(value).replaceAll('\\', '/').replace(/^\\\\\?\//, '');
|
|
@@ -106,6 +112,10 @@ export function assertVaultPathSafe(vaultBase, targetPath, {
|
|
|
106
112
|
} catch (cause) {
|
|
107
113
|
const error = unsafe(`${label} possui componente dangling ou irresolvível: ${logicalCursor}`, code);
|
|
108
114
|
error.cause = cause;
|
|
115
|
+
error[VAULT_PATH_FAILURE] = {
|
|
116
|
+
kind: 'component-realpath',
|
|
117
|
+
component: logicalCursor,
|
|
118
|
+
};
|
|
109
119
|
throw error;
|
|
110
120
|
}
|
|
111
121
|
const expectedPhysical = join(physicalRoot, ...segments.slice(0, index + 1));
|
|
@@ -118,8 +128,23 @@ export function assertVaultPathSafe(vaultBase, targetPath, {
|
|
|
118
128
|
}
|
|
119
129
|
}
|
|
120
130
|
|
|
121
|
-
if (!exists && !allowMissing)
|
|
122
|
-
|
|
131
|
+
if (!exists && !allowMissing) {
|
|
132
|
+
const error = unsafe(`${label} inexistente: ${target}`, code);
|
|
133
|
+
error[VAULT_PATH_FAILURE] = {
|
|
134
|
+
kind: 'component-missing',
|
|
135
|
+
component: target,
|
|
136
|
+
causeCode: 'ENOENT',
|
|
137
|
+
};
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
if (exists && mustNotExist) {
|
|
141
|
+
const error = unsafe(`${label} preexistente: ${target}`, code);
|
|
142
|
+
error[VAULT_PATH_FAILURE] = {
|
|
143
|
+
kind: 'target-preexisting',
|
|
144
|
+
component: target,
|
|
145
|
+
};
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
123
148
|
if (exists) {
|
|
124
149
|
if (targetStat?.isFile() && targetStat.nlink > 1) {
|
|
125
150
|
throw unsafe(`${label} preexistente possui hardlink (nlink=${targetStat.nlink}): ${target}`, code);
|
|
@@ -160,16 +185,25 @@ export function mkdirVaultPath(vaultBase, targetPath, {
|
|
|
160
185
|
let checked = assertVaultPathSafe(vaultBase, targetPath, {
|
|
161
186
|
expectedType: 'directory', label, code,
|
|
162
187
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
188
|
+
let created = false;
|
|
189
|
+
try {
|
|
190
|
+
if (exclusive || !checked.exists) {
|
|
191
|
+
// Deliberately adjacent to mkdir: this is the last userspace check before mutation.
|
|
192
|
+
checked = assertVaultPathSafe(vaultBase, targetPath, {
|
|
193
|
+
expectedType: 'directory', label, code,
|
|
194
|
+
});
|
|
195
|
+
mkdirSync(checked.target, { recursive: exclusive ? false : recursive });
|
|
196
|
+
created = true;
|
|
197
|
+
}
|
|
198
|
+
return assertVaultPathSafe(vaultBase, targetPath, {
|
|
199
|
+
allowMissing: false, expectedType: 'directory', label, code,
|
|
200
|
+
}).target;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
// Callers may safely clean only a directory whose mkdir syscall actually succeeded.
|
|
203
|
+
// In particular, EEXIST must never authorize cleanup of a pre-existing path.
|
|
204
|
+
if (created) error[VAULT_PATH_CREATED] = checked.target;
|
|
205
|
+
throw error;
|
|
169
206
|
}
|
|
170
|
-
return assertVaultPathSafe(vaultBase, targetPath, {
|
|
171
|
-
allowMissing: false, expectedType: 'directory', label, code,
|
|
172
|
-
}).target;
|
|
173
207
|
}
|
|
174
208
|
|
|
175
209
|
export function writeVaultFileSync(vaultBase, targetPath, content, encoding = 'utf8', {
|
|
@@ -301,25 +335,69 @@ function waitBriefly(ms) {
|
|
|
301
335
|
Atomics.wait(signal, 0, 0, ms);
|
|
302
336
|
}
|
|
303
337
|
|
|
304
|
-
// A lock may legitimately disappear
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
338
|
+
// A public lock may legitimately disappear while another owner releases it. Only ENOENT
|
|
339
|
+
// observed by an operation explicitly scoped to that canonical lock receives bounded
|
|
340
|
+
// backoff; private .pending paths and every unsafe topology fail closed.
|
|
341
|
+
function retryablePublicLockError(lock, error, code, { allowRaw = true } = {}) {
|
|
342
|
+
const failure = error?.[VAULT_PATH_FAILURE];
|
|
343
|
+
if (!failure) return allowRaw && error?.code === 'ENOENT' && !error?.cause;
|
|
344
|
+
const causeCode = error?.cause?.code || failure.causeCode;
|
|
345
|
+
return causeCode === 'ENOENT'
|
|
346
|
+
&& (error?.code === code || error?.code === 'VAULT_PATH_UNSAFE')
|
|
347
|
+
&& ['component-realpath', 'component-missing'].includes(failure.kind)
|
|
348
|
+
&& containedBy(resolve(lock), resolve(failure.component));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function vaultLockRenameCollision(error, pending, lock) {
|
|
352
|
+
const acceptedCodes = process.platform === 'win32'
|
|
353
|
+
? ['EEXIST', 'ENOTEMPTY', 'EPERM']
|
|
354
|
+
: ['EEXIST', 'ENOTEMPTY'];
|
|
355
|
+
return acceptedCodes.includes(error?.code)
|
|
356
|
+
&& error?.syscall === 'rename'
|
|
357
|
+
&& typeof error?.path === 'string'
|
|
358
|
+
&& typeof error?.dest === 'string'
|
|
359
|
+
&& pathKey(error.path) === pathKey(pending)
|
|
360
|
+
&& pathKey(error.dest) === pathKey(lock);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function preexistingPublicLockError(lock, error, code) {
|
|
364
|
+
const failure = error?.[VAULT_PATH_FAILURE];
|
|
365
|
+
return failure?.kind === 'target-preexisting'
|
|
366
|
+
&& (error?.code === code || error?.code === 'VAULT_PATH_UNSAFE')
|
|
367
|
+
&& typeof failure.component === 'string'
|
|
368
|
+
&& pathKey(failure.component) === pathKey(lock);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function vaultLockRetryDeadlineError() {
|
|
372
|
+
const error = new Error('deadline de retry do lock do Vault esgotado');
|
|
373
|
+
error[VAULT_LOCK_RETRY_DEADLINE] = true;
|
|
374
|
+
return error;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function withPublicLockRetry(lock, code, retryState, operation, initialError = null) {
|
|
378
|
+
let error = initialError;
|
|
379
|
+
while (true) {
|
|
380
|
+
if (error) {
|
|
381
|
+
if (!retryablePublicLockError(lock, error, code)
|
|
382
|
+
|| retryState.remaining <= 0) throw error;
|
|
383
|
+
const remainingMs = retryState.deadline - Date.now();
|
|
384
|
+
if (remainingMs <= 0) throw vaultLockRetryDeadlineError();
|
|
385
|
+
retryState.remaining -= 1;
|
|
386
|
+
waitBriefly(Math.min(VAULT_LOCK_TOPOLOGY_RETRY_MS, remainingMs));
|
|
387
|
+
if (Date.now() >= retryState.deadline) throw vaultLockRetryDeadlineError();
|
|
388
|
+
}
|
|
313
389
|
try {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return true;
|
|
318
|
-
} catch (recheckError) {
|
|
319
|
-
if (recheckError?.cause?.code !== 'ENOENT') return false;
|
|
390
|
+
return operation();
|
|
391
|
+
} catch (nextError) {
|
|
392
|
+
error = nextError;
|
|
320
393
|
}
|
|
321
394
|
}
|
|
322
|
-
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function inspectVaultLock(vaultBase, lock, code, retryState, initialError = null) {
|
|
398
|
+
return withPublicLockRetry(lock, code, retryState, () => assertVaultPathSafe(vaultBase, lock, {
|
|
399
|
+
expectedType: 'directory', label: 'lock de escrita do Vault', code,
|
|
400
|
+
}), initialError);
|
|
323
401
|
}
|
|
324
402
|
|
|
325
403
|
function processIsAlive(pid) {
|
|
@@ -333,23 +411,44 @@ function processIsAlive(pid) {
|
|
|
333
411
|
}
|
|
334
412
|
}
|
|
335
413
|
|
|
336
|
-
function vaultLockOwner(vaultBase, lock, code) {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
414
|
+
function vaultLockOwner(vaultBase, lock, code, retryState = null) {
|
|
415
|
+
const inspect = () => {
|
|
416
|
+
const checkedLock = assertVaultPathSafe(vaultBase, lock, {
|
|
417
|
+
expectedType: 'directory', label: 'lock de escrita do Vault', code,
|
|
418
|
+
});
|
|
419
|
+
const path = join(lock, VAULT_LOCK_OWNER_FILE);
|
|
420
|
+
if (!checkedLock.exists) {
|
|
421
|
+
return {
|
|
422
|
+
lockExists: false, ownerExists: false, owner: null, path, raw: '',
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const checked = assertVaultPathSafe(vaultBase, path, {
|
|
426
|
+
expectedType: 'file', label: 'owner do lock de escrita do Vault', code,
|
|
427
|
+
});
|
|
428
|
+
if (!checked.exists) {
|
|
429
|
+
return {
|
|
430
|
+
lockExists: true, ownerExists: false, owner: null, path: checked.target, raw: '',
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const raw = readFileSync(checked.target, 'utf8');
|
|
434
|
+
try {
|
|
435
|
+
const owner = JSON.parse(raw);
|
|
436
|
+
const valid = owner?.v === 1
|
|
437
|
+
&& Number.isSafeInteger(owner.pid) && owner.pid > 0
|
|
438
|
+
&& typeof owner.token === 'string' && owner.token.length > 0
|
|
439
|
+
&& typeof owner.created_at === 'string' && owner.created_at.length > 0;
|
|
440
|
+
return {
|
|
441
|
+
lockExists: true, ownerExists: true, owner: valid ? owner : null, path: checked.target, raw,
|
|
442
|
+
};
|
|
443
|
+
} catch {
|
|
444
|
+
return {
|
|
445
|
+
lockExists: true, ownerExists: true, owner: null, path: checked.target, raw,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
return retryState
|
|
450
|
+
? withPublicLockRetry(lock, code, retryState, inspect)
|
|
451
|
+
: inspect();
|
|
353
452
|
}
|
|
354
453
|
|
|
355
454
|
function vaultLockLease(lock, token) {
|
|
@@ -383,91 +482,138 @@ function removePreparedVaultLock(vaultBase, lock, token, code) {
|
|
|
383
482
|
});
|
|
384
483
|
}
|
|
385
484
|
|
|
386
|
-
function releaseOwnedVaultLock(vaultBase, lock, {
|
|
387
|
-
|
|
485
|
+
function releaseOwnedVaultLock(vaultBase, lock, {
|
|
486
|
+
pid, token, code, retryState,
|
|
487
|
+
}) {
|
|
488
|
+
const observedState = vaultLockOwner(vaultBase, lock, code, retryState);
|
|
489
|
+
if (!observedState.lockExists) return true;
|
|
490
|
+
const observed = observedState.owner;
|
|
388
491
|
if (observed?.pid !== pid || observed?.token !== token) return false;
|
|
389
492
|
// The token-specific lease is the filesystem CAS. An old finally/reaper can only
|
|
390
493
|
// remove the directory after successfully unlinking the lease it originally saw;
|
|
391
494
|
// a replacement lock never contains that unguessable path.
|
|
392
|
-
const leaseRemoved =
|
|
393
|
-
|
|
394
|
-
|
|
495
|
+
const leaseRemoved = withPublicLockRetry(lock, code, retryState, () => unlinkVaultFile(
|
|
496
|
+
vaultBase, vaultLockLease(lock, token), {
|
|
497
|
+
label: 'lease do lock de escrita do Vault', code,
|
|
498
|
+
},
|
|
499
|
+
));
|
|
395
500
|
if (!leaseRemoved) return false;
|
|
396
|
-
const
|
|
501
|
+
const currentState = vaultLockOwner(vaultBase, lock, code, retryState);
|
|
502
|
+
if (!currentState.lockExists) return true;
|
|
503
|
+
const current = currentState.owner;
|
|
397
504
|
if (current?.pid !== pid || current?.token !== token) return false;
|
|
398
505
|
const ownerPath = join(lock, VAULT_LOCK_OWNER_FILE);
|
|
399
|
-
if (!
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
506
|
+
if (!withPublicLockRetry(lock, code, retryState, () => unlinkVaultFile(
|
|
507
|
+
vaultBase, ownerPath, {
|
|
508
|
+
label: 'owner do lock de escrita do Vault', code,
|
|
509
|
+
},
|
|
510
|
+
))) return false;
|
|
511
|
+
return withPublicLockRetry(lock, code, retryState, () => removeVaultLockDirectory(
|
|
512
|
+
vaultBase, lock, {
|
|
513
|
+
missingOk: false, label: 'lock de escrita do Vault', code,
|
|
514
|
+
},
|
|
515
|
+
));
|
|
405
516
|
}
|
|
406
517
|
|
|
407
|
-
function reapDeadVaultLock(vaultBase, lock, staleMs, code) {
|
|
408
|
-
const
|
|
409
|
-
|
|
518
|
+
function reapDeadVaultLock(vaultBase, lock, staleMs, code, retryState) {
|
|
519
|
+
const initial = withPublicLockRetry(lock, code, retryState, () => {
|
|
520
|
+
const checked = assertVaultPathSafe(vaultBase, lock, {
|
|
521
|
+
expectedType: 'directory', label: 'lock de escrita do Vault', code,
|
|
522
|
+
});
|
|
523
|
+
if (!checked.exists) return null;
|
|
524
|
+
return { checked, before: statSync(checked.target) };
|
|
410
525
|
});
|
|
411
|
-
if (!
|
|
412
|
-
|
|
413
|
-
try {
|
|
414
|
-
before = statSync(checked.target);
|
|
415
|
-
} catch (error) {
|
|
416
|
-
if (error?.code === 'ENOENT') return true;
|
|
417
|
-
throw error;
|
|
418
|
-
}
|
|
526
|
+
if (!initial) return true;
|
|
527
|
+
const { checked, before } = initial;
|
|
419
528
|
if (Date.now() - before.mtimeMs <= staleMs) return false;
|
|
420
|
-
const observed = vaultLockOwner(vaultBase, checked.target, code);
|
|
529
|
+
const observed = vaultLockOwner(vaultBase, checked.target, code, retryState);
|
|
530
|
+
if (!observed.lockExists) return true;
|
|
421
531
|
if (observed.owner) {
|
|
422
532
|
if (processIsAlive(observed.owner.pid)) return false;
|
|
423
|
-
const lease =
|
|
424
|
-
|
|
533
|
+
const lease = withPublicLockRetry(lock, code, retryState, () => {
|
|
534
|
+
const current = assertVaultPathSafe(vaultBase, lock, {
|
|
535
|
+
expectedType: 'directory', label: 'lock de escrita do Vault', code,
|
|
536
|
+
});
|
|
537
|
+
if (!current.exists) return null;
|
|
538
|
+
return assertVaultPathSafe(vaultBase, vaultLockLease(current.target, observed.owner.token), {
|
|
539
|
+
expectedType: 'file', label: 'lease do lock de escrita do Vault', code,
|
|
540
|
+
});
|
|
425
541
|
});
|
|
542
|
+
if (!lease) return true;
|
|
426
543
|
if (lease.exists) {
|
|
427
544
|
return releaseOwnedVaultLock(vaultBase, checked.target, {
|
|
428
|
-
pid: observed.owner.pid, token: observed.owner.token, code,
|
|
545
|
+
pid: observed.owner.pid, token: observed.owner.token, code, retryState,
|
|
429
546
|
});
|
|
430
547
|
}
|
|
431
548
|
// Compatibility with owner-aware locks from 0.58.x, which predate token leases.
|
|
432
549
|
// A dead PID plus byte-identical owner and directory identity is sufficient here;
|
|
433
550
|
// a live legacy owner was returned above and is never reaped by age.
|
|
434
|
-
const
|
|
551
|
+
const legacy = withPublicLockRetry(lock, code, retryState, () => {
|
|
552
|
+
const current = assertVaultPathSafe(vaultBase, lock, {
|
|
553
|
+
expectedType: 'directory', label: 'lock legado de escrita do Vault', code,
|
|
554
|
+
});
|
|
555
|
+
if (!current.exists) return null;
|
|
556
|
+
return {
|
|
557
|
+
entries: readdirSync(current.target),
|
|
558
|
+
currentStat: statSync(current.target),
|
|
559
|
+
currentOwner: vaultLockOwner(vaultBase, current.target, code),
|
|
560
|
+
};
|
|
561
|
+
});
|
|
562
|
+
if (!legacy) return true;
|
|
563
|
+
const { entries, currentStat, currentOwner } = legacy;
|
|
435
564
|
if (entries.some((name) => name !== VAULT_LOCK_OWNER_FILE)) return false;
|
|
436
|
-
const currentStat = statSync(checked.target);
|
|
437
|
-
const currentOwner = vaultLockOwner(vaultBase, checked.target, code);
|
|
438
565
|
if (currentStat.birthtimeMs !== before.birthtimeMs
|
|
439
566
|
|| currentStat.mtimeMs !== before.mtimeMs
|
|
440
567
|
|| currentOwner.raw !== observed.raw) return false;
|
|
441
|
-
if (!
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
568
|
+
if (!withPublicLockRetry(lock, code, retryState, () => unlinkVaultFile(
|
|
569
|
+
vaultBase, currentOwner.path, {
|
|
570
|
+
label: 'owner legado morto do lock de escrita do Vault', code,
|
|
571
|
+
},
|
|
572
|
+
))) return false;
|
|
573
|
+
return withPublicLockRetry(lock, code, retryState, () => removeVaultLockDirectory(
|
|
574
|
+
vaultBase, checked.target, {
|
|
575
|
+
missingOk: false, label: 'lock legado de escrita do Vault', code,
|
|
576
|
+
},
|
|
577
|
+
));
|
|
447
578
|
}
|
|
448
579
|
|
|
449
580
|
// Locks are published by atomic directory rename only after owner + lease exist.
|
|
450
581
|
// Thus an old empty/partial directory is legacy or crash residue, never an in-flight
|
|
451
582
|
// live acquisition. Unknown children remain fail-closed.
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
allowMissing: false, expectedType: 'file', label: `resíduo do lock do Vault ${name}`, code,
|
|
583
|
+
const partial = withPublicLockRetry(lock, code, retryState, () => {
|
|
584
|
+
const current = assertVaultPathSafe(vaultBase, lock, {
|
|
585
|
+
expectedType: 'directory', label: 'lock de escrita do Vault', code,
|
|
456
586
|
});
|
|
457
|
-
|
|
587
|
+
if (!current.exists) return null;
|
|
588
|
+
const entries = readdirSync(current.target);
|
|
589
|
+
for (const name of entries) {
|
|
590
|
+
assertVaultPathSafe(vaultBase, join(current.target, name), {
|
|
591
|
+
allowMissing: false, expectedType: 'file', label: `resíduo do lock do Vault ${name}`, code,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
entries,
|
|
596
|
+
currentStat: statSync(current.target),
|
|
597
|
+
currentOwner: vaultLockOwner(vaultBase, current.target, code),
|
|
598
|
+
};
|
|
599
|
+
});
|
|
600
|
+
if (!partial) return true;
|
|
601
|
+
const { entries, currentStat, currentOwner } = partial;
|
|
458
602
|
if (entries.some((name) => name !== VAULT_LOCK_OWNER_FILE)) return false;
|
|
459
|
-
const currentStat = statSync(checked.target);
|
|
460
|
-
const currentOwner = vaultLockOwner(vaultBase, checked.target, code);
|
|
461
603
|
if (currentStat.birthtimeMs !== before.birthtimeMs
|
|
462
604
|
|| currentStat.mtimeMs !== before.mtimeMs
|
|
463
605
|
|| currentOwner.raw !== observed.raw) return false;
|
|
464
606
|
if (entries.includes(VAULT_LOCK_OWNER_FILE)
|
|
465
|
-
&& !
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
607
|
+
&& !withPublicLockRetry(lock, code, retryState, () => unlinkVaultFile(
|
|
608
|
+
vaultBase, currentOwner.path, {
|
|
609
|
+
label: 'owner parcial do lock de escrita do Vault', code,
|
|
610
|
+
},
|
|
611
|
+
))) return false;
|
|
612
|
+
return withPublicLockRetry(lock, code, retryState, () => removeVaultLockDirectory(
|
|
613
|
+
vaultBase, checked.target, {
|
|
614
|
+
missingOk: false, label: 'lock de escrita do Vault', code,
|
|
615
|
+
},
|
|
616
|
+
));
|
|
471
617
|
}
|
|
472
618
|
|
|
473
619
|
export function withVaultPathLock(vaultBase, path, fn, {
|
|
@@ -478,80 +624,103 @@ export function withVaultPathLock(vaultBase, path, fn, {
|
|
|
478
624
|
const lock = `${path}.lock`;
|
|
479
625
|
const deadline = Date.now() + timeoutMs;
|
|
480
626
|
const token = randomUUID();
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (retryableLockTopologyRace(vaultBase, lock, error, code)) continue;
|
|
496
|
-
if (error?.code === code || error?.code === 'VAULT_PATH_UNSAFE') throw error;
|
|
627
|
+
const retryState = { deadline, remaining: VAULT_LOCK_TOPOLOGY_RETRIES };
|
|
628
|
+
let firstAttempt = true;
|
|
629
|
+
try {
|
|
630
|
+
while (true) {
|
|
631
|
+
if (!firstAttempt && Date.now() >= deadline) return VAULT_LOCK_BUSY;
|
|
632
|
+
firstAttempt = false;
|
|
633
|
+
const current = inspectVaultLock(vaultBase, lock, code, retryState);
|
|
634
|
+
if (current.exists) {
|
|
635
|
+
const reaped = reapDeadVaultLock(vaultBase, lock, staleMs, code, retryState);
|
|
636
|
+
if (reaped) continue;
|
|
637
|
+
const remainingMs = deadline - Date.now();
|
|
638
|
+
if (remainingMs <= 0) return VAULT_LOCK_BUSY;
|
|
639
|
+
waitBriefly(Math.min(10, remainingMs));
|
|
640
|
+
continue;
|
|
497
641
|
}
|
|
498
|
-
if (reaped) continue;
|
|
499
|
-
if (Date.now() >= deadline) return VAULT_LOCK_BUSY;
|
|
500
|
-
waitBriefly(10);
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
642
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
mkdirVaultPath(vaultBase, pending, {
|
|
508
|
-
recursive: false, exclusive: true, label: 'lock de escrita do Vault', code,
|
|
509
|
-
});
|
|
510
|
-
pendingCreated = true;
|
|
511
|
-
writeVaultFileAtomic(vaultBase, join(pending, VAULT_LOCK_OWNER_FILE), `${JSON.stringify({
|
|
512
|
-
v: 1,
|
|
513
|
-
pid: process.pid,
|
|
514
|
-
token,
|
|
515
|
-
created_at: new Date().toISOString(),
|
|
516
|
-
})}\n`, 'utf8', { label: 'owner do lock de escrita do Vault', code });
|
|
517
|
-
writeVaultFileAtomic(vaultBase, vaultLockLease(pending, token), `${token}\n`, 'utf8', {
|
|
518
|
-
label: 'lease do lock de escrita do Vault', code,
|
|
519
|
-
});
|
|
520
|
-
const raced = assertVaultPathSafe(vaultBase, lock, {
|
|
521
|
-
expectedType: 'directory', label: 'lock de escrita do Vault', code,
|
|
522
|
-
});
|
|
523
|
-
if (raced.exists) {
|
|
524
|
-
removePreparedVaultLock(vaultBase, pending, token, code);
|
|
525
|
-
} else {
|
|
643
|
+
const pending = `${lock}.${process.pid}.${token}.pending`;
|
|
644
|
+
let pendingCreated = false;
|
|
645
|
+
try {
|
|
526
646
|
try {
|
|
527
|
-
|
|
528
|
-
|
|
647
|
+
mkdirVaultPath(vaultBase, pending, {
|
|
648
|
+
recursive: false, exclusive: true, label: 'lock de escrita do Vault', code,
|
|
529
649
|
});
|
|
530
|
-
|
|
650
|
+
pendingCreated = true;
|
|
531
651
|
} catch (error) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
652
|
+
pendingCreated = typeof error?.[VAULT_PATH_CREATED] === 'string'
|
|
653
|
+
&& pathKey(error[VAULT_PATH_CREATED]) === pathKey(pending);
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
writeVaultFileAtomic(vaultBase, join(pending, VAULT_LOCK_OWNER_FILE), `${JSON.stringify({
|
|
657
|
+
v: 1,
|
|
658
|
+
pid: process.pid,
|
|
659
|
+
token,
|
|
660
|
+
created_at: new Date().toISOString(),
|
|
661
|
+
})}\n`, 'utf8', { label: 'owner do lock de escrita do Vault', code });
|
|
662
|
+
writeVaultFileAtomic(vaultBase, vaultLockLease(pending, token), `${token}\n`, 'utf8', {
|
|
663
|
+
label: 'lease do lock de escrita do Vault', code,
|
|
664
|
+
});
|
|
665
|
+
const raced = inspectVaultLock(vaultBase, lock, code, retryState);
|
|
666
|
+
if (raced.exists) {
|
|
536
667
|
removePreparedVaultLock(vaultBase, pending, token, code);
|
|
668
|
+
} else {
|
|
669
|
+
try {
|
|
670
|
+
renameVaultPath(vaultBase, pending, lock, {
|
|
671
|
+
sourceType: 'directory', label: 'publicação do lock de escrita do Vault', code,
|
|
672
|
+
});
|
|
673
|
+
break;
|
|
674
|
+
} catch (error) {
|
|
675
|
+
const retryableRenameRace = retryablePublicLockError(lock, error, code, {
|
|
676
|
+
allowRaw: false,
|
|
677
|
+
});
|
|
678
|
+
const nativeRenameCollision = vaultLockRenameCollision(error, pending, lock);
|
|
679
|
+
const preflightRenameCollision = preexistingPublicLockError(lock, error, code);
|
|
680
|
+
if (!retryableRenameRace && !nativeRenameCollision && !preflightRenameCollision) {
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
683
|
+
const racedAfterRename = inspectVaultLock(
|
|
684
|
+
vaultBase, lock, code, retryState, retryableRenameRace ? error : null,
|
|
685
|
+
);
|
|
686
|
+
if (!racedAfterRename.exists) {
|
|
687
|
+
// EPERM is ambiguous on Windows; without an extant destination it remains fatal.
|
|
688
|
+
if (error?.code === 'EPERM') throw error;
|
|
689
|
+
removePreparedVaultLock(vaultBase, pending, token, code);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
removePreparedVaultLock(vaultBase, pending, token, code);
|
|
693
|
+
}
|
|
537
694
|
}
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (pendingCreated) {
|
|
697
|
+
try { removePreparedVaultLock(vaultBase, pending, token, code); }
|
|
698
|
+
catch { /* residue private remains recoverable; never clean through an alias */ }
|
|
699
|
+
}
|
|
700
|
+
throw error;
|
|
538
701
|
}
|
|
539
|
-
|
|
540
|
-
if (
|
|
541
|
-
|
|
542
|
-
catch { /* residue private remains recoverable; never clean through an alias */ }
|
|
543
|
-
}
|
|
544
|
-
throw error;
|
|
702
|
+
const remainingMs = deadline - Date.now();
|
|
703
|
+
if (remainingMs <= 0) return VAULT_LOCK_BUSY;
|
|
704
|
+
waitBriefly(Math.min(10, remainingMs));
|
|
545
705
|
}
|
|
546
|
-
|
|
547
|
-
|
|
706
|
+
} catch (error) {
|
|
707
|
+
if (error?.[VAULT_LOCK_RETRY_DEADLINE]) return VAULT_LOCK_BUSY;
|
|
708
|
+
throw error;
|
|
548
709
|
}
|
|
549
710
|
|
|
550
711
|
try {
|
|
551
712
|
return fn();
|
|
552
713
|
} finally {
|
|
553
714
|
try {
|
|
554
|
-
releaseOwnedVaultLock(vaultBase, lock, {
|
|
715
|
+
releaseOwnedVaultLock(vaultBase, lock, {
|
|
716
|
+
pid: process.pid,
|
|
717
|
+
token,
|
|
718
|
+
code,
|
|
719
|
+
retryState: {
|
|
720
|
+
deadline: Date.now() + VAULT_LOCK_RELEASE_RETRY_WINDOW_MS,
|
|
721
|
+
remaining: VAULT_LOCK_TOPOLOGY_RETRIES,
|
|
722
|
+
},
|
|
723
|
+
});
|
|
555
724
|
}
|
|
556
725
|
catch { /* a failed safe release leaves the lock in place; never remove through an alias */ }
|
|
557
726
|
}
|