From a9438c996cbddbfb07f47beb2ee40ca4429c28df Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Oct 2021 08:37:20 +0200 Subject: [PATCH] sandbox - restore session based file watching (#132282) --- src/vs/code/electron-main/app.ts | 4 +- src/vs/platform/files/common/fileService.ts | 46 +++--- ...hannel.ts => diskFileSystemProviderIpc.ts} | 107 +++++++++---- src/vs/server/remoteAgentFileSystemImpl.ts | 145 +++++++++--------- .../electron-browser/desktop.main.ts | 2 +- .../electron-sandbox/desktop.main.ts | 4 +- .../diskFileSystemProvider.ts | 2 +- .../common/remoteAgentFileSystemChannel.ts | 2 +- 8 files changed, 182 insertions(+), 130 deletions(-) rename src/vs/platform/files/electron-main/{diskFileSystemProviderChannel.ts => diskFileSystemProviderIpc.ts} (75%) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index c8f80a6fb11..c4574912081 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -47,7 +47,7 @@ import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/e import { IExternalTerminalMainService } from 'vs/platform/externalTerminal/common/externalTerminal'; import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; import { IFileService } from 'vs/platform/files/common/files'; -import { DiskFileSystemProviderChannel } from 'vs/platform/files/electron-main/diskFileSystemProviderChannel'; +import { DiskFileSystemProviderChannel } from 'vs/platform/files/electron-main/diskFileSystemProviderIpc'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -565,7 +565,7 @@ export class CodeApplication extends Disposable { const diskFileSystemProvider = this.fileService.getProvider(Schemas.file); assertType(diskFileSystemProvider instanceof DiskFileSystemProvider); const fileSystemProviderChannel = new DiskFileSystemProviderChannel(diskFileSystemProvider, this.logService); - mainProcessElectronServer.registerChannel('diskFiles', fileSystemProviderChannel); + mainProcessElectronServer.registerChannel('localFilesystem', fileSystemProviderChannel); // Configuration mainProcessElectronServer.registerChannel(UserConfigurationFileServiceId, ProxyChannel.fromService(new UserConfigurationFileService(this.environmentMainService, this.fileService, this.logService))); diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index 4409daaff40..968c9968ec7 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -1078,21 +1078,19 @@ export class FileService extends Disposable implements IFileService { // Forward watch request to provider and // wire in disposables. - { - let watchDisposed = false; - let disposeWatch = () => { watchDisposed = true; }; - disposables.add(toDisposable(() => disposeWatch())); + let watchDisposed = false; + let disposeWatch = () => { watchDisposed = true; }; + disposables.add(toDisposable(() => disposeWatch())); - // Watch and wire in disposable which is async but - // check if we got disposed meanwhile and forward - this.doWatch(resource, options).then(disposable => { - if (watchDisposed) { - dispose(disposable); - } else { - disposeWatch = () => dispose(disposable); - } - }, error => this.logService.error(error)); - } + // Watch and wire in disposable which is async but + // check if we got disposed meanwhile and forward + this.doWatch(resource, options).then(disposable => { + if (watchDisposed) { + dispose(disposable); + } else { + disposeWatch = () => dispose(disposable); + } + }, error => this.logService.error(error)); // Remember as watched resource and unregister // properly on disposal. @@ -1128,8 +1126,10 @@ export class FileService extends Disposable implements IFileService { const key = this.toWatchKey(provider, resource, options); // Only start watching if we are the first for the given key - const watcher = this.activeWatchers.get(key) || { count: 0, disposable: provider.watch(resource, options) }; - if (!this.activeWatchers.has(key)) { + let watcher = this.activeWatchers.get(key); + if (!watcher) { + watcher = { count: 0, disposable: provider.watch(resource, options) }; + this.activeWatchers.set(key, watcher); } @@ -1137,14 +1137,16 @@ export class FileService extends Disposable implements IFileService { watcher.count += 1; return toDisposable(() => { + if (watcher) { - // Unref - watcher.count--; + // Unref + watcher.count--; - // Dispose only when last user is reached - if (watcher.count === 0) { - dispose(watcher.disposable); - this.activeWatchers.delete(key); + // Dispose only when last user is reached + if (watcher.count === 0) { + dispose(watcher.disposable); + this.activeWatchers.delete(key); + } } }); } diff --git a/src/vs/platform/files/electron-main/diskFileSystemProviderChannel.ts b/src/vs/platform/files/electron-main/diskFileSystemProviderIpc.ts similarity index 75% rename from src/vs/platform/files/electron-main/diskFileSystemProviderChannel.ts rename to src/vs/platform/files/electron-main/diskFileSystemProviderIpc.ts index adfe1dc29dc..aa4f33f3132 100644 --- a/src/vs/platform/files/electron-main/diskFileSystemProviderChannel.ts +++ b/src/vs/platform/files/electron-main/diskFileSystemProviderIpc.ts @@ -16,7 +16,7 @@ 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 { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILogMessage, toFileChanges } from 'vs/platform/files/common/watcher'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; @@ -56,7 +56,7 @@ export class DiskFileSystemProviderChannel extends Disposable implements IServer listen(_: unknown, event: string, arg: any): Event { switch (event) { - case 'fileChange': return this.onDidChangeFileOrError.event; + case 'fileChange': return this.onFileChange(arg[0]); case 'readFileStream': return this.onReadFileStream(URI.revive(arg[0]), arg[1]); } @@ -170,44 +170,45 @@ export class DiskFileSystemProviderChannel extends Disposable implements IServer //#region File Watching - private readonly onDidChangeFileOrError = this._register(new Emitter()); + private readonly sessionToWatcher = new Map(); + private readonly watchRequests = new Map(); - private readonly nonRecursiveFileWatchers = new Map(); + private onFileChange(sessionId: string): Event { + + // We want a specific emitter for the given session so that events + // from the one session do not end up on the other session. As such + // we create a `SessionFileWatcher` and a `Emitter` for that session. + const emitter = new Emitter({ + onFirstListenerAdd: () => { + this.sessionToWatcher.set(sessionId, new SessionFileWatcher(emitter, this.logService)); + }, + onLastListenerRemove: () => { + dispose(this.sessionToWatcher.get(sessionId)); + this.sessionToWatcher.delete(sessionId); + } + }); + + return emitter.event; + } private async watch(sessionId: string, req: number, resource: URI, opts: IWatchOptions): Promise { if (opts.recursive) { throw createFileSystemProviderError('Recursive watcher is not supported from main process', FileSystemProviderErrorCode.Unavailable); } - 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); + const watcher = this.sessionToWatcher.get(sessionId); + if (watcher) { + const disposable = watcher.watch(req, resource); + this.watchRequests.set(sessionId + req, disposable); } - - this.logService[msg.type](msg.message); } private async unwatch(sessionId: string, req: number): Promise { const id = sessionId + req; - const disposable = this.nonRecursiveFileWatchers.get(id); + const disposable = this.watchRequests.get(id); if (disposable) { dispose(disposable); - this.nonRecursiveFileWatchers.delete(id); + this.watchRequests.delete(id); } } @@ -216,7 +217,57 @@ export class DiskFileSystemProviderChannel extends Disposable implements IServer override dispose(): void { super.dispose(); - this.nonRecursiveFileWatchers.forEach(disposable => dispose(disposable)); - this.nonRecursiveFileWatchers.clear(); + this.watchRequests.forEach(disposable => dispose(disposable)); + this.watchRequests.clear(); + + this.sessionToWatcher.forEach(disposable => dispose(disposable)); + this.sessionToWatcher.clear(); + } +} + +class SessionFileWatcher extends Disposable { + + private readonly watcherRequests = new Map(); + + constructor( + private readonly sessionEmitter: Emitter, + private readonly logService: ILogService + ) { + super(); + } + + watch(req: number, resource: URI): IDisposable { + const disposable = new DisposableStore(); + + this.watcherRequests.set(req, disposable); + disposable.add(toDisposable(() => this.watcherRequests.delete(req))); + + const watcher = disposable.add(new NodeJSWatcherService( + normalize(resource.fsPath), + changes => this.sessionEmitter.fire(toFileChanges(changes)), + msg => this.onWatcherLogMessage(msg), + this.logService.getLevel() === LogLevel.Trace + )); + + disposable.add(this.logService.onDidChangeLogLevel(() => { + watcher.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace); + })); + + return disposable; + } + + private onWatcherLogMessage(msg: ILogMessage): void { + if (msg.type === 'error') { + this.sessionEmitter.fire(msg.message); + } + + this.logService[msg.type](msg.message); + } + + override dispose(): void { + super.dispose(); + + this.watcherRequests.forEach(disposable => dispose(disposable)); + this.watcherRequests.clear(); } } diff --git a/src/vs/server/remoteAgentFileSystemImpl.ts b/src/vs/server/remoteAgentFileSystemImpl.ts index b81dd7d01c6..ff4730133cd 100644 --- a/src/vs/server/remoteAgentFileSystemImpl.ts +++ b/src/vs/server/remoteAgentFileSystemImpl.ts @@ -19,77 +19,6 @@ import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService'; import { listenStream, ReadableStreamEventPayload } from 'vs/base/common/stream'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -class SessionFileWatcher extends Disposable { - - private readonly watcherRequests = new Map(); - private readonly fileWatcher = this._register(new DiskFileSystemProvider(this.logService, { watcher: this.getWatcherOptions() })); - - constructor( - private readonly logService: ILogService, - private readonly environmentService: IServerEnvironmentService, - private readonly uriTransformer: IURITransformer, - emitter: Emitter - ) { - super(); - - this.registerListeners(emitter); - } - - private registerListeners(emitter: Emitter): void { - const localChangeEmitter = this._register(new Emitter()); - - this._register(localChangeEmitter.event((events) => { - emitter.fire( - events.map(e => ({ - resource: this.uriTransformer.transformOutgoingURI(e.resource), - type: e.type - })) - ); - })); - - this._register(this.fileWatcher.onDidChangeFile(events => localChangeEmitter.fire(events))); - this._register(this.fileWatcher.onDidErrorOccur(error => emitter.fire(error))); - } - - private getWatcherOptions(): IWatcherOptions | undefined { - const fileWatcherPolling = this.environmentService.args['fileWatcherPolling']; - if (fileWatcherPolling) { - const segments = fileWatcherPolling.split(delimiter); - const pollingInterval = Number(segments[0]); - if (pollingInterval > 0) { - const usePolling = segments.length > 1 ? segments.slice(1) : true; - return { usePolling, pollingInterval }; - } - } - - return undefined; - } - - watch(req: number, _resource: UriComponents, opts: IWatchOptions): IDisposable { - const resource = URI.revive(this.uriTransformer.transformIncoming(_resource)); - - if (this.environmentService.extensionsPath) { - // when opening the $HOME folder, we end up watching the extension folder - // so simply exclude watching the extensions folder - opts.excludes = [...(opts.excludes || []), posix.join(this.environmentService.extensionsPath, '**')]; - } - - this.watcherRequests.set(req, this.fileWatcher.watch(resource, opts)); - - return toDisposable(() => { - dispose(this.watcherRequests.get(req)); - this.watcherRequests.delete(req); - }); - } - - override dispose(): void { - super.dispose(); - - this.watcherRequests.forEach(disposable => dispose(disposable)); - this.watcherRequests.clear(); - } -} - export class RemoteAgentFileSystemChannel extends Disposable implements IServerChannel { private readonly uriTransformerCache = new Map(); @@ -292,11 +221,10 @@ export class RemoteAgentFileSystemChannel extends Disposable implements IServerC } private async watch(session: string, req: number, _resource: UriComponents, opts: IWatchOptions): Promise { - const id = session + req; const watcher = this.fileWatchers.get(session); if (watcher) { const disposable = watcher.watch(req, _resource, opts); - this.watchRequests.set(id, disposable); + this.watchRequests.set(session + req, disposable); } } @@ -321,3 +249,74 @@ export class RemoteAgentFileSystemChannel extends Disposable implements IServerC this.fileWatchers.clear(); } } + +class SessionFileWatcher extends Disposable { + + private readonly watcherRequests = new Map(); + private readonly fileWatcher = this._register(new DiskFileSystemProvider(this.logService, { watcher: this.getWatcherOptions() })); + + constructor( + private readonly logService: ILogService, + private readonly environmentService: IServerEnvironmentService, + private readonly uriTransformer: IURITransformer, + sessionEmitter: Emitter + ) { + super(); + + this.registerListeners(sessionEmitter); + } + + private registerListeners(sessionEmitter: Emitter): void { + const localChangeEmitter = this._register(new Emitter()); + + this._register(localChangeEmitter.event((events) => { + sessionEmitter.fire( + events.map(e => ({ + resource: this.uriTransformer.transformOutgoingURI(e.resource), + type: e.type + })) + ); + })); + + this._register(this.fileWatcher.onDidChangeFile(events => localChangeEmitter.fire(events))); + this._register(this.fileWatcher.onDidErrorOccur(error => sessionEmitter.fire(error))); + } + + private getWatcherOptions(): IWatcherOptions | undefined { + const fileWatcherPolling = this.environmentService.args['fileWatcherPolling']; + if (fileWatcherPolling) { + const segments = fileWatcherPolling.split(delimiter); + const pollingInterval = Number(segments[0]); + if (pollingInterval > 0) { + const usePolling = segments.length > 1 ? segments.slice(1) : true; + return { usePolling, pollingInterval }; + } + } + + return undefined; + } + + watch(req: number, _resource: UriComponents, opts: IWatchOptions): IDisposable { + const resource = URI.revive(this.uriTransformer.transformIncoming(_resource)); + + if (this.environmentService.extensionsPath) { + // when opening the $HOME folder, we end up watching the extension folder + // so simply exclude watching the extensions folder + opts.excludes = [...(opts.excludes || []), posix.join(this.environmentService.extensionsPath, '**')]; + } + + this.watcherRequests.set(req, this.fileWatcher.watch(resource, opts)); + + return toDisposable(() => { + dispose(this.watcherRequests.get(req)); + this.watcherRequests.delete(req); + }); + } + + override dispose(): void { + super.dispose(); + + this.watcherRequests.forEach(disposable => dispose(disposable)); + this.watcherRequests.clear(); + } +} diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 37252641f99..4748c305d00 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -35,7 +35,7 @@ class DesktopMain extends SharedDesktopMain { fileService.registerProvider(Schemas.file, diskFileSystemProvider); // User Data Provider - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, logService)); + fileService.registerProvider(Schemas.userData, this._register(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, logService))); } } diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index d0ca57a4ca3..207d4d69716 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -27,11 +27,11 @@ class DesktopMain extends SharedDesktopMain { ): void { // Local Files - const diskFileSystemProvider = new DiskFileSystemProvider(mainProcessService, sharedProcessWorkerWorkbenchService, logService); + const diskFileSystemProvider = this._register(new DiskFileSystemProvider(mainProcessService, sharedProcessWorkerWorkbenchService, logService)); fileService.registerProvider(Schemas.file, diskFileSystemProvider); // User Data Provider - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, logService)); + fileService.registerProvider(Schemas.userData, this._register(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, logService))); } } diff --git a/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts b/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts index e91dc9bf83f..647a1476cf0 100644 --- a/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts @@ -31,7 +31,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple private readonly provider = this._register(new class extends IPCFileSystemProvider { constructor(mainProcessService: IMainProcessService) { - super(mainProcessService.getChannel('diskFiles')); + super(mainProcessService.getChannel('localFilesystem')); } }(this.mainProcessService)); diff --git a/src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts index f490fc8f67a..6ef6d5aae2d 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts @@ -7,7 +7,7 @@ import { OperatingSystem } from 'vs/base/common/platform'; import { IPCFileSystemProvider } from 'vs/platform/files/common/ipcFileSystemProvider'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -export const REMOTE_FILE_SYSTEM_CHANNEL_NAME = 'remotefilesystem'; +export const REMOTE_FILE_SYSTEM_CHANNEL_NAME = 'remoteFilesystem'; export class RemoteFileSystemProvider extends IPCFileSystemProvider {