vscode/src/vs/server/remoteAgentFileSystemImpl.ts
2021-10-20 18:42:13 +02:00

306 lines
12 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IURITransformer } from 'vs/base/common/uriIpc';
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { FileDeleteOptions, FileOverwriteOptions, FileType, IFileChange, IStat, IWatchOptions, FileOpenOptions, FileWriteOptions, FileReadStreamOptions } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer';
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { DiskFileSystemProvider, IWatcherOptions } from 'vs/platform/files/node/diskFileSystemProvider';
import { VSBuffer } from 'vs/base/common/buffer';
import { posix, delimiter } from 'vs/base/common/path';
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<number, IDisposable>();
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<IFileChange[] | string>
) {
super();
this.registerListeners(emitter);
}
private registerListeners(emitter: Emitter<IFileChange[] | string>): void {
const localChangeEmitter = this._register(new Emitter<readonly IFileChange[]>());
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<RemoteAgentConnectionContext> {
private readonly BUFFER_SIZE = 256 * 1024; // slightly larger to reduce remote-communication overhead
private readonly uriTransformerCache = new Map<string, IURITransformer>();
private readonly fileWatchers = new Map<string, SessionFileWatcher>();
private readonly fsProvider = this._register(new DiskFileSystemProvider(this.logService, { bufferSize: this.BUFFER_SIZE }));
private readonly watchRequests = new Map<string, IDisposable>();
constructor(
private readonly logService: ILogService,
private readonly environmentService: IServerEnvironmentService
) {
super();
}
call(ctx: RemoteAgentConnectionContext, command: string, arg?: any): Promise<any> {
const uriTransformer = this.getUriTransformer(ctx.remoteAuthority);
switch (command) {
case 'stat': return this.stat(uriTransformer, arg[0]);
case 'readdir': return this.readdir(uriTransformer, arg[0]);
case 'open': return this.open(uriTransformer, arg[0], arg[1]);
case 'close': return this.close(arg[0]);
case 'read': return this.read(arg[0], arg[1], arg[2]);
case 'readFile': return this.readFile(uriTransformer, arg[0]);
case 'write': return this.write(arg[0], arg[1], arg[2], arg[3], arg[4]);
case 'writeFile': return this.writeFile(uriTransformer, arg[0], arg[1], arg[2]);
case 'rename': return this.rename(uriTransformer, arg[0], arg[1], arg[2]);
case 'copy': return this.copy(uriTransformer, arg[0], arg[1], arg[2]);
case 'mkdir': return this.mkdir(uriTransformer, arg[0]);
case 'delete': return this.delete(uriTransformer, arg[0], arg[1]);
case 'watch': return Promise.resolve(this.watch(arg[0], arg[1], arg[2], arg[3]));
case 'unwatch': return Promise.resolve(this.unwatch(arg[0], arg[1]));
}
throw new Error(`IPC Command ${command} not found`);
}
listen(ctx: RemoteAgentConnectionContext, event: string, arg: any): Event<any> {
const uriTransformer = this.getUriTransformer(ctx.remoteAuthority);
switch (event) {
case 'filechange': return this.onFileChange(uriTransformer, arg[0]);
case 'readFileStream': return this.onReadFileStream(uriTransformer, arg[0], arg[1]);
}
throw new Error(`Unknown event ${event}`);
}
private onFileChange(uriTransformer: IURITransformer, session: string): Event<IFileChange[] | string> {
const emitter = new Emitter<IFileChange[] | string>({
onFirstListenerAdd: () => {
this.fileWatchers.set(session, new SessionFileWatcher(this.logService, this.environmentService, uriTransformer, emitter));
},
onLastListenerRemove: () => {
dispose(this.fileWatchers.get(session));
this.fileWatchers.delete(session);
}
});
return emitter.event;
}
private onReadFileStream(uriTransformer: IURITransformer, _resource: URI, opts: FileReadStreamOptions): Event<ReadableStreamEventPayload<VSBuffer>> {
const resource = this.transformIncoming(uriTransformer, _resource, true);
const cancellableSource = new CancellationTokenSource();
const emitter = new Emitter<ReadableStreamEventPayload<VSBuffer>>({
onLastListenerRemove: () => {
// Ensure to cancel the read operation when there is no more
// listener on the other side to prevent unneeded work.
cancellableSource.cancel();
}
});
const fileStream = this.fsProvider.readFileStream(resource, opts, cancellableSource.token);
listenStream(fileStream, {
onData: chunk => emitter.fire(VSBuffer.wrap(chunk)),
onError: error => emitter.fire(error),
onEnd: () => {
emitter.fire('end');
// Cleanup
emitter.dispose();
cancellableSource.dispose();
}
});
return emitter.event;
}
private getUriTransformer(remoteAuthority: string): IURITransformer {
let transformer = this.uriTransformerCache.get(remoteAuthority);
if (!transformer) {
transformer = createRemoteURITransformer(remoteAuthority);
this.uriTransformerCache.set(remoteAuthority, transformer);
}
return transformer;
}
private stat(uriTransformer: IURITransformer, _resource: UriComponents): Promise<IStat> {
const resource = this.transformIncoming(uriTransformer, _resource, true);
return this.fsProvider.stat(resource);
}
private readdir(uriTransformer: IURITransformer, _resource: UriComponents): Promise<[string, FileType][]> {
const resource = this.transformIncoming(uriTransformer, _resource);
return this.fsProvider.readdir(resource);
}
private open(uriTransformer: IURITransformer, _resource: UriComponents, opts: FileOpenOptions): Promise<number> {
const resource = this.transformIncoming(uriTransformer, _resource, true);
return this.fsProvider.open(resource, opts);
}
private close(_fd: number): Promise<void> {
return this.fsProvider.close(_fd);
}
private async read(fd: number, pos: number, length: number): Promise<[VSBuffer, number]> {
const buffer = VSBuffer.alloc(length);
const bufferOffset = 0; // offset is 0 because we create a buffer to read into for each call
const bytesRead = await this.fsProvider.read(fd, pos, buffer.buffer, bufferOffset, length);
return [buffer, bytesRead];
}
private async readFile(uriTransformer: IURITransformer, _resource: UriComponents): Promise<VSBuffer> {
const resource = this.transformIncoming(uriTransformer, _resource, true);
const buff = await this.fsProvider.readFile(resource);
return VSBuffer.wrap(buff);
}
private write(fd: number, pos: number, data: VSBuffer, offset: number, length: number): Promise<number> {
return this.fsProvider.write(fd, pos, data.buffer, offset, length);
}
private writeFile(uriTransformer: IURITransformer, _resource: UriComponents, content: VSBuffer, opts: FileWriteOptions): Promise<void> {
const resource = this.transformIncoming(uriTransformer, _resource);
return this.fsProvider.writeFile(resource, content.buffer, opts);
}
private rename(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
const source = URI.revive(uriTransformer.transformIncoming(_source));
const target = URI.revive(uriTransformer.transformIncoming(_target));
return this.fsProvider.rename(source, target, opts);
}
private copy(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
const source = this.transformIncoming(uriTransformer, _source);
const target = this.transformIncoming(uriTransformer, _target);
return this.fsProvider.copy(source, target, opts);
}
private mkdir(uriTransformer: IURITransformer, _resource: UriComponents): Promise<void> {
const resource = this.transformIncoming(uriTransformer, _resource);
return this.fsProvider.mkdir(resource);
}
private delete(uriTransformer: IURITransformer, _resource: UriComponents, opts: FileDeleteOptions): Promise<void> {
const resource = this.transformIncoming(uriTransformer, _resource);
return this.fsProvider.delete(resource, opts);
}
private transformIncoming(uriTransformer: IURITransformer, _resource: UriComponents, supportVSCodeResource = false): URI {
if (supportVSCodeResource && _resource.path === '/vscode-resource' && _resource.query) {
const requestResourcePath = JSON.parse(_resource.query).requestResourcePath;
return URI.from({ scheme: 'file', path: requestResourcePath });
}
return URI.revive(uriTransformer.transformIncoming(_resource));
}
private watch(session: string, req: number, _resource: UriComponents, opts: IWatchOptions): void {
const id = session + req;
const watcher = this.fileWatchers.get(session);
if (watcher) {
const disposable = watcher.watch(req, _resource, opts);
this.watchRequests.set(id, disposable);
}
}
private unwatch(session: string, req: number): void {
const id = session + req;
const disposable = this.watchRequests.get(id);
if (disposable) {
dispose(disposable);
this.watchRequests.delete(id);
}
}
override dispose(): void {
super.dispose();
this.watchRequests.forEach(disposable => dispose(disposable));
this.watchRequests.clear();
this.fileWatchers.forEach(disposable => dispose(disposable));
this.fileWatchers.clear();
}
}