watcher - correlate events to their requesting source (#194776)

* watcher - emit `URI` instead of `string` for faster `fsPath` compute  (for #194341)

* wip

* more

* adopt

* some cleanup

* cleanup

* cleanup

* cleanup

* cleanup

* cleanup

* cleanup

* implement correlation

* cleanup

* add correlation

* undo, leave for later

* tests

* tests

* tests

* tests

* tests

* log cId

* simpler correlation id

* 💄

* tests

* runs

* skip normalization

* fix tests

* tests

* fix tests

* add `createWatcher` API

* partition events in ext host

* allow custom excludes

* remove disk file change

* 💄

* 💄

* 💄

* wire in

* wire in
This commit is contained in:
Benjamin Pasero 2023-10-10 10:27:18 +02:00 committed by GitHub
parent 8988b0fd43
commit 29b69437ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 887 additions and 397 deletions

View file

@ -8,6 +8,7 @@
"authSession", "authSession",
"contribViewsRemote", "contribViewsRemote",
"contribStatusBarItems", "contribStatusBarItems",
"createFileSystemWatcher",
"customEditorMove", "customEditorMove",
"diffCommand", "diffCommand",
"documentFiltersExclusive", "documentFiltersExclusive",

View file

@ -27,19 +27,28 @@ suite('vscode API - workspace-watcher', () => {
} }
} }
teardown(assertNoRpc); let fs: WatcherTestFs;
let disposable: vscode.Disposable;
test('createFileSystemWatcher', async function () { function onDidWatchPromise() {
const fs = new WatcherTestFs('watcherTest', false); const onDidWatchPromise = new Promise<IWatchRequest>(resolve => {
vscode.workspace.registerFileSystemProvider('watcherTest', fs); fs.onDidWatch(request => resolve(request));
});
function onDidWatchPromise() { return onDidWatchPromise;
const onDidWatchPromise = new Promise<IWatchRequest>(resolve => { }
fs.onDidWatch(request => resolve(request));
});
return onDidWatchPromise; setup(() => {
} fs = new WatcherTestFs('watcherTest', false);
disposable = vscode.workspace.registerFileSystemProvider('watcherTest', fs);
});
teardown(() => {
disposable.dispose();
assertNoRpc();
});
test('createFileSystemWatcher (old style)', async function () {
// Non-recursive // Non-recursive
let watchUri = vscode.Uri.from({ scheme: 'watcherTest', path: '/somePath/folder' }); let watchUri = vscode.Uri.from({ scheme: 'watcherTest', path: '/somePath/folder' });
@ -59,4 +68,29 @@ suite('vscode API - workspace-watcher', () => {
assert.strictEqual(request.uri.toString(), watchUri.toString()); assert.strictEqual(request.uri.toString(), watchUri.toString());
assert.strictEqual(request.options.recursive, true); assert.strictEqual(request.options.recursive, true);
}); });
test('createFileSystemWatcher (new style)', async function () {
// Non-recursive
let watchUri = vscode.Uri.from({ scheme: 'watcherTest', path: '/somePath/folder' });
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(watchUri, '*.txt'), { excludes: ['testing'], ignoreChangeEvents: true });
let request = await onDidWatchPromise();
assert.strictEqual(request.uri.toString(), watchUri.toString());
assert.strictEqual(request.options.recursive, false);
assert.strictEqual(request.options.excludes.length, 1);
assert.strictEqual(request.options.excludes[0], 'testing');
watcher.dispose();
// Recursive
watchUri = vscode.Uri.from({ scheme: 'watcherTest', path: '/somePath/folder' });
vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(watchUri, '**/*.txt'), { excludes: ['testing'], ignoreCreateEvents: true });
request = await onDidWatchPromise();
assert.strictEqual(request.uri.toString(), watchUri.toString());
assert.strictEqual(request.options.recursive, true);
assert.strictEqual(request.options.excludes.length, 1);
assert.strictEqual(request.options.excludes[0], 'testing');
});
}); });

View file

@ -11,7 +11,7 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'
import { normalize } from 'vs/base/common/path'; import { normalize } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { IFileChange, IFileSystemProvider, IWatchOptions } from 'vs/platform/files/common/files'; import { IFileChange, IFileSystemProvider, IWatchOptions } from 'vs/platform/files/common/files';
import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, IDiskFileChange, ILogMessage, INonRecursiveWatchRequest, IRecursiveWatcherOptions, isRecursiveWatchRequest, IUniversalWatchRequest, toFileChanges } from 'vs/platform/files/common/watcher'; import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILogMessage, INonRecursiveWatchRequest, IRecursiveWatcherOptions, isRecursiveWatchRequest, IUniversalWatchRequest, reviveFileChanges } from 'vs/platform/files/common/watcher';
import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { ILogService, LogLevel } from 'vs/platform/log/common/log';
export interface IDiskFileSystemProviderOptions { export interface IDiskFileSystemProviderOptions {
@ -72,7 +72,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
private watchUniversal(resource: URI, opts: IWatchOptions): IDisposable { private watchUniversal(resource: URI, opts: IWatchOptions): IDisposable {
// Add to list of paths to watch universally // Add to list of paths to watch universally
const pathToWatch: IUniversalWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, includes: opts.includes, recursive: opts.recursive }; const pathToWatch: IUniversalWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, includes: opts.includes, recursive: opts.recursive, correlationId: opts.correlationId };
const remove = insert(this.universalPathsToWatch, pathToWatch); const remove = insert(this.universalPathsToWatch, pathToWatch);
// Trigger update // Trigger update
@ -102,7 +102,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
// Create watcher if this is the first time // Create watcher if this is the first time
if (!this.universalWatcher) { if (!this.universalWatcher) {
this.universalWatcher = this._register(this.createUniversalWatcher( this.universalWatcher = this._register(this.createUniversalWatcher(
changes => this._onDidChangeFile.fire(toFileChanges(changes)), changes => this._onDidChangeFile.fire(reviveFileChanges(changes)),
msg => this.onWatcherLogMessage(msg), msg => this.onWatcherLogMessage(msg),
this.logService.getLevel() === LogLevel.Trace this.logService.getLevel() === LogLevel.Trace
)); ));
@ -136,7 +136,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
} }
protected abstract createUniversalWatcher( protected abstract createUniversalWatcher(
onChange: (changes: IDiskFileChange[]) => void, onChange: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void, onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean verboseLogging: boolean
): AbstractUniversalWatcherClient; ): AbstractUniversalWatcherClient;
@ -153,7 +153,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
private watchNonRecursive(resource: URI, opts: IWatchOptions): IDisposable { private watchNonRecursive(resource: URI, opts: IWatchOptions): IDisposable {
// Add to list of paths to watch non-recursively // Add to list of paths to watch non-recursively
const pathToWatch: INonRecursiveWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, includes: opts.includes, recursive: false }; const pathToWatch: INonRecursiveWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, includes: opts.includes, recursive: false, correlationId: opts.correlationId };
const remove = insert(this.nonRecursivePathsToWatch, pathToWatch); const remove = insert(this.nonRecursivePathsToWatch, pathToWatch);
// Trigger update // Trigger update
@ -183,7 +183,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
// Create watcher if this is the first time // Create watcher if this is the first time
if (!this.nonRecursiveWatcher) { if (!this.nonRecursiveWatcher) {
this.nonRecursiveWatcher = this._register(this.createNonRecursiveWatcher( this.nonRecursiveWatcher = this._register(this.createNonRecursiveWatcher(
changes => this._onDidChangeFile.fire(toFileChanges(changes)), changes => this._onDidChangeFile.fire(reviveFileChanges(changes)),
msg => this.onWatcherLogMessage(msg), msg => this.onWatcherLogMessage(msg),
this.logService.getLevel() === LogLevel.Trace this.logService.getLevel() === LogLevel.Trace
)); ));
@ -199,7 +199,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
} }
protected abstract createNonRecursiveWatcher( protected abstract createNonRecursiveWatcher(
onChange: (changes: IDiskFileChange[]) => void, onChange: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void, onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean verboseLogging: boolean
): AbstractNonRecursiveWatcherClient; ): AbstractNonRecursiveWatcherClient;

View file

@ -10,10 +10,11 @@ import { canceled } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { newWriteableStream, ReadableStreamEventPayload, ReadableStreamEvents } from 'vs/base/common/stream'; import { newWriteableStream, ReadableStreamEventPayload, ReadableStreamEvents } from 'vs/base/common/stream';
import { URI, UriComponents } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid'; import { generateUuid } from 'vs/base/common/uuid';
import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { createFileSystemProviderError, IFileAtomicReadOptions, FileChangeType, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IStat, IWatchOptions, IFileSystemProviderError } from 'vs/platform/files/common/files'; import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IStat, IWatchOptions, IFileSystemProviderError } from 'vs/platform/files/common/files';
import { reviveFileChanges } from 'vs/platform/files/common/watcher';
export const LOCAL_FILE_SYSTEM_CHANNEL_NAME = 'localFilesystem'; export const LOCAL_FILE_SYSTEM_CHANNEL_NAME = 'localFilesystem';
@ -229,10 +230,10 @@ export class DiskFileSystemProviderClient extends Disposable implements
// for both events and errors from the watcher. So we need to // for both events and errors from the watcher. So we need to
// unwrap the event from the remote and emit through the proper // unwrap the event from the remote and emit through the proper
// emitter. // emitter.
this._register(this.channel.listen<{ resource: UriComponents; type: FileChangeType }[] | string>('fileChange', [this.sessionId])(eventsOrError => { this._register(this.channel.listen<IFileChange[] | string>('fileChange', [this.sessionId])(eventsOrError => {
if (Array.isArray(eventsOrError)) { if (Array.isArray(eventsOrError)) {
const events = eventsOrError; const events = eventsOrError;
this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type }))); this._onDidChange.fire(reviveFileChanges(events));
} else { } else {
const error = eventsOrError; const error = eventsOrError;
this._onDidWatchError.fire(error); this._onDidWatchError.fire(error);

View file

@ -18,7 +18,7 @@ import { extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath } from 'vs/base/c
import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from 'vs/base/common/stream'; import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError, hasFileAtomicDeleteCapability, hasFileAtomicWriteCapability } from 'vs/platform/files/common/files'; import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError, hasFileAtomicDeleteCapability, hasFileAtomicWriteCapability, IWatchOptionsWithCorrelation, IFileSystemWatcher, IWatchOptionsWithoutCorrelation } from 'vs/platform/files/common/files';
import { readFileIntoStream } from 'vs/platform/files/common/io'; import { readFileIntoStream } from 'vs/platform/files/common/io';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { ErrorNoTelemetry } from 'vs/base/common/errors';
@ -63,7 +63,17 @@ export class FileService extends Disposable implements IFileService {
this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider }); this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider });
// Forward events from provider // Forward events from provider
providerDisposables.add(provider.onDidChangeFile(changes => this._onDidFilesChange.fire(new FileChangesEvent(changes, !this.isPathCaseSensitive(provider))))); providerDisposables.add(provider.onDidChangeFile(changes => {
const event = new FileChangesEvent(changes, !this.isPathCaseSensitive(provider));
// Always emit any event internally
this.internalOnDidFilesChange.fire(event);
// Only emit uncorrelated events in the global `onDidFilesChange` event
if (!event.hasCorrelation()) {
this._onDidUncorrelatedFilesChange.fire(event);
}
}));
if (typeof provider.onDidWatchError === 'function') { if (typeof provider.onDidWatchError === 'function') {
providerDisposables.add(provider.onDidWatchError(error => this._onDidWatchError.fire(new Error(error)))); providerDisposables.add(provider.onDidWatchError(error => this._onDidWatchError.fire(new Error(error))));
} }
@ -1094,15 +1104,31 @@ export class FileService extends Disposable implements IFileService {
//#region File Watching //#region File Watching
private readonly _onDidFilesChange = this._register(new Emitter<FileChangesEvent>()); private readonly internalOnDidFilesChange = this._register(new Emitter<FileChangesEvent>());
readonly onDidFilesChange = this._onDidFilesChange.event;
private readonly _onDidUncorrelatedFilesChange = this._register(new Emitter<FileChangesEvent>());
readonly onDidFilesChange = this._onDidUncorrelatedFilesChange.event; // global `onDidFilesChange` skips correlated events
private readonly _onDidWatchError = this._register(new Emitter<Error>()); private readonly _onDidWatchError = this._register(new Emitter<Error>());
readonly onDidWatchError = this._onDidWatchError.event; readonly onDidWatchError = this._onDidWatchError.event;
private readonly activeWatchers = new Map<number /* watch request hash */, { disposable: IDisposable; count: number }>(); private readonly activeWatchers = new Map<number /* watch request hash */, { disposable: IDisposable; count: number }>();
watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IDisposable { private static WATCHER_CORRELATION_IDS = 0;
createWatcher(resource: URI, options: IWatchOptionsWithoutCorrelation): IFileSystemWatcher {
return this.watch(resource, {
...options,
// Explicitly set a correlation id so that file events that originate
// from requests from extensions are exclusively routed back to the
// extension host and not into the workbench.
correlationId: FileService.WATCHER_CORRELATION_IDS++
});
}
watch(resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher;
watch(resource: URI, options?: IWatchOptionsWithoutCorrelation): IDisposable;
watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IFileSystemWatcher | IDisposable {
const disposables = new DisposableStore(); const disposables = new DisposableStore();
// Forward watch request to provider and wire in disposables // Forward watch request to provider and wire in disposables
@ -1125,6 +1151,25 @@ export class FileService extends Disposable implements IFileService {
} }
})(); })();
// When a correlation identifier is set, return a specific
// watcher that only emits events matching that correalation.
const correlationId = options.correlationId;
if (typeof correlationId === 'number') {
const fileChangeEmitter = disposables.add(new Emitter<FileChangesEvent>());
disposables.add(this.internalOnDidFilesChange.event(e => {
if (e.correlates(correlationId)) {
fileChangeEmitter.fire(e);
}
}));
const watcher: IFileSystemWatcher = {
onDidChange: fileChangeEmitter.event,
dispose: () => disposables.dispose()
};
return watcher;
}
return disposables; return disposables;
} }

View file

