From d827015b402eadd140ae0ac86e041f291fcabeea Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 18 Jan 2019 17:08:44 -0600 Subject: [PATCH] Add shared process active message (#16) * Add shared process active message * Add client function for calling bootstrap fork --- packages/protocol/src/browser/client.ts | 12 +- packages/protocol/src/node/command.ts | 17 ++- packages/protocol/src/node/server.ts | 9 +- packages/protocol/src/proto/client.proto | 5 +- packages/protocol/src/proto/client_pb.d.ts | 14 +-- packages/protocol/src/proto/client_pb.js | 102 ++++++++--------- packages/protocol/src/proto/command.proto | 3 + packages/protocol/src/proto/command_pb.d.ts | 4 + packages/protocol/src/proto/command_pb.js | 31 +++++- packages/protocol/src/proto/vscode.proto | 9 +- packages/protocol/src/proto/vscode_pb.d.ts | 28 ++--- packages/protocol/src/proto/vscode_pb.js | 116 ++++++-------------- packages/server/src/cli.ts | 43 +++++--- packages/server/src/server.ts | 42 +++---- packages/server/src/vscode/sharedProcess.ts | 66 +++++++---- 15 files changed, 260 insertions(+), 241 deletions(-) diff --git a/packages/protocol/src/browser/client.ts b/packages/protocol/src/browser/client.ts index 37545b4d..91250336 100644 --- a/packages/protocol/src/browser/client.ts +++ b/packages/protocol/src/browser/client.ts @@ -155,6 +155,15 @@ export class Client { return this.doSpawn(modulePath, args, options, true); } + /** + * VS Code specific. + * Forks a module from bootstrap-fork + * @param modulePath Path of the module + */ + public bootstrapFork(modulePath: string): ChildProcess { + return this.doSpawn(modulePath, [], undefined, true, true); + } + public createConnection(path: string, callback?: () => void): Socket; public createConnection(port: number, callback?: () => void): Socket; public createConnection(target: string | number, callback?: () => void): Socket { @@ -176,13 +185,14 @@ export class Client { return socket; } - private doSpawn(command: string, args: string[] = [], options?: SpawnOptions, isFork: boolean = false): ChildProcess { + private doSpawn(command: string, args: string[] = [], options?: SpawnOptions, isFork: boolean = false, isBootstrapFork: boolean = true): ChildProcess { const id = this.sessionId++; const newSess = new NewSessionMessage(); newSess.setId(id); newSess.setCommand(command); newSess.setArgsList(args); newSess.setIsFork(isFork); + newSess.setIsBootstrapFork(isBootstrapFork); if (options) { if (options.cwd) { newSess.setCwd(options.cwd); diff --git a/packages/protocol/src/node/command.ts b/packages/protocol/src/node/command.ts index b19d54fe..7b849e87 100644 --- a/packages/protocol/src/node/command.ts +++ b/packages/protocol/src/node/command.ts @@ -5,6 +5,7 @@ import * as stream from "stream"; import { TextEncoder } from "text-encoding"; import { NewSessionMessage, ServerMessage, SessionDoneMessage, SessionOutputMessage, ShutdownSessionMessage, IdentifySessionMessage, ClientMessage, NewConnectionMessage, ConnectionEstablishedMessage, NewConnectionFailureMessage, ConnectionCloseMessage, ConnectionOutputMessage } from "../proto"; import { SendableConnection } from "../common/connection"; +import { ServerOptions } from "./server"; export interface Process { stdin?: stream.Writable; @@ -22,7 +23,7 @@ export interface Process { title?: number; } -export const handleNewSession = (connection: SendableConnection, newSession: NewSessionMessage, onExit: () => void): Process => { +export const handleNewSession = (connection: SendableConnection, newSession: NewSessionMessage, serverOptions: ServerOptions | undefined, onExit: () => void): Process => { let process: Process; const env = {} as any; @@ -44,7 +45,15 @@ export const handleNewSession = (connection: SendableConnection, newSession: New }; let proc: cp.ChildProcess; if (newSession.getIsFork()) { - proc = cp.fork(newSession.getCommand(), newSession.getArgsList()); + if (!serverOptions) { + throw new Error("No forkProvider set for bootstrap-fork request"); + } + + if (!serverOptions.forkProvider) { + throw new Error("No forkProvider set for server options"); + } + + proc = serverOptions.forkProvider(newSession); } else { proc = cp.spawn(newSession.getCommand(), newSession.getArgsList(), options); } @@ -107,7 +116,7 @@ export const handleNewSession = (connection: SendableConnection, newSession: New export const handleNewConnection = (connection: SendableConnection, newConnection: NewConnectionMessage, onExit: () => void): net.Socket => { const id = newConnection.getId(); - let socket: net.Socket; + let socket: net.Socket; let didConnect = false; const connectCallback = () => { didConnect = true; @@ -134,7 +143,7 @@ export const handleNewConnection = (connection: SendableConnection, newConnectio const servMsg = new ServerMessage(); servMsg.setConnectionFailure(errMsg); connection.send(servMsg.serializeBinary()); - + onExit(); } }); diff --git a/packages/protocol/src/node/server.ts b/packages/protocol/src/node/server.ts index 251b5240..2c9f5b32 100644 --- a/packages/protocol/src/node/server.ts +++ b/packages/protocol/src/node/server.ts @@ -1,10 +1,11 @@ import * as os from "os"; +import * as cp from "child_process"; import * as path from "path"; import { mkdir } from "fs"; import { promisify } from "util"; import { TextDecoder } from "text-encoding"; import { logger, field } from "@coder/logger"; -import { ClientMessage, WorkingInitMessage, ServerMessage } from "../proto"; +import { ClientMessage, WorkingInitMessage, ServerMessage, NewSessionMessage } from "../proto"; import { evaluate } from "./evaluate"; import { ReadWriteConnection } from "../common/connection"; import { Process, handleNewSession, handleNewConnection } from "./command"; @@ -13,6 +14,8 @@ import * as net from "net"; export interface ServerOptions { readonly workingDirectory: string; readonly dataDirectory: string; + + forkProvider?(message: NewSessionMessage): cp.ChildProcess; } export class Server { @@ -22,7 +25,7 @@ export class Server { public constructor( private readonly connection: ReadWriteConnection, - options?: ServerOptions, + private readonly options?: ServerOptions, ) { connection.onMessage((data) => { try { @@ -89,7 +92,7 @@ export class Server { if (message.hasNewEval()) { evaluate(this.connection, message.getNewEval()!); } else if (message.hasNewSession()) { - const session = handleNewSession(this.connection, message.getNewSession()!, () => { + const session = handleNewSession(this.connection, message.getNewSession()!, this.options, () => { this.sessions.delete(message.getNewSession()!.getId()); }); this.sessions.set(message.getNewSession()!.getId(), session); diff --git a/packages/protocol/src/proto/client.proto b/packages/protocol/src/proto/client.proto index 76eb79d5..851e1a5c 100644 --- a/packages/protocol/src/proto/client.proto +++ b/packages/protocol/src/proto/client.proto @@ -17,8 +17,6 @@ message ClientMessage { // node.proto NewEvalMessage new_eval = 9; - - SharedProcessInitMessage shared_process_init = 10; } } @@ -39,6 +37,9 @@ message ServerMessage { EvalDoneMessage eval_done = 10; WorkingInitMessage init = 11; + + // vscode.proto + SharedProcessActiveMessage shared_process_active = 12; } } diff --git a/packages/protocol/src/proto/client_pb.d.ts b/packages/protocol/src/proto/client_pb.d.ts index 128ccb11..4bdce8ea 100644 --- a/packages/protocol/src/proto/client_pb.d.ts +++ b/packages/protocol/src/proto/client_pb.d.ts @@ -52,11 +52,6 @@ export class ClientMessage extends jspb.Message { getNewEval(): node_pb.NewEvalMessage | undefined; setNewEval(value?: node_pb.NewEvalMessage): void; - hasSharedProcessInit(): boolean; - clearSharedProcessInit(): void; - getSharedProcessInit(): vscode_pb.SharedProcessInitMessage | undefined; - setSharedProcessInit(value?: vscode_pb.SharedProcessInitMessage): void; - getMsgCase(): ClientMessage.MsgCase; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ClientMessage.AsObject; @@ -79,7 +74,6 @@ export namespace ClientMessage { connectionOutput?: command_pb.ConnectionOutputMessage.AsObject, connectionClose?: command_pb.ConnectionCloseMessage.AsObject, newEval?: node_pb.NewEvalMessage.AsObject, - sharedProcessInit?: vscode_pb.SharedProcessInitMessage.AsObject, } export enum MsgCase { @@ -93,7 +87,6 @@ export namespace ClientMessage { CONNECTION_OUTPUT = 7, CONNECTION_CLOSE = 8, NEW_EVAL = 9, - SHARED_PROCESS_INIT = 10, } } @@ -153,6 +146,11 @@ export class ServerMessage extends jspb.Message { getInit(): WorkingInitMessage | undefined; setInit(value?: WorkingInitMessage): void; + hasSharedProcessActive(): boolean; + clearSharedProcessActive(): void; + getSharedProcessActive(): vscode_pb.SharedProcessActiveMessage | undefined; + setSharedProcessActive(value?: vscode_pb.SharedProcessActiveMessage): void; + getMsgCase(): ServerMessage.MsgCase; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ServerMessage.AsObject; @@ -177,6 +175,7 @@ export namespace ServerMessage { evalFailed?: node_pb.EvalFailedMessage.AsObject, evalDone?: node_pb.EvalDoneMessage.AsObject, init?: WorkingInitMessage.AsObject, + sharedProcessActive?: vscode_pb.SharedProcessActiveMessage.AsObject, } export enum MsgCase { @@ -192,6 +191,7 @@ export namespace ServerMessage { EVAL_FAILED = 9, EVAL_DONE = 10, INIT = 11, + SHARED_PROCESS_ACTIVE = 12, } } diff --git a/packages/protocol/src/proto/client_pb.js b/packages/protocol/src/proto/client_pb.js index 8e7354d3..1ff7f1b6 100644 --- a/packages/protocol/src/proto/client_pb.js +++ b/packages/protocol/src/proto/client_pb.js @@ -42,7 +42,7 @@ if (goog.DEBUG && !COMPILED) { * @private {!Array>} * @const */ -proto.ClientMessage.oneofGroups_ = [[1,2,3,4,5,6,7,8,9,10]]; +proto.ClientMessage.oneofGroups_ = [[1,2,3,4,5,6,7,8,9]]; /** * @enum {number} @@ -57,8 +57,7 @@ proto.ClientMessage.MsgCase = { NEW_CONNECTION: 6, CONNECTION_OUTPUT: 7, CONNECTION_CLOSE: 8, - NEW_EVAL: 9, - SHARED_PROCESS_INIT: 10 + NEW_EVAL: 9 }; /** @@ -104,8 +103,7 @@ proto.ClientMessage.toObject = function(includeInstance, msg) { newConnection: (f = msg.getNewConnection()) && command_pb.NewConnectionMessage.toObject(includeInstance, f), connectionOutput: (f = msg.getConnectionOutput()) && command_pb.ConnectionOutputMessage.toObject(includeInstance, f), connectionClose: (f = msg.getConnectionClose()) && command_pb.ConnectionCloseMessage.toObject(includeInstance, f), - newEval: (f = msg.getNewEval()) && node_pb.NewEvalMessage.toObject(includeInstance, f), - sharedProcessInit: (f = msg.getSharedProcessInit()) && vscode_pb.SharedProcessInitMessage.toObject(includeInstance, f) + newEval: (f = msg.getNewEval()) && node_pb.NewEvalMessage.toObject(includeInstance, f) }; if (includeInstance) { @@ -187,11 +185,6 @@ proto.ClientMessage.deserializeBinaryFromReader = function(msg, reader) { reader.readMessage(value,node_pb.NewEvalMessage.deserializeBinaryFromReader); msg.setNewEval(value); break; - case 10: - var value = new vscode_pb.SharedProcessInitMessage; - reader.readMessage(value,vscode_pb.SharedProcessInitMessage.deserializeBinaryFromReader); - msg.setSharedProcessInit(value); - break; default: reader.skipField(); break; @@ -302,14 +295,6 @@ proto.ClientMessage.prototype.serializeBinaryToWriter = function (writer) { node_pb.NewEvalMessage.serializeBinaryToWriter ); } - f = this.getSharedProcessInit(); - if (f != null) { - writer.writeMessage( - 10, - f, - vscode_pb.SharedProcessInitMessage.serializeBinaryToWriter - ); - } }; @@ -592,36 +577,6 @@ proto.ClientMessage.prototype.hasNewEval = function() { }; -/** - * optional SharedProcessInitMessage shared_process_init = 10; - * @return {proto.SharedProcessInitMessage} - */ -proto.ClientMessage.prototype.getSharedProcessInit = function() { - return /** @type{proto.SharedProcessInitMessage} */ ( - jspb.Message.getWrapperField(this, vscode_pb.SharedProcessInitMessage, 10)); -}; - - -/** @param {proto.SharedProcessInitMessage|undefined} value */ -proto.ClientMessage.prototype.setSharedProcessInit = function(value) { - jspb.Message.setOneofWrapperField(this, 10, proto.ClientMessage.oneofGroups_[0], value); -}; - - -proto.ClientMessage.prototype.clearSharedProcessInit = function() { - this.setSharedProcessInit(undefined); -}; - - -/** - * Returns whether this field is set. - * @return{!boolean} - */ -proto.ClientMessage.prototype.hasSharedProcessInit = function() { - return jspb.Message.getField(this, 10) != null; -}; - - /** * Generated by JsPbCodeGenerator. @@ -648,7 +603,7 @@ if (goog.DEBUG && !COMPILED) { * @private {!Array>} * @const */ -proto.ServerMessage.oneofGroups_ = [[1,2,3,4,5,6,7,8,9,10,11]]; +proto.ServerMessage.oneofGroups_ = [[1,2,3,4,5,6,7,8,9,10,11,12]]; /** * @enum {number} @@ -665,7 +620,8 @@ proto.ServerMessage.MsgCase = { CONNECTION_ESTABLISHED: 8, EVAL_FAILED: 9, EVAL_DONE: 10, - INIT: 11 + INIT: 11, + SHARED_PROCESS_ACTIVE: 12 }; /** @@ -713,7 +669,8 @@ proto.ServerMessage.toObject = function(includeInstance, msg) { connectionEstablished: (f = msg.getConnectionEstablished()) && command_pb.ConnectionEstablishedMessage.toObject(includeInstance, f), evalFailed: (f = msg.getEvalFailed()) && node_pb.EvalFailedMessage.toObject(includeInstance, f), evalDone: (f = msg.getEvalDone()) && node_pb.EvalDoneMessage.toObject(includeInstance, f), - init: (f = msg.getInit()) && proto.WorkingInitMessage.toObject(includeInstance, f) + init: (f = msg.getInit()) && proto.WorkingInitMessage.toObject(includeInstance, f), + sharedProcessActive: (f = msg.getSharedProcessActive()) && vscode_pb.SharedProcessActiveMessage.toObject(includeInstance, f) }; if (includeInstance) { @@ -805,6 +762,11 @@ proto.ServerMessage.deserializeBinaryFromReader = function(msg, reader) { reader.readMessage(value,proto.WorkingInitMessage.deserializeBinaryFromReader); msg.setInit(value); break; + case 12: + var value = new vscode_pb.SharedProcessActiveMessage; + reader.readMessage(value,vscode_pb.SharedProcessActiveMessage.deserializeBinaryFromReader); + msg.setSharedProcessActive(value); + break; default: reader.skipField(); break; @@ -931,6 +893,14 @@ proto.ServerMessage.prototype.serializeBinaryToWriter = function (writer) { proto.WorkingInitMessage.serializeBinaryToWriter ); } + f = this.getSharedProcessActive(); + if (f != null) { + writer.writeMessage( + 12, + f, + vscode_pb.SharedProcessActiveMessage.serializeBinaryToWriter + ); + } }; @@ -1273,6 +1243,36 @@ proto.ServerMessage.prototype.hasInit = function() { }; +/** + * optional SharedProcessActiveMessage shared_process_active = 12; + * @return {proto.SharedProcessActiveMessage} + */ +proto.ServerMessage.prototype.getSharedProcessActive = function() { + return /** @type{proto.SharedProcessActiveMessage} */ ( + jspb.Message.getWrapperField(this, vscode_pb.SharedProcessActiveMessage, 12)); +}; + + +/** @param {proto.SharedProcessActiveMessage|undefined} value */ +proto.ServerMessage.prototype.setSharedProcessActive = function(value) { + jspb.Message.setOneofWrapperField(this, 12, proto.ServerMessage.oneofGroups_[0], value); +}; + + +proto.ServerMessage.prototype.clearSharedProcessActive = function() { + this.setSharedProcessActive(undefined); +}; + + +/** + * Returns whether this field is set. + * @return{!boolean} + */ +proto.ServerMessage.prototype.hasSharedProcessActive = function() { + return jspb.Message.getField(this, 12) != null; +}; + + /** * Generated by JsPbCodeGenerator. diff --git a/packages/protocol/src/proto/command.proto b/packages/protocol/src/proto/command.proto index 28a30fec..64419af0 100644 --- a/packages/protocol/src/proto/command.proto +++ b/packages/protocol/src/proto/command.proto @@ -13,6 +13,9 @@ message NewSessionMessage { string cwd = 5; TTYDimensions tty_dimensions = 6; bool is_fork = 7; + + // Janky, but required for having custom handling of the bootstrap fork + bool is_bootstrap_fork = 8; } // Sent when starting a session failed. diff --git a/packages/protocol/src/proto/command_pb.d.ts b/packages/protocol/src/proto/command_pb.d.ts index 73f4f988..0c555275 100644 --- a/packages/protocol/src/proto/command_pb.d.ts +++ b/packages/protocol/src/proto/command_pb.d.ts @@ -28,6 +28,9 @@ export class NewSessionMessage extends jspb.Message { getIsFork(): boolean; setIsFork(value: boolean): void; + getIsBootstrapFork(): boolean; + setIsBootstrapFork(value: boolean): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): NewSessionMessage.AsObject; static toObject(includeInstance: boolean, msg: NewSessionMessage): NewSessionMessage.AsObject; @@ -47,6 +50,7 @@ export namespace NewSessionMessage { cwd: string, ttyDimensions?: TTYDimensions.AsObject, isFork: boolean, + isBootstrapFork: boolean, } } diff --git a/packages/protocol/src/proto/command_pb.js b/packages/protocol/src/proto/command_pb.js index 9fc412d6..a24d0677 100644 --- a/packages/protocol/src/proto/command_pb.js +++ b/packages/protocol/src/proto/command_pb.js @@ -85,7 +85,8 @@ proto.NewSessionMessage.toObject = function(includeInstance, msg) { envMap: (f = msg.getEnvMap(true)) ? f.toArray() : [], cwd: msg.getCwd(), ttyDimensions: (f = msg.getTtyDimensions()) && proto.TTYDimensions.toObject(includeInstance, f), - isFork: msg.getIsFork() + isFork: msg.getIsFork(), + isBootstrapFork: msg.getIsBootstrapFork() }; if (includeInstance) { @@ -154,6 +155,10 @@ proto.NewSessionMessage.deserializeBinaryFromReader = function(msg, reader) { var value = /** @type {boolean} */ (reader.readBool()); msg.setIsFork(value); break; + case 8: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setIsBootstrapFork(value); + break; default: reader.skipField(); break; @@ -239,6 +244,13 @@ proto.NewSessionMessage.prototype.serializeBinaryToWriter = function (writer) { f ); } + f = this.getIsBootstrapFork(); + if (f) { + writer.writeBool( + 8, + f + ); + } }; @@ -378,6 +390,23 @@ proto.NewSessionMessage.prototype.setIsFork = function(value) { }; +/** + * optional bool is_bootstrap_fork = 8; + * Note that Boolean fields may be set to 0/1 when serialized from a Java server. + * You should avoid comparisons like {@code val === true/false} in those cases. + * @return {boolean} + */ +proto.NewSessionMessage.prototype.getIsBootstrapFork = function() { + return /** @type {boolean} */ (jspb.Message.getFieldProto3(this, 8, false)); +}; + + +/** @param {boolean} value */ +proto.NewSessionMessage.prototype.setIsBootstrapFork = function(value) { + jspb.Message.setField(this, 8, value); +}; + + /** * Generated by JsPbCodeGenerator. diff --git a/packages/protocol/src/proto/vscode.proto b/packages/protocol/src/proto/vscode.proto index 12e5dc53..aec37e6a 100644 --- a/packages/protocol/src/proto/vscode.proto +++ b/packages/protocol/src/proto/vscode.proto @@ -1,9 +1,6 @@ syntax = "proto3"; -message SharedProcessInitMessage { - uint64 window_id = 1; - string log_directory = 2; - - // Maps to `"vs/platform/log/common/log".LogLevel` - uint32 log_level = 3; +// Sent when a shared process becomes active +message SharedProcessActiveMessage { + string socket_path = 1; } \ No newline at end of file diff --git a/packages/protocol/src/proto/vscode_pb.d.ts b/packages/protocol/src/proto/vscode_pb.d.ts index 6db08705..e9bcd929 100644 --- a/packages/protocol/src/proto/vscode_pb.d.ts +++ b/packages/protocol/src/proto/vscode_pb.d.ts @@ -3,31 +3,23 @@ import * as jspb from "google-protobuf"; -export class SharedProcessInitMessage extends jspb.Message { - getWindowId(): number; - setWindowId(value: number): void; - - getLogDirectory(): string; - setLogDirectory(value: string): void; - - getLogLevel(): number; - setLogLevel(value: number): void; +export class SharedProcessActiveMessage extends jspb.Message { + getSocketPath(): string; + setSocketPath(value: string): void; serializeBinary(): Uint8Array; - toObject(includeInstance?: boolean): SharedProcessInitMessage.AsObject; - static toObject(includeInstance: boolean, msg: SharedProcessInitMessage): SharedProcessInitMessage.AsObject; + toObject(includeInstance?: boolean): SharedProcessActiveMessage.AsObject; + static toObject(includeInstance: boolean, msg: SharedProcessActiveMessage): SharedProcessActiveMessage.AsObject; static extensions: {[key: number]: jspb.ExtensionFieldInfo}; static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; - static serializeBinaryToWriter(message: SharedProcessInitMessage, writer: jspb.BinaryWriter): void; - static deserializeBinary(bytes: Uint8Array): SharedProcessInitMessage; - static deserializeBinaryFromReader(message: SharedProcessInitMessage, reader: jspb.BinaryReader): SharedProcessInitMessage; + static serializeBinaryToWriter(message: SharedProcessActiveMessage, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): SharedProcessActiveMessage; + static deserializeBinaryFromReader(message: SharedProcessActiveMessage, reader: jspb.BinaryReader): SharedProcessActiveMessage; } -export namespace SharedProcessInitMessage { +export namespace SharedProcessActiveMessage { export type AsObject = { - windowId: number, - logDirectory: string, - logLevel: number, + socketPath: string, } } diff --git a/packages/protocol/src/proto/vscode_pb.js b/packages/protocol/src/proto/vscode_pb.js index a2f40fd6..306b0f93 100644 --- a/packages/protocol/src/proto/vscode_pb.js +++ b/packages/protocol/src/proto/vscode_pb.js @@ -9,7 +9,7 @@ var jspb = require('google-protobuf'); var goog = jspb; var global = Function('return this')(); -goog.exportSymbol('proto.SharedProcessInitMessage', null, global); +goog.exportSymbol('proto.SharedProcessActiveMessage', null, global); /** * Generated by JsPbCodeGenerator. @@ -21,12 +21,12 @@ goog.exportSymbol('proto.SharedProcessInitMessage', null, global); * @extends {jspb.Message} * @constructor */ -proto.SharedProcessInitMessage = function(opt_data) { +proto.SharedProcessActiveMessage = function(opt_data) { jspb.Message.initialize(this, opt_data, 0, -1, null, null); }; -goog.inherits(proto.SharedProcessInitMessage, jspb.Message); +goog.inherits(proto.SharedProcessActiveMessage, jspb.Message); if (goog.DEBUG && !COMPILED) { - proto.SharedProcessInitMessage.displayName = 'proto.SharedProcessInitMessage'; + proto.SharedProcessActiveMessage.displayName = 'proto.SharedProcessActiveMessage'; } @@ -41,8 +41,8 @@ if (jspb.Message.GENERATE_TO_OBJECT) { * for transitional soy proto support: http://goto/soy-param-migration * @return {!Object} */ -proto.SharedProcessInitMessage.prototype.toObject = function(opt_includeInstance) { - return proto.SharedProcessInitMessage.toObject(opt_includeInstance, this); +proto.SharedProcessActiveMessage.prototype.toObject = function(opt_includeInstance) { + return proto.SharedProcessActiveMessage.toObject(opt_includeInstance, this); }; @@ -51,14 +51,12 @@ proto.SharedProcessInitMessage.prototype.toObject = function(opt_includeInstance * @param {boolean|undefined} includeInstance Whether to include the JSPB * instance for transitional soy proto support: * http://goto/soy-param-migration - * @param {!proto.SharedProcessInitMessage} msg The msg instance to transform. + * @param {!proto.SharedProcessActiveMessage} msg The msg instance to transform. * @return {!Object} */ -proto.SharedProcessInitMessage.toObject = function(includeInstance, msg) { +proto.SharedProcessActiveMessage.toObject = function(includeInstance, msg) { var f, obj = { - windowId: msg.getWindowId(), - logDirectory: msg.getLogDirectory(), - logLevel: msg.getLogLevel() + socketPath: msg.getSocketPath() }; if (includeInstance) { @@ -72,23 +70,23 @@ proto.SharedProcessInitMessage.toObject = function(includeInstance, msg) { /** * Deserializes binary data (in protobuf wire format). * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.SharedProcessInitMessage} + * @return {!proto.SharedProcessActiveMessage} */ -proto.SharedProcessInitMessage.deserializeBinary = function(bytes) { +proto.SharedProcessActiveMessage.deserializeBinary = function(bytes) { var reader = new jspb.BinaryReader(bytes); - var msg = new proto.SharedProcessInitMessage; - return proto.SharedProcessInitMessage.deserializeBinaryFromReader(msg, reader); + var msg = new proto.SharedProcessActiveMessage; + return proto.SharedProcessActiveMessage.deserializeBinaryFromReader(msg, reader); }; /** * Deserializes binary data (in protobuf wire format) from the * given reader into the given message object. - * @param {!proto.SharedProcessInitMessage} msg The message object to deserialize into. + * @param {!proto.SharedProcessActiveMessage} msg The message object to deserialize into. * @param {!jspb.BinaryReader} reader The BinaryReader to use. - * @return {!proto.SharedProcessInitMessage} + * @return {!proto.SharedProcessActiveMessage} */ -proto.SharedProcessInitMessage.deserializeBinaryFromReader = function(msg, reader) { +proto.SharedProcessActiveMessage.deserializeBinaryFromReader = function(msg, reader) { while (reader.nextField()) { if (reader.isEndGroup()) { break; @@ -96,16 +94,8 @@ proto.SharedProcessInitMessage.deserializeBinaryFromReader = function(msg, reade var field = reader.getFieldNumber(); switch (field) { case 1: - var value = /** @type {number} */ (reader.readUint64()); - msg.setWindowId(value); - break; - case 2: var value = /** @type {string} */ (reader.readString()); - msg.setLogDirectory(value); - break; - case 3: - var value = /** @type {number} */ (reader.readUint32()); - msg.setLogLevel(value); + msg.setSocketPath(value); break; default: reader.skipField(); @@ -119,10 +109,10 @@ proto.SharedProcessInitMessage.deserializeBinaryFromReader = function(msg, reade /** * Class method variant: serializes the given message to binary data * (in protobuf wire format), writing to the given BinaryWriter. - * @param {!proto.SharedProcessInitMessage} message + * @param {!proto.SharedProcessActiveMessage} message * @param {!jspb.BinaryWriter} writer */ -proto.SharedProcessInitMessage.serializeBinaryToWriter = function(message, writer) { +proto.SharedProcessActiveMessage.serializeBinaryToWriter = function(message, writer) { message.serializeBinaryToWriter(writer); }; @@ -131,7 +121,7 @@ proto.SharedProcessInitMessage.serializeBinaryToWriter = function(message, write * Serializes the message to binary data (in protobuf wire format). * @return {!Uint8Array} */ -proto.SharedProcessInitMessage.prototype.serializeBinary = function() { +proto.SharedProcessActiveMessage.prototype.serializeBinary = function() { var writer = new jspb.BinaryWriter(); this.serializeBinaryToWriter(writer); return writer.getResultBuffer(); @@ -143,26 +133,12 @@ proto.SharedProcessInitMessage.prototype.serializeBinary = function() { * writing to the given BinaryWriter. * @param {!jspb.BinaryWriter} writer */ -proto.SharedProcessInitMessage.prototype.serializeBinaryToWriter = function (writer) { +proto.SharedProcessActiveMessage.prototype.serializeBinaryToWriter = function (writer) { var f = undefined; - f = this.getWindowId(); - if (f !== 0) { - writer.writeUint64( - 1, - f - ); - } - f = this.getLogDirectory(); + f = this.getSocketPath(); if (f.length > 0) { writer.writeString( - 2, - f - ); - } - f = this.getLogLevel(); - if (f !== 0) { - writer.writeUint32( - 3, + 1, f ); } @@ -171,55 +147,25 @@ proto.SharedProcessInitMessage.prototype.serializeBinaryToWriter = function (wri /** * Creates a deep clone of this proto. No data is shared with the original. - * @return {!proto.SharedProcessInitMessage} The clone. + * @return {!proto.SharedProcessActiveMessage} The clone. */ -proto.SharedProcessInitMessage.prototype.cloneMessage = function() { - return /** @type {!proto.SharedProcessInitMessage} */ (jspb.Message.cloneMessage(this)); +proto.SharedProcessActiveMessage.prototype.cloneMessage = function() { + return /** @type {!proto.SharedProcessActiveMessage} */ (jspb.Message.cloneMessage(this)); }; /** - * optional uint64 window_id = 1; - * @return {number} - */ -proto.SharedProcessInitMessage.prototype.getWindowId = function() { - return /** @type {number} */ (jspb.Message.getFieldProto3(this, 1, 0)); -}; - - -/** @param {number} value */ -proto.SharedProcessInitMessage.prototype.setWindowId = function(value) { - jspb.Message.setField(this, 1, value); -}; - - -/** - * optional string log_directory = 2; + * optional string socket_path = 1; * @return {string} */ -proto.SharedProcessInitMessage.prototype.getLogDirectory = function() { - return /** @type {string} */ (jspb.Message.getFieldProto3(this, 2, "")); +proto.SharedProcessActiveMessage.prototype.getSocketPath = function() { + return /** @type {string} */ (jspb.Message.getFieldProto3(this, 1, "")); }; /** @param {string} value */ -proto.SharedProcessInitMessage.prototype.setLogDirectory = function(value) { - jspb.Message.setField(this, 2, value); -}; - - -/** - * optional uint32 log_level = 3; - * @return {number} - */ -proto.SharedProcessInitMessage.prototype.getLogLevel = function() { - return /** @type {number} */ (jspb.Message.getFieldProto3(this, 3, 0)); -}; - - -/** @param {number} value */ -proto.SharedProcessInitMessage.prototype.setLogLevel = function(value) { - jspb.Message.setField(this, 3, value); +proto.SharedProcessActiveMessage.prototype.setSocketPath = function(value) { + jspb.Message.setField(this, 1, value); }; diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index 928833fd..bca6ee22 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -1,12 +1,13 @@ -import { SharedProcessInitMessage } from "@coder/protocol/src/proto"; +import { field, logger } from "@coder/logger"; +import { ServerMessage, SharedProcessActiveMessage } from "@coder/protocol/src/proto"; import { Command, flags } from "@oclif/command"; -import { logger, field } from "@coder/logger"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { requireModule } from "./vscode/bootstrapFork"; +import * as WebSocket from "ws"; import { createApp } from "./server"; -import { SharedProcess } from './vscode/sharedProcess'; +import { requireModule } from "./vscode/bootstrapFork"; +import { SharedProcess, SharedProcessState } from './vscode/sharedProcess'; export class Entry extends Command { @@ -69,15 +70,22 @@ export class Entry extends Command { logger.info("Initializing", field("data-dir", dataDir), field("working-dir", workingDir)); const sharedProcess = new SharedProcess(dataDir); logger.info("Starting shared process...", field("socket", sharedProcess.socketPath)); - sharedProcess.onWillRestart(() => { - logger.info("Restarting shared process..."); - - sharedProcess.ready.then(() => { - logger.info("Shared process has restarted!"); - }); - }); - sharedProcess.ready.then(() => { - logger.info("Shared process has started up!"); + const sendSharedProcessReady = (socket: WebSocket) => { + const active = new SharedProcessActiveMessage(); + active.setSocketPath(sharedProcess.socketPath); + const serverMessage = new ServerMessage(); + serverMessage.setSharedProcessActive(active); + socket.send(serverMessage.serializeBinary()); + }; + sharedProcess.onState((event) => { + if (event.state === SharedProcessState.Stopped) { + logger.error("Shared process stopped. Restarting...", field("error", event.error)); + } else if (event.state === SharedProcessState.Starting) { + logger.info("Starting shared process..."); + } else if (event.state === SharedProcessState.Ready) { + logger.info("Shared process is ready!"); + app.wss.clients.forEach((c) => sendSharedProcessReady(c)); + } }); const app = createApp((app) => { @@ -108,13 +116,12 @@ export class Entry extends Command { let clientId = 1; app.wss.on("connection", (ws, req) => { const id = clientId++; - const spm = (req).sharedProcessInit as SharedProcessInitMessage; - if (!spm) { - logger.warn("Received a socket without init data. Not sure how this happened."); - return; + if (sharedProcess.state === SharedProcessState.Ready) { + sendSharedProcessReady(ws); } - logger.info(`WebSocket opened \u001B[0m${req.url}`, field("client", id), field("ip", req.socket.remoteAddress), field("window_id", spm.getWindowId()), field("log_directory", spm.getLogDirectory())); + + logger.info(`WebSocket opened \u001B[0m${req.url}`, field("client", id), field("ip", req.socket.remoteAddress)); ws.on("close", (code) => { logger.info(`WebSocket closed \u001B[0m${req.url}`, field("client", id), field("code", code)); diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index c8cd29b1..a6377b3c 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -1,10 +1,11 @@ import { ReadWriteConnection } from "@coder/protocol"; import { Server, ServerOptions } from "@coder/protocol/src/node/server"; +import { NewSessionMessage } from '@coder/protocol/src/proto'; +import { ChildProcess } from "child_process"; import * as express from "express"; import * as http from "http"; import * as ws from "ws"; -import * as url from "url"; -import { ClientMessage, SharedProcessInitMessage } from '@coder/protocol/src/proto'; +import { forkModule } from "./vscode/bootstrapFork"; export const createApp = (registerMiddleware?: (app: express.Application) => void, options?: ServerOptions): { readonly express: express.Application; @@ -19,27 +20,7 @@ export const createApp = (registerMiddleware?: (app: express.Application) => voi const wss = new ws.Server({ server }); wss.shouldHandle = (req): boolean => { - if (typeof req.url === "undefined") { - return false; - } - - const parsedUrl = url.parse(req.url, true); - const sharedProcessInit = parsedUrl.query["shared_process_init"]; - if (typeof sharedProcessInit === "undefined" || Array.isArray(sharedProcessInit)) { - return false; - } - - try { - const msg = ClientMessage.deserializeBinary(Buffer.from(sharedProcessInit, "base64")); - if (!msg.hasSharedProcessInit()) { - return false; - } - const spm = msg.getSharedProcessInit()!; - (req).sharedProcessInit = spm; - } catch (ex) { - return false; - } - + // Should handle auth here return true; }; @@ -59,7 +40,20 @@ export const createApp = (registerMiddleware?: (app: express.Application) => voi onClose: (cb): void => ws.addEventListener("close", () => cb()), }; - const server = new Server(connection, options); + const server = new Server(connection, options ? { + ...options, + forkProvider: (message: NewSessionMessage): ChildProcess => { + let proc: ChildProcess; + + if (message.getIsBootstrapFork()) { + proc = forkModule(message.getCommand()); + } else { + throw new Error("No support for non bootstrap-forking yet"); + } + + return proc; + }, + } : undefined); }); /** diff --git a/packages/server/src/vscode/sharedProcess.ts b/packages/server/src/vscode/sharedProcess.ts index 54e7df24..3421fd9b 100644 --- a/packages/server/src/vscode/sharedProcess.ts +++ b/packages/server/src/vscode/sharedProcess.ts @@ -9,41 +9,47 @@ import { ParsedArgs } from "vs/platform/environment/common/environment"; import { LogLevel } from "vs/platform/log/common/log"; import { Emitter, Event } from '@coder/events/src'; +export enum SharedProcessState { + Stopped, + Starting, + Ready, +} + +export type SharedProcessEvent = { + readonly state: SharedProcessState.Ready | SharedProcessState.Starting; +} | { + readonly state: SharedProcessState.Stopped; + readonly error: string; +} + export class SharedProcess { public readonly socketPath: string = path.join(os.tmpdir(), `.vscode-online${Math.random().toString()}`); - private _ready: Promise | undefined; + private _state: SharedProcessState = SharedProcessState.Stopped; private activeProcess: ChildProcess | undefined; private ipcHandler: StdioIpcHandler | undefined; - private readonly willRestartEmitter: Emitter; + private readonly onStateEmitter: Emitter; public constructor( private readonly userDataDir: string, ) { - this.willRestartEmitter = new Emitter(); + this.onStateEmitter = new Emitter(); this.restart(); } - public get onWillRestart(): Event { - return this.willRestartEmitter.event; + public get onState(): Event { + return this.onStateEmitter.event; } - public get ready(): Promise { - return this._ready!; + public get state(): SharedProcessState { + return this._state; } public restart(): void { - if (this.activeProcess) { - this.willRestartEmitter.emit(); - } - if (this.activeProcess && !this.activeProcess.killed) { this.activeProcess.kill(); } - let resolve: () => void; - let reject: (err: Error) => void; - const extensionsDir = path.join(this.userDataDir, "extensions"); const mkdir = (dir: string): void => { try { @@ -57,14 +63,18 @@ export class SharedProcess { mkdir(this.userDataDir); mkdir(extensionsDir); - this._ready = new Promise((res, rej) => { - resolve = res; - reject = rej; + this.setState({ + state: SharedProcessState.Starting, }); - let resolved: boolean = false; this.activeProcess = forkModule("vs/code/electron-browser/sharedProcess/sharedProcessMain"); - this.activeProcess.on("exit", () => { + this.activeProcess.on("exit", (err) => { + if (this._state !== SharedProcessState.Stopped) { + this.setState({ + error: `Exited with ${err}`, + state: SharedProcessState.Stopped, + }); + } this.restart(); }); this.ipcHandler = new StdioIpcHandler(this.activeProcess); @@ -86,11 +96,20 @@ export class SharedProcess { }); this.ipcHandler.once("handshake:im ready", () => { resolved = true; - resolve(); + this.setState({ + state: SharedProcessState.Ready, + }); }); this.activeProcess.stderr.on("data", (data) => { if (!resolved) { - reject(data.toString()); + this.setState({ + error: data.toString(), + state: SharedProcessState.Stopped, + }); + if (!this.activeProcess) { + return; + } + this.activeProcess.kill(); } else { logger.named("SHRD PROC").debug("stderr", field("message", data.toString())); } @@ -103,4 +122,9 @@ export class SharedProcess { } this.ipcHandler = undefined; } + + private setState(event: SharedProcessEvent): void { + this._state = event.state; + this.onStateEmitter.emit(event); + } }