@@ -11,6 +11,7 @@ import {
1111 DidChangeEnvironmentEventArgs ,
1212 DidChangeEnvironmentsEventArgs ,
1313 EnvironmentManager ,
14+ EnvironmentChangeKind ,
1415 GetEnvironmentScope ,
1516 GetEnvironmentsScope ,
1617 IconPath ,
@@ -51,6 +52,7 @@ import { normalizePath } from '../../../common/utils/pathUtils';
5152import { compareReleaseSegments , parseReleaseSegments } from '../../../common/utils/pep440Release' ;
5253import { getVenvPythonPath } from '../../../common/utils/virtualEnvironment' ;
5354import { NativePythonFinder } from '../../common/nativePythonFinder' ;
55+ import { sortEnvironments } from '../../common/utils' ;
5456import { resolveSystemPythonEnvironmentPath } from '../utils' ;
5557import * as uvPythonInstaller from '../uvPythonInstaller' ;
5658import { createWithProgress , resolveVenvPythonEnvironmentPath } from '../venvUtils' ;
@@ -64,6 +66,7 @@ const BASE_INTERPRETER_MANAGER_IDS = new Set([
6466const CACHE_LOCK_TIMEOUT_MS = 5 * 60 * 1000 ;
6567const CACHE_LOCK_RETRY_MS = 500 ;
6668const CACHED_ASSOCIATION_VALIDATION_INTERVAL_MS = 5_000 ;
69+ const DISCOVERY_RETRY_DELAYS_MS = [ 1_000 , 5_000 ] as const ;
6770/** Workspace-state key for PEP 723 script path to environment executable associations. */
6871export const INLINE_SCRIPT_ENVS_KEY = `${ ENVS_EXTENSION_ID } :inline-script:SCRIPT_ENVIRONMENTS` ;
6972
@@ -110,13 +113,19 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
110113 private readonly pendingCreations = new Map < string , Promise < PythonEnvironment | undefined > > ( ) ;
111114 private readonly directlyResolvedBaseInterpreters = new Map < string , PythonEnvironment > ( ) ;
112115 private baseInterpreterInstallationQueue : Promise < void > = Promise . resolve ( ) ;
116+ private collection : PythonEnvironment [ ] = [ ] ;
113117 private readonly pendingRehydrations = new Map < string , Promise < PythonEnvironment | undefined > > ( ) ;
114118 private readonly fsPathToEnv = new Map < string , PythonEnvironment > ( ) ;
115119 private readonly fsPathToPersistedEnvPath = new Map < string , string > ( ) ;
116120 private readonly cachedAssociationValidatedAt = new Map < string , number > ( ) ;
117121 private readonly associationRevisions = new Map < string , number > ( ) ;
122+ private pendingRefresh : Promise < boolean > | undefined ;
123+ private activationDiscoveryActive = false ;
124+ private discoveryRetryAttempt = 0 ;
125+ private discoveryRetryTimer : ReturnType < typeof setTimeout > | undefined ;
118126 private persistenceQueue : Promise < void > = Promise . resolve ( ) ;
119127 private selectionQueue : Promise < void > = Promise . resolve ( ) ;
128+ private disposed = false ;
120129
121130 private readonly _onDidChangeEnvironments = new EventEmitter < DidChangeEnvironmentsEventArgs > ( ) ;
122131 public readonly onDidChangeEnvironments : Event < DidChangeEnvironmentsEventArgs > =
@@ -264,10 +273,17 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
264273 }
265274
266275 async refresh ( _scope : RefreshEnvironmentsScope ) : Promise < void > {
267- return ;
276+ if ( this . disposed ) {
277+ return ;
278+ }
279+ this . stopActivationDiscovery ( ) ;
280+ await this . getOrStartRefreshPass ( ) ;
268281 }
269282
270- async getEnvironments ( _scope : GetEnvironmentsScope ) : Promise < PythonEnvironment [ ] > {
283+ async getEnvironments ( scope : GetEnvironmentsScope ) : Promise < PythonEnvironment [ ] > {
284+ if ( scope === 'all' ) {
285+ return Array . from ( this . collection ) ;
286+ }
271287 return [ ] ;
272288 }
273289
@@ -283,6 +299,257 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
283299 return undefined ;
284300 }
285301
302+ public startActivationDiscovery ( ) : void {
303+ if ( this . disposed || this . activationDiscoveryActive ) {
304+ return ;
305+ }
306+ this . activationDiscoveryActive = true ;
307+ this . discoveryRetryAttempt = 0 ;
308+ this . runActivationDiscoveryPass ( ) ;
309+ }
310+
311+ private async getOrStartRefreshPass ( ) : Promise < boolean > {
312+ const pending = this . pendingRefresh ;
313+ if ( pending ) {
314+ return pending ;
315+ }
316+
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 ;
324+ }
325+ }
326+ }
327+
328+ private runActivationDiscoveryPass ( ) : void {
329+ if ( this . disposed || ! this . activationDiscoveryActive ) {
330+ return ;
331+ }
332+
333+ void this . getOrStartRefreshPass ( )
334+ . then ( ( shouldRetry ) => {
335+ if ( this . disposed || ! this . activationDiscoveryActive ) {
336+ return ;
337+ }
338+ if ( ! shouldRetry ) {
339+ this . stopActivationDiscovery ( ) ;
340+ return ;
341+ }
342+ this . scheduleActivationDiscoveryRetry ( ) ;
343+ } )
344+ . catch ( ( error ) => {
345+ if ( this . disposed || ! this . activationDiscoveryActive ) {
346+ return ;
347+ }
348+ this . log . warn ( `Activation-time inline-script discovery failed: ${ getErrorMessage ( error ) } ` ) ;
349+ this . stopActivationDiscovery ( ) ;
350+ } ) ;
351+ }
352+
353+ private async refreshDiscoveredEnvironments ( ) : Promise < boolean > {
354+ const cacheRoot = getScriptEnvCacheRoot ( this . globalStorageUri ) ;
355+ const previousByKey = new Map (
356+ this . collection . map ( ( environment ) => [ this . getDiscoveredEnvironmentKey ( environment ) , environment ] ) ,
357+ ) ;
358+
359+ let entryNames : string [ ] ;
360+ try {
361+ entryNames = await fs . readdir ( cacheRoot . fsPath ) ;
362+ } catch ( error ) {
363+ if ( this . isDefinitivelyStalePathError ( error ) ) {
364+ entryNames = [ ] ;
365+ } else {
366+ this . log . warn (
367+ `Unable to inspect the inline-script cache root ${ cacheRoot . fsPath } : ${ getErrorMessage ( error ) } ` ,
368+ ) ;
369+ return true ;
370+ }
371+ }
372+
373+ const lockedKeys = new Set < string > ( ) ;
374+ const nextByKey = new Map < string , PythonEnvironment > ( ) ;
375+ let shouldRetry = false ;
376+ for ( const entryName of entryNames . sort ( ) ) {
377+ if ( entryName . endsWith ( '.lock' ) ) {
378+ lockedKeys . add ( normalizePath ( Uri . joinPath ( cacheRoot , entryName . slice ( 0 , - 5 ) ) . fsPath ) ) ;
379+ shouldRetry = true ;
380+ continue ;
381+ }
382+
383+ if ( this . disposed ) {
384+ return false ;
385+ }
386+
387+ const envDir = Uri . joinPath ( cacheRoot , entryName ) ;
388+ const key = normalizePath ( envDir . fsPath ) ;
389+ const discovered = await this . inspectDiscoveredCacheEntry ( cacheRoot , envDir ) ;
390+ if ( discovered . kind === 'resolved' ) {
391+ nextByKey . set ( key , discovered . environment ) ;
392+ } else if ( discovered . kind === 'preserve' ) {
393+ shouldRetry = true ;
394+ const previous = previousByKey . get ( key ) ;
395+ if ( previous ) {
396+ nextByKey . set ( key , previous ) ;
397+ }
398+ }
399+ }
400+ for ( const [ key , previous ] of previousByKey ) {
401+ if ( ! nextByKey . has ( key ) && lockedKeys . has ( key ) ) {
402+ nextByKey . set ( key , previous ) ;
403+ }
404+ }
405+
406+ if ( this . disposed ) {
407+ return false ;
408+ }
409+
410+ // Preserve previously known entries when a refresh cannot safely classify
411+ // them because a build is in progress or the filesystem is transiently unavailable.
412+ this . replaceDiscoveredEnvironments ( sortEnvironments ( Array . from ( nextByKey . values ( ) ) ) ) ;
413+ return shouldRetry ;
414+ }
415+
416+ private async inspectDiscoveredCacheEntry (
417+ cacheRoot : Uri ,
418+ envDir : Uri ,
419+ ) : Promise < DiscoveredCacheEntryResult > {
420+ try {
421+ const stat = await fs . lstat ( envDir . fsPath ) ;
422+ if ( ! stat . isDirectory ( ) || stat . isSymbolicLink ( ) ) {
423+ return { kind : 'skip' } ;
424+ }
425+ } catch ( error ) {
426+ return this . isDefinitivelyStalePathError ( error ) ? { kind : 'skip' } : { kind : 'preserve' } ;
427+ }
428+
429+ if ( await this . isCacheEntryBusy ( envDir . fsPath ) ) {
430+ return { kind : 'preserve' } ;
431+ }
432+
433+ try {
434+ if ( ! ( await resolveCacheEntryPath ( cacheRoot , envDir ) ) ) {
435+ return { kind : 'skip' } ;
436+ }
437+ } catch ( error ) {
438+ return this . isDefinitivelyStalePathError ( error ) ? { kind : 'skip' } : { kind : 'preserve' } ;
439+ }
440+
441+ const sidecarResult = await inspectMetaJson ( envDir ) ;
442+ if ( sidecarResult . kind !== 'valid' ) {
443+ return { kind : sidecarResult . kind === 'unavailable' ? 'preserve' : 'skip' } ;
444+ }
445+
446+ const baseInterpreterStatus = await getBaseInterpreterStatus ( envDir ) ;
447+ if ( baseInterpreterStatus !== 'available' ) {
448+ return { kind : baseInterpreterStatus === 'unavailable' ? 'preserve' : 'skip' } ;
449+ }
450+
451+ let environment : PythonEnvironment | undefined ;
452+ try {
453+ environment = await resolveVenvPythonEnvironmentPath (
454+ getVenvPythonPath ( envDir . fsPath ) ,
455+ this . nativeFinder ,
456+ this . api ,
457+ this ,
458+ this . baseManager ,
459+ ) ;
460+ } catch ( error ) {
461+ this . log . warn (
462+ `Unable to resolve inline-script cache entry ${ envDir . fsPath } : ${ getErrorMessage ( error ) } ` ,
463+ ) ;
464+ return { kind : 'preserve' } ;
465+ }
466+ if ( ! environment ) {
467+ return { kind : 'preserve' } ;
468+ }
469+
470+ const ownership = await inspectOwnedCacheEntry ( environment , cacheRoot , envDir ) ;
471+ if ( ownership !== 'expected' ) {
472+ return { kind : ownership === 'uncertain' ? 'preserve' : 'skip' } ;
473+ }
474+ if ( ! this . areEqualPythonReleases ( environment . version , sidecarResult . metadata . baseInterpreterVersion ) ) {
475+ return { kind : 'skip' } ;
476+ }
477+
478+ return { kind : 'resolved' , environment } ;
479+ }
480+
481+ private replaceDiscoveredEnvironments ( next : PythonEnvironment [ ] ) : void {
482+ const previousByKey = new Map (
483+ this . collection . map ( ( environment ) => [ this . getDiscoveredEnvironmentKey ( environment ) , environment ] ) ,
484+ ) ;
485+ const nextByKey = new Map ( next . map ( ( environment ) => [ this . getDiscoveredEnvironmentKey ( environment ) , environment ] ) ) ;
486+ const changes : DidChangeEnvironmentsEventArgs = [ ] ;
487+
488+ for ( const [ key , previous ] of previousByKey ) {
489+ const current = nextByKey . get ( key ) ;
490+ if ( ! current || ! this . isSameDiscoveredEnvironment ( previous , current ) ) {
491+ changes . push ( { kind : EnvironmentChangeKind . remove , environment : previous } ) ;
492+ }
493+ }
494+ for ( const [ key , current ] of nextByKey ) {
495+ const previous = previousByKey . get ( key ) ;
496+ if ( ! previous || ! this . isSameDiscoveredEnvironment ( previous , current ) ) {
497+ changes . push ( { kind : EnvironmentChangeKind . add , environment : current } ) ;
498+ }
499+ }
500+
501+ this . collection = next ;
502+ if ( changes . length > 0 ) {
503+ this . _onDidChangeEnvironments . fire ( changes ) ;
504+ }
505+ }
506+
507+ private getDiscoveredEnvironmentKey ( environment : PythonEnvironment ) : string {
508+ return normalizePath ( environment . sysPrefix ) ;
509+ }
510+
511+ private isSameDiscoveredEnvironment ( first : PythonEnvironment , second : PythonEnvironment ) : boolean {
512+ return (
513+ first . envId . managerId === second . envId . managerId &&
514+ normalizePath ( first . environmentPath . fsPath ) === normalizePath ( second . environmentPath . fsPath ) &&
515+ first . version === second . version
516+ ) ;
517+ }
518+
519+ private scheduleActivationDiscoveryRetry ( ) : void {
520+ if ( this . discoveryRetryTimer ) {
521+ return ;
522+ }
523+
524+ const delayMs = this . getDiscoveryRetryDelayMs ( this . discoveryRetryAttempt ) ;
525+ if ( delayMs === undefined ) {
526+ this . stopActivationDiscovery ( ) ;
527+ return ;
528+ }
529+
530+ this . discoveryRetryAttempt += 1 ;
531+ this . discoveryRetryTimer = setTimeout ( ( ) => {
532+ this . discoveryRetryTimer = undefined ;
533+ if ( this . disposed || ! this . activationDiscoveryActive ) {
534+ return ;
535+ }
536+ this . runActivationDiscoveryPass ( ) ;
537+ } , delayMs ) ;
538+ }
539+
540+ private getDiscoveryRetryDelayMs ( attempt : number ) : number | undefined {
541+ return DISCOVERY_RETRY_DELAYS_MS [ attempt ] ;
542+ }
543+
544+ private stopActivationDiscovery ( ) : void {
545+ if ( this . discoveryRetryTimer ) {
546+ clearTimeout ( this . discoveryRetryTimer ) ;
547+ this . discoveryRetryTimer = undefined ;
548+ }
549+ this . activationDiscoveryActive = false ;
550+ this . discoveryRetryAttempt = 0 ;
551+ }
552+
286553 private getScriptUri ( scope : CreateEnvironmentScope ) : Uri | undefined {
287554 const uri = scope instanceof Uri ? scope : Array . isArray ( scope ) && scope . length === 1 ? scope [ 0 ] : undefined ;
288555 return uri ?. scheme === 'file' ? uri : undefined ;
@@ -1390,6 +1657,8 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
13901657 }
13911658
13921659 dispose ( ) : void {
1660+ this . disposed = true ;
1661+ this . stopActivationDiscovery ( ) ;
13931662 this . _onDidChangeEnvironments . dispose ( ) ;
13941663 this . _onDidChangeEnvironment . dispose ( ) ;
13951664 }
@@ -1413,3 +1682,7 @@ interface PendingScriptUpdate extends ScriptReference {
14131682 readonly needsPersistence : boolean ;
14141683 readonly shouldNotify : boolean ;
14151684}
1685+
1686+ type DiscoveredCacheEntryResult =
1687+ | { readonly kind : 'preserve' | 'skip' }
1688+ | { readonly kind : 'resolved' ; readonly environment : PythonEnvironment } ;
0 commit comments