watcher - introduce `files.watcherInclude` for explicit watching of folders (#132483)

上级 4564730b
...@@ -1069,6 +1069,7 @@ export interface IFilesConfiguration { ...@@ -1069,6 +1069,7 @@ export interface IFilesConfiguration {
associations: { [filepattern: string]: string }; associations: { [filepattern: string]: string };
exclude: IExpression; exclude: IExpression;
watcherExclude: { [filepattern: string]: boolean }; watcherExclude: { [filepattern: string]: boolean };
watcherInclude: string[];
encoding: string; encoding: string;
autoGuessEncoding: boolean; autoGuessEncoding: boolean;
defaultLanguage: string; defaultLanguage: string;
......
...@@ -23,7 +23,7 @@ import { readFileIntoStream } from 'vs/platform/files/common/io'; ...@@ -23,7 +23,7 @@ import { readFileIntoStream } from 'vs/platform/files/common/io';
import { FileWatcher as NodeJSWatcherService } from 'vs/platform/files/node/watcher/nodejs/watcherService'; import { FileWatcher as NodeJSWatcherService } from 'vs/platform/files/node/watcher/nodejs/watcherService';
import { FileWatcher as NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcherService'; import { FileWatcher as NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcherService';
import { FileWatcher as UnixWatcherService } from 'vs/platform/files/node/watcher/unix/watcherService'; import { FileWatcher as UnixWatcherService } from 'vs/platform/files/node/watcher/unix/watcherService';
import { IDiskFileChange, ILogMessage, toFileChanges } from 'vs/platform/files/node/watcher/watcher'; import { IDiskFileChange, ILogMessage, IWatchRequest, toFileChanges } from 'vs/platform/files/node/watcher/watcher';
import { FileWatcher as WindowsWatcherService } from 'vs/platform/files/node/watcher/win32/watcherService'; import { FileWatcher as WindowsWatcherService } from 'vs/platform/files/node/watcher/win32/watcherService';
import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import product from 'vs/platform/product/common/product'; import product from 'vs/platform/product/common/product';
...@@ -533,23 +533,23 @@ export class DiskFileSystemProvider extends Disposable implements ...@@ -533,23 +533,23 @@ export class DiskFileSystemProvider extends Disposable implements
readonly onDidChangeFile = this._onDidChangeFile.event; readonly onDidChangeFile = this._onDidChangeFile.event;
private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | undefined; private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | undefined;
private readonly recursiveFoldersToWatch: { path: string, excludes: string[] }[] = []; private readonly recursiveFoldersToWatch: IWatchRequest[] = [];
private recursiveWatchRequestDelayer = this._register(new ThrottledDelayer<void>(0)); private recursiveWatchRequestDelayer = this._register(new ThrottledDelayer<void>(0));
private recursiveWatcherLogLevelListener: IDisposable | undefined; private recursiveWatcherLogLevelListener: IDisposable | undefined;
watch(resource: URI, opts: IWatchOptions): IDisposable { watch(resource: URI, opts: IWatchOptions): IDisposable {
if (opts.recursive) { if (opts.recursive) {
return this.watchRecursive(resource, opts.excludes); return this.watchRecursive(resource, opts);
} }
return this.watchNonRecursive(resource); return this.watchNonRecursive(resource);
} }
private watchRecursive(resource: URI, excludes: string[]): IDisposable { private watchRecursive(resource: URI, opts: IWatchOptions): IDisposable {
// Add to list of folders to watch recursively // Add to list of folders to watch recursively
const folderToWatch = { path: this.toFilePath(resource), excludes }; const folderToWatch: IWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes };
const remove = insert(this.recursiveFoldersToWatch, folderToWatch); const remove = insert(this.recursiveFoldersToWatch, folderToWatch);
// Trigger update // Trigger update
...@@ -578,7 +578,7 @@ export class DiskFileSystemProvider extends Disposable implements ...@@ -578,7 +578,7 @@ export class DiskFileSystemProvider extends Disposable implements
// Reuse existing // Reuse existing
if (this.recursiveWatcher instanceof NsfwWatcherService) { if (this.recursiveWatcher instanceof NsfwWatcherService) {
this.recursiveWatcher.setFolders(this.recursiveFoldersToWatch); this.recursiveWatcher.watch(this.recursiveFoldersToWatch);
} }
// Create new // Create new
...@@ -592,7 +592,7 @@ export class DiskFileSystemProvider extends Disposable implements ...@@ -592,7 +592,7 @@ export class DiskFileSystemProvider extends Disposable implements
if (this.recursiveFoldersToWatch.length > 0) { if (this.recursiveFoldersToWatch.length > 0) {
let watcherImpl: { let watcherImpl: {
new( new(
folders: { path: string, excludes: string[] }[], folders: IWatchRequest[],
onChange: (changes: IDiskFileChange[]) => void, onChange: (changes: IDiskFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void, onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean, verboseLogging: boolean,
......
...@@ -7,30 +7,27 @@ import * as nsfw from 'nsfw'; ...@@ -7,30 +7,27 @@ import * as nsfw from 'nsfw';
import { ThrottledDelayer } from 'vs/base/common/async'; import { ThrottledDelayer } from 'vs/base/common/async';
import { toErrorMessage } from 'vs/base/common/errorMessage'; import { toErrorMessage } from 'vs/base/common/errorMessage';
import { Emitter } from 'vs/base/common/event'; import { Emitter } from 'vs/base/common/event';
import { isEqualOrParent } from 'vs/base/common/extpath';
import { parse, ParsedPattern } from 'vs/base/common/glob'; import { parse, ParsedPattern } from 'vs/base/common/glob';
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';
import { TernarySearchTree } from 'vs/base/common/map';
import { normalizeNFC } from 'vs/base/common/normalization'; import { normalizeNFC } from 'vs/base/common/normalization';
import { join } from 'vs/base/common/path'; import { join } from 'vs/base/common/path';
import { isMacintosh } from 'vs/base/common/platform'; import { isMacintosh } from 'vs/base/common/platform';
import { realcaseSync, realpathSync } from 'vs/base/node/extpath'; import { realcaseSync, realpathSync } from 'vs/base/node/extpath';
import { FileChangeType } from 'vs/platform/files/common/files'; import { FileChangeType } from 'vs/platform/files/common/files';
import { IWatcherRequest, IWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcher'; import { IWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcher';
import { IDiskFileChange, ILogMessage, normalizeFileChanges } from 'vs/platform/files/node/watcher/watcher'; import { IDiskFileChange, ILogMessage, normalizeFileChanges, IWatchRequest } from 'vs/platform/files/node/watcher/watcher';
const nsfwActionToRawChangeType: { [key: number]: number } = [];
nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED;
nsfwActionToRawChangeType[nsfw.actions.MODIFIED] = FileChangeType.UPDATED;
nsfwActionToRawChangeType[nsfw.actions.DELETED] = FileChangeType.DELETED;
interface IWatcher { interface IWatcher {
start(): void;
stop(): void;
}
interface IPathWatcher { /**
readonly ready: Promise<IWatcher>; * The NSFW instance is resolved when the watching has started.
watcher?: IWatcher; */
readonly instance: Promise<nsfw.NSFW>;
/**
* Associated ignored patterns for the watcher that can be updated.
*/
ignored: ParsedPattern[]; ignored: ParsedPattern[];
} }
...@@ -38,15 +35,24 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { ...@@ -38,15 +35,24 @@ export class NsfwWatcherService extends Disposable 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 FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
private static readonly MAP_NSFW_ACTION_TO_FILE_CHANGE = new Map<number, number>(
[
[nsfw.actions.CREATED, FileChangeType.ADDED],
[nsfw.actions.MODIFIED, FileChangeType.UPDATED],
[nsfw.actions.DELETED, FileChangeType.DELETED],
]
);
private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>()); private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event; readonly onDidChangeFile = this._onDidChangeFile.event;
private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>()); private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>());
readonly onDidLogMessage = this._onDidLogMessage.event; readonly onDidLogMessage = this._onDidLogMessage.event;
private pathWatchers: { [watchPath: string]: IPathWatcher } = {}; private readonly watchers = new Map<string, IWatcher>();
private verboseLogging: boolean | undefined;
private enospcErrorLogged: boolean | undefined; private verboseLogging = false;
private enospcErrorLogged = false;
constructor() { constructor() {
super(); super();
...@@ -54,56 +60,135 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { ...@@ -54,56 +60,135 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
process.on('uncaughtException', (error: Error | string) => this.onError(error)); process.on('uncaughtException', (error: Error | string) => this.onError(error));
} }
async setRoots(roots: IWatcherRequest[]): Promise<void> { async watch(requests: IWatchRequest[]): Promise<void> {
const normalizedRoots = this.normalizeRoots(roots);
// Figure out duplicates to remove from the requests
const normalizedRequests = this.normalizeRequests(requests);
// Gather roots that are not currently being watched // Gather paths that we should start watching
const rootsToStartWatching = normalizedRoots.filter(root => { const requestsToStartWatching = normalizedRequests.filter(request => {
return !(root.path in this.pathWatchers); return !this.watchers.has(request.path);
}); });
// Gather current roots that don't exist in the new roots array // Gather paths that we should stop watching
const rootsToStopWatching = Object.keys(this.pathWatchers).filter(root => { const pathsToStopWatching = Array.from(this.watchers.keys()).filter(watchedPath => {
return normalizedRoots.every(normalizedRoot => normalizedRoot.path !== root); return !normalizedRequests.find(normalizedRequest => normalizedRequest.path === watchedPath);
}); });
// Logging // Logging
this.debug(`Start watching: ${rootsToStartWatching.map(root => `${root.path} (excludes: ${root.excludes})`).join(',')}`); this.debug(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes})`).join(',')}`);
this.debug(`Stop watching: ${rootsToStopWatching.join(',')}`); this.debug(`Request to stop watching: ${pathsToStopWatching.join(',')}`);
// Stop watching some roots // Stop watching as instructed
for (const root of rootsToStopWatching) { for (const pathToStopWatching of pathsToStopWatching) {
this.pathWatchers[root].ready.then(watcher => watcher.stop()); this.stopWatching(pathToStopWatching);
delete this.pathWatchers[root];
} }
// Start watching some roots // Start watching as instructed
for (const root of rootsToStartWatching) { for (const request of requestsToStartWatching) {
this.doWatch(root); this.startWatching(request);
} }
// Refresh ignored arrays in case they changed // Update ignore rules for all watchers
for (const root of roots) { for (const request of normalizedRequests) {
if (root.path in this.pathWatchers) { const watcher = this.watchers.get(request.path);
this.pathWatchers[root.path].ignored = Array.isArray(root.excludes) ? root.excludes.map(ignored => parse(ignored)) : []; if (watcher) {
watcher.ignored = this.toExcludePatterns(request.excludes);
} }
} }
} }
private doWatch(request: IWatcherRequest): void { private toExcludePatterns(excludes: string[] | undefined): ParsedPattern[] {
let readyPromiseResolve: (watcher: IWatcher) => void; return Array.isArray(excludes) ? excludes.map(exclude => parse(exclude)) : [];
this.pathWatchers[request.path] = { }
ready: new Promise<IWatcher>(resolve => readyPromiseResolve = resolve),
ignored: Array.isArray(request.excludes) ? request.excludes.map(ignored => parse(ignored)) : [] private startWatching(request: IWatchRequest): void {
// Remember as watcher instance
let nsfwPromiseResolve: (watcher: nsfw.NSFW) => void;
const watcher: IWatcher = {
instance: new Promise<nsfw.NSFW>(resolve => nsfwPromiseResolve = resolve),
ignored: this.toExcludePatterns(request.excludes)
}; };
this.watchers.set(request.path, watcher);
// Path checks for symbolic links / wrong casing
const { realBasePathDiffers, realBasePathLength } = this.checkRequest(request);
let undeliveredFileEvents: IDiskFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer<void>(NsfwWatcherService.FS_EVENT_DELAY);
const onFileEvent = (path: string, type: FileChangeType) => {
if (!this.isPathIgnored(path, watcher.ignored)) {
undeliveredFileEvents.push({ type, path });
} else if (this.verboseLogging) {
this.log(` >> ignored ${path}`);
}
};
nsfw(request.path, events => {
for (const event of events) {
// Logging
if (this.verboseLogging) {
const logPath = event.action === nsfw.actions.RENAMED ? `${join(event.directory, event.oldFile || '')} -> ${event.newFile}` : join(event.directory, event.file || '');
this.log(`${event.action === nsfw.actions.CREATED ? '[CREATED]' : event.action === nsfw.actions.DELETED ? '[DELETED]' : event.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`);
}
// Rename: convert into DELETE & ADD
if (event.action === nsfw.actions.RENAMED) {
onFileEvent(join(event.directory, event.oldFile || ''), FileChangeType.DELETED); // Rename fires when a file's name changes within a single directory
onFileEvent(join(event.newDirectory || event.directory, event.newFile || ''), FileChangeType.ADDED);
}
// Created, modified, deleted: taks as is
else {
onFileEvent(join(event.directory, event.file || ''), NsfwWatcherService.MAP_NSFW_ACTION_TO_FILE_CHANGE.get(event.action)!);
}
}
// Send events delayed and normalized
fileEventDelayer.trigger(async () => {
// Remember as delivered
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
// Broadcast to clients normalized
const normalizedEvents = normalizeFileChanges(this.normalizeEvents(events, request, realBasePathDiffers, realBasePathLength));
this._onDidChangeFile.fire(normalizedEvents);
// Logging
if (this.verboseLogging) {
for (const event of normalizedEvents) {
this.log(` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
}
}
});
}, {
errorCallback: error => this.onError(error)
}).then(async nsfwWatcher => {
// Begin watching
await nsfwWatcher.start();
return nsfwWatcher;
}).then(nsfwWatcher => {
this.debug(`Started watching: ${request.path}`);
nsfwPromiseResolve(nsfwWatcher);
});
}
private checkRequest(request: IWatchRequest): { realBasePathDiffers: boolean, realBasePathLength: number } {
let realBasePathDiffers = false;
let realBasePathLength = request.path.length;
// NSFW does not report file changes in the path provided on macOS if // NSFW does not report file changes in the path provided on macOS if
// - the path uses wrong casing // - the path uses wrong casing
// - the path is a symbolic link // - the path is a symbolic link
// We have to detect this case and massage the events to correct this. // We have to detect this case and massage the events to correct this.
// Note: Other platforms do not seem to have these path issues. // Note: Other platforms do not seem to have these path issues.
let realBasePathDiffers = false;
let realBasePathLength = request.path.length;
if (isMacintosh) { if (isMacintosh) {
try { try {
...@@ -119,152 +204,117 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { ...@@ -119,152 +204,117 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
realBasePathLength = realBasePath.length; realBasePathLength = realBasePath.length;
realBasePathDiffers = true; realBasePathDiffers = true;
this.warn(`Watcher basePath does not match version on disk and will be corrected (original: ${request.path}, real: ${realBasePath})`); this.warn(`correcting a path to watch that seems to be a symbolic link (original: ${request.path}, real: ${realBasePath})`);
} }
} catch (error) { } catch (error) {
// ignore // ignore
} }
} }
this.debug(`Start watching with nsfw: ${request.path}`); return { realBasePathDiffers, realBasePathLength };
}
let undeliveredFileEvents: IDiskFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer<void>(NsfwWatcherService.FS_EVENT_DELAY);
nsfw(request.path, events => {
for (const e of events) {
// Logging
if (this.verboseLogging) {
const logPath = e.action === nsfw.actions.RENAMED ? join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : join(e.directory, e.file || '');
this.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`);
}
// Convert nsfw event to `IRawFileChange` and add to queue
let absolutePath: string;
if (e.action === nsfw.actions.RENAMED) {
absolutePath = join(e.directory, e.oldFile || ''); // Rename fires when a file's name changes within a single directory
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { private normalizeEvents(events: IDiskFileChange[], request: IWatchRequest, realBasePathDiffers: boolean, realBasePathLength: number): IDiskFileChange[] {
undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath }); if (isMacintosh) {
} else if (this.verboseLogging) { for (const event of events) {
this.log(` >> ignored ${absolutePath}`);
}
absolutePath = join(e.newDirectory || e.directory, e.newFile || ''); // Mac uses NFD unicode form on disk, but we want NFC
event.path = normalizeNFC(event.path);
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { // Convert paths back to original form in case it differs
undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath }); if (realBasePathDiffers) {
} else if (this.verboseLogging) { event.path = request.path + event.path.substr(realBasePathLength);
this.log(` >> ignored ${absolutePath}`);
}
} else {
absolutePath = join(e.directory, e.file || '');
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({
type: nsfwActionToRawChangeType[e.action],
path: absolutePath
});
} else if (this.verboseLogging) {
this.log(` >> ignored ${absolutePath}`);
}
} }
} }
}
// Delay and send buffer return events;
fileEventDelayer.trigger(async () => {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
if (isMacintosh) {
for (const e of events) {
// Mac uses NFD unicode form on disk, but we want NFC
e.path = normalizeNFC(e.path);
// Convert paths back to original form in case it differs
if (realBasePathDiffers) {
e.path = request.path + e.path.substr(realBasePathLength);
}
}
}
// Broadcast to clients normalized
const normalizedEvents = normalizeFileChanges(events);
this._onDidChangeFile.fire(normalizedEvents);
// Logging
if (this.verboseLogging) {
for (const e of normalizedEvents) {
this.log(` >> normalized ${e.type === FileChangeType.ADDED ? '[ADDED]' : e.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${e.path}`);
}
}
});
}, {
errorCallback: error => this.onError(error)
}).then(watcher => {
this.pathWatchers[request.path].watcher = watcher;
const startPromise = watcher.start();
startPromise.then(() => readyPromiseResolve(watcher));
return startPromise;
});
} }
private onError(error: unknown): void { private onError(error: unknown): void {
const msg = toErrorMessage(error);
// Specially handle ENOSPC errors that can happen when // Specially handle ENOSPC errors that can happen when
// the watcher consumes so many file descriptors that // the watcher consumes so many file descriptors that
// we are running into a limit. We only want to warn // we are running into a limit. We only want to warn
// once in this case to avoid log spam. // once in this case to avoid log spam.
// See https://github.com/microsoft/vscode/issues/7950 // See https://github.com/microsoft/vscode/issues/7950
const msg = toErrorMessage(error);
if (msg.indexOf('Inotify limit reached') !== -1 && !this.enospcErrorLogged) { if (msg.indexOf('Inotify limit reached') !== -1 && !this.enospcErrorLogged) {
this.enospcErrorLogged = true; this.enospcErrorLogged = true;
this.error('Inotify limit reached (ENOSPC)'); this.error('Inotify limit reached (ENOSPC)');
} }
} }
async setVerboseLogging(enabled: boolean): Promise<void> { async stop(): Promise<void> {
this.verboseLogging = enabled; for (const [path] of this.watchers) {
this.stopWatching(path);
}
this.watchers.clear();
} }
async stop(): Promise<void> { private stopWatching(path: string): void {
for (let path in this.pathWatchers) { const watcher = this.watchers.get(path);
let watcher = this.pathWatchers[path]; if (watcher) {
watcher.ready.then(watcher => watcher.stop()); watcher.instance.then(watcher => watcher.stop());
this.watchers.delete(path);
}
}
delete this.pathWatchers[path]; protected normalizeRequests(requests: IWatchRequest[]): IWatchRequest[] {
const requestTrie = TernarySearchTree.forPaths<IWatchRequest>();
// Sort requests by path length to have shortest first
// to have a way to prevent children to be watched if
// parents exist.
requests.sort((requestA, requestB) => requestA.path.length - requestB.path.length);
// Only consider requests for watching that are not
// a child of an existing request path to prevent
// duplication.
//
// However, allow explicit requests to watch folders
// that are symbolic links because the NSFW watcher
// does not allow to recursively watch symbolic links.
for (const request of requests) {
if (requestTrie.findSubstr(request.path)) {
try {
const realpath = realpathSync(request.path);
if (realpath === request.path) {
continue; // path is not a symbolic link or similar
}
} catch (error) {
continue; // invalid path - ignore from watching
}
}
requestTrie.set(request.path, request);
} }
this.pathWatchers = Object.create(null); return Array.from(requestTrie).map(([, request]) => request);
} }
protected normalizeRoots(roots: IWatcherRequest[]): IWatcherRequest[] { private isPathIgnored(absolutePath: string, ignored: ParsedPattern[] | undefined): boolean {
// Normalizes a set of root paths by removing any root paths that are return Array.isArray(ignored) && ignored.some(ignore => ignore(absolutePath));
// sub-paths of other roots.
return roots.filter(root => roots.every(otherRoot => {
return !(root.path.length > otherRoot.path.length && isEqualOrParent(root.path, otherRoot.path));
}));
} }
private isPathIgnored(absolutePath: string, ignored: ParsedPattern[]): boolean { async setVerboseLogging(enabled: boolean): Promise<void> {
return ignored && ignored.some(ignore => ignore(absolutePath)); this.verboseLogging = enabled;
} }
private log(message: string) { private log(message: string) {
this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (nsfw)] ` + message }); this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (nsfw)] ${message}` });
} }
private warn(message: string) { private warn(message: string) {
this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (nsfw)] ` + message }); this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (nsfw)] ${message}` });
} }
private error(message: string) { private error(message: string) {
this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (nsfw)] ` + message }); this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (nsfw)] ${message}` });
} }
private debug(message: string) { private debug(message: string) {
this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (nsfw)] ` + message }); this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (nsfw)] ${message}` });
} }
} }
...@@ -4,55 +4,50 @@ ...@@ -4,55 +4,50 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as assert from 'assert'; import * as assert from 'assert';
import * as platform from 'vs/base/common/platform'; import { isWindows } from 'vs/base/common/platform';
import { IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher'; import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
import { IWatchRequest } from 'vs/platform/files/node/watcher/watcher';
suite('NSFW Watcher Service', async () => { suite('NSFW Watcher Service', () => {
// Load `nsfwWatcherService` within the suite to prevent all tests
// from failing to start if `nsfw` was not properly installed
const { NsfwWatcherService } = await import('vs/platform/files/node/watcher/nsfw/nsfwWatcherService');
class TestNsfwWatcherService extends NsfwWatcherService { class TestNsfwWatcherService extends NsfwWatcherService {
testNormalizeRoots(roots: string[]): string[] { testNormalizePaths(paths: string[]): string[] {
// Work with strings as paths to simplify testing // Work with strings as paths to simplify testing
const requests: IWatcherRequest[] = roots.map(r => { const requests: IWatchRequest[] = paths.map(path => {
return { path: r, excludes: [] }; return { path, excludes: [] };
}); });
return this.normalizeRoots(requests).map(r => r.path); return this.normalizeRequests(requests).map(request => request.path);
} }
} }
suite('_normalizeRoots', () => { test('should not impacts roots that do not overlap', () => {
test('should not impacts roots that don\'t overlap', () => { const service = new TestNsfwWatcherService();
const service = new TestNsfwWatcherService(); if (isWindows) {
if (platform.isWindows) { assert.deepStrictEqual(service.testNormalizePaths(['C:\\a']), ['C:\\a']);
assert.deepStrictEqual(service.testNormalizeRoots(['C:\\a']), ['C:\\a']); assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']);
assert.deepStrictEqual(service.testNormalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
assert.deepStrictEqual(service.testNormalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); } else {
} else { assert.deepStrictEqual(service.testNormalizePaths(['/a']), ['/a']);
assert.deepStrictEqual(service.testNormalizeRoots(['/a']), ['/a']); assert.deepStrictEqual(service.testNormalizePaths(['/a', '/b']), ['/a', '/b']);
assert.deepStrictEqual(service.testNormalizeRoots(['/a', '/b']), ['/a', '/b']); assert.deepStrictEqual(service.testNormalizePaths(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']);
assert.deepStrictEqual(service.testNormalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']); }
} });
});
test('should remove sub-folders of other roots', () => {
test('should remove sub-folders of other roots', () => { const service = new TestNsfwWatcherService();
const service = new TestNsfwWatcherService(); if (isWindows) {
if (platform.isWindows) { assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\a\\b']), ['C:\\a']);
assert.deepStrictEqual(service.testNormalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']); assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepStrictEqual(service.testNormalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); assert.deepStrictEqual(service.testNormalizePaths(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepStrictEqual(service.testNormalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']); assert.deepStrictEqual(service.testNormalizePaths(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']);
assert.deepStrictEqual(service.testNormalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']); } else {
} else { assert.deepStrictEqual(service.testNormalizePaths(['/a', '/a/b']), ['/a']);
assert.deepStrictEqual(service.testNormalizeRoots(['/a', '/a/b']), ['/a']); assert.deepStrictEqual(service.testNormalizePaths(['/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepStrictEqual(service.testNormalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']); assert.deepStrictEqual(service.testNormalizePaths(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepStrictEqual(service.testNormalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']); assert.deepStrictEqual(service.testNormalizePaths(['/a', '/a/b', '/a/c/d']), ['/a']);
assert.deepStrictEqual(service.testNormalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']); }
}
});
}); });
}); });
...@@ -4,20 +4,36 @@ ...@@ -4,20 +4,36 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import { IDiskFileChange, ILogMessage, IWatchRequest } from 'vs/platform/files/node/watcher/watcher';
export interface IWatcherRequest {
path: string;
excludes: string[];
}
export interface IWatcherService { export interface IWatcherService {
/**
* A normalized file change event from the raw events
* the watcher emits.
*/
readonly onDidChangeFile: Event<IDiskFileChange[]>; readonly onDidChangeFile: Event<IDiskFileChange[]>;
/**
* An event to indicate a message that should get logged.
*/
readonly onDidLogMessage: Event<ILogMessage>; readonly onDidLogMessage: Event<ILogMessage>;
setRoots(roots: IWatcherRequest[]): Promise<void>; /**
* Configures the watcher service to watch according
* to the requests. Any existing watched path that
* is not in the array, will be removed from watching
* and any new path will be added to watching.
*/
watch(requests: IWatchRequest[]): Promise<void>;
/**
* Enable verbose logging in the watcher.
*/
setVerboseLogging(enabled: boolean): Promise<void>; setVerboseLogging(enabled: boolean): Promise<void>;
/**
* Stop all watchers.
*/
stop(): Promise<void>; stop(): Promise<void>;
} }
...@@ -7,28 +7,26 @@ import { Disposable } from 'vs/base/common/lifecycle'; ...@@ -7,28 +7,26 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { FileAccess } from 'vs/base/common/network'; import { FileAccess } from 'vs/base/common/network';
import { getNextTickChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { getNextTickChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IWatcherRequest, IWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcher'; import { IWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcher';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import { IDiskFileChange, ILogMessage, IWatchRequest } from 'vs/platform/files/node/watcher/watcher';
export class FileWatcher extends Disposable { export class FileWatcher extends Disposable {
private static readonly MAX_RESTARTS = 5; private static readonly MAX_RESTARTS = 5;
private service: IWatcherService | undefined; private service: IWatcherService | undefined;
private isDisposed: boolean;
private restartCounter: number; private isDisposed = false;
private restartCounter = 0;
constructor( constructor(
private folders: IWatcherRequest[], private requests: IWatchRequest[],
private readonly onDidFilesChange: (changes: IDiskFileChange[]) => void, private readonly onDidFilesChange: (changes: IDiskFileChange[]) => void,
private readonly onLogMessage: (msg: ILogMessage) => void, private readonly onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean, private verboseLogging: boolean,
) { ) {
super(); super();
this.isDisposed = false;
this.restartCounter = 0;
this.startWatching(); this.startWatching();
} }
...@@ -69,13 +67,14 @@ export class FileWatcher extends Disposable { ...@@ -69,13 +67,14 @@ export class FileWatcher extends Disposable {
this._register(this.service.onDidLogMessage(e => this.onLogMessage(e))); this._register(this.service.onDidLogMessage(e => this.onLogMessage(e)));
// Start watching // Start watching
this.setFolders(this.folders); this.watch(this.requests);
} }
setVerboseLogging(verboseLogging: boolean): void { setVerboseLogging(verboseLogging: boolean): void {
this.verboseLogging = verboseLogging; this.verboseLogging = verboseLogging;
if (!this.isDisposed && this.service) {
this.service.setVerboseLogging(verboseLogging); if (!this.isDisposed) {
this.service?.setVerboseLogging(verboseLogging);
} }
} }
...@@ -83,12 +82,10 @@ export class FileWatcher extends Disposable { ...@@ -83,12 +82,10 @@ export class FileWatcher extends Disposable {
this.onLogMessage({ type: 'error', message: `[File Watcher (nsfw)] ${message}` }); this.onLogMessage({ type: 'error', message: `[File Watcher (nsfw)] ${message}` });
} }
setFolders(folders: IWatcherRequest[]): void { watch(requests: IWatchRequest[]): void {
this.folders = folders; this.requests = requests;
if (this.service) { this.service?.watch(requests);
this.service.setRoots(folders);
}
} }
override dispose(): void { override dispose(): void {
......
...@@ -16,8 +16,8 @@ import { normalizeNFC } from 'vs/base/common/normalization'; ...@@ -16,8 +16,8 @@ import { normalizeNFC } from 'vs/base/common/normalization';
import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { isLinux, isMacintosh } from 'vs/base/common/platform';
import { realcaseSync } from 'vs/base/node/extpath'; import { realcaseSync } from 'vs/base/node/extpath';
import { FileChangeType } from 'vs/platform/files/common/files'; import { FileChangeType } from 'vs/platform/files/common/files';
import { IWatcherOptions, IWatcherRequest, IWatcherService } from 'vs/platform/files/node/watcher/unix/watcher'; import { IWatcherOptions, IWatcherService } from 'vs/platform/files/node/watcher/unix/watcher';
import { IDiskFileChange, ILogMessage, normalizeFileChanges } from 'vs/platform/files/node/watcher/watcher'; import { IDiskFileChange, ILogMessage, IWatchRequest, normalizeFileChanges } from 'vs/platform/files/node/watcher/watcher';
gracefulFs.gracefulify(fs); // enable gracefulFs gracefulFs.gracefulify(fs); // enable gracefulFs
...@@ -28,7 +28,7 @@ interface IWatcher { ...@@ -28,7 +28,7 @@ interface IWatcher {
stop(): Promise<void>; stop(): Promise<void>;
} }
interface ExtendedWatcherRequest extends IWatcherRequest { interface ExtendedWatcherRequest extends IWatchRequest {
parsedPattern?: ParsedPattern; parsedPattern?: ParsedPattern;
} }
...@@ -68,7 +68,7 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic ...@@ -68,7 +68,7 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
this.verboseLogging = enabled; this.verboseLogging = enabled;
} }
async setRoots(requests: IWatcherRequest[]): Promise<void> { async watch(requests: IWatchRequest[]): Promise<void> {
const watchers = new Map<string, IWatcher>(); const watchers = new Map<string, IWatcher>();
const newRequests: string[] = []; const newRequests: string[] = [];
...@@ -93,13 +93,13 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic ...@@ -93,13 +93,13 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
// start all new watchers // start all new watchers
for (const basePath of newRequests) { for (const basePath of newRequests) {
const requests = requestsByBasePath[basePath]; const requests = requestsByBasePath[basePath];
watchers.set(basePath, this.watch(basePath, requests)); watchers.set(basePath, this.doWatch(basePath, requests));
} }
this.watchers = watchers; this.watchers = watchers;
} }
private watch(basePath: string, requests: IWatcherRequest[]): IWatcher { private doWatch(basePath: string, requests: IWatchRequest[]): IWatcher {
const pollingInterval = this.pollingInterval || 5000; const pollingInterval = this.pollingInterval || 5000;
let usePolling = this.usePolling; // boolean or a list of path patterns let usePolling = this.usePolling; // boolean or a list of path patterns
if (Array.isArray(usePolling)) { if (Array.isArray(usePolling)) {
...@@ -344,11 +344,11 @@ function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean { ...@@ -344,11 +344,11 @@ function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean {
* Normalizes a set of root paths by grouping by the most parent root path. * Normalizes a set of root paths by grouping by the most parent root path.
* equests with Sub paths are skipped if they have the same ignored set as the parent. * equests with Sub paths are skipped if they have the same ignored set as the parent.
*/ */
export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string]: IWatcherRequest[] } { export function normalizeRoots(requests: IWatchRequest[]): { [basePath: string]: IWatchRequest[] } {
requests = requests.sort((r1, r2) => r1.path.localeCompare(r2.path)); requests = requests.sort((r1, r2) => r1.path.localeCompare(r2.path));
let prevRequest: IWatcherRequest | null = null; let prevRequest: IWatchRequest | null = null;
const result: { [basePath: string]: IWatcherRequest[] } = Object.create(null); const result: { [basePath: string]: IWatchRequest[] } = Object.create(null);
for (const request of requests) { for (const request of requests) {
const basePath = request.path; const basePath = request.path;
const ignored = (request.excludes || []).sort(); const ignored = (request.excludes || []).sort();
...@@ -365,7 +365,7 @@ export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string ...@@ -365,7 +365,7 @@ export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string
return result; return result;
} }
function isEqualRequests(r1: readonly IWatcherRequest[], r2: readonly IWatcherRequest[]) { function isEqualRequests(r1: readonly IWatchRequest[], r2: readonly IWatchRequest[]) {
return equals(r1, r2, (a, b) => a.path === b.path && isEqualIgnore(a.excludes, b.excludes)); return equals(r1, r2, (a, b) => a.path === b.path && isEqualIgnore(a.excludes, b.excludes));
} }
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as platform from 'vs/base/common/platform'; import * as platform from 'vs/base/common/platform';
import { IWatcherRequest } from 'vs/platform/files/node/watcher/unix/watcher'; import { IWatchRequest } from 'vs/platform/files/node/watcher/watcher';
suite('Chokidar normalizeRoots', async () => { suite('Chokidar normalizeRoots', async () => {
...@@ -13,7 +13,7 @@ suite('Chokidar normalizeRoots', async () => { ...@@ -13,7 +13,7 @@ suite('Chokidar normalizeRoots', async () => {
// from failing to start if `chokidar` was not properly installed // from failing to start if `chokidar` was not properly installed
const { normalizeRoots } = await import('vs/platform/files/node/watcher/unix/chokidarWatcherService'); const { normalizeRoots } = await import('vs/platform/files/node/watcher/unix/chokidarWatcherService');
function newRequest(basePath: string, ignored: string[] = []): IWatcherRequest { function newRequest(basePath: string, ignored: string[] = []): IWatchRequest {
return { path: basePath, excludes: ignored }; return { path: basePath, excludes: ignored };
} }
...@@ -23,7 +23,7 @@ suite('Chokidar normalizeRoots', async () => { ...@@ -23,7 +23,7 @@ suite('Chokidar normalizeRoots', async () => {
assert.deepStrictEqual(Object.keys(actual).sort(), expectedPaths); assert.deepStrictEqual(Object.keys(actual).sort(), expectedPaths);
} }
function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) { function assertNormalizedRequests(inputRequests: IWatchRequest[], expectedRequests: { [path: string]: IWatchRequest[] }) {
const actual = normalizeRoots(inputRequests); const actual = normalizeRoots(inputRequests);
const actualPath = Object.keys(actual).sort(); const actualPath = Object.keys(actual).sort();
const expectedPaths = Object.keys(expectedRequests).sort(); const expectedPaths = Object.keys(expectedRequests).sort();
......
...@@ -4,12 +4,7 @@ ...@@ -4,12 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import { IDiskFileChange, ILogMessage, IWatchRequest } from 'vs/platform/files/node/watcher/watcher';
export interface IWatcherRequest {
path: string;
excludes: string[];
}
export interface IWatcherOptions { export interface IWatcherOptions {
pollingInterval?: number; pollingInterval?: number;
...@@ -24,7 +19,7 @@ export interface IWatcherService { ...@@ -24,7 +19,7 @@ export interface IWatcherService {
init(options: IWatcherOptions): Promise<void>; init(options: IWatcherOptions): Promise<void>;
setRoots(roots: IWatcherRequest[]): Promise<void>; watch(paths: IWatchRequest[]): Promise<void>;
setVerboseLogging(enabled: boolean): Promise<void>; setVerboseLogging(enabled: boolean): Promise<void>;
stop(): Promise<void>; stop(): Promise<void>;
......
...@@ -7,9 +7,12 @@ import { Disposable } from 'vs/base/common/lifecycle'; ...@@ -7,9 +7,12 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { FileAccess } from 'vs/base/common/network'; import { FileAccess } from 'vs/base/common/network';
import { getNextTickChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { getNextTickChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IWatcherOptions, IWatcherRequest, IWatcherService } from 'vs/platform/files/node/watcher/unix/watcher'; import { IWatcherOptions, IWatcherService } from 'vs/platform/files/node/watcher/unix/watcher';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import { IDiskFileChange, ILogMessage, IWatchRequest } from 'vs/platform/files/node/watcher/watcher';
/**
* @deprecated
*/
export class FileWatcher extends Disposable { export class FileWatcher extends Disposable {
private static readonly MAX_RESTARTS = 5; private static readonly MAX_RESTARTS = 5;
...@@ -19,7 +22,7 @@ export class FileWatcher extends Disposable { ...@@ -19,7 +22,7 @@ export class FileWatcher extends Disposable {
private service: IWatcherService | undefined; private service: IWatcherService | undefined;
constructor( constructor(
private folders: IWatcherRequest[], private folders: IWatchRequest[],
private readonly onDidFilesChange: (changes: IDiskFileChange[]) => void, private readonly onDidFilesChange: (changes: IDiskFileChange[]) => void,
private readonly onLogMessage: (msg: ILogMessage) => void, private readonly onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean, private verboseLogging: boolean,
...@@ -69,7 +72,7 @@ export class FileWatcher extends Disposable { ...@@ -69,7 +72,7 @@ export class FileWatcher extends Disposable {
this._register(this.service.onDidLogMessage(e => this.onLogMessage(e))); this._register(this.service.onDidLogMessage(e => this.onLogMessage(e)));
// Start watching // Start watching
this.service.setRoots(this.folders); this.service.watch(this.folders);
} }
error(message: string) { error(message: string) {
...@@ -84,11 +87,11 @@ export class FileWatcher extends Disposable { ...@@ -84,11 +87,11 @@ export class FileWatcher extends Disposable {
} }
} }
setFolders(folders: IWatcherRequest[]): void { watch(folders: IWatchRequest[]): void {
this.folders = folders; this.folders = folders;
if (this.service) { if (this.service) {
this.service.setRoots(folders); this.service.watch(folders);
} }
} }
......
...@@ -7,6 +7,19 @@ import { isLinux } from 'vs/base/common/platform'; ...@@ -7,6 +7,19 @@ import { isLinux } from 'vs/base/common/platform';
import { URI as uri } from 'vs/base/common/uri'; import { URI as uri } from 'vs/base/common/uri';
import { FileChangeType, IFileChange, isParent } from 'vs/platform/files/common/files'; import { FileChangeType, IFileChange, isParent } from 'vs/platform/files/common/files';
export interface IWatchRequest {
/**
* The path to watch.
*/
path: string;
/**
* A set of glob patterns or paths to exclude from watching.
*/
excludes: string[];
}
export interface IDiskFileChange { export interface IDiskFileChange {
type: FileChangeType; type: FileChangeType;
path: string; path: string;
......
...@@ -6,16 +6,19 @@ ...@@ -6,16 +6,19 @@
import { IDisposable } from 'vs/base/common/lifecycle'; import { IDisposable } from 'vs/base/common/lifecycle';
import { posix } from 'vs/base/common/path'; import { posix } from 'vs/base/common/path';
import { rtrim } from 'vs/base/common/strings'; import { rtrim } from 'vs/base/common/strings';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import { IDiskFileChange, ILogMessage, IWatchRequest } from 'vs/platform/files/node/watcher/watcher';
import { OutOfProcessWin32FolderWatcher } from 'vs/platform/files/node/watcher/win32/csharpWatcherService'; import { OutOfProcessWin32FolderWatcher } from 'vs/platform/files/node/watcher/win32/csharpWatcherService';
/**
* @deprecated
*/
export class FileWatcher implements IDisposable { export class FileWatcher implements IDisposable {
private folder: { path: string, excludes: string[] }; private folder: IWatchRequest;
private service: OutOfProcessWin32FolderWatcher | undefined = undefined; private service: OutOfProcessWin32FolderWatcher | undefined = undefined;
constructor( constructor(
folders: { path: string, excludes: string[] }[], folders: IWatchRequest[],
private readonly onDidFilesChange: (changes: IDiskFileChange[]) => void, private readonly onDidFilesChange: (changes: IDiskFileChange[]) => void,
private readonly onLogMessage: (msg: ILogMessage) => void, private readonly onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean private verboseLogging: boolean
......
...@@ -245,7 +245,16 @@ configurationRegistry.registerConfiguration({ ...@@ -245,7 +245,16 @@ configurationRegistry.registerConfiguration({
'files.watcherExclude': { 'files.watcherExclude': {
'type': 'object', 'type': 'object',
'default': { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true }, 'default': { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true },
'markdownDescription': nls.localize('watcherExclude', "Configure glob patterns of file paths to exclude from file watching. Patterns must match on absolute paths, i.e. prefix with `**/` or the full path to match properly and suffix with `/**` to match files within a path (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). Changing this setting requires a restart. When you experience Code consuming lots of CPU time on startup, you can exclude large folders to reduce the initial load."), 'markdownDescription': nls.localize('watcherExclude', "Configure glob patterns of file paths to exclude from file watching. Patterns must match on absolute paths, i.e. prefix with `**/` or the full path to match properly and suffix with `/**` to match files within a path (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). When you experience Code consuming lots of CPU time on startup, you can exclude large folders to reduce the initial load."),
'scope': ConfigurationScope.RESOURCE
},
'files.watcherInclude': {
'type': 'array',
'items': {
'type': 'string'
},
'default': [],
'description': nls.localize('watcherInclude', "Configure extra paths to watch for changes inside the workspace. By default, all workspace folders will be watched recursively, except for folders that are symbolic links. You can explicitly add absolute or relative paths to support watching folders that are symbolic links. Relative paths will be resolved against the workspace folder to form an absolute path."),
'scope': ConfigurationScope.RESOURCE 'scope': ConfigurationScope.RESOURCE
}, },
'files.legacyWatcher': { 'files.legacyWatcher': {
......
...@@ -3,17 +3,20 @@ ...@@ -3,17 +3,20 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; import { IDisposable, Disposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration, IFileService } from 'vs/platform/files/common/files'; import { IFilesConfiguration, IFileService } from 'vs/platform/files/common/files';
import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { ResourceMap } from 'vs/base/common/map'; import { ResourceMap } from 'vs/base/common/map';
import { onUnexpectedError } from 'vs/base/common/errors'; import { onUnexpectedError } from 'vs/base/common/errors';
import { INotificationService, Severity, NeverShowAgainScope } from 'vs/platform/notification/common/notification'; import { INotificationService, Severity, NeverShowAgainScope } from 'vs/platform/notification/common/notification';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { FileService } from 'vs/platform/files/common/fileService'; import { FileService } from 'vs/platform/files/common/fileService';
import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IOpenerService } from 'vs/platform/opener/common/opener';
import { isAbsolute } from 'vs/base/common/path';
import { isEqualOrParent } from 'vs/base/common/resources';
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
export class WorkspaceWatcher extends Disposable { export class WorkspaceWatcher extends Disposable {
...@@ -24,7 +27,8 @@ export class WorkspaceWatcher extends Disposable { ...@@ -24,7 +27,8 @@ export class WorkspaceWatcher extends Disposable {
@IConfigurationService private readonly configurationService: IConfigurationService, @IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@INotificationService private readonly notificationService: INotificationService, @INotificationService private readonly notificationService: INotificationService,
@IOpenerService private readonly openerService: IOpenerService @IOpenerService private readonly openerService: IOpenerService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService
) { ) {
super(); super();
...@@ -44,12 +48,12 @@ export class WorkspaceWatcher extends Disposable { ...@@ -44,12 +48,12 @@ export class WorkspaceWatcher extends Disposable {
// Removed workspace: Unwatch // Removed workspace: Unwatch
for (const removed of e.removed) { for (const removed of e.removed) {
this.unwatchWorkspace(removed.uri); this.unwatchWorkspace(removed);
} }
// Added workspace: Watch // Added workspace: Watch
for (const added of e.added) { for (const added of e.added) {
this.watchWorkspace(added.uri); this.watchWorkspace(added);
} }
} }
...@@ -58,7 +62,7 @@ export class WorkspaceWatcher extends Disposable { ...@@ -58,7 +62,7 @@ export class WorkspaceWatcher extends Disposable {
} }
private onDidChangeConfiguration(e: IConfigurationChangeEvent): void { private onDidChangeConfiguration(e: IConfigurationChangeEvent): void {
if (e.affectsConfiguration('files.watcherExclude')) { if (e.affectsConfiguration('files.watcherExclude') || e.affectsConfiguration('files.watcherInclude')) {
this.refresh(); this.refresh();
} }
} }
...@@ -102,11 +106,11 @@ export class WorkspaceWatcher extends Disposable { ...@@ -102,11 +106,11 @@ export class WorkspaceWatcher extends Disposable {
} }
} }
private watchWorkspace(resource: URI) { private watchWorkspace(workspace: IWorkspaceFolder): void {
// Compute the watcher exclude rules from configuration // Compute the watcher exclude rules from configuration
const excludes: string[] = []; const excludes: string[] = [];
const config = this.configurationService.getValue<IFilesConfiguration>({ resource }); const config = this.configurationService.getValue<IFilesConfiguration>({ resource: workspace.uri });
if (config.files?.watcherExclude) { if (config.files?.watcherExclude) {
for (const key in config.files.watcherExclude) { for (const key in config.files.watcherExclude) {
if (config.files.watcherExclude[key] === true) { if (config.files.watcherExclude[key] === true) {
...@@ -115,15 +119,46 @@ export class WorkspaceWatcher extends Disposable { ...@@ -115,15 +119,46 @@ export class WorkspaceWatcher extends Disposable {
} }
} }
// Watch workspace const pathsToWatch = new ResourceMap<URI>(uri => this.uriIdentityService.extUri.getComparisonKey(uri));
const disposable = this.fileService.watch(resource, { recursive: true, excludes });
this.watches.set(resource, disposable); // Add the workspace as path to watch
pathsToWatch.set(workspace.uri, workspace.uri);
// Compute additional includes from configuration
if (config.files?.watcherInclude) {
for (const includePath of config.files.watcherInclude) {
if (!includePath) {
continue;
}
// Absolute: verify a child of the workspace
if (isAbsolute(includePath)) {
const candidate = URI.file(includePath).with({ scheme: workspace.uri.scheme });
if (isEqualOrParent(candidate, workspace.uri)) {
pathsToWatch.set(candidate, candidate);
}
}
// Relative: join against workspace folder
else {
const candidate = workspace.toResource(includePath);
pathsToWatch.set(candidate, candidate);
}
}
}
// Watch all paths as instructed
const disposables = new DisposableStore();
for (const [, pathToWatch] of pathsToWatch) {
disposables.add(this.fileService.watch(pathToWatch, { recursive: true, excludes }));
}
this.watches.set(workspace.uri, disposables);
} }
private unwatchWorkspace(resource: URI) { private unwatchWorkspace(workspace: IWorkspaceFolder): void {
if (this.watches.has(resource)) { if (this.watches.has(workspace.uri)) {
dispose(this.watches.get(resource)); dispose(this.watches.get(workspace.uri));
this.watches.delete(resource); this.watches.delete(workspace.uri);
} }
} }
...@@ -134,11 +169,11 @@ export class WorkspaceWatcher extends Disposable { ...@@ -134,11 +169,11 @@ export class WorkspaceWatcher extends Disposable {
// Watch each workspace folder // Watch each workspace folder
for (const folder of this.contextService.getWorkspace().folders) { for (const folder of this.contextService.getWorkspace().folders) {
this.watchWorkspace(folder.uri); this.watchWorkspace(folder);
} }
} }
private unwatchWorkspaces() { private unwatchWorkspaces(): void {
this.watches.forEach(disposable => dispose(disposable)); this.watches.forEach(disposable => dispose(disposable));
this.watches.clear(); this.watches.clear();
} }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册