Extension host (#20)

* Implement net.Server

* Move Socket class into Client

This way we don't need to expose anything.

* Remove some unused imports

* Pass environment variables to bootstrap fork

* Add debug log for when socket disconnects from server

* Use VSCODE_ALLOW_IO for shared process only

* Extension host can send messages now

* Support callback for logging

This lets us do potentially expensive operations which will only be
performed if the log level is sufficiently low.

* Stop extension host from committing suicide

* Blank line

* Add static serve (#21)

* Add extension URLs

* how did i remove this

* Fix writing an empty string

* Implement dialogs on window service
This commit is contained in:
Asher
2019-01-25 18:18:21 -06:00
committed by Kyle Carberry
parent e43e7b36e7
commit c6d35d098a
27 changed files with 431 additions and 793 deletions

View File

@@ -1,13 +1,16 @@
import { ReadWriteConnection, InitData, OperatingSystem, ISharedProcessData } from "../common/connection";
import { NewEvalMessage, ServerMessage, EvalDoneMessage, EvalFailedMessage, TypedValue, ClientMessage, NewSessionMessage, TTYDimensions, SessionOutputMessage, CloseSessionInputMessage, WorkingInitMessage, NewConnectionMessage, NewServerMessage } from "../proto";
import { NewEvalMessage, ServerMessage, EvalDoneMessage, EvalFailedMessage, TypedValue, ClientMessage, NewSessionMessage, TTYDimensions, SessionOutputMessage, CloseSessionInputMessage, WorkingInitMessage } from "../proto";
import { Emitter, Event } from "@coder/events";
import { logger, field } from "@coder/logger";
import { ChildProcess, SpawnOptions, ServerProcess, ServerSocket, Socket, ServerListener, Server } from "./command";
import { ChildProcess, SpawnOptions, ForkOptions, ServerProcess, ServerSocket, Socket, ServerListener, Server } from "./command";
/**
* Client accepts an arbitrary connection intended to communicate with the Server.
*/
export class Client {
public Socket: typeof ServerSocket;
private evalId: number = 0;
private evalDoneEmitter: Emitter<EvalDoneMessage> = new Emitter();
private evalFailedEmitter: Emitter<EvalFailedMessage> = new Emitter();
@@ -41,6 +44,15 @@ export class Client {
}
});
const that = this;
this.Socket = class extends ServerSocket {
public constructor() {
super(that.connection, that.connectionId++, that.registerConnection);
}
};
this.initDataPromise = new Promise((resolve): void => {
this.initDataEmitter.event(resolve);
});
@@ -77,7 +89,7 @@ export class Client {
const newEval = new NewEvalMessage();
const id = this.evalId++;
newEval.setId(id);
newEval.setArgsList([a1, a2, a3, a4, a5, a6].filter(a => a).map(a => JSON.stringify(a)));
newEval.setArgsList([a1, a2, a3, a4, a5, a6].filter(a => typeof a !== "undefined").map(a => JSON.stringify(a)));
newEval.setFunction(func.toString());
const clientMsg = new ClientMessage();
@@ -158,7 +170,7 @@ export class Client {
* @param args Args to add for the module
* @param options Options to execute
*/
public fork(modulePath: string, args: string[] = [], options?: SpawnOptions): ChildProcess {
public fork(modulePath: string, args: string[] = [], options?: ForkOptions): ChildProcess {
return this.doSpawn(modulePath, args, options, true);
}
@@ -167,27 +179,17 @@ export class Client {
* 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 bootstrapFork(modulePath: string, args: string[] = [], options?: ForkOptions): ChildProcess {
return this.doSpawn(modulePath, args, options, true, true);
}
public createConnection(path: string, callback?: () => void): Socket;
public createConnection(port: number, callback?: () => void): Socket;
public createConnection(target: string | number, callback?: () => void): Socket {
public createConnection(path: string, callback?: Function): Socket;
public createConnection(port: number, callback?: Function): Socket;
public createConnection(target: string | number, callback?: Function): Socket;
public createConnection(target: string | number, callback?: Function): Socket {
const id = this.connectionId++;
const newCon = new NewConnectionMessage();
newCon.setId(id);
if (typeof target === "string") {
newCon.setPath(target);
} else {
newCon.setPort(target);
}
const clientMsg = new ClientMessage();
clientMsg.setNewConnection(newCon);
this.connection.send(clientMsg.serializeBinary());
const socket = new ServerSocket(this.connection, id, callback);
this.connections.set(id, socket);
const socket = new ServerSocket(this.connection, id, this.registerConnection);
socket.connect(target, callback);
return socket;
}
@@ -214,7 +216,9 @@ export class Client {
}
if (options.env) {
Object.keys(options.env).forEach((envKey) => {
newSess.getEnvMap().set(envKey, options.env![envKey]);
if (options.env![envKey]) {
newSess.getEnvMap().set(envKey, options.env![envKey].toString());
}
});
}
if (options.tty) {
@@ -356,9 +360,9 @@ export class Client {
return;
}
const conId = message.getServerConnectionEstablished()!.getConnectionId();
const serverSocket = new ServerSocket(this.connection, conId);
const serverSocket = new ServerSocket(this.connection, conId, this.registerConnection);
this.registerConnection(conId, serverSocket);
serverSocket.emit("connect");
this.connections.set(conId, serverSocket);
s.emit("connection", serverSocket);
} else if (message.getServerFailure()) {
const s = this.servers.get(message.getServerFailure()!.getId());
@@ -376,4 +380,12 @@ export class Client {
this.servers.delete(message.getServerClose()!.getId());
}
}
private registerConnection = (id: number, socket: ServerSocket): void => {
if (this.connections.has(id)) {
throw new Error(`${id} is already registered`);
}
this.connections.set(id, socket);
}
}

View File

@@ -1,7 +1,7 @@
import * as events from "events";
import * as stream from "stream";
import { ReadWriteConnection } from "../common/connection";
import { ShutdownSessionMessage, ClientMessage, WriteToSessionMessage, ResizeSessionTTYMessage, TTYDimensions as ProtoTTYDimensions, ConnectionOutputMessage, ConnectionCloseMessage, ServerCloseMessage, NewServerMessage } from "../proto";
import { NewConnectionMessage, ShutdownSessionMessage, ClientMessage, WriteToSessionMessage, ResizeSessionTTYMessage, TTYDimensions as ProtoTTYDimensions, ConnectionOutputMessage, ConnectionCloseMessage, ServerCloseMessage, NewServerMessage } from "../proto";
export interface TTYDimensions {
readonly columns: number;
@@ -10,10 +10,15 @@ export interface TTYDimensions {
export interface SpawnOptions {
cwd?: string;
env?: { readonly [key: string]: string };
env?: { [key: string]: string };
tty?: TTYDimensions;
}
export interface ForkOptions {
cwd?: string;
env?: { [key: string]: string };
}
export interface ChildProcess {
readonly stdin: stream.Writable;
readonly stdout: stream.Readable;
@@ -119,6 +124,9 @@ export interface Socket {
write(buffer: Buffer): void;
end(): void;
connect(path: string, callback?: () => void): void;
connect(port: number, callback?: () => void): void;
addListener(event: "data", listener: (data: Buffer) => void): this;
addListener(event: "close", listener: (hasError: boolean) => void): this;
addListener(event: "connect", listener: () => void): this;
@@ -151,21 +159,37 @@ export class ServerSocket extends events.EventEmitter implements Socket {
public readable: boolean = true;
private _destroyed: boolean = false;
private _connecting: boolean = true;
private _connecting: boolean = false;
public constructor(
private readonly connection: ReadWriteConnection,
private readonly id: number,
connectCallback?: () => void,
private readonly beforeConnect: (id: number, socket: ServerSocket) => void,
) {
super();
}
if (connectCallback) {
this.once("connect", () => {
this._connecting = false;
connectCallback();
});
public connect(target: string | number, callback?: Function): void {
this._connecting = true;
this.beforeConnect(this.id, this);
this.once("connect", () => {
this._connecting = false;
if (callback) {
callback();
}
});
const newCon = new NewConnectionMessage();
newCon.setId(this.id);
if (typeof target === "string") {
newCon.setPath(target);
} else {
newCon.setPort(target);
}
const clientMsg = new ClientMessage();
clientMsg.setNewConnection(newCon);
this.connection.send(clientMsg.serializeBinary());
}
public get destroyed(): boolean {
@@ -236,6 +260,7 @@ export class ServerSocket extends events.EventEmitter implements Socket {
public setDefaultEncoding(encoding: string): this {
throw new Error("Method not implemented.");
}
}
export interface Server {
@@ -266,6 +291,7 @@ export interface Server {
}
export class ServerListener extends events.EventEmitter implements Server {
private _listening: boolean = false;
public constructor(
@@ -309,11 +335,12 @@ export class ServerListener extends events.EventEmitter implements Server {
const clientMsg = new ClientMessage();
clientMsg.setServerClose(closeMsg);
this.connection.send(clientMsg.serializeBinary());
if (callback) {
callback();
}
return this;
}
}
}

View File

@@ -40,19 +40,38 @@ export class CP {
);
});
// @ts-ignore
// @ts-ignore TODO: not fully implemented
return childProcess;
}
public fork = (modulePath: string, args?: ReadonlyArray<string> | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
//@ts-ignore
return this.client.bootstrapFork(options && options.env && options.env.AMD_ENTRYPOINT || modulePath);
public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
if (options && options.env && options.env.AMD_ENTRYPOINT) {
// @ts-ignore TODO: not fully implemented
return this.client.bootstrapFork(
options.env.AMD_ENTRYPOINT,
Array.isArray(args) ? args : [],
// @ts-ignore TODO: env is a different type
Array.isArray(args) || !args ? options : args,
);
}
// @ts-ignore TODO: not fully implemented
return this.client.fork(
modulePath,
Array.isArray(args) ? args : [],
// @ts-ignore TODO: env is a different type
Array.isArray(args) || !args ? options : args,
);
}
public spawn = (command: string, args?: ReadonlyArray<string> | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => {
// TODO: fix this ignore. Should check for args or options here
//@ts-ignore
return this.client.spawn(command, args, options);
public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => {
// @ts-ignore TODO: not fully implemented
return this.client.spawn(
command,
Array.isArray(args) ? args : [],
// @ts-ignore TODO: env is a different type
Array.isArray(args) || !args ? options : args,
);
}
}

View File

@@ -358,9 +358,9 @@ export class FS {
return util.promisify(fs.read)(fd, buffer, 0, length, position).then((resp) => {
return {
bytesRead: resp.bytesRead,
content: buffer.toString("utf8"),
content: (resp.bytesRead < buffer.length ? buffer.slice(0, resp.bytesRead) : buffer).toString("utf8"),
};
}):
});
}, fd, length, position).then((resp) => {
const newBuf = Buffer.from(resp.content, "utf8");
buffer.set(newBuf, offset);

View File

@@ -13,7 +13,8 @@ export class Net implements NodeNet {
) {}
public get Socket(): typeof net.Socket {
throw new Error("not implemented");
// @ts-ignore
return this.client.Socket;
}
public get Server(): typeof net.Server {
@@ -24,10 +25,12 @@ export class Net implements NodeNet {
throw new Error("not implemented");
}
// tslint:disable-next-line no-any
public createConnection(...args: any[]): net.Socket {
//@ts-ignore
return this.client.createConnection(...args) as net.Socket;
public createConnection(target: string | number | net.NetConnectOpts, host?: string | Function, callback?: Function): net.Socket {
if (typeof target === "object") {
throw new Error("not implemented");
}
return this.client.createConnection(target, typeof host === "function" ? host : callback) as net.Socket;
}
public isIP(_input: string): number {