From a0121f2f0c77b4846a7c7736ef3a03239d81b452 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 1 Jul 2019 14:00:11 -0500 Subject: [PATCH] Implement file provider Reading, watching, saving, etc all seem to work now. --- channel.ts | 139 ++++++++++++++++++++++++++++++++++++++--------------- server.ts | 36 ++++++++------ 2 files changed, 122 insertions(+), 53 deletions(-) diff --git a/channel.ts b/channel.ts index 075c49a4..055baea8 100644 --- a/channel.ts +++ b/channel.ts @@ -1,94 +1,153 @@ import * as path from "path"; +import { VSBuffer } from "vs/base/common/buffer"; import { Emitter, Event } from "vs/base/common/event"; +import { IDisposable } from "vs/base/common/lifecycle"; +import { Schemas } from "vs/base/common/network"; import { OS } from "vs/base/common/platform"; -import { URI } from "vs/base/common/uri"; +import { URI, UriComponents } from "vs/base/common/uri"; import { IServerChannel } from "vs/base/parts/ipc/common/ipc"; import { IDiagnosticInfo } from "vs/platform/diagnostics/common/diagnosticsService"; import { IEnvironmentService } from "vs/platform/environment/common/environment"; import { FileDeleteOptions, FileOverwriteOptions, FileType, IStat, IWatchOptions, FileOpenOptions } from "vs/platform/files/common/files"; +import { ILogService } from "vs/platform/log/common/log"; import { IRemoteAgentEnvironment } from "vs/platform/remote/common/remoteAgentEnvironment"; +import { DiskFileSystemProvider } from "vs/workbench/services/files/node/diskFileSystemProvider"; + +/** + * Extend the file provider to allow unwatching. + */ +class Watcher extends DiskFileSystemProvider { + public readonly watches = new Map(); + + public dispose(): void { + this.watches.forEach((w) => w.dispose()); + this.watches.clear(); + super.dispose(); + } + + public _watch(req: number, resource: URI, opts: IWatchOptions): void { + this.watches.set(req, this.watch(resource, opts)); + } + + public unwatch(req: number): void { + this.watches.get(req)!.dispose(); + this.watches.delete(req); + } +} /** * See: src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts. */ export class FileProviderChannel implements IServerChannel { - public listen(_context: any, event: string): Event { + private readonly provider: DiskFileSystemProvider; + private readonly watchers = new Map(); + + public constructor(private readonly logService: ILogService) { + this.provider = new DiskFileSystemProvider(this.logService); + } + + public listen(_: unknown, event: string, args?: any): Event { switch (event) { + // This is where the actual file changes are sent. The watch method just + // adds things that will fire here. That means we have to split up + // watchers based on the session otherwise sessions would get events for + // other sessions. There is also no point in having the watcher unless + // something is listening. I'm not sure there is a different way to + // dispose, anyway. case "filechange": - // TODO: not sure what to do here yet - return new Emitter().event; + const session = args[0]; + const emitter = new Emitter({ + onFirstListenerAdd: () => { + const provider = new Watcher(this.logService); + this.watchers.set(session, provider); + provider.onDidChangeFile((events) => { + emitter.fire(events.map((event) => ({ + ...event, + resource: event.resource.with({ scheme: Schemas.vscodeRemote }), + }))); + }); + provider.onDidErrorOccur((event) => emitter.fire(event)); + }, + onLastListenerRemove: () => { + this.watchers.get(session)!.dispose(); + this.watchers.delete(session); + }, + }); + + return emitter.event; } throw new Error(`Invalid listen "${event}"`); } public call(_: unknown, command: string, args?: any): Promise { - console.log("got call", command, args); switch (command) { case "stat": return this.stat(args[0]); case "open": return this.open(args[0], args[1]); case "close": return this.close(args[0]); - case "read": return this.read(args[0], args[1], args[2], args[3], args[4]); + case "read": return this.read(args[0], args[1], args[2]); case "write": return this.write(args[0], args[1], args[2], args[3], args[4]); case "delete": return this.delete(args[0], args[1]); case "mkdir": return this.mkdir(args[0]); case "readdir": return this.readdir(args[0]); case "rename": return this.rename(args[0], args[1], args[2]); case "copy": return this.copy(args[0], args[1], args[2]); - case "watch": return this.watch(args[0], args[1]); - case "unwatch": return this.unwatch(args[0]), args[1]; + case "watch": return this.watch(args[0], args[1], args[2], args[3]); + case "unwatch": return this.unwatch(args[0], args[1]); } throw new Error(`Invalid call "${command}"`); } - private async stat(resource: URI): Promise { - throw new Error("not implemented"); + private async stat(resource: UriComponents): Promise { + return this.provider.stat(URI.from(resource)); } - private async open(resource: URI, opts: FileOpenOptions): Promise { - throw new Error("not implemented"); + private async open(resource: UriComponents, opts: FileOpenOptions): Promise { + return this.provider.open(URI.from(resource), opts); } private async close(fd: number): Promise { - throw new Error("not implemented"); + return this.provider.close(fd); } - private async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { - throw new Error("not implemented"); + private async read(fd: number, pos: number, length: number): Promise<[VSBuffer, number]> { + const buffer = VSBuffer.alloc(length); + const bytesRead = await this.provider.read(fd, pos, buffer.buffer, 0, length); + return [buffer, bytesRead]; } - private async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { - throw new Error("not implemented"); + private write(fd: number, pos: number, buffer: VSBuffer, offset: number, length: number): Promise { + return this.provider.write(fd, pos, buffer.buffer, offset, length); } - private async delete(resource: URI, opts: FileDeleteOptions): Promise { - throw new Error("not implemented"); + private async delete(resource: UriComponents, opts: FileDeleteOptions): Promise { + return this.provider.delete(URI.from(resource), opts); } - private async mkdir(resource: URI): Promise { - throw new Error("not implemented"); + private async mkdir(resource: UriComponents): Promise { + return this.provider.mkdir(URI.from(resource)); } - private async readdir(resource: URI): Promise<[string, FileType][]> { - throw new Error("not implemented"); + private async readdir(resource: UriComponents): Promise<[string, FileType][]> { + return this.provider.readdir(URI.from(resource)); } - private async rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise { - throw new Error("not implemented"); + private async rename(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise { + return this.provider.rename(URI.from(resource), URI.from(target), opts); } - private copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise { - throw new Error("not implemented"); + private copy(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise { + return this.provider.copy(URI.from(resource), URI.from(target), opts); } - private watch(resource: URI, opts: IWatchOptions): Promise { - throw new Error("not implemented"); + private async watch(session: string, req: number, resource: UriComponents, opts: IWatchOptions): Promise { + this.watchers.get(session)!._watch(req, URI.from(resource), opts); } - private unwatch(resource: URI): void { - throw new Error("not implemented"); + private async unwatch(session: string, req: number): Promise { + this.watchers.get(session)!.unwatch(req); } } @@ -112,16 +171,18 @@ export class ExtensionEnvironmentChannel implements IServerChannel { } private async getEnvironmentData(): Promise { + // TODO: this `with` stuff feels a bit jank. + // Maybe it should already come in like this instead. return { pid: process.pid, - appRoot: URI.file(this.environment.appRoot), - appSettingsHome: this.environment.appSettingsHome, - settingsPath: this.environment.machineSettingsHome, - logsPath: URI.file(this.environment.logsPath), - extensionsPath: URI.file(this.environment.extensionsPath), - extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, "extension-host")), // TODO - globalStorageHome: URI.file(this.environment.globalStorageHome), - userHome: URI.file(this.environment.userHome), + appRoot: URI.file(this.environment.appRoot).with({ scheme: Schemas.vscodeRemote }), + appSettingsHome: this.environment.appSettingsHome.with({ scheme: Schemas.vscodeRemote }), + settingsPath: this.environment.machineSettingsHome.with({ scheme: Schemas.vscodeRemote }), + logsPath: URI.file(this.environment.logsPath).with({ scheme: Schemas.vscodeRemote }), + extensionsPath: URI.file(this.environment.extensionsPath).with({ scheme: Schemas.vscodeRemote }), + extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, "extension-host")).with({ scheme: Schemas.vscodeRemote }), // TODO + globalStorageHome: URI.file(this.environment.globalStorageHome).with({ scheme: Schemas.vscodeRemote }), + userHome: URI.file(this.environment.userHome).with({ scheme: Schemas.vscodeRemote }), extensions: [], // TODO os: OS, }; diff --git a/server.ts b/server.ts index 29ff9dbc..b7a04e9a 100644 --- a/server.ts +++ b/server.ts @@ -7,7 +7,9 @@ import * as url from "url"; import { Emitter } from "vs/base/common/event"; import { getMediaMime } from "vs/base/common/mime"; +import { Schemas } from "vs/base/common/network"; import { extname } from "vs/base/common/path"; +import { URI } from "vs/base/common/uri"; import { IPCServer, ClientConnectionEvent } from "vs/base/parts/ipc/common/ipc"; import { validatePaths } from "vs/code/node/paths"; import { parseMainProcessArgv } from "vs/platform/environment/node/argvHelper"; @@ -16,8 +18,10 @@ import { EnvironmentService } from "vs/platform/environment/node/environmentServ import { InstantiationService } from "vs/platform/instantiation/common/instantiationService"; import { ConsoleLogMainService } from "vs/platform/log/common/log"; import { LogLevelSetterChannel } from "vs/platform/log/common/logIpc"; +import { IProductConfiguration } from "vs/platform/product/common/product"; import { ConnectionType } from "vs/platform/remote/common/remoteAgentConnection"; import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from "vs/platform/remote/common/remoteAgentFileSystemChannel"; +import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api"; import { Connection, Server as IServer } from "vs/server/connection"; import { ExtensionEnvironmentChannel, FileProviderChannel } from "vs/server/channel"; @@ -29,6 +33,13 @@ export enum HttpCode { BadRequest = 400, } +export interface Options { + WORKBENCH_WEB_CONGIGURATION: IWorkbenchConstructionOptions; + REMOTE_USER_DATA_URI: URI; + PRODUCT_CONFIGURATION: IProductConfiguration | null; + CONNECTION_AUTH_TOKEN: string; +} + export class HttpError extends Error { public constructor(message: string, public readonly code: number) { super(message); @@ -53,6 +64,8 @@ export class Server implements IServer { // The web server. private readonly server: http.Server; + private readonly environmentService: EnvironmentService; + // Persistent connections. These can reconnect within a timeout. Individual // sockets will add connections made through them to this map and remove them // when they close. @@ -97,7 +110,7 @@ export class Server implements IServer { return process.exit(1); } - const environmentService = new EnvironmentService(args, process.execPath); + this.environmentService = new EnvironmentService(args, process.execPath); // TODO: might want to use spdlog. const logService = new ConsoleLogMainService(); @@ -107,11 +120,11 @@ export class Server implements IServer { instantiationService.invokeFunction(() => { this.ipc.registerChannel( REMOTE_FILE_SYSTEM_CHANNEL_NAME, - new FileProviderChannel(), + new FileProviderChannel(logService), ); this.ipc.registerChannel( "remoteextensionsenvironment", - new ExtensionEnvironmentChannel(environmentService), + new ExtensionEnvironmentChannel(this.environmentService), ); }); } @@ -133,25 +146,20 @@ export class Server implements IServer { let html = await util.promisify(fs.readFile)(htmlPath, "utf8"); - const options = { - WEBVIEW_ENDPOINT: {}, + const options: Options = { WORKBENCH_WEB_CONGIGURATION: { - remoteAuthority: request.headers.host, + remoteAuthority: request.headers.host as string, }, - REMOTE_USER_DATA_URI: { - scheme: "http", - authority: request.headers.host, - path: "/", - }, - PRODUCT_CONFIGURATION: {}, - CONNECTION_AUTH_TOKEN: {} + REMOTE_USER_DATA_URI: this.environmentService.webUserDataHome.with({ scheme: Schemas.vscodeRemote }), + PRODUCT_CONFIGURATION: null, + CONNECTION_AUTH_TOKEN: "", }; Object.keys(options).forEach((key) => { html = html.replace(`"{{${key}}}"`, `'${JSON.stringify(options[key])}'`); }); - html = html.replace('{{WEBVIEW_ENDPOINT}}', JSON.stringify(options.WEBVIEW_ENDPOINT)); + html = html.replace('{{WEBVIEW_ENDPOINT}}', ""); // TODO return [html, { "Content-Type": "text/html",