@@ -66,7 +66,7 @@ const BASE_INTERPRETER_MANAGER_IDS = new Set([
6666const CACHE_LOCK_TIMEOUT_MS = 5 * 60 * 1000 ;
6767const CACHE_LOCK_RETRY_MS = 500 ;
6868const CACHED_ASSOCIATION_VALIDATION_INTERVAL_MS = 5_000 ;
69- const DISCOVERY_RETRY_DELAYS_MS = [ 1_000 , 5_000 ] as const ;
69+ const DISCOVERY_RETRY_DELAYS_MS = [ 1_000 , 5_000 , 30_000 ] as const ;
7070/** Workspace-state key for PEP 723 script path to environment executable associations. */
7171export const INLINE_SCRIPT_ENVS_KEY = `${ ENVS_EXTENSION_ID } :inline-script:SCRIPT_ENVIRONMENTS` ;
7272
@@ -103,6 +103,11 @@ type InstallPythonAndRefreshResult =
103103 | { readonly kind : 'declined' }
104104 | { readonly kind : 'failed' } ;
105105
106+ interface DiscoveryRefreshPass {
107+ readonly promise : Promise < boolean > ;
108+ readonly checksForSnapshotChanges : boolean ;
109+ }
110+
106111type CacheEntryInspection =
107112 | { readonly kind : 'absent' | 'stale' | 'uncertain' }
108113 | { readonly kind : 'reusable' ; readonly environment : PythonEnvironment } ;
@@ -119,7 +124,8 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
119124 private readonly fsPathToPersistedEnvPath = new Map < string , string > ( ) ;
120125 private readonly cachedAssociationValidatedAt = new Map < string , number > ( ) ;
121126 private readonly associationRevisions = new Map < string , number > ( ) ;
122- private pendingRefresh : Promise < boolean > | undefined ;
127+ private pendingRefresh : DiscoveryRefreshPass | undefined ;
128+ private pendingSnapshotRefresh : Promise < boolean > | undefined ;
123129 private activationDiscoveryActive = false ;
124130 private discoveryRetryAttempt = 0 ;
125131 private discoveryRetryTimer : ReturnType < typeof setTimeout > | undefined ;
@@ -277,7 +283,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
277283 return ;
278284 }
279285 this . stopActivationDiscovery ( ) ;
280- await this . getOrStartRefreshPass ( ) ;
286+ await this . getOrStartRefreshPass ( false ) ;
281287 }
282288
283289 async getEnvironments ( scope : GetEnvironmentsScope ) : Promise < PythonEnvironment [ ] > {
@@ -308,29 +314,82 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
308314 this . runActivationDiscoveryPass ( ) ;
309315 }
310316
311- private async getOrStartRefreshPass ( ) : Promise < boolean > {
317+ private getOrStartRefreshPass ( checkForSnapshotChanges : boolean ) : Promise < boolean > {
312318 const pending = this . pendingRefresh ;
319+ if ( pending ) {
320+ return checkForSnapshotChanges && ! pending . checksForSnapshotChanges
321+ ? this . getOrScheduleSnapshotRefresh ( pending )
322+ : pending . promise ;
323+ }
324+
325+ return this . startRefreshPass ( checkForSnapshotChanges ) ;
326+ }
327+
328+ private startRefreshPass ( checkForSnapshotChanges : boolean ) : Promise < boolean > {
329+ const pass : DiscoveryRefreshPass = {
330+ promise : this . refreshDiscoveredEnvironments ( checkForSnapshotChanges ) ,
331+ checksForSnapshotChanges : checkForSnapshotChanges ,
332+ } ;
333+ this . pendingRefresh = pass ;
334+ void pass . promise . then (
335+ ( ) => {
336+ if ( this . pendingRefresh === pass ) {
337+ this . pendingRefresh = undefined ;
338+ }
339+ } ,
340+ ( ) => {
341+ if ( this . pendingRefresh === pass ) {
342+ this . pendingRefresh = undefined ;
343+ }
344+ } ,
345+ ) ;
346+ return pass . promise ;
347+ }
348+
349+ private getOrScheduleSnapshotRefresh ( sharedPass : DiscoveryRefreshPass ) : Promise < boolean > {
350+ const pending = this . pendingSnapshotRefresh ;
313351 if ( pending ) {
314352 return pending ;
315353 }
316354
317- const refresh = this . refreshDiscoveredEnvironments ( ) ;
318- this . pendingRefresh = refresh ;
319- try {
320- return await refresh ;
321- } finally {
322- if ( this . pendingRefresh === refresh ) {
323- this . pendingRefresh = undefined ;
355+ const followUp = this . startSnapshotRefreshAfter ( sharedPass ) ;
356+ this . pendingSnapshotRefresh = followUp ;
357+ void followUp . then (
358+ ( ) => {
359+ if ( this . pendingSnapshotRefresh === followUp ) {
360+ this . pendingSnapshotRefresh = undefined ;
361+ }
362+ } ,
363+ ( ) => {
364+ if ( this . pendingSnapshotRefresh === followUp ) {
365+ this . pendingSnapshotRefresh = undefined ;
366+ }
367+ } ,
368+ ) ;
369+ return followUp ;
370+ }
371+
372+ private startSnapshotRefreshAfter ( sharedPass : DiscoveryRefreshPass ) : Promise < boolean > {
373+ return sharedPass . promise . then ( ( ) => {
374+ if ( this . disposed || ! this . activationDiscoveryActive ) {
375+ return false ;
324376 }
325- }
377+ const pending = this . pendingRefresh ;
378+ if ( pending && pending !== sharedPass ) {
379+ return pending . checksForSnapshotChanges
380+ ? pending . promise
381+ : this . startSnapshotRefreshAfter ( pending ) ;
382+ }
383+ return this . startRefreshPass ( true ) ;
384+ } ) ;
326385 }
327386
328387 private runActivationDiscoveryPass ( ) : void {
329388 if ( this . disposed || ! this . activationDiscoveryActive ) {
330389 return ;
331390 }
332391
333- void this . getOrStartRefreshPass ( )
392+ void this . getOrStartRefreshPass ( true )
334393 . then ( ( shouldRetry ) => {
335394 if ( this . disposed || ! this . activationDiscoveryActive ) {
336395 return ;
@@ -350,7 +409,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
350409 } ) ;
351410 }
352411
353- private async refreshDiscoveredEnvironments ( ) : Promise < boolean > {
412+ private async refreshDiscoveredEnvironments ( checkForSnapshotChanges : boolean ) : Promise < boolean > {
354413 const cacheRoot = getScriptEnvCacheRoot ( this . globalStorageUri ) ;
355414 const previousByKey = new Map (
356415 this . collection . map ( ( environment ) => [ this . getDiscoveredEnvironmentKey ( environment ) , environment ] ) ,
@@ -375,7 +434,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
375434 let shouldRetry = false ;
376435 for ( const entryName of entryNames . sort ( ) ) {
377436 if ( entryName . endsWith ( '.lock' ) ) {
378- lockedKeys . add ( normalizePath ( Uri . joinPath ( cacheRoot , entryName . slice ( 0 , - 5 ) ) . fsPath ) ) ;
437+ lockedKeys . add ( this . getDiscoveryEntryKey ( entryName . slice ( 0 , - 5 ) ) ) ;
379438 shouldRetry = true ;
380439 continue ;
381440 }
@@ -385,7 +444,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
385444 }
386445
387446 const envDir = Uri . joinPath ( cacheRoot , entryName ) ;
388- const key = normalizePath ( envDir . fsPath ) ;
447+ const key = this . getDiscoveryEntryKey ( entryName ) ;
389448 const discovered = await this . inspectDiscoveredCacheEntry ( cacheRoot , envDir ) ;
390449 if ( discovered . kind === 'resolved' ) {
391450 nextByKey . set ( key , discovered . environment ) ;
@@ -407,6 +466,25 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
407466 return false ;
408467 }
409468
469+ if ( checkForSnapshotChanges ) {
470+ try {
471+ const finalEntryNames = await fs . readdir ( cacheRoot . fsPath ) ;
472+ const initialEntries = new Set ( entryNames ) ;
473+ if (
474+ finalEntryNames . length !== entryNames . length ||
475+ finalEntryNames . some ( ( entryName ) => ! initialEntries . has ( entryName ) )
476+ ) {
477+ shouldRetry = true ;
478+ }
479+ } catch {
480+ shouldRetry = true ;
481+ }
482+ }
483+
484+ if ( this . disposed ) {
485+ return false ;
486+ }
487+
410488 // Preserve previously known entries when a refresh cannot safely classify
411489 // them because a build is in progress or the filesystem is transiently unavailable.
412490 this . replaceDiscoveredEnvironments ( sortEnvironments ( Array . from ( nextByKey . values ( ) ) ) ) ;
@@ -504,8 +582,12 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
504582 }
505583 }
506584
585+ private getDiscoveryEntryKey ( entryName : string ) : string {
586+ return normalizePath ( entryName ) ;
587+ }
588+
507589 private getDiscoveredEnvironmentKey ( environment : PythonEnvironment ) : string {
508- return normalizePath ( environment . sysPrefix ) ;
590+ return this . getDiscoveryEntryKey ( path . basename ( environment . sysPrefix ) ) ;
509591 }
510592
511593 private isSameDiscoveredEnvironment ( first : PythonEnvironment , second : PythonEnvironment ) : boolean {
@@ -1073,10 +1155,15 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
10731155 }
10741156
10751157 private async isCacheEntryBusy ( envDirPath : string ) : Promise < boolean > {
1076- return (
1077- this . pendingCreations . has ( path . basename ( envDirPath ) ) ||
1078- ( await fs . pathExists ( `${ path . resolve ( envDirPath ) } .lock` ) )
1079- ) ;
1158+ if ( this . pendingCreations . has ( path . basename ( envDirPath ) ) ) {
1159+ return true ;
1160+ }
1161+ try {
1162+ await fs . lstat ( `${ path . resolve ( envDirPath ) } .lock` ) ;
1163+ return true ;
1164+ } catch ( error ) {
1165+ return ! isFileNotFoundError ( error ) ;
1166+ }
10801167 }
10811168
10821169 private bumpAssociationRevision ( scriptPath : string ) : void {
0 commit comments