Skip to content

Commit ad675c1

Browse files
author
Kartik Raj
committed
Add FSWatching locator base class
1 parent f752706 commit ad675c1

File tree

6 files changed

+101
-45
lines changed

6 files changed

+101
-45
lines changed

src/client/common/platform/fileSystemWatcher.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as chokidar from 'chokidar';
77
import * as path from 'path';
8-
import { RelativePattern, workspace } from 'vscode';
8+
import { Disposable, RelativePattern, workspace } from 'vscode';
99
import { traceError, traceVerbose, traceWarning } from '../logger';
1010
import { normCasePath } from './fs-paths';
1111

@@ -22,35 +22,40 @@ const POLLING_INTERVAL = 5000;
2222
export function watchLocationForPattern(
2323
baseDir: string,
2424
pattern: string,
25-
callback: (type: FileChangeType, absPath: string) => void
26-
): void {
25+
callback: (type: FileChangeType, absPath: string) => void,
26+
): Disposable {
2727
// Use VSCode API iff base directory to exists within the current workspace folders
2828
const found = workspace.workspaceFolders?.find((e) => normCasePath(baseDir).startsWith(normCasePath(e.uri.fsPath)));
2929
if (found) {
30-
watchLocationUsingVSCodeAPI(baseDir, pattern, callback);
31-
} else {
32-
// Fallback to chokidar as base directory to lookup doesn't exist within the current workspace folders
33-
watchLocationUsingChokidar(baseDir, pattern, callback);
30+
return watchLocationUsingVSCodeAPI(baseDir, pattern, callback);
3431
}
32+
// Fallback to chokidar as base directory to lookup doesn't exist within the current workspace folders
33+
return watchLocationUsingChokidar(baseDir, pattern, callback);
3534
}
3635

3736
function watchLocationUsingVSCodeAPI(
3837
baseDir: string,
3938
pattern: string,
40-
callback: (type: FileChangeType, absPath: string) => void
39+
callback: (type: FileChangeType, absPath: string) => void,
4140
) {
4241
const globPattern = new RelativePattern(baseDir, pattern);
42+
const disposables: Disposable[] = [];
4343
traceVerbose(`Start watching: ${baseDir} with pattern ${pattern} using VSCode API`);
4444
const watcher = workspace.createFileSystemWatcher(globPattern);
45-
watcher.onDidCreate((e) => callback(FileChangeType.Created, e.fsPath));
46-
watcher.onDidChange((e) => callback(FileChangeType.Changed, e.fsPath));
47-
watcher.onDidDelete((e) => callback(FileChangeType.Deleted, e.fsPath));
45+
disposables.push(watcher.onDidCreate((e) => callback(FileChangeType.Created, e.fsPath)));
46+
disposables.push(watcher.onDidChange((e) => callback(FileChangeType.Changed, e.fsPath)));
47+
disposables.push(watcher.onDidDelete((e) => callback(FileChangeType.Deleted, e.fsPath)));
48+
return {
49+
dispose: async () => {
50+
disposables.forEach((d) => d.dispose());
51+
},
52+
};
4853
}
4954

5055
function watchLocationUsingChokidar(
5156
baseDir: string,
5257
pattern: string,
53-
callback: (type: FileChangeType, absPath: string) => void
58+
callback: (type: FileChangeType, absPath: string) => void,
5459
) {
5560
const watcherOpts: chokidar.WatchOptions = {
5661
cwd: baseDir,
@@ -69,9 +74,9 @@ function watchLocationUsingChokidar(
6974
'**/.hg/store/**',
7075
'/dev/**',
7176
'/proc/**',
72-
'/sys/**'
77+
'/sys/**',
7378
], // https://github.com/microsoft/vscode/issues/23954
74-
followSymlinks: false
79+
followSymlinks: false,
7580
};
7681
traceVerbose(`Start watching: ${baseDir} with pattern ${pattern} using chokidar`);
7782
let watcher: chokidar.FSWatcher | null = chokidar.watch(pattern, watcherOpts);
@@ -96,6 +101,13 @@ function watchLocationUsingChokidar(
96101
callback(eventType, absPath);
97102
});
98103

104+
const dispose = async () => {
105+
if (watcher) {
106+
await watcher.close();
107+
watcher = null;
108+
}
109+
};
110+
99111
watcher.on('error', async (error: NodeJS.ErrnoException) => {
100112
if (error) {
101113
// Specially handle ENOSPC errors that can happen when
@@ -105,13 +117,12 @@ function watchLocationUsingChokidar(
105117
// See https://github.com/Microsoft/vscode/issues/7950
106118
if (error.code === 'ENOSPC') {
107119
traceError(`Inotify limit reached (ENOSPC) for ${baseDir} with pattern ${pattern}`);
108-
if (watcher) {
109-
await watcher.close();
110-
watcher = null;
111-
}
120+
await dispose();
112121
} else {
113122
traceWarning(error.toString());
114123
}
115124
}
116125
});
126+
127+
return { dispose };
117128
}

