@@ -64,7 +64,7 @@ const BASE_INTERPRETER_MANAGER_IDS = new Set([
6464const CACHE_LOCK_TIMEOUT_MS = 5 * 60 * 1000 ;
6565const CACHE_LOCK_RETRY_MS = 500 ;
6666const CACHED_ASSOCIATION_VALIDATION_INTERVAL_MS = 5_000 ;
67- const DISCOVERY_RETRY_DELAYS_MS = [ 1_000 , 5_000 ] as const ;
67+ const DISCOVERY_RETRY_DELAYS_MS = [ 1_000 , 5_000 , 30_000 ] as const ;
6868/** Workspace-state key for PEP 723 script path to environment executable associations. */
6969export const INLINE_SCRIPT_ENVS_KEY = `${ ENVS_EXTENSION_ID } :inline-script:SCRIPT_ENVIRONMENTS` ;
7070
@@ -85,6 +85,11 @@ interface BuildCacheEntryResult {
8585 readonly retainLock ?: boolean ;
8686}
8787
88+ interface DiscoveryRefreshPass {
89+ readonly promise : Promise < boolean > ;
90+ readonly checksForSnapshotChanges : boolean ;
91+ }
92+
8893type CacheEntryInspection =
8994 | { readonly kind : 'absent' | 'stale' | 'uncertain' }
9095 | { readonly kind : 'reusable' ; readonly environment : PythonEnvironment } ;
@@ -101,7 +106,8 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
101106 private readonly fsPathToPersistedEnvPath = new Map < string , string > ( ) ;
102107 private readonly cachedAssociationValidatedAt = new Map < string , number > ( ) ;
103108 private readonly associationRevisions = new Map < string , number > ( ) ;
104- private pendingRefresh : Promise < boolean > | undefined ;
109+ private pendingRefresh : DiscoveryRefreshPass | undefined ;
110+ private pendingSnapshotRefresh : Promise < boolean > | undefined ;
105111 private activationDiscoveryActive = false ;
106112 private discoveryRetryAttempt = 0 ;
107113 private discoveryRetryTimer : ReturnType < typeof setTimeout > | undefined ;
@@ -241,7 +247,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
241247 return ;
242248 }
243249 this . stopActivationDiscovery ( ) ;
244- await this . getOrStartRefreshPass ( ) ;
250+ await this . getOrStartRefreshPass ( false ) ;
245251 }
246252
247253 async getEnvironments ( scope : GetEnvironmentsScope ) : Promise < PythonEnvironment [ ] > {
@@ -272,29 +278,82 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
272278 this . runActivationDiscoveryPass ( ) ;
273279 }
274280
275- private async getOrStartRefreshPass ( ) : Promise < boolean > {
281+ private getOrStartRefreshPass ( checkForSnapshotChanges : boolean ) : Promise < boolean > {
276282 const pending = this . pendingRefresh ;
283+ if ( pending ) {
284+ return checkForSnapshotChanges && ! pending . checksForSnapshotChanges
285+ ? this . getOrScheduleSnapshotRefresh ( pending )
286+ : pending . promise ;
287+ }
288+
289+ return this . startRefreshPass ( checkForSnapshotChanges ) ;
290+ }
291+
292+ private startRefreshPass ( checkForSnapshotChanges : boolean ) : Promise < boolean > {
293+ const pass : DiscoveryRefreshPass = {
294+ promise : this . refreshDiscoveredEnvironments ( checkForSnapshotChanges ) ,
295+ checksForSnapshotChanges : checkForSnapshotChanges ,
296+ } ;
297+ this . pendingRefresh = pass ;
298+ void pass . promise . then (
299+ ( ) => {
300+ if ( this . pendingRefresh === pass ) {
301+ this . pendingRefresh = undefined ;
302+ }
303+ } ,
304+ ( ) => {
305+ if ( this . pendingRefresh === pass ) {
306+ this . pendingRefresh = undefined ;
307+ }
308+ } ,
309+ ) ;
310+ return pass . promise ;
311+ }
312+
313+ private getOrScheduleSnapshotRefresh ( sharedPass : DiscoveryRefreshPass ) : Promise < boolean > {
314+ const pending = this . pendingSnapshotRefresh ;
277315 if ( pending ) {
278316 return pending ;
279317 }
280318
281- const refresh = this . refreshDiscoveredEnvironments ( ) ;
282- this . pendingRefresh = refresh ;
283- try {
284- return await refresh ;
285- } finally {
286- if ( this . pendingRefresh === refresh ) {
287- this . pendingRefresh = undefined ;
319+ const followUp = this . startSnapshotRefreshAfter ( sharedPass ) ;
320+ this . pendingSnapshotRefresh = followUp ;
321+ void followUp . then (
322+ ( ) => {
323+ if ( this . pendingSnapshotRefresh === followUp ) {
324+ this . pendingSnapshotRefresh = undefined ;
325+ }
326+ } ,
327+ ( ) => {
328+ if ( this . pendingSnapshotRefresh === followUp ) {
329+ this . pendingSnapshotRefresh = undefined ;
330+ }
331+ } ,
332+ ) ;
333+ return followUp ;
334+ }
335+
336+ private startSnapshotRefreshAfter ( sharedPass : DiscoveryRefreshPass ) : Promise < boolean > {
337+ return sharedPass . promise . then ( ( ) => {
338+ if ( this . disposed || ! this . activationDiscoveryActive ) {
339+ return false ;
288340 }
289- }
341+ const pending = this . pendingRefresh ;
342+ if ( pending && pending !== sharedPass ) {
343+ return pending . checksForSnapshotChanges
344+ ? pending . promise
345+ : this . startSnapshotRefreshAfter ( pending ) ;
346+ }
347+ return this . startRefreshPass ( true ) ;
348+ } ) ;
290349 }
291350
292351 private runActivationDiscoveryPass ( ) : void {
293352 if ( this . disposed || ! this . activationDiscoveryActive ) {
294353 return ;
295354 }
296355
297- void this . getOrStartRefreshPass ( )
356+ void this . getOrStartRefreshPass ( true )
298357 . then ( ( shouldRetry ) => {
299358 if ( this . disposed || ! this . activationDiscoveryActive ) {
300359 return ;
@@ -314,7 +373,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
314373 } ) ;
315374 }
316375
317- private async refreshDiscoveredEnvironments ( ) : Promise < boolean > {
376+ private async refreshDiscoveredEnvironments ( checkForSnapshotChanges : boolean ) : Promise < boolean > {
318377 const cacheRoot = getScriptEnvCacheRoot ( this . globalStorageUri ) ;
319378 const previousByKey = new Map (
320379 this . collection . map ( ( environment ) => [ this . getDiscoveredEnvironmentKey ( environment ) , environment ] ) ,
@@ -339,7 +398,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
339398 let shouldRetry = false ;
340399 for ( const entryName of entryNames . sort ( ) ) {
341400 if ( entryName . endsWith ( '.lock' ) ) {
342- lockedKeys . add ( normalizePath ( Uri . joinPath ( cacheRoot , entryName . slice ( 0 , - 5 ) ) . fsPath ) ) ;
401+ lockedKeys . add ( this . getDiscoveryEntryKey ( entryName . slice ( 0 , - 5 ) ) ) ;
343402 shouldRetry = true ;
344403 continue ;
345404 }
@@ -349,7 +408,7 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
349408 }
350409
351410 const envDir = Uri . joinPath ( cacheRoot , entryName ) ;
352- const key = normalizePath ( envDir . fsPath ) ;
411+ const key = this . getDiscoveryEntryKey ( entryName ) ;
353412 const discovered = await this . inspectDiscoveredCacheEntry ( cacheRoot , envDir ) ;
354413 if ( discovered . kind === 'resolved' ) {
355414 nextByKey . set ( key , discovered . environment ) ;
@@ -371,6 +430,25 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
371430 return false ;
372431 }
373432
433+ if ( checkForSnapshotChanges ) {
434+ try {
435+ const finalEntryNames = await fs . readdir ( cacheRoot . fsPath ) ;
436+ const initialEntries = new Set ( entryNames ) ;
437+ if (
438+ finalEntryNames . length !== entryNames . length ||
439+ finalEntryNames . some ( ( entryName ) => ! initialEntries . has ( entryName ) )
440+ ) {
441+ shouldRetry = true ;
442+ }
443+ } catch {
444+ shouldRetry = true ;
445+ }
446+ }
447+
448+ if ( this . disposed ) {
449+ return false ;
450+ }
451+
374452 // Preserve previously known entries when a refresh cannot safely classify
375453 // them because a build is in progress or the filesystem is transiently unavailable.
376454 this . replaceDiscoveredEnvironments ( sortEnvironments ( Array . from ( nextByKey . values ( ) ) ) ) ;
@@ -468,8 +546,12 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
468546 }
469547 }
470548
549+ private getDiscoveryEntryKey ( entryName : string ) : string {
550+ return normalizePath ( entryName ) ;
551+ }
552+
471553 private getDiscoveredEnvironmentKey ( environment : PythonEnvironment ) : string {
472- return normalizePath ( environment . sysPrefix ) ;
554+ return this . getDiscoveryEntryKey ( path . basename ( environment . sysPrefix ) ) ;
473555 }
474556
475557 private isSameDiscoveredEnvironment ( first : PythonEnvironment , second : PythonEnvironment ) : boolean {
@@ -1037,10 +1119,15 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
10371119 }
10381120
10391121 private async isCacheEntryBusy ( envDirPath : string ) : Promise < boolean > {
1040- return (
1041- this . pendingCreations . has ( path . basename ( envDirPath ) ) ||
1042- ( await fs . pathExists ( `${ path . resolve ( envDirPath ) } .lock` ) )
1043- ) ;
1122+ if ( this . pendingCreations . has ( path . basename ( envDirPath ) ) ) {
1123+ return true ;
1124+ }
1125+ try {
1126+ await fs . lstat ( `${ path . resolve ( envDirPath ) } .lock` ) ;
1127+ return true ;
1128+ } catch ( error ) {
1129+ return ! isFileNotFoundError ( error ) ;
1130+ }
10441131 }
10451132
10461133 private bumpAssociationRevision ( scriptPath : string ) : void {
0 commit comments