2019-03-13 03:45:44 +07:00
|
|
|
import { mkdirp } from "fs-extra";
|
2019-01-16 01:36:09 +07:00
|
|
|
import * as os from "os";
|
2019-03-27 01:01:25 +07:00
|
|
|
import { field, logger} from "@coder/logger";
|
2019-01-12 02:33:44 +07:00
|
|
|
import { ReadWriteConnection } from "../common/connection";
|
2019-03-27 01:01:25 +07:00
|
|
|
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
|
|
|
|
2019-01-16 01:36:09 +07:00
|
|
|
export interface ServerOptions {
|
|
|
|
readonly workingDirectory: string;
|
|
|
|
readonly dataDirectory: string;
|
2019-03-12 23:12:50 +07:00
|
|
|
readonly cacheDirectory: string;
|
2019-02-06 00:15:20 +07:00
|
|
|
readonly builtInExtensionsDirectory: string;
|
2019-02-23 04:56:29 +07:00
|
|
|
readonly fork?: ForkProvider;
|
2019-01-16 01:36:09 +07:00
|
|
|
}
|
|
|
|
|
2019-03-27 01:01:25 +07:00
|
|
|
interface ProxyData {
|
|
|
|
disposeTimeout?: number | NodeJS.Timer;
|
|
|
|
instance: any;
|
|
|
|
}
|
|
|
|
|
2019-01-12 02:33:44 +07:00
|
|
|
export class Server {
|
2019-03-27 01:01:25 +07:00
|
|
|
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,
|
2019-01-19 06:08:44 +07:00
|
|
|
private readonly options?: ServerOptions,
|
2019-01-12 02:33:44 +07:00
|
|
|
) {
|
2019-03-27 01:01:25 +07:00
|
|
|
connection.onMessage(async (data) => {
|
2019-01-12 02:33:44 +07:00
|
|
|
try {
|
2019-03-27 01:01:25 +07:00
|
|
|
await this.handleMessage(ClientMessage.deserializeBinary(data));
|
2019-01-12 02:33:44 +07:00
|
|
|
} catch (ex) {
|
2019-03-27 01:01:25 +07:00
|
|
|
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
|
|
|
}
|
|
|
|
});
|
2019-03-27 01:01:25 +07:00
|
|
|
|
2019-01-30 07:23:30 +07:00
|
|
|
connection.onClose(() => {
|
2019-03-27 01:01:25 +07:00
|
|
|
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);
|
|
|
|
});
|
2019-01-30 07:23:30 +07:00
|
|
|
});
|
2019-01-16 01:36:09 +07:00
|
|
|
|
2019-03-27 01:01:25 +07:00
|
|
|
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);
|
|
|
|
|
2019-02-19 23:17:03 +07:00
|
|
|
if (!this.options) {
|
2019-01-16 01:36:09 +07:00
|
|
|
logger.warn("No server options provided. InitMessage will not be sent.");
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-03-12 23:12:50 +07:00
|
|
|
Promise.all([
|
2019-03-13 03:45:44 +07:00
|
|
|
mkdirp(this.options.cacheDirectory),
|
|
|
|
mkdirp(this.options.dataDirectory),
|
|
|
|
mkdirp(this.options.workingDirectory),
|
2019-03-12 23:12:50 +07:00
|
|
|
]).catch((error) => {
|
2019-01-19 04:46:40 +07:00
|
|
|
logger.error(error.message, field("error", error));
|
|
|
|
});
|
|
|
|
|
|
|
|
const initMsg = new WorkingInitMessage();
|
2019-02-19 23:17:03 +07:00
|
|
|
initMsg.setDataDirectory(this.options.dataDirectory);
|
|
|
|
initMsg.setWorkingDirectory(this.options.workingDirectory);
|
|
|
|
initMsg.setBuiltinExtensionsDir(this.options.builtInExtensionsDirectory);
|
2019-01-16 01:36:09 +07:00
|
|
|
initMsg.setHomeDirectory(os.homedir());
|
|
|
|
initMsg.setTmpDirectory(os.tmpdir());
|
2019-03-27 01:01:25 +07:00
|
|
|
initMsg.setOperatingSystem(platformToProto(os.platform()));
|
2019-02-28 01:43:00 +07:00
|
|
|
initMsg.setShell(os.userInfo().shell || global.process.env.SHELL);
|
2019-01-16 01:36:09 +07:00
|
|
|
const srvMsg = new ServerMessage();
|
|
|
|
srvMsg.setInit(initMsg);
|
|
|
|
connection.send(srvMsg.serializeBinary());
|
2019-01-12 02:33:44 +07:00
|
|
|
}
|
|
|
|
|
2019-03-27 01:01:25 +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());
|
2019-01-26 07:18:21 +07:00
|
|
|
} else {
|
2019-02-19 23:17:03 +07:00
|
|
|
throw new Error("unknown message type");
|
2019-01-12 02:33:44 +07:00
|
|
|
}
|
|
|
|
}
|
2019-03-27 01:01:25 +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") {
|
2019-03-29 05:59:49 +07:00
|
|
|
throw new Error(`"${method}" is not a function on proxy ${proxyId}`);
|
2019-03-27 01:01:25 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
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)!;
|
|
|
|
}
|
2019-01-13 02:44:29 +07:00
|
|
|
}
|