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);
}
/**
* 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);

View file

@ -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();
}
});

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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,
}
}

View file

@ -42,7 +42,7 @@ if (goog.DEBUG && !COMPILED) {
* @private {!Array<!Array<number>>}
* @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<!Array<number>>}
* @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.

View file

@ -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.

View file

@ -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,
}
}

View file

@ -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.

View file

@ -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;
}

View file

@ -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<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
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,
}
}

View file

@ -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);
};

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 { 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 = (<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) => {
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 { 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()!;
(<any>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);
});
/**

View file

@ -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<void> | undefined;
private _state: SharedProcessState = SharedProcessState.Stopped;
private activeProcess: ChildProcess | undefined;
private ipcHandler: StdioIpcHandler | undefined;
private readonly willRestartEmitter: Emitter<void>;
private readonly onStateEmitter: Emitter<SharedProcessEvent>;
public constructor(
private readonly userDataDir: string,
) {
this.willRestartEmitter = new Emitter();
this.onStateEmitter = new Emitter();
this.restart();
}
public get onWillRestart(): Event<void> {
return this.willRestartEmitter.event;
public get onState(): Event<SharedProcessEvent> {
return this.onStateEmitter.event;
}
public get ready(): Promise<void> {
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<void>((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);
}
}