@@ -42,6 +42,12 @@ interface LeaseState {
4242 token ?: string
4343}
4444
45+ interface LeaseDirectory {
46+ handle : FileHandle
47+ path : string
48+ access : "descriptor-relative" | "generation-checked-path"
49+ }
50+
4551class LeaseSidecarUnsafeError extends Error { }
4652
4753export interface PlaygroundArchiveCacheLock {
@@ -161,15 +167,15 @@ export async function acquirePlaygroundArchiveReference(archivePath: string, opt
161167 const policy = cacheLeasePolicy ( )
162168 const leaseMs = options . leaseMs ?? policy . leaseMs
163169 const referencesDirectory = `${ archivePath } .refs`
164- const directoryHandle = await openSafeLeaseDirectory ( referencesDirectory , true )
170+ const directory = await openSafeLeaseDirectory ( referencesDirectory , true )
165171 const token = randomUUID ( )
166172 const fileName = `${ token } .json`
167173 const referencePath = join ( referencesDirectory , fileName )
168174 let lease : LeaseHandle
169175 try {
170- lease = await createFileLease ( directoryHandle , fileName , referencePath , token , leaseMs , options . heartbeat !== false , options . releaseInterlock )
176+ lease = await createFileLease ( directory , fileName , referencePath , token , leaseMs , options . heartbeat !== false , options . releaseInterlock )
171177 } catch ( error ) {
172- await directoryHandle . close ( )
178+ await directory . handle . close ( )
173179 throw error
174180 }
175181
@@ -359,31 +365,34 @@ async function tryAcquireCacheLock(lockPath: string, leaseMs: number): Promise<L
359365 }
360366 throw error
361367 }
362- let directoryHandle : FileHandle | undefined
368+ let directory : LeaseDirectory | undefined
363369 try {
364- directoryHandle = await openSafeLeaseDirectory ( lockPath , false )
365- return await createDirectoryLease ( directoryHandle , lockPath , token , leaseMs )
370+ directory = await openSafeLeaseDirectory ( lockPath , false )
371+ return await createDirectoryLease ( directory , lockPath , token , leaseMs )
366372 } catch ( error ) {
367- await directoryHandle ? .close ( ) . catch ( ( ) => undefined )
373+ await directory ?. handle . close ( ) . catch ( ( ) => undefined )
368374 await rmdir ( lockPath ) . catch ( ( ) => undefined )
369375 if ( isConcurrentDisappearance ( error ) ) return undefined
370376 throw error
371377 }
372378}
373379
374- async function createDirectoryLease ( directoryHandle : FileHandle , lockPath : string , token : string , leaseMs : number ) : Promise < LeaseHandle > {
380+ async function createDirectoryLease ( directory : LeaseDirectory , lockPath : string , token : string , leaseMs : number ) : Promise < LeaseHandle > {
375381 const fileName = `${ token } .lease.json`
376- return createFileLease ( directoryHandle , fileName , join ( lockPath , fileName ) , token , leaseMs , true )
382+ return createFileLease ( directory , fileName , join ( lockPath , fileName ) , token , leaseMs , true )
377383}
378384
379- async function createFileLease ( directoryHandle : FileHandle , fileName : string , displayPath : string , token : string , leaseMs : number , automaticHeartbeat : boolean , releaseInterlock ?: ( leasePath : string ) => void | Promise < void > ) : Promise < LeaseHandle > {
380- const anchoredPath = join ( fileDescriptorPath ( directoryHandle ) , fileName )
381- const fileHandle = await open ( anchoredPath , constants . O_CREAT | constants . O_EXCL | constants . O_RDWR | constants . O_NOFOLLOW , 0o600 )
385+ async function createFileLease ( directory : LeaseDirectory , fileName : string , displayPath : string , token : string , leaseMs : number , automaticHeartbeat : boolean , releaseInterlock ?: ( leasePath : string ) => void | Promise < void > ) : Promise < LeaseHandle > {
386+ const fileHandle = await openLeaseDirectoryChild ( directory , fileName , constants . O_CREAT | constants . O_EXCL | constants . O_RDWR | constants . O_NOFOLLOW , 0o600 )
382387 await writeOwnerToHandle ( fileHandle , await ownerRecord ( token , leaseMs ) )
383388 return heartbeatLease ( fileHandle , token , leaseMs , async ( ) => {
384389 await releaseInterlock ?.( displayPath )
385- await fileHandle . close ( )
386- await directoryHandle . close ( )
390+ try {
391+ if ( releaseInterlock ) await assertLeaseDirectoryGeneration ( directory )
392+ } finally {
393+ await fileHandle . close ( )
394+ await directory . handle . close ( )
395+ }
387396 } , displayPath , automaticHeartbeat )
388397}
389398
@@ -441,49 +450,48 @@ async function cacheLockIsActive(lockPath: string, now: number, policy: Pick<Pla
441450 throw error
442451 }
443452 if ( ! lockStat . isDirectory ( ) || lockStat . isSymbolicLink ( ) ) return true
444- let directoryHandle : FileHandle
453+ let directory : LeaseDirectory
445454 try {
446- directoryHandle = await openSafeLeaseDirectory ( lockPath , false )
455+ directory = await openSafeLeaseDirectory ( lockPath , false )
447456 } catch ( error ) {
448457 if ( isConcurrentDisappearance ( error ) ) return false
449458 if ( error instanceof LeaseSidecarUnsafeError || errorHasCode ( error , "ELOOP" ) || errorHasCode ( error , "ENOTDIR" ) ) return true
450459 throw error
451460 }
452461 let active = false
453462 try {
454- const anchoredDirectory = fileDescriptorPath ( directoryHandle )
455463 let names : string [ ]
456464 try {
457- names = await readdir ( anchoredDirectory )
465+ names = await readLeaseDirectory ( directory )
458466 } catch ( error ) {
459467 if ( isConcurrentDisappearance ( error ) ) return false
460468 throw error
461469 }
462470 for ( const name of names . sort ( ) ) {
463- const path = join ( anchoredDirectory , name )
471+ const path = join ( lockPath , name )
464472 if ( name !== "owner.json" && ! name . endsWith ( ".lease.json" ) ) {
465473 let unknownStat
466474 try {
467- unknownStat = await lstat ( path )
475+ unknownStat = await lstatLeaseDirectoryChild ( directory , name )
468476 } catch ( error ) {
469477 if ( isConcurrentDisappearance ( error ) ) continue
470478 throw error
471479 }
472480 if ( unknownStat . isDirectory ( ) || now - lockStat . mtimeMs <= policy . staleLockMs || ! removeStale ) {
473481 active = true
474482 } else {
475- await unlinkIfPresent ( path )
483+ await unlinkLeaseDirectoryChild ( directory , name )
476484 }
477485 continue
478486 }
479- const state = await inspectLeaseFile ( path , now , policy . staleLockMs , removeStale )
487+ const state = await inspectLeaseFile ( directory , name , now , policy . staleLockMs , removeStale )
480488 if ( state . active ) {
481489 active = true
482490 }
483491 }
484492 if ( names . length === 0 && now - lockStat . mtimeMs <= policy . staleLockMs ) active = true
485493 } finally {
486- await directoryHandle . close ( )
494+ await directory . handle . close ( )
487495 if ( removeStale && ! active ) {
488496 await rmdir ( lockPath ) . catch ( ( error ) => {
489497 if ( ! isConcurrentDisappearance ( error ) && ! errorHasCode ( error , "ENOTEMPTY" ) ) throw error
@@ -507,9 +515,9 @@ async function activeArchiveReferenceCount(archivePath: string, now: number, pol
507515 if ( removeStale ) await unlinkIfPresent ( referencesDirectory )
508516 return removeStale ? 0 : 1
509517 }
510- let directoryHandle : FileHandle
518+ let directory : LeaseDirectory
511519 try {
512- directoryHandle = await openSafeLeaseDirectory ( referencesDirectory , false )
520+ directory = await openSafeLeaseDirectory ( referencesDirectory , false )
513521 } catch ( error ) {
514522 if ( isConcurrentDisappearance ( error ) ) return 0
515523 if ( error instanceof LeaseSidecarUnsafeError || errorHasCode ( error , "ELOOP" ) || errorHasCode ( error , "ENOTDIR" ) ) {
@@ -518,71 +526,78 @@ async function activeArchiveReferenceCount(archivePath: string, now: number, pol
518526 }
519527 throw error
520528 }
521- const anchoredDirectory = fileDescriptorPath ( directoryHandle )
522529 let names : string [ ]
523530 try {
524- names = await readdir ( anchoredDirectory )
531+ names = await readLeaseDirectory ( directory )
525532 } catch ( error ) {
526- await directoryHandle . close ( )
533+ await directory . handle . close ( )
527534 if ( isConcurrentDisappearance ( error ) ) return 0
528535 throw error
529536 }
530537 let active = 0
531538 try {
532539 for ( const name of names . sort ( ) ) {
533- const path = join ( anchoredDirectory , name )
534- const state = await inspectLeaseFile ( path , now , policy . staleLockMs , removeStale , diagnostics )
540+ const state = await inspectLeaseFile ( directory , name , now , policy . staleLockMs , removeStale , diagnostics )
535541 if ( state . active ) {
536542 active += 1
537543 }
538544 }
539545 } finally {
540- await directoryHandle . close ( )
546+ await directory . handle . close ( )
541547 }
542548 return active
543549}
544550
545- async function inspectLeaseFile ( path : string , now : number , legacyStaleMs : number , removeUnsafe : boolean , diagnostics : PlaygroundCustomArchiveCacheDiagnostic [ ] = [ ] ) : Promise < LeaseState > {
546- let pathStat
547- try {
548- pathStat = await lstat ( path )
549- } catch ( error ) {
550- if ( isConcurrentDisappearance ( error ) ) return { active : false }
551- throw error
552- }
553- if ( ! pathStat . isFile ( ) || pathStat . isSymbolicLink ( ) || pathStat . nlink !== 1 ) {
554- diagnostics . push ( { code : "custom-archive-ref-entry-unsafe" , message : "Archive lease entry is not a singly-linked regular file and was not followed." , severity : "warning" , path } )
555- if ( removeUnsafe && ! pathStat . isDirectory ( ) ) await unlinkIfPresent ( path )
556- return { active : ! removeUnsafe || pathStat . isDirectory ( ) }
557- }
558-
559- let handle : FileHandle | undefined
551+ async function inspectLeaseFile ( directory : LeaseDirectory , name : string , now : number , legacyStaleMs : number , removeUnsafe : boolean , diagnostics : PlaygroundCustomArchiveCacheDiagnostic [ ] = [ ] ) : Promise < LeaseState > {
552+ const path = join ( directory . path , name )
560553 try {
561- handle = await open ( path , constants . O_RDONLY | constants . O_NOFOLLOW )
562- const openedStat = await handle . stat ( )
563- if ( openedStat . dev !== pathStat . dev || openedStat . ino !== pathStat . ino ) {
564- diagnostics . push ( { code : "custom-archive-ref-entry-changed" , message : "Archive lease entry changed while opening and was conservatively protected." , severity : "warning" , path } )
565- return { active : true }
554+ await assertLeaseDirectoryGeneration ( directory )
555+ let pathStat
556+ try {
557+ pathStat = await lstatLeaseDirectoryChild ( directory , name )
558+ } catch ( error ) {
559+ if ( isConcurrentDisappearance ( error ) ) return { active : false }
560+ throw error
566561 }
567- const owner = await readOwnerFromHandle ( handle )
568- if ( owner ) {
569- const expiresAt = Date . parse ( owner . expiresAt )
570- if ( Number . isFinite ( expiresAt ) && expiresAt > now ) return { active : true , token : owner . token }
571- if ( removeUnsafe ) await unlinkIfPresent ( path )
572- return { active : false , token : owner . token }
562+ if ( ! pathStat . isFile ( ) || pathStat . isSymbolicLink ( ) || pathStat . nlink !== 1 ) {
563+ diagnostics . push ( { code : "custom-archive-ref-entry-unsafe" , message : "Archive lease entry is not a singly-linked regular file and was not followed." , severity : "warning" , path } )
564+ if ( removeUnsafe && ! pathStat . isDirectory ( ) ) await unlinkLeaseDirectoryChild ( directory , name )
565+ return { active : ! removeUnsafe || pathStat . isDirectory ( ) }
573566 }
574- const active = now - pathStat . mtimeMs <= legacyStaleMs
575- if ( ! active && removeUnsafe ) await unlinkIfPresent ( path )
576- return { active }
577- } catch ( error ) {
578- if ( isConcurrentDisappearance ( error ) ) return { active : false }
579- if ( errorHasCode ( error , "ELOOP" ) ) {
580- diagnostics . push ( { code : "custom-archive-ref-entry-unsafe" , message : "Archive lease entry became a symlink and was conservatively protected." , severity : "warning" , path } )
581- return { active : true }
567+ let handle : FileHandle | undefined
568+ try {
569+ handle = await openLeaseDirectoryChild ( directory , name , constants . O_RDONLY | constants . O_NOFOLLOW )
570+ const openedStat = await handle . stat ( )
571+ if ( openedStat . dev !== pathStat . dev || openedStat . ino !== pathStat . ino ) {
572+ diagnostics . push ( { code : "custom-archive-ref-entry-changed" , message : "Archive lease entry changed while opening and was conservatively protected." , severity : "warning" , path } )
573+ return { active : true }
574+ }
575+ const owner = await readOwnerFromHandle ( handle )
576+ if ( owner ) {
577+ const expiresAt = Date . parse ( owner . expiresAt )
578+ if ( Number . isFinite ( expiresAt ) && expiresAt > now ) return { active : true , token : owner . token }
579+ if ( removeUnsafe ) await unlinkLeaseDirectoryChild ( directory , name )
580+ return { active : false , token : owner . token }
581+ }
582+ const active = now - pathStat . mtimeMs <= legacyStaleMs
583+ if ( ! active && removeUnsafe ) await unlinkLeaseDirectoryChild ( directory , name )
584+ return { active }
585+ } catch ( error ) {
586+ if ( isConcurrentDisappearance ( error ) ) return { active : false }
587+ if ( errorHasCode ( error , "ELOOP" ) ) {
588+ diagnostics . push ( { code : "custom-archive-ref-entry-unsafe" , message : "Archive lease entry became a symlink and was conservatively protected." , severity : "warning" , path } )
589+ return { active : true }
590+ }
591+ throw error
592+ } finally {
593+ await handle ?. close ( )
582594 }
583- throw error
584595 } finally {
585- await handle ?. close ( )
596+ try {
597+ await assertLeaseDirectoryGeneration ( directory )
598+ } catch ( error ) {
599+ if ( ! isConcurrentDisappearance ( error ) ) throw error
600+ }
586601 }
587602}
588603
@@ -649,7 +664,7 @@ async function removeOrphanReferencesDirectory(path: string, now: number, policy
649664 }
650665}
651666
652- async function openSafeLeaseDirectory ( path : string , create : boolean ) : Promise < FileHandle > {
667+ async function openSafeLeaseDirectory ( path : string , create : boolean ) : Promise < LeaseDirectory > {
653668 for ( let attempt = 0 ; attempt < ( create ? 3 : 1 ) ; attempt += 1 ) {
654669 try {
655670 if ( create ) {
@@ -673,18 +688,71 @@ async function openSafeLeaseDirectory(path: string, create: boolean): Promise<Fi
673688 await handle . close ( )
674689 throw new LeaseSidecarUnsafeError ( `Playground cache lease sidecar changed while opening: ${ path } ` )
675690 }
676- return handle
691+ return { handle, path , access : leaseDirectoryAccess ( ) }
677692 } catch ( error ) {
678693 if ( ! create || ! isConcurrentDisappearance ( error ) || attempt === 2 ) throw error
679694 }
680695 }
681696 throw new Error ( `Unable to create Playground cache lease sidecar: ${ path } ` )
682697}
683698
684- function fileDescriptorPath ( handle : FileHandle ) : string {
685- if ( process . platform === "linux" ) return `/proc/self/fd/${ handle . fd } `
686- if ( process . platform === "darwin" || process . platform === "freebsd" ) return `/dev/fd/${ handle . fd } `
687- throw new Error ( "Playground cache leases require a platform with file-descriptor paths" )
699+ function leaseDirectoryAccess ( ) : LeaseDirectory [ "access" ] {
700+ if ( process . platform === "linux" ) return "descriptor-relative"
701+ if ( process . platform === "darwin" || process . platform === "freebsd" ) return "generation-checked-path"
702+ throw new Error ( "Playground cache leases require Linux descriptor-relative access or Darwin/FreeBSD generation-checked path access" )
703+ }
704+
705+ function leaseDirectoryChildPath ( directory : LeaseDirectory , name ?: string ) : string {
706+ const path = directory . access === "descriptor-relative" ? `/proc/self/fd/${ directory . handle . fd } ` : directory . path
707+ return name === undefined ? path : join ( path , name )
708+ }
709+
710+ async function assertLeaseDirectoryGeneration ( directory : LeaseDirectory ) : Promise < void > {
711+ if ( directory . access !== "generation-checked-path" ) return
712+ const [ pathStat , handleStat ] = await Promise . all ( [ lstat ( directory . path ) , directory . handle . stat ( ) ] )
713+ if ( ! pathStat . isDirectory ( ) || pathStat . isSymbolicLink ( ) || pathStat . dev !== handleStat . dev || pathStat . ino !== handleStat . ino ) {
714+ throw new LeaseSidecarUnsafeError ( `Playground cache lease sidecar changed while accessing: ${ directory . path } ` )
715+ }
716+ }
717+
718+ async function readLeaseDirectory ( directory : LeaseDirectory ) : Promise < string [ ] > {
719+ await assertLeaseDirectoryGeneration ( directory )
720+ try {
721+ return await readdir ( leaseDirectoryChildPath ( directory ) )
722+ } finally {
723+ await assertLeaseDirectoryGeneration ( directory )
724+ }
725+ }
726+
727+ async function lstatLeaseDirectoryChild ( directory : LeaseDirectory , name : string ) {
728+ await assertLeaseDirectoryGeneration ( directory )
729+ try {
730+ return await lstat ( leaseDirectoryChildPath ( directory , name ) )
731+ } finally {
732+ await assertLeaseDirectoryGeneration ( directory )
733+ }
734+ }
735+
736+ async function openLeaseDirectoryChild ( directory : LeaseDirectory , name : string , flags : number , mode ?: number ) : Promise < FileHandle > {
737+ await assertLeaseDirectoryGeneration ( directory )
738+ let handle : FileHandle | undefined
739+ try {
740+ handle = await open ( leaseDirectoryChildPath ( directory , name ) , flags , mode )
741+ await assertLeaseDirectoryGeneration ( directory )
742+ return handle
743+ } catch ( error ) {
744+ await handle ?. close ( )
745+ throw error
746+ }
747+ }
748+
749+ async function unlinkLeaseDirectoryChild ( directory : LeaseDirectory , name : string ) : Promise < boolean > {
750+ await assertLeaseDirectoryGeneration ( directory )
751+ try {
752+ return await unlinkIfPresent ( leaseDirectoryChildPath ( directory , name ) )
753+ } finally {
754+ await assertLeaseDirectoryGeneration ( directory )
755+ }
688756}
689757
690758async function readOwnerFromHandle ( handle : FileHandle ) : Promise < CacheOwnerRecord | undefined > {
0 commit comments