@neottia/repository-store 0.2.1 → 0.3.2
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 +5 -3
- package/binding.gyp +2 -1
- package/dist/internal/filesystem.d.ts +2 -0
- package/dist/internal/filesystem.d.ts.map +1 -1
- package/dist/internal/filesystem.js +43 -10
- package/dist/internal/filesystem.js.map +1 -1
- package/dist/internal/model.d.ts +1 -0
- package/dist/internal/model.d.ts.map +1 -1
- package/dist/internal/model.js.map +1 -1
- package/dist/internal/native-integrity.d.ts +2 -0
- package/dist/internal/native-integrity.d.ts.map +1 -0
- package/dist/internal/native-integrity.js +49 -0
- package/dist/internal/native-integrity.js.map +1 -0
- package/dist/internal/platform-backend.d.ts +1 -0
- package/dist/internal/platform-backend.d.ts.map +1 -1
- package/dist/internal/platform-backend.js +12 -1
- package/dist/internal/platform-backend.js.map +1 -1
- package/dist/lease.d.ts +5 -0
- package/dist/lease.d.ts.map +1 -1
- package/dist/lease.js +250 -45
- package/dist/lease.js.map +1 -1
- package/native/freebsd_backend.cc +83 -0
- package/native-prebuilds.json +118 -0
- package/package.json +11 -4
- package/scripts/build-native.mjs +29 -0
- package/scripts/install-native.mjs +146 -6
- package/scripts/native-prebuild.mjs +289 -0
package/dist/lease.js
CHANGED
|
@@ -14,6 +14,8 @@ const DEFAULT_STALE_MS = 60_000;
|
|
|
14
14
|
const DEFAULT_POLL_MS = 20;
|
|
15
15
|
const DEFAULT_WAIT_MS = 10_000;
|
|
16
16
|
const MAX_LEASE_METADATA_BYTES = 4 * 1024;
|
|
17
|
+
const WINDOWS_RELEASE_RETRY_ATTEMPTS = 25;
|
|
18
|
+
const WINDOWS_RELEASE_RETRY_DELAY_MS = 10;
|
|
17
19
|
export async function withRepositoryLease(root, operation, options = {}) {
|
|
18
20
|
const rootState = getRootState(root);
|
|
19
21
|
if (rootState === undefined)
|
|
@@ -40,18 +42,38 @@ export async function withRepositoryLease(root, operation, options = {}) {
|
|
|
40
42
|
let ownerIdentity;
|
|
41
43
|
let leaseState;
|
|
42
44
|
let lockPath = '';
|
|
45
|
+
let lockKind = 'directory';
|
|
43
46
|
let token = '';
|
|
44
47
|
try {
|
|
45
48
|
checkAcquisitionControl(deadline, explicitDeadline !== undefined, options.signal);
|
|
46
49
|
ensurePrivateDirectory(controlRoot);
|
|
47
50
|
const cleanupBackend = requireRepositoryPlatformBackend();
|
|
48
51
|
assertPlatformFilesystem(cleanupBackend, controlRoot);
|
|
52
|
+
lockKind = cleanupBackend.leaseArtifact;
|
|
49
53
|
const hostId = ensureHostId(controlRoot);
|
|
50
54
|
lockPath = join(controlRoot, 'authority.lease');
|
|
51
55
|
const claimPath = join(controlRoot, 'authority.claim');
|
|
52
56
|
token = randomBytes(32).toString('hex');
|
|
53
57
|
while (!acquired) {
|
|
54
58
|
checkAcquisitionControl(deadline, explicitDeadline !== undefined, options.signal);
|
|
59
|
+
if (lockKind === 'file') {
|
|
60
|
+
if (!acquireClaim(lockPath, controlRoot, token, hostId)) {
|
|
61
|
+
if (tryReclaimFileLease(lockPath, controlRoot, hostId, staleMs))
|
|
62
|
+
continue;
|
|
63
|
+
await waitForLeasePoll(pollMs, deadline, explicitDeadline !== undefined, options.signal);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
emitLeaseFault('lease-directory-created');
|
|
67
|
+
const owner = readOwner(lockPath);
|
|
68
|
+
if (owner.metadata.token !== token)
|
|
69
|
+
throw new LeaseContentionError('Repository file lease ownership changed.', 'LEASE_OWNER_UNKNOWN');
|
|
70
|
+
lockIdentity = owner.identity;
|
|
71
|
+
ownerIdentity = owner.identity;
|
|
72
|
+
emitLeaseFault('lease-owner-written');
|
|
73
|
+
emitLeaseFault('lease-claim-removed');
|
|
74
|
+
acquired = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
55
77
|
if (!acquireClaim(claimPath, controlRoot, token, hostId)) {
|
|
56
78
|
if (tryReclaimClaim(claimPath, lockPath, controlRoot, hostId, staleMs))
|
|
57
79
|
continue;
|
|
@@ -87,7 +109,7 @@ export async function withRepositoryLease(root, operation, options = {}) {
|
|
|
87
109
|
syncDirectory(controlRoot);
|
|
88
110
|
if (!removeOwnedClaim(claimPath, token)) {
|
|
89
111
|
claimOwnershipUncertain = true;
|
|
90
|
-
releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity);
|
|
112
|
+
await releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity, lockKind);
|
|
91
113
|
throw new LeaseContentionError('Repository initialization claim ownership changed.', 'LEASE_OWNER_UNKNOWN');
|
|
92
114
|
}
|
|
93
115
|
emitLeaseFault('lease-claim-removed');
|
|
@@ -109,6 +131,7 @@ export async function withRepositoryLease(root, operation, options = {}) {
|
|
|
109
131
|
authorityId: rootState.authorityId,
|
|
110
132
|
token,
|
|
111
133
|
lockPath,
|
|
134
|
+
lockKind,
|
|
112
135
|
lockIdentity,
|
|
113
136
|
ownerIdentity,
|
|
114
137
|
pendingOperations: new Set(),
|
|
@@ -131,7 +154,7 @@ export async function withRepositoryLease(root, operation, options = {}) {
|
|
|
131
154
|
leaseState.active = false;
|
|
132
155
|
}
|
|
133
156
|
if (acquired && lockIdentity !== undefined && ownerIdentity !== undefined)
|
|
134
|
-
releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity);
|
|
157
|
+
await releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity, lockKind);
|
|
135
158
|
turn.release();
|
|
136
159
|
}
|
|
137
160
|
}
|
|
@@ -142,7 +165,7 @@ export function assertLiveLease(root, lease) {
|
|
|
142
165
|
throw new RepositoryStoreConfigError('Lease and managed root have different authorities.', 'AUTHORITY_MISMATCH');
|
|
143
166
|
if (!leaseState.active)
|
|
144
167
|
throw new LeaseContentionError('Repository lease is no longer active.', 'LEASE_BUSY');
|
|
145
|
-
if (!ownedLeaseIdentityMatches(leaseState.lockPath, leaseState.token, leaseState.lockIdentity, leaseState.ownerIdentity)) {
|
|
168
|
+
if (!ownedLeaseIdentityMatches(leaseState.lockPath, leaseState.token, leaseState.lockIdentity, leaseState.ownerIdentity, leaseState.lockKind)) {
|
|
146
169
|
leaseState.active = false;
|
|
147
170
|
throw new LeaseContentionError('Repository lease ownership changed.', 'LEASE_OWNER_UNKNOWN');
|
|
148
171
|
}
|
|
@@ -196,10 +219,7 @@ function ownerMetadata(token, hostId) {
|
|
|
196
219
|
function ensureHostId(controlRoot) {
|
|
197
220
|
const path = join(controlRoot, 'host-id');
|
|
198
221
|
try {
|
|
199
|
-
|
|
200
|
-
if (!/^[0-9a-f]{64}$/u.test(value))
|
|
201
|
-
throw new PathSafetyError('Repository host-id is malformed.');
|
|
202
|
-
return value;
|
|
222
|
+
return readHostId(path, controlRoot);
|
|
203
223
|
}
|
|
204
224
|
catch (error) {
|
|
205
225
|
if (!hasCode(error, 'ENOENT'))
|
|
@@ -229,6 +249,92 @@ function ensureHostId(controlRoot) {
|
|
|
229
249
|
rmSync(candidate, { force: true });
|
|
230
250
|
}
|
|
231
251
|
}
|
|
252
|
+
function readHostId(path, controlRoot) {
|
|
253
|
+
const identity = lstatSync(path);
|
|
254
|
+
if (identity.isSymbolicLink() || !identity.isFile())
|
|
255
|
+
throw new PathSafetyError('Repository host-id has an unsafe identity.', 'UNSAFE_LINK');
|
|
256
|
+
if (identity.nlink !== 1 && identity.nlink !== 2)
|
|
257
|
+
throw new PathSafetyError('Repository host-id has an unsafe hard-link count.', 'UNSAFE_HARD_LINK');
|
|
258
|
+
if (identity.nlink === 2) {
|
|
259
|
+
const prefix = '.host-id-';
|
|
260
|
+
const peers = readdirSync(controlRoot).filter((name) => {
|
|
261
|
+
if (!name.startsWith(prefix) || !name.endsWith('.tmp'))
|
|
262
|
+
return false;
|
|
263
|
+
const token = name.slice(prefix.length, -'.tmp'.length);
|
|
264
|
+
if (!/^[0-9a-f]{32}$/u.test(token))
|
|
265
|
+
return false;
|
|
266
|
+
try {
|
|
267
|
+
const peer = lstatSync(join(controlRoot, name));
|
|
268
|
+
return peer.isFile() && !peer.isSymbolicLink() && sameIdentity(identity, peer);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (hasCode(error, 'ENOENT'))
|
|
272
|
+
return false;
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
if (peers.length === 1) {
|
|
277
|
+
rmSync(join(controlRoot, peers[0]));
|
|
278
|
+
syncDirectory(controlRoot);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const stabilized = lstatSync(path);
|
|
282
|
+
if (stabilized.nlink !== 1 || !sameIdentity(identity, stabilized))
|
|
283
|
+
throw new PathSafetyError('Repository host-id hard link is not package-owned.', 'UNSAFE_HARD_LINK');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const value = new TextDecoder().decode(readRegularFile(path, MAX_LEASE_METADATA_BYTES)).trim();
|
|
287
|
+
if (!/^[0-9a-f]{64}$/u.test(value))
|
|
288
|
+
throw new PathSafetyError('Repository host-id is malformed.');
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
function tryReclaimFileLease(lockPath, controlRoot, hostId, staleMs) {
|
|
292
|
+
const controlDirectories = captureDirectories(controlRoot);
|
|
293
|
+
try {
|
|
294
|
+
const preliminary = lstatSync(lockPath);
|
|
295
|
+
assertPreliminaryFileLeaseIdentity(preliminary);
|
|
296
|
+
const verifiedOwner = readOwner(lockPath, true, true);
|
|
297
|
+
if (!sameIdentity(preliminary, verifiedOwner.identity))
|
|
298
|
+
throw new LeaseContentionError('File lease identity changed before owner verification.', 'LEASE_OWNER_UNKNOWN');
|
|
299
|
+
if (Date.now() - verifiedOwner.identity.mtimeMs < staleMs)
|
|
300
|
+
return false;
|
|
301
|
+
if (!conclusivelyDead(verifiedOwner.metadata, hostId))
|
|
302
|
+
return false;
|
|
303
|
+
normalizeInterruptedClaimLink(lockPath, controlRoot, true, verifiedOwner.metadata.token);
|
|
304
|
+
const normalizedOwner = readOwner(lockPath);
|
|
305
|
+
if (!sameIdentity(preliminary, normalizedOwner.identity) ||
|
|
306
|
+
normalizedOwner.metadata.token !== verifiedOwner.metadata.token)
|
|
307
|
+
throw new LeaseContentionError('File lease owner changed during reclamation.', 'LEASE_OWNER_UNKNOWN');
|
|
308
|
+
const quarantine = `${lockPath}.release-${normalizedOwner.metadata.token}`;
|
|
309
|
+
const expected = {
|
|
310
|
+
kind: 'file',
|
|
311
|
+
identity: normalizedOwner.identity,
|
|
312
|
+
token: normalizedOwner.metadata.token,
|
|
313
|
+
};
|
|
314
|
+
if (!quarantineNoReplace(lockPath, quarantine))
|
|
315
|
+
return false;
|
|
316
|
+
const moved = readOwner(quarantine);
|
|
317
|
+
if (!sameIdentity(normalizedOwner.identity, moved.identity) ||
|
|
318
|
+
moved.metadata.token !== normalizedOwner.metadata.token) {
|
|
319
|
+
restoreQuarantinedLease(quarantine, lockPath, expected, {
|
|
320
|
+
kind: 'file',
|
|
321
|
+
identity: moved.identity,
|
|
322
|
+
token: moved.metadata.token,
|
|
323
|
+
});
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
rmSync(quarantine);
|
|
327
|
+
syncDirectory(controlRoot);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (hasCode(error, 'ENOENT') || (error instanceof PathSafetyError && error.code === 'IDENTITY_CHANGED'))
|
|
332
|
+
return exactPathWasReleased(lockPath, controlDirectories, error, false);
|
|
333
|
+
if (error instanceof PathSafetyError || error instanceof ResourceLimitError)
|
|
334
|
+
throw error;
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
232
338
|
function tryReclaimLockedLease(lockPath, controlRoot, hostId, staleMs) {
|
|
233
339
|
const controlDirectories = captureDirectories(controlRoot);
|
|
234
340
|
try {
|
|
@@ -324,10 +430,7 @@ function tryReclaimClaim(claimPath, lockPath, controlRoot, hostId, staleMs) {
|
|
|
324
430
|
const controlDirectories = captureDirectories(controlRoot);
|
|
325
431
|
try {
|
|
326
432
|
const preliminary = lstatSync(claimPath);
|
|
327
|
-
|
|
328
|
-
throw new PathSafetyError('Unsafe repository claim symbolic link.', 'UNSAFE_LINK');
|
|
329
|
-
if (!preliminary.isFile() || (preliminary.nlink !== 1 && preliminary.nlink !== 2))
|
|
330
|
-
throw new PathSafetyError('Unsafe repository claim artifact.', 'UNSAFE_HARD_LINK');
|
|
433
|
+
assertPreliminaryClaimIdentity(preliminary);
|
|
331
434
|
const verifiedOwner = readOwner(claimPath, true);
|
|
332
435
|
if (!sameIdentity(preliminary, verifiedOwner.identity))
|
|
333
436
|
throw new LeaseContentionError('Claim identity changed before owner verification.', 'LEASE_OWNER_UNKNOWN');
|
|
@@ -373,6 +476,24 @@ function tryReclaimClaim(claimPath, lockPath, controlRoot, hostId, staleMs) {
|
|
|
373
476
|
return false;
|
|
374
477
|
}
|
|
375
478
|
}
|
|
479
|
+
export function assertPreliminaryFileLeaseIdentity(identity) {
|
|
480
|
+
if (identity.isSymbolicLink() || !identity.isFile())
|
|
481
|
+
throw new PathSafetyError('Unsafe repository file lease.', 'UNSAFE_HARD_LINK');
|
|
482
|
+
if (identity.nlink === 0)
|
|
483
|
+
throw new PathSafetyError('Repository file lease was released before owner verification.', 'IDENTITY_CHANGED');
|
|
484
|
+
if (identity.nlink !== 1 && identity.nlink !== 2)
|
|
485
|
+
throw new PathSafetyError('Unsafe repository file lease.', 'UNSAFE_HARD_LINK');
|
|
486
|
+
}
|
|
487
|
+
export function assertPreliminaryClaimIdentity(identity) {
|
|
488
|
+
if (identity.isSymbolicLink())
|
|
489
|
+
throw new PathSafetyError('Unsafe repository claim symbolic link.', 'UNSAFE_LINK');
|
|
490
|
+
if (!identity.isFile())
|
|
491
|
+
throw new PathSafetyError('Unsafe repository claim artifact.', 'UNSAFE_HARD_LINK');
|
|
492
|
+
if (identity.nlink === 0)
|
|
493
|
+
throw new PathSafetyError('Repository claim was released before owner verification.', 'IDENTITY_CHANGED');
|
|
494
|
+
if (identity.nlink !== 1 && identity.nlink !== 2)
|
|
495
|
+
throw new PathSafetyError('Unsafe repository claim artifact.', 'UNSAFE_HARD_LINK');
|
|
496
|
+
}
|
|
376
497
|
function cleanupInterruptedInitialization(lockPath, controlRoot, owner, staleMs) {
|
|
377
498
|
let lockStat;
|
|
378
499
|
try {
|
|
@@ -459,17 +580,22 @@ function cleanupOwnedInitialization(lockPath, controlRoot, token, expectedLock)
|
|
|
459
580
|
restoreQuarantinedLease(quarantine, lockPath, releaseExpectation);
|
|
460
581
|
}
|
|
461
582
|
}
|
|
462
|
-
function normalizeInterruptedClaimLink(claimPath, controlRoot) {
|
|
583
|
+
function normalizeInterruptedClaimLink(claimPath, controlRoot, allowReleasePeer = false, expectedToken) {
|
|
463
584
|
const claim = lstatSync(claimPath);
|
|
464
585
|
if (!claim.isFile() || claim.isSymbolicLink() || claim.nlink === 1)
|
|
465
586
|
return;
|
|
466
587
|
if (claim.nlink !== 2)
|
|
467
588
|
throw new PathSafetyError('Unsafe repository claim hard-link count.', 'UNSAFE_HARD_LINK');
|
|
468
|
-
const
|
|
469
|
-
const
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
if (
|
|
589
|
+
const base = basename(claimPath);
|
|
590
|
+
const prefixes = [`${base}.prepare-`, ...(allowReleasePeer ? [`${base}.release-`] : [])];
|
|
591
|
+
const candidates = readdirSync(dirname(claimPath)).flatMap((name) => {
|
|
592
|
+
const prefix = prefixes.find((candidate) => name.startsWith(candidate));
|
|
593
|
+
if (prefix === undefined)
|
|
594
|
+
return [];
|
|
595
|
+
return [{ name, token: name.slice(prefix.length) }];
|
|
596
|
+
});
|
|
597
|
+
const matching = candidates.filter(({ name, token }) => {
|
|
598
|
+
if (!/^[0-9a-f-]{32,64}$/u.test(token) || (expectedToken !== undefined && token !== expectedToken))
|
|
473
599
|
return false;
|
|
474
600
|
const stat = lstatSync(join(dirname(claimPath), name));
|
|
475
601
|
return stat.isFile() && !stat.isSymbolicLink() && sameIdentity(claim, stat);
|
|
@@ -480,7 +606,7 @@ function normalizeInterruptedClaimLink(claimPath, controlRoot) {
|
|
|
480
606
|
return;
|
|
481
607
|
throw new PathSafetyError('Repository claim hard link is not package-owned.');
|
|
482
608
|
}
|
|
483
|
-
rmSync(join(dirname(claimPath), matching[0]));
|
|
609
|
+
rmSync(join(dirname(claimPath), matching[0].name));
|
|
484
610
|
syncDirectory(controlRoot);
|
|
485
611
|
}
|
|
486
612
|
function conclusivelyDead(owner, hostId) {
|
|
@@ -508,13 +634,16 @@ function readProcessStart(pid) {
|
|
|
508
634
|
return undefined;
|
|
509
635
|
}
|
|
510
636
|
}
|
|
511
|
-
function readOwner(path, allowInterruptedClaimLink = false) {
|
|
637
|
+
function readOwner(path, allowInterruptedClaimLink = false, allowInterruptedReleaseLink = false) {
|
|
512
638
|
const initialIdentity = allowInterruptedClaimLink ? lstatSync(path) : undefined;
|
|
513
|
-
const stagingPeer = allowInterruptedClaimLink
|
|
639
|
+
const stagingPeer = allowInterruptedClaimLink
|
|
640
|
+
? interruptedClaimPeer(path, initialIdentity, allowInterruptedReleaseLink)
|
|
641
|
+
: undefined;
|
|
514
642
|
let file;
|
|
515
643
|
try {
|
|
516
644
|
file = readRegularFileWithIdentity(path, MAX_LEASE_METADATA_BYTES, {
|
|
517
645
|
allowedLinkCounts: allowInterruptedClaimLink ? [1, 2] : [1],
|
|
646
|
+
unlinkedAsIdentityChange: true,
|
|
518
647
|
});
|
|
519
648
|
}
|
|
520
649
|
catch (error) {
|
|
@@ -536,7 +665,9 @@ function readOwner(path, allowInterruptedClaimLink = false) {
|
|
|
536
665
|
throw new PathSafetyError('Interrupted claim identity changed during owner verification.', 'IDENTITY_CHANGED');
|
|
537
666
|
if (stabilized.nlink !== 1)
|
|
538
667
|
throw error;
|
|
539
|
-
file = readRegularFileWithIdentity(path, MAX_LEASE_METADATA_BYTES
|
|
668
|
+
file = readRegularFileWithIdentity(path, MAX_LEASE_METADATA_BYTES, {
|
|
669
|
+
unlinkedAsIdentityChange: true,
|
|
670
|
+
});
|
|
540
671
|
}
|
|
541
672
|
const owner = JSON.parse(new TextDecoder().decode(file.bytes));
|
|
542
673
|
if (owner.version !== 1 ||
|
|
@@ -551,27 +682,60 @@ function readOwner(path, allowInterruptedClaimLink = false) {
|
|
|
551
682
|
const finalIdentity = allowInterruptedClaimLink ? lstatSync(path) : undefined;
|
|
552
683
|
if (finalIdentity !== undefined && !sameIdentity(file.identity, finalIdentity))
|
|
553
684
|
throw new PathSafetyError('Interrupted claim identity changed after owner verification.', 'IDENTITY_CHANGED');
|
|
554
|
-
const finalStagingPeer = allowInterruptedClaimLink && finalIdentity !== undefined
|
|
685
|
+
const finalStagingPeer = allowInterruptedClaimLink && finalIdentity !== undefined
|
|
686
|
+
? interruptedClaimPeer(path, finalIdentity, allowInterruptedReleaseLink)
|
|
687
|
+
: undefined;
|
|
555
688
|
if ([stagingPeer, finalStagingPeer].some((peer) => peer !== undefined && owner.token !== peer.token))
|
|
556
689
|
throw new PathSafetyError('Interrupted claim token does not match its package-owned staging link.');
|
|
557
690
|
return { metadata: owner, identity: file.identity };
|
|
558
691
|
}
|
|
559
|
-
function interruptedClaimPeer(path, observed) {
|
|
692
|
+
export function interruptedClaimPeer(path, observed, allowReleasePeer = false) {
|
|
560
693
|
const stat = observed ?? lstatSync(path);
|
|
561
|
-
if (stat.isSymbolicLink() || !stat.isFile()
|
|
694
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
695
|
+
throw new PathSafetyError('Unsafe interrupted repository claim.', 'UNSAFE_HARD_LINK');
|
|
696
|
+
if (stat.nlink === 0)
|
|
697
|
+
throw new PathSafetyError('Interrupted claim was released before peer verification.', 'IDENTITY_CHANGED');
|
|
698
|
+
if (stat.nlink !== 1 && stat.nlink !== 2)
|
|
562
699
|
throw new PathSafetyError('Unsafe interrupted repository claim.', 'UNSAFE_HARD_LINK');
|
|
563
700
|
if (stat.nlink === 1)
|
|
564
701
|
return undefined;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (
|
|
702
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
703
|
+
const peers = matchingInterruptedClaimPeers(path, stat, allowReleasePeer);
|
|
704
|
+
if (peers.length === 1)
|
|
705
|
+
return peers[0];
|
|
706
|
+
let stabilized;
|
|
707
|
+
try {
|
|
708
|
+
stabilized = lstatSync(path);
|
|
709
|
+
}
|
|
710
|
+
catch (stabilizationError) {
|
|
711
|
+
if (hasCode(stabilizationError, 'ENOENT'))
|
|
712
|
+
throw new PathSafetyError('Interrupted claim was released during peer verification.', 'IDENTITY_CHANGED');
|
|
713
|
+
throw stabilizationError;
|
|
714
|
+
}
|
|
715
|
+
if (stabilized.isSymbolicLink() || !stabilized.isFile())
|
|
716
|
+
throw new PathSafetyError('Interrupted claim path became unsafe during peer verification.', 'UNSAFE_HARD_LINK');
|
|
717
|
+
if (!sameIdentity(stat, stabilized))
|
|
718
|
+
throw new PathSafetyError('Interrupted claim identity changed during peer verification.', 'IDENTITY_CHANGED');
|
|
719
|
+
if (stabilized.nlink === 1)
|
|
720
|
+
return undefined;
|
|
721
|
+
if (stabilized.nlink !== 2)
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
throw new PathSafetyError('Interrupted claim hard link is not package-owned.', 'UNSAFE_HARD_LINK');
|
|
725
|
+
}
|
|
726
|
+
function matchingInterruptedClaimPeers(path, identity, allowReleasePeer) {
|
|
727
|
+
const base = basename(path);
|
|
728
|
+
const prefixes = [`${base}.prepare-`, ...(allowReleasePeer ? [`${base}.release-`] : [])];
|
|
729
|
+
return readdirSync(dirname(path)).flatMap((name) => {
|
|
730
|
+
const prefix = prefixes.find((candidate) => name.startsWith(candidate));
|
|
731
|
+
if (prefix === undefined)
|
|
568
732
|
return [];
|
|
569
733
|
const token = name.slice(prefix.length);
|
|
570
734
|
if (!/^[0-9a-f-]{32,64}$/u.test(token))
|
|
571
735
|
return [];
|
|
572
736
|
try {
|
|
573
737
|
const peer = lstatSync(join(dirname(path), name));
|
|
574
|
-
return peer.isFile() && !peer.isSymbolicLink() && sameIdentity(
|
|
738
|
+
return peer.isFile() && !peer.isSymbolicLink() && sameIdentity(identity, peer) ? [{ token }] : [];
|
|
575
739
|
}
|
|
576
740
|
catch (error) {
|
|
577
741
|
if (hasCode(error, 'ENOENT'))
|
|
@@ -579,22 +743,12 @@ function interruptedClaimPeer(path, observed) {
|
|
|
579
743
|
throw error;
|
|
580
744
|
}
|
|
581
745
|
});
|
|
582
|
-
if (peers.length === 1)
|
|
583
|
-
return peers[0];
|
|
584
|
-
let stabilized;
|
|
585
|
-
try {
|
|
586
|
-
stabilized = lstatSync(path);
|
|
587
|
-
}
|
|
588
|
-
catch (stabilizationError) {
|
|
589
|
-
if (hasCode(stabilizationError, 'ENOENT'))
|
|
590
|
-
throw new PathSafetyError('Interrupted claim was released during peer verification.', 'IDENTITY_CHANGED');
|
|
591
|
-
throw stabilizationError;
|
|
592
|
-
}
|
|
593
|
-
if (!stabilized.isSymbolicLink() && stabilized.isFile() && stabilized.nlink === 1 && sameIdentity(stat, stabilized))
|
|
594
|
-
return undefined;
|
|
595
|
-
throw new PathSafetyError('Interrupted claim hard link is not package-owned.', 'UNSAFE_HARD_LINK');
|
|
596
746
|
}
|
|
597
|
-
function releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity) {
|
|
747
|
+
async function releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity, lockKind) {
|
|
748
|
+
if (lockKind === 'file') {
|
|
749
|
+
await releaseOwnedFileLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
598
752
|
const releasePath = `${lockPath}.release-${token}`;
|
|
599
753
|
const releaseExpectation = {
|
|
600
754
|
kind: 'directory',
|
|
@@ -612,7 +766,7 @@ function releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIden
|
|
|
612
766
|
!sameLeaseIdentity(ownerIdentity, currentOwner.identity) ||
|
|
613
767
|
currentOwner.metadata.token !== token)
|
|
614
768
|
return;
|
|
615
|
-
if (!
|
|
769
|
+
if (!(await quarantineOwnedRelease(lockPath, releasePath)))
|
|
616
770
|
return;
|
|
617
771
|
restorationSafe = true;
|
|
618
772
|
const movedLock = lstatSync(releasePath);
|
|
@@ -642,6 +796,37 @@ function releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIden
|
|
|
642
796
|
restoreQuarantinedLease(releasePath, lockPath, releaseExpectation);
|
|
643
797
|
}
|
|
644
798
|
}
|
|
799
|
+
async function releaseOwnedFileLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity) {
|
|
800
|
+
const releasePath = `${lockPath}.release-${token}`;
|
|
801
|
+
const expected = { kind: 'file', identity: lockIdentity, token };
|
|
802
|
+
let restorationSafe = false;
|
|
803
|
+
try {
|
|
804
|
+
const current = readOwner(lockPath);
|
|
805
|
+
if (current.metadata.token !== token ||
|
|
806
|
+
!sameIdentity(current.identity, lockIdentity) ||
|
|
807
|
+
!sameIdentity(current.identity, ownerIdentity))
|
|
808
|
+
return;
|
|
809
|
+
if (!(await quarantineOwnedRelease(lockPath, releasePath)))
|
|
810
|
+
return;
|
|
811
|
+
restorationSafe = true;
|
|
812
|
+
const moved = readOwner(releasePath);
|
|
813
|
+
if (moved.metadata.token !== token || !sameIdentity(moved.identity, lockIdentity)) {
|
|
814
|
+
restoreQuarantinedLease(releasePath, lockPath, expected, {
|
|
815
|
+
kind: 'file',
|
|
816
|
+
identity: moved.identity,
|
|
817
|
+
token: moved.metadata.token,
|
|
818
|
+
});
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
rmSync(releasePath);
|
|
822
|
+
restorationSafe = false;
|
|
823
|
+
syncDirectory(controlRoot);
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
if (restorationSafe)
|
|
827
|
+
restoreQuarantinedLease(releasePath, lockPath, expected);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
645
830
|
export function restoreQuarantinedLease(quarantine, lockPath, expected, movedFromLock) {
|
|
646
831
|
const verification = `${quarantine}.restore-${randomBytes(16).toString('hex')}`;
|
|
647
832
|
let staged = false;
|
|
@@ -801,8 +986,14 @@ function removeOwnedClaim(path, token) {
|
|
|
801
986
|
return false;
|
|
802
987
|
}
|
|
803
988
|
}
|
|
804
|
-
function ownedLeaseIdentityMatches(lockPath, token, lockIdentity, ownerIdentity) {
|
|
989
|
+
function ownedLeaseIdentityMatches(lockPath, token, lockIdentity, ownerIdentity, lockKind) {
|
|
805
990
|
try {
|
|
991
|
+
if (lockKind === 'file') {
|
|
992
|
+
const currentOwner = readOwner(lockPath);
|
|
993
|
+
return (currentOwner.metadata.token === token &&
|
|
994
|
+
sameIdentity(lockIdentity, currentOwner.identity) &&
|
|
995
|
+
sameIdentity(ownerIdentity, currentOwner.identity));
|
|
996
|
+
}
|
|
806
997
|
const currentLock = lstatSync(lockPath);
|
|
807
998
|
const currentOwner = readOwner(join(lockPath, 'owner.json'));
|
|
808
999
|
return (currentLock.isDirectory() &&
|
|
@@ -926,6 +1117,20 @@ function acquisitionTimeout(hasExplicitDeadline) {
|
|
|
926
1117
|
? new LeaseContentionError('Repository operation deadline was exceeded.', 'DEADLINE_EXCEEDED')
|
|
927
1118
|
: new LeaseContentionError('Repository authority lease is busy.', 'LEASE_BUSY');
|
|
928
1119
|
}
|
|
1120
|
+
async function quarantineOwnedRelease(source, destination) {
|
|
1121
|
+
const backend = requireRepositoryPlatformBackend();
|
|
1122
|
+
for (let attempt = 1;; attempt += 1) {
|
|
1123
|
+
try {
|
|
1124
|
+
return quarantineNoReplace(source, destination);
|
|
1125
|
+
}
|
|
1126
|
+
catch (error) {
|
|
1127
|
+
const retryableWindowsSharingFailure = backend.platform === 'win32' && (hasCode(error, 'EBUSY') || hasCode(error, 'EACCES'));
|
|
1128
|
+
if (!retryableWindowsSharingFailure || attempt >= WINDOWS_RELEASE_RETRY_ATTEMPTS)
|
|
1129
|
+
throw error;
|
|
1130
|
+
await interruptibleDelay(WINDOWS_RELEASE_RETRY_DELAY_MS);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
929
1134
|
function quarantineNoReplace(source, destination) {
|
|
930
1135
|
emitFilesystemFault('before-lease-artifact-quarantine', source);
|
|
931
1136
|
try {
|