1+ import { Disposable , FileSystemWatcher } from 'vscode' ;
2+ import { PythonEnvironment } from '../../api' ;
3+ import { traceError , traceInfo , traceVerbose } from '../../common/logging' ;
4+ import { createFileSystemWatcher } from '../../common/workspace.apis' ;
5+ import { EnvironmentManagers , InternalDidChangeEnvironmentsEventArgs , InternalPackageManager } from '../../internal.api' ;
6+ import { resolveSitePackagesPath } from './sitePackagesUtils' ;
7+
8+ /**
9+ * Manages file system watchers for site-packages directories across all Python environments.
10+ * Automatically refreshes package lists when packages are installed or uninstalled.
11+ */
12+ export class SitePackagesWatcherService implements Disposable {
13+ private readonly watchers = new Map < string , FileSystemWatcher > ( ) ;
14+ private readonly disposables : Disposable [ ] = [ ] ;
15+
16+ constructor ( private readonly environmentManagers : EnvironmentManagers ) {
17+ this . initializeService ( ) ;
18+ }
19+
20+ /**
21+ * Initializes the service by setting up event listeners and creating watchers for existing environments.
22+ */
23+ private initializeService ( ) : void {
24+ traceInfo ( 'SitePackagesWatcherService: Initializing automatic package refresh service' ) ;
25+
26+ // Listen for environment changes
27+ this . disposables . push (
28+ this . environmentManagers . onDidChangeEnvironments ( this . handleEnvironmentChanges . bind ( this ) )
29+ ) ;
30+
31+ // Set up watchers for existing environments
32+ this . setupWatchersForExistingEnvironments ( ) ;
33+ }
34+
35+ /**
36+ * Sets up watchers for all existing environments.
37+ */
38+ private async setupWatchersForExistingEnvironments ( ) : Promise < void > {
39+ try {
40+ const managers = this . environmentManagers . managers ;
41+ for ( const manager of managers ) {
42+ try {
43+ const environments = await manager . getEnvironments ( 'all' ) ;
44+ for ( const environment of environments ) {
45+ await this . addWatcherForEnvironment ( environment ) ;
46+ }
47+ } catch ( error ) {
48+ traceError ( `Failed to get environments from manager ${ manager . id } :` , error ) ;
49+ }
50+ }
51+ } catch ( error ) {
52+ traceError ( 'Failed to setup watchers for existing environments:' , error ) ;
53+ }
54+ }
55+
56+ /**
57+ * Handles environment changes by adding or removing watchers as needed.
58+ */
59+ private async handleEnvironmentChanges ( event : InternalDidChangeEnvironmentsEventArgs ) : Promise < void > {
60+ for ( const change of event . changes ) {
61+ try {
62+ switch ( change . kind ) {
63+ case 'add' :
64+ await this . addWatcherForEnvironment ( change . environment ) ;
65+ break ;
66+ case 'remove' :
67+ this . removeWatcherForEnvironment ( change . environment ) ;
68+ break ;
69+ }
70+ } catch ( error ) {
71+ traceError ( `Error handling environment change for ${ change . environment . displayName } :` , error ) ;
72+ }
73+ }
74+ }
75+
76+ /**
77+ * Adds a file system watcher for the given environment's site-packages directory.
78+ */
79+ private async addWatcherForEnvironment ( environment : PythonEnvironment ) : Promise < void > {
80+ const envId = environment . envId . id ;
81+
82+ // Check if we already have a watcher for this environment
83+ if ( this . watchers . has ( envId ) ) {
84+ traceVerbose ( `Watcher already exists for environment: ${ environment . displayName } ` ) ;
85+ return ;
86+ }
87+
88+ try {
89+ const sitePackagesUri = await resolveSitePackagesPath ( environment ) ;
90+ if ( ! sitePackagesUri ) {
91+ traceVerbose ( `Could not resolve site-packages path for environment: ${ environment . displayName } ` ) ;
92+ return ;
93+ }
94+
95+ const pattern = `${ sitePackagesUri . fsPath } /**` ;
96+ const watcher = createFileSystemWatcher (
97+ pattern ,
98+ false , // don't ignore create events
99+ false , // don't ignore change events
100+ false // don't ignore delete events
101+ ) ;
102+
103+ // Set up event handlers
104+ watcher . onDidCreate ( ( ) => this . onSitePackagesChange ( environment ) ) ;
105+ watcher . onDidChange ( ( ) => this . onSitePackagesChange ( environment ) ) ;
106+ watcher . onDidDelete ( ( ) => this . onSitePackagesChange ( environment ) ) ;
107+
108+ this . watchers . set ( envId , watcher ) ;
109+ traceInfo ( `Created site-packages watcher for environment: ${ environment . displayName } at ${ sitePackagesUri . fsPath } ` ) ;
110+
111+ } catch ( error ) {
112+ traceError ( `Failed to create watcher for environment ${ environment . displayName } :` , error ) ;
113+ }
114+ }
115+
116+ /**
117+ * Removes the file system watcher for the given environment.
118+ */
119+ private removeWatcherForEnvironment ( environment : PythonEnvironment ) : void {
120+ const envId = environment . envId . id ;
121+ const watcher = this . watchers . get ( envId ) ;
122+
123+ if ( watcher ) {
124+ watcher . dispose ( ) ;
125+ this . watchers . delete ( envId ) ;
126+ traceInfo ( `Removed site-packages watcher for environment: ${ environment . displayName } ` ) ;
127+ }
128+ }
129+
130+ /**
131+ * Handles site-packages changes by triggering a package refresh.
132+ */
133+ private async onSitePackagesChange ( environment : PythonEnvironment ) : Promise < void > {
134+ try {
135+ traceVerbose ( `Site-packages changed for environment: ${ environment . displayName } , triggering package refresh` ) ;
136+
137+ // Get the package manager for this environment
138+ const packageManager = this . getPackageManagerForEnvironment ( environment ) ;
139+ if ( packageManager ) {
140+ // Trigger refresh asynchronously to avoid blocking file system events
141+ setImmediate ( async ( ) => {
142+ try {
143+ await packageManager . refresh ( environment ) ;
144+ traceInfo ( `Package list refreshed automatically for environment: ${ environment . displayName } ` ) ;
145+ } catch ( error ) {
146+ traceError ( `Failed to refresh packages for environment ${ environment . displayName } :` , error ) ;
147+ }
148+ } ) ;
149+ } else {
150+ traceVerbose ( `No package manager found for environment: ${ environment . displayName } ` ) ;
151+ }
152+ } catch ( error ) {
153+ traceError ( `Error handling site-packages change for environment ${ environment . displayName } :` , error ) ;
154+ }
155+ }
156+
157+ /**
158+ * Gets the appropriate package manager for the given environment.
159+ */
160+ private getPackageManagerForEnvironment ( environment : PythonEnvironment ) : InternalPackageManager | undefined {
161+ try {
162+ // Try to get package manager by environment manager's preferred package manager
163+ const envManager = this . environmentManagers . managers . find ( m =>
164+ m . id === environment . envId . managerId
165+ ) ;
166+
167+ if ( envManager ) {
168+ return this . environmentManagers . getPackageManager ( envManager . preferredPackageManagerId ) ;
169+ }
170+
171+ // Fallback to default package manager
172+ return this . environmentManagers . getPackageManager ( environment ) ;
173+ } catch ( error ) {
174+ traceError ( `Error getting package manager for environment ${ environment . displayName } :` , error ) ;
175+ return undefined ;
176+ }
177+ }
178+
179+ /**
180+ * Disposes all watchers and cleans up resources.
181+ */
182+ dispose ( ) : void {
183+ traceInfo ( 'SitePackagesWatcherService: Disposing automatic package refresh service' ) ;
184+
185+ // Dispose all watchers
186+ for ( const watcher of this . watchers . values ( ) ) {
187+ watcher . dispose ( ) ;
188+ }
189+ this . watchers . clear ( ) ;
190+
191+ // Dispose event listeners
192+ for ( const disposable of this . disposables ) {
193+ disposable . dispose ( ) ;
194+ }
195+ this . disposables . length = 0 ;
196+ }
197+ }
0 commit comments