diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index e9ec6999c3f27..c8f80a6fb114f 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -564,8 +564,8 @@ export class CodeApplication extends Disposable { // Local Files const diskFileSystemProvider = this.fileService.getProvider(Schemas.file); assertType(diskFileSystemProvider instanceof DiskFileSystemProvider); - const fileSystemProviderChannel = new DiskFileSystemProviderChannel(diskFileSystemProvider); - mainProcessElectronServer.registerChannel('localFiles', fileSystemProviderChannel); + const fileSystemProviderChannel = new DiskFileSystemProviderChannel(diskFileSystemProvider, this.logService); + mainProcessElectronServer.registerChannel('diskFiles', fileSystemProviderChannel); // Configuration mainProcessElectronServer.registerChannel(UserConfigurationFileServiceId, ProxyChannel.fromService(new UserConfigurationFileService(this.environmentMainService, this.fileService, this.logService))); diff --git a/src/vs/platform/files/common/diskFileSystemProvider.ts b/src/vs/platform/files/common/diskFileSystemProvider.ts new file mode 100644 index 0000000000000..cc0a25840d798 --- /dev/null +++ b/src/vs/platform/files/common/diskFileSystemProvider.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { insert } from 'vs/base/common/arrays'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { Emitter } from 'vs/base/common/event'; +import { combinedDisposable, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { normalize } from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; +import { IFileChange, IWatchOptions } from 'vs/platform/files/common/files'; +import { IDiskFileChange, ILogMessage, IWatchRequest, toFileChanges, WatcherService } from 'vs/platform/files/common/watcher'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; + +export interface IWatcherOptions { + pollingInterval?: number; + usePolling: boolean | string[]; +} + +export abstract class AbstractDiskFileSystemProvider extends Disposable { + + constructor( + protected readonly logService: ILogService + ) { + super(); + } + + //#region File Watching + + protected readonly _onDidErrorOccur = this._register(new Emitter()); + readonly onDidErrorOccur = this._onDidErrorOccur.event; + + protected readonly _onDidChangeFile = this._register(new Emitter()); + readonly onDidChangeFile = this._onDidChangeFile.event; + + private recursiveWatcher: WatcherService | undefined; + private readonly recursiveFoldersToWatch: IWatchRequest[] = []; + private recursiveWatchRequestDelayer = this._register(new ThrottledDelayer(0)); + + watch(resource: URI, opts: IWatchOptions): IDisposable { + if (opts.recursive) { + return this.watchRecursive(resource, opts); + } + + return this.watchNonRecursive(resource); + } + + private watchRecursive(resource: URI, opts: IWatchOptions): IDisposable { + + // Add to list of folders to watch recursively + const folderToWatch: IWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes }; + const remove = insert(this.recursiveFoldersToWatch, folderToWatch); + + // Trigger update + this.refreshRecursiveWatchers(); + + return toDisposable(() => { + + // Remove from list of folders to watch recursively + remove(); + + // 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(async () => { + this.doRefreshRecursiveWatchers(); + }); + } + + private doRefreshRecursiveWatchers(): void { + + // Reuse existing + if (this.recursiveWatcher) { + this.recursiveWatcher.watch(this.recursiveFoldersToWatch); + } + + // Otherwise, create new if we have folders to watch + else if (this.recursiveFoldersToWatch.length > 0) { + this.recursiveWatcher = this._register(this.createRecursiveWatcher( + this.recursiveFoldersToWatch, + changes => this._onDidChangeFile.fire(toFileChanges(changes)), + msg => this.onWatcherLogMessage(msg), + this.logService.getLevel() === LogLevel.Trace + )); + + // Apply log levels dynamically + this._register(this.logService.onDidChangeLogLevel(() => { + this.recursiveWatcher?.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace); + })); + } + } + + protected abstract createRecursiveWatcher( + folders: IWatchRequest[], + onChange: (changes: IDiskFileChange[]) => void, + onLogMessage: (msg: ILogMessage) => void, + verboseLogging: boolean + ): WatcherService; + + private watchNonRecursive(resource: URI): IDisposable { + const watcherService = this.createNonRecursiveWatcher( + this.toFilePath(resource), + changes => this._onDidChangeFile.fire(toFileChanges(changes)), + msg => this.onWatcherLogMessage(msg), + this.logService.getLevel() === LogLevel.Trace + ); + + const logLevelListener = this.logService.onDidChangeLogLevel(() => { + watcherService.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace); + }); + + return combinedDisposable(watcherService, logLevelListener); + } + + private onWatcherLogMessage(msg: ILogMessage): void { + if (msg.type === 'error') { + this._onDidErrorOccur.fire(msg.message); + } + + this.logService[msg.type](msg.message); + } + + protected abstract createNonRecursiveWatcher( + path: string, + onChange: (changes: IDiskFileChange[]) => void, + onLogMessage: (msg: ILogMessage) => void, + verboseLogging: boolean + ): IDisposable & { setVerboseLogging: (verboseLogging: boolean) => void }; + + protected toFilePath(resource: URI): string { + return normalize(resource.fsPath); + } + + //#endregion +} diff --git a/src/vs/platform/files/common/ipcFileSystemProvider.ts b/src/vs/platform/files/common/ipcFileSystemProvider.ts index 45b1e839cd453..f0ae54386799d 100644 --- a/src/vs/platform/files/common/ipcFileSystemProvider.ts +++ b/src/vs/platform/files/common/ipcFileSystemProvider.ts @@ -20,23 +20,19 @@ interface IFileChangeDto { type: FileChangeType; } -/** - * An abstract file system provider that delegates all calls to a provided - * `IChannel` via IPC communication. - */ export abstract class IPCFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability { - private readonly session: string = generateUuid(); + constructor(private readonly channel: IChannel) { + super(); - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChangeFile = this._onDidChange.event; + this.registerFileChangeListeners(); + } - private _onDidWatchErrorOccur = this._register(new Emitter()); - readonly onDidErrorOccur = this._onDidWatchErrorOccur.event; + //#region File Capabilities private readonly _onDidChangeCapabilities = this._register(new Emitter()); readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; @@ -48,24 +44,6 @@ export abstract class IPCFileSystemProvider extends Disposable implements | FileSystemProviderCapabilities.FileWriteUnlock; get capabilities(): FileSystemProviderCapabilities { return this._capabilities; } - constructor(private readonly channel: IChannel) { - super(); - - this.registerListeners(); - } - - private registerListeners(): void { - this._register(this.channel.listen('filechange', [this.session])(eventsOrError => { - if (Array.isArray(eventsOrError)) { - const events = eventsOrError; - this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type }))); - } else { - const error = eventsOrError; - this._onDidWatchErrorOccur.fire(error); - } - })); - } - protected setCaseSensitive(isCaseSensitive: boolean) { if (isCaseSensitive) { this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive; @@ -73,34 +51,24 @@ export abstract class IPCFileSystemProvider extends Disposable implements this._capabilities &= ~FileSystemProviderCapabilities.PathCaseSensitive; } - this._onDidChangeCapabilities.fire(undefined); + this._onDidChangeCapabilities.fire(); } - // --- forwarding calls + //#endregion + + //#region File Metadata Resolving stat(resource: URI): Promise { return this.channel.call('stat', [resource]); } - open(resource: URI, opts: FileOpenOptions): Promise { - return this.channel.call('open', [resource, opts]); - } - - close(fd: number): Promise { - return this.channel.call('close', [fd]); + readdir(resource: URI): Promise<[string, FileType][]> { + return this.channel.call('readdir', [resource]); } - async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { - const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]); + //#endregion - // copy back the data that was written into the buffer on the remote - // side. we need to do this because buffers are not referenced by - // pointer, but only by value and as such cannot be directly written - // to from the other process. - data.set(bytes.buffer.slice(0, bytesRead), offset); - - return bytesRead; - } + //#region File Reading/Writing async readFile(resource: URI): Promise { const buff = await this.channel.call('readFile', [resource]); @@ -160,24 +128,44 @@ export abstract class IPCFileSystemProvider extends Disposable implements return stream; } - write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { - return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]); - } - writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]); } - delete(resource: URI, opts: FileDeleteOptions): Promise { - return this.channel.call('delete', [resource, opts]); + open(resource: URI, opts: FileOpenOptions): Promise { + return this.channel.call('open', [resource, opts]); } + close(fd: number): Promise { + return this.channel.call('close', [fd]); + } + + async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]); + + // copy back the data that was written into the buffer on the remote + // side. we need to do this because buffers are not referenced by + // pointer, but only by value and as such cannot be directly written + // to from the other process. + data.set(bytes.buffer.slice(0, bytesRead), offset); + + return bytesRead; + } + + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]); + } + + //#endregion + + //#region Move/Copy/Delete/Create Folder + mkdir(resource: URI): Promise { return this.channel.call('mkdir', [resource]); } - readdir(resource: URI): Promise<[string, FileType][]> { - return this.channel.call('readdir', [resource]); + delete(resource: URI, opts: FileDeleteOptions): Promise { + return this.channel.call('delete', [resource, opts]); } rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise { @@ -188,10 +176,50 @@ export abstract class IPCFileSystemProvider extends Disposable implements return this.channel.call('copy', [resource, target, opts]); } + //#endregion + + //#region File Watching + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChangeFile = this._onDidChange.event; + + private _onDidErrorOccur = this._register(new Emitter()); + readonly onDidErrorOccur = this._onDidErrorOccur.event; + + // The contract for file watching via remote is to identify us + // via a unique but readonly session ID. Since the remote is + // managing potentially many watchers from different clients, + // this helps the server to properly partition events to the right + // clients. + private readonly sessionId = generateUuid(); + + private registerFileChangeListeners(): void { + + // The contract for file changes is that there is one listener + // for both events and errors from the watcher. So we need to + // unwrap the event from the remote and emit through the proper + // emitter. + this._register(this.channel.listen('filechange', [this.sessionId])(eventsOrError => { + if (Array.isArray(eventsOrError)) { + const events = eventsOrError; + this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type }))); + } else { + const error = eventsOrError; + this._onDidErrorOccur.fire(error); + } + })); + } + watch(resource: URI, opts: IWatchOptions): IDisposable { - const req = Math.random(); - this.channel.call('watch', [this.session, req, resource, opts]); - return toDisposable(() => this.channel.call('unwatch', [this.session, req])); + // Generate a request UUID to correlate the watcher + // back to us when we ask to dispose the watcher later. + const req = generateUuid(); + + this.channel.call('watch', [this.sessionId, req, resource, opts]); + + return toDisposable(() => this.channel.call('unwatch', [this.sessionId, req])); } + + //#endregion } diff --git a/src/vs/platform/files/electron-main/diskFileSystemProviderChannel.ts b/src/vs/platform/files/electron-main/diskFileSystemProviderChannel.ts index 76862fe023fbb..6d522f8c22f68 100644 --- a/src/vs/platform/files/electron-main/diskFileSystemProviderChannel.ts +++ b/src/vs/platform/files/electron-main/diskFileSystemProviderChannel.ts @@ -7,20 +7,26 @@ import { shell } from 'electron'; import { localize } from 'vs/nls'; import { isWindows } from 'vs/base/common/platform'; import { Emitter, Event } from 'vs/base/common/event'; -import { URI, UriComponents } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { FileDeleteOptions, FileOverwriteOptions, FileType, IStat, FileOpenOptions, FileWriteOptions, FileReadStreamOptions } from 'vs/platform/files/common/files'; +import { FileDeleteOptions, FileOverwriteOptions, FileType, IStat, FileOpenOptions, FileWriteOptions, FileReadStreamOptions, IFileChange, IWatchOptions } from 'vs/platform/files/common/files'; +import { FileWatcher as NodeJSWatcherService } from 'vs/platform/files/node/watcher/nodejs/watcherService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { VSBuffer } from 'vs/base/common/buffer'; import { listenStream, ReadableStreamEventPayload } from 'vs/base/common/stream'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { basename, normalize } from 'vs/base/common/path'; +import { combinedDisposable, Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { ILogMessage, toFileChanges } from 'vs/platform/files/common/watcher'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; -export class DiskFileSystemProviderChannel implements IServerChannel { +export class DiskFileSystemProviderChannel extends Disposable implements IServerChannel { constructor( - private readonly provider: DiskFileSystemProvider + private readonly provider: DiskFileSystemProvider, + private readonly logService: ILogService ) { + super(); } call(_: unknown, command: string, arg?: any): Promise { @@ -37,6 +43,8 @@ export class DiskFileSystemProviderChannel implements IServerChannel { case 'copy': return this.copy(URI.revive(arg[0]), URI.revive(arg[1]), arg[2]); case 'mkdir': return this.mkdir(URI.revive(arg[0])); case 'delete': return this.delete(URI.revive(arg[0]), arg[1]); + case 'watch': return Promise.resolve(this.watch(arg[0], arg[1], URI.revive(arg[2]), arg[3])); + case 'unwatch': return Promise.resolve(this.unwatch(arg[0], arg[1])); } throw new Error(`IPC Command ${command} not found`); @@ -44,14 +52,34 @@ export class DiskFileSystemProviderChannel implements IServerChannel { listen(_: unknown, event: string, arg: any): Event { switch (event) { - case 'filechange': return Event.None; // not supported from here, needs to use shared process for file watching - case 'readFileStream': return this.onReadFileStream(arg[0], arg[1]); + case 'filechange': return this.onDidChangeFileOrError.event; + case 'readFileStream': return this.onReadFileStream(URI.revive(arg[0]), arg[1]); } throw new Error(`Unknown event ${event}`); } - private onReadFileStream(resource: UriComponents, opts: FileReadStreamOptions): Event> { + //#region File Metadata Resolving + + private stat(resource: URI): Promise { + return this.provider.stat(resource); + } + + private readdir(resource: URI): Promise<[string, FileType][]> { + return this.provider.readdir(resource); + } + + //#endregion + + //#region File Reading/Writing + + private async readFile(resource: URI): Promise { + const buff = await this.provider.readFile(resource); + + return VSBuffer.wrap(buff); + } + + private onReadFileStream(resource: URI, opts: FileReadStreamOptions): Event> { const cts = new CancellationTokenSource(); const emitter = new Emitter>({ @@ -63,11 +91,13 @@ export class DiskFileSystemProviderChannel implements IServerChannel { } }); - const fileStream = this.provider.readFileStream(URI.revive(resource), opts, cts.token); + const fileStream = this.provider.readFileStream(resource, opts, cts.token); listenStream(fileStream, { onData: chunk => emitter.fire(VSBuffer.wrap(chunk)), onError: error => emitter.fire(error), onEnd: () => { + + // Forward event emitter.fire('end'); // Cleanup @@ -79,12 +109,8 @@ export class DiskFileSystemProviderChannel implements IServerChannel { return emitter.event; } - private stat(resource: URI): Promise { - return this.provider.stat(resource); - } - - private readdir(resource: URI): Promise<[string, FileType][]> { - return this.provider.readdir(resource); + private writeFile(resource: URI, content: VSBuffer, opts: FileWriteOptions): Promise { + return this.provider.writeFile(resource, content.buffer, opts); } private open(resource: URI, opts: FileOpenOptions): Promise { @@ -103,27 +129,13 @@ export class DiskFileSystemProviderChannel implements IServerChannel { return [buffer, bytesRead]; } - private async readFile(resource: URI): Promise { - const buff = await this.provider.readFile(resource); - - return VSBuffer.wrap(buff); - } - private write(fd: number, pos: number, data: VSBuffer, offset: number, length: number): Promise { return this.provider.write(fd, pos, data.buffer, offset, length); } - private writeFile(resource: URI, content: VSBuffer, opts: FileWriteOptions): Promise { - return this.provider.writeFile(resource, content.buffer, opts); - } - - private rename(source: URI, target: URI, opts: FileOverwriteOptions): Promise { - return this.provider.rename(source, target, opts); - } + //#endregion - private copy(source: URI, target: URI, opts: FileOverwriteOptions): Promise { - return this.provider.copy(source, target, opts); - } + //#region Move/Copy/Delete/Create Folder private mkdir(resource: URI): Promise { return this.provider.mkdir(resource); @@ -141,4 +153,66 @@ export class DiskFileSystemProviderChannel implements IServerChannel { throw new Error(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath))); } } + + private rename(source: URI, target: URI, opts: FileOverwriteOptions): Promise { + return this.provider.rename(source, target, opts); + } + + private copy(source: URI, target: URI, opts: FileOverwriteOptions): Promise { + return this.provider.copy(source, target, opts); + } + + //#endregion + + //#region File Watching + + private readonly onDidChangeFileOrError = this._register(new Emitter()); + + private readonly nonRecursiveFileWatchers = new Map(); + + private watch(sessionId: string, req: number, resource: URI, opts: IWatchOptions): void { + if (opts.recursive) { + throw new Error('Recursive watcher is not supported from main process'); + } + + const watcher = new NodeJSWatcherService( + normalize(resource.fsPath), + changes => this.onDidChangeFileOrError.fire(toFileChanges(changes)), + msg => this.onWatcherLogMessage(msg), + this.logService.getLevel() === LogLevel.Trace + ); + + const logLevelListener = this.logService.onDidChangeLogLevel(() => { + watcher.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace); + }); + + const id = sessionId + req; + this.nonRecursiveFileWatchers.set(id, combinedDisposable(watcher, logLevelListener)); + } + + private onWatcherLogMessage(msg: ILogMessage): void { + if (msg.type === 'error') { + this.onDidChangeFileOrError.fire(msg.message); + } + + this.logService[msg.type](msg.message); + } + + private unwatch(sessionId: string, req: number): void { + const id = sessionId + req; + const disposable = this.nonRecursiveFileWatchers.get(id); + if (disposable) { + dispose(disposable); + this.nonRecursiveFileWatchers.delete(id); + } + } + + //#endregion + + override dispose(): void { + super.dispose(); + + this.nonRecursiveFileWatchers.forEach(disposable => dispose(disposable)); + this.nonRecursiveFileWatchers.clear(); + } } diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 918fc1675dbe1..f1c2d6f2395dc 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -4,29 +4,29 @@ *--------------------------------------------------------------------------------------------*/ import { Stats } from 'fs'; -import { insert } from 'vs/base/common/arrays'; -import { retry, ThrottledDelayer } from 'vs/base/common/async'; +import { retry } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { isEqual } from 'vs/base/common/extpath'; -import { combinedDisposable, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { basename, dirname, normalize } from 'vs/base/common/path'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { basename, dirname } from 'vs/base/common/path'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; import { newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; import { IDirent, Promises, RimRafMode, SymlinkSupport } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; -import { createFileSystemProviderError, FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileChange, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { createFileSystemProviderError, FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat } from 'vs/platform/files/common/files'; import { readFileIntoStream } from 'vs/platform/files/common/io'; 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 ParcelWatcherService } from 'vs/platform/files/node/watcher/parcel/watcherService'; import { FileWatcher as UnixWatcherService } from 'vs/platform/files/node/watcher/unix/watcherService'; -import { IDiskFileChange, ILogMessage, IWatchRequest, toFileChanges, WatcherService } from 'vs/platform/files/common/watcher'; -import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { IDiskFileChange, ILogMessage, IWatchRequest, WatcherService } from 'vs/platform/files/common/watcher'; +import { ILogService } from 'vs/platform/log/common/log'; import product from 'vs/platform/product/common/product'; +import { AbstractDiskFileSystemProvider } from 'vs/platform/files/common/diskFileSystemProvider'; export interface IWatcherOptions { pollingInterval?: number; @@ -39,7 +39,7 @@ export interface IDiskFileSystemProviderOptions { legacyWatcher?: string; } -export class DiskFileSystemProvider extends Disposable implements +export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, @@ -48,10 +48,10 @@ export class DiskFileSystemProvider extends Disposable implements private readonly BUFFER_SIZE = this.options?.bufferSize || 256 * 1024; constructor( - protected readonly logService: ILogService, + logService: ILogService, private readonly options?: IDiskFileSystemProviderOptions ) { - super(); + super(logService); } //#region File Capabilities @@ -526,81 +526,6 @@ export class DiskFileSystemProvider extends Disposable implements //#region File Watching - private readonly _onDidWatchErrorOccur = this._register(new Emitter()); - readonly onDidErrorOccur = this._onDidWatchErrorOccur.event; - - private readonly _onDidChangeFile = this._register(new Emitter()); - readonly onDidChangeFile = this._onDidChangeFile.event; - - private recursiveWatcher: WatcherService | undefined; - private readonly recursiveFoldersToWatch: IWatchRequest[] = []; - private recursiveWatchRequestDelayer = this._register(new ThrottledDelayer(0)); - - watch(resource: URI, opts: IWatchOptions): IDisposable { - if (opts.recursive) { - return this.watchRecursive(resource, opts); - } - - return this.watchNonRecursive(resource); - } - - private watchRecursive(resource: URI, opts: IWatchOptions): IDisposable { - - // Add to list of folders to watch recursively - const folderToWatch: IWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes }; - const remove = insert(this.recursiveFoldersToWatch, folderToWatch); - - // Trigger update - this.refreshRecursiveWatchers(); - - return toDisposable(() => { - - // Remove from list of folders to watch recursively - remove(); - - // 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(async () => { - this.doRefreshRecursiveWatchers(); - }); - } - - private doRefreshRecursiveWatchers(): void { - - // Reuse existing - if (this.recursiveWatcher) { - this.recursiveWatcher.watch(this.recursiveFoldersToWatch); - } - - // Otherwise, create new if we have folders to watch - else if (this.recursiveFoldersToWatch.length > 0) { - this.recursiveWatcher = this._register(this.createRecursiveWatcher( - this.recursiveFoldersToWatch, - 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 - )); - - // Apply log levels dynamically - this._register(this.logService.onDidChangeLogLevel(() => { - this.recursiveWatcher?.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace); - })); - } - } - protected createRecursiveWatcher( folders: IWatchRequest[], onChange: (changes: IDiskFileChange[]) => void, @@ -634,7 +559,7 @@ export class DiskFileSystemProvider extends Disposable implements if (product.quality === 'stable') { // in stable use legacy for single folder workspaces // TODO@bpasero remove me eventually - enableLegacyWatcher = this.recursiveFoldersToWatch.length === 1; + enableLegacyWatcher = folders.length === 1; } } @@ -659,35 +584,24 @@ export class DiskFileSystemProvider extends Disposable implements ); } - 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 + protected createNonRecursiveWatcher( + path: string, + onChange: (changes: IDiskFileChange[]) => void, + onLogMessage: (msg: ILogMessage) => void, + verboseLogging: boolean + ): IDisposable & { setVerboseLogging: (verboseLogging: boolean) => void } { + return new NodeJSWatcherService( + path, + changes => onChange(changes), + msg => onLogMessage(msg), + verboseLogging ); - - 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 diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 5907c9a8c1b20..d0ca57a4ca38b 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -27,7 +27,7 @@ class DesktopMain extends SharedDesktopMain { ): void { // Local Files - const diskFileSystemProvider = new DiskFileSystemProvider(mainProcessService); + const diskFileSystemProvider = new DiskFileSystemProvider(mainProcessService, sharedProcessWorkerWorkbenchService, logService); fileService.registerProvider(Schemas.file, diskFileSystemProvider); // User Data Provider diff --git a/src/vs/workbench/services/files/electron-browser/diskFileSystemProvider.ts b/src/vs/workbench/services/files/electron-browser/diskFileSystemProvider.ts index 95054df92d1e7..e2d1d50907958 100644 --- a/src/vs/workbench/services/files/electron-browser/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files/electron-browser/diskFileSystemProvider.ts @@ -6,42 +6,27 @@ import { basename } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; -import { FileSystemProviderCapabilities, FileDeleteOptions, IStat, FileType, FileReadStreamOptions, FileWriteOptions, FileOpenOptions, FileOverwriteOptions } from 'vs/platform/files/common/files'; +import { FileSystemProviderCapabilities, FileDeleteOptions, IStat, FileType, FileReadStreamOptions, FileWriteOptions, FileOpenOptions, FileOverwriteOptions, IWatchOptions } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider as NodeDiskFileSystemProvider, IDiskFileSystemProviderOptions as INodeDiskFileSystemProviderOptions } from 'vs/platform/files/node/diskFileSystemProvider'; +import { DiskFileSystemProvider as SandboxedDiskFileSystemProvider } from 'vs/workbench/services/files/electron-sandbox/diskFileSystemProvider'; import { ILogService } from 'vs/platform/log/common/log'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { ISharedProcessWorkerWorkbenchService } from 'vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessWorkerWorkbenchService'; -import { IWatchRequest, IDiskFileChange, ILogMessage, WatcherService } from 'vs/platform/files/common/watcher'; -import { ParcelFileWatcher } from 'vs/workbench/services/files/electron-sandbox/parcelWatcherService'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ReadableStreamEvents } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; -import { IPCFileSystemProvider } from 'vs/platform/files/common/ipcFileSystemProvider'; +import { IDisposable } from 'vs/base/common/lifecycle'; export interface IDiskFileSystemProviderOptions extends INodeDiskFileSystemProviderOptions { experimentalSandbox: boolean; } -class MainProcessDiskFileSystemProvider extends IPCFileSystemProvider { - - constructor(mainProcessService: IMainProcessService) { - super(mainProcessService.getChannel('localFiles')); - } -} - export class DiskFileSystemProvider extends NodeDiskFileSystemProvider { private readonly experimentalSandbox: boolean; - private _sandboxedFs: MainProcessDiskFileSystemProvider | undefined = undefined; - get sandboxedFs(): MainProcessDiskFileSystemProvider { - if (!this._sandboxedFs) { - this._sandboxedFs = new MainProcessDiskFileSystemProvider(this.mainProcessService); - } - - return this._sandboxedFs; - } + private readonly sandboxedFs = new SandboxedDiskFileSystemProvider(this.mainProcessService, this.sharedProcessWorkerWorkbenchService, this.logService); constructor( logService: ILogService, @@ -53,6 +38,14 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider { super(logService, options); this.experimentalSandbox = !!options?.experimentalSandbox; + this.registerListeners(); + } + + private registerListeners(): void { + + // Forward events from the embedded provider + this.sandboxedFs.onDidChangeFile(e => this._onDidChangeFile.fire(e)); + this.sandboxedFs.onDidErrorOccur(e => this._onDidErrorOccur.fire(e)); } //#region File Capabilities @@ -199,23 +192,12 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider { //#region File Watching - protected override createRecursiveWatcher( - folders: IWatchRequest[], - onChange: (changes: IDiskFileChange[]) => void, - onLogMessage: (msg: ILogMessage) => void, - verboseLogging: boolean - ): WatcherService { - if (!this.experimentalSandbox) { - return super.createRecursiveWatcher(folders, onChange, onLogMessage, verboseLogging); - } - - return new ParcelFileWatcher( - folders, - changes => onChange(changes), - msg => onLogMessage(msg), - verboseLogging, - this.sharedProcessWorkerWorkbenchService - ); + override watch(resource: URI, opts: IWatchOptions): IDisposable { + if (this.experimentalSandbox) { + return this.sandboxedFs.watch(resource, opts); + } + + return super.watch(resource, opts); } //#endregion diff --git a/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts b/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts index 6c0bd23509dcd..9716058401c01 100644 --- a/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts @@ -6,31 +6,55 @@ import { Event } from 'vs/base/common/event'; import { isLinux } from 'vs/base/common/platform'; import { FileSystemProviderCapabilities, FileDeleteOptions, IStat, FileType, FileReadStreamOptions, FileWriteOptions, FileOpenOptions, FileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, IWatchOptions } from 'vs/platform/files/common/files'; +import { AbstractDiskFileSystemProvider } from 'vs/platform/files/common/diskFileSystemProvider'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ReadableStreamEvents } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; import { IPCFileSystemProvider } from 'vs/platform/files/common/ipcFileSystemProvider'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDiskFileChange, ILogMessage, IWatchRequest, WatcherService } from 'vs/platform/files/common/watcher'; +import { ParcelFileWatcher } from 'vs/workbench/services/files/electron-sandbox/parcelWatcherService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ISharedProcessWorkerWorkbenchService } from 'vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessWorkerWorkbenchService'; class MainProcessDiskFileSystemProvider extends IPCFileSystemProvider { constructor(mainProcessService: IMainProcessService) { - super(mainProcessService.getChannel('localFiles')); + super(mainProcessService.getChannel('diskFiles')); } } -export class DiskFileSystemProvider implements +export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability { + private readonly provider = new MainProcessDiskFileSystemProvider(this.mainProcessService); + + constructor( + private readonly mainProcessService: IMainProcessService, + private readonly sharedProcessWorkerWorkbenchService: ISharedProcessWorkerWorkbenchService, + logService: ILogService + ) { + super(logService); + + this.registerListeners(); + } + + private registerListeners(): void { + + // Forward events from the embedded provider + this.provider.onDidChangeFile(e => this._onDidChangeFile.fire(e)); + this.provider.onDidErrorOccur(e => this._onDidErrorOccur.fire(e)); + } + //#region File Capabilities readonly onDidChangeCapabilities: Event = Event.None; - protected _capabilities: FileSystemProviderCapabilities | undefined; + private _capabilities: FileSystemProviderCapabilities | undefined; get capabilities(): FileSystemProviderCapabilities { if (!this._capabilities) { this._capabilities = @@ -38,6 +62,7 @@ export class DiskFileSystemProvider implements FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileReadStream | FileSystemProviderCapabilities.FileFolderCopy | + FileSystemProviderCapabilities.Trash | FileSystemProviderCapabilities.FileWriteUnlock; if (isLinux) { @@ -50,13 +75,6 @@ export class DiskFileSystemProvider implements //#endregion - private readonly provider = new MainProcessDiskFileSystemProvider(this.mainProcessService); - - constructor( - private readonly mainProcessService: IMainProcessService - ) { - } - //#region File Metadata Resolving stat(resource: URI): Promise { @@ -123,10 +141,34 @@ export class DiskFileSystemProvider implements //#region File Watching - onDidErrorOccur = Event.None; - onDidChangeFile = Event.None; - watch(resource: URI, opts: IWatchOptions): IDisposable { - return Disposable.None; + override watch(resource: URI, opts: IWatchOptions): IDisposable { + + // Recursive: via parcel file watcher from `createRecursiveWatcher` + if (opts.recursive) { + return super.watch(resource, opts); + } + + // Non-recursive: via main process services + return this.provider.watch(resource, opts); + } + + protected createRecursiveWatcher( + folders: IWatchRequest[], + onChange: (changes: IDiskFileChange[]) => void, + onLogMessage: (msg: ILogMessage) => void, + verboseLogging: boolean + ): WatcherService { + return new ParcelFileWatcher( + folders, + changes => onChange(changes), + msg => onLogMessage(msg), + verboseLogging, + this.sharedProcessWorkerWorkbenchService + ); + } + + protected createNonRecursiveWatcher(): never { + throw new Error('Method not implemented in sandbox.'); } //#endregion