-
Notifications
You must be signed in to change notification settings - Fork 29.8k
/
chokidarWatcherService.ts
175 lines (143 loc) · 6.4 KB
/
chokidarWatcherService.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as chokidar from 'vscode-chokidar';
import * as fs from 'fs';
import * as gracefulFs from 'graceful-fs';
gracefulFs.gracefulify(fs);
import { TPromise } from 'vs/base/common/winjs.base';
import { FileChangeType } from 'vs/platform/files/common/files';
import { ThrottledDelayer } from 'vs/base/common/async';
import * as strings from 'vs/base/common/strings';
import { normalizeNFC } from 'vs/base/common/normalization';
import { realcaseSync } from 'vs/base/node/extfs';
import { isMacintosh } from 'vs/base/common/platform';
import * as watcher from 'vs/workbench/services/files/node/watcher/common';
import { IWatcherRequest, IWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcher';
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
export class ChokidarWatcherService implements IWatcherService {
private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
private static readonly EVENT_SPAM_WARNING_THRESHOLD = 60 * 1000; // warn after certain time span of event spam
private spamCheckStartTime: number;
private spamWarningLogged: boolean;
private enospcErrorLogged: boolean;
private toDispose: IDisposable[] = [];
public watch(request: IWatcherRequest): TPromise<void> {
const watcherOpts: chokidar.IOptions = {
ignoreInitial: true,
ignorePermissionErrors: true,
followSymlinks: true, // this is the default of chokidar and supports file events through symlinks
ignored: request.ignored,
interval: 1000, // while not used in normal cases, if any error causes chokidar to fallback to polling, increase its intervals
binaryInterval: 1000,
disableGlobbing: true // fix https://github.com/Microsoft/vscode/issues/4586
};
// Chokidar fails when the basePath does not match case-identical to the path on disk
// so we have to find the real casing of the path and do some path massaging to fix this
// see https://github.com/paulmillr/chokidar/issues/418
const originalBasePath = request.basePath;
const realBasePath = isMacintosh ? (realcaseSync(originalBasePath) || originalBasePath) : originalBasePath;
const realBasePathLength = realBasePath.length;
const realBasePathDiffers = (originalBasePath !== realBasePath);
if (realBasePathDiffers) {
console.warn(`Watcher basePath does not match version on disk and was corrected (original: ${originalBasePath}, real: ${realBasePath})`);
}
const chokidarWatcher = chokidar.watch(realBasePath, watcherOpts);
// Detect if for some reason the native watcher library fails to load
if (isMacintosh && !chokidarWatcher.options.useFsEvents) {
console.error('Watcher is not using native fsevents library and is falling back to unefficient polling.');
}
let undeliveredFileEvents: watcher.IRawFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY);
this.toDispose.push(toDisposable(() => {
chokidarWatcher.close();
fileEventDelayer.cancel();
}));
return new TPromise<void>((c, e, p) => {
chokidarWatcher.on('all', (type: string, path: string) => {
if (isMacintosh) {
// Mac: uses NFD unicode form on disk, but we want NFC
// See also https://github.com/nodejs/node/issues/2165
path = normalizeNFC(path);
}
if (path.indexOf(realBasePath) < 0) {
return; // we really only care about absolute paths here in our basepath context here
}
// Make sure to convert the path back to its original basePath form if the realpath is different
if (realBasePathDiffers) {
path = originalBasePath + path.substr(realBasePathLength);
}
let event: watcher.IRawFileChange = null;
// Change
if (type === 'change') {
event = { type: 0, path };
}
// Add
else if (type === 'add' || type === 'addDir') {
event = { type: 1, path };
}
// Delete
else if (type === 'unlink' || type === 'unlinkDir') {
event = { type: 2, path };
}
if (event) {
// Logging
if (request.verboseLogging) {
console.log(event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', event.path);
}
// Check for spam
const now = Date.now();
if (undeliveredFileEvents.length === 0) {
this.spamWarningLogged = false;
this.spamCheckStartTime = now;
} else if (!this.spamWarningLogged && this.spamCheckStartTime + ChokidarWatcherService.EVENT_SPAM_WARNING_THRESHOLD < now) {
this.spamWarningLogged = true;
console.warn(strings.format('Watcher is busy catching up with {0} file changes in 60 seconds. Latest changed path is "{1}"', undeliveredFileEvents.length, event.path));
}
// Add to buffer
undeliveredFileEvents.push(event);
// Delay and send buffer
fileEventDelayer.trigger(() => {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
// Broadcast to clients normalized
const res = watcher.normalize(events);
p(res);
// Logging
if (request.verboseLogging) {
res.forEach(r => {
console.log(' >> normalized', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path);
});
}
return TPromise.as(null);
});
}
});
chokidarWatcher.on('error', (error: Error) => {
if (error) {
// Specially handle ENOSPC errors that can happen when
// the watcher consumes so many file descriptors that
// we are running into a limit. We only want to warn
// once in this case to avoid log spam.
// See https://github.com/Microsoft/vscode/issues/7950
if ((<any>error).code === 'ENOSPC') {
if (!this.enospcErrorLogged) {
this.enospcErrorLogged = true;
e(new Error('Inotify limit reached (ENOSPC)'));
}
} else {
console.error(error.toString());
}
}
});
}, () => {
this.toDispose = dispose(this.toDispose);
});
}
public stop(): TPromise<void> {
this.toDispose = dispose(this.toDispose);
return TPromise.as(void 0);
}
}