Skip to content

Commit 16d4701

Browse files
author
Kartik Raj
authored
Added FSWatching base class and made related changes (#14605)
* Add FSWatching locator base class * Correct glob pattern to not match python3.2whoa * Add documentation of python binary watcher * Fix lint errors * Update ignore list * Add disposable registry * Modify FSWatching Locator * Code reviews * Use string[] * Remove list disposable getter
1 parent 8174a6c commit 16d4701

File tree

8 files changed

+203
-64
lines changed

8 files changed

+203
-64
lines changed

src/client/common/platform/fileSystemWatcher.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import * as chokidar from 'chokidar';
77
import * as path from 'path';
88
import { RelativePattern, workspace } from 'vscode';
99
import { traceError, traceVerbose, traceWarning } from '../logger';
10+
import { DisposableRegistry } from '../syncDisposableRegistry';
11+
import { IDisposable } from '../types';
1012
import { normCasePath } from './fs-paths';
1113

1214
/**
@@ -23,35 +25,36 @@ export function watchLocationForPattern(
2325
baseDir: string,
2426
pattern: string,
2527
callback: (type: FileChangeType, absPath: string) => void
26-
): void {
28+
): IDisposable {
2729
// Use VSCode API iff base directory to exists within the current workspace folders
2830
const found = workspace.workspaceFolders?.find((e) => normCasePath(baseDir).startsWith(normCasePath(e.uri.fsPath)));
2931
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);
32+
return watchLocationUsingVSCodeAPI(baseDir, pattern, callback);
3433
}
34+
// Fallback to chokidar as base directory to lookup doesn't exist within the current workspace folders
35+
return watchLocationUsingChokidar(baseDir, pattern, callback);
3536
}
3637

3738
function watchLocationUsingVSCodeAPI(
3839
baseDir: string,
3940
pattern: string,
4041
callback: (type: FileChangeType, absPath: string) => void
41-
) {
42+
): IDisposable {
4243
const globPattern = new RelativePattern(baseDir, pattern);
44+
const disposables = new DisposableRegistry();
4345
traceVerbose(`Start watching: ${baseDir} with pattern ${pattern} using VSCode API`);
4446
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));
47+
disposables.push(watcher.onDidCreate((e) => callback(FileChangeType.Created, e.fsPath)));
48+
disposables.push(watcher.onDidChange((e) => callback(FileChangeType.Changed, e.fsPath)));
49+
disposables.push(watcher.onDidDelete((e) => callback(FileChangeType.Deleted, e.fsPath)));
50+
return disposables;
4851
}
4952

5053
function watchLocationUsingChokidar(
5154
baseDir: string,
5255
pattern: string,
5356
callback: (type: FileChangeType, absPath: string) => void
54-
) {
57+
): IDisposable {
5558
const watcherOpts: chokidar.WatchOptions = {
5659
cwd: baseDir,
5760
ignoreInitial: true,
@@ -69,7 +72,9 @@ function watchLocationUsingChokidar(
6972
'**/.hg/store/**',
7073
'/dev/**',
7174
'/proc/**',
72-
'/sys/**'
75+
'/sys/**',
76+
'**/lib/**',
77+
'**/includes/**'
7378
], // https://github.com/microsoft/vscode/issues/23954
7479
followSymlinks: false
7580
};
@@ -96,6 +101,18 @@ function watchLocationUsingChokidar(
96101
callback(eventType, absPath);
97102
});
98103

104+
const stopWatcher = async () => {
105+
if (watcher) {
106+
const obj = watcher;
107+
watcher = null;
108+
try {
109+
await obj.close();
110+
} catch (err) {
111+
traceError(`Failed to close FS watcher (${err})`);
112+
}
113+
}
114+
};
115+
99116
watcher.on('error', async (error: NodeJS.ErrnoException) => {
100117
if (error) {
101118
// Specially handle ENOSPC errors that can happen when
@@ -105,13 +122,12 @@ function watchLocationUsingChokidar(
105122
// See https://github.com/Microsoft/vscode/issues/7950
106123
if (error.code === 'ENOSPC') {
107124
traceError(`Inotify limit reached (ENOSPC) for ${baseDir} with pattern ${pattern}`);
108-
if (watcher) {
109-
await watcher.close();
110-
watcher = null;
111-
}
125+
await stopWatcher();
112126
} else {
113127
traceWarning(error.toString());
114128
}
115129
}
116130
});
131+
132+
return { dispose: () => stopWatcher().ignoreErrors() };
117133
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { traceWarning } from './logger';
7+
import { IDisposable } from './types';
8+
9+
/**
10+
* Responsible for disposing a list of disposables synchronusly.
11+
*/
12+
export class DisposableRegistry implements IDisposable {
13+
private _list: IDisposable[] = [];
14+
15+
public dispose(): void {
16+
this._list.forEach((l, index) => {
17+
try {
18+
l.dispose();
19+
} catch (err) {
20+
traceWarning(`dispose() #${index} failed (${err})`);
21+
}
22+
});
23+
this._list = [];
24+
}
25+
26+
public push(disposable?: IDisposable): void {
27+
if (disposable) {
28+
this._list.push(disposable);
29+
}
30+
}
31+
}

src/client/pythonEnvironments/base/locator.ts

Lines changed: 15 additions & 3 deletions
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 { DisposableRegistry } from '../../common/syncDisposableRegistry';
7+
import { IDisposable } from '../../common/types';
58
import { iterEmpty } from '../../common/utils/async';
69
import { PythonEnvInfo, PythonEnvKind } from './info';
710
import {
@@ -178,19 +181,28 @@ 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+
abstract class LocatorBase<E extends BasicPythonEnvsChangedEvent = PythonEnvsChangedEvent>
185+
implements IDisposable, ILocator<E> {
182186
public readonly onChanged: Event<E>;
183-
protected readonly emitter: IEmitter<E>;
187+
188+
protected readonly emitter: IPythonEnvsWatcher<E> & IEmitter<E>;
189+
190+
protected readonly disposables = new DisposableRegistry();
191+
184192
constructor(watcher: IPythonEnvsWatcher<E> & IEmitter<E>) {
185193
this.emitter = watcher;
186-
this.onChanged = watcher.onChanged;
194+
this.onChanged = this.emitter.onChanged;
187195
}
188196

189197
public abstract iterEnvs(query?: QueryForEvent<E>): IPythonEnvsIterator;
190198

191199
public async resolveEnv(_env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
192200
return undefined;
193201
}
202+
203+
public dispose(): void {
204+
this.disposables.dispose();
205+
}
194206
}
195207

196208
/**
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { FileChangeType } from '../../../../common/platform/fileSystemWatcher';
5+
import { sleep } from '../../../../common/utils/async';
6+
import { watchLocationForPythonBinaries } from '../../../common/pythonBinariesWatcher';
7+
import { PythonEnvKind } from '../../info';
8+
import { Locator } from '../../locator';
9+
10+
/**
11+
* The base for Python envs locators who watch the file system.
12+
* Most low-level locators should be using this.
13+
*
14+
* Subclasses can call `this.emitter.fire()` * to emit events.
15+
*/
16+
export abstract class FSWatchingLocator extends Locator {
17+
private initialized = false;
18+
19+
constructor(
20+
/**
21+
* Location(s) to watch for python binaries.
22+
*/
23+
private readonly getRoots: () => Promise<string[]> | string | string[],
24+
/**
25+
* Returns the kind of environment specific to locator given the path to exectuable.
26+
*/
27+
private readonly getKind: (executable: string) => Promise<PythonEnvKind>,
28+
private readonly opts: {
29+
/**
30+
* Glob which represents basename of the executable to watch.
31+
*/
32+
executableBaseGlob?: string,
33+
/**
34+
* Time to wait before handling an environment-created event.
35+
*/
36+
delayOnCreated?: number, // milliseconds
37+
} = {},
38+
) {
39+
super();
40+
}
41+
42+
public async initialize(): Promise<void> {
43+
if (this.initialized) {
44+
return;
45+
}
46+
this.initialized = true;
47+
await this.startWatchers();
48+
}
49+
50+
public dispose(): void {
51+
super.dispose();
52+
this.initialized = false;
53+
}
54+
55+
private async startWatchers(): Promise<void> {
56+
let roots = await this.getRoots();
57+
if (typeof roots === 'string') {
58+
roots = [roots];
59+
}
60+
roots.forEach((root) => this.startWatcher(root));
61+
}
62+
63+
private startWatcher(root: string): void {
64+
this.disposables.push(
65+
watchLocationForPythonBinaries(
66+
root,
67+
async (type: FileChangeType, executable: string) => {
68+
if (type === FileChangeType.Created) {
69+
if (this.opts.delayOnCreated !== undefined) {
70+
// Note detecting kind of env depends on the file structure around the
71+
// executable, so we need to wait before attempting to detect it.
72+
await sleep(this.opts.delayOnCreated);
73+
}
74+
}
75+
const kind = await this.getKind(executable);
76+
this.emitter.fire({ type, kind });
77+
},
78+
this.opts.executableBaseGlob,
79+
),
80+
);
81+
}
82+
}

src/client/pythonEnvironments/common/pythonBinariesWatcher.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,37 @@
66
import * as minimatch from 'minimatch';
77
import * as path from 'path';
88
import { FileChangeType, watchLocationForPattern } from '../../common/platform/fileSystemWatcher';
9+
import { DisposableRegistry } from '../../common/syncDisposableRegistry';
10+
import { IDisposable } from '../../common/types';
911
import { getOSType, OSType } from '../../common/utils/platform';
1012

1113
const [executable, binName] = getOSType() === OSType.Windows ? ['python.exe', 'Scripts'] : ['python', 'bin'];
1214

15+
/**
16+
* @param baseDir The base directory from which watch paths are to be derived.
17+
* @param callback The listener function will be called when the event happens.
18+
* @param executableBaseGlob Glob which represents basename of the executable to watch.
19+
*/
1320
export function watchLocationForPythonBinaries(
1421
baseDir: string,
1522
callback: (type: FileChangeType, absPath: string) => void,
16-
executableGlob: string = executable,
17-
): void {
18-
const patterns = [executableGlob, `*/${executableGlob}`, `*/${binName}/${executableGlob}`];
23+
executableBaseGlob: string = executable,
24+
): IDisposable {
25+
if (executableBaseGlob.includes(path.sep)) {
26+
throw new Error('Glob basename contains invalid characters');
27+
}
28+
const patterns = [executableBaseGlob, `*/${executableBaseGlob}`, `*/${binName}/${executableBaseGlob}`];
29+
const disposables = new DisposableRegistry();
1930
for (const pattern of patterns) {
20-
watchLocationForPattern(baseDir, pattern, (type: FileChangeType, e: string) => {
21-
const isMatch = minimatch(e, path.join('**', executableGlob), { nocase: getOSType() === OSType.Windows });
31+
disposables.push(watchLocationForPattern(baseDir, pattern, (type: FileChangeType, e: string) => {
32+
const isMatch = minimatch(e, path.join('**', executableBaseGlob), { nocase: getOSType() === OSType.Windows });
2233
if (!isMatch) {
2334
// When deleting the file for some reason path to all directories leading up to python are reported
2435
// Skip those events
2536
return;
2637
}
2738
callback(type, e);
28-
});
39+
}));
2940
}
41+
return disposables;
3042
}

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

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@
44
import { uniq } from 'lodash';
55
import * as path from 'path';
66
import { traceVerbose } from '../../../../common/logger';
7-
import { FileChangeType } from '../../../../common/platform/fileSystemWatcher';
8-
import { chain, iterable, sleep } from '../../../../common/utils/async';
7+
import { chain, iterable } from '../../../../common/utils/async';
98
import {
109
getEnvironmentVariable, getOSType, getUserHomeDir, OSType
1110
} from '../../../../common/utils/platform';
1211
import { PythonEnvInfo, PythonEnvKind, UNKNOWN_PYTHON_VERSION } from '../../../base/info';
1312
import { buildEnvInfo } from '../../../base/info/env';
14-
import { IPythonEnvsIterator, Locator } from '../../../base/locator';
13+
import { IPythonEnvsIterator } from '../../../base/locator';
14+
import { FSWatchingLocator } from '../../../base/locators/lowLevel/fsWatchingLocator';
1515
import { findInterpretersInDir } from '../../../common/commonUtils';
1616
import { getFileInfo, pathExists } from '../../../common/externalDependencies';
17-
import { watchLocationForPythonBinaries } from '../../../common/pythonBinariesWatcher';
1817
import { isPipenvEnvironment } from './pipEnvHelper';
1918
import {
2019
isVenvEnvironment,
@@ -80,17 +79,22 @@ async function getVirtualEnvKind(interpreterPath: string): Promise<PythonEnvKind
8079
/**
8180
* Finds and resolves virtual environments created in known global locations.
8281
*/
83-
export class GlobalVirtualEnvironmentLocator extends Locator {
82+
export class GlobalVirtualEnvironmentLocator extends FSWatchingLocator {
8483
private virtualEnvKinds = [
8584
PythonEnvKind.Venv,
8685
PythonEnvKind.VirtualEnv,
8786
PythonEnvKind.VirtualEnvWrapper,
8887
PythonEnvKind.Pipenv,
8988
];
9089

91-
public constructor(private readonly searchDepth?: number) {
92-
super();
93-
this.registerWatchers().ignoreErrors();
90+
constructor(private readonly searchDepth?: number) {
91+
super(getGlobalVirtualEnvDirs, getVirtualEnvKind, {
92+
// Note detecting kind of virtual env depends on the file structure around the
93+
// executable, so we need to wait before attempting to detect it. However even
94+
// if the type detected is incorrect, it doesn't do any practical harm as kinds
95+
// in this locator are used in the same way (same activation commands etc.)
96+
delayOnCreated: 1000,
97+
});
9498
}
9599

96100
public iterEnvs(): IPythonEnvsIterator {
@@ -167,16 +171,4 @@ export class GlobalVirtualEnvironmentLocator extends Locator {
167171
}
168172
return undefined;
169173
}
170-
171-
private async registerWatchers(): Promise<void> {
172-
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-
}));
181-
}
182174
}

0 commit comments

Comments
 (0)