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