Add channels
This commit is contained in:
parent
4861405683
commit
68fe085aa3
|
@ -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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<IStat> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async open(resource: URI, opts: FileOpenOptions): Promise<number> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async close(fd: number): Promise<void> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async mkdir(resource: URI): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
private copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
private watch(resource: URI, opts: IWatchOptions): Promise<void> {
|
||||||
|
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<any> {
|
||||||
|
throw new Error(`Invalid listen "${event}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public call(_: unknown, command: string, args?: any): Promise<any> {
|
||||||
|
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<IRemoteAgentEnvironment> {
|
||||||
|
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<IDiagnosticInfo> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
private disableTelemetry(): Promise<void> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { Emitter } from "vs/base/common/event";
|
||||||
import { PersistentProtocol, ISocket } from "vs/base/parts/ipc/common/ipc.net";
|
import { PersistentProtocol, ISocket } from "vs/base/parts/ipc/common/ipc.net";
|
||||||
import { VSBuffer } from "vs/base/common/buffer";
|
import { VSBuffer } from "vs/base/common/buffer";
|
||||||
|
|
||||||
|
export interface Server {
|
||||||
|
readonly _onDidClientConnect: Emitter<ClientConnectionEvent>;
|
||||||
|
readonly connections: Map<ConnectionType, Map<string, Connection>>;
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class Connection {
|
export abstract class Connection {
|
||||||
protected readonly _onClose = new Emitter<void>();
|
private readonly _onClose = new Emitter<void>();
|
||||||
public readonly onClose = this._onClose.event;
|
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(() => {
|
this.protocol.onSocketClose(() => {
|
||||||
// TODO: eventually we'll want to clean up the connection if nothing
|
this.timeout = setTimeout(() => {
|
||||||
// ever connects back to it
|
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 {
|
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.beginAcceptReconnection(socket, buffer);
|
||||||
this.protocol.endAcceptReconnection();
|
this.protocol.endAcceptReconnection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The management connection is used for all the IPC channels.
|
||||||
|
*/
|
||||||
export class ManagementConnection extends Connection {
|
export class ManagementConnection extends Connection {
|
||||||
// in here they accept the connection
|
public constructor(server: Server, protocol: PersistentProtocol) {
|
||||||
// to the ipc of the RemoteServer
|
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 {
|
export class ExtensionHostConnection extends Connection {
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { Server } from "./server";
|
||||||
|
|
||||||
|
const server = new Server();
|
||||||
|
server.listen();
|
2
main.js
2
main.js
|
@ -1 +1 @@
|
||||||
require("../../bootstrap-amd").load("vs/server/server");
|
require("../../bootstrap-amd").load("vs/server/entry");
|
||||||
|
|
83
server.ts
83
server.ts
|
@ -5,19 +5,29 @@ import * as path from "path";
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import * as url from "url";
|
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 { Emitter } from "vs/base/common/event";
|
||||||
import { ClientConnectionEvent } from "vs/base/parts/ipc/common/ipc";
|
import { IPCServer, ClientConnectionEvent } from "vs/base/parts/ipc/common/ipc";
|
||||||
import { Socket, Server as IServer } from "vs/server/socket";
|
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,
|
Ok = 200,
|
||||||
NotFound = 404,
|
NotFound = 404,
|
||||||
BadRequest = 400,
|
BadRequest = 400,
|
||||||
}
|
}
|
||||||
|
|
||||||
class HttpError extends Error {
|
export class HttpError extends Error {
|
||||||
public constructor(message: string, public readonly code: number) {
|
public constructor(message: string, public readonly code: number) {
|
||||||
super(message);
|
super(message);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -26,14 +36,24 @@ class HttpError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Server implements IServer {
|
export class Server implements IServer {
|
||||||
private readonly _onDidClientConnect = new Emitter<ClientConnectionEvent>();
|
// 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<ClientConnectionEvent>();
|
||||||
public readonly onDidClientConnect = this._onDidClientConnect.event;
|
public readonly onDidClientConnect = this._onDidClientConnect.event;
|
||||||
|
|
||||||
private readonly rootPath = path.resolve(__dirname, "../../..");
|
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;
|
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<ConnectionType, Map<string, Connection>>();
|
public readonly connections = new Map<ConnectionType, Map<string, Connection>>();
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
|
@ -52,17 +72,45 @@ class Server implements IServer {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.server.on("upgrade", (request, socket) => {
|
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) => {
|
this.server.on("error", (error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let args: ParsedArgs;
|
||||||
|
try {
|
||||||
|
args = parseMainProcessArgv(process.argv);
|
||||||
|
args = validatePaths(args);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.message);
|
||||||
|
return process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
const environmentService = new EnvironmentService(args, process.execPath);
|
||||||
this.connections.clear();
|
|
||||||
|
// 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<string | Buffer> {
|
private async handleRequest(request: http.IncomingMessage): Promise<string | Buffer> {
|
||||||
|
@ -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") {
|
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 = {
|
const options = {
|
||||||
|
@ -144,11 +192,11 @@ class Server implements IServer {
|
||||||
|
|
||||||
const nodeSocket = new Socket(socket, options);
|
const nodeSocket = new Socket(socket, options);
|
||||||
nodeSocket.upgrade(request.headers["sec-websocket-key"] as string);
|
nodeSocket.upgrade(request.headers["sec-websocket-key"] as string);
|
||||||
nodeSocket.handshake(this);
|
|
||||||
|
return nodeSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
public listen(): void {
|
public listen(port: number = 8443): void {
|
||||||
const port = 8443;
|
|
||||||
this.server.listen(port, () => {
|
this.server.listen(port, () => {
|
||||||
const address = this.server.address();
|
const address = this.server.address();
|
||||||
const location = typeof address === "string"
|
const location = typeof address === "string"
|
||||||
|
@ -159,6 +207,3 @@ class Server implements IServer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = new Server();
|
|
||||||
server.listen();
|
|
||||||
|
|
16
socket.ts
16
socket.ts
|
@ -1,10 +1,12 @@
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import * as net from "net";
|
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 { NodeSocket, WebSocketNodeSocket } from "vs/base/parts/ipc/node/ipc.net";
|
||||||
import { PersistentProtocol, ISocket } from "vs/base/parts/ipc/common/ipc.net";
|
import { PersistentProtocol, ISocket } from "vs/base/parts/ipc/common/ipc.net";
|
||||||
import { VSBuffer } from "vs/base/common/buffer";
|
import { AuthRequest, ConnectionType, ConnectionTypeRequest, HandshakeMessage } from "vs/platform/remote/common/remoteAgentConnection";
|
||||||
import { Connection, ExtensionHostConnection, ManagementConnection } from "vs/server/connection";
|
|
||||||
|
import { ExtensionHostConnection, ManagementConnection, Server } from "vs/server/connection";
|
||||||
|
|
||||||
export interface SocketOptions {
|
export interface SocketOptions {
|
||||||
readonly reconnectionToken: string;
|
readonly reconnectionToken: string;
|
||||||
|
@ -12,10 +14,6 @@ export interface SocketOptions {
|
||||||
readonly skipWebSocketFrames: boolean;
|
readonly skipWebSocketFrames: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Server {
|
|
||||||
readonly connections: Map<ConnectionType, Map<string, Connection>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Socket {
|
export class Socket {
|
||||||
private nodeSocket: ISocket;
|
private nodeSocket: ISocket;
|
||||||
public protocol: PersistentProtocol;
|
public protocol: PersistentProtocol;
|
||||||
|
@ -114,8 +112,8 @@ export class Socket {
|
||||||
this.sendControl(ok);
|
this.sendControl(ok);
|
||||||
|
|
||||||
const connection = message.desiredConnectionType === ConnectionType.Management
|
const connection = message.desiredConnectionType === ConnectionType.Management
|
||||||
? new ManagementConnection(this.protocol)
|
? new ManagementConnection(server, this.protocol)
|
||||||
: new ExtensionHostConnection(this.protocol);
|
: new ExtensionHostConnection(server, this.protocol);
|
||||||
|
|
||||||
connections.set(this.options.reconnectionToken, connection);
|
connections.set(this.options.reconnectionToken, connection);
|
||||||
connection.onClose(() => {
|
connection.onClose(() => {
|
||||||
|
|
Loading…
Reference in New Issue