@ -19,6 +19,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { isWeb } from 'vs/base/common/platform'; import { isWeb } from 'vs/base/common/platform';
import { Schemas } from 'vs/base/common/network'; import { Schemas } from 'vs/base/common/network';
import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Lazy } from 'vs/base/common/lazy';
//#region file service & providers //#region file service & providers
@ -237,10 +238,27 @@ export interface IFileService {
/** /**
* Allows to start a watcher that reports file/folder change events on the provided resource. * Allows to start a watcher that reports file/folder change events on the provided resource.
* *
* Note: recursive file watching is not supported from this method. Only events from files * The watcher runs correlated and thus, file events will be reported on the returned
* that are direct children of the provided resource will be reported. * `IFileSystemWatcher` and not on the generic `IFileService.onDidFilesChange` event.
*/ */
watch(resource: URI, options?: IWatchOptions): IDisposable; createWatcher(resource: URI, options: IWatchOptionsWithoutCorrelation): IFileSystemWatcher;
/**
* Allows to start a watcher that reports file/folder change events on the provided resource.
*
* The watcher runs correlated and thus, file events will be reported on the returned
* `IFileSystemWatcher` and not on the generic `IFileService.onDidFilesChange` event.
*/
watch(resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher;
/**
* Allows to start a watcher that reports file/folder change events on the provided resource.
*
* The watcher runs uncorrelated and thus will report all events from `IFileService.onDidFilesChange`.
* This means, most listeners in the application will receive your events. It is encouraged to
* use correlated watchers (via `IWatchOptionsWithCorrelation`) to limit events to your listener.
*/
watch(resource: URI, options?: IWatchOptionsWithoutCorrelation): IDisposable;
/** /**
* Frees up any resources occupied by this service. * Frees up any resources occupied by this service.
@ -484,13 +502,13 @@ export interface IStat {
readonly permissions?: FilePermission; readonly permissions?: FilePermission;
} }
export interface IWatchOptions { export interface IWatchOptionsWithoutCorrelation {
/** /**
* Set to `true` to watch for changes recursively in a folder * Set to `true` to watch for changes recursively in a folder
* and all of its children. * and all of its children.
*/ */
readonly recursive: boolean; recursive: boolean;
/** /**
* A set of glob patterns or paths to exclude from watching. * A set of glob patterns or paths to exclude from watching.
@ -511,6 +529,36 @@ export interface IWatchOptions {
includes?: Array<string | IRelativePattern>; includes?: Array<string | IRelativePattern>;
} }
export interface IWatchOptions extends IWatchOptionsWithoutCorrelation {
/**
* If provided, file change events from the watcher that
* are a result of this watch request will carry the same
* id.
*/
readonly correlationId?: number;
}
export interface IWatchOptionsWithCorrelation extends IWatchOptions {
readonly correlationId: number;
}
export interface IFileSystemWatcher extends IDisposable {
/**
* An event which fires on file/folder change only for changes
* that correlate to the watch request with matching correlation
* identifier.
*/
readonly onDidChange: Event<FileChangesEvent>;
}
export function isFileSystemWatcher(thing: unknown): thing is IFileSystemWatcher {
const candidate = thing as IFileSystemWatcher | undefined;
return !!candidate && typeof candidate.onDidChange === 'function';
}
export const enum FileSystemProviderCapabilities { export const enum FileSystemProviderCapabilities {
/** /**
@ -882,32 +930,32 @@ export interface IFileChange {
/** /**
* The type of change that occurred to the file. * The type of change that occurred to the file.
*/ */
readonly type: FileChangeType; type: FileChangeType;
/** /**
* The unified resource identifier of the file that changed. * The unified resource identifier of the file that changed.
*/ */
readonly resource: URI; readonly resource: URI;
/**
* If provided when starting the file watcher, the correlation
* identifier will match the original file watching request as
* a way to identify the original component that is interested
* in the change.
*/
readonly cId?: number;
} }
export class FileChangesEvent { export class FileChangesEvent {
private readonly added: TernarySearchTree<URI, IFileChange> | undefined = undefined; private static readonly MIXED_CORRELATION = null;
private readonly updated: TernarySearchTree<URI, IFileChange> | undefined = undefined;
private readonly deleted: TernarySearchTree<URI, IFileChange> | undefined = undefined;
constructor(changes: readonly IFileChange[], ignorePathCasing: boolean) { private readonly correlationId: number | undefined | typeof FileChangesEvent.MIXED_CORRELATION = undefined;
const entriesByType = new Map<FileChangeType, [URI, IFileChange][]>();
constructor(changes: readonly IFileChange[], private readonly ignorePathCasing: boolean) {
for (const change of changes) { for (const change of changes) {
const array = entriesByType.get(change.type);
if (array) {
array.push([change.resource, change]);
} else {
entriesByType.set(change.type, [[change.resource, change]]);
}
// Split by type
switch (change.type) { switch (change.type) {
case FileChangeType.ADDED: case FileChangeType.ADDED:
this.rawAdded.push(change.resource); this.rawAdded.push(change.resource);
@ -919,26 +967,45 @@ export class FileChangesEvent {
this.rawDeleted.push(change.resource); this.rawDeleted.push(change.resource);
break; break;
} }
}
for (const [key, value] of entriesByType) { // Figure out events correlation
switch (key) { if (this.correlationId !== FileChangesEvent.MIXED_CORRELATION) {
case FileChangeType.ADDED: if (typeof change.cId === 'number') {
this.added = TernarySearchTree.forUris<IFileChange>(() => ignorePathCasing); if (this.correlationId === undefined) {
this.added.fill(value); this.correlationId = change.cId; // correlation not yet set, just take it
break; } else if (this.correlationId !== change.cId) {
case FileChangeType.UPDATED: this.correlationId = FileChangesEvent.MIXED_CORRELATION; // correlation mismatch, we have mixed correlation
this.updated = TernarySearchTree.forUris<IFileChange>(() => ignorePathCasing); }
this.updated.fill(value); } else {
break; if (this.correlationId !== undefined) {
case FileChangeType.DELETED: this.correlationId = FileChangesEvent.MIXED_CORRELATION; // correlation mismatch, we have mixed correlation
this.deleted = TernarySearchTree.forUris<IFileChange>(() => ignorePathCasing); }
this.deleted.fill(value); }
break;
} }
} }
} }
private readonly added = new Lazy(() => {
const added = TernarySearchTree.forUris<boolean>(() => this.ignorePathCasing);
added.fill(this.rawAdded.map(resource => [resource, true]));
return added;
});
private readonly updated = new Lazy(() => {
const updated = TernarySearchTree.forUris<boolean>(() => this.ignorePathCasing);
updated.fill(this.rawUpdated.map(resource => [resource, true]));
return updated;
});
private readonly deleted = new Lazy(() => {
const deleted = TernarySearchTree.forUris<boolean>(() => this.ignorePathCasing);
deleted.fill(this.rawDeleted.map(resource => [resource, true]));
return deleted;
});
/** /**
* Find out if the file change events match the provided resource. * Find out if the file change events match the provided resource.
* *
@ -966,33 +1033,33 @@ export class FileChangesEvent {
// Added // Added
if (!hasTypesFilter || types.includes(FileChangeType.ADDED)) { if (!hasTypesFilter || types.includes(FileChangeType.ADDED)) {
if (this.added?.get(resource)) { if (this.added.value.get(resource)) {
return true; return true;
} }
if (options.includeChildren && this.added?.findSuperstr(resource)) { if (options.includeChildren && this.added.value.findSuperstr(resource)) {
return true; return true;
} }
} }
// Updated // Updated
if (!hasTypesFilter || types.includes(FileChangeType.UPDATED)) { if (!hasTypesFilter || types.includes(FileChangeType.UPDATED)) {
if (this.updated?.get(resource)) { if (this.updated.value.get(resource)) {
return true; return true;
} }
if (options.includeChildren && this.updated?.findSuperstr(resource)) { if (options.includeChildren && this.updated.value.findSuperstr(resource)) {
return true; return true;
} }
} }
// Deleted // Deleted
if (!hasTypesFilter || types.includes(FileChangeType.DELETED)) { if (!hasTypesFilter || types.includes(FileChangeType.DELETED)) {
if (this.deleted?.findSubstr(resource) /* deleted also considers parent folders */) { if (this.deleted.value.findSubstr(resource) /* deleted also considers parent folders */) {
return true; return true;
} }
if (options.includeChildren && this.deleted?.findSuperstr(resource)) { if (options.includeChildren && this.deleted.value.findSuperstr(resource)) {
return true; return true;
} }
} }
@ -1004,21 +1071,47 @@ export class FileChangesEvent {
* Returns if this event contains added files. * Returns if this event contains added files.
*/ */
gotAdded(): boolean { gotAdded(): boolean {
return !!this.added; return this.rawAdded.length > 0;
} }
/** /**
* Returns if this event contains deleted files. * Returns if this event contains deleted files.
*/ */
gotDeleted(): boolean { gotDeleted(): boolean {
return !!this.deleted; return this.rawDeleted.length > 0;
} }
/** /**
* Returns if this event contains updated files. * Returns if this event contains updated files.
*/ */
gotUpdated(): boolean { gotUpdated(): boolean {
return !!this.updated; return this.rawUpdated.length > 0;
}
/**
* Returns if this event contains changes that correlate to the
* provided `correlationId`.
*
* File change event correlation is an advanced watch feature that
* allows to identify from which watch request the events originate
* from. This correlation allows to route events specifically
* only to the requestor and not emit them to all listeners.
*/
correlates(correlationId: number): boolean {
return this.correlationId === correlationId;
}
/**
* Figure out if the event contains changes that correlate to one
* correlation identifier.
*
* File change event correlation is an advanced watch feature that
* allows to identify from which watch request the events originate
* from. This correlation allows to route events specifically
* only to the requestor and not emit them to all listeners.
*/
hasCorrelation(): boolean {
return typeof this.correlationId === 'number';
} }
/** /**

View file

@ -34,6 +34,13 @@ interface IWatchRequest {
* events. * events.
*/ */
readonly includes?: Array<string | IRelativePattern>; readonly includes?: Array<string | IRelativePattern>;
/**
* If provided, file change events from the watcher that
* are a result of this watch request will carry the same
* id.
*/
readonly correlationId?: number;
} }
export interface INonRecursiveWatchRequest extends IWatchRequest { export interface INonRecursiveWatchRequest extends IWatchRequest {
@ -70,7 +77,7 @@ interface IWatcher {
* A normalized file change event from the raw events * A normalized file change event from the raw events
* the watcher emits. * the watcher emits.
*/ */
readonly onDidChangeFile: Event<IDiskFileChange[]>; readonly onDidChangeFile: Event<IFileChange[]>;
/** /**
* An event to indicate a message that should get logged. * An event to indicate a message that should get logged.
@ -148,7 +155,7 @@ export abstract class AbstractWatcherClient extends Disposable {
private restartCounter = 0; private restartCounter = 0;
constructor( constructor(
private readonly onFileChanges: (changes: IDiskFileChange[]) => void, private readonly onFileChanges: (changes: IFileChange[]) => void,
private readonly onLogMessage: (msg: ILogMessage) => void, private readonly onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean, private verboseLogging: boolean,
private options: { private options: {
@ -234,7 +241,7 @@ export abstract class AbstractWatcherClient extends Disposable {
export abstract class AbstractNonRecursiveWatcherClient extends AbstractWatcherClient { export abstract class AbstractNonRecursiveWatcherClient extends AbstractWatcherClient {
constructor( constructor(
onFileChanges: (changes: IDiskFileChange[]) => void, onFileChanges: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void, onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean verboseLogging: boolean
) { ) {
@ -247,7 +254,7 @@ export abstract class AbstractNonRecursiveWatcherClient extends AbstractWatcherC
export abstract class AbstractUniversalWatcherClient extends AbstractWatcherClient { export abstract class AbstractUniversalWatcherClient extends AbstractWatcherClient {
constructor( constructor(
onFileChanges: (changes: IDiskFileChange[]) => void, onFileChanges: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void, onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean verboseLogging: boolean
) { ) {
@ -257,24 +264,20 @@ export abstract class AbstractUniversalWatcherClient extends AbstractWatcherClie
protected abstract override createWatcher(disposables: DisposableStore): IUniversalWatcher; protected abstract override createWatcher(disposables: DisposableStore): IUniversalWatcher;
} }
export interface IDiskFileChange {
type: FileChangeType;
readonly resource: URI;
}
export interface ILogMessage { export interface ILogMessage {
readonly type: 'trace' | 'warn' | 'error' | 'info' | 'debug'; readonly type: 'trace' | 'warn' | 'error' | 'info' | 'debug';
readonly message: string; readonly message: string;
} }
export function toFileChanges(changes: IDiskFileChange[]): IFileChange[] { export function reviveFileChanges(changes: IFileChange[]): IFileChange[] {
return changes.map(change => ({ return changes.map(change => ({
type: change.type, type: change.type,
resource: URI.revive(change.resource) resource: URI.revive(change.resource),
cId: change.cId
})); }));
} }
export function coalesceEvents(changes: IDiskFileChange[]): IDiskFileChange[] { export function coalesceEvents(changes: IFileChange[]): IFileChange[] {
// Build deltas // Build deltas
const coalescer = new EventCoalescer(); const coalescer = new EventCoalescer();
@ -312,10 +315,10 @@ export function parseWatcherPatterns(path: string, patterns: Array<string | IRel
class EventCoalescer { class EventCoalescer {
private readonly coalesced = new Set<IDiskFileChange>(); private readonly coalesced = new Set<IFileChange>();
private readonly mapPathToChange = new Map<string, IDiskFileChange>(); private readonly mapPathToChange = new Map<string, IFileChange>();
private toKey(event: IDiskFileChange): string { private toKey(event: IFileChange): string {
if (isLinux) { if (isLinux) {
return event.resource.fsPath; return event.resource.fsPath;
} }
@ -323,7 +326,7 @@ class EventCoalescer {
return event.resource.fsPath.toLowerCase(); // normalise to file system case sensitivity return event.resource.fsPath.toLowerCase(); // normalise to file system case sensitivity
} }
processEvent(event: IDiskFileChange): void { processEvent(event: IFileChange): void {
const existingEvent = this.mapPathToChange.get(this.toKey(event)); const existingEvent = this.mapPathToChange.get(this.toKey(event));
let keepEvent = false; let keepEvent = false;
@ -370,8 +373,8 @@ class EventCoalescer {
} }
} }
coalesce(): IDiskFileChange[] { coalesce(): IFileChange[] {
const addOrChangeEvents: IDiskFileChange[] = []; const addOrChangeEvents: IFileChange[] = [];
const deletedPaths: string[] = []; const deletedPaths: string[] = [];
// This algorithm will remove all DELETE events up to the root folder // This algorithm will remove all DELETE events up to the root folder

View file

@ -19,9 +19,9 @@ import { newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream'
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { IDirent, Promises, RimRafMode, SymlinkSupport } from 'vs/base/node/pfs'; import { IDirent, Promises, RimRafMode, SymlinkSupport } from 'vs/base/node/pfs';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, FilePermission, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileAtomicDeleteCapability } from 'vs/platform/files/common/files'; import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, FilePermission, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileChange } from 'vs/platform/files/common/files';
import { readFileIntoStream } from 'vs/platform/files/common/io'; import { readFileIntoStream } from 'vs/platform/files/common/io';
import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, IDiskFileChange, ILogMessage } from 'vs/platform/files/common/watcher'; import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILogMessage } from 'vs/platform/files/common/watcher';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { AbstractDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/common/diskFileSystemProvider'; import { AbstractDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/common/diskFileSystemProvider';
import { toErrorMessage } from 'vs/base/common/errorMessage'; import { toErrorMessage } from 'vs/base/common/errorMessage';
@ -812,7 +812,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
//#region File Watching //#region File Watching
protected createUniversalWatcher( protected createUniversalWatcher(
onChange: (changes: IDiskFileChange[]) => void, onChange: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void, onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean verboseLogging: boolean
): AbstractUniversalWatcherClient { ): AbstractUniversalWatcherClient {
@ -820,7 +820,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
} }
protected createNonRecursiveWatcher( protected createNonRecursiveWatcher(
onChange: (changes: IDiskFileChange[]) => void, onChange: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void, onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean verboseLogging: boolean
): AbstractNonRecursiveWatcherClient { ): AbstractNonRecursiveWatcherClient {

View file

@ -292,7 +292,8 @@ export abstract class AbstractSessionFileWatcher extends Disposable implements I
sessionEmitter.fire( sessionEmitter.fire(
events.map(e => ({ events.map(e => ({
resource: this.uriTransformer.transformOutgoingURI(e.resource), resource: this.uriTransformer.transformOutgoingURI(e.resource),
type: e.type type: e.type,
cId: e.cId
})) }))
); );
})); }));

View file

@ -4,13 +4,14 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { DisposableStore } from 'vs/base/common/lifecycle'; import { DisposableStore } from 'vs/base/common/lifecycle';
import { IDiskFileChange, ILogMessage, AbstractNonRecursiveWatcherClient, INonRecursiveWatcher } from 'vs/platform/files/common/watcher'; import { IFileChange } from 'vs/platform/files/common/files';
import { ILogMessage, AbstractNonRecursiveWatcherClient, INonRecursiveWatcher } from 'vs/platform/files/common/watcher';
import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher'; import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher';
export class NodeJSWatcherClient extends AbstractNonRecursiveWatcherClient { export class NodeJSWatcherClient extends AbstractNonRecursiveWatcherClient {
constructor( constructor(
onFileChanges: (changes: IDiskFileChange[]) => void, onFileChanges: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void, onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean verboseLogging: boolean
) { ) {

View file

@ -7,7 +7,8 @@ import { Event, Emitter } from 'vs/base/common/event';
import { patternsEquals } from 'vs/base/common/glob'; import { patternsEquals } from 'vs/base/common/glob';
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';
import { isLinux } from 'vs/base/common/platform'; import { isLinux } from 'vs/base/common/platform';
import { IDiskFileChange, ILogMessage, INonRecursiveWatchRequest, INonRecursiveWatcher } from 'vs/platform/files/common/watcher'; import { IFileChange } from 'vs/platform/files/common/files';
import { ILogMessage, INonRecursiveWatchRequest, INonRecursiveWatcher } from 'vs/platform/files/common/watcher';
import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib';
export interface INodeJSWatcherInstance { export interface INodeJSWatcherInstance {
@ -25,7 +26,7 @@ export interface INodeJSWatcherInstance {
export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher { export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher {
private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>()); private readonly _onDidChangeFile = this._register(new Emitter<IFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event; readonly onDidChangeFile = this._onDidChangeFile.event;
private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>()); private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>());
@ -61,7 +62,7 @@ export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher {
// Logging // Logging
if (requestsToStartWatching.length) { if (requestsToStartWatching.length) {
this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : '<none>'}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : '<all>'})`).join(',')}`); this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : '<none>'}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : '<all>'}, correlationId: ${typeof request.correlationId === 'number' ? request.correlationId : '<none>'})`).join(',')}`);
} }
if (pathsToStopWatching.length) { if (pathsToStopWatching.length) {
@ -107,15 +108,22 @@ export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher {
} }
private normalizeRequests(requests: INonRecursiveWatchRequest[]): INonRecursiveWatchRequest[] { private normalizeRequests(requests: INonRecursiveWatchRequest[]): INonRecursiveWatchRequest[] {
const requestsMap = new Map<string, INonRecursiveWatchRequest>(); const mapCorrelationtoRequests = new Map<number | undefined /* correlation */, Map<string, INonRecursiveWatchRequest>>();
// Ignore requests for the same paths // Ignore requests for the same paths that have the same correlation
for (const request of requests) { for (const request of requests) {
const path = isLinux ? request.path : request.path.toLowerCase(); // adjust for case sensitivity const path = isLinux ? request.path : request.path.toLowerCase(); // adjust for case sensitivity
requestsMap.set(path, request);
let requestsForCorrelation = mapCorrelationtoRequests.get(request.correlationId);
if (!requestsForCorrelation) {
requestsForCorrelation = new Map<string, INonRecursiveWatchRequest>();
mapCorrelationtoRequests.set(request.correlationId, requestsForCorrelation);
}
requestsForCorrelation.set(path, request);
} }
return Array.from(requestsMap.values()); return Array.from(mapCorrelationtoRequests.values()).map(requests => Array.from(requests.values())).flat();
} }
async setVerboseLogging(enabled: boolean): Promise<void> { async setVerboseLogging(enabled: boolean): Promise<void> {

View file

@ -11,11 +11,12 @@ import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/
import { normalizeNFC } from 'vs/base/common/normalization'; import { normalizeNFC } from 'vs/base/common/normalization';
import { basename, dirname, join } from 'vs/base/common/path'; import { basename, dirname, join } from 'vs/base/common/path';
import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { isLinux, isMacintosh } from 'vs/base/common/platform';
import { joinPath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { realcase } from 'vs/base/node/extpath'; import { realcase } from 'vs/base/node/extpath';
import { Promises } from 'vs/base/node/pfs'; import { Promises } from 'vs/base/node/pfs';
import { FileChangeType } from 'vs/platform/files/common/files'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files';
import { IDiskFileChange, ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; import { ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns } from 'vs/platform/files/common/watcher';
export class NodeJSFileWatcherLibrary extends Disposable { export class NodeJSFileWatcherLibrary extends Disposable {
@ -35,7 +36,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
// recursive watcher because we can have many individual // recursive watcher because we can have many individual
// node.js watchers per request. // node.js watchers per request.
// (https://github.com/microsoft/vscode/issues/124723) // (https://github.com/microsoft/vscode/issues/124723)
private readonly throttledFileChangesEmitter = this._register(new ThrottledWorker<IDiskFileChange>( private readonly throttledFileChangesEmitter = this._register(new ThrottledWorker<IFileChange>(
{ {
maxWorkChunkSize: 100, // only process up to 100 changes at once before... maxWorkChunkSize: 100, // only process up to 100 changes at once before...
throttleDelay: 200, // ...resting for 200ms until we process events again... throttleDelay: 200, // ...resting for 200ms until we process events again...
@ -46,7 +47,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
// Aggregate file changes over FILE_CHANGES_HANDLER_DELAY // Aggregate file changes over FILE_CHANGES_HANDLER_DELAY
// to coalesce events and reduce spam. // to coalesce events and reduce spam.
private readonly fileChangesAggregator = this._register(new RunOnceWorker<IDiskFileChange>(events => this.handleFileChanges(events), NodeJSFileWatcherLibrary.FILE_CHANGES_HANDLER_DELAY)); private readonly fileChangesAggregator = this._register(new RunOnceWorker<IFileChange>(events => this.handleFileChanges(events), NodeJSFileWatcherLibrary.FILE_CHANGES_HANDLER_DELAY));
private readonly excludes = parseWatcherPatterns(this.request.path, this.request.excludes); 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 includes = this.request.includes ? parseWatcherPatterns(this.request.path, this.request.includes) : undefined;
@ -57,7 +58,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
constructor( constructor(
private request: INonRecursiveWatchRequest, private request: INonRecursiveWatchRequest,
private onDidFilesChange: (changes: IDiskFileChange[]) => void, private onDidFilesChange: (changes: IFileChange[]) => void,
private onLogMessage?: (msg: ILogMessage) => void, private onLogMessage?: (msg: ILogMessage) => void,
private verboseLogging?: boolean private verboseLogging?: boolean
) { ) {
@ -128,6 +129,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
const disposables = new DisposableStore(); const disposables = new DisposableStore();
try { try {
const requestResource = URI.file(this.request.path);
const pathBasename = basename(path); const pathBasename = basename(path);
// Creating watcher can fail with an exception // Creating watcher can fail with an exception
@ -261,7 +263,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
type = FileChangeType.DELETED; type = FileChangeType.DELETED;
} }
this.onFileChange({ resource: URI.file(join(this.request.path, changedFileName)), type }); this.onFileChange({ resource: joinPath(requestResource, changedFileName), type, cId: this.request.correlationId });
}, NodeJSFileWatcherLibrary.FILE_DELETE_HANDLER_DELAY); }, NodeJSFileWatcherLibrary.FILE_DELETE_HANDLER_DELAY);
mapPathToStatDisposable.set(changedFileName, toDisposable(() => clearTimeout(timeoutHandle))); mapPathToStatDisposable.set(changedFileName, toDisposable(() => clearTimeout(timeoutHandle)));
@ -280,7 +282,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
folderChildren.add(changedFileName); folderChildren.add(changedFileName);
} }
this.onFileChange({ resource: URI.file(join(this.request.path, changedFileName)), type }); this.onFileChange({ resource: joinPath(requestResource, changedFileName), type, cId: this.request.correlationId });
} }
} }
@ -319,14 +321,14 @@ export class NodeJSFileWatcherLibrary extends Disposable {
// File still exists, so emit as change event and reapply the watcher // File still exists, so emit as change event and reapply the watcher
if (fileExists) { if (fileExists) {
this.onFileChange({ resource: URI.file(this.request.path), type: FileChangeType.UPDATED }, true /* skip excludes/includes (file is explicitly watched) */); this.onFileChange({ resource: requestResource, type: FileChangeType.UPDATED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */);
disposables.add(await this.doWatch(path, false)); disposables.add(await this.doWatch(path, false));
} }
// File seems to be really gone, so emit a deleted event and dispose // File seems to be really gone, so emit a deleted event and dispose
else { else {
this.onFileChange({ resource: URI.file(this.request.path), type: FileChangeType.DELETED }, true /* skip excludes/includes (file is explicitly watched) */); this.onFileChange({ resource: requestResource, type: FileChangeType.DELETED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */);
// Important to flush the event delivery // Important to flush the event delivery
// before disposing the watcher, otherwise // before disposing the watcher, otherwise
@ -345,7 +347,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
// File changed // File changed
else { else {
this.onFileChange({ resource: URI.file(this.request.path), type: FileChangeType.UPDATED }, true /* skip excludes/includes (file is explicitly watched) */); this.onFileChange({ resource: requestResource, type: FileChangeType.UPDATED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */);
} }
} }
}); });
@ -361,7 +363,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
}); });
} }
private onFileChange(event: IDiskFileChange, skipIncludeExcludeChecks = false): void { private onFileChange(event: IFileChange, skipIncludeExcludeChecks = false): void {
if (this.cts.token.isCancellationRequested) { if (this.cts.token.isCancellationRequested) {
return; return;
} }
@ -385,7 +387,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
} }
} }
private handleFileChanges(fileChanges: IDiskFileChange[]): void { private handleFileChanges(fileChanges: IFileChange[]): void {
// Coalesce events: merge events of same kind // Coalesce events: merge events of same kind
const coalescedFileChanges = coalesceEvents(fileChanges); const coalescedFileChanges = coalesceEvents(fileChanges);
@ -394,7 +396,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
// Logging // Logging
if (this.verboseLogging) { if (this.verboseLogging) {
for (const event of coalescedFileChanges) { for (const event of coalescedFileChanges) {
this.trace(`>> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`); this.trace(` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`);
} }
} }

View file

@ -20,8 +20,8 @@ import { dirname, normalize } from 'vs/base/common/path';
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { realcaseSync, realpathSync } from 'vs/base/node/extpath'; import { realcaseSync, realpathSync } from 'vs/base/node/extpath';
import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib';
import { FileChangeType } from 'vs/platform/files/common/files'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files';
import { IDiskFileChange, ILogMessage, coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; import { ILogMessage, coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher, parseWatcherPatterns } from 'vs/platform/files/common/watcher';
export interface IParcelWatcherInstance { export interface IParcelWatcherInstance {
@ -49,7 +49,7 @@ export interface IParcelWatcherInstance {
/** /**
* An event aggregator to coalesce events and reduce duplicates. * An event aggregator to coalesce events and reduce duplicates.
*/ */
readonly worker: RunOnceWorker<IDiskFileChange>; readonly worker: RunOnceWorker<IFileChange>;
/** /**
* Stops and disposes the watcher. This operation is async to await * Stops and disposes the watcher. This operation is async to await
@ -70,7 +70,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events'; private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events';
private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>()); private readonly _onDidChangeFile = this._register(new Emitter<IFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event; readonly onDidChangeFile = this._onDidChangeFile.event;
private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>()); private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>());
@ -95,7 +95,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
// Reduce likelyhood of spam from file events via throttling. // Reduce likelyhood of spam from file events via throttling.
// (https://github.com/microsoft/vscode/issues/124723) // (https://github.com/microsoft/vscode/issues/124723)
private readonly throttledFileChangesEmitter = this._register(new ThrottledWorker<IDiskFileChange>( private readonly throttledFileChangesEmitter = this._register(new ThrottledWorker<IFileChange>(
{ {
maxWorkChunkSize: 500, // only process up to 500 changes at once before... maxWorkChunkSize: 500, // only process up to 500 changes at once before...
throttleDelay: 200, // ...resting for 200ms until we process events again... throttleDelay: 200, // ...resting for 200ms until we process events again...
@ -150,7 +150,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
// Logging // Logging
if (requestsToStartWatching.length) { if (requestsToStartWatching.length) {
this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : '<none>'}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : '<all>'})`).join(',')}`); this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : '<none>'}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : '<all>'}, correlationId: ${typeof request.correlationId === 'number' ? request.correlationId : '<none>'})`).join(',')}`);
} }
if (pathsToStopWatching.length) { if (pathsToStopWatching.length) {
@ -185,7 +185,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
ready: instance.p, ready: instance.p,
restarts, restarts,
token: cts.token, token: cts.token,
worker: new RunOnceWorker<IDiskFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY), worker: new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),
stop: async () => { stop: async () => {
cts.dispose(true); cts.dispose(true);
@ -256,7 +256,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
ready: instance.p, ready: instance.p,
restarts, restarts,
token: cts.token, token: cts.token,
worker: new RunOnceWorker<IDiskFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY), worker: new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),
stop: async () => { stop: async () => {
cts.dispose(true); cts.dispose(true);
@ -315,7 +315,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
this.normalizeEvents(parcelEvents, watcher.request, realPathDiffers, realPathLength); this.normalizeEvents(parcelEvents, watcher.request, realPathDiffers, realPathLength);
// Check for includes // Check for includes
const includedEvents = this.handleIncludes(parcelEvents, includes); const includedEvents = this.handleIncludes(watcher, parcelEvents, includes);
// Add to event aggregator for later processing // Add to event aggregator for later processing
for (const includedEvent of includedEvents) { for (const includedEvent of includedEvents) {
@ -323,8 +323,8 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
} }
} }
private handleIncludes(parcelEvents: parcelWatcher.Event[], includes: ParsedPattern[] | undefined): IDiskFileChange[] { private handleIncludes(watcher: IParcelWatcherInstance, parcelEvents: parcelWatcher.Event[], includes: ParsedPattern[] | undefined): IFileChange[] {
const events: IDiskFileChange[] = []; const events: IFileChange[] = [];
for (const { path, type: parcelEventType } of parcelEvents) { for (const { path, type: parcelEventType } of parcelEvents) {
const type = ParcelWatcher.MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE.get(parcelEventType)!; const type = ParcelWatcher.MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE.get(parcelEventType)!;
@ -338,14 +338,14 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
this.trace(` >> ignored (not included) ${path}`); this.trace(` >> ignored (not included) ${path}`);
} }
} else { } else {
events.push({ type, resource: URI.file(path) }); events.push({ type, resource: URI.file(path), cId: watcher.request.correlationId });
} }
} }
return events; return events;
} }
private handleParcelEvents(parcelEvents: IDiskFileChange[], watcher: IParcelWatcherInstance): void { private handleParcelEvents(parcelEvents: IFileChange[], watcher: IParcelWatcherInstance): void {
// Coalesce events: merge events of same kind // Coalesce events: merge events of same kind
const coalescedEvents = coalesceEvents(parcelEvents); const coalescedEvents = coalesceEvents(parcelEvents);
@ -354,7 +354,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
const { events: filteredEvents, rootDeleted } = this.filterEvents(coalescedEvents, watcher); const { events: filteredEvents, rootDeleted } = this.filterEvents(coalescedEvents, watcher);
// Broadcast to clients // Broadcast to clients
this.emitEvents(filteredEvents); this.emitEvents(filteredEvents, watcher);
// Handle root path deletes // Handle root path deletes
if (rootDeleted) { if (rootDeleted) {
@ -362,7 +362,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
} }
} }
private emitEvents(events: IDiskFileChange[]): void { private emitEvents(events: IFileChange[], watcher: IParcelWatcherInstance): void {
if (events.length === 0) { if (events.length === 0) {
return; return;
} }
@ -370,7 +370,8 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
// Logging // Logging
if (this.verboseLogging) { if (this.verboseLogging) {
for (const event of events) { for (const event of events) {
this.trace(` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`); const traceMsg = ` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`;
this.trace(typeof watcher.request.correlationId === 'number' ? `${traceMsg} (correlationId: ${watcher.request.correlationId})` : traceMsg);
} }
} }
@ -440,8 +441,8 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
} }
} }
private filterEvents(events: IDiskFileChange[], watcher: IParcelWatcherInstance): { events: IDiskFileChange[]; rootDeleted?: boolean } { private filterEvents(events: IFileChange[], watcher: IParcelWatcherInstance): { events: IFileChange[]; rootDeleted?: boolean } {
const filteredEvents: IDiskFileChange[] = []; const filteredEvents: IFileChange[] = [];
let rootDeleted = false; let rootDeleted = false;
for (const event of events) { for (const event of events) {
@ -467,7 +468,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
const parentPath = dirname(watcher.request.path); const parentPath = dirname(watcher.request.path);
if (existsSync(parentPath)) { if (existsSync(parentPath)) {
const nodeWatcher = new NodeJSFileWatcherLibrary({ path: parentPath, excludes: [], recursive: false }, changes => { const nodeWatcher = new NodeJSFileWatcherLibrary({ path: parentPath, excludes: [], recursive: false, correlationId: watcher.request.correlationId }, changes => {
if (watcher.token.isCancellationRequested) { if (watcher.token.isCancellationRequested) {
return; // return early when disposed return; // return early when disposed
} }
@ -569,62 +570,86 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
} }
protected normalizeRequests(requests: IRecursiveWatchRequest[], validatePaths = true): IRecursiveWatchRequest[] { protected normalizeRequests(requests: IRecursiveWatchRequest[], validatePaths = true): IRecursiveWatchRequest[] {
const requestTrie = TernarySearchTree.forPaths<IRecursiveWatchRequest>(!isLinux);
// Sort requests by path length to have shortest first // Sort requests by path length to have shortest first
// to have a way to prevent children to be watched if // to have a way to prevent children to be watched if
// parents exist. // parents exist.
requests.sort((requestA, requestB) => requestA.path.length - requestB.path.length); requests.sort((requestA, requestB) => requestA.path.length - requestB.path.length);
// Only consider requests for watching that are not // Map request paths to correlation and ignore identical paths
// a child of an existing request path to prevent const mapCorrelationtoRequests = new Map<number | undefined /* correlation */, Map<string, IRecursiveWatchRequest>>();
// duplication. In addition, drop any request where
// everything is excluded (via `**` glob).
//
// However, allow explicit requests to watch folders
// that are symbolic links because the Parcel watcher
// does not allow to recursively watch symbolic links.
for (const request of requests) { for (const request of requests) {
if (request.excludes.includes(GLOBSTAR)) { if (request.excludes.includes(GLOBSTAR)) {
continue; // path is ignored entirely (via `**` glob exclude) continue; // path is ignored entirely (via `**` glob exclude)
} }
// Check for overlapping requests const path = isLinux ? request.path : request.path.toLowerCase(); // adjust for case sensitivity
if (requestTrie.findSubstr(request.path)) {
try {
const realpath = realpathSync(request.path);
if (realpath === request.path) {
this.trace(`ignoring a path for watching who's parent is already watched: ${request.path}`);
continue; let requestsForCorrelation = mapCorrelationtoRequests.get(request.correlationId);
} if (!requestsForCorrelation) {
} catch (error) { requestsForCorrelation = new Map<string, IRecursiveWatchRequest>();
this.trace(`ignoring a path for watching who's realpath failed to resolve: ${request.path} (error: ${error})`); mapCorrelationtoRequests.set(request.correlationId, requestsForCorrelation);
continue;
}
} }
// Check for invalid paths requestsForCorrelation.set(path, request);
if (validatePaths) {
try {
const stat = statSync(request.path);
if (!stat.isDirectory()) {
this.trace(`ignoring a path for watching that is a file and not a folder: ${request.path}`);
continue;
}
} catch (error) {
this.trace(`ignoring a path for watching who's stat info failed to resolve: ${request.path} (error: ${error})`);
continue;
}
}
requestTrie.set(request.path, request);
} }
return Array.from(requestTrie).map(([, request]) => request); const normalizedRequests: IRecursiveWatchRequest[] = [];
for (const requestsForCorrelation of mapCorrelationtoRequests.values()) {
// Only consider requests for watching that are not
// a child of an existing request path to prevent
// duplication. In addition, drop any request where
// everything is excluded (via `**` glob).
//
// However, allow explicit requests to watch folders
// that are symbolic links because the Parcel watcher
// does not allow to recursively watch symbolic links.
const requestTrie = TernarySearchTree.forPaths<IRecursiveWatchRequest>(!isLinux);
for (const request of requestsForCorrelation.values()) {
// Check for overlapping requests
if (requestTrie.findSubstr(request.path)) {
try {
const realpath = realpathSync(request.path);
if (realpath === request.path) {
this.trace(`ignoring a path for watching who's parent is already watched: ${request.path}`);
continue;
}
} catch (error) {
this.trace(`ignoring a path for watching who's realpath failed to resolve: ${request.path} (error: ${error})`);
continue;
}
}
// Check for invalid paths
if (validatePaths) {
try {
const stat = statSync(request.path);
if (!stat.isDirectory()) {
this.trace(`ignoring a path for watching that is a file and not a folder: ${request.path}`);
continue;
}
} catch (error) {
this.trace(`ignoring a path for watching who's stat info failed to resolve: ${request.path} (error: ${error})`);
continue;
}
}
requestTrie.set(request.path, request);
}
normalizedRequests.push(...Array.from(requestTrie).map(([, request]) => request));
}
return normalizedRequests;
} }
async setVerboseLogging(enabled: boolean): Promise<void> { async setVerboseLogging(enabled: boolean): Promise<void> {

View file

@ -7,12 +7,13 @@ import { DisposableStore } from 'vs/base/common/lifecycle';
import { FileAccess } from 'vs/base/common/network'; import { FileAccess } from 'vs/base/common/network';
import { getNextTickChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { getNextTickChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { AbstractUniversalWatcherClient, IDiskFileChange, ILogMessage, IUniversalWatcher } from 'vs/platform/files/common/watcher'; import { IFileChange } from 'vs/platform/files/common/files';
import { AbstractUniversalWatcherClient, ILogMessage, IUniversalWatcher } from 'vs/platform/files/common/watcher';
export class UniversalWatcherClient extends AbstractUniversalWatcherClient { export class UniversalWatcherClient extends AbstractUniversalWatcherClient {
constructor( constructor(
onFileChanges: (changes: IDiskFileChange[]) => void, onFileChanges: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void, onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean verboseLogging: boolean
) { ) {

View file

@ -7,12 +7,13 @@ import * as assert from 'assert';
import { DeferredPromise, timeout } from 'vs/base/common/async'; import { DeferredPromise, timeout } from 'vs/base/common/async';
import { bufferToReadable, bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { bufferToReadable, bufferToStream, VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { isEqual } from 'vs/base/common/resources'; import { isEqual } from 'vs/base/common/resources';
import { consumeStream, newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream'; import { consumeStream, newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { IFileOpenOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileType, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IStat, IFileAtomicReadOptions, IFileAtomicWriteOptions, IFileAtomicDeleteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileAtomicOptions } from 'vs/platform/files/common/files'; import { IFileOpenOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileType, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IStat, IFileAtomicReadOptions, IFileAtomicWriteOptions, IFileAtomicDeleteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileAtomicOptions, IFileChange, isFileSystemWatcher, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService'; import { FileService } from 'vs/platform/files/common/fileService';
import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider'; import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider';
import { NullLogService } from 'vs/platform/log/common/log'; import { NullLogService } from 'vs/platform/log/common/log';
@ -142,6 +143,58 @@ suite('File Service', () => {
service.dispose(); service.dispose();
}); });
test('watch - with corelation', async () => {
const service = disposables.add(new FileService(new NullLogService()));
const provider = new class extends NullFileSystemProvider {
private readonly _testOnDidChangeFile = new Emitter<readonly IFileChange[]>();
override readonly onDidChangeFile: Event<readonly IFileChange[]> = this._testOnDidChangeFile.event;
fireFileChange(changes: readonly IFileChange[]) {
this._testOnDidChangeFile.fire(changes);
}
};
disposables.add(service.registerProvider('test', provider));
await service.activateProvider('test');
const globalEvents: FileChangesEvent[] = [];
disposables.add(service.onDidFilesChange(e => {
globalEvents.push(e);
}));
const watcher0 = disposables.add(service.watch(URI.parse('test://watch/folder1'), { recursive: true, excludes: [], includes: [] }));
assert.strictEqual(isFileSystemWatcher(watcher0), false);
const watcher1 = disposables.add(service.watch(URI.parse('test://watch/folder2'), { recursive: true, excludes: [], includes: [], correlationId: 100 }));
assert.strictEqual(isFileSystemWatcher(watcher1), true);
const watcher2 = disposables.add(service.watch(URI.parse('test://watch/folder3'), { recursive: true, excludes: [], includes: [], correlationId: 200 }));
assert.strictEqual(isFileSystemWatcher(watcher2), true);
const watcher1Events: FileChangesEvent[] = [];
disposables.add(watcher1.onDidChange(e => {
watcher1Events.push(e);
}));
const watcher2Events: FileChangesEvent[] = [];
disposables.add(watcher2.onDidChange(e => {
watcher2Events.push(e);
}));
provider.fireFileChange([{ resource: URI.parse('test://watch/folder1'), type: FileChangeType.ADDED }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder2'), type: FileChangeType.ADDED, cId: 100 }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder2'), type: FileChangeType.ADDED, cId: 100 }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder3/file'), type: FileChangeType.UPDATED, cId: 200 }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder3'), type: FileChangeType.UPDATED, cId: 200 }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder4'), type: FileChangeType.ADDED, cId: 50 }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder4'), type: FileChangeType.ADDED, cId: 60 }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder4'), type: FileChangeType.ADDED, cId: 70 }]);
assert.strictEqual(globalEvents.length, 1);
assert.strictEqual(watcher1Events.length, 2);
assert.strictEqual(watcher2Events.length, 2);
});
test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060) - async', async () => { test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060) - async', async () => {
testReadErrorBubbles(true); testReadErrorBubbles(true);
}); });

View file

@ -8,7 +8,7 @@ import { isEqual, isEqualOrParent } from 'vs/base/common/extpath';
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils';
import { FileChangesEvent, FileChangeType, isParent } from 'vs/platform/files/common/files'; import { FileChangesEvent, FileChangeType, IFileChange, isParent } from 'vs/platform/files/common/files';
suite('Files', () => { suite('Files', () => {
@ -109,6 +109,51 @@ suite('Files', () => {
} }
}); });
test('FileChangesEvent - correlation', function () {
let changes: IFileChange[] = [
{ resource: toResource.call(this, '/foo/updated.txt'), type: FileChangeType.UPDATED },
{ resource: toResource.call(this, '/foo/otherupdated.txt'), type: FileChangeType.UPDATED },
{ resource: toResource.call(this, '/added.txt'), type: FileChangeType.ADDED },
];
let event: FileChangesEvent = new FileChangesEvent(changes, true);
assert.strictEqual(event.hasCorrelation(), false);
assert.strictEqual(event.correlates(100), false);
changes = [
{ resource: toResource.call(this, '/foo/updated.txt'), type: FileChangeType.UPDATED, cId: 100 },
{ resource: toResource.call(this, '/foo/otherupdated.txt'), type: FileChangeType.UPDATED, cId: 100 },
{ resource: toResource.call(this, '/added.txt'), type: FileChangeType.ADDED, cId: 100 },
];
event = new FileChangesEvent(changes, true);
assert.strictEqual(event.hasCorrelation(), true);
assert.strictEqual(event.correlates(100), true);
assert.strictEqual(event.correlates(120), false);
changes = [
{ resource: toResource.call(this, '/foo/updated.txt'), type: FileChangeType.UPDATED, cId: 100 },
{ resource: toResource.call(this, '/foo/otherupdated.txt'), type: FileChangeType.UPDATED },
{ resource: toResource.call(this, '/added.txt'), type: FileChangeType.ADDED, cId: 100 },
];
event = new FileChangesEvent(changes, true);
assert.strictEqual(event.hasCorrelation(), false);
assert.strictEqual(event.correlates(100), false);
assert.strictEqual(event.correlates(120), false);
changes = [
{ resource: toResource.call(this, '/foo/updated.txt'), type: FileChangeType.UPDATED, cId: 100 },
{ resource: toResource.call(this, '/foo/otherupdated.txt'), type: FileChangeType.UPDATED, cId: 120 },
{ resource: toResource.call(this, '/added.txt'), type: FileChangeType.ADDED, cId: 100 },
];
event = new FileChangesEvent(changes, true);
assert.strictEqual(event.hasCorrelation(), false);
assert.strictEqual(event.correlates(100), false);
assert.strictEqual(event.correlates(120), false);
});
function testIsEqual(testMethod: (pA: string, pB: string, ignoreCase: boolean) => boolean): void { function testIsEqual(testMethod: (pA: string, pB: string, ignoreCase: boolean) => boolean): void {
// corner cases // corner cases

View file

@ -11,7 +11,7 @@ import { isEqual } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { FileChangesEvent, FileChangeType, IFileChange } from 'vs/platform/files/common/files'; import { FileChangesEvent, FileChangeType, IFileChange } from 'vs/platform/files/common/files';
import { IDiskFileChange, coalesceEvents, toFileChanges, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; import { coalesceEvents, reviveFileChanges, parseWatcherPatterns } from 'vs/platform/files/common/watcher';
class TestFileWatcher extends Disposable { class TestFileWatcher extends Disposable {
private readonly _onDidFilesChange: Emitter<{ raw: IFileChange[]; event: FileChangesEvent }>; private readonly _onDidFilesChange: Emitter<{ raw: IFileChange[]; event: FileChangesEvent }>;
@ -26,23 +26,23 @@ class TestFileWatcher extends Disposable {
return this._onDidFilesChange.event; return this._onDidFilesChange.event;
} }
report(changes: IDiskFileChange[]): void { report(changes: IFileChange[]): void {
this.onRawFileEvents(changes); this.onRawFileEvents(changes);
} }
private onRawFileEvents(events: IDiskFileChange[]): void { private onRawFileEvents(events: IFileChange[]): void {
// Coalesce // Coalesce
const coalescedEvents = coalesceEvents(events); const coalescedEvents = coalesceEvents(events);
// Emit through event emitter // Emit through event emitter
if (coalescedEvents.length > 0) { if (coalescedEvents.length > 0) {
this._onDidFilesChange.fire({ raw: toFileChanges(coalescedEvents), event: this.toFileChangesEvent(coalescedEvents) }); this._onDidFilesChange.fire({ raw: reviveFileChanges(coalescedEvents), event: this.toFileChangesEvent(coalescedEvents) });
} }
} }
private toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent { private toFileChangesEvent(changes: IFileChange[]): FileChangesEvent {
return new FileChangesEvent(toFileChanges(changes), !isLinux); return new FileChangesEvent(reviveFileChanges(changes), !isLinux);
} }
} }
@ -126,7 +126,7 @@ suite('Watcher Events Normalizer', () => {
const updated = URI.file('/users/data/src/updated.txt'); const updated = URI.file('/users/data/src/updated.txt');
const deleted = URI.file('/users/data/src/deleted.txt'); const deleted = URI.file('/users/data/src/deleted.txt');
const raw: IDiskFileChange[] = [ const raw: IFileChange[] = [
{ resource: added, type: FileChangeType.ADDED }, { resource: added, type: FileChangeType.ADDED },
{ resource: updated, type: FileChangeType.UPDATED }, { resource: updated, type: FileChangeType.UPDATED },
{ resource: deleted, type: FileChangeType.DELETED }, { resource: deleted, type: FileChangeType.DELETED },
@ -159,7 +159,7 @@ suite('Watcher Events Normalizer', () => {
const addedFile = URI.file(path === Path.UNIX ? '/users/data/src/added.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\added.txt' : '\\\\localhost\\users\\data\\src\\added.txt'); const addedFile = URI.file(path === Path.UNIX ? '/users/data/src/added.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\added.txt' : '\\\\localhost\\users\\data\\src\\added.txt');
const updatedFile = URI.file(path === Path.UNIX ? '/users/data/src/updated.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\updated.txt' : '\\\\localhost\\users\\data\\src\\updated.txt'); const updatedFile = URI.file(path === Path.UNIX ? '/users/data/src/updated.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\updated.txt' : '\\\\localhost\\users\\data\\src\\updated.txt');
const raw: IDiskFileChange[] = [ const raw: IFileChange[] = [
{ resource: deletedFolderA, type: FileChangeType.DELETED }, { resource: deletedFolderA, type: FileChangeType.DELETED },
{ resource: deletedFolderB, type: FileChangeType.DELETED }, { resource: deletedFolderB, type: FileChangeType.DELETED },
{ resource: deletedFolderBF1, type: FileChangeType.DELETED }, { resource: deletedFolderBF1, type: FileChangeType.DELETED },
@ -194,7 +194,7 @@ suite('Watcher Events Normalizer', () => {
const deleted = URI.file('/users/data/src/related'); const deleted = URI.file('/users/data/src/related');
const unrelated = URI.file('/users/data/src/unrelated'); const unrelated = URI.file('/users/data/src/unrelated');
const raw: IDiskFileChange[] = [ const raw: IFileChange[] = [
{ resource: created, type: FileChangeType.ADDED }, { resource: created, type: FileChangeType.ADDED },
{ resource: deleted, type: FileChangeType.DELETED }, { resource: deleted, type: FileChangeType.DELETED },
{ resource: unrelated, type: FileChangeType.UPDATED }, { resource: unrelated, type: FileChangeType.UPDATED },
@ -219,7 +219,7 @@ suite('Watcher Events Normalizer', () => {
const created = URI.file('/users/data/src/related'); const created = URI.file('/users/data/src/related');
const unrelated = URI.file('/users/data/src/unrelated'); const unrelated = URI.file('/users/data/src/unrelated');
const raw: IDiskFileChange[] = [ const raw: IFileChange[] = [
{ resource: deleted, type: FileChangeType.DELETED }, { resource: deleted, type: FileChangeType.DELETED },
{ resource: created, type: FileChangeType.ADDED }, { resource: created, type: FileChangeType.ADDED },
{ resource: unrelated, type: FileChangeType.UPDATED }, { resource: unrelated, type: FileChangeType.UPDATED },
@ -245,7 +245,7 @@ suite('Watcher Events Normalizer', () => {
const updated = URI.file('/users/data/src/related'); const updated = URI.file('/users/data/src/related');
const unrelated = URI.file('/users/data/src/unrelated'); const unrelated = URI.file('/users/data/src/unrelated');
const raw: IDiskFileChange[] = [ const raw: IFileChange[] = [
{ resource: created, type: FileChangeType.ADDED }, { resource: created, type: FileChangeType.ADDED },
{ resource: updated, type: FileChangeType.UPDATED }, { resource: updated, type: FileChangeType.UPDATED },
{ resource: unrelated, type: FileChangeType.UPDATED }, { resource: unrelated, type: FileChangeType.UPDATED },
@ -273,7 +273,7 @@ suite('Watcher Events Normalizer', () => {
const deleted = URI.file('/users/data/src/related'); const deleted = URI.file('/users/data/src/related');
const unrelated = URI.file('/users/data/src/unrelated'); const unrelated = URI.file('/users/data/src/unrelated');
const raw: IDiskFileChange[] = [ const raw: IFileChange[] = [
{ resource: updated, type: FileChangeType.UPDATED }, { resource: updated, type: FileChangeType.UPDATED },
{ resource: updated2, type: FileChangeType.UPDATED }, { resource: updated2, type: FileChangeType.UPDATED },
{ resource: unrelated, type: FileChangeType.UPDATED }, { resource: unrelated, type: FileChangeType.UPDATED },
@ -300,7 +300,7 @@ suite('Watcher Events Normalizer', () => {
const oldPath = URI.file('/users/data/src/added'); const oldPath = URI.file('/users/data/src/added');
const newPath = URI.file('/users/data/src/ADDED'); const newPath = URI.file('/users/data/src/ADDED');
const raw: IDiskFileChange[] = [ const raw: IFileChange[] = [
{ resource: newPath, type: FileChangeType.ADDED }, { resource: newPath, type: FileChangeType.ADDED },
{ resource: oldPath, type: FileChangeType.DELETED } { resource: oldPath, type: FileChangeType.DELETED }
]; ];

View file

@ -17,13 +17,16 @@ import { DeferredPromise } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher'; import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher';
import { FileAccess } from 'vs/base/common/network'; import { FileAccess } from 'vs/base/common/network';
import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { addUNCHostToAllowlist } from 'vs/base/node/unc';
// this suite has shown flaky runs in Azure pipelines where // this suite has shown flaky runs in Azure pipelines where
// tasks would just hang and timeout after a while (not in // tasks would just hang and timeout after a while (not in
// mocha but generally). as such they will run only on demand // mocha but generally). as such they will run only on demand
// whenever we update the watcher library. // whenever we update the watcher library.
((process.env['BUILD_SOURCEVERSION'] || process.env['CI']) ? suite.skip : flakySuite)('File Watcher (node.js)', () => { flakySuite('File Watcher (node.js)', () => {
class TestNodeJSWatcher extends NodeJSWatcher { class TestNodeJSWatcher extends NodeJSWatcher {
@ -105,16 +108,22 @@ import { FileAccess } from 'vs/base/common/network';
} }
} }
async function awaitEvent(service: TestNodeJSWatcher, path: string, type: FileChangeType): Promise<void> { async function awaitEvent(service: TestNodeJSWatcher, path: string, type: FileChangeType, correlationId?: number | null, expectedCount?: number): Promise<void> {
if (loggingEnabled) { if (loggingEnabled) {
console.log(`Awaiting change type '${toMsg(type)}' on file '${path}'`); console.log(`Awaiting change type '${toMsg(type)}' on file '${path}'`);
} }
// Await the event // Await the event
await new Promise<void>(resolve => { await new Promise<void>(resolve => {
let counter = 0;
const disposable = service.onDidChangeFile(events => { const disposable = service.onDidChangeFile(events => {
for (const event of events) { for (const event of events) {
if (event.resource.fsPath === path && event.type === type) { if (extUriBiasedIgnorePathCase.isEqual(event.resource, URI.file(path)) && event.type === type && (correlationId === null || event.cId === correlationId)) {
counter++;
if (typeof expectedCount === 'number' && counter < expectedCount) {
continue; // not yet
}
disposable.dispose(); disposable.dispose();
resolve(); resolve();
break; break;
@ -406,6 +415,13 @@ import { FileAccess } from 'vs/base/common/network';
return basicCrudTest(join(testDir, 'files-includes.txt')); return basicCrudTest(join(testDir, 'files-includes.txt'));
}); });
test('correlationId is supported', async function () {
const correlationId = Math.random();
await watcher.watch([{ correlationId, path: testDir, excludes: [], recursive: false }]);
return basicCrudTest(join(testDir, 'newFile.txt'), undefined, correlationId);
});
(isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (folder watch)', async function () { (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (folder watch)', async function () {
const link = join(testDir, 'deep-linked'); const link = join(testDir, 'deep-linked');
const linkTarget = join(testDir, 'deep'); const linkTarget = join(testDir, 'deep');
@ -416,23 +432,23 @@ import { FileAccess } from 'vs/base/common/network';
return basicCrudTest(join(link, 'newFile.txt')); return basicCrudTest(join(link, 'newFile.txt'));
}); });
async function basicCrudTest(filePath: string, skipAdd?: boolean): Promise<void> { async function basicCrudTest(filePath: string, skipAdd?: boolean, correlationId?: number | null, expectedCount?: number): Promise<void> {
let changeFuture: Promise<unknown>; let changeFuture: Promise<unknown>;
// New file // New file
if (!skipAdd) { if (!skipAdd) {
changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED); changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, correlationId, expectedCount);
await Promises.writeFile(filePath, 'Hello World'); await Promises.writeFile(filePath, 'Hello World');
await changeFuture; await changeFuture;
} }
// Change file // Change file
changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED); changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED, correlationId, expectedCount);
await Promises.writeFile(filePath, 'Hello Change'); await Promises.writeFile(filePath, 'Hello Change');
await changeFuture; await changeFuture;
// Delete file // Delete file
changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED); changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, correlationId, expectedCount);
await Promises.unlink(await Promises.realpath(filePath)); // support symlinks await Promises.unlink(await Promises.realpath(filePath)); // support symlinks
await changeFuture; await changeFuture;
} }
@ -448,6 +464,7 @@ import { FileAccess } from 'vs/base/common/network';
}); });
(!isWindows /* UNC is windows only */ ? test.skip : test)('unc support (folder watch)', async function () { (!isWindows /* UNC is windows only */ ? test.skip : test)('unc support (folder watch)', async function () {
addUNCHostToAllowlist('localhost');
// Local UNC paths are in the form of: \\localhost\c$\my_dir // Local UNC paths are in the form of: \\localhost\c$\my_dir
const uncPath = `\\\\localhost\\${getDriveLetter(testDir)?.toLowerCase()}$\\${ltrim(testDir.substr(testDir.indexOf(':') + 1), '\\')}`; const uncPath = `\\\\localhost\\${getDriveLetter(testDir)?.toLowerCase()}$\\${ltrim(testDir.substr(testDir.indexOf(':') + 1), '\\')}`;
@ -458,6 +475,7 @@ import { FileAccess } from 'vs/base/common/network';
}); });
(!isWindows /* UNC is windows only */ ? test.skip : test)('unc support (file watch)', async function () { (!isWindows /* UNC is windows only */ ? test.skip : test)('unc support (file watch)', async function () {
addUNCHostToAllowlist('localhost');
// Local UNC paths are in the form of: \\localhost\c$\my_dir // Local UNC paths are in the form of: \\localhost\c$\my_dir
const uncPath = `\\\\localhost\\${getDriveLetter(testDir)?.toLowerCase()}$\\${ltrim(testDir.substr(testDir.indexOf(':') + 1), '\\')}\\lorem.txt`; const uncPath = `\\\\localhost\\${getDriveLetter(testDir)?.toLowerCase()}$\\${ltrim(testDir.substr(testDir.indexOf(':') + 1), '\\')}\\lorem.txt`;
@ -528,4 +546,17 @@ import { FileAccess } from 'vs/base/common/network';
return watchPromise; return watchPromise;
}); });
test('watching same or overlapping paths supported when correlation is applied', async () => {
// same path, same options
await watcher.watch([
{ path: testDir, excludes: [], recursive: false, correlationId: 1 },
{ path: testDir, excludes: [], recursive: false, correlationId: 2, },
{ path: testDir, excludes: [], recursive: false, correlationId: undefined }
]);
await basicCrudTest(join(testDir, 'newFile.txt'), undefined, null, 3);
await basicCrudTest(join(testDir, 'otherNewFile.txt'), undefined, null, 3);
});
}); });

View file

@ -7,23 +7,26 @@ import * as assert from 'assert';
import { realpathSync } from 'fs'; import { realpathSync } from 'fs';
import { tmpdir } from 'os'; import { tmpdir } from 'os';
import { timeout } from 'vs/base/common/async'; import { timeout } from 'vs/base/common/async';
import { join } from 'vs/base/common/path'; import { dirname, join } from 'vs/base/common/path';
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { Promises, RimRafMode } from 'vs/base/node/pfs'; import { Promises, RimRafMode } from 'vs/base/node/pfs';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { FileChangeType } from 'vs/platform/files/common/files'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files';
import { ParcelWatcher } from 'vs/platform/files/node/watcher/parcel/parcelWatcher'; import { ParcelWatcher } from 'vs/platform/files/node/watcher/parcel/parcelWatcher';
import { IDiskFileChange, IRecursiveWatchRequest } from 'vs/platform/files/common/watcher'; import { IRecursiveWatchRequest } from 'vs/platform/files/common/watcher';
import { getDriveLetter } from 'vs/base/common/extpath'; import { getDriveLetter } from 'vs/base/common/extpath';
import { ltrim } from 'vs/base/common/strings'; import { ltrim } from 'vs/base/common/strings';
import { FileAccess } from 'vs/base/common/network'; import { FileAccess } from 'vs/base/common/network';
import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { addUNCHostToAllowlist } from 'vs/base/node/unc';
// this suite has shown flaky runs in Azure pipelines where // this suite has shown flaky runs in Azure pipelines where
// tasks would just hang and timeout after a while (not in // tasks would just hang and timeout after a while (not in
// mocha but generally). as such they will run only on demand // mocha but generally). as such they will run only on demand
// whenever we update the watcher library. // whenever we update the watcher library.
((process.env['BUILD_SOURCEVERSION'] || process.env['CI']) ? suite.skip : flakySuite)('File Watcher (parcel)', () => { flakySuite('File Watcher (parcel)', () => {
class TestParcelWatcher extends ParcelWatcher { class TestParcelWatcher extends ParcelWatcher {
@ -103,16 +106,22 @@ import { FileAccess } from 'vs/base/common/network';
} }
} }
async function awaitEvent(service: TestParcelWatcher, path: string, type: FileChangeType, failOnEventReason?: string): Promise<IDiskFileChange[]> { async function awaitEvent(watcher: TestParcelWatcher, path: string, type: FileChangeType, failOnEventReason?: string, correlationId?: number | null, expectedCount?: number): Promise<IFileChange[]> {
if (loggingEnabled) { if (loggingEnabled) {
console.log(`Awaiting change type '${toMsg(type)}' on file '${path}'`); console.log(`Awaiting change type '${toMsg(type)}' on file '${path}'`);
} }
// Await the event // Await the event
const res = await new Promise<IDiskFileChange[]>((resolve, reject) => { const res = await new Promise<IFileChange[]>((resolve, reject) => {
const disposable = service.onDidChangeFile(events => { let counter = 0;
const disposable = watcher.onDidChangeFile(events => {
for (const event of events) { for (const event of events) {
if (event.resource.fsPath === path && event.type === type) { if (extUriBiasedIgnorePathCase.isEqual(event.resource, URI.file(path)) && event.type === type && (correlationId === null || event.cId === correlationId)) {
counter++;
if (typeof expectedCount === 'number' && counter < expectedCount) {
continue; // not yet
}
disposable.dispose(); disposable.dispose();
if (failOnEventReason) { if (failOnEventReason) {
reject(new Error(`Unexpected file event: ${failOnEventReason}`)); reject(new Error(`Unexpected file event: ${failOnEventReason}`));
@ -134,14 +143,14 @@ import { FileAccess } from 'vs/base/common/network';
return res; return res;
} }
function awaitMessage(service: TestParcelWatcher, type: 'trace' | 'warn' | 'error' | 'info' | 'debug'): Promise<void> { function awaitMessage(watcher: TestParcelWatcher, type: 'trace' | 'warn' | 'error' | 'info' | 'debug'): Promise<void> {
if (loggingEnabled) { if (loggingEnabled) {
console.log(`Awaiting message of type ${type}`); console.log(`Awaiting message of type ${type}`);
} }
// Await the message // Await the message
return new Promise<void>(resolve => { return new Promise<void>(resolve => {
const disposable = service.onDidLogMessage(msg => { const disposable = watcher.onDidLogMessage(msg => {
if (msg.type === type) { if (msg.type === type) {
disposable.dispose(); disposable.dispose();
resolve(); resolve();
@ -287,20 +296,20 @@ import { FileAccess } from 'vs/base/common/network';
return basicCrudTest(join(testDir, 'deep', 'newFile.txt')); return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));
}); });
async function basicCrudTest(filePath: string): Promise<void> { async function basicCrudTest(filePath: string, correlationId?: number | null, expectedCount?: number): Promise<void> {
// New file // New file
let changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED); let changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, undefined, correlationId, expectedCount);
await Promises.writeFile(filePath, 'Hello World'); await Promises.writeFile(filePath, 'Hello World');
await changeFuture; await changeFuture;
// Change file // Change file
changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED); changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED, undefined, correlationId, expectedCount);
await Promises.writeFile(filePath, 'Hello Change'); await Promises.writeFile(filePath, 'Hello Change');
await changeFuture; await changeFuture;
// Delete file // Delete file
changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED); changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, undefined, correlationId, expectedCount);
await Promises.unlink(filePath); await Promises.unlink(filePath);
await changeFuture; await changeFuture;
} }
@ -509,6 +518,7 @@ import { FileAccess } from 'vs/base/common/network';
}); });
(!isWindows /* UNC is windows only */ ? test.skip : test)('unc support', async function () { (!isWindows /* UNC is windows only */ ? test.skip : test)('unc support', async function () {
addUNCHostToAllowlist('localhost');
// Local UNC paths are in the form of: \\localhost\c$\my_dir // Local UNC paths are in the form of: \\localhost\c$\my_dir
const uncPath = `\\\\localhost\\${getDriveLetter(testDir)?.toLowerCase()}$\\${ltrim(testDir.substr(testDir.indexOf(':') + 1), '\\')}`; const uncPath = `\\\\localhost\\${getDriveLetter(testDir)?.toLowerCase()}$\\${ltrim(testDir.substr(testDir.indexOf(':') + 1), '\\')}`;
@ -532,7 +542,7 @@ import { FileAccess } from 'vs/base/common/network';
await watcher.watch([{ path: invalidPath, excludes: [], recursive: true }]); await watcher.watch([{ path: invalidPath, excludes: [], recursive: true }]);
}); });
test('deleting watched path is handled properly', async function () { (isWindows /* flaky on windows */ ? test.skip : test)('deleting watched path is handled properly', async function () {
const watchedPath = join(testDir, 'deep'); const watchedPath = join(testDir, 'deep');
await watcher.watch([{ path: watchedPath, excludes: [], recursive: true }]); await watcher.watch([{ path: watchedPath, excludes: [], recursive: true }]);
@ -555,6 +565,13 @@ import { FileAccess } from 'vs/base/common/network';
await changeFuture; await changeFuture;
}); });
test('correlationId is supported', async function () {
const correlationId = Math.random();
await watcher.watch([{ correlationId, path: testDir, excludes: [], recursive: true }]);
return basicCrudTest(join(testDir, 'newFile.txt'), correlationId);
});
test('should not exclude roots that do not overlap', () => { test('should not exclude roots that do not overlap', () => {
if (isWindows) { if (isWindows) {
assert.deepStrictEqual(watcher.testNormalizePaths(['C:\\a']), ['C:\\a']); assert.deepStrictEqual(watcher.testNormalizePaths(['C:\\a']), ['C:\\a']);
@ -584,4 +601,49 @@ import { FileAccess } from 'vs/base/common/network';
test('should ignore when everything excluded', () => { test('should ignore when everything excluded', () => {
assert.deepStrictEqual(watcher.testNormalizePaths(['/foo/bar', '/bar'], ['**', 'something']), []); assert.deepStrictEqual(watcher.testNormalizePaths(['/foo/bar', '/bar'], ['**', 'something']), []);
}); });
test('watching same or overlapping paths supported when correlation is applied', async () => {
// same path, same options
await watcher.watch([
{ path: testDir, excludes: [], recursive: true, correlationId: 1 },
{ path: testDir, excludes: [], recursive: true, correlationId: 2, },
{ path: testDir, excludes: [], recursive: true, correlationId: undefined }
]);
await basicCrudTest(join(testDir, 'newFile.txt'), null, 3);
await basicCrudTest(join(testDir, 'otherNewFile.txt'), null, 3);
// same path, different options
await watcher.watch([
{ path: testDir, excludes: [], recursive: true, correlationId: 1 },
{ path: testDir, excludes: [], recursive: true, correlationId: 2 },
{ path: testDir, excludes: [], recursive: true, correlationId: undefined },
{ path: testDir, excludes: [join(realpathSync(testDir), 'deep')], recursive: true, correlationId: 3 },
{ path: testDir, excludes: [join(realpathSync(testDir), 'other')], recursive: true, correlationId: 4 },
]);
await basicCrudTest(join(testDir, 'newFile.txt'), null, 5);
await basicCrudTest(join(testDir, 'otherNewFile.txt'), null, 5);
// overlapping paths (same options)
await watcher.watch([
{ path: dirname(testDir), excludes: [], recursive: true, correlationId: 1 },
{ path: testDir, excludes: [], recursive: true, correlationId: 2 },
{ path: join(testDir, 'deep'), excludes: [], recursive: true, correlationId: 3 },
]);
await basicCrudTest(join(testDir, 'deep', 'newFile.txt'), null, 3);
await basicCrudTest(join(testDir, 'deep', 'otherNewFile.txt'), null, 3);
// overlapping paths (different options)
await watcher.watch([
{ path: dirname(testDir), excludes: [], recursive: true, correlationId: 1 },
{ path: testDir, excludes: [join(realpathSync(testDir), 'some')], recursive: true, correlationId: 2 },
{ path: join(testDir, 'deep'), excludes: [join(realpathSync(testDir), 'other')], recursive: true, correlationId: 3 },
]);
await basicCrudTest(join(testDir, 'deep', 'newFile.txt'), null, 3);
await basicCrudTest(join(testDir, 'deep', 'otherNewFile.txt'), null, 3);
});
}); });

View file

@ -156,7 +156,8 @@ export class FileUserDataProvider extends Disposable implements
if (this.watchResources.findSubstr(userDataResource)) { if (this.watchResources.findSubstr(userDataResource)) {
userDataChanges.push({ userDataChanges.push({
resource: userDataResource, resource: userDataResource,
type: change.type type: change.type,
cId: change.cId
}); });
} }
} }

View file

@ -6,17 +6,10 @@
import { Emitter, Event } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable, toDisposable, DisposableStore, DisposableMap } from 'vs/base/common/lifecycle'; import { IDisposable, toDisposable, DisposableStore, DisposableMap } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri'; import { URI, UriComponents } from 'vs/base/common/uri';
import { IFileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IStat, IWatchOptions, FileType, IFileOverwriteOptions, IFileDeleteOptions, IFileOpenOptions, FileOperationError, FileOperationResult, FileSystemProviderErrorCode, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileFolderCopyCapability, FilePermission, toFileSystemProviderErrorCode, IFilesConfiguration, IFileStatWithPartialMetadata, IFileStat } from 'vs/platform/files/common/files'; import { IFileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IStat, IWatchOptions, FileType, IFileOverwriteOptions, IFileDeleteOptions, IFileOpenOptions, FileOperationError, FileOperationResult, FileSystemProviderErrorCode, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileFolderCopyCapability, FilePermission, toFileSystemProviderErrorCode, IFileStatWithPartialMetadata, IFileStat } from 'vs/platform/files/common/files';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { ExtHostContext, ExtHostFileSystemShape, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../common/extHost.protocol'; import { ExtHostContext, ExtHostFileSystemShape, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../common/extHost.protocol';
import { VSBuffer } from 'vs/base/common/buffer'; import { VSBuffer } from 'vs/base/common/buffer';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ILogService } from 'vs/platform/log/common/log';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files';
import { normalizeWatcherPattern } from 'vs/platform/files/common/watcher';
import { GLOBSTAR } from 'vs/base/common/glob';
import { rtrim } from 'vs/base/common/strings';
import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IMarkdownString } from 'vs/base/common/htmlContent';
@extHostNamedCustomer(MainContext.MainThreadFileSystem) @extHostNamedCustomer(MainContext.MainThreadFileSystem)
@ -25,14 +18,10 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape {
private readonly _proxy: ExtHostFileSystemShape; private readonly _proxy: ExtHostFileSystemShape;
private readonly _fileProvider = new DisposableMap<number, RemoteFileSystemProvider>(); private readonly _fileProvider = new DisposableMap<number, RemoteFileSystemProvider>();
private readonly _disposables = new DisposableStore(); private readonly _disposables = new DisposableStore();
private readonly _watches = new DisposableMap<number>();
constructor( constructor(
extHostContext: IExtHostContext, extHostContext: IExtHostContext,
@IWorkbenchFileService private readonly _fileService: IWorkbenchFileService, @IFileService private readonly _fileService: IFileService
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
@ILogService private readonly _logService: ILogService,
@IConfigurationService private readonly _configurationService: IConfigurationService
) { ) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystem); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystem);
@ -48,7 +37,6 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape {
dispose(): void { dispose(): void {
this._disposables.dispose(); this._disposables.dispose();
this._fileProvider.dispose(); this._fileProvider.dispose();
this._watches.dispose();
} }
async $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities, readonlyMessage?: IMarkdownString): Promise<void> { async $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities, readonlyMessage?: IMarkdownString): Promise<void> {
@ -165,99 +153,7 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape {
return this._fileService.activateProvider(scheme); return this._fileService.activateProvider(scheme);
} }
async $watch(extensionId: string, session: number, resource: UriComponents, unvalidatedOpts: IWatchOptions): Promise<void> {
const uri = URI.revive(resource);
const workspaceFolder = this._contextService.getWorkspaceFolder(uri);
const opts = { ...unvalidatedOpts };
// Convert a recursive watcher to a flat watcher if the path
// turns out to not be a folder. Recursive watching is only
// possible on folders, so we help all file watchers by checking
// early.
if (opts.recursive) {
try {
const stat = await this._fileService.stat(uri);
if (!stat.isDirectory) {
opts.recursive = false;
}
} catch (error) {
this._logService.error(`MainThreadFileSystem#$watch(): failed to stat a resource for file watching (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session}): ${error}`);
}
}
// Refuse to watch anything that is already watched via
// our workspace watchers in case the request is a
// recursive file watcher.
// Still allow for non-recursive watch requests as a way
// to bypass configured exclude rules though
// (see https://github.com/microsoft/vscode/issues/146066)
if (workspaceFolder && opts.recursive) {
this._logService.trace(`MainThreadFileSystem#$watch(): ignoring request to start watching because path is inside workspace (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`);
return;
}
this._logService.trace(`MainThreadFileSystem#$watch(): request to start watching (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`);
// Automatically add `files.watcherExclude` patterns when watching
// recursively to give users a chance to configure exclude rules
// for reducing the overhead of watching recursively
if (opts.recursive) {
const config = this._configurationService.getValue<IFilesConfiguration>();
if (config.files?.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (config.files.watcherExclude[key] === true) {
opts.excludes.push(key);
}
}
}
}
// Non-recursive watching inside the workspace will overlap with
// our standard workspace watchers. To prevent duplicate events,
// we only want to include events for files that are otherwise
// excluded via `files.watcherExclude`. As such, we configure
// to include each configured exclude pattern so that only those
// events are reported that are otherwise excluded.
// However, we cannot just use the pattern as is, because a pattern
// such as `bar` for a exclude, will work to exclude any of
// `<workspace path>/bar` but will not work as include for files within
// `bar` unless a suffix of `/**` if added.
// (https://github.com/microsoft/vscode/issues/148245)
else if (workspaceFolder) {
const config = this._configurationService.getValue<IFilesConfiguration>();
if (config.files?.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (config.files.watcherExclude[key] === true) {
if (!opts.includes) {
opts.includes = [];
}
const includePattern = `${rtrim(key, '/')}/${GLOBSTAR}`;
opts.includes.push(normalizeWatcherPattern(workspaceFolder.uri.fsPath, includePattern));
}
}
}
// Still ignore watch request if there are actually no configured
// exclude rules, because in that case our default recursive watcher
// should be able to take care of all events.
if (!opts.includes || opts.includes.length === 0) {
this._logService.trace(`MainThreadFileSystem#$watch(): ignoring request to start watching because path is inside workspace and no excludes are configured (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`);
return;
}
}
const subscription = this._fileService.watch(uri, opts);
this._watches.set(session, subscription);
}
$unwatch(session: number): void {
if (this._watches.has(session)) {
this._logService.trace(`MainThreadFileSystem#$unwatch(): request to stop watching (session: ${session})`);
this._watches.deleteAndDispose(session);
}
}
} }
class RemoteFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileFolderCopyCapability { class RemoteFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileFolderCopyCapability {

View file

@ -3,10 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { DisposableStore } from 'vs/base/common/lifecycle'; import { DisposableMap, DisposableStore } from 'vs/base/common/lifecycle';
import { FileOperation, IFileService } from 'vs/platform/files/common/files'; import { FileOperation, IFileService, IFilesConfiguration, IWatchOptions } from 'vs/platform/files/common/files';
import { extHostCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { ExtHostContext } from '../common/extHost.protocol'; import { ExtHostContext, ExtHostFileSystemEventServiceShape, MainContext, MainThreadFileSystemEventServiceShape } from '../common/extHost.protocol';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { IWorkingCopyFileOperationParticipant, IWorkingCopyFileService, SourceTargetPair, IFileOperationUndoRedoInfo } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkingCopyFileOperationParticipant, IWorkingCopyFileService, SourceTargetPair, IFileOperationUndoRedoInfo } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
@ -22,17 +22,26 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { reviveWorkspaceEditDto } from 'vs/workbench/api/browser/mainThreadBulkEdits'; import { reviveWorkspaceEditDto } from 'vs/workbench/api/browser/mainThreadBulkEdits';
import { GLOBSTAR } from 'vs/base/common/glob';
import { rtrim } from 'vs/base/common/strings';
import { UriComponents, URI } from 'vs/base/common/uri';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { normalizeWatcherPattern } from 'vs/platform/files/common/watcher';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
@extHostCustomer @extHostNamedCustomer(MainContext.MainThreadFileSystemEventService)
export class MainThreadFileSystemEventService { export class MainThreadFileSystemEventService implements MainThreadFileSystemEventServiceShape {
static readonly MementoKeyAdditionalEdits = `file.particpants.additionalEdits`; static readonly MementoKeyAdditionalEdits = `file.particpants.additionalEdits`;
private readonly _proxy: ExtHostFileSystemEventServiceShape;
private readonly _listener = new DisposableStore(); private readonly _listener = new DisposableStore();
private readonly _watches = new DisposableMap<number>();
constructor( constructor(
extHostContext: IExtHostContext, extHostContext: IExtHostContext,
@IFileService fileService: IFileService, @IFileService private readonly _fileService: IFileService,
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,
@IBulkEditService bulkEditService: IBulkEditService, @IBulkEditService bulkEditService: IBulkEditService,
@IProgressService progressService: IProgressService, @IProgressService progressService: IProgressService,
@ -40,19 +49,22 @@ export class MainThreadFileSystemEventService {
@IStorageService storageService: IStorageService, @IStorageService storageService: IStorageService,
@ILogService logService: ILogService, @ILogService logService: ILogService,
@IEnvironmentService envService: IEnvironmentService, @IEnvironmentService envService: IEnvironmentService,
@IUriIdentityService uriIdentService: IUriIdentityService @IUriIdentityService uriIdentService: IUriIdentityService,
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
@ILogService private readonly _logService: ILogService,
@IConfigurationService private readonly _configurationService: IConfigurationService
) { ) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService);
const proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService); this._listener.add(_fileService.onDidFilesChange(event => {
this._proxy.$onFileEvent({
this._listener.add(fileService.onDidFilesChange(event => {
proxy.$onFileEvent({
created: event.rawAdded, created: event.rawAdded,
changed: event.rawUpdated, changed: event.rawUpdated,
deleted: event.rawDeleted deleted: event.rawDeleted
}); });
})); }));
const that = this;
const fileOperationParticipant = new class implements IWorkingCopyFileOperationParticipant { const fileOperationParticipant = new class implements IWorkingCopyFileOperationParticipant {
async participate(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, timeout: number, token: CancellationToken) { async participate(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, timeout: number, token: CancellationToken) {
if (undoInfo?.isUndoing) { if (undoInfo?.isUndoing) {
@ -69,7 +81,7 @@ export class MainThreadFileSystemEventService {
delay: Math.min(timeout / 2, 3000) delay: Math.min(timeout / 2, 3000)
}, () => { }, () => {
// race extension host event delivery against timeout AND user-cancel // race extension host event delivery against timeout AND user-cancel
const onWillEvent = proxy.$onWillRunFileOperation(operation, files, timeout, cts.token); const onWillEvent = that._proxy.$onWillRunFileOperation(operation, files, timeout, cts.token);
return raceCancellation(onWillEvent, cts.token); return raceCancellation(onWillEvent, cts.token);
}, () => { }, () => {
// user-cancel // user-cancel
@ -197,11 +209,132 @@ export class MainThreadFileSystemEventService {
this._listener.add(workingCopyFileService.addFileOperationParticipant(fileOperationParticipant)); this._listener.add(workingCopyFileService.addFileOperationParticipant(fileOperationParticipant));
// AFTER file operation // AFTER file operation
this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => proxy.$onDidRunFileOperation(e.operation, e.files))); this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => this._proxy.$onDidRunFileOperation(e.operation, e.files)));
}
async $watch(extensionId: string, session: number, resource: UriComponents, unvalidatedOpts: IWatchOptions): Promise<void> {
const uri = URI.revive(resource);
const correlate = Array.isArray(unvalidatedOpts?.excludes) && unvalidatedOpts.excludes.length > 0; // TODO@bpasero for now only correlate proposed new file system watcher API with excludes
const opts: IWatchOptions = {
...unvalidatedOpts
};
// Convert a recursive watcher to a flat watcher if the path
// turns out to not be a folder. Recursive watching is only
// possible on folders, so we help all file watchers by checking
// early.
if (opts.recursive) {
try {
const stat = await this._fileService.stat(uri);
if (!stat.isDirectory) {
opts.recursive = false;
}
} catch (error) {
this._logService.error(`MainThreadFileSystemEventService#$watch(): failed to stat a resource for file watching (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session}): ${error}`);
}
}
// Correlated file watching is taken as is
if (correlate) {
this._logService.trace(`MainThreadFileSystemEventService#$watch(): request to start watching correlated (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`);
const watcherDisposables = new DisposableStore();
const subscription = watcherDisposables.add(this._fileService.createWatcher(uri, opts));
watcherDisposables.add(subscription.onDidChange(event => {
this._proxy.$onFileEvent({
session,
created: event.rawAdded,
changed: event.rawUpdated,
deleted: event.rawDeleted
});
}));
this._watches.set(session, watcherDisposables);
}
// Uncorrelated file watching gets special treatment
else {
// Refuse to watch anything that is already watched via
// our workspace watchers in case the request is a
// recursive file watcher and does not opt-in to event
// correlation via specific exclude rules.
// Still allow for non-recursive watch requests as a way
// to bypass configured exclude rules though
// (see https://github.com/microsoft/vscode/issues/146066)
const workspaceFolder = this._contextService.getWorkspaceFolder(uri);
if (workspaceFolder && opts.recursive) {
this._logService.trace(`MainThreadFileSystemEventService#$watch(): ignoring request to start watching because path is inside workspace (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`);
return;
}
this._logService.trace(`MainThreadFileSystemEventService#$watch(): request to start watching uncorrelated (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`);
// Automatically add `files.watcherExclude` patterns when watching
// recursively to give users a chance to configure exclude rules
// for reducing the overhead of watching recursively
if (opts.recursive && opts.excludes.length === 0) {
const config = this._configurationService.getValue<IFilesConfiguration>();
if (config.files?.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (config.files.watcherExclude[key] === true) {
opts.excludes.push(key);
}
}
}
}
// Non-recursive watching inside the workspace will overlap with
// our standard workspace watchers. To prevent duplicate events,
// we only want to include events for files that are otherwise
// excluded via `files.watcherExclude`. As such, we configure
// to include each configured exclude pattern so that only those
// events are reported that are otherwise excluded.
// However, we cannot just use the pattern as is, because a pattern
// such as `bar` for a exclude, will work to exclude any of
// `<workspace path>/bar` but will not work as include for files within
// `bar` unless a suffix of `/**` if added.
// (https://github.com/microsoft/vscode/issues/148245)
else if (workspaceFolder) {
const config = this._configurationService.getValue<IFilesConfiguration>();
if (config.files?.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (config.files.watcherExclude[key] === true) {
if (!opts.includes) {
opts.includes = [];
}
const includePattern = `${rtrim(key, '/')}/${GLOBSTAR}`;
opts.includes.push(normalizeWatcherPattern(workspaceFolder.uri.fsPath, includePattern));
}
}
}
// Still ignore watch request if there are actually no configured
// exclude rules, because in that case our default recursive watcher
// should be able to take care of all events.
if (!opts.includes || opts.includes.length === 0) {
this._logService.trace(`MainThreadFileSystemEventService#$watch(): ignoring request to start watching because path is inside workspace and no excludes are configured (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`);
return;
}
}
const subscription = this._fileService.watch(uri, opts);
this._watches.set(session, subscription);
}
}
$unwatch(session: number): void {
if (this._watches.has(session)) {
this._logService.trace(`MainThreadFileSystemEventService#$unwatch(): request to stop watching (session: ${session})`);
this._watches.deleteAndDispose(session);
}
} }
dispose(): void { dispose(): void {
this._listener.dispose(); this._listener.dispose();
this._watches.dispose();
} }
} }

View file

@ -938,8 +938,21 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
applyEdit(edit: vscode.WorkspaceEdit, metadata?: vscode.WorkspaceEditMetadata): Thenable<boolean> { applyEdit(edit: vscode.WorkspaceEdit, metadata?: vscode.WorkspaceEditMetadata): Thenable<boolean> {
return extHostBulkEdits.applyWorkspaceEdit(edit, extension, metadata); return extHostBulkEdits.applyWorkspaceEdit(edit, extension, metadata);
}, },
createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): vscode.FileSystemWatcher => { createFileSystemWatcher: (pattern, optionsOrIgnoreCreate, ignoreChange?, ignoreDelete?): vscode.FileSystemWatcher => {
return extHostFileSystemEvent.createFileSystemWatcher(extHostWorkspace, extension, pattern, ignoreCreate, ignoreChange, ignoreDelete); let options: vscode.FileSystemWatcherOptions | undefined = undefined;
if (typeof optionsOrIgnoreCreate === 'boolean') {
options = {
ignoreCreateEvents: Boolean(optionsOrIgnoreCreate),
ignoreChangeEvents: Boolean(ignoreChange),
ignoreDeleteEvents: Boolean(ignoreDelete)
};
} else if (optionsOrIgnoreCreate) {
checkProposedApiEnabled(extension, 'createFileSystemWatcher');
options = optionsOrIgnoreCreate;
}
return extHostFileSystemEvent.createFileSystemWatcher(extHostWorkspace, extension, pattern, options);
}, },
get textDocuments() { get textDocuments() {
return extHostDocuments.getAllDocumentData().map(data => data.document); return extHostDocuments.getAllDocumentData().map(data => data.document);

View file

@ -1310,10 +1310,12 @@ export interface MainThreadFileSystemShape extends IDisposable {
$mkdir(resource: UriComponents): Promise<void>; $mkdir(resource: UriComponents): Promise<void>;
$delete(resource: UriComponents, opts: files.IFileDeleteOptions): Promise<void>; $delete(resource: UriComponents, opts: files.IFileDeleteOptions): Promise<void>;
$ensureActivation(scheme: string): Promise<void>;
}
export interface MainThreadFileSystemEventServiceShape extends IDisposable {
$watch(extensionId: string, session: number, resource: UriComponents, opts: files.IWatchOptions): void; $watch(extensionId: string, session: number, resource: UriComponents, opts: files.IWatchOptions): void;
$unwatch(session: number): void; $unwatch(session: number): void;
$ensureActivation(scheme: string): Promise<void>;
} }
export interface MainThreadLabelServiceShape extends IDisposable { export interface MainThreadLabelServiceShape extends IDisposable {
@ -1762,6 +1764,7 @@ export interface ExtHostExtensionServiceShape {
} }
export interface FileSystemEvents { export interface FileSystemEvents {
session?: number;
created: UriComponents[]; created: UriComponents[];
changed: UriComponents[]; changed: UriComponents[];
deleted: UriComponents[]; deleted: UriComponents[];
@ -2700,6 +2703,7 @@ export const MainContext = {
MainThreadProfileContentHandlers: createProxyIdentifier<MainThreadProfileContentHandlersShape>('MainThreadProfileContentHandlers'), MainThreadProfileContentHandlers: createProxyIdentifier<MainThreadProfileContentHandlersShape>('MainThreadProfileContentHandlers'),
MainThreadWorkspace: createProxyIdentifier<MainThreadWorkspaceShape>('MainThreadWorkspace'), MainThreadWorkspace: createProxyIdentifier<MainThreadWorkspaceShape>('MainThreadWorkspace'),
MainThreadFileSystem: createProxyIdentifier<MainThreadFileSystemShape>('MainThreadFileSystem'), MainThreadFileSystem: createProxyIdentifier<MainThreadFileSystemShape>('MainThreadFileSystem'),
MainThreadFileSystemEventService: createProxyIdentifier<MainThreadFileSystemEventServiceShape>('MainThreadFileSystemEventService'),
MainThreadExtensionService: createProxyIdentifier<MainThreadExtensionServiceShape>('MainThreadExtensionService'), MainThreadExtensionService: createProxyIdentifier<MainThreadExtensionServiceShape>('MainThreadExtensionService'),
MainThreadSCM: createProxyIdentifier<MainThreadSCMShape>('MainThreadSCM'), MainThreadSCM: createProxyIdentifier<MainThreadSCMShape>('MainThreadSCM'),
MainThreadSearch: createProxyIdentifier<MainThreadSearchShape>('MainThreadSearch'), MainThreadSearch: createProxyIdentifier<MainThreadSearchShape>('MainThreadSearch'),

View file

@ -18,8 +18,18 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { Lazy } from 'vs/base/common/lazy'; import { Lazy } from 'vs/base/common/lazy';
interface FileSystemWatcherCreateOptions {
readonly ignoreCreateEvents?: boolean;
readonly ignoreChangeEvents?: boolean;
readonly ignoreDeleteEvents?: boolean;
readonly excludes?: string[];
}
class FileSystemWatcher implements vscode.FileSystemWatcher { class FileSystemWatcher implements vscode.FileSystemWatcher {
private readonly session = Math.random();
private readonly _onDidCreate = new Emitter<vscode.Uri>(); private readonly _onDidCreate = new Emitter<vscode.Uri>();
private readonly _onDidChange = new Emitter<vscode.Uri>(); private readonly _onDidChange = new Emitter<vscode.Uri>();
private readonly _onDidDelete = new Emitter<vscode.Uri>(); private readonly _onDidDelete = new Emitter<vscode.Uri>();
@ -39,17 +49,15 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
return Boolean(this._config & 0b100); return Boolean(this._config & 0b100);
} }
constructor(mainContext: IMainContext, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event<FileSystemEvents>, globPattern: string | IRelativePatternDto, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean) { constructor(mainContext: IMainContext, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event<FileSystemEvents>, globPattern: string | IRelativePatternDto, options?: FileSystemWatcherCreateOptions) {
const watcherDisposable = this.ensureWatching(mainContext, extension, globPattern);
this._config = 0; this._config = 0;
if (ignoreCreateEvents) { if (options?.ignoreCreateEvents) {
this._config += 0b001; this._config += 0b001;
} }
if (ignoreChangeEvents) { if (options?.ignoreChangeEvents) {
this._config += 0b010; this._config += 0b010;
} }
if (ignoreDeleteEvents) { if (options?.ignoreDeleteEvents) {
this._config += 0b100; this._config += 0b100;
} }
@ -63,7 +71,11 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
const excludeOutOfWorkspaceEvents = typeof globPattern === 'string'; const excludeOutOfWorkspaceEvents = typeof globPattern === 'string';
const subscription = dispatcher(events => { const subscription = dispatcher(events => {
if (!ignoreCreateEvents) { if (typeof events.session === 'number' && events.session !== this.session) {
return; // ignore events from other file watchers
}
if (!options?.ignoreCreateEvents) {
for (const created of events.created) { for (const created of events.created) {
const uri = URI.revive(created); const uri = URI.revive(created);
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) { if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
@ -71,7 +83,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
} }
} }
} }
if (!ignoreChangeEvents) { if (!options?.ignoreChangeEvents) {
for (const changed of events.changed) { for (const changed of events.changed) {
const uri = URI.revive(changed); const uri = URI.revive(changed);
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) { if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
@ -79,7 +91,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
} }
} }
} }
if (!ignoreDeleteEvents) { if (!options?.ignoreDeleteEvents) {
for (const deleted of events.deleted) { for (const deleted of events.deleted) {
const uri = URI.revive(deleted); const uri = URI.revive(deleted);
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) { if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
@ -89,27 +101,26 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
} }
}); });
this._disposable = Disposable.from(watcherDisposable, this._onDidCreate, this._onDidChange, this._onDidDelete, subscription); this._disposable = Disposable.from(this.ensureWatching(mainContext, extension, globPattern, options), this._onDidCreate, this._onDidChange, this._onDidDelete, subscription);
} }
private ensureWatching(mainContext: IMainContext, extension: IExtensionDescription, globPattern: string | IRelativePatternDto): Disposable { private ensureWatching(mainContext: IMainContext, extension: IExtensionDescription, globPattern: string | IRelativePatternDto, options?: FileSystemWatcherCreateOptions): Disposable {
const disposable = Disposable.from(); const disposable = Disposable.from();
if (typeof globPattern === 'string') { if (typeof globPattern === 'string') {
return disposable; // a pattern alone does not carry sufficient information to start watching anything return disposable; // workspace is already watched by default, no need to watch again!
} }
const proxy = mainContext.getProxy(MainContext.MainThreadFileSystem); const proxy = mainContext.getProxy(MainContext.MainThreadFileSystemEventService);
let recursive = false; let recursive = false;
if (globPattern.pattern.includes(GLOBSTAR) || globPattern.pattern.includes(GLOB_SPLIT)) { if (globPattern.pattern.includes(GLOBSTAR) || globPattern.pattern.includes(GLOB_SPLIT)) {
recursive = true; // only watch recursively if pattern indicates the need for it recursive = true; // only watch recursively if pattern indicates the need for it
} }
const session = Math.random(); proxy.$watch(extension.identifier.value, this.session, globPattern.baseUri, { recursive, excludes: options?.excludes ?? [] });
proxy.$watch(extension.identifier.value, session, globPattern.baseUri, { recursive, excludes: [] /* excludes are not yet surfaced in the API */ });
return Disposable.from({ dispose: () => proxy.$unwatch(session) }); return Disposable.from({ dispose: () => proxy.$unwatch(this.session) });
} }
dispose() { dispose() {
@ -138,6 +149,8 @@ class LazyRevivedFileSystemEvents implements FileSystemEvents {
constructor(private readonly _events: FileSystemEvents) { } constructor(private readonly _events: FileSystemEvents) { }
readonly session = this._events.session;
private _created = new Lazy(() => this._events.created.map(URI.revive) as URI[]); private _created = new Lazy(() => this._events.created.map(URI.revive) as URI[]);
get created(): URI[] { return this._created.value; } get created(): URI[] { return this._created.value; }
@ -173,15 +186,14 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
//--- file events //--- file events
createFileSystemWatcher(workspace: IExtHostWorkspace, extension: IExtensionDescription, globPattern: vscode.GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): vscode.FileSystemWatcher { createFileSystemWatcher(workspace: IExtHostWorkspace, extension: IExtensionDescription, globPattern: vscode.GlobPattern, options?: FileSystemWatcherCreateOptions): vscode.FileSystemWatcher {
return new FileSystemWatcher(this._mainContext, workspace, extension, this._onFileSystemEvent.event, typeConverter.GlobPattern.from(globPattern), ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); return new FileSystemWatcher(this._mainContext, workspace, extension, this._onFileSystemEvent.event, typeConverter.GlobPattern.from(globPattern), options);
} }
$onFileEvent(events: FileSystemEvents) { $onFileEvent(events: FileSystemEvents) {
this._onFileSystemEvent.fire(new LazyRevivedFileSystemEvents(events)); this._onFileSystemEvent.fire(new LazyRevivedFileSystemEvents(events));
} }
//--- file operations //--- file operations
$onDidRunFileOperation(operation: FileOperation, files: SourceTargetPair[]): void { $onDidRunFileOperation(operation: FileOperation, files: SourceTargetPair[]): void {

View file

@ -22,13 +22,13 @@ suite('ExtHostFileSystemEventService', () => {
drain: undefined! drain: undefined!
}; };
const watcher1 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher(undefined!, undefined!, '**/somethingInteresting', false, false, false); const watcher1 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher(undefined!, undefined!, '**/somethingInteresting', {});
assert.strictEqual(watcher1.ignoreChangeEvents, false); assert.strictEqual(watcher1.ignoreChangeEvents, false);
assert.strictEqual(watcher1.ignoreCreateEvents, false); assert.strictEqual(watcher1.ignoreCreateEvents, false);
assert.strictEqual(watcher1.ignoreDeleteEvents, false); assert.strictEqual(watcher1.ignoreDeleteEvents, false);
watcher1.dispose(); watcher1.dispose();
const watcher2 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher(undefined!, undefined!, '**/somethingBoring', true, true, true); const watcher2 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher(undefined!, undefined!, '**/somethingBoring', { ignoreCreateEvents: true, ignoreChangeEvents: true, ignoreDeleteEvents: true });
assert.strictEqual(watcher2.ignoreChangeEvents, true); assert.strictEqual(watcher2.ignoreChangeEvents, true);
assert.strictEqual(watcher2.ignoreCreateEvents, true); assert.strictEqual(watcher2.ignoreCreateEvents, true);
assert.strictEqual(watcher2.ignoreDeleteEvents, true); assert.strictEqual(watcher2.ignoreDeleteEvents, true);

View file

@ -20,7 +20,7 @@ import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteA
import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService'; import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService';
import { IRemoteAuthorityResolverService, RemoteConnectionType } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRemoteAuthorityResolverService, RemoteConnectionType } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files'; import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService'; import { FileService } from 'vs/platform/files/common/fileService';
import { Schemas, connectionTokenCookieName } from 'vs/base/common/network'; import { Schemas, connectionTokenCookieName } from 'vs/base/common/network';
import { IAnyWorkspaceIdentifier, IWorkspaceContextService, UNKNOWN_EMPTY_WINDOW_WORKSPACE, isTemporaryWorkspace, isWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { IAnyWorkspaceIdentifier, IWorkspaceContextService, UNKNOWN_EMPTY_WINDOW_WORKSPACE, isTemporaryWorkspace, isWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
@ -261,7 +261,7 @@ export class BrowserMain extends Disposable {
// Files // Files
const fileLogger = new BufferLogger(); const fileLogger = new BufferLogger();
const fileService = this._register(new FileService(fileLogger)); const fileService = this._register(new FileService(fileLogger));
serviceCollection.set(IWorkbenchFileService, fileService); serviceCollection.set(IFileService, fileService);
// Logger // Logger
const loggerService = new FileLoggerService(getLogLevel(environmentService), logsPath, fileService); const loggerService = new FileLoggerService(getLogLevel(environmentService), logsPath, fileService);
@ -429,7 +429,8 @@ export class BrowserMain extends Disposable {
} }
} }
private async registerIndexedDBFileSystemProviders(environmentService: IWorkbenchEnvironmentService, fileService: IWorkbenchFileService, logService: ILogService, loggerService: ILoggerService, logsPath: URI): Promise<void> { private async registerIndexedDBFileSystemProviders(environmentService: IWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService, loggerService: ILoggerService, logsPath: URI): Promise<void> {
// IndexedDB is used for logging and user data // IndexedDB is used for logging and user data
let indexedDB: IndexedDB | undefined; let indexedDB: IndexedDB | undefined;
const userDataStore = 'vscode-userdata-store'; const userDataStore = 'vscode-userdata-store';

View file

@ -7,7 +7,7 @@ import { localize } from 'vs/nls';
import { IDisposable, Disposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { IDisposable, Disposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration } from 'vs/platform/files/common/files'; import { IFileService, IFilesConfiguration } from 'vs/platform/files/common/files';
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { ResourceMap } from 'vs/base/common/map'; import { ResourceMap } from 'vs/base/common/map';
import { INotificationService, Severity, NeverShowAgainScope, NotificationPriority } from 'vs/platform/notification/common/notification'; import { INotificationService, Severity, NeverShowAgainScope, NotificationPriority } from 'vs/platform/notification/common/notification';
@ -15,14 +15,13 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { isAbsolute } from 'vs/base/common/path'; import { isAbsolute } from 'vs/base/common/path';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files';
export class WorkspaceWatcher extends Disposable { export class WorkspaceWatcher extends Disposable {
private readonly watchedWorkspaces = new ResourceMap<IDisposable>(resource => this.uriIdentityService.extUri.getComparisonKey(resource)); private readonly watchedWorkspaces = new ResourceMap<IDisposable>(resource => this.uriIdentityService.extUri.getComparisonKey(resource));
constructor( constructor(
@IWorkbenchFileService private readonly fileService: IWorkbenchFileService, @IFileService private readonly fileService: IFileService,
@IConfigurationService private readonly configurationService: IConfigurationService, @IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@INotificationService private readonly notificationService: INotificationService, @INotificationService private readonly notificationService: INotificationService,

View file

@ -29,7 +29,7 @@ import { IRemoteAuthorityResolverService, RemoteConnectionType } from 'vs/platfo
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentService';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { FileService } from 'vs/platform/files/common/fileService'; import { FileService } from 'vs/platform/files/common/fileService';
import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files'; import { IFileService } from 'vs/platform/files/common/files';
import { RemoteFileSystemProviderClient } from 'vs/workbench/services/remote/common/remoteFileSystemProviderClient'; import { RemoteFileSystemProviderClient } from 'vs/workbench/services/remote/common/remoteFileSystemProviderClient';
import { ConfigurationCache } from 'vs/workbench/services/configuration/common/configurationCache'; import { ConfigurationCache } from 'vs/workbench/services/configuration/common/configurationCache';
import { ISignService } from 'vs/platform/sign/common/sign'; import { ISignService } from 'vs/platform/sign/common/sign';
@ -227,7 +227,7 @@ export class DesktopMain extends Disposable {
// Files // Files
const fileService = this._register(new FileService(logService)); const fileService = this._register(new FileService(logService));
serviceCollection.set(IWorkbenchFileService, fileService); serviceCollection.set(IFileService, fileService);
// Remote // Remote
const remoteAuthorityResolverService = new RemoteAuthorityResolverService(productService, new ElectronRemoteResourceLoader(environmentService.window.id, mainProcessService, fileService)); const remoteAuthorityResolverService = new RemoteAuthorityResolverService(productService, new ElectronRemoteResourceLoader(environmentService.window.id, mainProcessService, fileService));

View file

@ -32,6 +32,7 @@ export const allApiProposals = Object.freeze({
contribStatusBarItems: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribStatusBarItems.d.ts', contribStatusBarItems: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribStatusBarItems.d.ts',
contribViewsRemote: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts', contribViewsRemote: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts',
contribViewsWelcome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', contribViewsWelcome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts',
createFileSystemWatcher: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts',
customEditorMove: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', customEditorMove: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts',
debugFocus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugFocus.d.ts', debugFocus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugFocus.d.ts',
diffCommand: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', diffCommand: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts',

View file

@ -1,22 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IFileService, IWatchOptions } from 'vs/platform/files/common/files';
import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IWorkbenchFileService = refineServiceDecorator<IFileService, IWorkbenchFileService>(IFileService);
export interface IWorkbenchFileService extends IFileService {
/**
* Allows to start a watcher that reports file/folder change events on the provided resource.
*
* Note: watching a folder does not report events recursively unless the provided options
* explicitly opt-in to recursive watching.
*/
watch(resource: URI, options?: IWatchOptions): IDisposable;
}

View file

@ -5,14 +5,14 @@
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { isLinux } from 'vs/base/common/platform'; import { isLinux } from 'vs/base/common/platform';
import { FileSystemProviderCapabilities, IFileDeleteOptions, IStat, FileType, IFileReadStreamOptions, IFileWriteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileAtomicReadOptions, IFileSystemProviderWithFileCloneCapability } from 'vs/platform/files/common/files'; import { FileSystemProviderCapabilities, IFileDeleteOptions, IStat, FileType, IFileReadStreamOptions, IFileWriteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileAtomicReadOptions, IFileSystemProviderWithFileCloneCapability, IFileChange } from 'vs/platform/files/common/files';
import { AbstractDiskFileSystemProvider } from 'vs/platform/files/common/diskFileSystemProvider'; import { AbstractDiskFileSystemProvider } from 'vs/platform/files/common/diskFileSystemProvider';
import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { ReadableStreamEvents } from 'vs/base/common/stream'; import { ReadableStreamEvents } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { DiskFileSystemProviderClient, LOCAL_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/files/common/diskFileSystemProviderClient'; import { DiskFileSystemProviderClient, LOCAL_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/files/common/diskFileSystemProviderClient';
import { IDiskFileChange, ILogMessage, AbstractUniversalWatcherClient } from 'vs/platform/files/common/watcher'; import { ILogMessage, AbstractUniversalWatcherClient } from 'vs/platform/files/common/watcher';
import { UniversalWatcherClient } from 'vs/workbench/services/files/electron-sandbox/watcherClient'; import { UniversalWatcherClient } from 'vs/workbench/services/files/electron-sandbox/watcherClient';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { IUtilityProcessWorkerWorkbenchService } from 'vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService'; import { IUtilityProcessWorkerWorkbenchService } from 'vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService';
@ -132,7 +132,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
//#region File Watching //#region File Watching
protected createUniversalWatcher( protected createUniversalWatcher(
onChange: (changes: IDiskFileChange[]) => void, onChange: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void, onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean verboseLogging: boolean
): AbstractUniversalWatcherClient { ): AbstractUniversalWatcherClient {

View file

@ -5,13 +5,14 @@
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { getDelayedChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { getDelayedChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { AbstractUniversalWatcherClient, IDiskFileChange, ILogMessage, IRecursiveWatcher } from 'vs/platform/files/common/watcher'; import { IFileChange } from 'vs/platform/files/common/files';
import { AbstractUniversalWatcherClient, ILogMessage, IRecursiveWatcher } from 'vs/platform/files/common/watcher';
import { IUtilityProcessWorkerWorkbenchService } from 'vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService'; import { IUtilityProcessWorkerWorkbenchService } from 'vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService';
export class UniversalWatcherClient extends AbstractUniversalWatcherClient { export class UniversalWatcherClient extends AbstractUniversalWatcherClient {
constructor( constructor(
onFileChanges: (changes: IDiskFileChange[]) => void, onFileChanges: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void, onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean, verboseLogging: boolean,
private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService

View file

@ -45,9 +45,6 @@ import { Codicon } from 'vs/base/common/codicons';
import { listErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { listErrorForeground } from 'vs/platform/theme/common/colorRegistry';
import { firstOrDefault } from 'vs/base/common/arrays'; import { firstOrDefault } from 'vs/base/common/arrays';
/**
* The workbench file service implementation implements the raw file service spec and adds additional methods on top.
*/
export abstract class AbstractTextFileService extends Disposable implements ITextFileService { export abstract class AbstractTextFileService extends Disposable implements ITextFileService {
declare readonly _serviceBrand: undefined; declare readonly _serviceBrand: undefined;

View file

@ -23,7 +23,7 @@ import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbe
import { IWorkspaceContextService, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceContextService, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
import { ILifecycleService, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent, BeforeShutdownErrorEvent, InternalBeforeShutdownEvent, IWillShutdownEventJoiner } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILifecycleService, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent, BeforeShutdownErrorEvent, InternalBeforeShutdownEvent, IWillShutdownEventJoiner } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { FileOperationEvent, IFileService, IFileStat, IFileStatResult, FileChangesEvent, IResolveFileOptions, ICreateFileOptions, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, IFileDeleteOptions, IFileOverwriteOptions, IFileWriteOptions, IFileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions, IReadFileOptions, IFileContent, IFileStreamContent, FileOperationError, IFileSystemProviderWithFileReadStreamCapability, IFileReadStreamOptions, IReadFileStreamOptions, IFileSystemProviderCapabilitiesChangeEvent, IFileStatWithPartialMetadata } from 'vs/platform/files/common/files'; import { FileOperationEvent, IFileService, IFileStat, IFileStatResult, FileChangesEvent, IResolveFileOptions, ICreateFileOptions, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, IFileDeleteOptions, IFileOverwriteOptions, IFileWriteOptions, IFileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions, IReadFileOptions, IFileContent, IFileStreamContent, FileOperationError, IFileSystemProviderWithFileReadStreamCapability, IFileReadStreamOptions, IReadFileStreamOptions, IFileSystemProviderCapabilitiesChangeEvent, IFileStatWithPartialMetadata, IFileSystemWatcher, IWatchOptionsWithCorrelation } from 'vs/platform/files/common/files';
import { IModelService } from 'vs/editor/common/services/model'; import { IModelService } from 'vs/editor/common/services/model';
import { LanguageService } from 'vs/editor/common/services/languageService'; import { LanguageService } from 'vs/editor/common/services/languageService';
import { ModelService } from 'vs/editor/common/services/modelService'; import { ModelService } from 'vs/editor/common/services/modelService';
@ -1148,7 +1148,17 @@ export class TestFileService implements IFileService {
async del(_resource: URI, _options?: { useTrash?: boolean; recursive?: boolean }): Promise<void> { } async del(_resource: URI, _options?: { useTrash?: boolean; recursive?: boolean }): Promise<void> { }
createWatcher(resource: URI, options: IWatchOptions): IFileSystemWatcher {
return {
onDidChange: Event.None,
dispose: () => { }
};
}
readonly watches: URI[] = []; readonly watches: URI[] = [];
watch(_resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher;
watch(_resource: URI): IDisposable;
watch(_resource: URI): IDisposable { watch(_resource: URI): IDisposable {
this.watches.push(_resource); this.watches.push(_resource);
@ -1368,7 +1378,7 @@ export class RemoteFileSystemProvider implements IFileSystemProvider {
readonly capabilities: FileSystemProviderCapabilities = this.wrappedFsp.capabilities; readonly capabilities: FileSystemProviderCapabilities = this.wrappedFsp.capabilities;
readonly onDidChangeCapabilities: Event<void> = this.wrappedFsp.onDidChangeCapabilities; readonly onDidChangeCapabilities: Event<void> = this.wrappedFsp.onDidChangeCapabilities;
readonly onDidChangeFile: Event<readonly IFileChange[]> = Event.map(this.wrappedFsp.onDidChangeFile, changes => changes.map((c): IFileChange => { readonly onDidChangeFile: Event<readonly IFileChange[]> = Event.map(this.wrappedFsp.onDidChangeFile, changes => changes.map(c => {
return { return {
type: c.type, type: c.type,
resource: c.resource.with({ scheme: Schemas.vscodeRemote, authority: this.remoteAuthority }), resource: c.resource.with({ scheme: Schemas.vscodeRemote, authority: this.remoteAuthority }),

View file

@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// https://github.com/microsoft/vscode/issues/169724 @bpasero
declare module 'vscode' {
export interface FileSystemWatcherOptions {
/**
* Ignore when files have been created.
*/
readonly ignoreCreateEvents?: boolean;
/**
* Ignore when files have been changed.
*/
readonly ignoreChangeEvents?: boolean;
/**
* Ignore when files have been deleted.
*/
readonly ignoreDeleteEvents?: boolean;
/**
* An optional set of glob patterns to exclude from watching.
* Glob patterns are always matched relative to the watched folder.
*/
readonly excludes?: string[];
}
export namespace workspace {
export function createFileSystemWatcher(pattern: RelativePattern, options?: FileSystemWatcherOptions): FileSystemWatcher;
}
}