Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 (#6381)

* Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973

* disable strict null check
This commit is contained in:
Anthony Dresser
2019-07-15 22:35:46 -07:00
committed by GitHub
parent f720ec642f
commit 0b7e7ddbf9
2406 changed files with 59140 additions and 35464 deletions

View File

@@ -0,0 +1,556 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { mkdir, open, close, read, write, fdatasync, Dirent, Stats } from 'fs';
import { promisify } from 'util';
import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { statLink, unlink, move, copy, readFile, truncate, rimraf, RimRafMode, exists, readdirWithFileTypes } from 'vs/base/node/pfs';
import { normalize, basename, dirname } from 'vs/base/common/path';
import { joinPath } from 'vs/base/common/resources';
import { isEqual } from 'vs/base/common/extpath';
import { retry, ThrottledDelayer } from 'vs/base/common/async';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { localize } from 'vs/nls';
import { IDiskFileChange, toFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { FileWatcher as UnixWatcherService } from 'vs/platform/files/node/watcher/unix/watcherService';
import { FileWatcher as WindowsWatcherService } from 'vs/platform/files/node/watcher/win32/watcherService';
import { FileWatcher as NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcherService';
import { FileWatcher as NodeJSWatcherService } from 'vs/platform/files/node/watcher/nodejs/watcherService';
export interface IWatcherOptions {
pollingInterval?: number;
usePolling: boolean;
}
export class DiskFileSystemProvider extends Disposable implements IFileSystemProvider {
constructor(private logService: ILogService, private watcherOptions?: IWatcherOptions) {
super();
}
//#region File Capabilities
onDidChangeCapabilities: Event<void> = Event.None;
protected _capabilities: FileSystemProviderCapabilities;
get capabilities(): FileSystemProviderCapabilities {
if (!this._capabilities) {
this._capabilities =
FileSystemProviderCapabilities.FileReadWrite |
FileSystemProviderCapabilities.FileOpenReadWriteClose |
FileSystemProviderCapabilities.FileFolderCopy;
if (isLinux) {
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
}
}
return this._capabilities;
}
//#endregion
//#region File Metadata Resolving
async stat(resource: URI): Promise<IStat> {
try {
const { stat, isSymbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly
return {
type: this.toType(stat, isSymbolicLink),
ctime: stat.ctime.getTime(),
mtime: stat.mtime.getTime(),
size: stat.size
};
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async readdir(resource: URI): Promise<[string, FileType][]> {
try {
const children = await readdirWithFileTypes(this.toFilePath(resource));
const result: [string, FileType][] = [];
await Promise.all(children.map(async child => {
try {
let type: FileType;
if (child.isSymbolicLink()) {
type = (await this.stat(joinPath(resource, child.name))).type; // always resolve target the link points to if any
} else {
type = this.toType(child);
}
result.push([child.name, type]);
} catch (error) {
this.logService.trace(error); // ignore errors for individual entries that can arise from permission denied
}
}));
return result;
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
private toType(entry: Stats | Dirent, isSymbolicLink = entry.isSymbolicLink()): FileType {
if (isSymbolicLink) {
return FileType.SymbolicLink | (entry.isDirectory() ? FileType.Directory : FileType.File);
}
return entry.isFile() ? FileType.File : entry.isDirectory() ? FileType.Directory : FileType.Unknown;
}
//#endregion
//#region File Reading/Writing
async readFile(resource: URI): Promise<Uint8Array> {
try {
const filePath = this.toFilePath(resource);
return await readFile(filePath);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
let handle: number | undefined = undefined;
try {
const filePath = this.toFilePath(resource);
// Validate target
const fileExists = await exists(filePath);
if (fileExists && !opts.overwrite) {
throw createFileSystemProviderError(new Error(localize('fileExists', "File already exists")), FileSystemProviderErrorCode.FileExists);
} else if (!fileExists && !opts.create) {
throw createFileSystemProviderError(new Error(localize('fileNotExists', "File does not exist")), FileSystemProviderErrorCode.FileNotFound);
}
// Open
handle = await this.open(resource, { create: true });
// Write content at once
await this.write(handle, 0, content, 0, content.byteLength);
} catch (error) {
throw this.toFileSystemProviderError(error);
} finally {
if (typeof handle === 'number') {
await this.close(handle);
}
}
}
private writeHandles: Set<number> = new Set();
private canFlush: boolean = true;
async open(resource: URI, opts: FileOpenOptions): Promise<number> {
try {
const filePath = this.toFilePath(resource);
let flags: string | undefined = undefined;
if (opts.create) {
if (isWindows && await exists(filePath)) {
try {
// On Windows and if the file exists, we use a different strategy of saving the file
// by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows
// (see https://github.com/Microsoft/vscode/issues/931) and prevent removing alternate data streams
// (see https://github.com/Microsoft/vscode/issues/6363)
await truncate(filePath, 0);
// After a successful truncate() the flag can be set to 'r+' which will not truncate.
flags = 'r+';
} catch (error) {
this.logService.trace(error);
}
}
// we take opts.create as a hint that the file is opened for writing
// as such we use 'w' to truncate an existing or create the
// file otherwise. we do not allow reading.
if (!flags) {
flags = 'w';
}
} else {
// otherwise we assume the file is opened for reading
// as such we use 'r' to neither truncate, nor create
// the file.
flags = 'r';
}
const handle = await promisify(open)(filePath, flags);
// remember that this handle was used for writing
if (opts.create) {
this.writeHandles.add(handle);
}
return handle;
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async close(fd: number): Promise<void> {
try {
// if a handle is closed that was used for writing, ensure
// to flush the contents to disk if possible.
if (this.writeHandles.delete(fd) && this.canFlush) {
try {
await promisify(fdatasync)(fd);
} catch (error) {
// In some exotic setups it is well possible that node fails to sync
// In that case we disable flushing and log the error to our logger
this.canFlush = false;
this.logService.error(error);
}
}
return await promisify(close)(fd);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
try {
const result = await promisify(read)(fd, data, offset, length, pos);
if (typeof result === 'number') {
return result; // node.d.ts fail
}
return result.bytesRead;
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
// we know at this point that the file to write to is truncated and thus empty
// if the write now fails, the file remains empty. as such we really try hard
// to ensure the write succeeds by retrying up to three times.
return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */);
}
private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
try {
const result = await promisify(write)(fd, data, offset, length, pos);
if (typeof result === 'number') {
return result; // node.d.ts fail
}
return result.bytesWritten;
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
//#endregion
//#region Move/Copy/Delete/Create Folder
async mkdir(resource: URI): Promise<void> {
try {
await promisify(mkdir)(this.toFilePath(resource));
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
try {
const filePath = this.toFilePath(resource);
await this.doDelete(filePath, opts);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
if (opts.recursive) {
await rimraf(filePath, RimRafMode.MOVE);
} else {
await unlink(filePath);
}
}
async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
const fromFilePath = this.toFilePath(from);
const toFilePath = this.toFilePath(to);
if (fromFilePath === toFilePath) {
return; // simulate node.js behaviour here and do a no-op if paths match
}
try {
// Ensure target does not exist
await this.validateTargetDeleted(from, to, 'move', opts && opts.overwrite);
// Move
await move(fromFilePath, toFilePath);
} catch (error) {
// rewrite some typical errors that can happen especially around symlinks
// to something the user can better understand
if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {
error = new Error(localize('moveError', "Unable to move '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));
}
throw this.toFileSystemProviderError(error);
}
}
async copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
const fromFilePath = this.toFilePath(from);
const toFilePath = this.toFilePath(to);
if (fromFilePath === toFilePath) {
return; // simulate node.js behaviour here and do a no-op if paths match
}
try {
// Ensure target does not exist
await this.validateTargetDeleted(from, to, 'copy', opts && opts.overwrite);
// Copy
await copy(fromFilePath, toFilePath);
} catch (error) {
// rewrite some typical errors that can happen especially around symlinks
// to something the user can better understand
if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {
error = new Error(localize('copyError', "Unable to copy '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));
}
throw this.toFileSystemProviderError(error);
}
}
private async validateTargetDeleted(from: URI, to: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<void> {
const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
const fromFilePath = this.toFilePath(from);
const toFilePath = this.toFilePath(to);
let isSameResourceWithDifferentPathCase = false;
if (!isPathCaseSensitive) {
isSameResourceWithDifferentPathCase = isEqual(fromFilePath, toFilePath, true /* ignore case */);
}
if (isSameResourceWithDifferentPathCase && mode === 'copy') {
throw createFileSystemProviderError(new Error('File cannot be copied to same path with different path case'), FileSystemProviderErrorCode.FileExists);
}
// handle existing target (unless this is a case change)
if (!isSameResourceWithDifferentPathCase && await exists(toFilePath)) {
if (!overwrite) {
throw createFileSystemProviderError(new Error('File at target already exists'), FileSystemProviderErrorCode.FileExists);
}
// Delete target
await this.delete(to, { recursive: true, useTrash: false });
}
}
//#endregion
//#region File Watching
private _onDidWatchErrorOccur: Emitter<string> = this._register(new Emitter<string>());
readonly onDidErrorOccur: Event<string> = this._onDidWatchErrorOccur.event;
private _onDidChangeFile: Emitter<IFileChange[]> = this._register(new Emitter<IFileChange[]>());
get onDidChangeFile(): Event<IFileChange[]> { return this._onDidChangeFile.event; }
private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | undefined;
private recursiveFoldersToWatch: { path: string, excludes: string[] }[] = [];
private recursiveWatchRequestDelayer: ThrottledDelayer<void> = this._register(new ThrottledDelayer<void>(0));
private recursiveWatcherLogLevelListener: IDisposable | undefined;
watch(resource: URI, opts: IWatchOptions): IDisposable {
if (opts.recursive) {
return this.watchRecursive(resource, opts.excludes);
}
return this.watchNonRecursive(resource); // TODO@ben ideally the same watcher can be used in both cases
}
private watchRecursive(resource: URI, excludes: string[]): IDisposable {
// Add to list of folders to watch recursively
const folderToWatch = { path: this.toFilePath(resource), excludes };
this.recursiveFoldersToWatch.push(folderToWatch);
// Trigger update
this.refreshRecursiveWatchers();
return toDisposable(() => {
// Remove from list of folders to watch recursively
this.recursiveFoldersToWatch.splice(this.recursiveFoldersToWatch.indexOf(folderToWatch), 1);
// Trigger update
this.refreshRecursiveWatchers();
});
}
private refreshRecursiveWatchers(): void {
// Buffer requests for recursive watching to decide on right watcher
// that supports potentially watching more than one folder at once
this.recursiveWatchRequestDelayer.trigger(() => {
this.doRefreshRecursiveWatchers();
return Promise.resolve();
});
}
private doRefreshRecursiveWatchers(): void {
// Reuse existing
if (this.recursiveWatcher instanceof NsfwWatcherService) {
this.recursiveWatcher.setFolders(this.recursiveFoldersToWatch);
}
// Create new
else {
// Dispose old
dispose(this.recursiveWatcher);
this.recursiveWatcher = undefined;
// Create new if we actually have folders to watch
if (this.recursiveFoldersToWatch.length > 0) {
let watcherImpl: {
new(
folders: { path: string, excludes: string[] }[],
onChange: (changes: IDiskFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean,
watcherOptions?: IWatcherOptions
): WindowsWatcherService | UnixWatcherService | NsfwWatcherService
};
let watcherOptions = undefined;
if (this.watcherOptions && this.watcherOptions.usePolling) {
// requires a polling watcher
watcherImpl = UnixWatcherService;
watcherOptions = this.watcherOptions;
} else {
// Single Folder Watcher
if (this.recursiveFoldersToWatch.length === 1) {
if (isWindows) {
watcherImpl = WindowsWatcherService;
} else {
watcherImpl = UnixWatcherService;
}
}
// Multi Folder Watcher
else {
watcherImpl = NsfwWatcherService;
}
}
// Create and start watching
this.recursiveWatcher = new watcherImpl(
this.recursiveFoldersToWatch,
event => this._onDidChangeFile.fire(toFileChanges(event)),
msg => {
if (msg.type === 'error') {
this._onDidWatchErrorOccur.fire(msg.message);
}
this.logService[msg.type](msg.message);
},
this.logService.getLevel() === LogLevel.Trace,
watcherOptions
);
if (!this.recursiveWatcherLogLevelListener) {
this.recursiveWatcherLogLevelListener = this.logService.onDidChangeLogLevel(_ => {
if (this.recursiveWatcher) {
this.recursiveWatcher.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);
}
});
}
}
}
}
private watchNonRecursive(resource: URI): IDisposable {
const watcherService = new NodeJSWatcherService(
this.toFilePath(resource),
changes => this._onDidChangeFile.fire(toFileChanges(changes)),
msg => {
if (msg.type === 'error') {
this._onDidWatchErrorOccur.fire(msg.message);
}
this.logService[msg.type](msg.message);
},
this.logService.getLevel() === LogLevel.Trace
);
const logLevelListener = this.logService.onDidChangeLogLevel(_ => {
watcherService.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);
});
return combinedDisposable(watcherService, logLevelListener);
}
//#endregion
//#region Helpers
protected toFilePath(resource: URI): string {
return normalize(resource.fsPath);
}
private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError {
if (error instanceof FileSystemProviderError) {
return error; // avoid double conversion
}
let code: FileSystemProviderErrorCode;
switch (error.code) {
case 'ENOENT':
code = FileSystemProviderErrorCode.FileNotFound;
break;
case 'EISDIR':
code = FileSystemProviderErrorCode.FileIsADirectory;
break;
case 'EEXIST':
code = FileSystemProviderErrorCode.FileExists;
break;
case 'EPERM':
case 'EACCES':
code = FileSystemProviderErrorCode.NoPermissions;
break;
default:
code = FileSystemProviderErrorCode.Unknown;
}
return createFileSystemProviderError(error, code);
}
//#endregion
dispose(): void {
super.dispose();
dispose(this.recursiveWatcher);
this.recursiveWatcher = undefined;
dispose(this.recursiveWatcherLogLevelListener);
this.recursiveWatcherLogLevelListener = undefined;
}
}

View File

@@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
import { statLink } from 'vs/base/node/pfs';
import { realpath } from 'vs/base/node/extpath';
import { watchFolder, watchFile, CHANGE_BUFFER_DELAY } from 'vs/base/node/watcher';
import { FileChangeType } from 'vs/platform/files/common/files';
import { ThrottledDelayer } from 'vs/base/common/async';
import { join, basename } from 'vs/base/common/path';
export class FileWatcher extends Disposable {
private isDisposed: boolean;
private fileChangesDelayer: ThrottledDelayer<void> = this._register(new ThrottledDelayer<void>(CHANGE_BUFFER_DELAY * 2 /* sync on delay from underlying library */));
private fileChangesBuffer: IDiskFileChange[] = [];
constructor(
private path: string,
private onFileChanges: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean
) {
super();
this.startWatching();
}
setVerboseLogging(verboseLogging: boolean): void {
this.verboseLogging = verboseLogging;
}
private async startWatching(): Promise<void> {
try {
const { stat, isSymbolicLink } = await statLink(this.path);
if (this.isDisposed) {
return;
}
let pathToWatch = this.path;
if (isSymbolicLink) {
try {
pathToWatch = await realpath(pathToWatch);
} catch (error) {
this.onError(error);
}
}
// Watch Folder
if (stat.isDirectory()) {
this._register(watchFolder(pathToWatch, (eventType, path) => {
this.onFileChange({
type: eventType === 'changed' ? FileChangeType.UPDATED : eventType === 'added' ? FileChangeType.ADDED : FileChangeType.DELETED,
path: join(this.path, basename(path)) // ensure path is identical with what was passed in
});
}, error => this.onError(error)));
}
// Watch File
else {
this._register(watchFile(pathToWatch, eventType => {
this.onFileChange({
type: eventType === 'changed' ? FileChangeType.UPDATED : FileChangeType.DELETED,
path: this.path // ensure path is identical with what was passed in
});
}, error => this.onError(error)));
}
} catch (error) {
this.onError(error);
}
}
private onFileChange(event: IDiskFileChange): void {
// Add to buffer
this.fileChangesBuffer.push(event);
// Logging
if (this.verboseLogging) {
this.onVerbose(`${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
}
// Handle emit through delayer to accommodate for bulk changes and thus reduce spam
this.fileChangesDelayer.trigger(() => {
const fileChanges = this.fileChangesBuffer;
this.fileChangesBuffer = [];
// Event normalization
const normalizedFileChanges = normalizeFileChanges(fileChanges);
// Logging
if (this.verboseLogging) {
normalizedFileChanges.forEach(event => {
this.onVerbose(`>> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
});
}
// Fire
if (normalizedFileChanges.length > 0) {
this.onFileChanges(normalizedFileChanges);
}
return Promise.resolve();
});
}
private onError(error: string): void {
if (!this.isDisposed) {
this.onLogMessage({ type: 'error', message: `[File Watcher (node.js)] ${error}` });
}
}
private onVerbose(message: string): void {
if (!this.isDisposed) {
this.onLogMessage({ type: 'trace', message: `[File Watcher (node.js)] ${message}` });
}
}
dispose(): void {
this.isDisposed = true;
super.dispose();
}
}

View File

@@ -0,0 +1,257 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as glob from 'vs/base/common/glob';
import * as extpath from 'vs/base/common/extpath';
import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import * as nsfw from 'nsfw';
import { IWatcherService, IWatcherRequest, IWatcherOptions } from 'vs/platform/files/node/watcher/nsfw/watcher';
import { ThrottledDelayer } from 'vs/base/common/async';
import { FileChangeType } from 'vs/platform/files/common/files';
import { normalizeNFC } from 'vs/base/common/normalization';
import { Event, Emitter } from 'vs/base/common/event';
import { realcaseSync, realpathSync } from 'vs/base/node/extpath';
const nsfwActionToRawChangeType: { [key: number]: number } = [];
nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED;
nsfwActionToRawChangeType[nsfw.actions.MODIFIED] = FileChangeType.UPDATED;
nsfwActionToRawChangeType[nsfw.actions.DELETED] = FileChangeType.DELETED;
interface IWatcherObjet {
start(): any;
stop(): any;
}
interface IPathWatcher {
ready: Promise<IWatcherObjet>;
watcher?: IWatcherObjet;
ignored: glob.ParsedPattern[];
}
export class NsfwWatcherService implements IWatcherService {
private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
private _pathWatchers: { [watchPath: string]: IPathWatcher } = {};
private _verboseLogging: boolean;
private enospcErrorLogged: boolean;
private _onWatchEvent = new Emitter<IDiskFileChange[]>();
readonly onWatchEvent = this._onWatchEvent.event;
private _onLogMessage = new Emitter<ILogMessage>();
readonly onLogMessage: Event<ILogMessage> = this._onLogMessage.event;
watch(options: IWatcherOptions): Event<IDiskFileChange[]> {
return this.onWatchEvent;
}
private _watch(request: IWatcherRequest): void {
let undeliveredFileEvents: IDiskFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer<void>(NsfwWatcherService.FS_EVENT_DELAY);
let readyPromiseResolve: (watcher: IWatcherObjet) => void;
this._pathWatchers[request.path] = {
ready: new Promise<IWatcherObjet>(resolve => readyPromiseResolve = resolve),
ignored: Array.isArray(request.excludes) ? request.excludes.map(ignored => glob.parse(ignored)) : []
};
process.on('uncaughtException', (e: Error | string) => {
// 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 (e === 'Inotify limit reached' && !this.enospcErrorLogged) {
this.enospcErrorLogged = true;
this.error('Inotify limit reached (ENOSPC)');
}
});
// NSFW does not report file changes in the path provided on macOS if
// - the path uses wrong casing
// - the path is a symbolic link
// We have to detect this case and massage the events to correct this.
let realBasePathDiffers = false;
let realBasePathLength = request.path.length;
if (platform.isMacintosh) {
try {
// First check for symbolic link
let realBasePath = realpathSync(request.path);
// Second check for casing difference
if (request.path === realBasePath) {
realBasePath = (realcaseSync(request.path) || request.path);
}
if (request.path !== realBasePath) {
realBasePathLength = realBasePath.length;
realBasePathDiffers = true;
this.warn(`Watcher basePath does not match version on disk and will be corrected (original: ${request.path}, real: ${realBasePath})`);
}
} catch (error) {
// ignore
}
}
nsfw(request.path, events => {
for (const e of events) {
// Logging
if (this._verboseLogging) {
const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : path.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) {
// Rename fires when a file's name changes within a single directory
absolutePath = path.join(e.directory, e.oldFile || '');
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath });
} else if (this._verboseLogging) {
this.log(` >> ignored ${absolutePath}`);
}
absolutePath = path.join(e.newDirectory || e.directory, e.newFile || '');
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath });
} else if (this._verboseLogging) {
this.log(` >> ignored ${absolutePath}`);
}
} else {
absolutePath = path.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
fileEventDelayer.trigger(() => {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
if (platform.isMacintosh) {
events.forEach(e => {
// 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 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);
});
}).then(watcher => {
this._pathWatchers[request.path].watcher = watcher;
const startPromise = watcher.start();
startPromise.then(() => readyPromiseResolve(watcher));
return startPromise;
});
}
public setRoots(roots: IWatcherRequest[]): Promise<void> {
const promises: Promise<void>[] = [];
const normalizedRoots = this._normalizeRoots(roots);
// Gather roots that are not currently being watched
const rootsToStartWatching = normalizedRoots.filter(r => {
return !(r.path in this._pathWatchers);
});
// Gather current roots that don't exist in the new roots array
const rootsToStopWatching = Object.keys(this._pathWatchers).filter(r => {
return normalizedRoots.every(normalizedRoot => normalizedRoot.path !== r);
});
// Logging
if (this._verboseLogging) {
this.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
}
// Stop watching some roots
rootsToStopWatching.forEach(root => {
this._pathWatchers[root].ready.then(watcher => watcher.stop());
delete this._pathWatchers[root];
});
// Start watching some roots
rootsToStartWatching.forEach(root => this._watch(root));
// Refresh ignored arrays in case they changed
roots.forEach(root => {
if (root.path in this._pathWatchers) {
this._pathWatchers[root.path].ignored = Array.isArray(root.excludes) ? root.excludes.map(ignored => glob.parse(ignored)) : [];
}
});
return Promise.all(promises).then(() => undefined);
}
public setVerboseLogging(enabled: boolean): Promise<void> {
this._verboseLogging = enabled;
return Promise.resolve(undefined);
}
public stop(): Promise<void> {
for (let path in this._pathWatchers) {
let watcher = this._pathWatchers[path];
watcher.ready.then(watcher => watcher.stop());
delete this._pathWatchers[path];
}
this._pathWatchers = Object.create(null);
return Promise.resolve();
}
/**
* Normalizes a set of root paths by removing any root paths that are
* sub-paths of other roots.
*/
protected _normalizeRoots(roots: IWatcherRequest[]): IWatcherRequest[] {
return roots.filter(r => roots.every(other => {
return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path));
}));
}
private _isPathIgnored(absolutePath: string, ignored: glob.ParsedPattern[]): boolean {
return ignored && ignored.some(i => i(absolutePath));
}
private log(message: string) {
this._onLogMessage.fire({ type: 'trace', message: `[File Watcher (nswf)] ` + message });
}
private warn(message: string) {
this._onLogMessage.fire({ type: 'warn', message: `[File Watcher (nswf)] ` + message });
}
private error(message: string) {
this._onLogMessage.fire({ type: 'error', message: `[File Watcher (nswf)] ` + message });
}
}

View File

@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as platform from 'vs/base/common/platform';
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
import { IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
class TestNsfwWatcherService extends NsfwWatcherService {
public normalizeRoots(roots: string[]): string[] {
// Work with strings as paths to simplify testing
const requests: IWatcherRequest[] = roots.map(r => {
return { path: r, excludes: [] };
});
return this._normalizeRoots(requests).map(r => r.path);
}
}
suite('NSFW Watcher Service', () => {
suite('_normalizeRoots', () => {
test('should not impacts roots that don\'t overlap', () => {
const service = new TestNsfwWatcherService();
if (platform.isWindows) {
assert.deepEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
} else {
assert.deepEqual(service.normalizeRoots(['/a']), ['/a']);
assert.deepEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']);
assert.deepEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']);
}
});
test('should remove sub-folders of other roots', () => {
const service = new TestNsfwWatcherService();
if (platform.isWindows) {
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']);
} else {
assert.deepEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']);
assert.deepEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']);
}
});
});
});

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
export interface IWatcherRequest {
path: string;
excludes: string[];
}
export interface IWatcherOptions {
}
export interface IWatcherService {
watch(options: IWatcherOptions): Event<IDiskFileChange[]>;
setRoots(roots: IWatcherRequest[]): Promise<void>;
setVerboseLogging(enabled: boolean): Promise<void>;
onLogMessage: Event<ILogMessage>;
stop(): Promise<void>;
}

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { WatcherChannel } from 'vs/platform/files/node/watcher/nsfw/watcherIpc';
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
const server = new Server('watcher');
const service = new NsfwWatcherService();
const channel = new WatcherChannel(service);
server.registerChannel('watcher', channel);

View File

@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IWatcherRequest, IWatcherService, IWatcherOptions } from './watcher';
import { Event } from 'vs/base/common/event';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
export class WatcherChannel implements IServerChannel {
constructor(private service: IWatcherService) { }
listen(_: unknown, event: string, arg?: any): Event<any> {
switch (event) {
case 'watch': return this.service.watch(arg);
case 'onLogMessage': return this.service.onLogMessage;
}
throw new Error(`Event not found: ${event}`);
}
call(_: unknown, command: string, arg?: any): Promise<any> {
switch (command) {
case 'setRoots': return this.service.setRoots(arg);
case 'setVerboseLogging': return this.service.setVerboseLogging(arg);
case 'stop': return this.service.stop();
}
throw new Error(`Call not found: ${command}`);
}
}
export class WatcherChannelClient implements IWatcherService {
constructor(private channel: IChannel) { }
watch(options: IWatcherOptions): Event<IDiskFileChange[]> {
return this.channel.listen('watch', options);
}
setVerboseLogging(enable: boolean): Promise<void> {
return this.channel.call('setVerboseLogging', enable);
}
setRoots(roots: IWatcherRequest[]): Promise<void> {
return this.channel.call('setRoots', roots);
}
get onLogMessage(): Event<ILogMessage> {
return this.channel.listen('onLogMessage');
}
stop(): Promise<void> {
return this.channel.call('stop');
}
}

View File

@@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { WatcherChannelClient } from 'vs/platform/files/node/watcher/nsfw/watcherIpc';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
import { getPathFromAmdModule } from 'vs/base/common/amd';
export class FileWatcher extends Disposable {
private static readonly MAX_RESTARTS = 5;
private service: WatcherChannelClient;
private isDisposed: boolean;
private restartCounter: number;
constructor(
private folders: IWatcherRequest[],
private onFileChanges: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean,
) {
super();
this.isDisposed = false;
this.restartCounter = 0;
this.startWatching();
}
private startWatching(): void {
const client = this._register(new Client(
getPathFromAmdModule(require, 'bootstrap-fork'),
{
serverName: 'File Watcher (nsfw)',
args: ['--type=watcherService'],
env: {
AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: 'true' // transmit console logs from server to client
}
}
));
this._register(client.onDidProcessExit(() => {
// our watcher app should never be completed because it keeps on watching. being in here indicates
// that the watcher process died and we want to restart it here. we only do it a max number of times
if (!this.isDisposed) {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.error('terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.error('failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}));
// Initialize watcher
const channel = getNextTickChannel(client.getChannel('watcher'));
this.service = new WatcherChannelClient(channel);
this.service.setVerboseLogging(this.verboseLogging);
const options = {};
this._register(this.service.watch(options)(e => !this.isDisposed && this.onFileChanges(e)));
this._register(this.service.onLogMessage(m => this.onLogMessage(m)));
// Start watching
this.setFolders(this.folders);
}
setVerboseLogging(verboseLogging: boolean): void {
this.verboseLogging = verboseLogging;
if (!this.isDisposed) {
this.service.setVerboseLogging(verboseLogging);
}
}
error(message: string) {
this.onLogMessage({ type: 'error', message: `[File Watcher (nsfw)] ${message}` });
}
setFolders(folders: IWatcherRequest[]): void {
this.folders = folders;
this.service.setRoots(folders);
}
dispose(): void {
this.isDisposed = true;
super.dispose();
}
}

View File

@@ -0,0 +1,376 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as chokidar from 'vscode-chokidar';
import * as fs from 'fs';
import * as gracefulFs from 'graceful-fs';
gracefulFs.gracefulify(fs);
import * as extpath from 'vs/base/common/extpath';
import * as glob from 'vs/base/common/glob';
import { FileChangeType } from 'vs/platform/files/common/files';
import { ThrottledDelayer } from 'vs/base/common/async';
import { normalizeNFC } from 'vs/base/common/normalization';
import { realcaseSync } from 'vs/base/node/extpath';
import { isMacintosh, isLinux } from 'vs/base/common/platform';
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { IWatcherRequest, IWatcherService, IWatcherOptions } from 'vs/platform/files/node/watcher/unix/watcher';
import { Emitter, Event } from 'vs/base/common/event';
interface IWatcher {
requests: ExtendedWatcherRequest[];
stop(): any;
}
interface ExtendedWatcherRequest extends IWatcherRequest {
parsedPattern?: glob.ParsedPattern;
}
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 _watchers: { [watchPath: string]: IWatcher };
private _watcherCount: number;
private _pollingInterval?: number;
private _usePolling?: boolean;
private _verboseLogging: boolean;
private spamCheckStartTime: number;
private spamWarningLogged: boolean;
private enospcErrorLogged: boolean;
private _onWatchEvent = new Emitter<IDiskFileChange[]>();
readonly onWatchEvent = this._onWatchEvent.event;
private _onLogMessage = new Emitter<ILogMessage>();
readonly onLogMessage: Event<ILogMessage> = this._onLogMessage.event;
public watch(options: IWatcherOptions): Event<IDiskFileChange[]> {
this._pollingInterval = options.pollingInterval;
this._usePolling = options.usePolling;
this._watchers = Object.create(null);
this._watcherCount = 0;
return this.onWatchEvent;
}
public setVerboseLogging(enabled: boolean): Promise<void> {
this._verboseLogging = enabled;
return Promise.resolve();
}
public setRoots(requests: IWatcherRequest[]): Promise<void> {
const watchers = Object.create(null);
const newRequests: string[] = [];
const requestsByBasePath = normalizeRoots(requests);
// evaluate new & remaining watchers
for (let basePath in requestsByBasePath) {
let watcher = this._watchers[basePath];
if (watcher && isEqualRequests(watcher.requests, requestsByBasePath[basePath])) {
watchers[basePath] = watcher;
delete this._watchers[basePath];
} else {
newRequests.push(basePath);
}
}
// stop all old watchers
for (let path in this._watchers) {
this._watchers[path].stop();
}
// start all new watchers
for (let basePath of newRequests) {
let requests = requestsByBasePath[basePath];
watchers[basePath] = this._watch(basePath, requests);
}
this._watchers = watchers;
return Promise.resolve();
}
// for test purposes
public get wacherCount() {
return this._watcherCount;
}
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;
}
public stop(): Promise<void> {
for (let path in this._watchers) {
let watcher = this._watchers[path];
watcher.stop();
}
this._watchers = Object.create(null);
return Promise.resolve();
}
private log(message: string) {
this._onLogMessage.fire({ type: 'trace', message: `[File Watcher (chokidar)] ` + message });
}
private warn(message: string) {
this._onLogMessage.fire({ type: 'warn', message: `[File Watcher (chokidar)] ` + message });
}
private error(message: string) {
this._onLogMessage.fire({ type: 'error', message: `[File Watcher (chokidar)] ` + message });
}
}
function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean {
for (let request of requests) {
if (request.path === path) {
return false;
}
if (extpath.isEqualOrParent(path, request.path)) {
if (!request.parsedPattern) {
if (request.excludes && request.excludes.length > 0) {
let pattern = `{${request.excludes.join(',')}}`;
request.parsedPattern = glob.parse(pattern);
} else {
request.parsedPattern = () => false;
}
}
const relPath = path.substr(request.path.length + 1);
if (!request.parsedPattern(relPath)) {
return false;
}
}
}
return true;
}
/**
* 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.
*/
export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string]: IWatcherRequest[] } {
requests = requests.sort((r1, r2) => r1.path.localeCompare(r2.path));
let prevRequest: IWatcherRequest | null = null;
let result: { [basePath: string]: IWatcherRequest[] } = Object.create(null);
for (let request of requests) {
let basePath = request.path;
let ignored = (request.excludes || []).sort();
if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.path))) {
if (!isEqualIgnore(ignored, prevRequest.excludes)) {
result[prevRequest.path].push({ path: basePath, excludes: ignored });
}
} else {
prevRequest = { path: basePath, excludes: ignored };
result[basePath] = [prevRequest];
}
}
return result;
}
function isEqualRequests(r1: IWatcherRequest[], r2: IWatcherRequest[]) {
if (r1.length !== r2.length) {
return false;
}
for (let k = 0; k < r1.length; k++) {
if (r1[k].path !== r2[k].path || !isEqualIgnore(r1[k].excludes, r2[k].excludes)) {
return false;
}
}
return true;
}
function isEqualIgnore(i1: string[], i2: string[]) {
if (i1.length !== i2.length) {
return false;
}
for (let k = 0; k < i1.length; k++) {
if (i1[k] !== i2[k]) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,322 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as os from 'os';
import * as path from 'vs/base/common/path';
import * as pfs from 'vs/base/node/pfs';
import { normalizeRoots, ChokidarWatcherService } from '../chokidarWatcherService';
import { IWatcherRequest } from '../watcher';
import * as platform from 'vs/base/common/platform';
import { Delayer } from 'vs/base/common/async';
import { IDiskFileChange } from 'vs/platform/files/node/watcher/watcher';
import { FileChangeType } from 'vs/platform/files/common/files';
function newRequest(basePath: string, ignored: string[] = []): IWatcherRequest {
return { path: basePath, excludes: ignored };
}
function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) {
const requests = inputPaths.map(path => newRequest(path));
const actual = normalizeRoots(requests);
assert.deepEqual(Object.keys(actual).sort(), expectedPaths);
}
function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) {
const actual = normalizeRoots(inputRequests);
const actualPath = Object.keys(actual).sort();
const expectedPaths = Object.keys(expectedRequests).sort();
assert.deepEqual(actualPath, expectedPaths);
for (let path of actualPath) {
let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
assert.deepEqual(a, e);
}
}
function sort(changes: IDiskFileChange[]) {
return changes.sort((c1, c2) => {
return c1.path.localeCompare(c2.path);
});
}
function wait(time: number) {
return new Delayer<void>(time).trigger(() => { });
}
async function assertFileEvents(actuals: IDiskFileChange[], expected: IDiskFileChange[]) {
let repeats = 40;
while ((actuals.length < expected.length) && repeats-- > 0) {
await wait(50);
}
assert.deepEqual(sort(actuals), sort(expected));
actuals.length = 0;
}
suite('Chokidar normalizeRoots', () => {
test('should not impacts roots that don\'t overlap', () => {
if (platform.isWindows) {
assertNormalizedRootPath(['C:\\a'], ['C:\\a']);
assertNormalizedRootPath(['C:\\a', 'C:\\b'], ['C:\\a', 'C:\\b']);
assertNormalizedRootPath(['C:\\a', 'C:\\b', 'C:\\c\\d\\e'], ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
} else {
assertNormalizedRootPath(['/a'], ['/a']);
assertNormalizedRootPath(['/a', '/b'], ['/a', '/b']);
assertNormalizedRootPath(['/a', '/b', '/c/d/e'], ['/a', '/b', '/c/d/e']);
}
});
test('should remove sub-folders of other roots', () => {
if (platform.isWindows) {
assertNormalizedRootPath(['C:\\a', 'C:\\a\\b'], ['C:\\a']);
assertNormalizedRootPath(['C:\\a', 'C:\\b', 'C:\\a\\b'], ['C:\\a', 'C:\\b']);
assertNormalizedRootPath(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b'], ['C:\\a', 'C:\\b']);
assertNormalizedRootPath(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d'], ['C:\\a']);
} else {
assertNormalizedRootPath(['/a', '/a/b'], ['/a']);
assertNormalizedRootPath(['/a', '/b', '/a/b'], ['/a', '/b']);
assertNormalizedRootPath(['/b/a', '/a', '/b', '/a/b'], ['/a', '/b']);
assertNormalizedRootPath(['/a', '/a/b', '/a/c/d'], ['/a']);
assertNormalizedRootPath(['/a/c/d/e', '/a/b/d', '/a/c/d', '/a/c/e/f', '/a/b'], ['/a/b', '/a/c/d', '/a/c/e/f']);
}
});
test('should remove duplicates', () => {
if (platform.isWindows) {
assertNormalizedRootPath(['C:\\a', 'C:\\a\\', 'C:\\a'], ['C:\\a']);
} else {
assertNormalizedRootPath(['/a', '/a/', '/a'], ['/a']);
assertNormalizedRootPath(['/a', '/b', '/a/b'], ['/a', '/b']);
assertNormalizedRootPath(['/b/a', '/a', '/b', '/a/b'], ['/a', '/b']);
assertNormalizedRootPath(['/a', '/a/b', '/a/c/d'], ['/a']);
}
});
test('nested requests', () => {
let p1, p2, p3;
if (platform.isWindows) {
p1 = 'C:\\a';
p2 = 'C:\\a\\b';
p3 = 'C:\\a\\b\\c';
} else {
p1 = '/a';
p2 = '/a/b';
p3 = '/a/b/c';
}
const r1 = newRequest(p1, ['**/*.ts']);
const r2 = newRequest(p2, ['**/*.js']);
const r3 = newRequest(p3, ['**/*.ts']);
assertNormalizedRequests([r1, r2], { [p1]: [r1, r2] });
assertNormalizedRequests([r2, r1], { [p1]: [r1, r2] });
assertNormalizedRequests([r1, r2, r3], { [p1]: [r1, r2, r3] });
assertNormalizedRequests([r1, r3], { [p1]: [r1] });
assertNormalizedRequests([r2, r3], { [p2]: [r2, r3] });
});
});
suite.skip('Chokidar watching', () => {
const tmpdir = os.tmpdir();
const testDir = path.join(tmpdir, 'chokidartest-' + Date.now());
const aFolder = path.join(testDir, 'a');
const bFolder = path.join(testDir, 'b');
const b2Folder = path.join(bFolder, 'b2');
const service = new ChokidarWatcherService();
const result: IDiskFileChange[] = [];
let error: string | null = null;
suiteSetup(async () => {
await pfs.mkdirp(testDir);
await pfs.mkdirp(aFolder);
await pfs.mkdirp(bFolder);
await pfs.mkdirp(b2Folder);
const opts = { verboseLogging: false, pollingInterval: 200 };
service.watch(opts)(e => {
if (Array.isArray(e)) {
result.push(...e);
}
});
service.onLogMessage(msg => {
if (msg.type === 'error') {
console.log('set error', msg.message);
error = msg.message;
}
});
});
suiteTeardown(async () => {
await pfs.rimraf(testDir, pfs.RimRafMode.MOVE);
await service.stop();
});
setup(() => {
result.length = 0;
assert.equal(error, null);
});
teardown(() => {
assert.equal(error, null);
});
test('simple file operations, single root, no ignore', async () => {
let request: IWatcherRequest = { path: testDir, excludes: [] };
service.setRoots([request]);
await wait(300);
assert.equal(service.wacherCount, 1);
// create a file
let testFilePath = path.join(testDir, 'file.txt');
await pfs.writeFile(testFilePath, '');
await assertFileEvents(result, [{ path: testFilePath, type: FileChangeType.ADDED }]);
// modify a file
await pfs.writeFile(testFilePath, 'Hello');
await assertFileEvents(result, [{ path: testFilePath, type: FileChangeType.UPDATED }]);
// create a folder
let testFolderPath = path.join(testDir, 'newFolder');
await pfs.mkdirp(testFolderPath);
// copy a file
let copiedFilePath = path.join(testFolderPath, 'file2.txt');
await pfs.copy(testFilePath, copiedFilePath);
await assertFileEvents(result, [{ path: copiedFilePath, type: FileChangeType.ADDED }, { path: testFolderPath, type: FileChangeType.ADDED }]);
// delete a file
await pfs.rimraf(copiedFilePath, pfs.RimRafMode.MOVE);
let renamedFilePath = path.join(testFolderPath, 'file3.txt');
// move a file
await pfs.rename(testFilePath, renamedFilePath);
await assertFileEvents(result, [{ path: copiedFilePath, type: FileChangeType.DELETED }, { path: testFilePath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.ADDED }]);
// delete a folder
await pfs.rimraf(testFolderPath, pfs.RimRafMode.MOVE);
await assertFileEvents(result, [{ path: testFolderPath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.DELETED }]);
});
test('simple file operations, ignore', async () => {
let request: IWatcherRequest = { path: testDir, excludes: ['**/b/**', '**/*.js', '.git/**'] };
service.setRoots([request]);
await wait(300);
assert.equal(service.wacherCount, 1);
// create various ignored files
let file1 = path.join(bFolder, 'file1.txt'); // hidden
await pfs.writeFile(file1, 'Hello');
let file2 = path.join(b2Folder, 'file2.txt'); // hidden
await pfs.writeFile(file2, 'Hello');
let folder1 = path.join(bFolder, 'folder1'); // hidden
await pfs.mkdirp(folder1);
let folder2 = path.join(aFolder, 'b'); // hidden
await pfs.mkdirp(folder2);
let folder3 = path.join(testDir, '.git'); // hidden
await pfs.mkdirp(folder3);
let folder4 = path.join(testDir, '.git1');
await pfs.mkdirp(folder4);
let folder5 = path.join(aFolder, '.git');
await pfs.mkdirp(folder5);
let file3 = path.join(aFolder, 'file3.js'); // hidden
await pfs.writeFile(file3, 'var x;');
let file4 = path.join(aFolder, 'file4.txt');
await pfs.writeFile(file4, 'Hello');
await assertFileEvents(result, [{ path: file4, type: FileChangeType.ADDED }, { path: folder4, type: FileChangeType.ADDED }, { path: folder5, type: FileChangeType.ADDED }]);
// move some files
let movedFile1 = path.join(folder2, 'file1.txt'); // from ignored to ignored
await pfs.rename(file1, movedFile1);
let movedFile2 = path.join(aFolder, 'file2.txt'); // from ignored to visible
await pfs.rename(file2, movedFile2);
let movedFile3 = path.join(aFolder, 'file3.txt'); // from ignored file ext to visible
await pfs.rename(file3, movedFile3);
await assertFileEvents(result, [{ path: movedFile2, type: FileChangeType.ADDED }, { path: movedFile3, type: FileChangeType.ADDED }]);
// delete all files
await pfs.rimraf(movedFile1); // hidden
await pfs.rimraf(movedFile2, pfs.RimRafMode.MOVE);
await pfs.rimraf(movedFile3, pfs.RimRafMode.MOVE);
await pfs.rimraf(folder1); // hidden
await pfs.rimraf(folder2); // hidden
await pfs.rimraf(folder3); // hidden
await pfs.rimraf(folder4, pfs.RimRafMode.MOVE);
await pfs.rimraf(folder5, pfs.RimRafMode.MOVE);
await pfs.rimraf(file4, pfs.RimRafMode.MOVE);
await assertFileEvents(result, [{ path: movedFile2, type: FileChangeType.DELETED }, { path: movedFile3, type: FileChangeType.DELETED }, { path: file4, type: FileChangeType.DELETED }, { path: folder4, type: FileChangeType.DELETED }, { path: folder5, type: FileChangeType.DELETED }]);
});
test('simple file operations, multiple roots', async () => {
let request1: IWatcherRequest = { path: aFolder, excludes: ['**/*.js'] };
let request2: IWatcherRequest = { path: b2Folder, excludes: ['**/*.ts'] };
service.setRoots([request1, request2]);
await wait(300);
assert.equal(service.wacherCount, 2);
// create some files
let folderPath1 = path.join(aFolder, 'folder1');
await pfs.mkdirp(folderPath1);
let filePath1 = path.join(folderPath1, 'file1.json');
await pfs.writeFile(filePath1, '');
let filePath2 = path.join(folderPath1, 'file2.js'); // filtered
await pfs.writeFile(filePath2, '');
let folderPath2 = path.join(b2Folder, 'folder2');
await pfs.mkdirp(folderPath2);
let filePath3 = path.join(folderPath2, 'file3.ts'); // filtered
await pfs.writeFile(filePath3, '');
let filePath4 = path.join(testDir, 'file4.json'); // outside roots
await pfs.writeFile(filePath4, '');
await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.ADDED }, { path: filePath1, type: FileChangeType.ADDED }, { path: folderPath2, type: FileChangeType.ADDED }]);
// change roots
let request3: IWatcherRequest = { path: aFolder, excludes: ['**/*.json'] };
service.setRoots([request3]);
await wait(300);
assert.equal(service.wacherCount, 1);
// delete all
await pfs.rimraf(folderPath1, pfs.RimRafMode.MOVE);
await pfs.rimraf(folderPath2, pfs.RimRafMode.MOVE);
await pfs.rimraf(filePath4, pfs.RimRafMode.MOVE);
await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.DELETED }, { path: filePath2, type: FileChangeType.DELETED }]);
});
test('simple file operations, nested roots', async () => {
let request1: IWatcherRequest = { path: testDir, excludes: ['**/b2/**'] };
let request2: IWatcherRequest = { path: bFolder, excludes: ['**/b3/**'] };
service.setRoots([request1, request2]);
await wait(300);
assert.equal(service.wacherCount, 1);
// create files
let filePath1 = path.join(bFolder, 'file1.xml'); // visible by both
await pfs.writeFile(filePath1, '');
let filePath2 = path.join(b2Folder, 'file2.xml'); // filtered by root1, but visible by root2
await pfs.writeFile(filePath2, '');
let folderPath1 = path.join(b2Folder, 'b3'); // filtered
await pfs.mkdirp(folderPath1);
let filePath3 = path.join(folderPath1, 'file3.xml'); // filtered
await pfs.writeFile(filePath3, '');
await assertFileEvents(result, [{ path: filePath1, type: FileChangeType.ADDED }, { path: filePath2, type: FileChangeType.ADDED }]);
let renamedFilePath2 = path.join(folderPath1, 'file2.xml');
// move a file
await pfs.rename(filePath2, renamedFilePath2);
await assertFileEvents(result, [{ path: filePath2, type: FileChangeType.DELETED }]);
// delete all
await pfs.rimraf(folderPath1, pfs.RimRafMode.MOVE);
await pfs.rimraf(filePath1, pfs.RimRafMode.MOVE);
await assertFileEvents(result, [{ path: filePath1, type: FileChangeType.DELETED }]);
});
});

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
export interface IWatcherRequest {
path: string;
excludes: string[];
}
export interface IWatcherOptions {
pollingInterval?: number;
usePolling?: boolean;
}
export interface IWatcherService {
watch(options: IWatcherOptions): Event<IDiskFileChange[]>;
setRoots(roots: IWatcherRequest[]): Promise<void>;
setVerboseLogging(enabled: boolean): Promise<void>;
onLogMessage: Event<ILogMessage>;
stop(): Promise<void>;
}

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { WatcherChannel } from 'vs/platform/files/node/watcher/unix/watcherIpc';
import { ChokidarWatcherService } from 'vs/platform/files/node/watcher/unix/chokidarWatcherService';
const server = new Server('watcher');
const service = new ChokidarWatcherService();
const channel = new WatcherChannel(service);
server.registerChannel('watcher', channel);

View File

@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IWatcherRequest, IWatcherService, IWatcherOptions } from './watcher';
import { Event } from 'vs/base/common/event';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
export class WatcherChannel implements IServerChannel {
constructor(private service: IWatcherService) { }
listen(_: unknown, event: string, arg?: any): Event<any> {
switch (event) {
case 'watch': return this.service.watch(arg);
case 'onLogMessage': return this.service.onLogMessage;
}
throw new Error(`Event not found: ${event}`);
}
call(_: unknown, command: string, arg?: any): Promise<any> {
switch (command) {
case 'setRoots': return this.service.setRoots(arg);
case 'setVerboseLogging': return this.service.setVerboseLogging(arg);
case 'stop': return this.service.stop();
}
throw new Error(`Call not found: ${command}`);
}
}
export class WatcherChannelClient implements IWatcherService {
constructor(private channel: IChannel) { }
watch(options: IWatcherOptions): Event<IDiskFileChange[]> {
return this.channel.listen('watch', options);
}
setVerboseLogging(enable: boolean): Promise<void> {
return this.channel.call('setVerboseLogging', enable);
}
get onLogMessage(): Event<ILogMessage> {
return this.channel.listen('onLogMessage');
}
setRoots(roots: IWatcherRequest[]): Promise<void> {
return this.channel.call('setRoots', roots);
}
stop(): Promise<void> {
return this.channel.call('stop');
}
}

View File

@@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { WatcherChannelClient } from 'vs/platform/files/node/watcher/unix/watcherIpc';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWatcherRequest, IWatcherOptions } from 'vs/platform/files/node/watcher/unix/watcher';
import { getPathFromAmdModule } from 'vs/base/common/amd';
export class FileWatcher extends Disposable {
private static readonly MAX_RESTARTS = 5;
private isDisposed: boolean;
private restartCounter: number;
private service: WatcherChannelClient;
constructor(
private folders: IWatcherRequest[],
private onFileChanges: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean,
private watcherOptions: IWatcherOptions = {}
) {
super();
this.isDisposed = false;
this.restartCounter = 0;
this.startWatching();
}
private startWatching(): void {
const client = this._register(new Client(
getPathFromAmdModule(require, 'bootstrap-fork'),
{
serverName: 'File Watcher (chokidar)',
args: ['--type=watcherService'],
env: {
AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: 'true' // transmit console logs from server to client
}
}
));
this._register(client.onDidProcessExit(() => {
// our watcher app should never be completed because it keeps on watching. being in here indicates
// that the watcher process died and we want to restart it here. we only do it a max number of times
if (!this.isDisposed) {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.error('terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.error('failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}));
// Initialize watcher
const channel = getNextTickChannel(client.getChannel('watcher'));
this.service = new WatcherChannelClient(channel);
this.service.setVerboseLogging(this.verboseLogging);
this._register(this.service.watch(this.watcherOptions)(e => !this.isDisposed && this.onFileChanges(e)));
this._register(this.service.onLogMessage(m => this.onLogMessage(m)));
// Start watching
this.service.setRoots(this.folders);
}
error(message: string) {
this.onLogMessage({ type: 'error', message: `[File Watcher (chokidar)] ${message}` });
}
setVerboseLogging(verboseLogging: boolean): void {
this.verboseLogging = verboseLogging;
this.service.setVerboseLogging(verboseLogging);
}
setFolders(folders: IWatcherRequest[]): void {
this.folders = folders;
this.service.setRoots(folders);
}
dispose(): void {
this.isDisposed = true;
super.dispose();
}
}

View File

@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI as uri } from 'vs/base/common/uri';
import { FileChangeType, isParent, IFileChange } from 'vs/platform/files/common/files';
import { isLinux } from 'vs/base/common/platform';
export interface IDiskFileChange {
type: FileChangeType;
path: string;
}
export interface ILogMessage {
type: 'trace' | 'warn' | 'error';
message: string;
}
export function toFileChanges(changes: IDiskFileChange[]): IFileChange[] {
return changes.map(change => ({
type: change.type,
resource: uri.file(change.path)
}));
}
export function normalizeFileChanges(changes: IDiskFileChange[]): IDiskFileChange[] {
// Build deltas
const normalizer = new EventNormalizer();
for (const event of changes) {
normalizer.processEvent(event);
}
return normalizer.normalize();
}
class EventNormalizer {
private normalized: IDiskFileChange[] = [];
private mapPathToChange: Map<string, IDiskFileChange> = new Map();
processEvent(event: IDiskFileChange): void {
const existingEvent = this.mapPathToChange.get(event.path);
// Event path already exists
if (existingEvent) {
const currentChangeType = existingEvent.type;
const newChangeType = event.type;
// ignore CREATE followed by DELETE in one go
if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.DELETED) {
this.mapPathToChange.delete(event.path);
this.normalized.splice(this.normalized.indexOf(existingEvent), 1);
}
// flatten DELETE followed by CREATE into CHANGE
else if (currentChangeType === FileChangeType.DELETED && newChangeType === FileChangeType.ADDED) {
existingEvent.type = FileChangeType.UPDATED;
}
// Do nothing. Keep the created event
else if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.UPDATED) { }
// Otherwise apply change type
else {
existingEvent.type = newChangeType;
}
}
// Otherwise store new
else {
this.normalized.push(event);
this.mapPathToChange.set(event.path, event);
}
}
normalize(): IDiskFileChange[] {
const addedChangeEvents: IDiskFileChange[] = [];
const deletedPaths: string[] = [];
// This algorithm will remove all DELETE events up to the root folder
// that got deleted if any. This ensures that we are not producing
// DELETE events for each file inside a folder that gets deleted.
//
// 1.) split ADD/CHANGE and DELETED events
// 2.) sort short deleted paths to the top
// 3.) for each DELETE, check if there is a deleted parent and ignore the event in that case
return this.normalized.filter(e => {
if (e.type !== FileChangeType.DELETED) {
addedChangeEvents.push(e);
return false; // remove ADD / CHANGE
}
return true; // keep DELETE
}).sort((e1, e2) => {
return e1.path.length - e2.path.length; // shortest path first
}).filter(e => {
if (deletedPaths.some(d => isParent(e.path, d, !isLinux /* ignorecase */))) {
return false; // DELETE is ignored if parent is deleted already
}
// otherwise mark as deleted
deletedPaths.push(e.path);
return true;
}).concat(addedChangeEvents);
}
}

