Path: blob/main/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts
3296 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();6667this.includes = this.request.includes ? parseWatcherPatterns(this.request.path, this.request.includes) : undefined;68this.excludes = this.request.excludes ? parseWatcherPatterns(this.request.path, this.request.excludes) : undefined;6970this._register(toDisposable(() => this.subscriptions.clear()));71}7273subscribe(path: string, callback: (change: IFileChange) => void): IDisposable {74path = URI.file(path).fsPath; // make sure to store the path in `fsPath` form to match it with events later7576let subscriptions = this.subscriptions.get(path);77if (!subscriptions) {78subscriptions = new Set();79this.subscriptions.set(path, subscriptions);80}8182subscriptions.add(callback);8384return toDisposable(() => {85const subscriptions = this.subscriptions.get(path);86if (subscriptions) {87subscriptions.delete(callback);8889if (subscriptions.size === 0) {90this.subscriptions.delete(path);91}92}93});94}9596get subscriptionsCount(): number {97return this.subscriptions.size;98}99100notifyFileChange(path: string, change: IFileChange): void {101const subscriptions = this.subscriptions.get(path);102if (subscriptions) {103for (const subscription of subscriptions) {104subscription(change);105}106}107}108109notifyWatchFailed(): void {110this.didFail = true;111112this._onDidFail.fire();113}114115include(path: string): boolean {116if (!this.includes || this.includes.length === 0) {117return true; // no specific includes defined, include all118}119120return this.includes.some(include => include(path));121}122123exclude(path: string): boolean {124return Boolean(this.excludes?.some(exclude => exclude(path)));125}126127async stop(joinRestart: Promise<void> | undefined): Promise<void> {128this.didStop = true;129130try {131await this.stopFn();132} finally {133this._onDidStop.fire({ joinRestart });134this.dispose();135}136}137}138139export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithSubscribe {140141private static readonly MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE = new Map<parcelWatcher.EventType, number>(142[143['create', FileChangeType.ADDED],144['update', FileChangeType.UPDATED],145['delete', FileChangeType.DELETED]146]147);148149private static readonly PREDEFINED_EXCLUDES: { [platform: string]: string[] } = {150'win32': [],151'darwin': [152join(homedir(), 'Library', 'Containers') // Triggers access dialog from macOS 14 (https://github.com/microsoft/vscode/issues/208105)153],154'linux': []155};156157private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events';158159private readonly _onDidError = this._register(new Emitter<IWatcherErrorEvent>());160readonly onDidError = this._onDidError.event;161162private readonly _watchers = new Map<string /* path */ | number /* correlation ID */, ParcelWatcherInstance>();163get watchers() { return this._watchers.values(); }164165// A delay for collecting file changes from Parcel166// before collecting them for coalescing and emitting.167// Parcel internally uses 50ms as delay, so we use 75ms,168// to schedule sufficiently after Parcel.169//170// Note: since Parcel 2.0.7, the very first event is171// emitted without delay if no events occured over a172// duration of 500ms. But we always want to aggregate173// events to apply our coleasing logic.174//175private static readonly FILE_CHANGES_HANDLER_DELAY = 75;176177// Reduce likelyhood of spam from file events via throttling.178// (https://github.com/microsoft/vscode/issues/124723)179private readonly throttledFileChangesEmitter = this._register(new ThrottledWorker<IFileChange>(180{181maxWorkChunkSize: 500, // only process up to 500 changes at once before...182throttleDelay: 200, // ...resting for 200ms until we process events again...183maxBufferedWork: 30000 // ...but never buffering more than 30000 events in memory184},185events => this._onDidChangeFile.fire(events)186));187188private enospcErrorLogged = false;189190constructor() {191super();192193this.registerListeners();194}195196private registerListeners(): void {197const onUncaughtException = (error: unknown) => this.onUnexpectedError(error);198const onUnhandledRejection = (error: unknown) => this.onUnexpectedError(error);199200process.on('uncaughtException', onUncaughtException);201process.on('unhandledRejection', onUnhandledRejection);202203this._register(toDisposable(() => {204process.off('uncaughtException', onUncaughtException);205process.off('unhandledRejection', onUnhandledRejection);206}));207}208209protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise<void> {210211// Figure out duplicates to remove from the requests212requests = await this.removeDuplicateRequests(requests);213214// Figure out which watchers to start and which to stop215const requestsToStart: IRecursiveWatchRequest[] = [];216const watchersToStop = new Set(Array.from(this.watchers));217for (const request of requests) {218const watcher = this._watchers.get(this.requestToWatcherKey(request));219if (watcher && patternsEquals(watcher.request.excludes, request.excludes) && patternsEquals(watcher.request.includes, request.includes) && watcher.request.pollingInterval === request.pollingInterval) {220watchersToStop.delete(watcher); // keep watcher221} else {222requestsToStart.push(request); // start watching223}224}225226// Logging227if (requestsToStart.length) {228this.trace(`Request to start watching: ${requestsToStart.map(request => this.requestToString(request)).join(',')}`);229}230231if (watchersToStop.size) {232this.trace(`Request to stop watching: ${Array.from(watchersToStop).map(watcher => this.requestToString(watcher.request)).join(',')}`);233}234235// Stop watching as instructed236for (const watcher of watchersToStop) {237await this.stopWatching(watcher);238}239240// Start watching as instructed241for (const request of requestsToStart) {242if (request.pollingInterval) {243await this.startPolling(request, request.pollingInterval);244} else {245await this.startWatching(request);246}247}248}249250private requestToWatcherKey(request: IRecursiveWatchRequest): string | number {251return typeof request.correlationId === 'number' ? request.correlationId : this.pathToWatcherKey(request.path);252}253254private pathToWatcherKey(path: string): string {255return isLinux ? path : path.toLowerCase() /* ignore path casing */;256}257258private async startPolling(request: IRecursiveWatchRequest, pollingInterval: number, restarts = 0): Promise<void> {259const cts = new CancellationTokenSource();260261const instance = new DeferredPromise<void>();262263const snapshotFile = randomPath(tmpdir(), 'vscode-watcher-snapshot');264265// Remember as watcher instance266const watcher: ParcelWatcherInstance = new ParcelWatcherInstance(267instance.p,268request,269restarts,270cts.token,271new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),272async () => {273cts.dispose(true);274275watcher.worker.flush();276watcher.worker.dispose();277278pollingWatcher.dispose();279await promises.unlink(snapshotFile);280}281);282this._watchers.set(this.requestToWatcherKey(request), watcher);283284// Path checks for symbolic links / wrong casing285const { realPath, realPathDiffers, realPathLength } = await this.normalizePath(request);286287this.trace(`Started watching: '${realPath}' with polling interval '${pollingInterval}'`);288289let counter = 0;290291const pollingWatcher = new RunOnceScheduler(async () => {292counter++;293294if (cts.token.isCancellationRequested) {295return;296}297298// We already ran before, check for events since299const parcelWatcherLib = parcelWatcher;300try {301if (counter > 1) {302const parcelEvents = await parcelWatcherLib.getEventsSince(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND });303304if (cts.token.isCancellationRequested) {305return;306}307308// Handle & emit events309this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength);310}311312// Store a snapshot of files to the snapshot file313await parcelWatcherLib.writeSnapshot(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND });314} catch (error) {315this.onUnexpectedError(error, request);316}317318// Signal we are ready now when the first snapshot was written319if (counter === 1) {320instance.complete();321}322323if (cts.token.isCancellationRequested) {324return;325}326327// Schedule again at the next interval328pollingWatcher.schedule();329}, pollingInterval);330pollingWatcher.schedule(0);331}332333private async startWatching(request: IRecursiveWatchRequest, restarts = 0): Promise<void> {334const cts = new CancellationTokenSource();335336const instance = new DeferredPromise<parcelWatcher.AsyncSubscription | undefined>();337338// Remember as watcher instance339const watcher: ParcelWatcherInstance = new ParcelWatcherInstance(340instance.p,341request,342restarts,343cts.token,344new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),345async () => {346cts.dispose(true);347348watcher.worker.flush();349watcher.worker.dispose();350351const watcherInstance = await instance.p;352await watcherInstance?.unsubscribe();353}354);355this._watchers.set(this.requestToWatcherKey(request), watcher);356357// Path checks for symbolic links / wrong casing358const { realPath, realPathDiffers, realPathLength } = await this.normalizePath(request);359360try {361const parcelWatcherLib = parcelWatcher;362const parcelWatcherInstance = await parcelWatcherLib.subscribe(realPath, (error, parcelEvents) => {363if (watcher.token.isCancellationRequested) {364return; // return early when disposed365}366367// In any case of an error, treat this like a unhandled exception368// that might require the watcher to restart. We do not really know369// the state of parcel at this point and as such will try to restart370// up to our maximum of restarts.371if (error) {372this.onUnexpectedError(error, request);373}374375// Handle & emit events376this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength);377}, {378backend: ParcelWatcher.PARCEL_WATCHER_BACKEND,379ignore: this.addPredefinedExcludes(watcher.request.excludes)380});381382this.trace(`Started watching: '${realPath}' with backend '${ParcelWatcher.PARCEL_WATCHER_BACKEND}'`);383384instance.complete(parcelWatcherInstance);385} catch (error) {386this.onUnexpectedError(error, request);387388instance.complete(undefined);389390watcher.notifyWatchFailed();391this._onDidWatchFail.fire(request);392}393}394395private addPredefinedExcludes(initialExcludes: string[]): string[] {396const excludes = [...initialExcludes];397398const predefinedExcludes = ParcelWatcher.PREDEFINED_EXCLUDES[process.platform];399if (Array.isArray(predefinedExcludes)) {400for (const exclude of predefinedExcludes) {401if (!excludes.includes(exclude)) {402excludes.push(exclude);403}404}405}406407return excludes;408}409410private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: ParcelWatcherInstance, realPathDiffers: boolean, realPathLength: number): void {411if (parcelEvents.length === 0) {412return;413}414415// Normalize events: handle NFC normalization and symlinks416// It is important to do this before checking for includes417// to check on the original path.418this.normalizeEvents(parcelEvents, watcher.request, realPathDiffers, realPathLength);419420// Check for includes421const includedEvents = this.handleIncludes(watcher, parcelEvents);422423// Add to event aggregator for later processing424for (const includedEvent of includedEvents) {425watcher.worker.work(includedEvent);426}427}428429private handleIncludes(watcher: ParcelWatcherInstance, parcelEvents: parcelWatcher.Event[]): IFileChange[] {430const events: IFileChange[] = [];431432for (const { path, type: parcelEventType } of parcelEvents) {433const type = ParcelWatcher.MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE.get(parcelEventType)!;434if (this.verboseLogging) {435this.traceWithCorrelation(`${type === FileChangeType.ADDED ? '[ADDED]' : type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`, watcher.request);436}437438// Apply include filter if any439if (!watcher.include(path)) {440if (this.verboseLogging) {441this.traceWithCorrelation(` >> ignored (not included) ${path}`, watcher.request);442}443} else {444events.push({ type, resource: URI.file(path), cId: watcher.request.correlationId });445}446}447448return events;449}450451private handleParcelEvents(parcelEvents: IFileChange[], watcher: ParcelWatcherInstance): void {452453// Coalesce events: merge events of same kind454const coalescedEvents = coalesceEvents(parcelEvents);455456// Filter events: check for specific events we want to exclude457const { events: filteredEvents, rootDeleted } = this.filterEvents(coalescedEvents, watcher);458459// Broadcast to clients460this.emitEvents(filteredEvents, watcher);461462// Handle root path deletes463if (rootDeleted) {464this.onWatchedPathDeleted(watcher);465}466}467468private emitEvents(events: IFileChange[], watcher: ParcelWatcherInstance): void {469if (events.length === 0) {470return;471}472473// Broadcast to clients via throttler474const worked = this.throttledFileChangesEmitter.work(events);475476// Logging477if (!worked) {478this.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).`);479} else {480if (this.throttledFileChangesEmitter.pending > 0) {481this.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);482}483}484}485486private async normalizePath(request: IRecursiveWatchRequest): Promise<{ realPath: string; realPathDiffers: boolean; realPathLength: number }> {487let realPath = request.path;488let realPathDiffers = false;489let realPathLength = request.path.length;490491try {492493// First check for symbolic link494realPath = await Promises.realpath(request.path);495496// Second check for casing difference497// Note: this will be a no-op on Linux platforms498if (request.path === realPath) {499realPath = await realcase(request.path) ?? request.path;500}501502// Correct watch path as needed503if (request.path !== realPath) {504realPathLength = realPath.length;505realPathDiffers = true;506507this.trace(`correcting a path to watch that seems to be a symbolic link or wrong casing (original: ${request.path}, real: ${realPath})`);508}509} catch (error) {510// ignore511}512513return { realPath, realPathDiffers, realPathLength };514}515516private normalizeEvents(events: parcelWatcher.Event[], request: IRecursiveWatchRequest, realPathDiffers: boolean, realPathLength: number): void {517for (const event of events) {518519// Mac uses NFD unicode form on disk, but we want NFC520if (isMacintosh) {521event.path = normalizeNFC(event.path);522}523524// Workaround for https://github.com/parcel-bundler/watcher/issues/68525// where watching root drive letter adds extra backslashes.526if (isWindows) {527if (request.path.length <= 3) { // for ex. c:, C:\528event.path = normalize(event.path);529}530}531532// Convert paths back to original form in case it differs533if (realPathDiffers) {534event.path = request.path + event.path.substr(realPathLength);535}536}537}538539private filterEvents(events: IFileChange[], watcher: ParcelWatcherInstance): { events: IFileChange[]; rootDeleted?: boolean } {540const filteredEvents: IFileChange[] = [];541let rootDeleted = false;542543const filter = this.isCorrelated(watcher.request) ? watcher.request.filter : undefined; // filtering is only enabled when correlating because watchers are otherwise potentially reused544for (const event of events) {545546// Emit to instance subscriptions if any before filtering547if (watcher.subscriptionsCount > 0) {548watcher.notifyFileChange(event.resource.fsPath, event);549}550551// Filtering552rootDeleted = event.type === FileChangeType.DELETED && isEqual(event.resource.fsPath, watcher.request.path, !isLinux);553if (isFiltered(event, filter)) {554if (this.verboseLogging) {555this.traceWithCorrelation(` >> ignored (filtered) ${event.resource.fsPath}`, watcher.request);556}557558continue;559}560561// Logging562this.traceEvent(event, watcher.request);563564filteredEvents.push(event);565}566567return { events: filteredEvents, rootDeleted };568}569570private onWatchedPathDeleted(watcher: ParcelWatcherInstance): void {571this.warn('Watcher shutdown because watched path got deleted', watcher);572573watcher.notifyWatchFailed();574this._onDidWatchFail.fire(watcher.request);575}576577private onUnexpectedError(error: unknown, request?: IRecursiveWatchRequest): void {578const msg = toErrorMessage(error);579580// Specially handle ENOSPC errors that can happen when581// the watcher consumes so many file descriptors that582// we are running into a limit. We only want to warn583// once in this case to avoid log spam.584// See https://github.com/microsoft/vscode/issues/7950585if (msg.indexOf('No space left on device') !== -1) {586if (!this.enospcErrorLogged) {587this.error('Inotify limit reached (ENOSPC)', request);588589this.enospcErrorLogged = true;590}591}592593// Version 2.5.1 introduces 3 new errors on macOS594// via https://github.dev/parcel-bundler/watcher/pull/196595else if (msg.indexOf('File system must be re-scanned') !== -1) {596this.error(msg, request);597}598599// Any other error is unexpected and we should try to600// restart the watcher as a result to get into healthy601// state again if possible and if not attempted too much602else {603this.error(`Unexpected error: ${msg} (EUNKNOWN)`, request);604605this._onDidError.fire({ request, error: msg });606}607}608609override async stop(): Promise<void> {610await super.stop();611612for (const watcher of this.watchers) {613await this.stopWatching(watcher);614}615}616617protected restartWatching(watcher: ParcelWatcherInstance, delay = 800): void {618619// Restart watcher delayed to accomodate for620// changes on disk that have triggered the621// need for a restart in the first place.622const scheduler = new RunOnceScheduler(async () => {623if (watcher.token.isCancellationRequested) {624return; // return early when disposed625}626627const restartPromise = new DeferredPromise<void>();628try {629630// Await the watcher having stopped, as this is631// needed to properly re-watch the same path632await this.stopWatching(watcher, restartPromise.p);633634// Start watcher again counting the restarts635if (watcher.request.pollingInterval) {636await this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1);637} else {638await this.startWatching(watcher.request, watcher.restarts + 1);639}640} finally {641restartPromise.complete();642}643}, delay);644645scheduler.schedule();646watcher.token.onCancellationRequested(() => scheduler.dispose());647}648649private async stopWatching(watcher: ParcelWatcherInstance, joinRestart?: Promise<void>): Promise<void> {650this.trace(`stopping file watcher`, watcher);651652this._watchers.delete(this.requestToWatcherKey(watcher.request));653654try {655await watcher.stop(joinRestart);656} catch (error) {657this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher.request);658}659}660661protected async removeDuplicateRequests(requests: IRecursiveWatchRequest[], validatePaths = true): Promise<IRecursiveWatchRequest[]> {662663// Sort requests by path length to have shortest first664// to have a way to prevent children to be watched if665// parents exist.666requests.sort((requestA, requestB) => requestA.path.length - requestB.path.length);667668// Ignore requests for the same paths that have the same correlation669const mapCorrelationtoRequests = new Map<number | undefined /* correlation */, Map<string, IRecursiveWatchRequest>>();670for (const request of requests) {671if (request.excludes.includes(GLOBSTAR)) {672continue; // path is ignored entirely (via `**` glob exclude)673}674675676let requestsForCorrelation = mapCorrelationtoRequests.get(request.correlationId);677if (!requestsForCorrelation) {678requestsForCorrelation = new Map<string, IRecursiveWatchRequest>();679mapCorrelationtoRequests.set(request.correlationId, requestsForCorrelation);680}681682const path = this.pathToWatcherKey(request.path);683if (requestsForCorrelation.has(path)) {684this.trace(`ignoring a request for watching who's path is already watched: ${this.requestToString(request)}`);685}686687requestsForCorrelation.set(path, request);688}689690const normalizedRequests: IRecursiveWatchRequest[] = [];691692for (const requestsForCorrelation of mapCorrelationtoRequests.values()) {693694// Only consider requests for watching that are not695// a child of an existing request path to prevent696// duplication. In addition, drop any request where697// everything is excluded (via `**` glob).698//699// However, allow explicit requests to watch folders700// that are symbolic links because the Parcel watcher701// does not allow to recursively watch symbolic links.702703const requestTrie = TernarySearchTree.forPaths<IRecursiveWatchRequest>(!isLinux);704705for (const request of requestsForCorrelation.values()) {706707// Check for overlapping request paths (but preserve symbolic links)708if (requestTrie.findSubstr(request.path)) {709if (requestTrie.has(request.path)) {710this.trace(`ignoring a request for watching who's path is already watched: ${this.requestToString(request)}`);711} else {712try {713if (!(await promises.lstat(request.path)).isSymbolicLink()) {714this.trace(`ignoring a request for watching who's parent is already watched: ${this.requestToString(request)}`);715716continue;717}718} catch (error) {719this.trace(`ignoring a request for watching who's lstat failed to resolve: ${this.requestToString(request)} (error: ${error})`);720721this._onDidWatchFail.fire(request);722723continue;724}725}726}727728// Check for invalid paths729if (validatePaths && !(await this.isPathValid(request.path))) {730this._onDidWatchFail.fire(request);731732continue;733}734735requestTrie.set(request.path, request);736}737738normalizedRequests.push(...Array.from(requestTrie).map(([, request]) => request));739}740741return normalizedRequests;742}743744private async isPathValid(path: string): Promise<boolean> {745try {746const stat = await promises.stat(path);747if (!stat.isDirectory()) {748this.trace(`ignoring a path for watching that is a file and not a folder: ${path}`);749750return false;751}752} catch (error) {753this.trace(`ignoring a path for watching who's stat info failed to resolve: ${path} (error: ${error})`);754755return false;756}757758return true;759}760761subscribe(path: string, callback: (error: true | null, change?: IFileChange) => void): IDisposable | undefined {762for (const watcher of this.watchers) {763if (watcher.failed) {764continue; // watcher has already failed765}766767if (!isEqualOrParent(path, watcher.request.path, !isLinux)) {768continue; // watcher does not consider this path769}770771if (772watcher.exclude(path) ||773!watcher.include(path)774) {775continue; // parcel instance does not consider this path776}777778const disposables = new DisposableStore();779780disposables.add(Event.once(watcher.onDidStop)(async e => {781await e.joinRestart; // if we are restarting, await that so that we can possibly reuse this watcher again782if (disposables.isDisposed) {783return;784}785786callback(true /* error */);787}));788disposables.add(Event.once(watcher.onDidFail)(() => callback(true /* error */)));789disposables.add(watcher.subscribe(path, change => callback(null, change)));790791return disposables;792}793794return undefined;795}796797protected trace(message: string, watcher?: ParcelWatcherInstance): void {798if (this.verboseLogging) {799this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher?.request) });800}801}802803protected warn(message: string, watcher?: ParcelWatcherInstance) {804this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher?.request) });805}806807private error(message: string, request?: IRecursiveWatchRequest) {808this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, request) });809}810811private toMessage(message: string, request?: IRecursiveWatchRequest): string {812return request ? `[File Watcher ('parcel')] ${message} (path: ${request.path})` : `[File Watcher ('parcel')] ${message}`;813}814815protected get recursiveWatcher() { return this; }816}817818819