code-server/packages/protocol/src/node/server.ts

342 lines
10 KiB
TypeScript
Raw Normal View History

import { mkdirp } from "fs-extra";
import * as os from "os";
import { field, logger} from "@coder/logger";
2019-01-12 02:33:44 +07:00
import { ReadWriteConnection } from "../common/connection";
import { Module, ServerProxy } from "../common/proxy";
import { isPromise, isProxy, moduleToProto, parse, platformToProto, protoToModule, stringify } from "../common/util";
import { CallbackMessage, ClientMessage, EventMessage, FailMessage, MethodMessage, NamedCallbackMessage, NamedEventMessage, NumberedCallbackMessage, NumberedEventMessage, Pong, ServerMessage, SuccessMessage, WorkingInitMessage } from "../proto";
import { ChildProcessModuleProxy, ForkProvider, FsModuleProxy, NetModuleProxy, NodePtyModuleProxy, SpdlogModuleProxy, TrashModuleProxy } from "./modules";
// tslint:disable no-any
2019-01-12 02:33:44 +07:00
export interface ServerOptions {
readonly workingDirectory: string;
readonly dataDirectory: string;
readonly cacheDirectory: string;
readonly builtInExtensionsDirectory: string;
readonly fork?: ForkProvider;
}
interface ProxyData {
disposeTimeout?: number | NodeJS.Timer;
instance: any;
}
2019-01-12 02:33:44 +07:00
export class Server {
private proxyId = 0;
private readonly proxies = new Map<number | Module, ProxyData>();
private disconnected: boolean = false;
private responseTimeout = 10000;
2019-01-24 07:00:38 +07:00
2019-01-12 02:33:44 +07:00
public constructor(
private readonly connection: ReadWriteConnection,
private readonly options?: ServerOptions,
2019-01-12 02:33:44 +07:00
) {
connection.onMessage(async (data) => {
2019-01-12 02:33:44 +07:00
try {
await this.handleMessage(ClientMessage.deserializeBinary(data));
2019-01-12 02:33:44 +07:00
} catch (ex) {
logger.error(
"Failed to handle client message",
field("length", data.byteLength),
field("exception", {
message: ex.message,
stack: ex.stack,
}),
);
2019-01-12 02:33:44 +07:00
}
});
connection.onClose(() => {
this.disconnected = true;
logger.trace(() => [
"disconnected from client",
field("proxies", this.proxies.size),
]);
this.proxies.forEach((proxy, proxyId) => {
if (isProxy(proxy.instance)) {
proxy.instance.dispose();
}
this.removeProxy(proxyId);
});
});
this.storeProxy(new ChildProcessModuleProxy(this.options ? this.options.fork : undefined), Module.ChildProcess);
this.storeProxy(new FsModuleProxy(), Module.Fs);
this.storeProxy(new NetModuleProxy(), Module.Net);
this.storeProxy(new NodePtyModuleProxy(), Module.NodePty);
this.storeProxy(new SpdlogModuleProxy(), Module.Spdlog);
this.storeProxy(new TrashModuleProxy(), Module.Trash);
Make everything use active evals (#30) * Add trace log level * Use active eval to implement spdlog * Split server/client active eval interfaces Since all properties are *not* valid on both sides * +200% fire resistance * Implement exec using active evaluations * Fully implement child process streams * Watch impl, move child_process back to explicitly adding events Automatically forwarding all events might be the right move, but wanna think/discuss it a bit more because it didn't come out very cleanly. * Would you like some args with that callback? * Implement the rest of child_process using active evals * Rampant memory leaks Emit "kill" to active evaluations when client disconnects in order to kill processes. Most likely won't be the final solution. * Resolve some minor issues with output panel * Implement node-pty with active evals * Provide clearTimeout to vm sandbox * Implement socket with active evals * Extract some callback logic Also remove some eval interfaces, need to re-think those. * Implement net.Server and remainder of net.Socket using active evals * Implement dispose for active evaluations * Use trace for express requests * Handle sending buffers through evaluation events * Make event logging a bit more clear * Fix some errors due to us not actually instantiating until connect/listen * is this a commit message? * We can just create the evaluator in the ctor Not sure what I was thinking. * memory leak for you, memory leak for everyone * it's a ternary now * Don't dispose automatically on close or error The code may or may not be disposable at that point. * Handle parsing buffers on the client side as well * Remove unused protobuf * Remove TypedValue * Remove unused forkProvider and test * Improve dispose pattern for active evals * Socket calls close after error; no need to bind both * Improve comment * Comment is no longer wishy washy due to explicit boolean * Simplify check for sendHandle and options * Replace _require with __non_webpack_require__ Webpack will then replace this with `require` which we then provide to the vm sandbox. * Provide path.parse * Prevent original-fs from loading * Start with a pid of -1 vscode immediately checks the PID to see if the debug process launch correctly, but of course we don't get the pid synchronously. * Pass arguments to bootstrap-fork * Fully implement streams Was causing errors because internally the stream would set this.writing to true and it would never become false, so subsequent messages would never send. * Fix serializing errors and streams emitting errors multiple times * Was emitting close to data * Fix missing path for spawned processes * Move evaluation onDispose call Now it's accurate and runs when the active evaluation has actually disposed. * Fix promisifying fs.exists * Fix some active eval callback issues * Patch existsSync in debug adapter
2019-02-19 23:17:03 +07:00
if (!this.options) {
logger.warn("No server options provided. InitMessage will not be sent.");
return;
}
Promise.all([
mkdirp(this.options.cacheDirectory),
mkdirp(this.options.dataDirectory),
mkdirp(this.options.workingDirectory),
]).catch((error) => {
logger.error(error.message, field("error", error));
});
const initMsg = new WorkingInitMessage();
Make everything use active evals (#30) * Add trace log level * Use active eval to implement spdlog * Split server/client active eval interfaces Since all properties are *not* valid on both sides * +200% fire resistance * Implement exec using active evaluations * Fully implement child process streams * Watch impl, move child_process back to explicitly adding events Automatically forwarding all events might be the right move, but wanna think/discuss it a bit more because it didn't come out very cleanly. * Would you like some args with that callback? * Implement the rest of child_process using active evals * Rampant memory leaks Emit "kill" to active evaluations when client disconnects in order to kill processes. Most likely won't be the final solution. * Resolve some minor issues with output panel * Implement node-pty with active evals * Provide clearTimeout to vm sandbox * Implement socket with active evals * Extract some callback logic Also remove some eval interfaces, need to re-think those. * Implement net.Server and remainder of net.Socket using active evals * Implement dispose for active evaluations * Use trace for express requests * Handle sending buffers through evaluation events * Make event logging a bit more clear * Fix some errors due to us not actually instantiating until connect/listen * is this a commit message? * We can just create the evaluator in the ctor Not sure what I was thinking. * memory leak for you, memory leak for everyone * it's a ternary now * Don't dispose automatically on close or error The code may or may not be disposable at that point. * Handle parsing buffers on the client side as well * Remove unused protobuf * Remove TypedValue * Remove unused forkProvider and test * Improve dispose pattern for active evals * Socket calls close after error; no need to bind both * Improve comment * Comment is no longer wishy washy due to explicit boolean * Simplify check for sendHandle and options * Replace _require with __non_webpack_require__ Webpack will then replace this with `require` which we then provide to the vm sandbox. * Provide path.parse * Prevent original-fs from loading * Start with a pid of -1 vscode immediately checks the PID to see if the debug process launch correctly, but of course we don't get the pid synchronously. * Pass arguments to bootstrap-fork * Fully implement streams Was causing errors because internally the stream would set this.writing to true and it would never become false, so subsequent messages would never send. * Fix serializing errors and streams emitting errors multiple times * Was emitting close to data * Fix missing path for spawned processes * Move evaluation onDispose call Now it's accurate and runs when the active evaluation has actually disposed. * Fix promisifying fs.exists * Fix some active eval callback issues * Patch existsSync in debug adapter
2019-02-19 23:17:03 +07:00
initMsg.setDataDirectory(this.options.dataDirectory);
initMsg.setWorkingDirectory(this.options.workingDirectory);
initMsg.setBuiltinExtensionsDir(this.options.builtInExtensionsDirectory);
initMsg.setHomeDirectory(os.homedir());
initMsg.setTmpDirectory(os.tmpdir());
initMsg.setOperatingSystem(platformToProto(os.platform()));
2019-02-28 01:43:00 +07:00
initMsg.setShell(os.userInfo().shell || global.process.env.SHELL);
const srvMsg = new ServerMessage();
srvMsg.setInit(initMsg);
connection.send(srvMsg.serializeBinary());
2019-01-12 02:33:44 +07:00
}
/**
* Handle all messages from the client.
*/
private async handleMessage(message: ClientMessage): Promise<void> {
if (message.hasMethod()) {
await this.runMethod(message.getMethod()!);
2019-03-05 10:26:17 +07:00
} else if (message.hasPing()) {
logger.trace("ping");
const srvMsg = new ServerMessage();
srvMsg.setPong(new Pong());
this.connection.send(srvMsg.serializeBinary());
} else {
Make everything use active evals (#30) * Add trace log level * Use active eval to implement spdlog * Split server/client active eval interfaces Since all properties are *not* valid on both sides * +200% fire resistance * Implement exec using active evaluations * Fully implement child process streams * Watch impl, move child_process back to explicitly adding events Automatically forwarding all events might be the right move, but wanna think/discuss it a bit more because it didn't come out very cleanly. * Would you like some args with that callback? * Implement the rest of child_process using active evals * Rampant memory leaks Emit "kill" to active evaluations when client disconnects in order to kill processes. Most likely won't be the final solution. * Resolve some minor issues with output panel * Implement node-pty with active evals * Provide clearTimeout to vm sandbox * Implement socket with active evals * Extract some callback logic Also remove some eval interfaces, need to re-think those. * Implement net.Server and remainder of net.Socket using active evals * Implement dispose for active evaluations * Use trace for express requests * Handle sending buffers through evaluation events * Make event logging a bit more clear * Fix some errors due to us not actually instantiating until connect/listen * is this a commit message? * We can just create the evaluator in the ctor Not sure what I was thinking. * memory leak for you, memory leak for everyone * it's a ternary now * Don't dispose automatically on close or error The code may or may not be disposable at that point. * Handle parsing buffers on the client side as well * Remove unused protobuf * Remove TypedValue * Remove unused forkProvider and test * Improve dispose pattern for active evals * Socket calls close after error; no need to bind both * Improve comment * Comment is no longer wishy washy due to explicit boolean * Simplify check for sendHandle and options * Replace _require with __non_webpack_require__ Webpack will then replace this with `require` which we then provide to the vm sandbox. * Provide path.parse * Prevent original-fs from loading * Start with a pid of -1 vscode immediately checks the PID to see if the debug process launch correctly, but of course we don't get the pid synchronously. * Pass arguments to bootstrap-fork * Fully implement streams Was causing errors because internally the stream would set this.writing to true and it would never become false, so subsequent messages would never send. * Fix serializing errors and streams emitting errors multiple times * Was emitting close to data * Fix missing path for spawned processes * Move evaluation onDispose call Now it's accurate and runs when the active evaluation has actually disposed. * Fix promisifying fs.exists * Fix some active eval callback issues * Patch existsSync in debug adapter
2019-02-19 23:17:03 +07:00
throw new Error("unknown message type");
2019-01-12 02:33:44 +07:00
}
}
/**
* Run a method on a proxy.
*/
private async runMethod(message: MethodMessage): Promise<void> {
const proxyMessage = message.getNamedProxy()! || message.getNumberedProxy()!;
const id = proxyMessage.getId();
const proxyId = message.hasNamedProxy()
? protoToModule(message.getNamedProxy()!.getModule())
: message.getNumberedProxy()!.getProxyId();
const method = proxyMessage.getMethod();
const args = proxyMessage.getArgsList().map((a) => parse(
a,
(id, args) => this.sendCallback(proxyId, id, args),
));
logger.trace(() => [
"received",
field("id", id),
field("proxyId", proxyId),
field("method", method),
field("args", proxyMessage.getArgsList()),
]);
let response: any;
try {
const proxy = this.getProxy(proxyId);
if (typeof proxy.instance[method] !== "function") {
throw new Error(`"${method}" is not a function on proxy ${proxyId}`);
}
response = proxy.instance[method](...args);
// We wait for the client to call "dispose" instead of doing it onDone to
// ensure all the messages it sent get processed before we get rid of it.
if (method === "dispose") {
this.removeProxy(proxyId);
}
// Proxies must always return promises.
if (!isPromise(response)) {
throw new Error('"${method}" must return a promise');
}
} catch (error) {
logger.error(
error.message,
field("type", typeof response),
field("proxyId", proxyId),
);
this.sendException(id, error);
}
try {
this.sendResponse(id, await response);
} catch (error) {
this.sendException(id, error);
}
}
/**
* Send a callback to the client.
*/
private sendCallback(proxyId: number | Module, callbackId: number, args: any[]): void {
const stringifiedArgs = args.map((a) => this.stringify(a));
logger.trace(() => [
"sending callback",
field("proxyId", proxyId),
field("callbackId", callbackId),
field("args", stringifiedArgs),
]);
const message = new CallbackMessage();
let callbackMessage: NamedCallbackMessage | NumberedCallbackMessage;
if (typeof proxyId === "string") {
callbackMessage = new NamedCallbackMessage();
callbackMessage.setModule(moduleToProto(proxyId));
message.setNamedCallback(callbackMessage);
} else {
callbackMessage = new NumberedCallbackMessage();
callbackMessage.setProxyId(proxyId);
message.setNumberedCallback(callbackMessage);
}
callbackMessage.setCallbackId(callbackId);
callbackMessage.setArgsList(stringifiedArgs);
const serverMessage = new ServerMessage();
serverMessage.setCallback(message);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Store a proxy and bind events to send them back to the client.
*/
private storeProxy(instance: ServerProxy): number;
private storeProxy(instance: any, moduleProxyId: Module): Module;
private storeProxy(instance: ServerProxy | any, moduleProxyId?: Module): number | Module {
// In case we disposed while waiting for a function to return.
if (this.disconnected) {
if (isProxy(instance)) {
instance.dispose();
}
throw new Error("disposed");
}
const proxyId = moduleProxyId || this.proxyId++;
logger.trace(() => [
"storing proxy",
field("proxyId", proxyId),
]);
this.proxies.set(proxyId, { instance });
if (isProxy(instance)) {
instance.onEvent((event, ...args) => this.sendEvent(proxyId, event, ...args));
instance.onDone(() => {
// It might have finished because we disposed it due to a disconnect.
if (!this.disconnected) {
this.sendEvent(proxyId, "done");
this.getProxy(proxyId).disposeTimeout = setTimeout(() => {
instance.dispose();
this.removeProxy(proxyId);
}, this.responseTimeout);
}
});
}
return proxyId;
}
/**
* Send an event to the client.
*/
private sendEvent(proxyId: number | Module, event: string, ...args: any[]): void {
const stringifiedArgs = args.map((a) => this.stringify(a));
logger.trace(() => [
"sending event",
field("proxyId", proxyId),
field("event", event),
field("args", stringifiedArgs),
]);
const message = new EventMessage();
let eventMessage: NamedEventMessage | NumberedEventMessage;
if (typeof proxyId === "string") {
eventMessage = new NamedEventMessage();
eventMessage.setModule(moduleToProto(proxyId));
message.setNamedEvent(eventMessage);
} else {
eventMessage = new NumberedEventMessage();
eventMessage.setProxyId(proxyId);
message.setNumberedEvent(eventMessage);
}
eventMessage.setEvent(event);
eventMessage.setArgsList(stringifiedArgs);
const serverMessage = new ServerMessage();
serverMessage.setEvent(message);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Send a response back to the client.
*/
private sendResponse(id: number, response: any): void {
const stringifiedResponse = this.stringify(response);
logger.trace(() => [
"sending resolve",
field("id", id),
field("response", stringifiedResponse),
]);
const successMessage = new SuccessMessage();
successMessage.setId(id);
successMessage.setResponse(stringifiedResponse);
const serverMessage = new ServerMessage();
serverMessage.setSuccess(successMessage);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Send an exception back to the client.
*/
private sendException(id: number, error: Error): void {
const stringifiedError = stringify(error);
logger.trace(() => [
"sending reject",
field("id", id) ,
field("response", stringifiedError),
]);
const failedMessage = new FailMessage();
failedMessage.setId(id);
failedMessage.setResponse(stringifiedError);
const serverMessage = new ServerMessage();
serverMessage.setFail(failedMessage);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Call after disposing a proxy.
*/
private removeProxy(proxyId: number | Module): void {
clearTimeout(this.getProxy(proxyId).disposeTimeout as any);
this.proxies.delete(proxyId);
logger.trace(() => [
"disposed and removed proxy",
field("proxyId", proxyId),
field("proxies", this.proxies.size),
]);
}
private stringify(value: any): string {
return stringify(value, undefined, (p) => this.storeProxy(p));
}
private getProxy(proxyId: number | Module): ProxyData {
if (!this.proxies.has(proxyId)) {
throw new Error(`proxy ${proxyId} disposed too early`);
}
return this.proxies.get(proxyId)!;
}
}