View File

@@ -0,0 +1,8 @@
# Native File Watching for Windows using C# FileSystemWatcher
- Repository: https://github.com/Microsoft/vscode-filewatcher-windows
# Build
- Build in "Release" config
- Copy CodeHelper.exe over into this folder

View File

@@ -0,0 +1,140 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import { FileChangeType } from 'vs/platform/files/common/files';
import * as decoder from 'vs/base/node/decoder';
import * as glob from 'vs/base/common/glob';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { getPathFromAmdModule } from 'vs/base/common/amd';
export class OutOfProcessWin32FolderWatcher {
private static readonly MAX_RESTARTS = 5;
private static changeTypeMap: FileChangeType[] = [FileChangeType.UPDATED, FileChangeType.ADDED, FileChangeType.DELETED];
private ignored: glob.ParsedPattern[];
private handle: cp.ChildProcess;
private restartCounter: number;
constructor(
private watchedFolder: string,
ignored: string[],
private eventCallback: (events: IDiskFileChange[]) => void,
private logCallback: (message: ILogMessage) => void,
private verboseLogging: boolean
) {
this.restartCounter = 0;
if (Array.isArray(ignored)) {
this.ignored = ignored.map(i => glob.parse(i));
} else {
this.ignored = [];
}
// Logging
if (this.verboseLogging) {
this.log(`Start watching: ${watchedFolder}`);
}
this.startWatcher();
}
private startWatcher(): void {
const args = [this.watchedFolder];
if (this.verboseLogging) {
args.push('-verbose');
}
this.handle = cp.spawn(getPathFromAmdModule(require, 'vs/platform/files/node/watcher/win32/CodeHelper.exe'), args);
const stdoutLineDecoder = new decoder.LineDecoder();
// Events over stdout
this.handle.stdout.on('data', (data: Buffer) => {
// Collect raw events from output
const rawEvents: IDiskFileChange[] = [];
stdoutLineDecoder.write(data).forEach((line) => {
const eventParts = line.split('|');
if (eventParts.length === 2) {
const changeType = Number(eventParts[0]);
const absolutePath = eventParts[1];
// File Change Event (0 Changed, 1 Created, 2 Deleted)
if (changeType >= 0 && changeType < 3) {
// Support ignores
if (this.ignored && this.ignored.some(ignore => ignore(absolutePath))) {
if (this.verboseLogging) {
this.log(absolutePath);
}
return;
}
// Otherwise record as event
rawEvents.push({
type: OutOfProcessWin32FolderWatcher.changeTypeMap[changeType],
path: absolutePath
});
}
// 3 Logging
else {
this.log(eventParts[1]);
}
}
});
// Trigger processing of events through the delayer to batch them up properly
if (rawEvents.length > 0) {
this.eventCallback(rawEvents);
}
});
// Errors
this.handle.on('error', (error: Error) => this.onError(error));
this.handle.stderr.on('data', (data: Buffer) => this.onError(data));
// Exit
this.handle.on('exit', (code: number, signal: string) => this.onExit(code, signal));
}
private onError(error: Error | Buffer): void {
this.error('process error: ' + error.toString());
}
private onExit(code: number, signal: string): void {
if (this.handle) { // exit while not yet being disposed is unexpected!
this.error(`terminated unexpectedly (code: ${code}, signal: ${signal})`);
if (this.restartCounter <= OutOfProcessWin32FolderWatcher.MAX_RESTARTS) {
this.error('is restarted again...');
this.restartCounter++;
this.startWatcher(); // restart
} else {
this.error('Watcher failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}
private error(message: string) {
this.logCallback({ type: 'error', message: `[File Watcher (C#)] ${message}` });
}
private log(message: string) {
this.logCallback({ type: 'trace', message: `[File Watcher (C#)] ${message}` });
}
public dispose(): void {
if (this.handle) {
this.handle.kill();
this.handle = null!; // StrictNullOverride: nulling out ok in dispose
}
}
}

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { OutOfProcessWin32FolderWatcher } from 'vs/platform/files/node/watcher/win32/csharpWatcherService';
import { posix } from 'vs/base/common/path';
import { rtrim, endsWith } from 'vs/base/common/strings';
import { IDisposable } from 'vs/base/common/lifecycle';
export class FileWatcher implements IDisposable {
private folder: { path: string, excludes: string[] };
private service: OutOfProcessWin32FolderWatcher | undefined = undefined;
constructor(
folders: { path: string, excludes: string[] }[],
private onFileChanges: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean
) {
this.folder = folders[0];
if (this.folder.path.indexOf('\\\\') === 0 && endsWith(this.folder.path, posix.sep)) {
// for some weird reason, node adds a trailing slash to UNC paths
// we never ever want trailing slashes as our base path unless
// someone opens root ("/").
// See also https://github.com/nodejs/io.js/issues/1765
this.folder.path = rtrim(this.folder.path, posix.sep);
}
this.service = this.startWatching();
}
private get isDisposed(): boolean {
return !this.service;
}
private startWatching(): OutOfProcessWin32FolderWatcher {
return new OutOfProcessWin32FolderWatcher(
this.folder.path,
this.folder.excludes,
events => this.onFileEvents(events),
message => this.onLogMessage(message),
this.verboseLogging
);
}
setVerboseLogging(verboseLogging: boolean): void {
this.verboseLogging = verboseLogging;
if (this.service) {
this.service.dispose();
this.service = this.startWatching();
}
}
private onFileEvents(events: IDiskFileChange[]): void {
if (this.isDisposed) {
return;
}
// Emit through event emitter
if (events.length > 0) {
this.onFileChanges(events);
}
}
dispose(): void {
if (this.service) {
this.service.dispose();
this.service = undefined;
}
}
}