1
>
/*---------------------------------------------------------------------------------------------
nodejsWatcherLib.ts
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 { watch, promises } from 'fs';
7
>
import { RunOnceWorker, ThrottledWorker } from '../../../../../base/common/async.js';
8
>
import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
9
>
import { isEqual, isEqualOrParent } from '../../../../../base/common/extpath.js';
10
>
import { Disposable, DisposableStore, IDisposable, thenRegisterOrDispose, toDisposable } from '../../../../../base/common/lifecycle.js';
11
>
import { normalizeNFC } from '../../../../../base/common/normalization.js';
12
>
import { basename, dirname, join } from '../../../../../base/common/path.js';
13
>
import { isLinux, isMacintosh } from '../../../../../base/common/platform.js';
14
>
import { joinPath } from '../../../../../base/common/resources.js';
15
>
import { URI } from '../../../../../base/common/uri.js';
16
>
import { Promises } from '../../../../../base/node/pfs.js';
17
>
import { FileChangeFilter, FileChangeType, IFileChange } from '../../../common/files.js';
18
>
import { ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, isWatchRequestWithCorrelation } from '../../../common/watcher.js';
19
>
import { Lazy } from '../../../../../base/common/lazy.js';
20
>
import { ParsedPattern } from '../../../../../base/common/glob.js';
21
>
22
>
export class NodeJSFileWatcherLibrary extends Disposable {
23
>
24
>
// A delay in reacting to file deletes to support
25
>
// atomic save operations where a tool may chose
26
>
// to delete a file before creating it again for
27
>
// an update.
28
>
private static readonly FILE_DELETE_HANDLER_DELAY = 100;
29
>
30
>
// A delay for collecting file changes from node.js
31
>
// before collecting them for coalescing and emitting
32
>
// Same delay as used for the recursive watcher.
33
>
private static readonly FILE_CHANGES_HANDLER_DELAY = 75;
34
>
35
>
// Reduce likelyhood of spam from file events via throttling.
36
>
// These numbers are a bit more aggressive compared to the
37
>
// recursive watcher because we can have many individual
38
>
// node.js watchers per request.
39
>
// (https://github.com/microsoft/vscode/issues/124723)
40
>
private readonly throttledFileChangesEmitter = this._register(new ThrottledWorker<IFileChange>(
41
>
{
42
>
maxWorkChunkSize: 100, // only process up to 100 changes at once before...
43
>
throttleDelay: 200, // ...resting for 200ms until we process events again...
44
>
maxBufferedWork: 10000 // ...but never buffering more than 10000 events in memory
45
>
},
46
>
events => this.onDidFilesChange(events)
47
>
));
48
>
49
>
// Aggregate file changes over FILE_CHANGES_HANDLER_DELAY
50
>
// to coalesce events and reduce spam.
51
>
private readonly fileChangesAggregator = this._register(new RunOnceWorker<IFileChange>(events => this.handleFileChanges(events), NodeJSFileWatcherLibrary.FILE_CHANGES_HANDLER_DELAY));
52
>
53
>
private readonly excludes: ParsedPattern[];
54
>
private readonly includes: ParsedPattern[] | undefined;
55
>
private readonly filter: FileChangeFilter | undefined;
56
>
57
>
private readonly cts = new CancellationTokenSource();
58
>
59
>
private readonly realPath = new Lazy(async () => {
60
>
61
>
// This property is intentionally `Lazy` and not using `realcase()` as the counterpart
62
>
// in the recursive watcher because of the amount of paths this watcher is dealing with.
63
>
// We try as much as possible to avoid even needing `realpath()` if we can because even
64
>
// that method does an `lstat()` per segment of the path.
65
>
66
>
let result = this.request.path;
67
>
68
>
try {
69
>
result = await Promises.realpath(this.request.path);
70
>
71
>
if (this.request.path !== result) {
72
>
this.trace(`correcting a path to watch that seems to be a symbolic link (original: ${this.request.path}, real: ${result})`);
73
>
}
74
>
} catch (error) {
75
>
// ignore
76
>
}
77
>
78
>
return result;
79
>
});
80
>
81
>
readonly ready: Promise<void>;
82
>
83
>
private _isReusingRecursiveWatcher = false;
84
>
get isReusingRecursiveWatcher(): boolean { return this._isReusingRecursiveWatcher; }
85
>
86
>
private didFail = false;
87
>
get failed(): boolean { return this.didFail; }
88
>
89
>
constructor(
90
private readonly request: INonRecursiveWatchRequest,
91
private readonly recursiveWatcher: IRecursiveWatcherWithSubscribe | undefined,