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