Add shared process active message (#16)

* Add shared process active message

* Add client function for calling bootstrap fork
This commit is contained in:
Kyle Carberry 2019-01-18 17:08:44 -06:00
parent 72bf4547d4
commit d827015b40
No known key found for this signature in database
GPG Key ID: A0409BDB6B0B3EDB
15 changed files with 260 additions and 241 deletions

View File

@ -155,6 +155,15 @@ export class Client {
return this.doSpawn(modulePath, args, options, true); 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(path: string, callback?: () => void): Socket;
public createConnection(port: number, callback?: () => void): Socket; public createConnection(port: number, callback?: () => void): Socket;
public createConnection(target: string | number, callback?: () => void): Socket { public createConnection(target: string | number, callback?: () => void): Socket {
@ -176,13 +185,14 @@ export class Client {
return socket; 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 id = this.sessionId++;
const newSess = new NewSessionMessage(); const newSess = new NewSessionMessage();
newSess.setId(id); newSess.setId(id);
newSess.setCommand(command); newSess.setCommand(command);
newSess.setArgsList(args); newSess.setArgsList(args);
newSess.setIsFork(isFork); newSess.setIsFork(isFork);
newSess.setIsBootstrapFork(isBootstrapFork);
if (options) { if (options) {
if (options.cwd) { if (options.cwd) {
newSess.setCwd(options.cwd); newSess.setCwd(options.cwd);

View File

@ -5,6 +5,7 @@ import * as stream from "stream";
import { TextEncoder } from "text-encoding"; import { TextEncoder } from "text-encoding";
import { NewSessionMessage, ServerMessage, SessionDoneMessage, SessionOutputMessage, ShutdownSessionMessage, IdentifySessionMessage, ClientMessage, NewConnectionMessage, ConnectionEstablishedMessage, NewConnectionFailureMessage, ConnectionCloseMessage, ConnectionOutputMessage } from "../proto"; import { NewSessionMessage, ServerMessage, SessionDoneMessage, SessionOutputMessage, ShutdownSessionMessage, IdentifySessionMessage, ClientMessage, NewConnectionMessage, ConnectionEstablishedMessage, NewConnectionFailureMessage, ConnectionCloseMessage, ConnectionOutputMessage } from "../proto";
import { SendableConnection } from "../common/connection"; import { SendableConnection } from "../common/connection";
import { ServerOptions } from "./server";
export interface Process { export interface Process {
stdin?: stream.Writable; stdin?: stream.Writable;
@ -22,7 +23,7 @@ export interface Process {
title?: number; 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; let process: Process;
const env = {} as any; const env = {} as any;
@ -44,7 +45,15 @@ export const handleNewSession = (connection: SendableConnection, newSession: New
}; };
let proc: cp.ChildProcess; let proc: cp.ChildProcess;
if (newSession.getIsFork()) { 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 { } else {
proc = cp.spawn(newSession.getCommand(), newSession.getArgsList(), options); proc = cp.spawn(newSession.getCommand(), newSession.getArgsList(), options);
} }

View File

@ -1,10 +1,11 @@
import * as os from "os"; import * as os from "os";
import * as cp from "child_process";
import * as path from "path"; import * as path from "path";
import { mkdir } from "fs"; import { mkdir } from "fs";
import { promisify } from "util"; import { promisify } from "util";
import { TextDecoder } from "text-encoding"; import { TextDecoder } from "text-encoding";
import { logger, field } from "@coder/logger"; import { logger, field } from "@coder/logger";
import { ClientMessage, WorkingInitMessage, ServerMessage } from "../proto"; import { ClientMessage, WorkingInitMessage, ServerMessage, NewSessionMessage } from "../proto";
import { evaluate } from "./evaluate"; import { evaluate } from "./evaluate";
import { ReadWriteConnection } from "../common/connection"; import { ReadWriteConnection } from "../common/connection";
import { Process, handleNewSession, handleNewConnection } from "./command"; import { Process, handleNewSession, handleNewConnection } from "./command";
@ -13,6 +14,8 @@ import * as net from "net";
export interface ServerOptions { export interface ServerOptions {
readonly workingDirectory: string; readonly workingDirectory: string;
readonly dataDirectory: string; readonly dataDirectory: string;
forkProvider?(message: NewSessionMessage): cp.ChildProcess;
} }
export class Server { export class Server {
@ -22,7 +25,7 @@ export class Server {
public constructor( public constructor(
private readonly connection: ReadWriteConnection, private readonly connection: ReadWriteConnection,
options?: ServerOptions, private readonly options?: ServerOptions,
) { ) {
connection.onMessage((data) => { connection.onMessage((data) => {
try { try {
@ -89,7 +92,7 @@ export class Server {
if (message.hasNewEval()) { if (message.hasNewEval()) {
evaluate(this.connection, message.getNewEval()!); evaluate(this.connection, message.getNewEval()!);
} else if (message.hasNewSession()) { } 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.delete(message.getNewSession()!.getId());
}); });
this.sessions.set(message.getNewSession()!.getId(), session); this.sessions.set(message.getNewSession()!.getId(), session);

View File

@ -17,8 +17,6 @@ message ClientMessage {
// node.proto // node.proto
NewEvalMessage new_eval = 9; NewEvalMessage new_eval = 9;
SharedProcessInitMessage shared_process_init = 10;
} }
} }
@ -39,6 +37,9 @@ message ServerMessage {
EvalDoneMessage eval_done = 10; EvalDoneMessage eval_done = 10;
WorkingInitMessage init = 11; WorkingInitMessage init = 11;
// vscode.proto
SharedProcessActiveMessage shared_process_active = 12;
} }
} }

View File

@ -52,11 +52,6 @@ export class ClientMessage extends jspb.Message {
getNewEval(): node_pb.NewEvalMessage | undefined; getNewEval(): node_pb.NewEvalMessage | undefined;
setNewEval(value?: node_pb.NewEvalMessage): void; setNewEval(value?: node_pb.NewEvalMessage): void;
hasSharedProcessInit(): boolean;
clearSharedProcessInit(): void;
getSharedProcessInit(): vscode_pb.SharedProcessInitMessage | undefined;
setSharedProcessInit(value?: vscode_pb.SharedProcessInitMessage): void;
getMsgCase(): ClientMessage.MsgCase; getMsgCase(): ClientMessage.MsgCase;
serializeBinary(): Uint8Array; serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ClientMessage.AsObject; toObject(includeInstance?: boolean): ClientMessage.AsObject;
@ -79,7 +74,6 @@ export namespace ClientMessage {
connectionOutput?: command_pb.ConnectionOutputMessage.AsObject, connectionOutput?: command_pb.ConnectionOutputMessage.AsObject,
connectionClose?: command_pb.ConnectionCloseMessage.AsObject, connectionClose?: command_pb.ConnectionCloseMessage.AsObject,
newEval?: node_pb.NewEvalMessage.AsObject, newEval?: node_pb.NewEvalMessage.AsObject,
sharedProcessInit?: vscode_pb.SharedProcessInitMessage.AsObject,
} }
export enum MsgCase { export enum MsgCase {
@ -93,7 +87,6 @@ export namespace ClientMessage {
CONNECTION_OUTPUT = 7, CONNECTION_OUTPUT = 7,
CONNECTION_CLOSE = 8, CONNECTION_CLOSE = 8,
NEW_EVAL = 9, NEW_EVAL = 9,
SHARED_PROCESS_INIT = 10,
} }
} }
@ -153,6 +146,11 @@ export class ServerMessage extends jspb.Message {
getInit(): WorkingInitMessage | undefined; getInit(): WorkingInitMessage | undefined;
setInit(value?: WorkingInitMessage): void; setInit(value?: WorkingInitMessage): void;
hasSharedProcessActive(): boolean;
clearSharedProcessActive(): void;
getSharedProcessActive(): vscode_pb.SharedProcessActiveMessage | undefined;
setSharedProcessActive(value?: vscode_pb.SharedProcessActiveMessage): void;
getMsgCase(): ServerMessage.MsgCase; getMsgCase(): ServerMessage.MsgCase;
serializeBinary(): Uint8Array; serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ServerMessage.AsObject; toObject(includeInstance?: boolean): ServerMessage.AsObject;
@ -177,6 +175,7 @@ export namespace ServerMessage {
evalFailed?: node_pb.EvalFailedMessage.AsObject, evalFailed?: node_pb.EvalFailedMessage.AsObject,
evalDone?: node_pb.EvalDoneMessage.AsObject, evalDone?: node_pb.EvalDoneMessage.AsObject,
init?: WorkingInitMessage.AsObject, init?: WorkingInitMessage.AsObject,
sharedProcessActive?: vscode_pb.SharedProcessActiveMessage.AsObject,
} }
export enum MsgCase { export enum MsgCase {
@ -192,6 +191,7 @@ export namespace ServerMessage {
EVAL_FAILED = 9, EVAL_FAILED = 9,
EVAL_DONE = 10, EVAL_DONE = 10,
INIT = 11, INIT = 11,
SHARED_PROCESS_ACTIVE = 12,
} }
} }

View File

@ -42,7 +42,7 @@ if (goog.DEBUG && !COMPILED) {
* @private {!Array<!Array<number>>} * @private {!Array<!Array<number>>}
* @const * @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} * @enum {number}
@ -57,8 +57,7 @@ proto.ClientMessage.MsgCase = {
NEW_CONNECTION: 6, NEW_CONNECTION: 6,
CONNECTION_OUTPUT: 7, CONNECTION_OUTPUT: 7,
CONNECTION_CLOSE: 8, CONNECTION_CLOSE: 8,
NEW_EVAL: 9, NEW_EVAL: 9
SHARED_PROCESS_INIT: 10
}; };
/** /**
@ -104,8 +103,7 @@ proto.ClientMessage.toObject = function(includeInstance, msg) {
newConnection: (f = msg.getNewConnection()) && command_pb.NewConnectionMessage.toObject(includeInstance, f), newConnection: (f = msg.getNewConnection()) && command_pb.NewConnectionMessage.toObject(includeInstance, f),
connectionOutput: (f = msg.getConnectionOutput()) && command_pb.ConnectionOutputMessage.toObject(includeInstance, f), connectionOutput: (f = msg.getConnectionOutput()) && command_pb.ConnectionOutputMessage.toObject(includeInstance, f),
connectionClose: (f = msg.getConnectionClose()) && command_pb.ConnectionCloseMessage.toObject(includeInstance, f), connectionClose: (f = msg.getConnectionClose()) && command_pb.ConnectionCloseMessage.toObject(includeInstance, f),
newEval: (f = msg.getNewEval()) && node_pb.NewEvalMessage.toObject(includeInstance, f), newEval: (f = msg.getNewEval()) && node_pb.NewEvalMessage.toObject(includeInstance, f)
sharedProcessInit: (f = msg.getSharedProcessInit()) && vscode_pb.SharedProcessInitMessage.toObject(includeInstance, f)
}; };
if (includeInstance) { if (includeInstance) {
@ -187,11 +185,6 @@ proto.ClientMessage.deserializeBinaryFromReader = function(msg, reader) {
reader.readMessage(value,node_pb.NewEvalMessage.deserializeBinaryFromReader); reader.readMessage(value,node_pb.NewEvalMessage.deserializeBinaryFromReader);
msg.setNewEval(value); msg.setNewEval(value);
break; break;
case 10:
var value = new vscode_pb.SharedProcessInitMessage;
reader.readMessage(value,vscode_pb.SharedProcessInitMessage.deserializeBinaryFromReader);
msg.setSharedProcessInit(value);
break;
default: default:
reader.skipField(); reader.skipField();
break; break;
@ -302,14 +295,6 @@ proto.ClientMessage.prototype.serializeBinaryToWriter = function (writer) {
node_pb.NewEvalMessage.serializeBinaryToWriter 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. * Generated by JsPbCodeGenerator.
@ -648,7 +603,7 @@ if (goog.DEBUG && !COMPILED) {
* @private {!Array<!Array<number>>} * @private {!Array<!Array<number>>}
* @const * @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} * @enum {number}
@ -665,7 +620,8 @@ proto.ServerMessage.MsgCase = {
CONNECTION_ESTABLISHED: 8, CONNECTION_ESTABLISHED: 8,
EVAL_FAILED: 9, EVAL_FAILED: 9,
EVAL_DONE: 10, 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), connectionEstablished: (f = msg.getConnectionEstablished()) && command_pb.ConnectionEstablishedMessage.toObject(includeInstance, f),
evalFailed: (f = msg.getEvalFailed()) && node_pb.EvalFailedMessage.toObject(includeInstance, f), evalFailed: (f = msg.getEvalFailed()) && node_pb.EvalFailedMessage.toObject(includeInstance, f),
evalDone: (f = msg.getEvalDone()) && node_pb.EvalDoneMessage.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) { if (includeInstance) {
@ -805,6 +762,11 @@ proto.ServerMessage.deserializeBinaryFromReader = function(msg, reader) {
reader.readMessage(value,proto.WorkingInitMessage.deserializeBinaryFromReader); reader.readMessage(value,proto.WorkingInitMessage.deserializeBinaryFromReader);
msg.setInit(value); msg.setInit(value);
break; break;
case 12:
var value = new vscode_pb.SharedProcessActiveMessage;
reader.readMessage(value,vscode_pb.SharedProcessActiveMessage.deserializeBinaryFromReader);
msg.setSharedProcessActive(value);
break;
default: default:
reader.skipField(); reader.skipField();
break; break;
@ -931,6 +893,14 @@ proto.ServerMessage.prototype.serializeBinaryToWriter = function (writer) {
proto.WorkingInitMessage.serializeBinaryToWriter 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. * Generated by JsPbCodeGenerator.

View File

@ -13,6 +13,9 @@ message NewSessionMessage {
string cwd = 5; string cwd = 5;
TTYDimensions tty_dimensions = 6; TTYDimensions tty_dimensions = 6;
bool is_fork = 7; 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. // Sent when starting a session failed.

View File

@ -28,6 +28,9 @@ export class NewSessionMessage extends jspb.Message {
getIsFork(): boolean; getIsFork(): boolean;
setIsFork(value: boolean): void; setIsFork(value: boolean): void;
getIsBootstrapFork(): boolean;
setIsBootstrapFork(value: boolean): void;
serializeBinary(): Uint8Array; serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NewSessionMessage.AsObject; toObject(includeInstance?: boolean): NewSessionMessage.AsObject;
static toObject(includeInstance: boolean, msg: NewSessionMessage): NewSessionMessage.AsObject; static toObject(includeInstance: boolean, msg: NewSessionMessage): NewSessionMessage.AsObject;
@ -47,6 +50,7 @@ export namespace NewSessionMessage {
cwd: string, cwd: string,
ttyDimensions?: TTYDimensions.AsObject, ttyDimensions?: TTYDimensions.AsObject,
isFork: boolean, isFork: boolean,
isBootstrapFork: boolean,
} }
} }

View File

@ -85,7 +85,8 @@ proto.NewSessionMessage.toObject = function(includeInstance, msg) {
envMap: (f = msg.getEnvMap(true)) ? f.toArray() : [], envMap: (f = msg.getEnvMap(true)) ? f.toArray() : [],
cwd: msg.getCwd(), cwd: msg.getCwd(),
ttyDimensions: (f = msg.getTtyDimensions()) && proto.TTYDimensions.toObject(includeInstance, f), ttyDimensions: (f = msg.getTtyDimensions()) && proto.TTYDimensions.toObject(includeInstance, f),
isFork: msg.getIsFork() isFork: msg.getIsFork(),
isBootstrapFork: msg.getIsBootstrapFork()
}; };
if (includeInstance) { if (includeInstance) {
@ -154,6 +155,10 @@ proto.NewSessionMessage.deserializeBinaryFromReader = function(msg, reader) {
var value = /** @type {boolean} */ (reader.readBool()); var value = /** @type {boolean} */ (reader.readBool());
msg.setIsFork(value); msg.setIsFork(value);
break; break;
case 8:
var value = /** @type {boolean} */ (reader.readBool());
msg.setIsBootstrapFork(value);
break;
default: default:
reader.skipField(); reader.skipField();
break; break;
@ -239,6 +244,13 @@ proto.NewSessionMessage.prototype.serializeBinaryToWriter = function (writer) {
f 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. * Generated by JsPbCodeGenerator.

View File

@ -1,9 +1,6 @@
syntax = "proto3"; syntax = "proto3";
message SharedProcessInitMessage { // Sent when a shared process becomes active
uint64 window_id = 1; message SharedProcessActiveMessage {
string log_directory = 2; string socket_path = 1;
// Maps to `"vs/platform/log/common/log".LogLevel`
uint32 log_level = 3;
} }

View File

@ -3,31 +3,23 @@
import * as jspb from "google-protobuf"; import * as jspb from "google-protobuf";
export class SharedProcessInitMessage extends jspb.Message { export class SharedProcessActiveMessage extends jspb.Message {
getWindowId(): number; getSocketPath(): string;
setWindowId(value: number): void; setSocketPath(value: string): void;
getLogDirectory(): string;
setLogDirectory(value: string): void;
getLogLevel(): number;
setLogLevel(value: number): void;
serializeBinary(): Uint8Array; serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): SharedProcessInitMessage.AsObject; toObject(includeInstance?: boolean): SharedProcessActiveMessage.AsObject;
static toObject(includeInstance: boolean, msg: SharedProcessInitMessage): SharedProcessInitMessage.AsObject; static toObject(includeInstance: boolean, msg: SharedProcessActiveMessage): SharedProcessActiveMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>}; static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>}; static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: SharedProcessInitMessage, writer: jspb.BinaryWriter): void; static serializeBinaryToWriter(message: SharedProcessActiveMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): SharedProcessInitMessage; static deserializeBinary(bytes: Uint8Array): SharedProcessActiveMessage;
static deserializeBinaryFromReader(message: SharedProcessInitMessage, reader: jspb.BinaryReader): SharedProcessInitMessage; static deserializeBinaryFromReader(message: SharedProcessActiveMessage, reader: jspb.BinaryReader): SharedProcessActiveMessage;
} }
export namespace SharedProcessInitMessage { export namespace SharedProcessActiveMessage {
export type AsObject = { export type AsObject = {
windowId: number, socketPath: string,
logDirectory: string,
logLevel: number,
} }
} }

View File

@ -9,7 +9,7 @@ var jspb = require('google-protobuf');
var goog = jspb; var goog = jspb;
var global = Function('return this')(); var global = Function('return this')();
goog.exportSymbol('proto.SharedProcessInitMessage', null, global); goog.exportSymbol('proto.SharedProcessActiveMessage', null, global);
/** /**
* Generated by JsPbCodeGenerator. * Generated by JsPbCodeGenerator.
@ -21,12 +21,12 @@ goog.exportSymbol('proto.SharedProcessInitMessage', null, global);
* @extends {jspb.Message} * @extends {jspb.Message}
* @constructor * @constructor
*/ */
proto.SharedProcessInitMessage = function(opt_data) { proto.SharedProcessActiveMessage = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null); 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) { 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 * for transitional soy proto support: http://goto/soy-param-migration
* @return {!Object} * @return {!Object}
*/ */
proto.SharedProcessInitMessage.prototype.toObject = function(opt_includeInstance) { proto.SharedProcessActiveMessage.prototype.toObject = function(opt_includeInstance) {
return proto.SharedProcessInitMessage.toObject(opt_includeInstance, this); 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 * @param {boolean|undefined} includeInstance Whether to include the JSPB
* instance for transitional soy proto support: * instance for transitional soy proto support:
* http://goto/soy-param-migration * 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} * @return {!Object}
*/ */
proto.SharedProcessInitMessage.toObject = function(includeInstance, msg) { proto.SharedProcessActiveMessage.toObject = function(includeInstance, msg) {
var f, obj = { var f, obj = {
windowId: msg.getWindowId(), socketPath: msg.getSocketPath()
logDirectory: msg.getLogDirectory(),
logLevel: msg.getLogLevel()
}; };
if (includeInstance) { if (includeInstance) {
@ -72,23 +70,23 @@ proto.SharedProcessInitMessage.toObject = function(includeInstance, msg) {
/** /**
* Deserializes binary data (in protobuf wire format). * Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize. * @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 reader = new jspb.BinaryReader(bytes);
var msg = new proto.SharedProcessInitMessage; var msg = new proto.SharedProcessActiveMessage;
return proto.SharedProcessInitMessage.deserializeBinaryFromReader(msg, reader); return proto.SharedProcessActiveMessage.deserializeBinaryFromReader(msg, reader);
}; };
/** /**
* Deserializes binary data (in protobuf wire format) from the * Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object. * 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. * @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()) { while (reader.nextField()) {
if (reader.isEndGroup()) { if (reader.isEndGroup()) {
break; break;
@ -96,16 +94,8 @@ proto.SharedProcessInitMessage.deserializeBinaryFromReader = function(msg, reade
var field = reader.getFieldNumber(); var field = reader.getFieldNumber();
switch (field) { switch (field) {
case 1: case 1:
var value = /** @type {number} */ (reader.readUint64());
msg.setWindowId(value);
break;
case 2:
var value = /** @type {string} */ (reader.readString()); var value = /** @type {string} */ (reader.readString());
msg.setLogDirectory(value); msg.setSocketPath(value);
break;
case 3:
var value = /** @type {number} */ (reader.readUint32());
msg.setLogLevel(value);
break; break;
default: default:
reader.skipField(); reader.skipField();
@ -119,10 +109,10 @@ proto.SharedProcessInitMessage.deserializeBinaryFromReader = function(msg, reade
/** /**
* Class method variant: serializes the given message to binary data * Class method variant: serializes the given message to binary data
* (in protobuf wire format), writing to the given BinaryWriter. * (in protobuf wire format), writing to the given BinaryWriter.
* @param {!proto.SharedProcessInitMessage} message * @param {!proto.SharedProcessActiveMessage} message
* @param {!jspb.BinaryWriter} writer * @param {!jspb.BinaryWriter} writer
*/ */
proto.SharedProcessInitMessage.serializeBinaryToWriter = function(message, writer) { proto.SharedProcessActiveMessage.serializeBinaryToWriter = function(message, writer) {
message.serializeBinaryToWriter(writer); message.serializeBinaryToWriter(writer);
}; };
@ -131,7 +121,7 @@ proto.SharedProcessInitMessage.serializeBinaryToWriter = function(message, write
* Serializes the message to binary data (in protobuf wire format). * Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array} * @return {!Uint8Array}
*/ */
proto.SharedProcessInitMessage.prototype.serializeBinary = function() { proto.SharedProcessActiveMessage.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter(); var writer = new jspb.BinaryWriter();
this.serializeBinaryToWriter(writer); this.serializeBinaryToWriter(writer);
return writer.getResultBuffer(); return writer.getResultBuffer();
@ -143,26 +133,12 @@ proto.SharedProcessInitMessage.prototype.serializeBinary = function() {
* writing to the given BinaryWriter. * writing to the given BinaryWriter.
* @param {!jspb.BinaryWriter} writer * @param {!jspb.BinaryWriter} writer
*/ */
proto.SharedProcessInitMessage.prototype.serializeBinaryToWriter = function (writer) { proto.SharedProcessActiveMessage.prototype.serializeBinaryToWriter = function (writer) {
var f = undefined; var f = undefined;
f = this.getWindowId(); f = this.getSocketPath();
if (f !== 0) {
writer.writeUint64(
1,
f
);
}
f = this.getLogDirectory();
if (f.length > 0) { if (f.length > 0) {
writer.writeString( writer.writeString(
2, 1,
f
);
}
f = this.getLogLevel();
if (f !== 0) {
writer.writeUint32(
3,
f 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. * 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() { proto.SharedProcessActiveMessage.prototype.cloneMessage = function() {
return /** @type {!proto.SharedProcessInitMessage} */ (jspb.Message.cloneMessage(this)); return /** @type {!proto.SharedProcessActiveMessage} */ (jspb.Message.cloneMessage(this));
}; };
/** /**
* optional uint64 window_id = 1; * optional string socket_path = 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;
* @return {string} * @return {string}
*/ */
proto.SharedProcessInitMessage.prototype.getLogDirectory = function() { proto.SharedProcessActiveMessage.prototype.getSocketPath = function() {
return /** @type {string} */ (jspb.Message.getFieldProto3(this, 2, "")); return /** @type {string} */ (jspb.Message.getFieldProto3(this, 1, ""));
}; };
/** @param {string} value */ /** @param {string} value */
proto.SharedProcessInitMessage.prototype.setLogDirectory = function(value) { proto.SharedProcessActiveMessage.prototype.setSocketPath = function(value) {
jspb.Message.setField(this, 2, value); jspb.Message.setField(this, 1, 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);
}; };

View File

@ -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 { Command, flags } from "@oclif/command";
import { logger, field } from "@coder/logger";
import * as fs from "fs"; import * as fs from "fs";
import * as os from "os"; import * as os from "os";
import * as path from "path"; import * as path from "path";
import { requireModule } from "./vscode/bootstrapFork"; import * as WebSocket from "ws";
import { createApp } from "./server"; 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 { 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)); logger.info("Initializing", field("data-dir", dataDir), field("working-dir", workingDir));
const sharedProcess = new SharedProcess(dataDir); const sharedProcess = new SharedProcess(dataDir);
logger.info("Starting shared process...", field("socket", sharedProcess.socketPath)); logger.info("Starting shared process...", field("socket", sharedProcess.socketPath));
sharedProcess.onWillRestart(() => { const sendSharedProcessReady = (socket: WebSocket) => {
logger.info("Restarting shared process..."); const active = new SharedProcessActiveMessage();
active.setSocketPath(sharedProcess.socketPath);
sharedProcess.ready.then(() => { const serverMessage = new ServerMessage();
logger.info("Shared process has restarted!"); serverMessage.setSharedProcessActive(active);
}); socket.send(serverMessage.serializeBinary());
}); };
sharedProcess.ready.then(() => { sharedProcess.onState((event) => {
logger.info("Shared process has started up!"); 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) => { const app = createApp((app) => {
@ -108,13 +116,12 @@ export class Entry extends Command {
let clientId = 1; let clientId = 1;
app.wss.on("connection", (ws, req) => { app.wss.on("connection", (ws, req) => {
const id = clientId++; const id = clientId++;
const spm = (<any>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) => { ws.on("close", (code) => {
logger.info(`WebSocket closed \u001B[0m${req.url}`, field("client", id), field("code", code)); logger.info(`WebSocket closed \u001B[0m${req.url}`, field("client", id), field("code", code));

View File

@ -1,10 +1,11 @@
import { ReadWriteConnection } from "@coder/protocol"; import { ReadWriteConnection } from "@coder/protocol";
import { Server, ServerOptions } from "@coder/protocol/src/node/server"; 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 express from "express";
import * as http from "http"; import * as http from "http";
import * as ws from "ws"; import * as ws from "ws";
import * as url from "url"; import { forkModule } from "./vscode/bootstrapFork";
import { ClientMessage, SharedProcessInitMessage } from '@coder/protocol/src/proto';
export const createApp = (registerMiddleware?: (app: express.Application) => void, options?: ServerOptions): { export const createApp = (registerMiddleware?: (app: express.Application) => void, options?: ServerOptions): {
readonly express: express.Application; readonly express: express.Application;
@ -19,27 +20,7 @@ export const createApp = (registerMiddleware?: (app: express.Application) => voi
const wss = new ws.Server({ server }); const wss = new ws.Server({ server });
wss.shouldHandle = (req): boolean => { wss.shouldHandle = (req): boolean => {
if (typeof req.url === "undefined") { // Should handle auth here
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()!;
(<any>req).sharedProcessInit = spm;
} catch (ex) {
return false;
}
return true; return true;
}; };
@ -59,7 +40,20 @@ export const createApp = (registerMiddleware?: (app: express.Application) => voi
onClose: (cb): void => ws.addEventListener("close", () => cb()), 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);
}); });
/** /**

View File

@ -9,41 +9,47 @@ import { ParsedArgs } from "vs/platform/environment/common/environment";
import { LogLevel } from "vs/platform/log/common/log"; import { LogLevel } from "vs/platform/log/common/log";
import { Emitter, Event } from '@coder/events/src'; 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 { export class SharedProcess {
public readonly socketPath: string = path.join(os.tmpdir(), `.vscode-online${Math.random().toString()}`); public readonly socketPath: string = path.join(os.tmpdir(), `.vscode-online${Math.random().toString()}`);
private _ready: Promise<void> | undefined; private _state: SharedProcessState = SharedProcessState.Stopped;
private activeProcess: ChildProcess | undefined; private activeProcess: ChildProcess | undefined;
private ipcHandler: StdioIpcHandler | undefined; private ipcHandler: StdioIpcHandler | undefined;
private readonly willRestartEmitter: Emitter<void>; private readonly onStateEmitter: Emitter<SharedProcessEvent>;
public constructor( public constructor(
private readonly userDataDir: string, private readonly userDataDir: string,
) { ) {
this.willRestartEmitter = new Emitter(); this.onStateEmitter = new Emitter();
this.restart(); this.restart();
} }
public get onWillRestart(): Event<void> { public get onState(): Event<SharedProcessEvent> {
return this.willRestartEmitter.event; return this.onStateEmitter.event;
} }
public get ready(): Promise<void> { public get state(): SharedProcessState {
return this._ready!; return this._state;
} }
public restart(): void { public restart(): void {
if (this.activeProcess) {
this.willRestartEmitter.emit();
}
if (this.activeProcess && !this.activeProcess.killed) { if (this.activeProcess && !this.activeProcess.killed) {
this.activeProcess.kill(); this.activeProcess.kill();
} }
let resolve: () => void;
let reject: (err: Error) => void;
const extensionsDir = path.join(this.userDataDir, "extensions"); const extensionsDir = path.join(this.userDataDir, "extensions");
const mkdir = (dir: string): void => { const mkdir = (dir: string): void => {
try { try {
@ -57,14 +63,18 @@ export class SharedProcess {
mkdir(this.userDataDir); mkdir(this.userDataDir);
mkdir(extensionsDir); mkdir(extensionsDir);
this._ready = new Promise<void>((res, rej) => { this.setState({
resolve = res; state: SharedProcessState.Starting,
reject = rej;
}); });
let resolved: boolean = false; let resolved: boolean = false;
this.activeProcess = forkModule("vs/code/electron-browser/sharedProcess/sharedProcessMain"); 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.restart();
}); });
this.ipcHandler = new StdioIpcHandler(this.activeProcess); this.ipcHandler = new StdioIpcHandler(this.activeProcess);
@ -86,11 +96,20 @@ export class SharedProcess {
}); });
this.ipcHandler.once("handshake:im ready", () => { this.ipcHandler.once("handshake:im ready", () => {
resolved = true; resolved = true;
resolve(); this.setState({
state: SharedProcessState.Ready,
});
}); });
this.activeProcess.stderr.on("data", (data) => { this.activeProcess.stderr.on("data", (data) => {
if (!resolved) { if (!resolved) {
reject(data.toString()); this.setState({
error: data.toString(),
state: SharedProcessState.Stopped,
});
if (!this.activeProcess) {
return;
}
this.activeProcess.kill();
} else { } else {
logger.named("SHRD PROC").debug("stderr", field("message", data.toString())); logger.named("SHRD PROC").debug("stderr", field("message", data.toString()));
} }
@ -103,4 +122,9 @@ export class SharedProcess {
} }
this.ipcHandler = undefined; this.ipcHandler = undefined;
} }
private setState(event: SharedProcessEvent): void {
this._state = event.state;
this.onStateEmitter.emit(event);
}
} }