diff --git a/channel.ts b/channel.ts new file mode 100644 index 00000000..075c49a4 --- /dev/null +++ b/channel.ts @@ -0,0 +1,137 @@ +import * as path from "path"; + +import { Emitter, Event } from "vs/base/common/event"; +import { OS } from "vs/base/common/platform"; +import { URI } 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 { IRemoteAgentEnvironment } from "vs/platform/remote/common/remoteAgentEnvironment"; + +/** + * See: src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts. + */ +export class FileProviderChannel implements IServerChannel { + public listen(_context: any, event: string): Event { + switch (event) { + case "filechange": + // TODO: not sure what to do here yet + return new 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 "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]; + } + + throw new Error(`Invalid call "${command}"`); + } + + private async stat(resource: URI): Promise { + throw new Error("not implemented"); + } + + private async open(resource: URI, opts: FileOpenOptions): Promise { + throw new Error("not implemented"); + } + + private async close(fd: number): Promise { + throw new Error("not implemented"); + } + + private async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + throw new Error("not implemented"); + } + + private async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + throw new Error("not implemented"); + } + + private async delete(resource: URI, opts: FileDeleteOptions): Promise { + throw new Error("not implemented"); + } + + private async mkdir(resource: URI): Promise { + throw new Error("not implemented"); + } + + private async readdir(resource: URI): Promise<[string, FileType][]> { + throw new Error("not implemented"); + } + + private async rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise { + throw new Error("not implemented"); + } + + private copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise { + throw new Error("not implemented"); + } + + private watch(resource: URI, opts: IWatchOptions): Promise { + throw new Error("not implemented"); + } + + private unwatch(resource: URI): void { + throw new Error("not implemented"); + } +} + +/** + * See: src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts. + */ +export class ExtensionEnvironmentChannel implements IServerChannel { + public constructor(private readonly environment: IEnvironmentService) {} + + public listen(_context: any, event: string): Event { + throw new Error(`Invalid listen "${event}"`); + } + + public call(_: unknown, command: string, args?: any): Promise { + switch (command) { + case "getEnvironmentData": return this.getEnvironmentData(); + case "getDiagnosticInfo": return this.getDiagnosticInfo(); + case "disableTelemetry": return this.disableTelemetry(); + } + throw new Error(`Invalid call "${command}"`); + } + + private async getEnvironmentData(): Promise { + 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), + extensions: [], // TODO + os: OS, + }; + } + + private getDiagnosticInfo(): Promise { + throw new Error("not implemented"); + } + + private disableTelemetry(): Promise { + throw new Error("not implemented"); + } +} diff --git a/connection.ts b/connection.ts index 3b43fd99..1a25b161 100644 --- a/connection.ts +++ b/connection.ts @@ -1,27 +1,68 @@ +import { ClientConnectionEvent } from "vs/base/parts/ipc/common/ipc"; +import { ConnectionType } from "vs/platform/remote/common/remoteAgentConnection"; import { Emitter } from "vs/base/common/event"; import { PersistentProtocol, ISocket } from "vs/base/parts/ipc/common/ipc.net"; import { VSBuffer } from "vs/base/common/buffer"; +export interface Server { + readonly _onDidClientConnect: Emitter; + readonly connections: Map>; +} + export abstract class Connection { - protected readonly _onClose = new Emitter(); + private readonly _onClose = new Emitter(); public readonly onClose = this._onClose.event; - public constructor(private readonly protocol: PersistentProtocol) { + private timeout: NodeJS.Timeout | undefined; + private readonly wait = 1000 * 60 * 60; + + public constructor( + protected readonly server: Server, + private readonly protocol: PersistentProtocol, + ) { + // onClose seems to mean we want to disconnect, so dispose immediately. + this.protocol.onClose(() => this.dispose()); + + // If the socket closes, we want to wait before disposing so we can + // reconnect. this.protocol.onSocketClose(() => { - // TODO: eventually we'll want to clean up the connection if nothing - // ever connects back to it + this.timeout = setTimeout(() => { + this.dispose(); + }, this.wait); }); } + /** + * Completely close and clean up the connection. Should only do this once we + * don't need or want the connection. It cannot be re-used after this. + */ + public dispose(): void { + this.protocol.sendDisconnect(); + this.protocol.getSocket().end(); + this.protocol.dispose(); + this._onClose.fire(); + } + public reconnect(socket: ISocket, buffer: VSBuffer): void { + clearTimeout(this.timeout as any); // Not sure why the type doesn't work. this.protocol.beginAcceptReconnection(socket, buffer); this.protocol.endAcceptReconnection(); } } +/** + * The management connection is used for all the IPC channels. + */ export class ManagementConnection extends Connection { - // in here they accept the connection - // to the ipc of the RemoteServer + public constructor(server: Server, protocol: PersistentProtocol) { + super(server, protocol); + // This will communicate back to the IPCServer that a new client has + // connected. + this.server._onDidClientConnect.fire({ + protocol, + onDidClientDisconnect: this.onClose, + }); + } } export class ExtensionHostConnection extends Connection { diff --git a/entry.ts b/entry.ts new file mode 100644 index 00000000..5990806f --- /dev/null +++ b/entry.ts @@ -0,0 +1,4 @@ +import { Server } from "./server"; + +const server = new Server(); +server.listen(); diff --git a/main.js b/main.js index 0fc2dbf9..6bdb6c03 100644 --- a/main.js +++ b/main.js @@ -1 +1 @@ -require("../../bootstrap-amd").load("vs/server/server"); +require("../../bootstrap-amd").load("vs/server/entry"); diff --git a/server.ts b/server.ts index ca9f114b..f7728973 100644 --- a/server.ts +++ b/server.ts @@ -5,19 +5,29 @@ import * as path from "path"; import * as util from "util"; import * as url from "url"; -import { Connection } from "vs/server/connection"; -import { ConnectionType } from "vs/platform/remote/common/remoteAgentConnection"; import { Emitter } from "vs/base/common/event"; -import { ClientConnectionEvent } from "vs/base/parts/ipc/common/ipc"; -import { Socket, Server as IServer } from "vs/server/socket"; +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"; +import { ParsedArgs } from "vs/platform/environment/common/environment"; +import { EnvironmentService } from "vs/platform/environment/node/environmentService"; +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 { ConnectionType } from "vs/platform/remote/common/remoteAgentConnection"; +import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from "vs/platform/remote/common/remoteAgentFileSystemChannel"; -enum HttpCode { +import { Connection, Server as IServer } from "vs/server/connection"; +import { ExtensionEnvironmentChannel, FileProviderChannel } from "vs/server/channel"; +import { Socket } from "vs/server/socket"; + +export enum HttpCode { Ok = 200, NotFound = 404, BadRequest = 400, } -class HttpError extends Error { +export class HttpError extends Error { public constructor(message: string, public readonly code: number) { super(message); // @ts-ignore @@ -26,14 +36,24 @@ class HttpError extends Error { } } -class Server implements IServer { - private readonly _onDidClientConnect = new Emitter(); +export class Server implements IServer { + // When a new client connects, it will fire this event which is used in the + // IPC server which manages channels. + public readonly _onDidClientConnect = new Emitter(); public readonly onDidClientConnect = this._onDidClientConnect.event; private readonly rootPath = path.resolve(__dirname, "../../.."); + // This is separate instead of just extending this class since we can't + // use properties in the super call. This manages channels. + private readonly ipc = new IPCServer(this.onDidClientConnect); + + // The web server. private readonly server: http.Server; + // 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. public readonly connections = new Map>(); public constructor() { @@ -52,17 +72,45 @@ class Server implements IServer { }); this.server.on("upgrade", (request, socket) => { - this.handleUpgrade(request, socket); + try { + const nodeSocket = this.handleUpgrade(request, socket); + nodeSocket.handshake(this); + } catch (error) { + socket.end(error.message); + } }); this.server.on("error", (error) => { console.error(error); process.exit(1); }); - } - public dispose(): void { - this.connections.clear(); + let args: ParsedArgs; + try { + args = parseMainProcessArgv(process.argv); + args = validatePaths(args); + } catch (error) { + console.error(error.message); + return process.exit(1); + } + + const environmentService = new EnvironmentService(args, process.execPath); + + // TODO: might want to use spdlog. + const logService = new ConsoleLogMainService(); + this.ipc.registerChannel("loglevel", new LogLevelSetterChannel(logService)); + + const instantiationService = new InstantiationService(); + instantiationService.invokeFunction(() => { + this.ipc.registerChannel( + REMOTE_FILE_SYSTEM_CHANNEL_NAME, + new FileProviderChannel(), + ); + this.ipc.registerChannel( + "remoteextensionsenvironment", + new ExtensionEnvironmentChannel(environmentService), + ); + }); } private async handleRequest(request: http.IncomingMessage): Promise { @@ -118,9 +166,9 @@ class Server implements IServer { } } - private handleUpgrade(request: http.IncomingMessage, socket: net.Socket): void { + private handleUpgrade(request: http.IncomingMessage, socket: net.Socket): Socket { if (request.headers.upgrade !== "websocket") { - return socket.end("HTTP/1.1 400 Bad Request"); + throw new Error("HTTP/1.1 400 Bad Request"); } const options = { @@ -144,11 +192,11 @@ class Server implements IServer { const nodeSocket = new Socket(socket, options); nodeSocket.upgrade(request.headers["sec-websocket-key"] as string); - nodeSocket.handshake(this); + + return nodeSocket; } - public listen(): void { - const port = 8443; + public listen(port: number = 8443): void { this.server.listen(port, () => { const address = this.server.address(); const location = typeof address === "string" @@ -159,6 +207,3 @@ class Server implements IServer { }); } } - -const server = new Server(); -server.listen(); diff --git a/socket.ts b/socket.ts index 4c56e854..8c3ee546 100644 --- a/socket.ts +++ b/socket.ts @@ -1,10 +1,12 @@ import * as crypto from "crypto"; import * as net from "net"; -import { AuthRequest, ConnectionType, ConnectionTypeRequest, HandshakeMessage } from "vs/platform/remote/common/remoteAgentConnection"; + +import { VSBuffer } from "vs/base/common/buffer"; import { NodeSocket, WebSocketNodeSocket } from "vs/base/parts/ipc/node/ipc.net"; import { PersistentProtocol, ISocket } from "vs/base/parts/ipc/common/ipc.net"; -import { VSBuffer } from "vs/base/common/buffer"; -import { Connection, ExtensionHostConnection, ManagementConnection } from "vs/server/connection"; +import { AuthRequest, ConnectionType, ConnectionTypeRequest, HandshakeMessage } from "vs/platform/remote/common/remoteAgentConnection"; + +import { ExtensionHostConnection, ManagementConnection, Server } from "vs/server/connection"; export interface SocketOptions { readonly reconnectionToken: string; @@ -12,10 +14,6 @@ export interface SocketOptions { readonly skipWebSocketFrames: boolean; } -export interface Server { - readonly connections: Map>; -} - export class Socket { private nodeSocket: ISocket; public protocol: PersistentProtocol; @@ -114,8 +112,8 @@ export class Socket { this.sendControl(ok); const connection = message.desiredConnectionType === ConnectionType.Management - ? new ManagementConnection(this.protocol) - : new ExtensionHostConnection(this.protocol); + ? new ManagementConnection(server, this.protocol) + : new ExtensionHostConnection(server, this.protocol); connections.set(this.options.reconnectionToken, connection); connection.onClose(() => {