src/client/pythonEnvironments/base/locator.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
// eslint-disable-next-line max-classes-per-file
45
import { Event, Uri } from 'vscode';
6+
import { AsyncDisposableRegistry } from '../../common/asyncDisposableRegistry';
7+
import { IAsyncDisposableRegistry, IDisposable } from '../../common/types';
58
import { iterEmpty } from '../../common/utils/async';
69
import { PythonEnvInfo, PythonEnvKind } from './info';
710
import {
@@ -178,9 +181,14 @@ interface IEmitter<E extends BasicPythonEnvsChangedEvent> {
178181
* should be used. Only in low-level cases should you consider using
179182
* `BasicPythonEnvsChangedEvent`.
180183
*/
181-
export abstract class LocatorBase<E extends BasicPythonEnvsChangedEvent = PythonEnvsChangedEvent> implements ILocator<E> {
184+
export abstract class LocatorBase<E extends BasicPythonEnvsChangedEvent = PythonEnvsChangedEvent>
185+
implements IDisposable, ILocator<E> {
182186
public readonly onChanged: Event<E>;
187+
183188
protected readonly emitter: IEmitter<E>;
189+
190+
protected readonly disposables: IAsyncDisposableRegistry = new AsyncDisposableRegistry();
191+
184192
constructor(watcher: IPythonEnvsWatcher<E> & IEmitter<E>) {
185193
this.emitter = watcher;
186194
this.onChanged = watcher.onChanged;
@@ -191,6 +199,10 @@ export abstract class LocatorBase<E extends BasicPythonEnvsChangedEvent = Python
191199
public async resolveEnv(_env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
192200
return undefined;
193201
}
202+
203+
public dispose(): void {
204+
this.disposables.dispose().ignoreErrors();
205+
}
194206
}
195207

196208
/**
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { Locator } from '../../locator';
5+
6+
/**
7+
* The base for Python envs locators who watch the file system.
8+
* Most low-level locators should be using this.
9+
*
10+
* Subclasses can call `this.emitter.fire()` * to emit events.
11+
*/
12+
export abstract class FSWatchingLocator extends Locator {
13+
public async abstract initialize(): Promise<void>;
14+
}

src/client/pythonEnvironments/common/pythonBinariesWatcher.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as minimatch from 'minimatch';
77
import * as path from 'path';
8+
import { Disposable } from 'vscode';
89
import { FileChangeType, watchLocationForPattern } from '../../common/platform/fileSystemWatcher';
910
import { getOSType, OSType } from '../../common/utils/platform';
1011

@@ -14,17 +15,23 @@ export function watchLocationForPythonBinaries(
1415
baseDir: string,
1516
callback: (type: FileChangeType, absPath: string) => void,
1617
executableGlob: string = executable,
17-
): void {
18+
): Disposable {
1819
const patterns = [executableGlob, `*/${executableGlob}`, `*/${binName}/${executableGlob}`];
20+
const disposables: Disposable[] = [];
1921
for (const pattern of patterns) {
20-
watchLocationForPattern(baseDir, pattern, (type: FileChangeType, e: string) => {
22+
disposables.push(watchLocationForPattern(baseDir, pattern, (type: FileChangeType, e: string) => {
2123
const isMatch = minimatch(e, path.join('**', executableGlob), { nocase: getOSType() === OSType.Windows });
2224
if (!isMatch) {
2325
// When deleting the file for some reason path to all directories leading up to python are reported
2426
// Skip those events
2527
return;
2628
}
2729
callback(type, e);
28-
});
30+
}));
2931
}
32+
return {
33+
dispose: async () => {
34+
disposables.forEach((d) => d.dispose());
35+
},
36+
};
3037
}

src/client/pythonEnvironments/discovery/locators/services/globalVirtualEnvronmentLocator.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,20 @@ import { traceVerbose } from '../../../../common/logger';
77
import { FileChangeType } from '../../../../common/platform/fileSystemWatcher';
88
import { chain, iterable, sleep } from '../../../../common/utils/async';
99
import {
10-
getEnvironmentVariable, getOSType, getUserHomeDir, OSType
10+
getEnvironmentVariable, getOSType, getUserHomeDir, OSType,
1111
} from '../../../../common/utils/platform';
1212
import { PythonEnvInfo, PythonEnvKind, UNKNOWN_PYTHON_VERSION } from '../../../base/info';
1313
import { buildEnvInfo } from '../../../base/info/env';
14-
import { IPythonEnvsIterator, Locator } from '../../../base/locator';
14+
import { IPythonEnvsIterator } from '../../../base/locator';
15+
import { FSWatchingLocator } from '../../../base/locators/lowLevel/locator';
1516
import { findInterpretersInDir } from '../../../common/commonUtils';
1617
import { getFileInfo, pathExists } from '../../../common/externalDependencies';
1718
import { watchLocationForPythonBinaries } from '../../../common/pythonBinariesWatcher';
1819
import { isPipenvEnvironment } from './pipEnvHelper';
1920
import {
2021
isVenvEnvironment,
2122
isVirtualenvEnvironment,
22-
isVirtualenvwrapperEnvironment
23+
isVirtualenvwrapperEnvironment,
2324
} from './virtualEnvironmentIdentifier';
2425

2526
const DEFAULT_SEARCH_DEPTH = 2;
@@ -80,7 +81,7 @@ async function getVirtualEnvKind(interpreterPath: string): Promise<PythonEnvKind
8081
/**
8182
* Finds and resolves virtual environments created in known global locations.
8283
*/
83-
export class GlobalVirtualEnvironmentLocator extends Locator {
84+
export class GlobalVirtualEnvironmentLocator extends FSWatchingLocator {
8485
private virtualEnvKinds = [
8586
PythonEnvKind.Venv,
8687
PythonEnvKind.VirtualEnv,
@@ -90,7 +91,10 @@ export class GlobalVirtualEnvironmentLocator extends Locator {
9091

9192
public constructor(private readonly searchDepth?: number) {
9293
super();
93-
this.registerWatchers().ignoreErrors();
94+
}
95+
96+
public async initialize(): Promise<void> {
97+
await this.startWatchers();
9498
}
9599

96100
public iterEnvs(): IPythonEnvsIterator {
@@ -168,15 +172,20 @@ export class GlobalVirtualEnvironmentLocator extends Locator {
168172
return undefined;
169173
}
170174

171-
private async registerWatchers(): Promise<void> {
175+
private async startWatchers(): Promise<void> {
172176
const dirs = await getGlobalVirtualEnvDirs();
173-
dirs.forEach((d) => watchLocationForPythonBinaries(d, async (type: FileChangeType, executablePath: string) => {
174-
// Note detecting kind of virtual env depends on the file structure around the executable, so we need to
175-
// wait before attempting to detect it. However even if the type detected is incorrect, it doesn't do any
176-
// practical harm as kinds in this locator are used in the same way (same activation commands etc.)
177-
await sleep(1000);
178-
const kind = await getVirtualEnvKind(executablePath);
179-
this.emitter.fire({ type, kind });
180-
}));
177+
dirs.forEach(
178+
(d) => this.disposables.push(
179+
watchLocationForPythonBinaries(d, async (type: FileChangeType, executablePath: string) => {
180+
// Note detecting kind of virtual env depends on the file structure around the
181+
// executable, so we need to wait before attempting to detect it. However even
182+
// if the type detected is incorrect, it doesn't do any practical harm as kinds
183+
// in this locator are used in the same way (same activation commands etc.)
184+
await sleep(1000);
185+
const kind = await getVirtualEnvKind(executablePath);
186+
this.emitter.fire({ type, kind });
187+
}),
188+
),
189+
);
181190
}
182191
}

src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { Architecture, getEnvironmentVariable } from '../../../../common/utils/p
1010
import { PythonEnvInfo, PythonEnvKind } from '../../../base/info';
1111
import { buildEnvInfo } from '../../../base/info/env';
1212
import { getPythonVersionFromPath } from '../../../base/info/pythonVersion';
13-
import { IPythonEnvsIterator, Locator } from '../../../base/locator';
13+
import { IPythonEnvsIterator } from '../../../base/locator';
14+
import { FSWatchingLocator } from '../../../base/locators/lowLevel/locator';
1415
import { getFileInfo } from '../../../common/externalDependencies';
1516
import { watchLocationForPythonBinaries } from '../../../common/pythonBinariesWatcher';
1617

@@ -137,10 +138,10 @@ export async function getWindowsStorePythonExes(): Promise<string[]> {
137138
.filter(isWindowsStorePythonExe);
138139
}
139140

140-
export class WindowsStoreLocator extends Locator {
141+
export class WindowsStoreLocator extends FSWatchingLocator {
141142
private readonly kind: PythonEnvKind = PythonEnvKind.WindowsStore;
142143

143-
public initialize(): void {
144+
public async initialize(): Promise<void> {
144145
this.startWatcher();
145146
}
146147

@@ -176,12 +177,14 @@ export class WindowsStoreLocator extends Locator {
176177

177178
private startWatcher(): void {
178179
const windowsAppsRoot = getWindowsStoreAppsRoot();
179-
watchLocationForPythonBinaries(
180-
windowsAppsRoot,
181-
(type: FileChangeType) => {
182-
this.emitter.fire({ type, kind: this.kind });
183-
},
184-
pythonExeGlob,
180+
this.disposables.push(
181+
watchLocationForPythonBinaries(
182+
windowsAppsRoot,
183+
(type: FileChangeType) => {
184+
this.emitter.fire({ type, kind: this.kind });
185+
},
186+
pythonExeGlob,
187+
),
185188
);
186189
}
187190
}

0 commit comments

Comments
 (0)