Path: blob/main/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts
5241 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import parcelWatcher from '@parcel/watcher';6import { promises } from 'fs';7import { tmpdir, homedir } from 'os';8import { URI } from '../../../../../base/common/uri.js';9import { DeferredPromise, RunOnceScheduler, RunOnceWorker, ThrottledWorker } from '../../../../../base/common/async.js';10import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';11import { toErrorMessage } from '../../../../../base/common/errorMessage.js';12import { Emitter, Event } from '../../../../../base/common/event.js';13import { randomPath, isEqual, isEqualOrParent } from '../../../../../base/common/extpath.js';14import { GLOBSTAR, ParsedPattern, patternsEquals } from '../../../../../base/common/glob.js';15import { BaseWatcher } from '../baseWatcher.js';16import { TernarySearchTree } from '../../../../../base/common/ternarySearchTree.js';17import { normalizeNFC } from '../../../../../base/common/normalization.js';18import { normalize, join } from '../../../../../base/common/path.js';19import { isLinux, isMacintosh, isWindows } from '../../../../../base/common/platform.js';20import { Promises, realcase } from '../../../../../base/node/pfs.js';21import { FileChangeType, IFileChange } from '../../../common/files.js';22import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, IWatcherErrorEvent } from '../../../common/watcher.js';23import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';2425export class ParcelWatcherInstance extends Disposable {2627private readonly _onDidStop = this._register(new Emitter<{ joinRestart?: Promise<void> }>());28readonly onDidStop = this._onDidStop.event;2930private readonly _onDidFail = this._register(new Emitter<void>());31readonly onDidFail = this._onDidFail.event;3233private didFail = false;34get failed(): boolean { return this.didFail; }3536private didStop = false;37get stopped(): boolean { return this.didStop; }3839private readonly includes: ParsedPattern[] | undefined;40private readonly excludes: ParsedPattern[] | undefined;4142private readonly subscriptions = new Map<string, Set<(change: IFileChange) => void>>();4344constructor(45/**46* Signals when the watcher is ready to watch.47*/48readonly ready: Promise<unknown>,49readonly request: IRecursiveWatchRequest,50/**51* How often this watcher has been restarted in case of an unexpected52* shutdown.53*/54readonly restarts: number,55/**56* The cancellation token associated with the lifecycle of the watcher.57*/58readonly token: CancellationToken,59/**60* An event aggregator to coalesce events and reduce duplicates.61*/62readonly worker: RunOnceWorker<IFileChange>,63private readonly stopFn: () => Promise<void>64) {65super();6667const ignoreCase = !isLinux;68this.includes = this.request.includes ? parseWatcherPatterns(this.request.path, this.request.includes, ignoreCase) : undefined;69this.excludes = this.request.excludes ? parseWatcherPatterns(this.request.path, this.request.excludes, ignoreCase) : undefined;7071this._register(toDisposable(() => this.subscriptions.clear()));72}7374subscribe(path: string, callback: (change: IFileChange) => void): IDisposable {75path = URI.file(path).fsPath; // make sure to store the path in `fsPath` form to match it with events later7677let subscriptions = this.subscriptions.get(path);78if (!subscriptions) {79subscriptions = new Set();80this.subscriptions.set(path, subscriptions);81}8283subscriptions.add(callback);8485return toDisposable(() => {86const subscriptions = this.subscriptions.get(path);87if (subscriptions) {88subscriptions.delete(callback);8990if (subscriptions.size === 0) {91this.subscriptions.delete(path);92}93}94});95}9697get subscriptionsCount(): number {98return this.subscriptions.size;99}100101notifyFileChange(path: string, change: IFileChange): void {102const subscriptions = this.subscriptions.get(path);103if (subscriptions) {104for (const subscription of subscriptions) {105subscription(change);106}107}108}109110notifyWatchFailed(): void {111this.didFail = true;112113this._onDidFail.fire();114}115116include(path: string): boolean {117if (!this.includes || this.includes.length === 0) {118return true; // no specific includes defined, include all119}120121return this.includes.some(include => include(path));122}123124exclude(path: string): boolean {125return Boolean(this.excludes?.some(exclude => exclude(path)));126}127128async stop(joinRestart: Promise<void> | undefined): Promise<void> {129this.didStop = true;130131try {132await this.stopFn();133} finally {134this._onDidStop.fire({ joinRestart });135this.dispose();136}137}138}139140export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithSubscribe {141142private static readonly MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE = new Map<parcelWatcher.EventType, number>(143[144['create', FileChangeType.ADDED],145['update', FileChangeType.UPDATED],146['delete', FileChangeType.DELETED]147]148);149150private static readonly PREDEFINED_EXCLUDES: { [platform: string]: string[] } = {151'win32': [],152'darwin': [153join(homedir(), 'Library', 'Containers') // Triggers access dialog from macOS 14 (https://github.com/microsoft/vscode/issues/208105)154],155'linux': []156};157158private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events';159160private readonly _onDidError = this._register(new Emitter<IWatcherErrorEvent>());161readonly onDidError = this._onDidError.event;162163private readonly _watchers = new Map<string /* path */ | number /* correlation ID */, ParcelWatcherInstance>();164get watchers() { return this._watchers.values(); }165166// A delay for collecting file changes from Parcel167// before collecting them for coalescing and emitting.168// Parcel internally uses 50ms as delay, so we use 75ms,169// to schedule sufficiently after Parcel.170//171// Note: since Parcel 2.0.7, the very first event is172// emitted without delay if no events occured over a173// duration of 500ms. But we always want to aggregate174// events to apply our coleasing logic.175//176private static readonly FILE_CHANGES_HANDLER_DELAY = 75;177178// Reduce likelyhood of spam from file events via throttling.179// (https://github.com/microsoft/vscode/issues/124723)180private readonly throttledFileChangesEmitter = this._register(new ThrottledWorker<IFileChange>(181{182maxWorkChunkSize: 500, // only process up to 500 changes at once before...183throttleDelay: 200, // ...resting for 200ms until we process events again...184maxBufferedWork: 30000 // ...but never buffering more than 30000 events in memory185},186events => this._onDidChangeFile.fire(events)187));188189private enospcErrorLogged = false;190191constructor() {192super();193194this.registerListeners();195}196197private registerListeners(): void {198const onUncaughtException = (error: unknown) => this.onUnexpectedError(error);199const onUnhandledRejection = (error: unknown) => this.onUnexpectedError(error);200201process.on('uncaughtException', onUncaughtException);202process.on('unhandledRejection', onUnhandledRejection);203204this._register(toDisposable(() => {205process.off('uncaughtException', onUncaughtException);206process.off('unhandledRejection', onUnhandledRejection);207}));208}209210protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise<void> {211212// Figure out duplicates to remove from the requests213requests = await this.removeDuplicateRequests(requests);214215// Figure out which watchers to start and which to stop216const requestsToStart: IRecursiveWatchRequest[] = [];217const watchersToStop = new Set(Array.from(this.watchers));218for (const request of requests) {219const watcher = this._watchers.get(this.requestToWatcherKey(request));220if (watcher && patternsEquals(watcher.request.excludes, request.excludes) && patternsEquals(watcher.request.includes, request.includes) && watcher.request.pollingInterval === request.pollingInterval) {221watchersToStop.delete(watcher); // keep watcher222} else {223requestsToStart.push(request); // start watching224}225}226227// Logging228if (requestsToStart.length) {229this.trace(`Request to start watching: ${requestsToStart.map(request => this.requestToString(request)).join(',')}`);230}231232if (watchersToStop.size) {233this.trace(`Request to stop watching: ${Array.from(watchersToStop).map(watcher => this.requestToString(watcher.request)).join(',')}`);234}235236// Stop watching as instructed237for (const watcher of watchersToStop) {238await this.stopWatching(watcher);239}240241// Start watching as instructed242for (const request of requestsToStart) {243if (request.pollingInterval) {244await this.startPolling(request, request.pollingInterval);245} else {246await this.startWatching(request);247}248}249}250251private requestToWatcherKey(request: IRecursiveWatchRequest): string | number {252return typeof request.correlationId === 'number' ? request.correlationId : this.pathToWatcherKey(request.path);253}254255private pathToWatcherKey(path: string): string {256return isLinux ? path : path.toLowerCase() /* ignore path casing */;257}258259private async startPolling(request: IRecursiveWatchRequest, pollingInterval: number, restarts = 0): Promise<void> {260const cts = new CancellationTokenSource();261262const instance = new DeferredPromise<void>();263264const snapshotFile = randomPath(tmpdir(), 'vscode-watcher-snapshot');265266// Remember as watcher instance267const watcher: ParcelWatcherInstance = new ParcelWatcherInstance(268instance.p,269request,270restarts,271cts.token,272new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),273async () => {274cts.dispose(true);275276watcher.worker.flush();277watcher.worker.dispose();278279pollingWatcher.dispose();280await promises.unlink(snapshotFile);281}282);283this._watchers.set(this.requestToWatcherKey(request), watcher);284285// Path checks for symbolic links / wrong casing286const { realPath, realPathDiffers, realPathLength } = await this.normalizePath(request);287288this.trace(`Started watching: '${realPath}' with polling interval '${pollingInterval}'`);289290let counter = 0;291292const pollingWatcher = new RunOnceScheduler(async () => {293counter++;294295if (cts.token.isCancellationRequested) {296return;297}298299// We already ran before, check for events since300const parcelWatcherLib = parcelWatcher;301try {302if (counter > 1) {303const parcelEvents = await parcelWatcherLib.getEventsSince(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND });304305if (cts.token.isCancellationRequested) {306return;307}308309// Handle & emit events310this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength);311}312313// Store a snapshot of files to the snapshot file314await parcelWatcherLib.writeSnapshot(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND });315} catch (error) {316this.onUnexpectedError(error, request);317}318319// Signal we are ready now when the first snapshot was written320if (counter === 1) {321instance.complete();322}323324if (cts.token.isCancellationRequested) {325return;326}327328// Schedule again at the next interval329pollingWatcher.schedule();330}, pollingInterval);331pollingWatcher.schedule(0);332}333334private async startWatching(request: IRecursiveWatchRequest, restarts = 0): Promise<void> {335const cts = new CancellationTokenSource();336337const instance = new DeferredPromise<parcelWatcher.AsyncSubscription | undefined>();338339// Remember as watcher instance340const watcher: ParcelWatcherInstance = new ParcelWatcherInstance(341instance.p,342request,343restarts,344cts.token,345new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),346async () => {347cts.dispose(true);348349watcher.worker.flush();350watcher.worker.dispose();351352const watcherInstance = await instance.p;353await watcherInstance?.unsubscribe();354}355);356this._watchers.set(this.requestToWatcherKey(request), watcher);357358// Path checks for symbolic links / wrong casing359const { realPath, realPathDiffers, realPathLength } = await this.normalizePath(request);360361try {362const parcelWatcherLib = parcelWatcher;363const parcelWatcherInstance = await parcelWatcherLib.subscribe(realPath, (error, parcelEvents) => {364if (watcher.token.isCancellationRequested) {365return; // return early when disposed366}367368// In any case of an error, treat this like a unhandled exception369// that might require the watcher to restart. We do not really know370// the state of parcel at this point and as such will try to restart371// up to our maximum of restarts.372if (error) {373this.onUnexpectedError(error, request);374}375376// Handle & emit events377this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength);378}, {379backend: ParcelWatcher.PARCEL_WATCHER_BACKEND,380ignore: this.addPredefinedExcludes(watcher.request.excludes)381});382383this.trace(`Started watching: '${realPath}' with backend '${ParcelWatcher.PARCEL_WATCHER_BACKEND}'`);384385instance.complete(parcelWatcherInstance);386} catch (error) {387this.onUnexpectedError(error, request);388389instance.complete(undefined);390391watcher.notifyWatchFailed();392this._onDidWatchFail.fire(request);393}394}395396private addPredefinedExcludes(initialExcludes: string[]): string[] {397const excludes = [...initialExcludes];398399const predefinedExcludes = ParcelWatcher.PREDEFINED_EXCLUDES[process.platform];400if (Array.isArray(predefinedExcludes)) {401for (const exclude of predefinedExcludes) {402if (!excludes.includes(exclude)) {403excludes.push(exclude);404}405}406}407408return excludes;409}410411private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: ParcelWatcherInstance, realPathDiffers: boolean, realPathLength: number): void {412if (parcelEvents.length === 0) {413return;414}415416// Normalize events: handle NFC normalization and symlinks417// It is important to do this before checking for includes418// to check on the original path.419this.normalizeEvents(parcelEvents, watcher.request, realPathDiffers, realPathLength);420421// Check for includes422const includedEvents = this.handleIncludes(watcher, parcelEvents);423424// Add to event aggregator for later processing425for (const includedEvent of includedEvents) {426watcher.worker.work(includedEvent);427}428}429430private handleIncludes(watcher: ParcelWatcherInstance, parcelEvents: parcelWatcher.Event[]): IFileChange[] {431const events: IFileChange[] = [];432433for (const { path, type: parcelEventType } of parcelEvents) {434const type = ParcelWatcher.MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE.get(parcelEventType)!;435if (this.verboseLogging) {436this.traceWithCorrelation(`${type === FileChangeType.ADDED ? '[ADDED]' : type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`, watcher.request);437}438439// Apply include filter if any440if (!watcher.include(path)) {441if (this.verboseLogging) {442this.traceWithCorrelation(` >> ignored (not included) ${path}`, watcher.request);443}444} else {445events.push({ type, resource: URI.file(path), cId: watcher.request.correlationId });446}447}448449return events;450}451452private handleParcelEvents(parcelEvents: IFileChange[], watcher: ParcelWatcherInstance): void {453454// Coalesce events: merge events of same kind455const coalescedEvents = coalesceEvents(parcelEvents);456457// Filter events: check for specific events we want to exclude458const { events: filteredEvents, rootDeleted } = this.filterEvents(coalescedEvents, watcher);459460// Broadcast to clients461this.emitEvents(filteredEvents, watcher);462463// Handle root path deletes464if (rootDeleted) {465this.onWatchedPathDeleted(watcher);466}467}468469private emitEvents(events: IFileChange[], watcher: ParcelWatcherInstance): void {470if (events.length === 0) {471return;472}473474// Broadcast to clients via throttler475const worked = this.throttledFileChangesEmitter.work(events);476477// Logging478if (!worked) {479this.warn(`started ignoring events due to too many file change events at once (incoming: ${events.length}, most recent change: ${events[0].resource.fsPath}). Use 'files.watcherExclude' setting to exclude folders with lots of changing files (e.g. compilation output).`);480} else {481if (this.throttledFileChangesEmitter.pending > 0) {482this.trace(`started throttling events due to large amount of file change events at once (pending: ${this.throttledFileChangesEmitter.pending}, most recent change: ${events[0].resource.fsPath}). Use 'files.watcherExclude' setting to exclude folders with lots of changing files (e.g. compilation output).`, watcher);483}484}485}486487private async normalizePath(request: IRecursiveWatchRequest): Promise<{ realPath: string; realPathDiffers: boolean; realPathLength: number }> {488let realPath = request.path;489let realPathDiffers = false;490let realPathLength = request.path.length;491492try {493494// First check for symbolic link495realPath = await Promises.realpath(request.path);496497// Second check for casing difference498// Note: this will be a no-op on Linux platforms499if (request.path === realPath) {500realPath = await realcase(request.path) ?? request.path;501}502503// Correct watch path as needed504if (request.path !== realPath) {505realPathLength = realPath.length;506realPathDiffers = true;507508this.trace(`correcting a path to watch that seems to be a symbolic link or wrong casing (original: ${request.path}, real: ${realPath})`);509}510} catch (error) {511// ignore512}513514return { realPath, realPathDiffers, realPathLength };515}516517private normalizeEvents(events: parcelWatcher.Event[], request: IRecursiveWatchRequest, realPathDiffers: boolean, realPathLength: number): void {518for (const event of events) {519520// Mac uses NFD unicode form on disk, but we want NFC521if (isMacintosh) {522event.path = normalizeNFC(event.path);523}524525// Workaround for https://github.com/parcel-bundler/watcher/issues/68526// where watching root drive letter adds extra backslashes.527if (isWindows) {528if (request.path.length <= 3) { // for ex. c:, C:\529event.path = normalize(event.path);530}531}532533// Convert paths back to original form in case it differs534if (realPathDiffers) {535event.path = request.path + event.path.substr(realPathLength);536}537}538}539540private filterEvents(events: IFileChange[], watcher: ParcelWatcherInstance): { events: IFileChange[]; rootDeleted?: boolean } {541const filteredEvents: IFileChange[] = [];542let rootDeleted = false;543544const filter = this.isCorrelated(watcher.request) ? watcher.request.filter : undefined; // filtering is only enabled when correlating because watchers are otherwise potentially reused545for (const event of events) {546547// Emit to instance subscriptions if any before filtering548if (watcher.subscriptionsCount > 0) {549watcher.notifyFileChange(event.resource.fsPath, event);550}551552// Filtering553rootDeleted = event.type === FileChangeType.DELETED && isEqual(event.resource.fsPath, watcher.request.path, !isLinux);554if (isFiltered(event, filter)) {555if (this.verboseLogging) {556this.traceWithCorrelation(` >> ignored (filtered) ${event.resource.fsPath}`, watcher.request);557}558559continue;560}561562// Logging563this.traceEvent(event, watcher.request);564565filteredEvents.push(event);566}567568return { events: filteredEvents, rootDeleted };569}570571private onWatchedPathDeleted(watcher: ParcelWatcherInstance): void {572this.warn('Watcher shutdown because watched path got deleted', watcher);573574watcher.notifyWatchFailed();575this._onDidWatchFail.fire(watcher.request);576}577578private onUnexpectedError(error: unknown, request?: IRecursiveWatchRequest): void {579const msg = toErrorMessage(error);580581// Specially handle ENOSPC errors that can happen when582// the watcher consumes so many file descriptors that583// we are running into a limit. We only want to warn584// once in this case to avoid log spam.585// See https://github.com/microsoft/vscode/issues/7950586if (msg.indexOf('No space left on device') !== -1) {587if (!this.enospcErrorLogged) {588this.error('Inotify limit reached (ENOSPC)', request);589590this.enospcErrorLogged = true;591}592}593594// Version 2.5.1 introduces 3 new errors on macOS595// via https://github.dev/parcel-bundler/watcher/pull/196596else if (msg.indexOf('File system must be re-scanned') !== -1) {597this.error(msg, request);598}599600// Any other error is unexpected and we should try to601// restart the watcher as a result to get into healthy602// state again if possible and if not attempted too much603else {604this.error(`Unexpected error: ${msg} (EUNKNOWN)`, request);605606this._onDidError.fire({ request, error: msg });607}608}609610override async stop(): Promise<void> {611await super.stop();612613for (const watcher of this.watchers) {614await this.stopWatching(watcher);615}616}617618protected restartWatching(watcher: ParcelWatcherInstance, delay = 800): void {619620// Restart watcher delayed to accommodate for621// changes on disk that have triggered the622// need for a restart in the first place.623const scheduler = new RunOnceScheduler(async () => {624if (watcher.token.isCancellationRequested) {625return; // return early when disposed626}627628const restartPromise = new DeferredPromise<void>();629try {630631// Await the watcher having stopped, as this is632// needed to properly re-watch the same path633await this.stopWatching(watcher, restartPromise.p);634635// Start watcher again counting the restarts636if (watcher.request.pollingInterval) {637await this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1);638} else {639await this.startWatching(watcher.request, watcher.restarts + 1);640}641} finally {642restartPromise.complete();643}644}, delay);645646scheduler.schedule();647watcher.token.onCancellationRequested(() => scheduler.dispose());648}649650private async stopWatching(watcher: ParcelWatcherInstance, joinRestart?: Promise<void>): Promise<void> {651this.trace(`stopping file watcher`, watcher);652653this._watchers.delete(this.requestToWatcherKey(watcher.request));654655try {656await watcher.stop(joinRestart);657} catch (error) {658this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher.request);659}660}661662protected async removeDuplicateRequests(requests: IRecursiveWatchRequest[], validatePaths = true): Promise<IRecursiveWatchRequest[]> {663664// Sort requests by path length to have shortest first665// to have a way to prevent children to be watched if666// parents exist.667requests.sort((requestA, requestB) => requestA.path.length - requestB.path.length);668669// Ignore requests for the same paths that have the same correlation670const mapCorrelationtoRequests = new Map<number | undefined /* correlation */, Map<string, IRecursiveWatchRequest>>();671for (const request of requests) {672if (request.excludes.includes(GLOBSTAR)) {673continue; // path is ignored entirely (via `**` glob exclude)674}675676677let requestsForCorrelation = mapCorrelationtoRequests.get(request.correlationId);678if (!requestsForCorrelation) {679requestsForCorrelation = new Map<string, IRecursiveWatchRequest>();680mapCorrelationtoRequests.set(request.correlationId, requestsForCorrelation);681}682683const path = this.pathToWatcherKey(request.path);684if (requestsForCorrelation.has(path)) {685this.trace(`ignoring a request for watching who's path is already watched: ${this.requestToString(request)}`);686}687688requestsForCorrelation.set(path, request);689}690691const normalizedRequests: IRecursiveWatchRequest[] = [];692693for (const requestsForCorrelation of mapCorrelationtoRequests.values()) {694695// Only consider requests for watching that are not696// a child of an existing request path to prevent697// duplication. In addition, drop any request where698// everything is excluded (via `**` glob).699//700// However, allow explicit requests to watch folders701// that are symbolic links because the Parcel watcher702// does not allow to recursively watch symbolic links.703704const requestTrie = TernarySearchTree.forPaths<IRecursiveWatchRequest>(!isLinux);705706for (const request of requestsForCorrelation.values()) {707708// Check for overlapping request paths (but preserve symbolic links)709if (requestTrie.findSubstr(request.path)) {710if (requestTrie.has(request.path)) {711this.trace(`ignoring a request for watching who's path is already watched: ${this.requestToString(request)}`);712} else {713try {714if (!(await promises.lstat(request.path)).isSymbolicLink()) {715this.trace(`ignoring a request for watching who's parent is already watched: ${this.requestToString(request)}`);716717continue;718}719} catch (error) {720this.trace(`ignoring a request for watching who's lstat failed to resolve: ${this.requestToString(request)} (error: ${error})`);721722this._onDidWatchFail.fire(request);723724continue;725}726}727}728729// Check for invalid paths730if (validatePaths && !(await this.isPathValid(request.path))) {731this._onDidWatchFail.fire(request);732733continue;734}735736requestTrie.set(request.path, request);737}738739normalizedRequests.push(...Array.from(requestTrie).map(([, request]) => request));740}741742return normalizedRequests;743}744745private async isPathValid(path: string): Promise<boolean> {746try {747const stat = await promises.stat(path);748if (!stat.isDirectory()) {749this.trace(`ignoring a path for watching that is a file and not a folder: ${path}`);750751return false;752}753} catch (error) {754this.trace(`ignoring a path for watching who's stat info failed to resolve: ${path} (error: ${error})`);755756return false;757}758759return true;760}761762subscribe(path: string, callback: (error: true | null, change?: IFileChange) => void): IDisposable | undefined {763for (const watcher of this.watchers) {764if (watcher.failed) {765continue; // watcher has already failed766}767768if (!isEqualOrParent(path, watcher.request.path, !isLinux)) {769continue; // watcher does not consider this path770}771772if (773watcher.exclude(path) ||774!watcher.include(path)775) {776continue; // parcel instance does not consider this path777}778779const disposables = new DisposableStore();780781disposables.add(Event.once(watcher.onDidStop)(async e => {782await e.joinRestart; // if we are restarting, await that so that we can possibly reuse this watcher again783if (disposables.isDisposed) {784return;785}786787callback(true /* error */);788}));789disposables.add(Event.once(watcher.onDidFail)(() => callback(true /* error */)));790disposables.add(watcher.subscribe(path, change => callback(null, change)));791792return disposables;793}794795return undefined;796}797798protected trace(message: string, watcher?: ParcelWatcherInstance): void {799if (this.verboseLogging) {800this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher?.request) });801}802}803804protected warn(message: string, watcher?: ParcelWatcherInstance) {805this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher?.request) });806}807808private error(message: string, request?: IRecursiveWatchRequest) {809this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, request) });810}811812private toMessage(message: string, request?: IRecursiveWatchRequest): string {813return request ? `[File Watcher ('parcel')] ${message} (path: ${request.path})` : `[File Watcher ('parcel')] ${message}`;814}815816protected get recursiveWatcher() { return this; }817}818819820