@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/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
- const value = new TextDecoder().decode(readRegularFile(path, MAX_LEASE_METADATA_BYTES)).trim();
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
- if (preliminary.isSymbolicLink())
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 prefix = `${basename(claimPath)}.prepare-`;
469
- const candidates = readdirSync(dirname(claimPath)).filter((name) => name.startsWith(prefix));
470
- const matching = candidates.filter((name) => {
471
- const suffix = name.slice(prefix.length);
472
- if (!/^[0-9a-f-]{32,64}$/u.test(suffix))
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 ? interruptedClaimPeer(path, initialIdentity) : undefined;
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 ? interruptedClaimPeer(path, 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() || (stat.nlink !== 1 && stat.nlink !== 2))
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
- const prefix = `${basename(path)}.prepare-`;
566
- const peers = readdirSync(dirname(path)).flatMap((name) => {
567
- if (!name.startsWith(prefix))
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(stat, peer) ? [{ token }] : [];
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 (!quarantineNoReplace(lockPath, releasePath))
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 {