mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 21:09:43 +00:00
watcher - deduplicate non-recursive file watch requests if possible (#208325)
This commit is contained in:
parent
16a32fa6c4
commit
4b90a20d3a
|
@ -211,6 +211,10 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
|
||||||
this._onDidWatchError.fire(msg.message);
|
this._onDidWatchError.fire(msg.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logWatcherMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected logWatcherMessage(msg: ILogMessage): void {
|
||||||
this.logService[msg.type](msg.message);
|
this.logService[msg.type](msg.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { Event } from 'vs/base/common/event';
|
import { Event } from 'vs/base/common/event';
|
||||||
import { GLOBSTAR, IRelativePattern, parse, ParsedPattern } from 'vs/base/common/glob';
|
import { GLOBSTAR, IRelativePattern, parse, ParsedPattern } from 'vs/base/common/glob';
|
||||||
import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
|
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { isAbsolute } from 'vs/base/common/path';
|
import { isAbsolute } from 'vs/base/common/path';
|
||||||
import { isLinux } from 'vs/base/common/platform';
|
import { isLinux } from 'vs/base/common/platform';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
|
@ -122,6 +122,20 @@ export interface IRecursiveWatcher extends IWatcher {
|
||||||
watch(requests: IRecursiveWatchRequest[]): Promise<void>;
|
watch(requests: IRecursiveWatchRequest[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IRecursiveWatcherWithSubscribe extends IRecursiveWatcher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to file events for the given path. The callback is called
|
||||||
|
* whenever a file event occurs for the path. I fthe watcher failed,
|
||||||
|
* the error parameter is set to `true`.
|
||||||
|
*
|
||||||
|
* @returns an `IDisposable` to stop listening to events or `undefined`
|
||||||
|
* if no events can be watched for the path given the current set of
|
||||||
|
* recursive watch requests.
|
||||||
|
*/
|
||||||
|
subscribe(path: string, callback: (error: boolean, change?: IFileChange) => void): IDisposable | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IRecursiveWatcherOptions {
|
export interface IRecursiveWatcherOptions {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
|
|
||||||
import { watchFile, unwatchFile, Stats } from 'fs';
|
import { watchFile, unwatchFile, Stats } from 'fs';
|
||||||
import { Disposable, DisposableMap, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
import { Disposable, DisposableMap, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { ILogMessage, IUniversalWatchRequest, IWatchRequestWithCorrelation, IWatcher, isWatchRequestWithCorrelation } from 'vs/platform/files/common/watcher';
|
import { ILogMessage, IRecursiveWatcherWithSubscribe, IUniversalWatchRequest, IWatchRequestWithCorrelation, IWatcher, isWatchRequestWithCorrelation } from 'vs/platform/files/common/watcher';
|
||||||
import { Emitter, Event } from 'vs/base/common/event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
import { FileChangeType, IFileChange } from 'vs/platform/files/common/files';
|
import { FileChangeType, IFileChange } from 'vs/platform/files/common/files';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
|
import { DeferredPromise } from 'vs/base/common/async';
|
||||||
|
|
||||||
export abstract class BaseWatcher extends Disposable implements IWatcher {
|
export abstract class BaseWatcher extends Disposable implements IWatcher {
|
||||||
|
|
||||||
|
@ -25,9 +26,12 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
|
||||||
private readonly allCorrelatedWatchRequests = new Map<number /* correlation ID */, IWatchRequestWithCorrelation>();
|
private readonly allCorrelatedWatchRequests = new Map<number /* correlation ID */, IWatchRequestWithCorrelation>();
|
||||||
|
|
||||||
private readonly suspendedWatchRequests = this._register(new DisposableMap<number /* correlation ID */>());
|
private readonly suspendedWatchRequests = this._register(new DisposableMap<number /* correlation ID */>());
|
||||||
|
private readonly suspendedWatchRequestsWithPolling = new Set<number /* correlation ID */>();
|
||||||
|
|
||||||
protected readonly suspendedWatchRequestPollingInterval: number = 5007; // node.js default
|
protected readonly suspendedWatchRequestPollingInterval: number = 5007; // node.js default
|
||||||
|
|
||||||
|
private joinWatch = new DeferredPromise<void>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
@ -41,6 +45,11 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
|
||||||
// to experiment with this feature in a controlled way. Monitoring requests
|
// to experiment with this feature in a controlled way. Monitoring requests
|
||||||
// requires us to install polling watchers (via `fs.watchFile()`) and thus
|
// requires us to install polling watchers (via `fs.watchFile()`) and thus
|
||||||
// should be used sparingly.
|
// should be used sparingly.
|
||||||
|
//
|
||||||
|
// TODO@bpasero revisit this in the future to have a more general approach
|
||||||
|
// for suspend/resume and drop the `legacyMonitorRequest` in parcel.
|
||||||
|
// One issue is that we need to be able to uniquely identify a request and
|
||||||
|
// without correlation that is actually harder...
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -53,26 +62,36 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
async watch(requests: IUniversalWatchRequest[]): Promise<void> {
|
async watch(requests: IUniversalWatchRequest[]): Promise<void> {
|
||||||
this.allCorrelatedWatchRequests.clear();
|
if (!this.joinWatch.isSettled) {
|
||||||
this.allNonCorrelatedWatchRequests.clear();
|
this.joinWatch.complete();
|
||||||
|
|
||||||
// Figure out correlated vs. non-correlated requests
|
|
||||||
for (const request of requests) {
|
|
||||||
if (this.isCorrelated(request)) {
|
|
||||||
this.allCorrelatedWatchRequests.set(request.correlationId, request);
|
|
||||||
} else {
|
|
||||||
this.allNonCorrelatedWatchRequests.add(request);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.joinWatch = new DeferredPromise<void>();
|
||||||
|
|
||||||
// Remove all suspended correlated watch requests that are no longer watched
|
try {
|
||||||
for (const [correlationId] of this.suspendedWatchRequests) {
|
this.allCorrelatedWatchRequests.clear();
|
||||||
if (!this.allCorrelatedWatchRequests.has(correlationId)) {
|
this.allNonCorrelatedWatchRequests.clear();
|
||||||
this.suspendedWatchRequests.deleteAndDispose(correlationId);
|
|
||||||
|
// Figure out correlated vs. non-correlated requests
|
||||||
|
for (const request of requests) {
|
||||||
|
if (this.isCorrelated(request)) {
|
||||||
|
this.allCorrelatedWatchRequests.set(request.correlationId, request);
|
||||||
|
} else {
|
||||||
|
this.allNonCorrelatedWatchRequests.add(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return this.updateWatchers();
|
// Remove all suspended correlated watch requests that are no longer watched
|
||||||
|
for (const [correlationId] of this.suspendedWatchRequests) {
|
||||||
|
if (!this.allCorrelatedWatchRequests.has(correlationId)) {
|
||||||
|
this.suspendedWatchRequests.deleteAndDispose(correlationId);
|
||||||
|
this.suspendedWatchRequestsWithPolling.delete(correlationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.updateWatchers();
|
||||||
|
} finally {
|
||||||
|
this.joinWatch.complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateWatchers(): Promise<void> {
|
private updateWatchers(): Promise<void> {
|
||||||
|
@ -82,7 +101,15 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspendWatchRequest(request: IWatchRequestWithCorrelation): void {
|
isSuspended(request: IUniversalWatchRequest): 'polling' | boolean {
|
||||||
|
if (typeof request.correlationId !== 'number') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.suspendedWatchRequestsWithPolling.has(request.correlationId) ? 'polling' : this.suspendedWatchRequests.has(request.correlationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async suspendWatchRequest(request: IWatchRequestWithCorrelation): Promise<void> {
|
||||||
if (this.suspendedWatchRequests.has(request.correlationId)) {
|
if (this.suspendedWatchRequests.has(request.correlationId)) {
|
||||||
return; // already suspended
|
return; // already suspended
|
||||||
}
|
}
|
||||||
|
@ -90,6 +117,17 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
|
||||||
const disposables = new DisposableStore();
|
const disposables = new DisposableStore();
|
||||||
this.suspendedWatchRequests.set(request.correlationId, disposables);
|
this.suspendedWatchRequests.set(request.correlationId, disposables);
|
||||||
|
|
||||||
|
// It is possible that a watch request fails right during watch()
|
||||||
|
// phase while other requests succeed. To increase the chance of
|
||||||
|
// reusing another watcher for suspend/resume tracking, we await
|
||||||
|
// all watch requests having processed.
|
||||||
|
|
||||||
|
await this.joinWatch.p;
|
||||||
|
|
||||||
|
if (disposables.isDisposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.monitorSuspendedWatchRequest(request, disposables);
|
this.monitorSuspendedWatchRequest(request, disposables);
|
||||||
|
|
||||||
this.updateWatchers();
|
this.updateWatchers();
|
||||||
|
@ -97,14 +135,44 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
|
||||||
|
|
||||||
private resumeWatchRequest(request: IWatchRequestWithCorrelation): void {
|
private resumeWatchRequest(request: IWatchRequestWithCorrelation): void {
|
||||||
this.suspendedWatchRequests.deleteAndDispose(request.correlationId);
|
this.suspendedWatchRequests.deleteAndDispose(request.correlationId);
|
||||||
|
this.suspendedWatchRequestsWithPolling.delete(request.correlationId);
|
||||||
|
|
||||||
this.updateWatchers();
|
this.updateWatchers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private monitorSuspendedWatchRequest(request: IWatchRequestWithCorrelation, disposables: DisposableStore) {
|
private monitorSuspendedWatchRequest(request: IWatchRequestWithCorrelation, disposables: DisposableStore): void {
|
||||||
const resource = URI.file(request.path);
|
if (this.doMonitorWithExistingWatcher(request, disposables)) {
|
||||||
const that = this;
|
this.trace(`reusing an existing recursive watcher to monitor ${request.path}`);
|
||||||
|
this.suspendedWatchRequestsWithPolling.delete(request.correlationId);
|
||||||
|
} else {
|
||||||
|
this.doMonitorWithNodeJS(request, disposables);
|
||||||
|
this.suspendedWatchRequestsWithPolling.add(request.correlationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private doMonitorWithExistingWatcher(request: IWatchRequestWithCorrelation, disposables: DisposableStore): boolean {
|
||||||
|
const subscription = this.recursiveWatcher?.subscribe(request.path, (error, change) => {
|
||||||
|
if (disposables.isDisposed) {
|
||||||
|
return; // return early if already disposed
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
this.monitorSuspendedWatchRequest(request, disposables);
|
||||||
|
} else if (change?.type === FileChangeType.ADDED) {
|
||||||
|
this.onMonitoredPathAdded(request);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subscription) {
|
||||||
|
disposables.add(subscription);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private doMonitorWithNodeJS(request: IWatchRequestWithCorrelation, disposables: DisposableStore): void {
|
||||||
let pathNotFound = false;
|
let pathNotFound = false;
|
||||||
|
|
||||||
const watchFileCallback: (curr: Stats, prev: Stats) => void = (curr, prev) => {
|
const watchFileCallback: (curr: Stats, prev: Stats) => void = (curr, prev) => {
|
||||||
|
@ -119,15 +187,7 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
|
||||||
|
|
||||||
// Watch path created: resume watching request
|
// Watch path created: resume watching request
|
||||||
if (!currentPathNotFound && (previousPathNotFound || oldPathNotFound)) {
|
if (!currentPathNotFound && (previousPathNotFound || oldPathNotFound)) {
|
||||||
this.trace(`fs.watchFile() detected ${request.path} exists again, resuming watcher (correlationId: ${request.correlationId})`);
|
this.onMonitoredPathAdded(request);
|
||||||
|
|
||||||
// Emit as event
|
|
||||||
const event: IFileChange = { resource, type: FileChangeType.ADDED, cId: request.correlationId };
|
|
||||||
that._onDidChangeFile.fire([event]);
|
|
||||||
this.traceEvent(event, request);
|
|
||||||
|
|
||||||
// Resume watching
|
|
||||||
this.resumeWatchRequest(request);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -149,12 +209,25 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onMonitoredPathAdded(request: IWatchRequestWithCorrelation) {
|
||||||
|
this.trace(`detected ${request.path} exists again, resuming watcher (correlationId: ${request.correlationId})`);
|
||||||
|
|
||||||
|
// Emit as event
|
||||||
|
const event: IFileChange = { resource: URI.file(request.path), type: FileChangeType.ADDED, cId: request.correlationId };
|
||||||
|
this._onDidChangeFile.fire([event]);
|
||||||
|
this.traceEvent(event, request);
|
||||||
|
|
||||||
|
// Resume watching
|
||||||
|
this.resumeWatchRequest(request);
|
||||||
|
}
|
||||||
|
|
||||||
private isPathNotFound(stats: Stats): boolean {
|
private isPathNotFound(stats: Stats): boolean {
|
||||||
return stats.ctimeMs === 0 && stats.ino === 0;
|
return stats.ctimeMs === 0 && stats.ino === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
this.suspendedWatchRequests.clearAndDisposeAll();
|
this.suspendedWatchRequests.clearAndDisposeAll();
|
||||||
|
this.suspendedWatchRequestsWithPolling.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected traceEvent(event: IFileChange, request: IUniversalWatchRequest): void {
|
protected traceEvent(event: IFileChange, request: IUniversalWatchRequest): void {
|
||||||
|
@ -168,6 +241,8 @@ export abstract class BaseWatcher extends Disposable implements IWatcher {
|
||||||
|
|
||||||
protected abstract doWatch(requests: IUniversalWatchRequest[]): Promise<void>;
|
protected abstract doWatch(requests: IUniversalWatchRequest[]): Promise<void>;
|
||||||
|
|
||||||
|
protected abstract readonly recursiveWatcher: IRecursiveWatcherWithSubscribe | undefined;
|
||||||
|
|
||||||
protected abstract trace(message: string): void;
|
protected abstract trace(message: string): void;
|
||||||
protected abstract warn(message: string): void;
|
protected abstract warn(message: string): void;
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,6 @@ export class NodeJSWatcherClient extends AbstractNonRecursiveWatcherClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override createWatcher(disposables: DisposableStore): INonRecursiveWatcher {
|
protected override createWatcher(disposables: DisposableStore): INonRecursiveWatcher {
|
||||||
return disposables.add(new NodeJSWatcher()) satisfies INonRecursiveWatcher;
|
return disposables.add(new NodeJSWatcher(undefined /* no recursive watching support here */)) satisfies INonRecursiveWatcher;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/event';
|
||||||
import { patternsEquals } from 'vs/base/common/glob';
|
import { patternsEquals } from 'vs/base/common/glob';
|
||||||
import { BaseWatcher } from 'vs/platform/files/node/watcher/baseWatcher';
|
import { BaseWatcher } from 'vs/platform/files/node/watcher/baseWatcher';
|
||||||
import { isLinux } from 'vs/base/common/platform';
|
import { isLinux } from 'vs/base/common/platform';
|
||||||
import { INonRecursiveWatchRequest, INonRecursiveWatcher } from 'vs/platform/files/common/watcher';
|
import { INonRecursiveWatchRequest, INonRecursiveWatcher, IRecursiveWatcherWithSubscribe } 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';
|
||||||
import { isEqual } from 'vs/base/common/extpath';
|
import { isEqual } from 'vs/base/common/extpath';
|
||||||
|
|
||||||
|
@ -28,10 +28,14 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher {
|
||||||
|
|
||||||
readonly onDidError = Event.None;
|
readonly onDidError = Event.None;
|
||||||
|
|
||||||
protected readonly watchers = new Set<INodeJSWatcherInstance>();
|
readonly watchers = new Set<INodeJSWatcherInstance>();
|
||||||
|
|
||||||
private verboseLogging = false;
|
private verboseLogging = false;
|
||||||
|
|
||||||
|
constructor(protected readonly recursiveWatcher: IRecursiveWatcherWithSubscribe | undefined) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
protected override async doWatch(requests: INonRecursiveWatchRequest[]): Promise<void> {
|
protected override async doWatch(requests: INonRecursiveWatchRequest[]): Promise<void> {
|
||||||
|
|
||||||
// Figure out duplicates to remove from the requests
|
// Figure out duplicates to remove from the requests
|
||||||
|
@ -47,7 +51,6 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher {
|
||||||
} else {
|
} else {
|
||||||
requestsToStart.push(request); // start watching
|
requestsToStart.push(request); // start watching
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
|
@ -95,7 +98,7 @@ export class NodeJSWatcher extends BaseWatcher implements INonRecursiveWatcher {
|
||||||
private startWatching(request: INonRecursiveWatchRequest): void {
|
private startWatching(request: INonRecursiveWatchRequest): void {
|
||||||
|
|
||||||
// Start via node.js lib
|
// Start via node.js lib
|
||||||
const instance = new NodeJSFileWatcherLibrary(request, changes => this._onDidChangeFile.fire(changes), () => this._onDidWatchFail.fire(request), msg => this._onDidLogMessage.fire(msg), this.verboseLogging);
|
const instance = new NodeJSFileWatcherLibrary(request, this.recursiveWatcher, changes => this._onDidChangeFile.fire(changes), () => this._onDidWatchFail.fire(request), msg => this._onDidLogMessage.fire(msg), this.verboseLogging);
|
||||||
|
|
||||||
// Remember as watcher instance
|
// Remember as watcher instance
|
||||||
const watcher: INodeJSWatcherInstance = { request, instance };
|
const watcher: INodeJSWatcherInstance = { request, instance };
|
||||||
|
|
|
@ -16,7 +16,7 @@ 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, IFileChange } from 'vs/platform/files/common/files';
|
import { FileChangeType, IFileChange } from 'vs/platform/files/common/files';
|
||||||
import { ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns } from 'vs/platform/files/common/watcher';
|
import { ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe } from 'vs/platform/files/common/watcher';
|
||||||
|
|
||||||
export class NodeJSFileWatcherLibrary extends Disposable {
|
export class NodeJSFileWatcherLibrary extends Disposable {
|
||||||
|
|
||||||
|
@ -56,8 +56,15 @@ export class NodeJSFileWatcherLibrary extends Disposable {
|
||||||
|
|
||||||
readonly ready = this.watch();
|
readonly ready = this.watch();
|
||||||
|
|
||||||
|
private _isReusingRecursiveWatcher = false;
|
||||||
|
get isReusingRecursiveWatcher(): boolean { return this._isReusingRecursiveWatcher; }
|
||||||
|
|
||||||
|
private didFail = false;
|
||||||
|
get failed(): boolean { return this.didFail; }
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly request: INonRecursiveWatchRequest,
|
private readonly request: INonRecursiveWatchRequest,
|
||||||
|
private readonly recursiveWatcher: IRecursiveWatcherWithSubscribe | undefined,
|
||||||
private readonly onDidFilesChange: (changes: IFileChange[]) => void,
|
private readonly onDidFilesChange: (changes: IFileChange[]) => void,
|
||||||
private readonly onDidWatchFail?: () => void,
|
private readonly onDidWatchFail?: () => void,
|
||||||
private readonly onLogMessage?: (msg: ILogMessage) => void,
|
private readonly onLogMessage?: (msg: ILogMessage) => void,
|
||||||
|
@ -88,10 +95,16 @@ export class NodeJSFileWatcherLibrary extends Disposable {
|
||||||
this.trace(`ignoring a path for watching who's stat info failed to resolve: ${this.request.path} (error: ${error})`);
|
this.trace(`ignoring a path for watching who's stat info failed to resolve: ${this.request.path} (error: ${error})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onDidWatchFail?.();
|
this.notifyWatchFailed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private notifyWatchFailed(): void {
|
||||||
|
this.didFail = true;
|
||||||
|
|
||||||
|
this.onDidWatchFail?.();
|
||||||
|
}
|
||||||
|
|
||||||
private async normalizePath(request: INonRecursiveWatchRequest): Promise<string> {
|
private async normalizePath(request: INonRecursiveWatchRequest): Promise<string> {
|
||||||
let realPath = request.path;
|
let realPath = request.path;
|
||||||
|
|
||||||
|
@ -117,41 +130,95 @@ export class NodeJSFileWatcherLibrary extends Disposable {
|
||||||
return realPath;
|
return realPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doWatch(path: string, isDirectory: boolean): Promise<IDisposable> {
|
private async doWatch(realPath: string, isDirectory: boolean): Promise<IDisposable> {
|
||||||
|
const disposables = new DisposableStore();
|
||||||
|
|
||||||
|
if (this.doWatchWithExistingWatcher(realPath, isDirectory, disposables)) {
|
||||||
|
this.trace(`reusing an existing recursive watcher for ${this.request.path}`);
|
||||||
|
this._isReusingRecursiveWatcher = true;
|
||||||
|
} else {
|
||||||
|
this._isReusingRecursiveWatcher = false;
|
||||||
|
await this.doWatchWithNodeJS(realPath, isDirectory, disposables);
|
||||||
|
}
|
||||||
|
|
||||||
|
return disposables;
|
||||||
|
}
|
||||||
|
|
||||||
|
private doWatchWithExistingWatcher(realPath: string, isDirectory: boolean, disposables: DisposableStore): boolean {
|
||||||
|
if (isDirectory) {
|
||||||
|
return false; // only supported for files where we have the full path known upfront
|
||||||
|
}
|
||||||
|
|
||||||
|
const resource = URI.file(this.request.path);
|
||||||
|
const subscription = this.recursiveWatcher?.subscribe(this.request.path, async (error, change) => {
|
||||||
|
if (disposables.isDisposed) {
|
||||||
|
return; // return early if already disposed
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const watchDisposable = await this.doWatch(realPath, isDirectory);
|
||||||
|
if (!disposables.isDisposed) {
|
||||||
|
disposables.add(watchDisposable);
|
||||||
|
} else {
|
||||||
|
watchDisposable.dispose();
|
||||||
|
}
|
||||||
|
} else if (change) {
|
||||||
|
if (typeof change.cId === 'number' || typeof this.request.correlationId === 'number') {
|
||||||
|
// Re-emit this change with the correlation id of the request
|
||||||
|
// so that the client can correlate the event with the request
|
||||||
|
// properly. Without correlation, we do not have to do that
|
||||||
|
// because the event will appear on the global listener already.
|
||||||
|
this.onDidFilesChange([{ resource, type: change.type, cId: this.request.correlationId }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subscription) {
|
||||||
|
disposables.add(subscription);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doWatchWithNodeJS(realPath: string, isDirectory: boolean, disposables: DisposableStore): Promise<void> {
|
||||||
|
|
||||||
// macOS: watching samba shares can crash VSCode so we do
|
// macOS: watching samba shares can crash VSCode so we do
|
||||||
// a simple check for the file path pointing to /Volumes
|
// a simple check for the file path pointing to /Volumes
|
||||||
// (https://github.com/microsoft/vscode/issues/106879)
|
// (https://github.com/microsoft/vscode/issues/106879)
|
||||||
// TODO@electron this needs a revisit when the crash is
|
// TODO@electron this needs a revisit when the crash is
|
||||||
// fixed or mitigated upstream.
|
// fixed or mitigated upstream.
|
||||||
if (isMacintosh && isEqualOrParent(path, '/Volumes/', true)) {
|
if (isMacintosh && isEqualOrParent(realPath, '/Volumes/', true)) {
|
||||||
this.error(`Refusing to watch ${path} for changes using fs.watch() for possibly being a network share where watching is unreliable and unstable.`);
|
this.error(`Refusing to watch ${realPath} for changes using fs.watch() for possibly being a network share where watching is unreliable and unstable.`);
|
||||||
|
|
||||||
return Disposable.None;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cts = new CancellationTokenSource(this.cts.token);
|
const cts = new CancellationTokenSource(this.cts.token);
|
||||||
|
disposables.add(toDisposable(() => cts.dispose(true)));
|
||||||
|
|
||||||
const disposables = new DisposableStore();
|
const watcherDisposables = new DisposableStore(); // we need a separate disposable store because we re-create the watcher from within in some cases
|
||||||
|
disposables.add(watcherDisposables);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestResource = URI.file(this.request.path);
|
const requestResource = URI.file(this.request.path);
|
||||||
const pathBasename = basename(path);
|
const pathBasename = basename(realPath);
|
||||||
|
|
||||||
// Creating watcher can fail with an exception
|
// Creating watcher can fail with an exception
|
||||||
const watcher = watch(path);
|
const watcher = watch(realPath);
|
||||||
disposables.add(toDisposable(() => {
|
watcherDisposables.add(toDisposable(() => {
|
||||||
watcher.removeAllListeners();
|
watcher.removeAllListeners();
|
||||||
watcher.close();
|
watcher.close();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.trace(`Started watching: '${path}'`);
|
this.trace(`Started watching: '${realPath}'`);
|
||||||
|
|
||||||
// Folder: resolve children to emit proper events
|
// Folder: resolve children to emit proper events
|
||||||
const folderChildren = new Set<string>();
|
const folderChildren = new Set<string>();
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
try {
|
try {
|
||||||
for (const child of await Promises.readdir(path)) {
|
for (const child of await Promises.readdir(realPath)) {
|
||||||
folderChildren.add(child);
|
folderChildren.add(child);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -159,8 +226,12 @@ export class NodeJSFileWatcherLibrary extends Disposable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cts.token.isCancellationRequested) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const mapPathToStatDisposable = new Map<string, IDisposable>();
|
const mapPathToStatDisposable = new Map<string, IDisposable>();
|
||||||
disposables.add(toDisposable(() => {
|
watcherDisposables.add(toDisposable(() => {
|
||||||
for (const [, disposable] of mapPathToStatDisposable) {
|
for (const [, disposable] of mapPathToStatDisposable) {
|
||||||
disposable.dispose();
|
disposable.dispose();
|
||||||
}
|
}
|
||||||
|
@ -168,9 +239,13 @@ export class NodeJSFileWatcherLibrary extends Disposable {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
watcher.on('error', (code: number, signal: string) => {
|
watcher.on('error', (code: number, signal: string) => {
|
||||||
this.error(`Failed to watch ${path} for changes using fs.watch() (${code}, ${signal})`);
|
if (cts.token.isCancellationRequested) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.onDidWatchFail?.();
|
this.error(`Failed to watch ${realPath} for changes using fs.watch() (${code}, ${signal})`);
|
||||||
|
|
||||||
|
this.notifyWatchFailed();
|
||||||
});
|
});
|
||||||
|
|
||||||
watcher.on('change', (type, raw) => {
|
watcher.on('change', (type, raw) => {
|
||||||
|
@ -228,17 +303,21 @@ export class NodeJSFileWatcherLibrary extends Disposable {
|
||||||
// file watching specifically we want to handle
|
// file watching specifically we want to handle
|
||||||
// the atomic-write cases where the file is being
|
// the atomic-write cases where the file is being
|
||||||
// deleted and recreated with different contents.
|
// deleted and recreated with different contents.
|
||||||
if (changedFileName === pathBasename && !await Promises.exists(path)) {
|
if (changedFileName === pathBasename && !await Promises.exists(realPath)) {
|
||||||
this.onWatchedPathDeleted(requestResource);
|
this.onWatchedPathDeleted(requestResource);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cts.token.isCancellationRequested) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// In order to properly detect renames on a case-insensitive
|
// In order to properly detect renames on a case-insensitive
|
||||||
// file system, we need to use `existsChildStrictCase` helper
|
// file system, we need to use `existsChildStrictCase` helper
|
||||||
// because otherwise we would wrongly assume a file exists
|
// because otherwise we would wrongly assume a file exists
|
||||||
// when it was renamed to same name but different case.
|
// when it was renamed to same name but different case.
|
||||||
const fileExists = await this.existsChildStrictCase(join(path, changedFileName));
|
const fileExists = await this.existsChildStrictCase(join(realPath, changedFileName));
|
||||||
|
|
||||||
if (cts.token.isCancellationRequested) {
|
if (cts.token.isCancellationRequested) {
|
||||||
return; // ignore if disposed by now
|
return; // ignore if disposed by now
|
||||||
|
@ -310,7 +389,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
|
||||||
// because the watcher is disposed then.
|
// because the watcher is disposed then.
|
||||||
|
|
||||||
const timeoutHandle = setTimeout(async () => {
|
const timeoutHandle = setTimeout(async () => {
|
||||||
const fileExists = await Promises.exists(path);
|
const fileExists = await Promises.exists(realPath);
|
||||||
|
|
||||||
if (cts.token.isCancellationRequested) {
|
if (cts.token.isCancellationRequested) {
|
||||||
return; // ignore if disposed by now
|
return; // ignore if disposed by now
|
||||||
|
@ -320,7 +399,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
|
||||||
if (fileExists) {
|
if (fileExists) {
|
||||||
this.onFileChange({ resource: requestResource, type: FileChangeType.UPDATED, cId: this.request.correlationId }, 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));
|
watcherDisposables.add(await this.doWatch(realPath, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
// File seems to be really gone, so emit a deleted and failed event
|
// File seems to be really gone, so emit a deleted and failed event
|
||||||
|
@ -331,8 +410,8 @@ export class NodeJSFileWatcherLibrary extends Disposable {
|
||||||
|
|
||||||
// Very important to dispose the watcher which now points to a stale inode
|
// Very important to dispose the watcher which now points to a stale inode
|
||||||
// and wire in a new disposable that tracks our timeout that is installed
|
// and wire in a new disposable that tracks our timeout that is installed
|
||||||
disposables.clear();
|
watcherDisposables.clear();
|
||||||
disposables.add(toDisposable(() => clearTimeout(timeoutHandle)));
|
watcherDisposables.add(toDisposable(() => clearTimeout(timeoutHandle)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// File changed
|
// File changed
|
||||||
|
@ -343,16 +422,11 @@ export class NodeJSFileWatcherLibrary extends Disposable {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!cts.token.isCancellationRequested) {
|
if (!cts.token.isCancellationRequested) {
|
||||||
this.error(`Failed to watch ${path} for changes using fs.watch() (${error.toString()})`);
|
this.error(`Failed to watch ${realPath} for changes using fs.watch() (${error.toString()})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onDidWatchFail?.();
|
this.notifyWatchFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
return toDisposable(() => {
|
|
||||||
cts.dispose(true);
|
|
||||||
disposables.dispose();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWatchedPathDeleted(resource: URI): void {
|
private onWatchedPathDeleted(resource: URI): void {
|
||||||
|
@ -362,7 +436,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
|
||||||
this.onFileChange({ resource, type: FileChangeType.DELETED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */);
|
this.onFileChange({ resource, type: FileChangeType.DELETED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */);
|
||||||
this.fileChangesAggregator.flush();
|
this.fileChangesAggregator.flush();
|
||||||
|
|
||||||
this.onDidWatchFail?.();
|
this.notifyWatchFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onFileChange(event: IFileChange, skipIncludeExcludeChecks = false): void {
|
private onFileChange(event: IFileChange, skipIncludeExcludeChecks = false): void {
|
||||||
|
@ -476,7 +550,7 @@ export async function watchFileContents(path: string, onData: (chunk: Uint8Array
|
||||||
let isReading = false;
|
let isReading = false;
|
||||||
|
|
||||||
const request: INonRecursiveWatchRequest = { path, excludes: [], recursive: false };
|
const request: INonRecursiveWatchRequest = { path, excludes: [], recursive: false };
|
||||||
const watcher = new NodeJSFileWatcherLibrary(request, changes => {
|
const watcher = new NodeJSFileWatcherLibrary(request, undefined, changes => {
|
||||||
(async () => {
|
(async () => {
|
||||||
for (const { type } of changes) {
|
for (const { type } of changes) {
|
||||||
if (type === FileChangeType.UPDATED) {
|
if (type === FileChangeType.UPDATED) {
|
||||||
|
|
|
@ -10,9 +10,9 @@ import { URI } from 'vs/base/common/uri';
|
||||||
import { DeferredPromise, RunOnceScheduler, RunOnceWorker, ThrottledWorker } from 'vs/base/common/async';
|
import { DeferredPromise, RunOnceScheduler, RunOnceWorker, ThrottledWorker } from 'vs/base/common/async';
|
||||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||||
import { Emitter } from 'vs/base/common/event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
import { randomPath, isEqual } from 'vs/base/common/extpath';
|
import { randomPath, isEqual, isEqualOrParent } from 'vs/base/common/extpath';
|
||||||
import { GLOBSTAR, ParsedPattern, patternsEquals } from 'vs/base/common/glob';
|
import { GLOBSTAR, patternsEquals } from 'vs/base/common/glob';
|
||||||
import { BaseWatcher } from 'vs/platform/files/node/watcher/baseWatcher';
|
import { BaseWatcher } from 'vs/platform/files/node/watcher/baseWatcher';
|
||||||
import { TernarySearchTree } from 'vs/base/common/ternarySearchTree';
|
import { TernarySearchTree } from 'vs/base/common/ternarySearchTree';
|
||||||
import { normalizeNFC } from 'vs/base/common/normalization';
|
import { normalizeNFC } from 'vs/base/common/normalization';
|
||||||
|
@ -21,44 +21,121 @@ 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, IFileChange } from 'vs/platform/files/common/files';
|
import { FileChangeType, IFileChange } from 'vs/platform/files/common/files';
|
||||||
import { coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher, parseWatcherPatterns } from 'vs/platform/files/common/watcher';
|
import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe } from 'vs/platform/files/common/watcher';
|
||||||
|
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
|
|
||||||
export interface IParcelWatcherInstance {
|
export class ParcelWatcherInstance extends Disposable {
|
||||||
|
|
||||||
/**
|
private readonly _onDidStop = this._register(new Emitter<{ joinRestart?: Promise<void> }>());
|
||||||
* Signals when the watcher is ready to watch.
|
readonly onDidStop = this._onDidStop.event;
|
||||||
*/
|
|
||||||
readonly ready: Promise<unknown>;
|
|
||||||
|
|
||||||
/**
|
private readonly _onDidFail = this._register(new Emitter<void>());
|
||||||
* The watch request associated to the watcher.
|
readonly onDidFail = this._onDidFail.event;
|
||||||
*/
|
|
||||||
readonly request: IRecursiveWatchRequest;
|
|
||||||
|
|
||||||
/**
|
private didFail = false;
|
||||||
* How often this watcher has been restarted in case of an unexpected
|
get failed(): boolean { return this.didFail; }
|
||||||
* shutdown.
|
|
||||||
*/
|
|
||||||
readonly restarts: number;
|
|
||||||
|
|
||||||
/**
|
private didStop = false;
|
||||||
* The cancellation token associated with the lifecycle of the watcher.
|
get stopped(): boolean { return this.didStop; }
|
||||||
*/
|
|
||||||
readonly token: CancellationToken;
|
|
||||||
|
|
||||||
/**
|
private readonly includes = this.request.includes ? parseWatcherPatterns(this.request.path, this.request.includes) : undefined;
|
||||||
* An event aggregator to coalesce events and reduce duplicates.
|
private readonly excludes = this.request.excludes ? parseWatcherPatterns(this.request.path, this.request.excludes) : undefined;
|
||||||
*/
|
|
||||||
readonly worker: RunOnceWorker<IFileChange>;
|
|
||||||
|
|
||||||
/**
|
private readonly subscriptions = new Map<string, Set<(change: IFileChange) => void>>();
|
||||||
* Stops and disposes the watcher. This operation is async to await
|
|
||||||
* unsubscribe call in Parcel.
|
constructor(
|
||||||
*/
|
/**
|
||||||
stop(): Promise<void>;
|
* Signals when the watcher is ready to watch.
|
||||||
|
*/
|
||||||
|
readonly ready: Promise<unknown>,
|
||||||
|
readonly request: IRecursiveWatchRequest,
|
||||||
|
/**
|
||||||
|
* How often this watcher has been restarted in case of an unexpected
|
||||||
|
* shutdown.
|
||||||
|
*/
|
||||||
|
readonly restarts: number,
|
||||||
|
/**
|
||||||
|
* The cancellation token associated with the lifecycle of the watcher.
|
||||||
|
*/
|
||||||
|
readonly token: CancellationToken,
|
||||||
|
/**
|
||||||
|
* An event aggregator to coalesce events and reduce duplicates.
|
||||||
|
*/
|
||||||
|
readonly worker: RunOnceWorker<IFileChange>,
|
||||||
|
private readonly stopFn: () => Promise<void>
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._register(toDisposable(() => this.subscriptions.clear()));
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(path: string, callback: (change: IFileChange) => void): IDisposable {
|
||||||
|
path = URI.file(path).fsPath; // make sure to store the path in `fsPath` form to match it with events later
|
||||||
|
|
||||||
|
let subscriptions = this.subscriptions.get(path);
|
||||||
|
if (!subscriptions) {
|
||||||
|
subscriptions = new Set();
|
||||||
|
this.subscriptions.set(path, subscriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.add(callback);
|
||||||
|
|
||||||
|
return toDisposable(() => {
|
||||||
|
const subscriptions = this.subscriptions.get(path);
|
||||||
|
if (subscriptions) {
|
||||||
|
subscriptions.delete(callback);
|
||||||
|
|
||||||
|
if (subscriptions.size === 0) {
|
||||||
|
this.subscriptions.delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get subscriptionsCount(): number {
|
||||||
|
return this.subscriptions.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyFileChange(path: string, change: IFileChange): void {
|
||||||
|
const subscriptions = this.subscriptions.get(path);
|
||||||
|
if (subscriptions) {
|
||||||
|
for (const subscription of subscriptions) {
|
||||||
|
subscription(change);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyWatchFailed(): void {
|
||||||
|
this.didFail = true;
|
||||||
|
|
||||||
|
this._onDidFail.fire();
|
||||||
|
}
|
||||||
|
|
||||||
|
include(path: string): boolean {
|
||||||
|
if (!this.includes || this.includes.length === 0) {
|
||||||
|
return true; // no specific includes defined, include all
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.includes.some(include => include(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
exclude(path: string): boolean {
|
||||||
|
return Boolean(this.excludes?.some(exclude => exclude(path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(joinRestart: Promise<void> | undefined): Promise<void> {
|
||||||
|
this.didStop = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.stopFn();
|
||||||
|
} finally {
|
||||||
|
this._onDidStop.fire({ joinRestart });
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithSubscribe {
|
||||||
|
|
||||||
private static readonly MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE = new Map<parcelWatcher.EventType, number>(
|
private static readonly MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE = new Map<parcelWatcher.EventType, number>(
|
||||||
[
|
[
|
||||||
|
@ -73,7 +150,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
private readonly _onDidError = this._register(new Emitter<string>());
|
private readonly _onDidError = this._register(new Emitter<string>());
|
||||||
readonly onDidError = this._onDidError.event;
|
readonly onDidError = this._onDidError.event;
|
||||||
|
|
||||||
protected readonly watchers = new Set<IParcelWatcherInstance>();
|
readonly watchers = new Set<ParcelWatcherInstance>();
|
||||||
|
|
||||||
// A delay for collecting file changes from Parcel
|
// A delay for collecting file changes from Parcel
|
||||||
// before collecting them for coalescing and emitting.
|
// before collecting them for coalescing and emitting.
|
||||||
|
@ -155,7 +232,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private findWatcher(request: IRecursiveWatchRequest): IParcelWatcherInstance | undefined {
|
private findWatcher(request: IRecursiveWatchRequest): ParcelWatcherInstance | undefined {
|
||||||
for (const watcher of this.watchers) {
|
for (const watcher of this.watchers) {
|
||||||
|
|
||||||
// Requests or watchers with correlation always match on that
|
// Requests or watchers with correlation always match on that
|
||||||
|
@ -184,13 +261,13 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
const snapshotFile = randomPath(tmpdir(), 'vscode-watcher-snapshot');
|
const snapshotFile = randomPath(tmpdir(), 'vscode-watcher-snapshot');
|
||||||
|
|
||||||
// Remember as watcher instance
|
// Remember as watcher instance
|
||||||
const watcher: IParcelWatcherInstance = {
|
const watcher: ParcelWatcherInstance = new ParcelWatcherInstance(
|
||||||
|
instance.p,
|
||||||
request,
|
request,
|
||||||
ready: instance.p,
|
|
||||||
restarts,
|
restarts,
|
||||||
token: cts.token,
|
cts.token,
|
||||||
worker: new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),
|
new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),
|
||||||
stop: async () => {
|
async () => {
|
||||||
cts.dispose(true);
|
cts.dispose(true);
|
||||||
|
|
||||||
watcher.worker.flush();
|
watcher.worker.flush();
|
||||||
|
@ -199,15 +276,12 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
pollingWatcher.dispose();
|
pollingWatcher.dispose();
|
||||||
unlinkSync(snapshotFile);
|
unlinkSync(snapshotFile);
|
||||||
}
|
}
|
||||||
};
|
);
|
||||||
this.watchers.add(watcher);
|
this.watchers.add(watcher);
|
||||||
|
|
||||||
// Path checks for symbolic links / wrong casing
|
// Path checks for symbolic links / wrong casing
|
||||||
const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request);
|
const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request);
|
||||||
|
|
||||||
// Warm up include patterns for usage
|
|
||||||
const includePatterns = request.includes ? parseWatcherPatterns(request.path, request.includes) : undefined;
|
|
||||||
|
|
||||||
this.trace(`Started watching: '${realPath}' with polling interval '${pollingInterval}'`);
|
this.trace(`Started watching: '${realPath}' with polling interval '${pollingInterval}'`);
|
||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
@ -228,7 +302,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle & emit events
|
// Handle & emit events
|
||||||
this.onParcelEvents(parcelEvents, watcher, includePatterns, realPathDiffers, realPathLength);
|
this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store a snapshot of files to the snapshot file
|
// Store a snapshot of files to the snapshot file
|
||||||
|
@ -255,13 +329,13 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
const instance = new DeferredPromise<parcelWatcher.AsyncSubscription | undefined>();
|
const instance = new DeferredPromise<parcelWatcher.AsyncSubscription | undefined>();
|
||||||
|
|
||||||
// Remember as watcher instance
|
// Remember as watcher instance
|
||||||
const watcher: IParcelWatcherInstance = {
|
const watcher: ParcelWatcherInstance = new ParcelWatcherInstance(
|
||||||
|
instance.p,
|
||||||
request,
|
request,
|
||||||
ready: instance.p,
|
|
||||||
restarts,
|
restarts,
|
||||||
token: cts.token,
|
cts.token,
|
||||||
worker: new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),
|
new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),
|
||||||
stop: async () => {
|
async () => {
|
||||||
cts.dispose(true);
|
cts.dispose(true);
|
||||||
|
|
||||||
watcher.worker.flush();
|
watcher.worker.flush();
|
||||||
|
@ -270,15 +344,12 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
const watcherInstance = await instance.p;
|
const watcherInstance = await instance.p;
|
||||||
await watcherInstance?.unsubscribe();
|
await watcherInstance?.unsubscribe();
|
||||||
}
|
}
|
||||||
};
|
);
|
||||||
this.watchers.add(watcher);
|
this.watchers.add(watcher);
|
||||||
|
|
||||||
// Path checks for symbolic links / wrong casing
|
// Path checks for symbolic links / wrong casing
|
||||||
const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request);
|
const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request);
|
||||||
|
|
||||||
// Warm up include patterns for usage
|
|
||||||
const includePatterns = request.includes ? parseWatcherPatterns(request.path, request.includes) : undefined;
|
|
||||||
|
|
||||||
parcelWatcher.subscribe(realPath, (error, parcelEvents) => {
|
parcelWatcher.subscribe(realPath, (error, parcelEvents) => {
|
||||||
if (watcher.token.isCancellationRequested) {
|
if (watcher.token.isCancellationRequested) {
|
||||||
return; // return early when disposed
|
return; // return early when disposed
|
||||||
|
@ -293,7 +364,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle & emit events
|
// Handle & emit events
|
||||||
this.onParcelEvents(parcelEvents, watcher, includePatterns, realPathDiffers, realPathLength);
|
this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength);
|
||||||
}, {
|
}, {
|
||||||
backend: ParcelWatcher.PARCEL_WATCHER_BACKEND,
|
backend: ParcelWatcher.PARCEL_WATCHER_BACKEND,
|
||||||
ignore: watcher.request.excludes
|
ignore: watcher.request.excludes
|
||||||
|
@ -306,11 +377,12 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
|
|
||||||
instance.complete(undefined);
|
instance.complete(undefined);
|
||||||
|
|
||||||
|
watcher.notifyWatchFailed();
|
||||||
this._onDidWatchFail.fire(request);
|
this._onDidWatchFail.fire(request);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: IParcelWatcherInstance, includes: ParsedPattern[] | undefined, realPathDiffers: boolean, realPathLength: number): void {
|
private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: ParcelWatcherInstance, realPathDiffers: boolean, realPathLength: number): void {
|
||||||
if (parcelEvents.length === 0) {
|
if (parcelEvents.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -321,7 +393,7 @@ export class ParcelWatcher extends BaseWatcher 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(watcher, parcelEvents, includes);
|
const includedEvents = this.handleIncludes(watcher, parcelEvents);
|
||||||
|
|
||||||
// Add to event aggregator for later processing
|
// Add to event aggregator for later processing
|
||||||
for (const includedEvent of includedEvents) {
|
for (const includedEvent of includedEvents) {
|
||||||
|
@ -329,7 +401,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleIncludes(watcher: IParcelWatcherInstance, parcelEvents: parcelWatcher.Event[], includes: ParsedPattern[] | undefined): IFileChange[] {
|
private handleIncludes(watcher: ParcelWatcherInstance, parcelEvents: parcelWatcher.Event[]): IFileChange[] {
|
||||||
const events: IFileChange[] = [];
|
const events: IFileChange[] = [];
|
||||||
|
|
||||||
for (const { path, type: parcelEventType } of parcelEvents) {
|
for (const { path, type: parcelEventType } of parcelEvents) {
|
||||||
|
@ -339,7 +411,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply include filter if any
|
// Apply include filter if any
|
||||||
if (includes && includes.length > 0 && !includes.some(include => include(path))) {
|
if (!watcher.include(path)) {
|
||||||
if (this.verboseLogging) {
|
if (this.verboseLogging) {
|
||||||
this.trace(` >> ignored (not included) ${path}`);
|
this.trace(` >> ignored (not included) ${path}`);
|
||||||
}
|
}
|
||||||
|
@ -351,7 +423,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleParcelEvents(parcelEvents: IFileChange[], watcher: IParcelWatcherInstance): void {
|
private handleParcelEvents(parcelEvents: IFileChange[], watcher: ParcelWatcherInstance): void {
|
||||||
|
|
||||||
// Coalesce events: merge events of same kind
|
// Coalesce events: merge events of same kind
|
||||||
const coalescedEvents = coalesceEvents(parcelEvents);
|
const coalescedEvents = coalesceEvents(parcelEvents);
|
||||||
|
@ -368,14 +440,20 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitEvents(events: IFileChange[], watcher: IParcelWatcherInstance): void {
|
private emitEvents(events: IFileChange[], watcher: ParcelWatcherInstance): void {
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logging
|
for (const event of events) {
|
||||||
if (this.verboseLogging) {
|
|
||||||
for (const event of events) {
|
// Emit to instance subscriptions if any
|
||||||
|
if (watcher.subscriptionsCount > 0) {
|
||||||
|
watcher.notifyFileChange(event.resource.fsPath, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
if (this.verboseLogging) {
|
||||||
this.traceEvent(event, watcher.request);
|
this.traceEvent(event, watcher.request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -446,7 +524,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterEvents(events: IFileChange[], watcher: IParcelWatcherInstance): { events: IFileChange[]; rootDeleted?: boolean } {
|
private filterEvents(events: IFileChange[], watcher: ParcelWatcherInstance): { events: IFileChange[]; rootDeleted?: boolean } {
|
||||||
const filteredEvents: IFileChange[] = [];
|
const filteredEvents: IFileChange[] = [];
|
||||||
let rootDeleted = false;
|
let rootDeleted = false;
|
||||||
|
|
||||||
|
@ -475,29 +553,28 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
return { events: filteredEvents, rootDeleted };
|
return { events: filteredEvents, rootDeleted };
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWatchedPathDeleted(watcher: IParcelWatcherInstance): void {
|
private onWatchedPathDeleted(watcher: ParcelWatcherInstance): void {
|
||||||
this.warn('Watcher shutdown because watched path got deleted', watcher);
|
this.warn('Watcher shutdown because watched path got deleted', watcher);
|
||||||
|
|
||||||
this._onDidWatchFail.fire(watcher.request);
|
let legacyMonitored = false;
|
||||||
|
|
||||||
// Do monitoring of the request path parent unless this request
|
|
||||||
// can be handled via suspend/resume in the super class
|
|
||||||
//
|
|
||||||
// TODO@bpasero we should remove this logic in favor of the
|
|
||||||
// support in the super class so that we have 1 consistent
|
|
||||||
// solution for handling this.
|
|
||||||
|
|
||||||
if (!this.isCorrelated(watcher.request)) {
|
if (!this.isCorrelated(watcher.request)) {
|
||||||
this.legacyMonitorRequest(watcher);
|
// Do monitoring of the request path parent unless this request
|
||||||
|
// can be handled via suspend/resume in the super class
|
||||||
|
legacyMonitored = this.legacyMonitorRequest(watcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!legacyMonitored) {
|
||||||
|
watcher.notifyWatchFailed();
|
||||||
|
this._onDidWatchFail.fire(watcher.request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private legacyMonitorRequest(watcher: IParcelWatcherInstance): void {
|
private legacyMonitorRequest(watcher: ParcelWatcherInstance): boolean {
|
||||||
const parentPath = dirname(watcher.request.path);
|
const parentPath = dirname(watcher.request.path);
|
||||||
if (existsSync(parentPath)) {
|
if (existsSync(parentPath)) {
|
||||||
this.trace('Trying to watch on the parent path to restart the watcher...', watcher);
|
this.trace('Trying to watch on the parent path to restart the watcher...', watcher);
|
||||||
|
|
||||||
const nodeWatcher = new NodeJSFileWatcherLibrary({ path: parentPath, excludes: [], recursive: false, correlationId: watcher.request.correlationId }, changes => {
|
const nodeWatcher = new NodeJSFileWatcherLibrary({ path: parentPath, excludes: [], recursive: false, correlationId: watcher.request.correlationId }, undefined, changes => {
|
||||||
if (watcher.token.isCancellationRequested) {
|
if (watcher.token.isCancellationRequested) {
|
||||||
return; // return early when disposed
|
return; // return early when disposed
|
||||||
}
|
}
|
||||||
|
@ -522,10 +599,14 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
|
|
||||||
// Make sure to stop watching when the watcher is disposed
|
// Make sure to stop watching when the watcher is disposed
|
||||||
watcher.token.onCancellationRequested(() => nodeWatcher.dispose());
|
watcher.token.onCancellationRequested(() => nodeWatcher.dispose());
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onUnexpectedError(error: unknown, watcher?: IParcelWatcherInstance): void {
|
private onUnexpectedError(error: unknown, watcher?: ParcelWatcherInstance): void {
|
||||||
const msg = toErrorMessage(error);
|
const msg = toErrorMessage(error);
|
||||||
|
|
||||||
// Specially handle ENOSPC errors that can happen when
|
// Specially handle ENOSPC errors that can happen when
|
||||||
|
@ -559,7 +640,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected restartWatching(watcher: IParcelWatcherInstance, delay = 800): void {
|
protected restartWatching(watcher: ParcelWatcherInstance, delay = 800): void {
|
||||||
|
|
||||||
// Restart watcher delayed to accomodate for
|
// Restart watcher delayed to accomodate for
|
||||||
// changes on disk that have triggered the
|
// changes on disk that have triggered the
|
||||||
|
@ -569,15 +650,21 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
return; // return early when disposed
|
return; // return early when disposed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Await the watcher having stopped, as this is
|
const restartPromise = new DeferredPromise<void>();
|
||||||
// needed to properly re-watch the same path
|
try {
|
||||||
await this.stopWatching(watcher);
|
|
||||||
|
|
||||||
// Start watcher again counting the restarts
|
// Await the watcher having stopped, as this is
|
||||||
if (watcher.request.pollingInterval) {
|
// needed to properly re-watch the same path
|
||||||
this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1);
|
await this.stopWatching(watcher, restartPromise.p);
|
||||||
} else {
|
|
||||||
this.startWatching(watcher.request, watcher.restarts + 1);
|
// Start watcher again counting the restarts
|
||||||
|
if (watcher.request.pollingInterval) {
|
||||||
|
this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1);
|
||||||
|
} else {
|
||||||
|
this.startWatching(watcher.request, watcher.restarts + 1);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
restartPromise.complete();
|
||||||
}
|
}
|
||||||
}, delay);
|
}, delay);
|
||||||
|
|
||||||
|
@ -585,13 +672,13 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
watcher.token.onCancellationRequested(() => scheduler.dispose());
|
watcher.token.onCancellationRequested(() => scheduler.dispose());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async stopWatching(watcher: IParcelWatcherInstance): Promise<void> {
|
private async stopWatching(watcher: ParcelWatcherInstance, joinRestart?: Promise<void>): Promise<void> {
|
||||||
this.trace(`stopping file watcher`, watcher);
|
this.trace(`stopping file watcher`, watcher);
|
||||||
|
|
||||||
this.watchers.delete(watcher);
|
this.watchers.delete(watcher);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await watcher.stop();
|
await watcher.stop(joinRestart);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher);
|
this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher);
|
||||||
}
|
}
|
||||||
|
@ -694,25 +781,63 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcher {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subscribe(path: string, callback: (error: boolean, change?: IFileChange) => void): IDisposable | undefined {
|
||||||
|
for (const watcher of this.watchers) {
|
||||||
|
if (watcher.failed) {
|
||||||
|
continue; // watcher has already failed
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEqualOrParent(path, watcher.request.path, !isLinux)) {
|
||||||
|
continue; // watcher does not consider this path
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
watcher.exclude(path) ||
|
||||||
|
!watcher.include(path)
|
||||||
|
) {
|
||||||
|
continue; // parcel instance does not consider this path
|
||||||
|
}
|
||||||
|
|
||||||
|
const disposables = new DisposableStore();
|
||||||
|
|
||||||
|
disposables.add(Event.once(watcher.onDidStop)(async e => {
|
||||||
|
await e.joinRestart; // if we are restarting, await that so that we can possibly reuse this watcher again
|
||||||
|
if (disposables.isDisposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(true /* error */);
|
||||||
|
}));
|
||||||
|
disposables.add(Event.once(watcher.onDidFail)(() => callback(true /* error */)));
|
||||||
|
disposables.add(watcher.subscribe(path, change => callback(false, change)));
|
||||||
|
|
||||||
|
return disposables;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async setVerboseLogging(enabled: boolean): Promise<void> {
|
async setVerboseLogging(enabled: boolean): Promise<void> {
|
||||||
this.verboseLogging = enabled;
|
this.verboseLogging = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected trace(message: string, watcher?: IParcelWatcherInstance): void {
|
protected trace(message: string, watcher?: ParcelWatcherInstance): void {
|
||||||
if (this.verboseLogging) {
|
if (this.verboseLogging) {
|
||||||
this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher) });
|
this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected warn(message: string, watcher?: IParcelWatcherInstance) {
|
protected warn(message: string, watcher?: ParcelWatcherInstance) {
|
||||||
this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher) });
|
this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher) });
|
||||||
}
|
}
|
||||||
|
|
||||||
private error(message: string, watcher: IParcelWatcherInstance | undefined) {
|
private error(message: string, watcher: ParcelWatcherInstance | undefined) {
|
||||||
this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, watcher) });
|
this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, watcher) });
|
||||||
}
|
}
|
||||||
|
|
||||||
private toMessage(message: string, watcher?: IParcelWatcherInstance): string {
|
private toMessage(message: string, watcher?: ParcelWatcherInstance): string {
|
||||||
return watcher ? `[File Watcher (parcel)] ${message} (path: ${watcher.request.path})` : `[File Watcher (parcel)] ${message}`;
|
return watcher ? `[File Watcher (parcel)] ${message} (path: ${watcher.request.path})` : `[File Watcher (parcel)] ${message}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get recursiveWatcher() { return this; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,29 +4,61 @@
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { Disposable } from 'vs/base/common/lifecycle';
|
import { Disposable } from 'vs/base/common/lifecycle';
|
||||||
import { IUniversalWatcher, IUniversalWatchRequest } from 'vs/platform/files/common/watcher';
|
import { ILogMessage, IUniversalWatcher, IUniversalWatchRequest } from 'vs/platform/files/common/watcher';
|
||||||
import { Event } from 'vs/base/common/event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
import { ParcelWatcher } from 'vs/platform/files/node/watcher/parcel/parcelWatcher';
|
import { ParcelWatcher } from 'vs/platform/files/node/watcher/parcel/parcelWatcher';
|
||||||
import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher';
|
import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher';
|
||||||
import { Promises } from 'vs/base/common/async';
|
import { Promises } from 'vs/base/common/async';
|
||||||
|
import { computeStats } from 'vs/platform/files/node/watcher/watcherStats';
|
||||||
|
|
||||||
export class UniversalWatcher extends Disposable implements IUniversalWatcher {
|
export class UniversalWatcher extends Disposable implements IUniversalWatcher {
|
||||||
|
|
||||||
private readonly recursiveWatcher = this._register(new ParcelWatcher());
|
private readonly recursiveWatcher = this._register(new ParcelWatcher());
|
||||||
private readonly nonRecursiveWatcher = this._register(new NodeJSWatcher());
|
private readonly nonRecursiveWatcher = this._register(new NodeJSWatcher(this.recursiveWatcher));
|
||||||
|
|
||||||
readonly onDidChangeFile = Event.any(this.recursiveWatcher.onDidChangeFile, this.nonRecursiveWatcher.onDidChangeFile);
|
readonly onDidChangeFile = Event.any(this.recursiveWatcher.onDidChangeFile, this.nonRecursiveWatcher.onDidChangeFile);
|
||||||
readonly onDidLogMessage = Event.any(this.recursiveWatcher.onDidLogMessage, this.nonRecursiveWatcher.onDidLogMessage);
|
|
||||||
readonly onDidError = Event.any(this.recursiveWatcher.onDidError, this.nonRecursiveWatcher.onDidError);
|
readonly onDidError = Event.any(this.recursiveWatcher.onDidError, this.nonRecursiveWatcher.onDidError);
|
||||||
|
|
||||||
|
private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>());
|
||||||
|
readonly onDidLogMessage = Event.any(this._onDidLogMessage.event, this.recursiveWatcher.onDidLogMessage, this.nonRecursiveWatcher.onDidLogMessage);
|
||||||
|
|
||||||
|
private requests: IUniversalWatchRequest[] = [];
|
||||||
|
|
||||||
async watch(requests: IUniversalWatchRequest[]): Promise<void> {
|
async watch(requests: IUniversalWatchRequest[]): Promise<void> {
|
||||||
await Promises.settled([
|
this.requests = requests;
|
||||||
this.recursiveWatcher.watch(requests.filter(request => request.recursive)),
|
|
||||||
this.nonRecursiveWatcher.watch(requests.filter(request => !request.recursive))
|
// Watch recursively first to give recursive watchers a chance
|
||||||
]);
|
// to step in for non-recursive watch requests, thus reducing
|
||||||
|
// watcher duplication.
|
||||||
|
|
||||||
|
let error: Error | undefined;
|
||||||
|
try {
|
||||||
|
await this.recursiveWatcher.watch(requests.filter(request => request.recursive));
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.nonRecursiveWatcher.watch(requests.filter(request => !request.recursive));
|
||||||
|
} catch (e) {
|
||||||
|
if (!error) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setVerboseLogging(enabled: boolean): Promise<void> {
|
async setVerboseLogging(enabled: boolean): Promise<void> {
|
||||||
|
|
||||||
|
// Log stats
|
||||||
|
if (enabled && this.requests.length > 0) {
|
||||||
|
this._onDidLogMessage.fire({ type: 'trace', message: computeStats(this.requests, this.recursiveWatcher, this.nonRecursiveWatcher) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to watchers
|
||||||
await Promises.settled([
|
await Promises.settled([
|
||||||
this.recursiveWatcher.setVerboseLogging(enabled),
|
this.recursiveWatcher.setVerboseLogging(enabled),
|
||||||
this.nonRecursiveWatcher.setVerboseLogging(enabled)
|
this.nonRecursiveWatcher.setVerboseLogging(enabled)
|
||||||
|
|
207
src/vs/platform/files/node/watcher/watcherStats.ts
Normal file
207
src/vs/platform/files/node/watcher/watcherStats.ts
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { IUniversalWatchRequest } from 'vs/platform/files/common/watcher';
|
||||||
|
import { INodeJSWatcherInstance, NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher';
|
||||||
|
import { ParcelWatcher, ParcelWatcherInstance } from 'vs/platform/files/node/watcher/parcel/parcelWatcher';
|
||||||
|
|
||||||
|
export function computeStats(
|
||||||
|
requests: IUniversalWatchRequest[],
|
||||||
|
recursiveWatcher: ParcelWatcher,
|
||||||
|
nonRecursiveWatcher: NodeJSWatcher
|
||||||
|
): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
const recursiveRequests = sortByPathPrefix(requests.filter(request => request.recursive));
|
||||||
|
const recursiveRequestsStatus = computeRequestStatus(recursiveRequests, recursiveWatcher);
|
||||||
|
const recursiveWatcherStatus = computeRecursiveWatchStatus(recursiveWatcher);
|
||||||
|
|
||||||
|
const nonRecursiveRequests = sortByPathPrefix(requests.filter(request => !request.recursive));
|
||||||
|
const nonRecursiveRequestsStatus = computeRequestStatus(nonRecursiveRequests, nonRecursiveWatcher);
|
||||||
|
const nonRecursiveWatcherStatus = computeNonRecursiveWatchStatus(nonRecursiveWatcher);
|
||||||
|
|
||||||
|
lines.push('[Summary]');
|
||||||
|
lines.push(`- Recursive Requests: total: ${recursiveRequests.length}, suspended: ${recursiveRequestsStatus.suspended}, polling: ${recursiveRequestsStatus.polling}`);
|
||||||
|
lines.push(`- Non-Recursive Requests: total: ${nonRecursiveRequests.length}, suspended: ${nonRecursiveRequestsStatus.suspended}, polling: ${nonRecursiveRequestsStatus.polling}`);
|
||||||
|
lines.push(`- Recursive Watchers: total: ${recursiveWatcher.watchers.size}, active: ${recursiveWatcherStatus.active}, failed: ${recursiveWatcherStatus.failed}, stopped: ${recursiveWatcherStatus.stopped}`);
|
||||||
|
lines.push(`- Non-Recursive Watchers: total: ${nonRecursiveWatcher.watchers.size}, active: ${nonRecursiveWatcherStatus.active}, failed: ${nonRecursiveWatcherStatus.failed}, reusing: ${nonRecursiveWatcherStatus.reusing}`);
|
||||||
|
lines.push(`- I/O Handles Impact: total: ${recursiveRequestsStatus.polling + nonRecursiveRequestsStatus.polling + recursiveWatcherStatus.active + nonRecursiveWatcherStatus.active}`);
|
||||||
|
|
||||||
|
lines.push(`\n[Recursive Requests (${recursiveRequests.length}, suspended: ${recursiveRequestsStatus.suspended}, polling: ${recursiveRequestsStatus.polling})]:`);
|
||||||
|
for (const request of recursiveRequests) {
|
||||||
|
fillRequestStats(lines, request, recursiveWatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`\n[Non-Recursive Requests (${nonRecursiveRequests.length}, suspended: ${nonRecursiveRequestsStatus.suspended}, polling: ${nonRecursiveRequestsStatus.polling})]:`);
|
||||||
|
for (const request of nonRecursiveRequests) {
|
||||||
|
fillRequestStats(lines, request, nonRecursiveWatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
fillRecursiveWatcherStats(lines, recursiveWatcher);
|
||||||
|
fillNonRecursiveWatcherStats(lines, nonRecursiveWatcher);
|
||||||
|
|
||||||
|
let maxLength = 0;
|
||||||
|
for (const line of lines) {
|
||||||
|
maxLength = Math.max(maxLength, line.split('\t')[0].length);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const parts = line.split('\t');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
const padding = ' '.repeat(maxLength - parts[0].length);
|
||||||
|
lines[i] = `${parts[0]}${padding}\t${parts[1]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `\n\n[File Watcher] request stats:\n\n${lines.join('\n')}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeRequestStatus(requests: IUniversalWatchRequest[], watcher: ParcelWatcher | NodeJSWatcher): { suspended: number; polling: number } {
|
||||||
|
let polling = 0;
|
||||||
|
let suspended = 0;
|
||||||
|
|
||||||
|
for (const request of requests) {
|
||||||
|
const isSuspended = watcher.isSuspended(request);
|
||||||
|
if (isSuspended === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
suspended++;
|
||||||
|
|
||||||
|
if (isSuspended === 'polling') {
|
||||||
|
polling++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { suspended, polling };
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeRecursiveWatchStatus(recursiveWatcher: ParcelWatcher): { active: number; failed: number; stopped: number } {
|
||||||
|
let active = 0;
|
||||||
|
let failed = 0;
|
||||||
|
let stopped = 0;
|
||||||
|
|
||||||
|
for (const watcher of recursiveWatcher.watchers.values()) {
|
||||||
|
if (!watcher.failed && !watcher.stopped) {
|
||||||
|
active++;
|
||||||
|
}
|
||||||
|
if (watcher.failed) {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
if (watcher.stopped) {
|
||||||
|
stopped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { active, failed, stopped };
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeNonRecursiveWatchStatus(nonRecursiveWatcher: NodeJSWatcher): { active: number; failed: number; reusing: number } {
|
||||||
|
let active = 0;
|
||||||
|
let failed = 0;
|
||||||
|
let reusing = 0;
|
||||||
|
|
||||||
|
for (const watcher of nonRecursiveWatcher.watchers) {
|
||||||
|
if (!watcher.instance.failed && !watcher.instance.isReusingRecursiveWatcher) {
|
||||||
|
active++;
|
||||||
|
}
|
||||||
|
if (watcher.instance.failed) {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
if (watcher.instance.isReusingRecursiveWatcher) {
|
||||||
|
reusing++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { active, failed, reusing };
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByPathPrefix(requests: IUniversalWatchRequest[]): IUniversalWatchRequest[];
|
||||||
|
function sortByPathPrefix(requests: INodeJSWatcherInstance[]): INodeJSWatcherInstance[];
|
||||||
|
function sortByPathPrefix(requests: ParcelWatcherInstance[]): ParcelWatcherInstance[];
|
||||||
|
function sortByPathPrefix(requests: IUniversalWatchRequest[] | INodeJSWatcherInstance[] | ParcelWatcherInstance[]): IUniversalWatchRequest[] | INodeJSWatcherInstance[] | ParcelWatcherInstance[] {
|
||||||
|
requests.sort((r1, r2) => {
|
||||||
|
const p1 = isUniversalWatchRequest(r1) ? r1.path : r1.request.path;
|
||||||
|
const p2 = isUniversalWatchRequest(r2) ? r2.path : r2.request.path;
|
||||||
|
|
||||||
|
const minLength = Math.min(p1.length, p2.length);
|
||||||
|
for (let i = 0; i < minLength; i++) {
|
||||||
|
if (p1[i] !== p2[i]) {
|
||||||
|
return (p1[i] < p2[i]) ? -1 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p1.length - p2.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
return requests;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUniversalWatchRequest(obj: unknown): obj is IUniversalWatchRequest {
|
||||||
|
const candidate = obj as IUniversalWatchRequest | undefined;
|
||||||
|
|
||||||
|
return typeof candidate?.path === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillRequestStats(lines: string[], request: IUniversalWatchRequest, watcher: ParcelWatcher | NodeJSWatcher): void {
|
||||||
|
const decorations = [];
|
||||||
|
const suspended = watcher.isSuspended(request);
|
||||||
|
if (suspended !== false) {
|
||||||
|
if (suspended === 'polling') {
|
||||||
|
decorations.push('[SUSPENDED <polling>]');
|
||||||
|
} else {
|
||||||
|
decorations.push('[SUSPENDED <non-polling>]');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`${request.path}\t${decorations.length > 0 ? decorations.join(' ') + ' ' : ''}(${requestDetailsToString(request)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestDetailsToString(request: IUniversalWatchRequest): string {
|
||||||
|
return `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>'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillRecursiveWatcherStats(lines: string[], recursiveWatcher: ParcelWatcher): void {
|
||||||
|
const watchers = sortByPathPrefix(Array.from(recursiveWatcher.watchers.values()));
|
||||||
|
|
||||||
|
const { active, failed, stopped } = computeRecursiveWatchStatus(recursiveWatcher);
|
||||||
|
lines.push(`\n[Recursive Watchers (${watchers.length}, active: ${active}, failed: ${failed}, stopped: ${stopped})]:`);
|
||||||
|
|
||||||
|
for (const watcher of watchers) {
|
||||||
|
const decorations = [];
|
||||||
|
if (watcher.failed) {
|
||||||
|
decorations.push('[FAILED]');
|
||||||
|
}
|
||||||
|
if (watcher.stopped) {
|
||||||
|
decorations.push('[STOPPED]');
|
||||||
|
}
|
||||||
|
if (watcher.subscriptionsCount > 0) {
|
||||||
|
decorations.push(`[SUBSCRIBED:${watcher.subscriptionsCount}]`);
|
||||||
|
}
|
||||||
|
if (watcher.restarts > 0) {
|
||||||
|
decorations.push(`[RESTARTED:${watcher.restarts}]`);
|
||||||
|
}
|
||||||
|
lines.push(`${watcher.request.path}\t${decorations.length > 0 ? decorations.join(' ') + ' ' : ''}(${requestDetailsToString(watcher.request)})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillNonRecursiveWatcherStats(lines: string[], nonRecursiveWatcher: NodeJSWatcher): void {
|
||||||
|
const watchers = sortByPathPrefix(Array.from(nonRecursiveWatcher.watchers.values()));
|
||||||
|
|
||||||
|
const { active, failed, reusing } = computeNonRecursiveWatchStatus(nonRecursiveWatcher);
|
||||||
|
lines.push(`\n[Non-Recursive Watchers (${watchers.length}, active: ${active}, failed: ${failed}, reusing: ${reusing})]:`);
|
||||||
|
|
||||||
|
for (const watcher of watchers) {
|
||||||
|
const decorations = [];
|
||||||
|
if (watcher.instance.failed) {
|
||||||
|
decorations.push('[FAILED]');
|
||||||
|
}
|
||||||
|
if (watcher.instance.isReusingRecursiveWatcher) {
|
||||||
|
decorations.push('[REUSING]');
|
||||||
|
}
|
||||||
|
lines.push(`${watcher.request.path}\t${decorations.length > 0 ? decorations.join(' ') + ' ' : ''}(${requestDetailsToString(watcher.request)})`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,12 +3,13 @@
|
||||||
* 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 * as assert from 'assert';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { basename, dirname, join } from 'vs/base/common/path';
|
import { basename, dirname, join } from 'vs/base/common/path';
|
||||||
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 } from 'vs/platform/files/common/files';
|
||||||
import { INonRecursiveWatchRequest } from 'vs/platform/files/common/watcher';
|
import { INonRecursiveWatchRequest, IRecursiveWatcherWithSubscribe } from 'vs/platform/files/common/watcher';
|
||||||
import { watchFileContents } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib';
|
import { watchFileContents } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib';
|
||||||
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
|
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
|
||||||
import { getDriveLetter } from 'vs/base/common/extpath';
|
import { getDriveLetter } from 'vs/base/common/extpath';
|
||||||
|
@ -21,6 +22,7 @@ import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { addUNCHostToAllowlist } from 'vs/base/node/unc';
|
import { addUNCHostToAllowlist } from 'vs/base/node/unc';
|
||||||
import { Emitter, Event } from 'vs/base/common/event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.integrationTest';
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -61,7 +63,20 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
enableLogging(false);
|
enableLogging(false);
|
||||||
|
|
||||||
setup(async () => {
|
setup(async () => {
|
||||||
watcher = new TestNodeJSWatcher();
|
await createWatcher(undefined);
|
||||||
|
|
||||||
|
testDir = URI.file(getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher')).fsPath;
|
||||||
|
|
||||||
|
const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath;
|
||||||
|
|
||||||
|
await Promises.copy(sourceDir, testDir, { preserveSymlinks: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createWatcher(accessor: IRecursiveWatcherWithSubscribe | undefined) {
|
||||||
|
await watcher?.stop();
|
||||||
|
watcher?.dispose();
|
||||||
|
|
||||||
|
watcher = new TestNodeJSWatcher(accessor);
|
||||||
watcher?.setVerboseLogging(loggingEnabled);
|
watcher?.setVerboseLogging(loggingEnabled);
|
||||||
|
|
||||||
watcher.onDidLogMessage(e => {
|
watcher.onDidLogMessage(e => {
|
||||||
|
@ -75,13 +90,7 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
console.log(`[non-recursive watcher test error] ${e}`);
|
console.log(`[non-recursive watcher test error] ${e}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher');
|
|
||||||
|
|
||||||
const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath;
|
|
||||||
|
|
||||||
await Promises.copy(sourceDir, testDir, { preserveSymlinks: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
teardown(async () => {
|
teardown(async () => {
|
||||||
await watcher.stop();
|
await watcher.stop();
|
||||||
|
@ -128,7 +137,13 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
}
|
}
|
||||||
|
|
||||||
test('basics (folder watch)', async function () {
|
test('basics (folder watch)', async function () {
|
||||||
await watcher.watch([{ path: testDir, excludes: [], recursive: false }]);
|
const request = { path: testDir, excludes: [], recursive: false };
|
||||||
|
await watcher.watch([request]);
|
||||||
|
assert.strictEqual(watcher.isSuspended(request), false);
|
||||||
|
|
||||||
|
const instance = Array.from(watcher.watchers)[0].instance;
|
||||||
|
assert.strictEqual(instance.isReusingRecursiveWatcher, false);
|
||||||
|
assert.strictEqual(instance.failed, false);
|
||||||
|
|
||||||
// New file
|
// New file
|
||||||
const newFilePath = join(testDir, 'newFile.txt');
|
const newFilePath = join(testDir, 'newFile.txt');
|
||||||
|
@ -236,7 +251,13 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
|
||||||
test('basics (file watch)', async function () {
|
test('basics (file watch)', async function () {
|
||||||
const filePath = join(testDir, 'lorem.txt');
|
const filePath = join(testDir, 'lorem.txt');
|
||||||
await watcher.watch([{ path: filePath, excludes: [], recursive: false }]);
|
const request = { path: filePath, excludes: [], recursive: false };
|
||||||
|
await watcher.watch([request]);
|
||||||
|
assert.strictEqual(watcher.isSuspended(request), false);
|
||||||
|
|
||||||
|
const instance = Array.from(watcher.watchers)[0].instance;
|
||||||
|
assert.strictEqual(instance.isReusingRecursiveWatcher, false);
|
||||||
|
assert.strictEqual(instance.failed, false);
|
||||||
|
|
||||||
// Change file
|
// Change file
|
||||||
let changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED);
|
let changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED);
|
||||||
|
@ -554,17 +575,20 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
|
||||||
await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]);
|
await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]);
|
||||||
|
|
||||||
|
const instance = Array.from(watcher.watchers)[0].instance;
|
||||||
|
|
||||||
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
||||||
const changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, 1);
|
const changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, 1);
|
||||||
Promises.unlink(filePath);
|
Promises.unlink(filePath);
|
||||||
await onDidWatchFail;
|
await onDidWatchFail;
|
||||||
await changeFuture;
|
await changeFuture;
|
||||||
|
assert.strictEqual(instance.failed, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
(isMacintosh /* macOS: does not seem to report deletes on folders */ ? test.skip : test)('deleting watched path emits watcher fail and delete event when correlated (folder watch)', async function () {
|
(isMacintosh || isWindows /* macOS: does not seem to report deletes on folders | Windows: reports on('error') event only */ ? test.skip : test)('deleting watched path emits watcher fail and delete event when correlated (folder watch)', async function () {
|
||||||
const folderPath = join(testDir, 'deep');
|
const folderPath = join(testDir, 'deep');
|
||||||
|
|
||||||
await watcher.watch([{ path: folderPath, excludes: [], recursive: false }]);
|
await watcher.watch([{ path: folderPath, excludes: [], recursive: false, correlationId: 1 }]);
|
||||||
|
|
||||||
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
||||||
const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, 1);
|
const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, 1);
|
||||||
|
@ -577,8 +601,10 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
const filePath = join(testDir, 'not-found.txt');
|
const filePath = join(testDir, 'not-found.txt');
|
||||||
|
|
||||||
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
||||||
await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]);
|
const request = { path: filePath, excludes: [], recursive: false, correlationId: 1 };
|
||||||
|
await watcher.watch([request]);
|
||||||
await onDidWatchFail;
|
await onDidWatchFail;
|
||||||
|
assert.strictEqual(watcher.isSuspended(request), 'polling');
|
||||||
|
|
||||||
await basicCrudTest(filePath, undefined, 1, undefined, true);
|
await basicCrudTest(filePath, undefined, 1, undefined, true);
|
||||||
await basicCrudTest(filePath, undefined, 1, undefined, true);
|
await basicCrudTest(filePath, undefined, 1, undefined, true);
|
||||||
|
@ -586,11 +612,13 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
|
||||||
test('correlated watch requests support suspend/resume (file, exists in beginning)', async function () {
|
test('correlated watch requests support suspend/resume (file, exists in beginning)', async function () {
|
||||||
const filePath = join(testDir, 'lorem.txt');
|
const filePath = join(testDir, 'lorem.txt');
|
||||||
await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId: 1 }]);
|
const request = { path: filePath, excludes: [], recursive: false, correlationId: 1 };
|
||||||
|
await watcher.watch([request]);
|
||||||
|
|
||||||
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
||||||
await basicCrudTest(filePath, true, 1);
|
await basicCrudTest(filePath, true, 1);
|
||||||
await onDidWatchFail;
|
await onDidWatchFail;
|
||||||
|
assert.strictEqual(watcher.isSuspended(request), 'polling');
|
||||||
|
|
||||||
await basicCrudTest(filePath, undefined, 1, undefined, true);
|
await basicCrudTest(filePath, undefined, 1, undefined, true);
|
||||||
});
|
});
|
||||||
|
@ -599,8 +627,10 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
let onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
let onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
||||||
|
|
||||||
const folderPath = join(testDir, 'not-found');
|
const folderPath = join(testDir, 'not-found');
|
||||||
await watcher.watch([{ path: folderPath, excludes: [], recursive: false, correlationId: 1 }]);
|
const request = { path: folderPath, excludes: [], recursive: false, correlationId: 1 };
|
||||||
|
await watcher.watch([request]);
|
||||||
await onDidWatchFail;
|
await onDidWatchFail;
|
||||||
|
assert.strictEqual(watcher.isSuspended(request), 'polling');
|
||||||
|
|
||||||
let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1);
|
let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1);
|
||||||
let onDidWatch = Event.toPromise(watcher.onDidWatch);
|
let onDidWatch = Event.toPromise(watcher.onDidWatch);
|
||||||
|
@ -608,6 +638,8 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
await changeFuture;
|
await changeFuture;
|
||||||
await onDidWatch;
|
await onDidWatch;
|
||||||
|
|
||||||
|
assert.strictEqual(watcher.isSuspended(request), false);
|
||||||
|
|
||||||
const filePath = join(folderPath, 'newFile.txt');
|
const filePath = join(folderPath, 'newFile.txt');
|
||||||
await basicCrudTest(filePath, undefined, 1);
|
await basicCrudTest(filePath, undefined, 1);
|
||||||
|
|
||||||
|
@ -649,4 +681,83 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
|
||||||
await basicCrudTest(filePath, undefined, 1);
|
await basicCrudTest(filePath, undefined, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parcel watcher reused when present for non-recursive file watching (uncorrelated)', function () {
|
||||||
|
return testParcelWatcherReused(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parcel watcher reused when present for non-recursive file watching (correlated)', function () {
|
||||||
|
return testParcelWatcherReused(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
function createParcelWatcher() {
|
||||||
|
const recursiveWatcher = new TestParcelWatcher();
|
||||||
|
recursiveWatcher.setVerboseLogging(loggingEnabled);
|
||||||
|
recursiveWatcher.onDidLogMessage(e => {
|
||||||
|
if (loggingEnabled) {
|
||||||
|
console.log(`[recursive watcher test message] ${e.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
recursiveWatcher.onDidError(e => {
|
||||||
|
if (loggingEnabled) {
|
||||||
|
console.log(`[recursive watcher test error] ${e}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return recursiveWatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testParcelWatcherReused(correlationId: number | undefined) {
|
||||||
|
const recursiveWatcher = createParcelWatcher();
|
||||||
|
await recursiveWatcher.watch([{ path: testDir, excludes: [], recursive: true, correlationId: 1 }]);
|
||||||
|
|
||||||
|
const recursiveInstance = Array.from(recursiveWatcher.watchers)[0];
|
||||||
|
assert.strictEqual(recursiveInstance.subscriptionsCount, 0);
|
||||||
|
|
||||||
|
await createWatcher(recursiveWatcher);
|
||||||
|
|
||||||
|
const filePath = join(testDir, 'deep', 'conway.js');
|
||||||
|
await watcher.watch([{ path: filePath, excludes: [], recursive: false, correlationId }]);
|
||||||
|
|
||||||
|
const { instance } = Array.from(watcher.watchers)[0];
|
||||||
|
assert.strictEqual(instance.isReusingRecursiveWatcher, true);
|
||||||
|
assert.strictEqual(recursiveInstance.subscriptionsCount, 1);
|
||||||
|
|
||||||
|
let changeFuture = awaitEvent(watcher, filePath, isMacintosh /* somehow fsevents seems to report still on the initial create from test setup */ ? FileChangeType.ADDED : FileChangeType.UPDATED, correlationId);
|
||||||
|
await Promises.writeFile(filePath, 'Hello World');
|
||||||
|
await changeFuture;
|
||||||
|
|
||||||
|
await recursiveWatcher.stop();
|
||||||
|
recursiveWatcher.dispose();
|
||||||
|
|
||||||
|
await timeout(500); // give the watcher some time to restart
|
||||||
|
|
||||||
|
changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED, correlationId);
|
||||||
|
await Promises.writeFile(filePath, 'Hello World');
|
||||||
|
await changeFuture;
|
||||||
|
|
||||||
|
assert.strictEqual(instance.isReusingRecursiveWatcher, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('correlated watch requests support suspend/resume (file, does not exist in beginning, parcel watcher reused)', async function () {
|
||||||
|
const recursiveWatcher = createParcelWatcher();
|
||||||
|
await recursiveWatcher.watch([{ path: testDir, excludes: [], recursive: true }]);
|
||||||
|
|
||||||
|
await createWatcher(recursiveWatcher);
|
||||||
|
|
||||||
|
const filePath = join(testDir, 'not-found-2.txt');
|
||||||
|
|
||||||
|
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
||||||
|
const request = { path: filePath, excludes: [], recursive: false, correlationId: 1 };
|
||||||
|
await watcher.watch([request]);
|
||||||
|
await onDidWatchFail;
|
||||||
|
assert.strictEqual(watcher.isSuspended(request), true);
|
||||||
|
|
||||||
|
const changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, 1);
|
||||||
|
await Promises.writeFile(filePath, 'Hello World');
|
||||||
|
await changeFuture;
|
||||||
|
|
||||||
|
assert.strictEqual(watcher.isSuspended(request), false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,6 +21,40 @@ import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { addUNCHostToAllowlist } from 'vs/base/node/unc';
|
import { addUNCHostToAllowlist } from 'vs/base/node/unc';
|
||||||
import { Emitter, Event } from 'vs/base/common/event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||||
|
|
||||||
|
export class TestParcelWatcher extends ParcelWatcher {
|
||||||
|
|
||||||
|
protected override readonly suspendedWatchRequestPollingInterval = 100;
|
||||||
|
|
||||||
|
private readonly _onDidWatch = this._register(new Emitter<void>());
|
||||||
|
readonly onDidWatch = this._onDidWatch.event;
|
||||||
|
|
||||||
|
readonly onWatchFail = this._onDidWatchFail.event;
|
||||||
|
|
||||||
|
testRemoveDuplicateRequests(paths: string[], excludes: string[] = []): string[] {
|
||||||
|
|
||||||
|
// Work with strings as paths to simplify testing
|
||||||
|
const requests: IRecursiveWatchRequest[] = paths.map(path => {
|
||||||
|
return { path, excludes, recursive: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */).map(request => request.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise<void> {
|
||||||
|
await super.doWatch(requests);
|
||||||
|
await this.whenReady();
|
||||||
|
|
||||||
|
this._onDidWatch.fire();
|
||||||
|
}
|
||||||
|
|
||||||
|
async whenReady(): Promise<void> {
|
||||||
|
for (const watcher of this.watchers) {
|
||||||
|
await watcher.ready;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -29,39 +63,6 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
|
||||||
((process.env['BUILD_SOURCEVERSION'] || process.env['CI']) ? suite.skip : flakySuite)('File Watcher (parcel)', () => {
|
((process.env['BUILD_SOURCEVERSION'] || process.env['CI']) ? suite.skip : flakySuite)('File Watcher (parcel)', () => {
|
||||||
|
|
||||||
class TestParcelWatcher extends ParcelWatcher {
|
|
||||||
|
|
||||||
protected override readonly suspendedWatchRequestPollingInterval = 100;
|
|
||||||
|
|
||||||
private readonly _onDidWatch = this._register(new Emitter<void>());
|
|
||||||
readonly onDidWatch = this._onDidWatch.event;
|
|
||||||
|
|
||||||
readonly onWatchFail = this._onDidWatchFail.event;
|
|
||||||
|
|
||||||
testRemoveDuplicateRequests(paths: string[], excludes: string[] = []): string[] {
|
|
||||||
|
|
||||||
// Work with strings as paths to simplify testing
|
|
||||||
const requests: IRecursiveWatchRequest[] = paths.map(path => {
|
|
||||||
return { path, excludes, recursive: true };
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.removeDuplicateRequests(requests, false /* validate paths skipped for tests */).map(request => request.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise<void> {
|
|
||||||
await super.doWatch(requests);
|
|
||||||
await this.whenReady();
|
|
||||||
|
|
||||||
this._onDidWatch.fire();
|
|
||||||
}
|
|
||||||
|
|
||||||
async whenReady(): Promise<void> {
|
|
||||||
for (const watcher of this.watchers) {
|
|
||||||
await watcher.ready;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let testDir: string;
|
let testDir: string;
|
||||||
let watcher: TestParcelWatcher;
|
let watcher: TestParcelWatcher;
|
||||||
|
|
||||||
|
@ -90,7 +91,7 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher');
|
testDir = URI.file(getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher')).fsPath;
|
||||||
|
|
||||||
const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath;
|
const sourceDir = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/service').fsPath;
|
||||||
|
|
||||||
|
@ -98,7 +99,18 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
});
|
});
|
||||||
|
|
||||||
teardown(async () => {
|
teardown(async () => {
|
||||||
|
const watchers = watcher.watchers.size;
|
||||||
|
let stoppedInstances = 0;
|
||||||
|
for (const instance of watcher.watchers) {
|
||||||
|
Event.once(instance.onDidStop)(() => {
|
||||||
|
if (instance.stopped) {
|
||||||
|
stoppedInstances++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await watcher.stop();
|
await watcher.stop();
|
||||||
|
assert.strictEqual(stoppedInstances, watchers, 'All watchers must be stopped before the test ends');
|
||||||
watcher.dispose();
|
watcher.dispose();
|
||||||
|
|
||||||
// Possible that the file watcher is still holding
|
// Possible that the file watcher is still holding
|
||||||
|
@ -170,37 +182,68 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
}
|
}
|
||||||
|
|
||||||
test('basics', async function () {
|
test('basics', async function () {
|
||||||
await watcher.watch([{ path: testDir, excludes: [], recursive: true }]);
|
const request = { path: testDir, excludes: [], recursive: true };
|
||||||
|
await watcher.watch([request]);
|
||||||
|
assert.strictEqual(watcher.watchers.size, watcher.watchers.size);
|
||||||
|
|
||||||
|
const instance = Array.from(watcher.watchers)[0];
|
||||||
|
assert.strictEqual(request, instance.request);
|
||||||
|
assert.strictEqual(instance.failed, false);
|
||||||
|
assert.strictEqual(instance.stopped, false);
|
||||||
|
|
||||||
|
const disposables = new DisposableStore();
|
||||||
|
|
||||||
|
const subscriptions1 = new Map<string, FileChangeType>();
|
||||||
|
const subscriptions2 = new Map<string, FileChangeType>();
|
||||||
|
|
||||||
// New file
|
// New file
|
||||||
const newFilePath = join(testDir, 'deep', 'newFile.txt');
|
const newFilePath = join(testDir, 'deep', 'newFile.txt');
|
||||||
|
disposables.add(instance.subscribe(newFilePath, change => subscriptions1.set(change.resource.fsPath, change.type)));
|
||||||
|
disposables.add(instance.subscribe(newFilePath, change => subscriptions2.set(change.resource.fsPath, change.type))); // can subscribe multiple times
|
||||||
|
assert.strictEqual(instance.include(newFilePath), true);
|
||||||
|
assert.strictEqual(instance.exclude(newFilePath), false);
|
||||||
let changeFuture: Promise<unknown> = awaitEvent(watcher, newFilePath, FileChangeType.ADDED);
|
let changeFuture: Promise<unknown> = awaitEvent(watcher, newFilePath, FileChangeType.ADDED);
|
||||||
await Promises.writeFile(newFilePath, 'Hello World');
|
await Promises.writeFile(newFilePath, 'Hello World');
|
||||||
await changeFuture;
|
await changeFuture;
|
||||||
|
assert.strictEqual(subscriptions1.get(newFilePath), FileChangeType.ADDED);
|
||||||
|
assert.strictEqual(subscriptions2.get(newFilePath), FileChangeType.ADDED);
|
||||||
|
|
||||||
// New folder
|
// New folder
|
||||||
const newFolderPath = join(testDir, 'deep', 'New Folder');
|
const newFolderPath = join(testDir, 'deep', 'New Folder');
|
||||||
|
disposables.add(instance.subscribe(newFolderPath, change => subscriptions1.set(change.resource.fsPath, change.type)));
|
||||||
|
const disposable = instance.subscribe(newFolderPath, change => subscriptions2.set(change.resource.fsPath, change.type));
|
||||||
|
disposable.dispose();
|
||||||
|
assert.strictEqual(instance.include(newFolderPath), true);
|
||||||
|
assert.strictEqual(instance.exclude(newFolderPath), false);
|
||||||
changeFuture = awaitEvent(watcher, newFolderPath, FileChangeType.ADDED);
|
changeFuture = awaitEvent(watcher, newFolderPath, FileChangeType.ADDED);
|
||||||
await Promises.mkdir(newFolderPath);
|
await Promises.mkdir(newFolderPath);
|
||||||
await changeFuture;
|
await changeFuture;
|
||||||
|
assert.strictEqual(subscriptions1.get(newFolderPath), FileChangeType.ADDED);
|
||||||
|
assert.strictEqual(subscriptions2.has(newFolderPath), false /* subscription was disposed before the event */);
|
||||||
|
|
||||||
// Rename file
|
// Rename file
|
||||||
let renamedFilePath = join(testDir, 'deep', 'renamedFile.txt');
|
let renamedFilePath = join(testDir, 'deep', 'renamedFile.txt');
|
||||||
|
disposables.add(instance.subscribe(renamedFilePath, change => subscriptions1.set(change.resource.fsPath, change.type)));
|
||||||
changeFuture = Promise.all([
|
changeFuture = Promise.all([
|
||||||
awaitEvent(watcher, newFilePath, FileChangeType.DELETED),
|
awaitEvent(watcher, newFilePath, FileChangeType.DELETED),
|
||||||
awaitEvent(watcher, renamedFilePath, FileChangeType.ADDED)
|
awaitEvent(watcher, renamedFilePath, FileChangeType.ADDED)
|
||||||
]);
|
]);
|
||||||
await Promises.rename(newFilePath, renamedFilePath);
|
await Promises.rename(newFilePath, renamedFilePath);
|
||||||
await changeFuture;
|
await changeFuture;
|
||||||
|
assert.strictEqual(subscriptions1.get(newFilePath), FileChangeType.DELETED);
|
||||||
|
assert.strictEqual(subscriptions1.get(renamedFilePath), FileChangeType.ADDED);
|
||||||
|
|
||||||
// Rename folder
|
// Rename folder
|
||||||
let renamedFolderPath = join(testDir, 'deep', 'Renamed Folder');
|
let renamedFolderPath = join(testDir, 'deep', 'Renamed Folder');
|
||||||
|
disposables.add(instance.subscribe(renamedFolderPath, change => subscriptions1.set(change.resource.fsPath, change.type)));
|
||||||
changeFuture = Promise.all([
|
changeFuture = Promise.all([
|
||||||
awaitEvent(watcher, newFolderPath, FileChangeType.DELETED),
|
awaitEvent(watcher, newFolderPath, FileChangeType.DELETED),
|
||||||
awaitEvent(watcher, renamedFolderPath, FileChangeType.ADDED)
|
awaitEvent(watcher, renamedFolderPath, FileChangeType.ADDED)
|
||||||
]);
|
]);
|
||||||
await Promises.rename(newFolderPath, renamedFolderPath);
|
await Promises.rename(newFolderPath, renamedFolderPath);
|
||||||
await changeFuture;
|
await changeFuture;
|
||||||
|
assert.strictEqual(subscriptions1.get(newFolderPath), FileChangeType.DELETED);
|
||||||
|
assert.strictEqual(subscriptions1.get(renamedFolderPath), FileChangeType.ADDED);
|
||||||
|
|
||||||
// Rename file (same name, different case)
|
// Rename file (same name, different case)
|
||||||
const caseRenamedFilePath = join(testDir, 'deep', 'RenamedFile.txt');
|
const caseRenamedFilePath = join(testDir, 'deep', 'RenamedFile.txt');
|
||||||
|
@ -280,13 +323,19 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
|
||||||
// Delete file
|
// Delete file
|
||||||
changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.DELETED);
|
changeFuture = awaitEvent(watcher, copiedFilepath, FileChangeType.DELETED);
|
||||||
|
disposables.add(instance.subscribe(copiedFilepath, change => subscriptions1.set(change.resource.fsPath, change.type)));
|
||||||
await Promises.unlink(copiedFilepath);
|
await Promises.unlink(copiedFilepath);
|
||||||
await changeFuture;
|
await changeFuture;
|
||||||
|
assert.strictEqual(subscriptions1.get(copiedFilepath), FileChangeType.DELETED);
|
||||||
|
|
||||||
// Delete folder
|
// Delete folder
|
||||||
changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.DELETED);
|
changeFuture = awaitEvent(watcher, copiedFolderpath, FileChangeType.DELETED);
|
||||||
|
disposables.add(instance.subscribe(copiedFolderpath, change => subscriptions1.set(change.resource.fsPath, change.type)));
|
||||||
await Promises.rmdir(copiedFolderpath);
|
await Promises.rmdir(copiedFolderpath);
|
||||||
await changeFuture;
|
await changeFuture;
|
||||||
|
assert.strictEqual(subscriptions1.get(copiedFolderpath), FileChangeType.DELETED);
|
||||||
|
|
||||||
|
disposables.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
(isMacintosh /* this test seems not possible with fsevents backend */ ? test.skip : test)('basics (atomic writes)', async function () {
|
(isMacintosh /* this test seems not possible with fsevents backend */ ? test.skip : test)('basics (atomic writes)', async function () {
|
||||||
|
@ -676,26 +725,59 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
|
||||||
await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]);
|
await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]);
|
||||||
|
|
||||||
|
let failed = false;
|
||||||
|
const instance = Array.from(watcher.watchers)[0];
|
||||||
|
assert.strictEqual(instance.include(folderPath), true);
|
||||||
|
instance.onDidFail(() => failed = true);
|
||||||
|
|
||||||
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
const onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
||||||
const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, undefined, 1);
|
const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.DELETED, undefined, 1);
|
||||||
Promises.rm(folderPath, RimRafMode.UNLINK);
|
Promises.rm(folderPath, RimRafMode.UNLINK);
|
||||||
await onDidWatchFail;
|
await onDidWatchFail;
|
||||||
await changeFuture;
|
await changeFuture;
|
||||||
|
assert.strictEqual(failed, true);
|
||||||
|
assert.strictEqual(instance.failed, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('correlated watch requests support suspend/resume (folder, does not exist in beginning)', async () => {
|
test('correlated watch requests support suspend/resume (folder, does not exist in beginning, not reusing watcher)', async () => {
|
||||||
|
await testCorrelatedWatchFolderDoesNotExist(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correlated watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => {
|
||||||
|
await testCorrelatedWatchFolderDoesNotExist(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function testCorrelatedWatchFolderDoesNotExist(reuseExistingWatcher: boolean) {
|
||||||
let onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
let onDidWatchFail = Event.toPromise(watcher.onWatchFail);
|
||||||
|
|
||||||
const folderPath = join(testDir, 'not-found');
|
const folderPath = join(testDir, 'not-found');
|
||||||
await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]);
|
|
||||||
|
const requests: IRecursiveWatchRequest[] = [];
|
||||||
|
if (reuseExistingWatcher) {
|
||||||
|
requests.push({ path: testDir, excludes: [], recursive: true });
|
||||||
|
await watcher.watch(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request: IRecursiveWatchRequest = { path: folderPath, excludes: [], recursive: true, correlationId: 1 };
|
||||||
|
requests.push(request);
|
||||||
|
|
||||||
|
await watcher.watch(requests);
|
||||||
await onDidWatchFail;
|
await onDidWatchFail;
|
||||||
|
|
||||||
|
if (reuseExistingWatcher) {
|
||||||
|
assert.strictEqual(watcher.isSuspended(request), true);
|
||||||
|
} else {
|
||||||
|
assert.strictEqual(watcher.isSuspended(request), 'polling');
|
||||||
|
}
|
||||||
|
|
||||||
let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1);
|
let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1);
|
||||||
let onDidWatch = Event.toPromise(watcher.onDidWatch);
|
let onDidWatch = Event.toPromise(watcher.onDidWatch);
|
||||||
await Promises.mkdir(folderPath);
|
await Promises.mkdir(folderPath);
|
||||||
await changeFuture;
|
await changeFuture;
|
||||||
await onDidWatch;
|
await onDidWatch;
|
||||||
|
|
||||||
|
assert.strictEqual(watcher.isSuspended(request), false);
|
||||||
|
|
||||||
const filePath = join(folderPath, 'newFile.txt');
|
const filePath = join(folderPath, 'newFile.txt');
|
||||||
await basicCrudTest(filePath, 1);
|
await basicCrudTest(filePath, 1);
|
||||||
|
|
||||||
|
@ -710,11 +792,25 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
await onDidWatch;
|
await onDidWatch;
|
||||||
|
|
||||||
await basicCrudTest(filePath, 1);
|
await basicCrudTest(filePath, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('correlated watch requests support suspend/resume (folder, exist in beginning, not reusing watcher)', async () => {
|
||||||
|
await testCorrelatedWatchFolderExists(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('correlated watch requests support suspend/resume (folder, exist in beginning)', async () => {
|
test('correlated watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => {
|
||||||
|
await testCorrelatedWatchFolderExists(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function testCorrelatedWatchFolderExists(reuseExistingWatcher: boolean) {
|
||||||
const folderPath = join(testDir, 'deep');
|
const folderPath = join(testDir, 'deep');
|
||||||
await watcher.watch([{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]);
|
|
||||||
|
const requests: IRecursiveWatchRequest[] = [{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }];
|
||||||
|
if (reuseExistingWatcher) {
|
||||||
|
requests.push({ path: testDir, excludes: [], recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await watcher.watch(requests);
|
||||||
|
|
||||||
const filePath = join(folderPath, 'newFile.txt');
|
const filePath = join(folderPath, 'newFile.txt');
|
||||||
await basicCrudTest(filePath, 1);
|
await basicCrudTest(filePath, 1);
|
||||||
|
@ -730,5 +826,25 @@ import { Emitter, Event } from 'vs/base/common/event';
|
||||||
await onDidWatch;
|
await onDidWatch;
|
||||||
|
|
||||||
await basicCrudTest(filePath, 1);
|
await basicCrudTest(filePath, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('watch request reuses another recursive watcher even when requests are coming in at the same time', async function () {
|
||||||
|
const folderPath1 = join(testDir, 'deep', 'not-existing1');
|
||||||
|
const folderPath2 = join(testDir, 'deep', 'not-existing2');
|
||||||
|
const folderPath3 = join(testDir, 'not-existing3');
|
||||||
|
|
||||||
|
const requests: IRecursiveWatchRequest[] = [
|
||||||
|
{ path: folderPath1, excludes: [], recursive: true, correlationId: 1 },
|
||||||
|
{ path: folderPath2, excludes: [], recursive: true, correlationId: 2 },
|
||||||
|
{ path: folderPath3, excludes: [], recursive: true, correlationId: 3 },
|
||||||
|
{ path: join(testDir, 'deep'), excludes: [], recursive: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
await watcher.watch(requests);
|
||||||
|
|
||||||
|
assert.strictEqual(watcher.isSuspended(requests[0]), true);
|
||||||
|
assert.strictEqual(watcher.isSuspended(requests[1]), true);
|
||||||
|
assert.strictEqual(watcher.isSuspended(requests[2]), 'polling');
|
||||||
|
assert.strictEqual(watcher.isSuspended(requests[3]), false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -240,7 +240,7 @@ export class DesktopMain extends Disposable {
|
||||||
serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService);
|
serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService);
|
||||||
|
|
||||||
// Local Files
|
// Local Files
|
||||||
const diskFileSystemProvider = this._register(new DiskFileSystemProvider(mainProcessService, utilityProcessWorkerWorkbenchService, logService));
|
const diskFileSystemProvider = this._register(new DiskFileSystemProvider(mainProcessService, utilityProcessWorkerWorkbenchService, logService, loggerService));
|
||||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||||
|
|
||||||
// URI Identity
|
// URI Identity
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* 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 { localize } from 'vs/nls';
|
||||||
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, IFileChange } 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';
|
||||||
|
@ -14,8 +15,9 @@ 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 { 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 { ILoggerService, 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';
|
||||||
|
import { LogService } from 'vs/platform/log/common/logService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sandbox ready disk file system provider that delegates almost all calls
|
* A sandbox ready disk file system provider that delegates almost all calls
|
||||||
|
@ -35,7 +37,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
|
||||||
constructor(
|
constructor(
|
||||||
private readonly mainProcessService: IMainProcessService,
|
private readonly mainProcessService: IMainProcessService,
|
||||||
private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService,
|
private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService,
|
||||||
logService: ILogService
|
logService: ILogService,
|
||||||
|
private readonly loggerService: ILoggerService
|
||||||
) {
|
) {
|
||||||
super(logService, { watcher: { forceUniversal: true /* send all requests to universal watcher process */ } });
|
super(logService, { watcher: { forceUniversal: true /* send all requests to universal watcher process */ } });
|
||||||
|
|
||||||
|
@ -143,5 +146,22 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
|
||||||
throw new Error('Method not implemented in sandbox.'); // we never expect this to be called given we set `forceUniversal: true`
|
throw new Error('Method not implemented in sandbox.'); // we never expect this to be called given we set `forceUniversal: true`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _watcherLogService: ILogService | undefined = undefined;
|
||||||
|
private get watcherLogService(): ILogService {
|
||||||
|
if (!this._watcherLogService) {
|
||||||
|
this._watcherLogService = new LogService(this.loggerService.createLogger('fileWatcher', { name: localize('fileWatcher', "File Watcher") }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._watcherLogService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override logWatcherMessage(msg: ILogMessage): void {
|
||||||
|
this.watcherLogService[msg.type](msg.message);
|
||||||
|
|
||||||
|
if (msg.type !== 'trace' && msg.type !== 'debug') {
|
||||||
|
super.logWatcherMessage(msg); // allow non-verbose log messages in window log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue