Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import parcelWatcher from '@parcel/watcher';
7
import { promises } from 'fs';
8
import { tmpdir, homedir } from 'os';
9
import { URI } from '../../../../../base/common/uri.js';
10
import { DeferredPromise, RunOnceScheduler, RunOnceWorker, ThrottledWorker } from '../../../../../base/common/async.js';
11
import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
12
import { toErrorMessage } from '../../../../../base/common/errorMessage.js';
13
import { Emitter, Event } from '../../../../../base/common/event.js';
14
import { randomPath, isEqual, isEqualOrParent } from '../../../../../base/common/extpath.js';
15
import { GLOBSTAR, ParsedPattern, patternsEquals } from '../../../../../base/common/glob.js';
16
import { BaseWatcher } from '../baseWatcher.js';
17
import { TernarySearchTree } from '../../../../../base/common/ternarySearchTree.js';
18
import { normalizeNFC } from '../../../../../base/common/normalization.js';
19
import { normalize, join } from '../../../../../base/common/path.js';
20
import { isLinux, isMacintosh, isWindows } from '../../../../../base/common/platform.js';
21
import { Promises, realcase } from '../../../../../base/node/pfs.js';
22
import { FileChangeType, IFileChange } from '../../../common/files.js';
23
import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, IWatcherErrorEvent } from '../../../common/watcher.js';
24
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
25
26
export class ParcelWatcherInstance extends Disposable {
27
28
private readonly _onDidStop = this._register(new Emitter<{ joinRestart?: Promise<void> }>());
29
readonly onDidStop = this._onDidStop.event;
30
31
private readonly _onDidFail = this._register(new Emitter<void>());
32
readonly onDidFail = this._onDidFail.event;
33
34
private didFail = false;
35
get failed(): boolean { return this.didFail; }
36
37
private didStop = false;
38
get stopped(): boolean { return this.didStop; }
39
40
private readonly includes: ParsedPattern[] | undefined;
41
private readonly excludes: ParsedPattern[] | undefined;
42
43
private readonly subscriptions = new Map<string, Set<(change: IFileChange) => void>>();
44
45
constructor(
46
/**
47
* Signals when the watcher is ready to watch.
48
*/
49
readonly ready: Promise<unknown>,
50
readonly request: IRecursiveWatchRequest,
51
/**
52
* How often this watcher has been restarted in case of an unexpected
53
* shutdown.
54
*/
55
readonly restarts: number,
56
/**
57
* The cancellation token associated with the lifecycle of the watcher.
58
*/
59
readonly token: CancellationToken,
60
/**
61
* An event aggregator to coalesce events and reduce duplicates.
62
*/
63
readonly worker: RunOnceWorker<IFileChange>,
64
private readonly stopFn: () => Promise<void>
65
) {
66
super();
67
68
this.includes = this.request.includes ? parseWatcherPatterns(this.request.path, this.request.includes) : undefined;
69
this.excludes = this.request.excludes ? parseWatcherPatterns(this.request.path, this.request.excludes) : undefined;
70
71
this._register(toDisposable(() => this.subscriptions.clear()));
72
}
73
74
subscribe(path: string, callback: (change: IFileChange) => void): IDisposable {
75
path = URI.file(path).fsPath; // make sure to store the path in `fsPath` form to match it with events later
76
77
let subscriptions = this.subscriptions.get(path);
78
if (!subscriptions) {
79
subscriptions = new Set();
80
this.subscriptions.set(path, subscriptions);
81
}
82
83
subscriptions.add(callback);
84
85
return toDisposable(() => {
86
const subscriptions = this.subscriptions.get(path);
87
if (subscriptions) {
88
subscriptions.delete(callback);
89
90
if (subscriptions.size === 0) {
91
this.subscriptions.delete(path);
92
}
93
}
94
});
95
}
96
97
get subscriptionsCount(): number {
98
return this.subscriptions.size;
99
}
100
101
notifyFileChange(path: string, change: IFileChange): void {
102
const subscriptions = this.subscriptions.get(path);
103
if (subscriptions) {
104
for (const subscription of subscriptions) {
105
subscription(change);
106
}
107
}
108
}
109
110
notifyWatchFailed(): void {
111
this.didFail = true;
112
113
this._onDidFail.fire();
114
}
115
116
include(path: string): boolean {
117
if (!this.includes || this.includes.length === 0) {
118
return true; // no specific includes defined, include all
119
}
120
121
return this.includes.some(include => include(path));
122
}
123
124
exclude(path: string): boolean {
125
return Boolean(this.excludes?.some(exclude => exclude(path)));
126
}
127
128
async stop(joinRestart: Promise<void> | undefined): Promise<void> {
129
this.didStop = true;
130
131
try {
132
await this.stopFn();
133
} finally {
134
this._onDidStop.fire({ joinRestart });
135
this.dispose();
136
}
137
}
138
}
139
140
export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithSubscribe {
141
142
private 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
);
149
150
private static readonly PREDEFINED_EXCLUDES: { [platform: string]: string[] } = {
151
'win32': [],
152
'darwin': [
153
join(homedir(), 'Library', 'Containers') // Triggers access dialog from macOS 14 (https://github.com/microsoft/vscode/issues/208105)
154
],
155
'linux': []
156
};
157
158
private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events';
159
160
private readonly _onDidError = this._register(new Emitter<IWatcherErrorEvent>());
161
readonly onDidError = this._onDidError.event;
162
163
private readonly _watchers = new Map<string /* path */ | number /* correlation ID */, ParcelWatcherInstance>();
164
get watchers() { return this._watchers.values(); }
165
166
// A delay for collecting file changes from Parcel
167
// 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 is
172
// emitted without delay if no events occured over a
173
// duration of 500ms. But we always want to aggregate
174
// events to apply our coleasing logic.
175
//
176
private static readonly FILE_CHANGES_HANDLER_DELAY = 75;
177
178
// Reduce likelyhood of spam from file events via throttling.
179
// (https://github.com/microsoft/vscode/issues/124723)
180
private readonly throttledFileChangesEmitter = this._register(new ThrottledWorker<IFileChange>(
181
{
182
maxWorkChunkSize: 500, // only process up to 500 changes at once before...
183
throttleDelay: 200, // ...resting for 200ms until we process events again...
184
maxBufferedWork: 30000 // ...but never buffering more than 30000 events in memory
185
},
186
events => this._onDidChangeFile.fire(events)
187
));
188
189
private enospcErrorLogged = false;
190
191
constructor() {
192
super();
193
194
this.registerListeners();
195
}
196
197
private registerListeners(): void {
198
const onUncaughtException = (error: unknown) => this.onUnexpectedError(error);
199
const onUnhandledRejection = (error: unknown) => this.onUnexpectedError(error);
200
201
process.on('uncaughtException', onUncaughtException);
202
process.on('unhandledRejection', onUnhandledRejection);
203
204
this._register(toDisposable(() => {
205
process.off('uncaughtException', onUncaughtException);
206
process.off('unhandledRejection', onUnhandledRejection);
207
}));
208
}
209
210
protected override async doWatch(requests: IRecursiveWatchRequest[]): Promise<void> {
211
212
// Figure out duplicates to remove from the requests
213
requests = await this.removeDuplicateRequests(requests);
214
215
// Figure out which watchers to start and which to stop
216
const requestsToStart: IRecursiveWatchRequest[] = [];
217
const watchersToStop = new Set(Array.from(this.watchers));
218
for (const request of requests) {
219
const watcher = this._watchers.get(this.requestToWatcherKey(request));
220
if (watcher && patternsEquals(watcher.request.excludes, request.excludes) && patternsEquals(watcher.request.includes, request.includes) && watcher.request.pollingInterval === request.pollingInterval) {
221
watchersToStop.delete(watcher); // keep watcher
222
} else {
223
requestsToStart.push(request); // start watching
224
}
225
}
226
227
// Logging
228
if (requestsToStart.length) {
229
this.trace(`Request to start watching: ${requestsToStart.map(request => this.requestToString(request)).join(',')}`);
230
}
231
232
if (watchersToStop.size) {
233
this.trace(`Request to stop watching: ${Array.from(watchersToStop).map(watcher => this.requestToString(watcher.request)).join(',')}`);
234
}
235
236
// Stop watching as instructed
237
for (const watcher of watchersToStop) {
238
await this.stopWatching(watcher);
239
}
240
241
// Start watching as instructed
242
for (const request of requestsToStart) {
243
if (request.pollingInterval) {
244
await this.startPolling(request, request.pollingInterval);
245
} else {
246
await this.startWatching(request);
247
}
248
}
249
}
250
251
private requestToWatcherKey(request: IRecursiveWatchRequest): string | number {
252
return typeof request.correlationId === 'number' ? request.correlationId : this.pathToWatcherKey(request.path);
253
}
254
255
private pathToWatcherKey(path: string): string {
256
return isLinux ? path : path.toLowerCase() /* ignore path casing */;
257
}
258
259
private async startPolling(request: IRecursiveWatchRequest, pollingInterval: number, restarts = 0): Promise<void> {
260
const cts = new CancellationTokenSource();
261
262
const instance = new DeferredPromise<void>();
263
264
const snapshotFile = randomPath(tmpdir(), 'vscode-watcher-snapshot');
265
266
// Remember as watcher instance
267
const watcher: ParcelWatcherInstance = new ParcelWatcherInstance(
268
instance.p,
269
request,
270
restarts,
271
cts.token,
272
new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),
273
async () => {
274
cts.dispose(true);
275
276
watcher.worker.flush();
277
watcher.worker.dispose();
278
279
pollingWatcher.dispose();
280
await promises.unlink(snapshotFile);
281
}
282
);
283
this._watchers.set(this.requestToWatcherKey(request), watcher);
284
285
// Path checks for symbolic links / wrong casing
286
const { realPath, realPathDiffers, realPathLength } = await this.normalizePath(request);
287
288
this.trace(`Started watching: '${realPath}' with polling interval '${pollingInterval}'`);
289
290
let counter = 0;
291
292
const pollingWatcher = new RunOnceScheduler(async () => {
293
counter++;
294
295
if (cts.token.isCancellationRequested) {
296
return;
297
}
298
299
// We already ran before, check for events since
300
const parcelWatcherLib = parcelWatcher;
301
try {
302
if (counter > 1) {
303
const parcelEvents = await parcelWatcherLib.getEventsSince(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND });
304
305
if (cts.token.isCancellationRequested) {
306
return;
307
}
308
309
// Handle & emit events
310
this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength);
311
}
312
313
// Store a snapshot of files to the snapshot file
314
await parcelWatcherLib.writeSnapshot(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND });
315
} catch (error) {
316
this.onUnexpectedError(error, request);
317
}
318
319
// Signal we are ready now when the first snapshot was written
320
if (counter === 1) {
321
instance.complete();
322
}
323
324
if (cts.token.isCancellationRequested) {
325
return;
326
}
327
328
// Schedule again at the next interval
329
pollingWatcher.schedule();
330
}, pollingInterval);
331
pollingWatcher.schedule(0);
332
}
333
334
private async startWatching(request: IRecursiveWatchRequest, restarts = 0): Promise<void> {
335
const cts = new CancellationTokenSource();
336
337
const instance = new DeferredPromise<parcelWatcher.AsyncSubscription | undefined>();
338
339
// Remember as watcher instance
340
const watcher: ParcelWatcherInstance = new ParcelWatcherInstance(
341
instance.p,
342
request,
343
restarts,
344
cts.token,
345
new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),
346
async () => {
347
cts.dispose(true);
348
349
watcher.worker.flush();
350
watcher.worker.dispose();
351
352
const watcherInstance = await instance.p;
353
await watcherInstance?.unsubscribe();
354
}
355
);
356
this._watchers.set(this.requestToWatcherKey(request), watcher);
357
358
// Path checks for symbolic links / wrong casing
359
const { realPath, realPathDiffers, realPathLength } = await this.normalizePath(request);
360
361
try {
362
const parcelWatcherLib = parcelWatcher;
363
const parcelWatcherInstance = await parcelWatcherLib.subscribe(realPath, (error, parcelEvents) => {
364
if (watcher.token.isCancellationRequested) {
365
return; // return early when disposed
366
}
367
368
// In any case of an error, treat this like a unhandled exception
369
// that might require the watcher to restart. We do not really know
370
// the state of parcel at this point and as such will try to restart
371
// up to our maximum of restarts.
372
if (error) {
373
this.onUnexpectedError(error, request);
374
}
375
376
// Handle & emit events
377
this.onParcelEvents(parcelEvents, watcher, realPathDiffers, realPathLength);
378
}, {
379
backend: ParcelWatcher.PARCEL_WATCHER_BACKEND,
380
ignore: this.addPredefinedExcludes(watcher.request.excludes)
381
});
382
383
this.trace(`Started watching: '${realPath}' with backend '${ParcelWatcher.PARCEL_WATCHER_BACKEND}'`);
384
385
instance.complete(parcelWatcherInstance);
386
} catch (error) {
387
this.onUnexpectedError(error, request);
388
389
instance.complete(undefined);
390
391
watcher.notifyWatchFailed();
392
this._onDidWatchFail.fire(request);
393
}
394
}
395
396
private addPredefinedExcludes(initialExcludes: string[]): string[] {
397
const excludes = [...initialExcludes];
398
399
const predefinedExcludes = ParcelWatcher.PREDEFINED_EXCLUDES[process.platform];
400
if (Array.isArray(predefinedExcludes)) {
401
for (const exclude of predefinedExcludes) {
402
if (!excludes.includes(exclude)) {
403
excludes.push(exclude);
404
}
405
}
406
}
407
408
return excludes;
409
}
410
411
private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: ParcelWatcherInstance, realPathDiffers: boolean, realPathLength: number): void {
412
if (parcelEvents.length === 0) {
413
return;
414
}
415
416
// Normalize events: handle NFC normalization and symlinks
417
// It is important to do this before checking for includes
418
// to check on the original path.
419
this.normalizeEvents(parcelEvents, watcher.request, realPathDiffers, realPathLength);
420
421
// Check for includes
422
const includedEvents = this.handleIncludes(watcher, parcelEvents);
423
424
// Add to event aggregator for later processing
425
for (const includedEvent of includedEvents) {
426
watcher.worker.work(includedEvent);
427
}
428
}
429
430
private handleIncludes(watcher: ParcelWatcherInstance, parcelEvents: parcelWatcher.Event[]): IFileChange[] {
431
const events: IFileChange[] = [];
432
433
for (const { path, type: parcelEventType } of parcelEvents) {
434
const type = ParcelWatcher.MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE.get(parcelEventType)!;
435
if (this.verboseLogging) {
436
this.traceWithCorrelation(`${type === FileChangeType.ADDED ? '[ADDED]' : type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`, watcher.request);
437
}
438
439
// Apply include filter if any
440
if (!watcher.include(path)) {
441
if (this.verboseLogging) {
442
this.traceWithCorrelation(` >> ignored (not included) ${path}`, watcher.request);
443
}
444
} else {
445
events.push({ type, resource: URI.file(path), cId: watcher.request.correlationId });
446
}
447
}
448
449
return events;
450
}
451
452
private handleParcelEvents(parcelEvents: IFileChange[], watcher: ParcelWatcherInstance): void {
453
454
// Coalesce events: merge events of same kind
455
const coalescedEvents = coalesceEvents(parcelEvents);
456
457
// Filter events: check for specific events we want to exclude
458
const { events: filteredEvents, rootDeleted } = this.filterEvents(coalescedEvents, watcher);
459
460
// Broadcast to clients
461
this.emitEvents(filteredEvents, watcher);
462
463
// Handle root path deletes
464
if (rootDeleted) {
465
this.onWatchedPathDeleted(watcher);
466
}
467
}
468
469
private emitEvents(events: IFileChange[], watcher: ParcelWatcherInstance): void {
470
if (events.length === 0) {
471
return;
472
}
473
474
// Broadcast to clients via throttler
475
const worked = this.throttledFileChangesEmitter.work(events);
476
477
// Logging
478
if (!worked) {
479
this.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 {
481
if (this.throttledFileChangesEmitter.pending > 0) {
482
this.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
}
486
487
private async normalizePath(request: IRecursiveWatchRequest): Promise<{ realPath: string; realPathDiffers: boolean; realPathLength: number }> {
488
let realPath = request.path;
489
let realPathDiffers = false;
490
let realPathLength = request.path.length;
491
492
try {
493
494
// First check for symbolic link
495
realPath = await Promises.realpath(request.path);
496
497
// Second check for casing difference
498
// Note: this will be a no-op on Linux platforms
499
if (request.path === realPath) {
500
realPath = await realcase(request.path) ?? request.path;
501
}
502
503
// Correct watch path as needed
504
if (request.path !== realPath) {
505
realPathLength = realPath.length;
506
realPathDiffers = true;
507
508
this.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
// ignore
512
}
513
514
return { realPath, realPathDiffers, realPathLength };
515
}
516
517
private normalizeEvents(events: parcelWatcher.Event[], request: IRecursiveWatchRequest, realPathDiffers: boolean, realPathLength: number): void {
518
for (const event of events) {
519
520
// Mac uses NFD unicode form on disk, but we want NFC
521
if (isMacintosh) {
522
event.path = normalizeNFC(event.path);
523
}
524
525
// Workaround for https://github.com/parcel-bundler/watcher/issues/68
526
// where watching root drive letter adds extra backslashes.
527
if (isWindows) {
528
if (request.path.length <= 3) { // for ex. c:, C:\
529
event.path = normalize(event.path);
530
}
531
}
532
533
// Convert paths back to original form in case it differs
534
if (realPathDiffers) {
535
event.path = request.path + event.path.substr(realPathLength);
536
}
537
}
538
}
539
540
private filterEvents(events: IFileChange[], watcher: ParcelWatcherInstance): { events: IFileChange[]; rootDeleted?: boolean } {
541
const filteredEvents: IFileChange[] = [];
542
let rootDeleted = false;
543
544
const filter = this.isCorrelated(watcher.request) ? watcher.request.filter : undefined; // filtering is only enabled when correlating because watchers are otherwise potentially reused
545
for (const event of events) {
546
547
// Emit to instance subscriptions if any before filtering
548
if (watcher.subscriptionsCount > 0) {
549
watcher.notifyFileChange(event.resource.fsPath, event);
550
}
551
552
// Filtering
553
rootDeleted = event.type === FileChangeType.DELETED && isEqual(event.resource.fsPath, watcher.request.path, !isLinux);
554
if (isFiltered(event, filter)) {
555
if (this.verboseLogging) {
556
this.traceWithCorrelation(` >> ignored (filtered) ${event.resource.fsPath}`, watcher.request);
557
}
558
559
continue;
560
}
561
562
// Logging
563
this.traceEvent(event, watcher.request);
564
565
filteredEvents.push(event);
566
}
567
568
return { events: filteredEvents, rootDeleted };
569
}
570
571
private onWatchedPathDeleted(watcher: ParcelWatcherInstance): void {
572
this.warn('Watcher shutdown because watched path got deleted', watcher);
573
574
watcher.notifyWatchFailed();
575
this._onDidWatchFail.fire(watcher.request);
576
}
577
578
private onUnexpectedError(error: unknown, request?: IRecursiveWatchRequest): void {
579
const msg = toErrorMessage(error);
580
581
// Specially handle ENOSPC errors that can happen when
582
// the watcher consumes so many file descriptors that
583
// we are running into a limit. We only want to warn
584
// once in this case to avoid log spam.
585
// See https://github.com/microsoft/vscode/issues/7950
586
if (msg.indexOf('No space left on device') !== -1) {
587
if (!this.enospcErrorLogged) {
588
this.error('Inotify limit reached (ENOSPC)', request);
589
590
this.enospcErrorLogged = true;
591
}
592
}
593
594
// Version 2.5.1 introduces 3 new errors on macOS
595
// via https://github.dev/parcel-bundler/watcher/pull/196
596
else if (msg.indexOf('File system must be re-scanned') !== -1) {
597
this.error(msg, request);
598
}
599
600
// Any other error is unexpected and we should try to
601
// restart the watcher as a result to get into healthy
602
// state again if possible and if not attempted too much
603
else {
604
this.error(`Unexpected error: ${msg} (EUNKNOWN)`, request);
605
606
this._onDidError.fire({ request, error: msg });
607
}
608
}
609
610
override async stop(): Promise<void> {
611
await super.stop();
612
613
for (const watcher of this.watchers) {
614
await this.stopWatching(watcher);
615
}
616
}
617
618
protected restartWatching(watcher: ParcelWatcherInstance, delay = 800): void {
619
620
// Restart watcher delayed to accomodate for
621
// changes on disk that have triggered the
622
// need for a restart in the first place.
623
const scheduler = new RunOnceScheduler(async () => {
624
if (watcher.token.isCancellationRequested) {
625
return; // return early when disposed
626
}
627
628
const restartPromise = new DeferredPromise<void>();
629
try {
630
631
// Await the watcher having stopped, as this is
632
// needed to properly re-watch the same path
633
await this.stopWatching(watcher, restartPromise.p);
634
635
// Start watcher again counting the restarts
636
if (watcher.request.pollingInterval) {
637
await this.startPolling(watcher.request, watcher.request.pollingInterval, watcher.restarts + 1);
638
} else {
639
await this.startWatching(watcher.request, watcher.restarts + 1);
640
}
641
} finally {
642
restartPromise.complete();
643
}
644
}, delay);
645
646
scheduler.schedule();
647
watcher.token.onCancellationRequested(() => scheduler.dispose());
648
}
649
650
private async stopWatching(watcher: ParcelWatcherInstance, joinRestart?: Promise<void>): Promise<void> {
651
this.trace(`stopping file watcher`, watcher);
652
653
this._watchers.delete(this.requestToWatcherKey(watcher.request));
654
655
try {
656
await watcher.stop(joinRestart);
657
} catch (error) {
658
this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher.request);
659
}
660
}
661
662
protected async removeDuplicateRequests(requests: IRecursiveWatchRequest[], validatePaths = true): Promise<IRecursiveWatchRequest[]> {
663
664
// Sort requests by path length to have shortest first
665
// to have a way to prevent children to be watched if
666
// parents exist.
667
requests.sort((requestA, requestB) => requestA.path.length - requestB.path.length);
668
669
// Ignore requests for the same paths that have the same correlation
670
const mapCorrelationtoRequests = new Map<number | undefined /* correlation */, Map<string, IRecursiveWatchRequest>>();
671
for (const request of requests) {
672
if (request.excludes.includes(GLOBSTAR)) {
673
continue; // path is ignored entirely (via `**` glob exclude)
674
}
675
676
677
let requestsForCorrelation = mapCorrelationtoRequests.get(request.correlationId);
678
if (!requestsForCorrelation) {
679
requestsForCorrelation = new Map<string, IRecursiveWatchRequest>();
680
mapCorrelationtoRequests.set(request.correlationId, requestsForCorrelation);
681
}
682
683
const path = this.pathToWatcherKey(request.path);
684
if (requestsForCorrelation.has(path)) {
685
this.trace(`ignoring a request for watching who's path is already watched: ${this.requestToString(request)}`);
686
}
687
688
requestsForCorrelation.set(path, request);
689
}
690
691
const normalizedRequests: IRecursiveWatchRequest[] = [];
692
693
for (const requestsForCorrelation of mapCorrelationtoRequests.values()) {
694
695
// Only consider requests for watching that are not
696
// a child of an existing request path to prevent
697
// duplication. In addition, drop any request where
698
// everything is excluded (via `**` glob).
699
//
700
// However, allow explicit requests to watch folders
701
// that are symbolic links because the Parcel watcher
702
// does not allow to recursively watch symbolic links.
703
704
const requestTrie = TernarySearchTree.forPaths<IRecursiveWatchRequest>(!isLinux);
705
706
for (const request of requestsForCorrelation.values()) {
707
708
// Check for overlapping request paths (but preserve symbolic links)
709
if (requestTrie.findSubstr(request.path)) {
710
if (requestTrie.has(request.path)) {
711
this.trace(`ignoring a request for watching who's path is already watched: ${this.requestToString(request)}`);
712
} else {
713
try {
714
if (!(await promises.lstat(request.path)).isSymbolicLink()) {
715
this.trace(`ignoring a request for watching who's parent is already watched: ${this.requestToString(request)}`);
716
717
continue;
718
}
719
} catch (error) {
720
this.trace(`ignoring a request for watching who's lstat failed to resolve: ${this.requestToString(request)} (error: ${error})`);
721
722
this._onDidWatchFail.fire(request);
723
724
continue;
725
}
726
}
727
}
728
729
// Check for invalid paths
730
if (validatePaths && !(await this.isPathValid(request.path))) {
731
this._onDidWatchFail.fire(request);
732
733
continue;
734
}
735
736
requestTrie.set(request.path, request);
737
}
738
739
normalizedRequests.push(...Array.from(requestTrie).map(([, request]) => request));
740
}
741
742
return normalizedRequests;
743
}
744
745
private async isPathValid(path: string): Promise<boolean> {
746
try {
747
const stat = await promises.stat(path);
748
if (!stat.isDirectory()) {
749
this.trace(`ignoring a path for watching that is a file and not a folder: ${path}`);
750
751
return false;
752
}
753
} catch (error) {
754
this.trace(`ignoring a path for watching who's stat info failed to resolve: ${path} (error: ${error})`);
755
756
return false;
757
}
758
759
return true;
760
}
761
762
subscribe(path: string, callback: (error: true | null, change?: IFileChange) => void): IDisposable | undefined {
763
for (const watcher of this.watchers) {
764
if (watcher.failed) {
765
continue; // watcher has already failed
766
}
767
768
if (!isEqualOrParent(path, watcher.request.path, !isLinux)) {
769
continue; // watcher does not consider this path
770
}
771
772
if (
773
watcher.exclude(path) ||
774
!watcher.include(path)
775
) {
776
continue; // parcel instance does not consider this path
777
}
778
779
const disposables = new DisposableStore();
780
781
disposables.add(Event.once(watcher.onDidStop)(async e => {
782
await e.joinRestart; // if we are restarting, await that so that we can possibly reuse this watcher again
783
if (disposables.isDisposed) {
784
return;
785
}
786
787
callback(true /* error */);
788
}));
789
disposables.add(Event.once(watcher.onDidFail)(() => callback(true /* error */)));
790
disposables.add(watcher.subscribe(path, change => callback(null, change)));
791
792
return disposables;
793
}
794
795
return undefined;
796
}
797
798
protected trace(message: string, watcher?: ParcelWatcherInstance): void {
799
if (this.verboseLogging) {
800
this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher?.request) });
801
}
802
}
803
804
protected warn(message: string, watcher?: ParcelWatcherInstance) {
805
this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher?.request) });
806
}
807
808
private error(message: string, request?: IRecursiveWatchRequest) {
809
this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, request) });
810
}
811
812
private toMessage(message: string, request?: IRecursiveWatchRequest): string {
813
return request ? `[File Watcher ('parcel')] ${message} (path: ${request.path})` : `[File Watcher ('parcel')] ${message}`;
814
}
815
816
protected get recursiveWatcher() { return this; }
817
}
818
819