in src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts [101:284]
private _watch(basePath: string, requests: IWatcherRequest[]): IWatcher {
if (this._verboseLogging) {
this.log(`Start watching: ${basePath}]`);
}
const pollingInterval = this._pollingInterval || 5000;
const usePolling = this._usePolling;
if (usePolling && this._verboseLogging) {
this.log(`Use polling instead of fs.watch: Polling interval ${pollingInterval} ms`);
}
const watcherOpts: chokidar.WatchOptions = {
ignoreInitial: true,
ignorePermissionErrors: true,
followSymlinks: true, // this is the default of chokidar and supports file events through symlinks
interval: pollingInterval, // while not used in normal cases, if any error causes chokidar to fallback to polling, increase its intervals
binaryInterval: pollingInterval,
usePolling: usePolling,
disableGlobbing: true // fix https://github.com/Microsoft/vscode/issues/4586
};
const excludes: string[] = [];
// if there's only one request, use the built-in ignore-filterering
const isSingleFolder = requests.length === 1;
if (isSingleFolder) {
excludes.push(...requests[0].excludes);
}
if ((isMacintosh || isLinux) && (basePath.length === 0 || basePath === '/')) {
excludes.push('/dev/**');
if (isLinux) {
excludes.push('/proc/**', '/sys/**');
}
}
watcherOpts.ignored = excludes;
// 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 realBasePath = isMacintosh ? (realcaseSync(basePath) || basePath) : basePath;
const realBasePathLength = realBasePath.length;
const realBasePathDiffers = (basePath !== realBasePath);
if (realBasePathDiffers) {
this.warn(`Watcher basePath does not match version on disk and was corrected (original: ${basePath}, real: ${realBasePath})`);
}
let chokidarWatcher: chokidar.FSWatcher | null = chokidar.watch(realBasePath, watcherOpts);
this._watcherCount++;
// Detect if for some reason the native watcher library fails to load
if (isMacintosh && chokidarWatcher.options && !chokidarWatcher.options.useFsEvents) {
this.warn('Watcher is not using native fsevents library and is falling back to unefficient polling.');
}
let undeliveredFileEvents: IDiskFileChange[] = [];
let fileEventDelayer: ThrottledDelayer<undefined> | null = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY);
const watcher: IWatcher = {
requests,
stop: () => {
try {
if (this._verboseLogging) {
this.log(`Stop watching: ${basePath}]`);
}
if (chokidarWatcher) {
chokidarWatcher.close();
this._watcherCount--;
chokidarWatcher = null;
}
if (fileEventDelayer) {
fileEventDelayer.cancel();
fileEventDelayer = null;
}
} catch (error) {
this.warn('Error while stopping watcher: ' + error.toString());
}
}
};
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 = basePath + path.substr(realBasePathLength);
}
let eventType: FileChangeType;
switch (type) {
case 'change':
eventType = FileChangeType.UPDATED;
break;
case 'add':
case 'addDir':
eventType = FileChangeType.ADDED;
break;
case 'unlink':
case 'unlinkDir':
eventType = FileChangeType.DELETED;
break;
default:
return;
}
// if there's more than one request we need to do
// extra filtering due to potentially overlapping roots
if (!isSingleFolder) {
if (isIgnored(path, watcher.requests)) {
return;
}
}
let event = { type: eventType, path };
// Logging
if (this._verboseLogging) {
this.log(`${eventType === FileChangeType.ADDED ? '[ADDED]' : eventType === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${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;
this.warn(`Watcher is busy catching up with ${undeliveredFileEvents.length} file changes in 60 seconds. Latest changed path is "${event.path}"`);
}
// Add to buffer
undeliveredFileEvents.push(event);
if (fileEventDelayer) {
// Delay and send buffer
fileEventDelayer.trigger(() => {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
// Broadcast to clients normalized
const res = normalizeFileChanges(events);
this._onWatchEvent.fire(res);
// Logging
if (this._verboseLogging) {
res.forEach(r => {
this.log(` >> normalized ${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`);
});
}
return Promise.resolve(undefined);
});
}
});
chokidarWatcher.on('error', (error: NodeJS.ErrnoException) => {
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 (error.code === 'ENOSPC') {
if (!this.enospcErrorLogged) {
this.enospcErrorLogged = true;
this.stop();
this.error('Inotify limit reached (ENOSPC)');
}
} else {
this.warn(error.toString());
}
}
});
return watcher;
}