diff --git a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts index ac64655eb29..6ffefc6e2d9 100644 --- a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts @@ -6,22 +6,22 @@ import { mkdir, open, close, read, write } from 'fs'; import { promisify } from 'util'; import { IDisposable, Disposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, FileChangeType } from 'vs/platform/files/common/files'; +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, readdir, unlink, move, copy, readFile, writeFile, fileExists, truncate, rimraf, RimRafMode } from 'vs/base/node/pfs'; -import { watchFolder, watchFile } from 'vs/base/node/watcher'; 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, normalizeFileChanges, toFileChanges } from 'vs/workbench/services/files2/node/watcher/normalizer'; +import { IDiskFileChange, toFileChanges } from 'vs/workbench/services/files2/node/watcher/normalizer'; import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files2/node/watcher/unix/watcherService'; import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files2/node/watcher/win32/watcherService'; import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files2/node/watcher/nsfw/watcherService'; +import { FileWatcher as NodeJSWatcherService } from 'vs/workbench/services/files2/node/watcher/nodejs/watcherService'; export class DiskFileSystemProvider extends Disposable implements IFileSystemProvider { @@ -314,9 +314,6 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro private _onDidChangeFile: Emitter = this._register(new Emitter()); get onDidChangeFile(): Event { return this._onDidChangeFile.event; } - private nonRecursiveFileChangesDelayer: ThrottledDelayer = this._register(new ThrottledDelayer(50)); - private nonRecursiveFileChangesBuffer: IDiskFileChange[] = []; - private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService; private recursiveFoldersToWatch: { path: string, excludes: string[] }[] = []; private recursiveWatchRequestDelayer: ThrottledDelayer = this._register(new ThrottledDelayer(0)); @@ -406,62 +403,13 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro } private watchNonRecursive(resource: URI): IDisposable { - let disposed = false; - let disposable = toDisposable(() => disposed = true); - - this.stat(resource).then(fileStat => { - if (disposed) { - return; - } - - // Watch Folder - if (fileStat.type === FileType.Directory) { - disposable = watchFolder(resource.fsPath, (eventType, path) => { - this.onNonRecursiveFileChange({ type: eventType === 'changed' ? FileChangeType.UPDATED : eventType === 'added' ? FileChangeType.ADDED : FileChangeType.DELETED, path }); - }, error => this.logService.error(error)); - } - - // Watch File - else { - disposable = watchFile(resource.fsPath, (eventType, path) => { - this.onNonRecursiveFileChange({ type: eventType === 'changed' ? FileChangeType.UPDATED : FileChangeType.DELETED, path }); - }, error => this.logService.error(error)); - } - }, error => this.logService.error(error)); - - return toDisposable(() => dispose(disposable)); - } - - private onNonRecursiveFileChange(event: IDiskFileChange): void { - - // Add to buffer - this.nonRecursiveFileChangesBuffer.push(event); - - // Logging - if (this.logService.getLevel() === LogLevel.Trace) { - this.logService.trace(`[File Watcher (node.js)] ${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.nonRecursiveFileChangesDelayer.trigger(() => { - const nonRecursiveFileChanges = this.nonRecursiveFileChangesBuffer; - this.nonRecursiveFileChangesBuffer = []; - - // Event normalization - const normalizedNonRecursiveFileChangesEvents = normalizeFileChanges(nonRecursiveFileChanges); - - // Logging - if (this.logService.getLevel() === LogLevel.Trace) { - normalizedNonRecursiveFileChangesEvents.forEach(event => { - this.logService.trace(`[File Watcher (node.js)] >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`); - }); - } - - // Fire - this._onDidChangeFile.fire(toFileChanges(normalizedNonRecursiveFileChangesEvents)); - - return Promise.resolve(); - }); + return new NodeJSWatcherService( + this.toFilePath(resource), + changes => this._onDidChangeFile.fire(toFileChanges(changes)), + error => this._onDidWatchErrorOccur.fire(new Error(error)), + info => this.logService.trace(info), + this.logService.getLevel() === LogLevel.Trace + ); } //#endregion diff --git a/src/vs/workbench/services/files2/node/watcher/nodejs/watcherService.ts b/src/vs/workbench/services/files2/node/watcher/nodejs/watcherService.ts new file mode 100644 index 00000000000..965d975b059 --- /dev/null +++ b/src/vs/workbench/services/files2/node/watcher/nodejs/watcherService.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDiskFileChange, normalizeFileChanges } from 'vs/workbench/services/files2/node/watcher/normalizer'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { stat } from 'vs/base/node/pfs'; +import { watchFolder, watchFile } from 'vs/base/node/watcher'; +import { FileChangeType } from 'vs/platform/files/common/files'; +import { ThrottledDelayer } from 'vs/base/common/async'; + +export class FileWatcher extends Disposable { + private isDisposed: boolean; + + private fileChangesDelayer: ThrottledDelayer = this._register(new ThrottledDelayer(50)); + private fileChangesBuffer: IDiskFileChange[] = []; + + constructor( + private path: string, + private onFileChanges: (changes: IDiskFileChange[]) => void, + private errorLogger: (msg: string) => void, + private verboseLogger: (msg: string) => void, + private verboseLogging: boolean + ) { + super(); + + this.startWatching(); + } + + private async startWatching(): Promise { + try { + const fileStat = await stat(this.path); + + if (this.isDisposed) { + return; + } + + // Watch Folder + if (fileStat.isDirectory()) { + watchFolder(this.path, (eventType, path) => { + this.onFileChange({ type: eventType === 'changed' ? FileChangeType.UPDATED : eventType === 'added' ? FileChangeType.ADDED : FileChangeType.DELETED, path }); + }, error => this.onError(error)); + } + + // Watch File + else { + this._register(watchFile(this.path, (eventType, path) => { + this.onFileChange({ type: eventType === 'changed' ? FileChangeType.UPDATED : FileChangeType.DELETED, path }); + }, 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(`[File Watcher (node.js)] ${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(`[File Watcher (node.js)] >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`); + }); + } + + // Fire + this.onFileEvents(normalizedFileChanges); + + return Promise.resolve(); + }); + } + + private onFileEvents(events: IDiskFileChange[]): void { + if (this.isDisposed) { + return; + } + + // Emit through event emitter + if (events.length > 0) { + this.onFileChanges(events); + } + } + + private onError(error: string): void { + if (!this.isDisposed) { + this.errorLogger(error); + } + } + + private onVerbose(msg: string): void { + if (!this.isDisposed) { + this.verboseLogger(msg); + } + } + + dispose(): void { + this.isDisposed = true; + + super.dispose(); + } +} \ No newline at end of file