Path: blob/main/src/vs/platform/files/node/diskFileSystemProviderServer.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../base/common/event.js';6import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';7import { DiskFileSystemProvider } from './diskFileSystemProvider.js';8import { Disposable, dispose, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';9import { ILogService } from '../../log/common/log.js';10import { IURITransformer } from '../../../base/common/uriIpc.js';11import { URI, UriComponents } from '../../../base/common/uri.js';12import { VSBuffer } from '../../../base/common/buffer.js';13import { ReadableStreamEventPayload, listenStream } from '../../../base/common/stream.js';14import { IStat, IFileReadStreamOptions, IFileWriteOptions, IFileOpenOptions, IFileDeleteOptions, IFileOverwriteOptions, IFileChange, IWatchOptions, FileType, IFileAtomicReadOptions } from '../common/files.js';15import { CancellationTokenSource } from '../../../base/common/cancellation.js';16import { IEnvironmentService } from '../../environment/common/environment.js';17import { IRecursiveWatcherOptions } from '../common/watcher.js';1819export interface ISessionFileWatcher extends IDisposable {20watch(req: number, resource: URI, opts: IWatchOptions): IDisposable;21}2223/**24* A server implementation for a IPC based file system provider client.25*/26export abstract class AbstractDiskFileSystemProviderChannel<T> extends Disposable implements IServerChannel<T> {2728constructor(29protected readonly provider: DiskFileSystemProvider,30protected readonly logService: ILogService31) {32super();33}3435call(ctx: T, command: string, arg?: any): Promise<any> {36const uriTransformer = this.getUriTransformer(ctx);3738switch (command) {39case 'stat': return this.stat(uriTransformer, arg[0]);40case 'realpath': return this.realpath(uriTransformer, arg[0]);41case 'readdir': return this.readdir(uriTransformer, arg[0]);42case 'open': return this.open(uriTransformer, arg[0], arg[1]);43case 'close': return this.close(arg[0]);44case 'read': return this.read(arg[0], arg[1], arg[2]);45case 'readFile': return this.readFile(uriTransformer, arg[0], arg[1]);46case 'write': return this.write(arg[0], arg[1], arg[2], arg[3], arg[4]);47case 'writeFile': return this.writeFile(uriTransformer, arg[0], arg[1], arg[2]);48case 'rename': return this.rename(uriTransformer, arg[0], arg[1], arg[2]);49case 'copy': return this.copy(uriTransformer, arg[0], arg[1], arg[2]);50case 'cloneFile': return this.cloneFile(uriTransformer, arg[0], arg[1]);51case 'mkdir': return this.mkdir(uriTransformer, arg[0]);52case 'delete': return this.delete(uriTransformer, arg[0], arg[1]);53case 'watch': return this.watch(uriTransformer, arg[0], arg[1], arg[2], arg[3]);54case 'unwatch': return this.unwatch(arg[0], arg[1]);55}5657throw new Error(`IPC Command ${command} not found`);58}5960listen(ctx: T, event: string, arg: any): Event<any> {61const uriTransformer = this.getUriTransformer(ctx);6263switch (event) {64case 'fileChange': return this.onFileChange(uriTransformer, arg[0]);65case 'readFileStream': return this.onReadFileStream(uriTransformer, arg[0], arg[1]);66}6768throw new Error(`Unknown event ${event}`);69}7071protected abstract getUriTransformer(ctx: T): IURITransformer;7273protected abstract transformIncoming(uriTransformer: IURITransformer, _resource: UriComponents, supportVSCodeResource?: boolean): URI;7475//#region File Metadata Resolving7677private stat(uriTransformer: IURITransformer, _resource: UriComponents): Promise<IStat> {78const resource = this.transformIncoming(uriTransformer, _resource, true);7980return this.provider.stat(resource);81}8283private realpath(uriTransformer: IURITransformer, _resource: UriComponents): Promise<string> {84const resource = this.transformIncoming(uriTransformer, _resource, true);8586return this.provider.realpath(resource);87}8889private readdir(uriTransformer: IURITransformer, _resource: UriComponents): Promise<[string, FileType][]> {90const resource = this.transformIncoming(uriTransformer, _resource);9192return this.provider.readdir(resource);93}9495//#endregion9697//#region File Reading/Writing9899private async readFile(uriTransformer: IURITransformer, _resource: UriComponents, opts?: IFileAtomicReadOptions): Promise<VSBuffer> {100const resource = this.transformIncoming(uriTransformer, _resource, true);101const buffer = await this.provider.readFile(resource, opts);102103return VSBuffer.wrap(buffer);104}105106private onReadFileStream(uriTransformer: IURITransformer, _resource: URI, opts: IFileReadStreamOptions): Event<ReadableStreamEventPayload<VSBuffer>> {107const resource = this.transformIncoming(uriTransformer, _resource, true);108const cts = new CancellationTokenSource();109110const emitter = new Emitter<ReadableStreamEventPayload<VSBuffer>>({111onDidRemoveLastListener: () => {112113// Ensure to cancel the read operation when there is no more114// listener on the other side to prevent unneeded work.115cts.cancel();116}117});118119const fileStream = this.provider.readFileStream(resource, opts, cts.token);120listenStream(fileStream, {121onData: chunk => emitter.fire(VSBuffer.wrap(chunk)),122onError: error => emitter.fire(error),123onEnd: () => {124125// Forward event126emitter.fire('end');127128// Cleanup129emitter.dispose();130cts.dispose();131}132});133134return emitter.event;135}136137private writeFile(uriTransformer: IURITransformer, _resource: UriComponents, content: VSBuffer, opts: IFileWriteOptions): Promise<void> {138const resource = this.transformIncoming(uriTransformer, _resource);139140return this.provider.writeFile(resource, content.buffer, opts);141}142143private open(uriTransformer: IURITransformer, _resource: UriComponents, opts: IFileOpenOptions): Promise<number> {144const resource = this.transformIncoming(uriTransformer, _resource, true);145146return this.provider.open(resource, opts);147}148149private close(fd: number): Promise<void> {150return this.provider.close(fd);151}152153private async read(fd: number, pos: number, length: number): Promise<[VSBuffer, number]> {154const buffer = VSBuffer.alloc(length);155const bufferOffset = 0; // offset is 0 because we create a buffer to read into for each call156const bytesRead = await this.provider.read(fd, pos, buffer.buffer, bufferOffset, length);157158return [buffer, bytesRead];159}160161private write(fd: number, pos: number, data: VSBuffer, offset: number, length: number): Promise<number> {162return this.provider.write(fd, pos, data.buffer, offset, length);163}164165//#endregion166167//#region Move/Copy/Delete/Create Folder168169private mkdir(uriTransformer: IURITransformer, _resource: UriComponents): Promise<void> {170const resource = this.transformIncoming(uriTransformer, _resource);171172return this.provider.mkdir(resource);173}174175protected delete(uriTransformer: IURITransformer, _resource: UriComponents, opts: IFileDeleteOptions): Promise<void> {176const resource = this.transformIncoming(uriTransformer, _resource);177178return this.provider.delete(resource, opts);179}180181private rename(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents, opts: IFileOverwriteOptions): Promise<void> {182const source = this.transformIncoming(uriTransformer, _source);183const target = this.transformIncoming(uriTransformer, _target);184185return this.provider.rename(source, target, opts);186}187188private copy(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents, opts: IFileOverwriteOptions): Promise<void> {189const source = this.transformIncoming(uriTransformer, _source);190const target = this.transformIncoming(uriTransformer, _target);191192return this.provider.copy(source, target, opts);193}194195//#endregion196197//#region Clone File198199private cloneFile(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents): Promise<void> {200const source = this.transformIncoming(uriTransformer, _source);201const target = this.transformIncoming(uriTransformer, _target);202203return this.provider.cloneFile(source, target);204}205206//#endregion207208//#region File Watching209210private readonly sessionToWatcher = new Map<string /* session ID */, ISessionFileWatcher>();211private readonly watchRequests = new Map<string /* session ID + request ID */, IDisposable>();212213private onFileChange(uriTransformer: IURITransformer, sessionId: string): Event<IFileChange[] | string> {214215// We want a specific emitter for the given session so that events216// from the one session do not end up on the other session. As such217// we create a `SessionFileWatcher` and a `Emitter` for that session.218219const emitter = new Emitter<IFileChange[] | string>({220onWillAddFirstListener: () => {221this.sessionToWatcher.set(sessionId, this.createSessionFileWatcher(uriTransformer, emitter));222},223onDidRemoveLastListener: () => {224dispose(this.sessionToWatcher.get(sessionId));225this.sessionToWatcher.delete(sessionId);226}227});228229return emitter.event;230}231232private async watch(uriTransformer: IURITransformer, sessionId: string, req: number, _resource: UriComponents, opts: IWatchOptions): Promise<void> {233const watcher = this.sessionToWatcher.get(sessionId);234if (watcher) {235const resource = this.transformIncoming(uriTransformer, _resource);236const disposable = watcher.watch(req, resource, opts);237this.watchRequests.set(sessionId + req, disposable);238}239}240241private async unwatch(sessionId: string, req: number): Promise<void> {242const id = sessionId + req;243const disposable = this.watchRequests.get(id);244if (disposable) {245dispose(disposable);246this.watchRequests.delete(id);247}248}249250protected abstract createSessionFileWatcher(uriTransformer: IURITransformer, emitter: Emitter<IFileChange[] | string>): ISessionFileWatcher;251252//#endregion253254override dispose(): void {255super.dispose();256257for (const [, disposable] of this.watchRequests) {258disposable.dispose();259}260this.watchRequests.clear();261262for (const [, disposable] of this.sessionToWatcher) {263disposable.dispose();264}265this.sessionToWatcher.clear();266}267}268269export abstract class AbstractSessionFileWatcher extends Disposable implements ISessionFileWatcher {270271private readonly watcherRequests = new Map<number, IDisposable>();272273// To ensure we use one file watcher per session, we keep a274// disk file system provider instantiated for this session.275// The provider is cheap and only stateful when file watching276// starts.277//278// This is important because we want to ensure that we only279// forward events from the watched paths for this session and280// not other clients that asked to watch other paths.281private readonly fileWatcher: DiskFileSystemProvider;282283constructor(284private readonly uriTransformer: IURITransformer,285sessionEmitter: Emitter<IFileChange[] | string>,286logService: ILogService,287private readonly environmentService: IEnvironmentService288) {289super();290291this.fileWatcher = this._register(new DiskFileSystemProvider(logService));292293this.registerListeners(sessionEmitter);294}295296private registerListeners(sessionEmitter: Emitter<IFileChange[] | string>): void {297const localChangeEmitter = this._register(new Emitter<readonly IFileChange[]>());298299this._register(localChangeEmitter.event((events) => {300sessionEmitter.fire(301events.map(e => ({302resource: this.uriTransformer.transformOutgoingURI(e.resource),303type: e.type,304cId: e.cId305}))306);307}));308309this._register(this.fileWatcher.onDidChangeFile(events => localChangeEmitter.fire(events)));310this._register(this.fileWatcher.onDidWatchError(error => sessionEmitter.fire(error)));311}312313protected getRecursiveWatcherOptions(environmentService: IEnvironmentService): IRecursiveWatcherOptions | undefined {314return undefined; // subclasses can override315}316317protected getExtraExcludes(environmentService: IEnvironmentService): string[] | undefined {318return undefined; // subclasses can override319}320321watch(req: number, resource: URI, opts: IWatchOptions): IDisposable {322const extraExcludes = this.getExtraExcludes(this.environmentService);323if (Array.isArray(extraExcludes)) {324opts.excludes = [...opts.excludes, ...extraExcludes];325}326327this.watcherRequests.set(req, this.fileWatcher.watch(resource, opts));328329return toDisposable(() => {330dispose(this.watcherRequests.get(req));331this.watcherRequests.delete(req);332});333}334335override dispose(): void {336for (const [, disposable] of this.watcherRequests) {337disposable.dispose();338}339this.watcherRequests.clear();340341super.dispose();342}343}344345346