watcher - support suspend/resume for non-correlated requests (#228703)

This commit is contained in:
Benjamin Pasero
2024-09-16 12:24:34 +02:00
committed by GitHub
parent 3645d2dea1
commit 88b706b2bb
12 changed files with 134 additions and 177 deletions

View File

@@ -9,7 +9,6 @@
"aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
"enabledApiProposals": [
"workspaceTrust",
"createFileSystemWatcher",
"multiDocumentHighlightProvider",
"mappedEditsProvider",
"codeActionAI",

View File

@@ -117,7 +117,7 @@ export interface TypeScriptServiceConfiguration {
readonly enableProjectDiagnostics: boolean;
readonly maxTsServerMemory: number;
readonly enablePromptUseWorkspaceTsdk: boolean;
readonly useVsCodeWatcher: boolean; // TODO@bpasero remove this setting eventually
readonly useVsCodeWatcher: boolean;
readonly watchOptions: Proto.WatchOptions | undefined;
readonly includePackageJsonAutoImports: 'auto' | 'on' | 'off' | undefined;
readonly enableTsServerTracing: boolean;
@@ -223,7 +223,12 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu
}
private readUseVsCodeWatcher(configuration: vscode.WorkspaceConfiguration): boolean {
return configuration.get<boolean>('typescript.tsserver.experimental.useVsCodeWatcher', false);
const watcherExcludes = configuration.get<Record<string, boolean>>('files.watcherExclude') ?? {};
if (watcherExcludes['**/node_modules/*/**'] /* VS Code default prior to 1.94.x */ === true) {
return false; // we cannot use the VS Code watcher if node_modules are excluded
}
return configuration.get<boolean>('typescript.tsserver.experimental.useVsCodeWatcher', true);
}
private readWatchOptions(configuration: vscode.WorkspaceConfiguration): Proto.WatchOptions | undefined {

View File

@@ -1152,7 +1152,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
ignoreChangeEvents?: boolean,
) {
const disposable = new DisposableStore();
const watcher = disposable.add(vscode.workspace.createFileSystemWatcher(pattern, { excludes: [] /* TODO:: need to fill in excludes list */, ignoreChangeEvents }));
const watcher = disposable.add(vscode.workspace.createFileSystemWatcher(pattern, undefined, ignoreChangeEvents));
disposable.add(watcher.onDidChange(changeFile =>
this.addWatchEvent(id, 'updated', changeFile.fsPath)
));

View File

@@ -11,7 +11,6 @@
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts",
"../../src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts",
"../../src/vscode-dts/vscode.proposed.codeActionAI.d.ts",
"../../src/vscode-dts/vscode.proposed.codeActionRanges.d.ts",
"../../src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts",

1
package-lock.json generated
View File

@@ -945,6 +945,7 @@
"version": "2.4.2-alpha.0",
"resolved": "git+ssh://git@github.com/bpasero/watcher.git#3e5e50c275590703f3eb46fac777b720e515d0d5",
"integrity": "sha512-kfF+SmdrcDHkwLdnGtK0EknGv6uPhF5tBc04dJ0xkLNMcIocZAINg1+p2ZTfqwEfCbxp+djHWw37f400fKtY7g==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^1.0.3",

View File

@@ -49,6 +49,7 @@
"version": "2.4.2-alpha.0",
"resolved": "git+ssh://git@github.com/bpasero/watcher.git#3e5e50c275590703f3eb46fac777b720e515d0d5",
"integrity": "sha512-kfF+SmdrcDHkwLdnGtK0EknGv6uPhF5tBc04dJ0xkLNMcIocZAINg1+p2ZTfqwEfCbxp+djHWw37f400fKtY7g==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^1.0.3",

View File

@@ -10,6 +10,13 @@ import { Emitter, Event } from '../../../../base/common/event.js';
import { FileChangeType, IFileChange } from '../../common/files.js';
import { URI } from '../../../../base/common/uri.js';
import { DeferredPromise, ThrottledDelayer } from '../../../../base/common/async.js';
import { hash } from '../../../../base/common/hash.js';
interface ISuspendedWatchRequest {
readonly id: number;
readonly correlationId: number | undefined;
readonly path: string;
}
export abstract class BaseWatcher extends Disposable implements IWatcher {
@@ -22,11 +29,11 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
protected readonly _onDidWatchFail = this._register(new Emitter<IUniversalWatchRequest>());
private readonly onDidWatchFail = this._onDidWatchFail.event;
private readonly allNonCorrelatedWatchRequests = new Set<IUniversalWatchRequest>();
private readonly allCorrelatedWatchRequests = new Map<number /* correlation ID */, IWatchRequestWithCorrelation>();
private readonly correlatedWatchRequests = new Map<number /* request ID */, IWatchRequestWithCorrelation>();
private readonly nonCorrelatedWatchRequests = new Map<number /* request ID */, IUniversalWatchRequest>();
private readonly suspendedWatchRequests = this._register(new DisposableMap<number /* correlation ID */>());
private readonly suspendedWatchRequestsWithPolling = new Set<number /* correlation ID */>();
private readonly suspendedWatchRequests = this._register(new DisposableMap<number /* request ID */>());
private readonly suspendedWatchRequestsWithPolling = new Set<number /* request ID */>();
private readonly updateWatchersDelayer = this._register(new ThrottledDelayer<void>(this.getUpdateWatchersDelay()));
@@ -37,32 +44,28 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
constructor() {
super();
this._register(this.onDidWatchFail(request => this.handleDidWatchFail(request)));
}
private handleDidWatchFail(request: IUniversalWatchRequest): void {
if (!this.isCorrelated(request)) {
// For now, limit failed watch monitoring to requests with a correlationId
// to experiment with this feature in a controlled way. Monitoring requests
// requires us to install polling watchers (via `fs.watchFile()`) and thus
// should be used sparingly.
//
// TODO@bpasero revisit this in the future to have a more general approach
// for suspend/resume and drop the `legacyMonitorRequest` in parcel.
// One issue is that we need to be able to uniquely identify a request and
// without correlation that is actually harder...
return;
}
this.suspendWatchRequest(request);
this._register(this.onDidWatchFail(request => this.suspendWatchRequest({
id: this.computeId(request),
correlationId: this.isCorrelated(request) ? request.correlationId : undefined,
path: request.path
})));
}
protected isCorrelated(request: IUniversalWatchRequest): request is IWatchRequestWithCorrelation {
return isWatchRequestWithCorrelation(request);
}
private computeId(request: IUniversalWatchRequest): number {
if (this.isCorrelated(request)) {
return request.correlationId;
} else {
// Requests without correlation do not carry any unique identifier, so we have to
// come up with one based on the options of the request. This matches what the
// file service does (vs/platform/files/common/fileService.ts#L1178).
return hash(request);
}
}
async watch(requests: IUniversalWatchRequest[]): Promise<void> {
if (!this.joinWatch.isSettled) {
this.joinWatch.complete();
@@ -70,23 +73,23 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
this.joinWatch = new DeferredPromise<void>();
try {
this.allCorrelatedWatchRequests.clear();
this.allNonCorrelatedWatchRequests.clear();
this.correlatedWatchRequests.clear();
this.nonCorrelatedWatchRequests.clear();
// Figure out correlated vs. non-correlated requests
for (const request of requests) {
if (this.isCorrelated(request)) {
this.allCorrelatedWatchRequests.set(request.correlationId, request);
this.correlatedWatchRequests.set(request.correlationId, request);
} else {
this.allNonCorrelatedWatchRequests.add(request);
this.nonCorrelatedWatchRequests.set(this.computeId(request), request);
}
}
// Remove all suspended correlated watch requests that are no longer watched
for (const [correlationId] of this.suspendedWatchRequests) {
if (!this.allCorrelatedWatchRequests.has(correlationId)) {
this.suspendedWatchRequests.deleteAndDispose(correlationId);
this.suspendedWatchRequestsWithPolling.delete(correlationId);
// Remove all suspended watch requests that are no longer watched
for (const [id] of this.suspendedWatchRequests) {
if (!this.nonCorrelatedWatchRequests.has(id) && !this.correlatedWatchRequests.has(id)) {
this.suspendedWatchRequests.deleteAndDispose(id);
this.suspendedWatchRequestsWithPolling.delete(id);
}
}
@@ -97,10 +100,14 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
}
private updateWatchers(delayed: boolean): Promise<void> {
return this.updateWatchersDelayer.trigger(() => this.doWatch([
...this.allNonCorrelatedWatchRequests,
...Array.from(this.allCorrelatedWatchRequests.values()).filter(request => !this.suspendedWatchRequests.has(request.correlationId))
]), delayed ? this.getUpdateWatchersDelay() : 0);
const nonSuspendedRequests: IUniversalWatchRequest[] = [];
for (const [id, request] of [...this.nonCorrelatedWatchRequests, ...this.correlatedWatchRequests]) {
if (!this.suspendedWatchRequests.has(id)) {
nonSuspendedRequests.push(request);
}
}
return this.updateWatchersDelayer.trigger(() => this.doWatch(nonSuspendedRequests), delayed ? this.getUpdateWatchersDelay() : 0);
}
protected getUpdateWatchersDelay(): number {
@@ -108,20 +115,17 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
}
isSuspended(request: IUniversalWatchRequest): 'polling' | boolean {
if (typeof request.correlationId !== 'number') {
return false;
}
return this.suspendedWatchRequestsWithPolling.has(request.correlationId) ? 'polling' : this.suspendedWatchRequests.has(request.correlationId);
const id = this.computeId(request);
return this.suspendedWatchRequestsWithPolling.has(id) ? 'polling' : this.suspendedWatchRequests.has(id);
}
private async suspendWatchRequest(request: IWatchRequestWithCorrelation): Promise<void> {
if (this.suspendedWatchRequests.has(request.correlationId)) {
private async suspendWatchRequest(request: ISuspendedWatchRequest): Promise<void> {
if (this.suspendedWatchRequests.has(request.id)) {
return; // already suspended
}
const disposables = new DisposableStore();
this.suspendedWatchRequests.set(request.correlationId, disposables);
this.suspendedWatchRequests.set(request.id, disposables);
// It is possible that a watch request fails right during watch()
// phase while other requests succeed. To increase the chance of
@@ -139,24 +143,24 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
this.updateWatchers(true /* delay this call as we might accumulate many failing watch requests on startup */);
}
private resumeWatchRequest(request: IWatchRequestWithCorrelation): void {
this.suspendedWatchRequests.deleteAndDispose(request.correlationId);
this.suspendedWatchRequestsWithPolling.delete(request.correlationId);
private resumeWatchRequest(request: ISuspendedWatchRequest): void {
this.suspendedWatchRequests.deleteAndDispose(request.id);
this.suspendedWatchRequestsWithPolling.delete(request.id);
this.updateWatchers(false);
}
private monitorSuspendedWatchRequest(request: IWatchRequestWithCorrelation, disposables: DisposableStore): void {
private monitorSuspendedWatchRequest(request: ISuspendedWatchRequest, disposables: DisposableStore): void {
if (this.doMonitorWithExistingWatcher(request, disposables)) {
this.trace(`reusing an existing recursive watcher to monitor ${request.path}`);
this.suspendedWatchRequestsWithPolling.delete(request.correlationId);
this.suspendedWatchRequestsWithPolling.delete(request.id);
} else {
this.doMonitorWithNodeJS(request, disposables);
this.suspendedWatchRequestsWithPolling.add(request.correlationId);
this.suspendedWatchRequestsWithPolling.add(request.id);
}
}
private doMonitorWithExistingWatcher(request: IWatchRequestWithCorrelation, disposables: DisposableStore): boolean {
private doMonitorWithExistingWatcher(request: ISuspendedWatchRequest, disposables: DisposableStore): boolean {
const subscription = this.recursiveWatcher?.subscribe(request.path, (error, change) => {
if (disposables.isDisposed) {
return; // return early if already disposed
@@ -178,7 +182,7 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
return false;
}
private doMonitorWithNodeJS(request: IWatchRequestWithCorrelation, disposables: DisposableStore): void {
private doMonitorWithNodeJS(request: ISuspendedWatchRequest, disposables: DisposableStore): void {
let pathNotFound = false;
const watchFileCallback: (curr: Stats, prev: Stats) => void = (curr, prev) => {
@@ -215,7 +219,7 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
}));
}
private onMonitoredPathAdded(request: IWatchRequestWithCorrelation) {
private onMonitoredPathAdded(request: ISuspendedWatchRequest): void {
this.trace(`detected ${request.path} exists again, resuming watcher (correlationId: ${request.correlationId})`);
// Emit as event
@@ -236,14 +240,14 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
this.suspendedWatchRequestsWithPolling.clear();
}
protected traceEvent(event: IFileChange, request: IUniversalWatchRequest): void {
protected traceEvent(event: IFileChange, request: IUniversalWatchRequest | ISuspendedWatchRequest): void {
if (this.verboseLogging) {
const traceMsg = ` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`;
this.traceWithCorrelation(traceMsg, request);
}
}
protected traceWithCorrelation(message: string, request: IUniversalWatchRequest): void {
protected traceWithCorrelation(message: string, request: IUniversalWatchRequest | ISuspendedWatchRequest): void {
if (this.verboseLogging) {
this.trace(`${message}${typeof request.correlationId === 'number' ? ` <${request.correlationId}> ` : ``}`);
}

View File

@@ -51,7 +51,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
private readonly excludes = parseWatcherPatterns(this.request.path, this.request.excludes);
private readonly includes = this.request.includes ? parseWatcherPatterns(this.request.path, this.request.includes) : undefined;
private readonly filter = isWatchRequestWithCorrelation(this.request) ? this.request.filter : undefined; // TODO@bpasero filtering for now is only enabled when correlating because watchers are otherwise potentially reused
private readonly filter = isWatchRequestWithCorrelation(this.request) ? this.request.filter : undefined; // filtering is only enabled when correlating because watchers are otherwise potentially reused
private readonly cts = new CancellationTokenSource();

View File

@@ -5,7 +5,7 @@
import * as parcelWatcher from '@parcel/watcher';
import * as parcelWatcher2 from '@bpasero/watcher';
import { existsSync, statSync, unlinkSync } from 'fs';
import { statSync, unlinkSync } from 'fs';
import { tmpdir, homedir } from 'os';
import { URI } from '../../../../../base/common/uri.js';
import { DeferredPromise, RunOnceScheduler, RunOnceWorker, ThrottledWorker } from '../../../../../base/common/async.js';
@@ -17,10 +17,9 @@ import { GLOBSTAR, patternsEquals } from '../../../../../base/common/glob.js';
import { BaseWatcher } from '../baseWatcher.js';
import { TernarySearchTree } from '../../../../../base/common/ternarySearchTree.js';
import { normalizeNFC } from '../../../../../base/common/normalization.js';
import { dirname, normalize, join } from '../../../../../base/common/path.js';
import { normalize, join } from '../../../../../base/common/path.js';
import { isLinux, isMacintosh, isWindows } from '../../../../../base/common/platform.js';
import { realcaseSync, realpathSync } from '../../../../../base/node/extpath.js';
import { NodeJSFileWatcherLibrary } from '../nodejs/nodejsWatcherLib.js';
import { FileChangeType, IFileChange } from '../../../common/files.js';
import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, IWatcherErrorEvent } from '../../../common/watcher.js';
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
@@ -542,7 +541,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS
const filteredEvents: IFileChange[] = [];
let rootDeleted = false;
const filter = this.isCorrelated(watcher.request) ? watcher.request.filter : undefined; // TODO@bpasero filtering for now is only enabled when correlating because watchers are otherwise potentially reused
const filter = this.isCorrelated(watcher.request) ? watcher.request.filter : undefined; // filtering is only enabled when correlating because watchers are otherwise potentially reused
for (const event of events) {
// Emit to instance subscriptions if any before filtering
@@ -552,20 +551,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS
// Filtering
rootDeleted = event.type === FileChangeType.DELETED && isEqual(event.resource.fsPath, watcher.request.path, !isLinux);
if (
isFiltered(event, filter) ||
// Explicitly exclude changes to root if we have any
// to avoid VS Code closing all opened editors which
// can happen e.g. in case of network connectivity
// issues
// (https://github.com/microsoft/vscode/issues/136673)
//
// Update 2024: with the new correlated events, we
// really do not want to skip over file events any
// more, so we only ignore this event for non-correlated
// watch requests.
(rootDeleted && !this.isCorrelated(watcher.request))
) {
if (isFiltered(event, filter)) {
if (this.verboseLogging) {
this.traceWithCorrelation(` >> ignored (filtered) ${event.resource.fsPath}`, watcher.request);
}
@@ -585,54 +571,8 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS
private onWatchedPathDeleted(watcher: ParcelWatcherInstance): void {
this.warn('Watcher shutdown because watched path got deleted', watcher);
let legacyMonitored = false;
if (!this.isCorrelated(watcher.request)) {
// Do monitoring of the request path parent unless this request
// can be handled via suspend/resume in the super class
legacyMonitored = this.legacyMonitorRequest(watcher);
}
if (!legacyMonitored) {
watcher.notifyWatchFailed();
this._onDidWatchFail.fire(watcher.request);
}
}
private legacyMonitorRequest(watcher: ParcelWatcherInstance): boolean {
const parentPath = dirname(watcher.request.path);
if (existsSync(parentPath)) {
this.trace('Trying to watch on the parent path to restart the watcher...', watcher);
const nodeWatcher = new NodeJSFileWatcherLibrary({ path: parentPath, excludes: [], recursive: false, correlationId: watcher.request.correlationId }, undefined, changes => {
if (watcher.token.isCancellationRequested) {
return; // return early when disposed
}
// Watcher path came back! Restart watching...
for (const { resource, type } of changes) {
if (isEqual(resource.fsPath, watcher.request.path, !isLinux) && (type === FileChangeType.ADDED || type === FileChangeType.UPDATED)) {
if (this.isPathValid(watcher.request.path)) {
this.warn('Watcher restarts because watched path got created again', watcher);
// Stop watching that parent folder
nodeWatcher.dispose();
// Restart the file watching
this.restartWatching(watcher);
break;
}
}
}
}, undefined, msg => this._onDidLogMessage.fire(msg), this.verboseLogging);
// Make sure to stop watching when the watcher is disposed
watcher.token.onCancellationRequested(() => nodeWatcher.dispose());
return true;
}
return false;
watcher.notifyWatchFailed();
this._onDidWatchFail.fire(watcher.request);
}
private onUnexpectedError(error: unknown, request?: IRecursiveWatchRequest): void {

View File

@@ -23,14 +23,18 @@ import { extUriBiasedIgnorePathCase } from '../../../../base/common/resources.js
import { URI } from '../../../../base/common/uri.js';
import { addUNCHostToAllowlist } from '../../../../base/node/unc.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { TestParcelWatcher } from './parcelWatcher.integrationTest.js';
import { TestParcelWatcher } from './parcelWatcher.test.js';
// this suite has shown flaky runs in Azure pipelines where
// tasks would just hang and timeout after a while (not in
// mocha but generally). as such they will run only on demand
// whenever we update the watcher library.
suite.skip('File Watcher (node.js)', () => {
/* eslint-disable local/code-ensure-no-disposables-leak-in-test */
suite.skip('File Watcher (node.js)', function () {
this.timeout(10000);
class TestNodeJSWatcher extends NodeJSWatcher {
@@ -65,7 +69,7 @@ suite.skip('File Watcher (node.js)', () => {
watcher?.setVerboseLogging(enable);
}
enableLogging(false);
enableLogging(loggingEnabled);
setup(async () => {
await createWatcher(undefined);
@@ -602,42 +606,42 @@ suite.skip('File Watcher (node.js)', () => {
await changeFuture;
});
test('correlated watch requests support suspend/resume (file, does not exist in beginning)', async function () {
test('watch requests support suspend/resume (file, does not exist in beginning)', async function () {
const filePath = join(testDir, 'not-found.txt');
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
const request = { path: filePath, excludes: [], recursive: false, correlationId: 1 };
const request = { path: filePath, excludes: [], recursive: false };
await watcher.watch([request]);
await onDidWatchFail;
assert.strictEqual(watcher.isSuspended(request), 'polling');
await basicCrudTest(filePath, undefined, 1, undefined, true);
await basicCrudTest(filePath, undefined, 1, undefined, true);
await basicCrudTest(filePath, undefined, null, undefined, true);
await basicCrudTest(filePath, undefined, null, undefined, true);
});
test('correlated watch requests support suspend/resume (file, exists in beginning)', async function () {
test('watch requests support suspend/resume (file, exists in beginning)', async function () {
const filePath = join(testDir, 'lorem.txt');
const request = { path: filePath, excludes: [], recursive: false, correlationId: 1 };
const request = { path: filePath, excludes: [], recursive: false };
await watcher.watch([request]);
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
await basicCrudTest(filePath, true, 1);
await basicCrudTest(filePath, true);
await onDidWatchFail;
assert.strictEqual(watcher.isSuspended(request), 'polling');
await basicCrudTest(filePath, undefined, 1, undefined, true);
await basicCrudTest(filePath, undefined, null, undefined, true);
});
test('correlated watch requests support suspend/resume (folder, does not exist in beginning)', async function () {
test('watch requests support suspend/resume (folder, does not exist in beginning)', async function () {
let onDidWatchFail = Event.toPromise(watcher.onWatchFail);
const folderPath = join(testDir, 'not-found');
const request = { path: folderPath, excludes: [], recursive: false, correlationId: 1 };
const request = { path: folderPath, excludes: [], recursive: false };
await watcher.watch([request]);
await onDidWatchFail;
assert.strictEqual(watcher.isSuspended(request), 'polling');
let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1);
let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED);
let onDidWatch = Event.toPromise(watcher.onDidWatch);
await fs.promises.mkdir(folderPath);
await changeFuture;
@@ -645,15 +649,15 @@ suite.skip('File Watcher (node.js)', () => {
assert.strictEqual(watcher.isSuspended(request), false);
const filePath = join(folderPath, 'newFile.txt');
await basicCrudTest(filePath, undefined, 1);
if (isWindows) { // somehow failing on macOS/Linux
const filePath = join(folderPath, 'newFile.txt');
await basicCrudTest(filePath);
if (!isMacintosh) { // macOS does not report DELETE events for folders
onDidWatchFail = Event.toPromise(watcher.onWatchFail);
await fs.promises.rmdir(folderPath);
await onDidWatchFail;
changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1);
changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED);
onDidWatch = Event.toPromise(watcher.onDidWatch);
await fs.promises.mkdir(folderPath);
await changeFuture;
@@ -661,22 +665,22 @@ suite.skip('File Watcher (node.js)', () => {
await timeout(500); // somehow needed on Linux
await basicCrudTest(filePath, undefined, 1);
await basicCrudTest(filePath);
}
});
(isMacintosh /* macOS: does not seem to report this */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, exists in beginning)', async function () {
(isMacintosh /* macOS: does not seem to report this */ ? test.skip : test)('watch requests support suspend/resume (folder, exists in beginning)', async function () {
const folderPath = join(testDir, 'deep');
await watcher.watch([{ path: folderPath, excludes: [], recursive: false, correlationId: 1 }]);
await watcher.watch([{ path: folderPath, excludes: [], recursive: false }]);
const filePath = join(folderPath, 'newFile.txt');
await basicCrudTest(filePath, undefined, 1);
await basicCrudTest(filePath);
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
await Promises.rm(folderPath);
await onDidWatchFail;
const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1);
const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED);
const onDidWatch = Event.toPromise(watcher.onDidWatch);
await fs.promises.mkdir(folderPath);
await changeFuture;
@@ -684,7 +688,7 @@ suite.skip('File Watcher (node.js)', () => {
await timeout(500); // somehow needed on Linux
await basicCrudTest(filePath, undefined, 1);
await basicCrudTest(filePath);
});
test('parcel watcher reused when present for non-recursive file watching (uncorrelated)', function () {
@@ -745,7 +749,7 @@ suite.skip('File Watcher (node.js)', () => {
assert.strictEqual(instance.isReusingRecursiveWatcher, false);
}
test('correlated watch requests support suspend/resume (file, does not exist in beginning, parcel watcher reused)', async function () {
test('watch requests support suspend/resume (file, does not exist in beginning, parcel watcher reused)', async function () {
const recursiveWatcher = createParcelWatcher();
await recursiveWatcher.watch([{ path: testDir, excludes: [], recursive: true }]);
@@ -754,12 +758,12 @@ suite.skip('File Watcher (node.js)', () => {
const filePath = join(testDir, 'not-found-2.txt');
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
const request = { path: filePath, excludes: [], recursive: false, correlationId: 1 };
const request = { path: filePath, excludes: [], recursive: false };
await watcher.watch([request]);
await onDidWatchFail;
assert.strictEqual(watcher.isSuspended(request), true);
const changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, 1);
const changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED);
await Promises.writeFile(filePath, 'Hello World');
await changeFuture;

View File

@@ -65,7 +65,11 @@ export class TestParcelWatcher extends ParcelWatcher {
// mocha but generally). as such they will run only on demand
// whenever we update the watcher library.
suite.skip('File Watcher (parcel)', () => {
/* eslint-disable local/code-ensure-no-disposables-leak-in-test */
suite.skip('File Watcher (parcel)', function () {
this.timeout(10000);
let testDir: string;
let watcher: TestParcelWatcher;
@@ -77,7 +81,7 @@ suite.skip('File Watcher (parcel)', () => {
watcher?.setVerboseLogging(enable);
}
enableLogging(false);
enableLogging(loggingEnabled);
setup(async () => {
watcher = new TestParcelWatcher();
@@ -743,15 +747,15 @@ suite.skip('File Watcher (parcel)', () => {
assert.strictEqual(instance.failed, true);
});
test('correlated watch requests support suspend/resume (folder, does not exist in beginning, not reusing watcher)', async () => {
await testCorrelatedWatchFolderDoesNotExist(false);
test('watch requests support suspend/resume (folder, does not exist in beginning, not reusing watcher)', async () => {
await testWatchFolderDoesNotExist(false);
});
(!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => {
await testCorrelatedWatchFolderDoesNotExist(true);
(!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => {
await testWatchFolderDoesNotExist(true);
});
async function testCorrelatedWatchFolderDoesNotExist(reuseExistingWatcher: boolean) {
async function testWatchFolderDoesNotExist(reuseExistingWatcher: boolean) {
let onDidWatchFail = Event.toPromise(watcher.onWatchFail);
const folderPath = join(testDir, 'not-found');
@@ -762,7 +766,7 @@ suite.skip('File Watcher (parcel)', () => {
await watcher.watch(requests);
}
const request: IRecursiveWatchRequest = { path: folderPath, excludes: [], recursive: true, correlationId: 1 };
const request: IRecursiveWatchRequest = { path: folderPath, excludes: [], recursive: true };
requests.push(request);
await watcher.watch(requests);
@@ -774,7 +778,7 @@ suite.skip('File Watcher (parcel)', () => {
assert.strictEqual(watcher.isSuspended(request), 'polling');
}
let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1);
let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED);
let onDidWatch = Event.toPromise(watcher.onDidWatch);
await promises.mkdir(folderPath);
await changeFuture;
@@ -783,33 +787,33 @@ suite.skip('File Watcher (parcel)', () => {
assert.strictEqual(watcher.isSuspended(request), false);
const filePath = join(folderPath, 'newFile.txt');
await basicCrudTest(filePath, 1);
await basicCrudTest(filePath);
onDidWatchFail = Event.toPromise(watcher.onWatchFail);
await Promises.rm(folderPath);
await onDidWatchFail;
changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1);
changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED);
onDidWatch = Event.toPromise(watcher.onDidWatch);
await promises.mkdir(folderPath);
await changeFuture;
await onDidWatch;
await basicCrudTest(filePath, 1);
await basicCrudTest(filePath);
}
test('correlated watch requests support suspend/resume (folder, exist in beginning, not reusing watcher)', async () => {
await testCorrelatedWatchFolderExists(false);
test('watch requests support suspend/resume (folder, exist in beginning, not reusing watcher)', async () => {
await testWatchFolderExists(false);
});
(!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => {
await testCorrelatedWatchFolderExists(true);
(!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => {
await testWatchFolderExists(true);
});
async function testCorrelatedWatchFolderExists(reuseExistingWatcher: boolean) {
async function testWatchFolderExists(reuseExistingWatcher: boolean) {
const folderPath = join(testDir, 'deep');
const requests: IRecursiveWatchRequest[] = [{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }];
const requests: IRecursiveWatchRequest[] = [{ path: folderPath, excludes: [], recursive: true }];
if (reuseExistingWatcher) {
requests.push({ path: testDir, excludes: [], recursive: true });
}
@@ -817,19 +821,19 @@ suite.skip('File Watcher (parcel)', () => {
await watcher.watch(requests);
const filePath = join(folderPath, 'newFile.txt');
await basicCrudTest(filePath, 1);
await basicCrudTest(filePath);
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
await Promises.rm(folderPath);
await onDidWatchFail;
const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1);
const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED);
const onDidWatch = Event.toPromise(watcher.onDidWatch);
await promises.mkdir(folderPath);
await changeFuture;
await onDidWatch;
await basicCrudTest(filePath, 1);
await basicCrudTest(filePath);
}
test('watch request reuses another recursive watcher even when requests are coming in at the same time', async function () {

View File

@@ -295,7 +295,7 @@ configurationRegistry.registerConfiguration({
'patternProperties': {
'.*': { 'type': 'boolean' }
},
'default': { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true },
'default': { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/.hg/store/**': true },
'markdownDescription': nls.localize('watcherExclude', "Configure paths or [glob patterns](https://aka.ms/vscode-glob-patterns) to exclude from file watching. Paths can either be relative to the watched folder or absolute. Glob patterns are matched relative from the watched folder. When you experience the file watcher process consuming a lot of CPU, make sure to exclude large folders that are of less interest (such as build output folders)."),
'scope': ConfigurationScope.RESOURCE
},