2019-01-16 01:36:09 +07:00
|
|
|
import * as os from "os";
|
2019-01-19 06:08:44 +07:00
|
|
|
import * as cp from "child_process";
|
2019-01-19 04:46:40 +07:00
|
|
|
import * as path from "path";
|
2019-01-30 07:23:30 +07:00
|
|
|
import { mkdir } from "fs";
|
2019-01-19 04:46:40 +07:00
|
|
|
import { promisify } from "util";
|
2019-01-13 02:44:29 +07:00
|
|
|
import { TextDecoder } from "text-encoding";
|
2019-01-26 07:18:21 +07:00
|
|
|
import { logger, field } from "@coder/logger";
|
2019-01-24 00:52:58 +07:00
|
|
|
import { ClientMessage, WorkingInitMessage, ServerMessage, NewSessionMessage, WriteToSessionMessage } from "../proto";
|
2019-01-30 07:48:02 +07:00
|
|
|
import { evaluate, ActiveEvaluation } from "./evaluate";
|
2019-01-12 02:33:44 +07:00
|
|
|
import { ReadWriteConnection } from "../common/connection";
|
2019-01-24 07:00:38 +07:00
|
|
|
import { Process, handleNewSession, handleNewConnection, handleNewServer } from "./command";
|
2019-01-19 04:46:40 +07:00
|
|
|
import * as net from "net";
|
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-01-19 06:08:44 +07:00
|
|
|
|
|
|
|
forkProvider?(message: NewSessionMessage): cp.ChildProcess;
|
2019-01-16 01:36:09 +07:00
|
|
|
}
|
|
|
|
|
2019-01-12 02:33:44 +07:00
|
|
|
export class Server {
|
|
|
|
|
2019-01-19 04:46:40 +07:00
|
|
|
private readonly sessions: Map<number, Process> = new Map();
|
|
|
|
private readonly connections: Map<number, net.Socket> = new Map();
|
2019-01-24 07:00:38 +07:00
|
|
|
private readonly servers: Map<number, net.Server> = new Map();
|
2019-01-30 07:48:02 +07:00
|
|
|
private readonly evals: Map<number, ActiveEvaluation> = new Map();
|
2019-01-24 07:00:38 +07:00
|
|
|
|
|
|
|
private connectionId: number = Number.MAX_SAFE_INTEGER;
|
2019-01-13 02:44:29 +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
|
|
|
) {
|
|
|
|
connection.onMessage((data) => {
|
|
|
|
try {
|
|
|
|
this.handleMessage(ClientMessage.deserializeBinary(data));
|
|
|
|
} catch (ex) {
|
|
|
|
logger.error("Failed to handle client message", field("length", data.byteLength), field("exception", ex));
|
|
|
|
}
|
|
|
|
});
|
2019-01-30 07:23:30 +07:00
|
|
|
connection.onClose(() => {
|
|
|
|
this.sessions.forEach((s) => {
|
|
|
|
s.kill();
|
|
|
|
});
|
|
|
|
this.connections.forEach((c) => {
|
|
|
|
c.destroy();
|
|
|
|
});
|
|
|
|
this.servers.forEach((s) => {
|
|
|
|
s.close();
|
|
|
|
});
|
|
|
|
});
|
2019-01-16 01:36:09 +07:00
|
|
|
|
|
|
|
if (!options) {
|
|
|
|
logger.warn("No server options provided. InitMessage will not be sent.");
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-01-19 04:46:40 +07:00
|
|
|
// Ensure the data directory exists.
|
|
|
|
const mkdirP = async (path: string): Promise<void> => {
|
|
|
|
const split = path.replace(/^\/*|\/*$/g, "").split("/");
|
|
|
|
let dir = "";
|
|
|
|
while (split.length > 0) {
|
|
|
|
dir += "/" + split.shift();
|
|
|
|
try {
|
|
|
|
await promisify(mkdir)(dir);
|
|
|
|
} catch (error) {
|
|
|
|
if (error.code !== "EEXIST") {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Promise.all([ mkdirP(path.join(options.dataDirectory, "User", "workspaceStorage")) ]).then(() => {
|
|
|
|
logger.info("Created data directory");
|
|
|
|
}).catch((error) => {
|
|
|
|
logger.error(error.message, field("error", error));
|
|
|
|
});
|
|
|
|
|
|
|
|
const initMsg = new WorkingInitMessage();
|
2019-01-16 01:36:09 +07:00
|
|
|
initMsg.setDataDirectory(options.dataDirectory);
|
|
|
|
initMsg.setWorkingDirectory(options.workingDirectory);
|
|
|
|
initMsg.setHomeDirectory(os.homedir());
|
|
|
|
initMsg.setTmpDirectory(os.tmpdir());
|
|
|
|
const platform = os.platform();
|
2019-01-19 04:46:40 +07:00
|
|
|
let operatingSystem: WorkingInitMessage.OperatingSystem;
|
2019-01-16 01:36:09 +07:00
|
|
|
switch (platform) {
|
|
|
|
case "win32":
|
2019-01-19 04:46:40 +07:00
|
|
|
operatingSystem = WorkingInitMessage.OperatingSystem.WINDOWS;
|
2019-01-16 01:36:09 +07:00
|
|
|
break;
|
|
|
|
case "linux":
|
2019-01-19 04:46:40 +07:00
|
|
|
operatingSystem = WorkingInitMessage.OperatingSystem.LINUX;
|
2019-01-16 01:36:09 +07:00
|
|
|
break;
|
|
|
|
case "darwin":
|
2019-01-19 04:46:40 +07:00
|
|
|
operatingSystem = WorkingInitMessage.OperatingSystem.MAC;
|
2019-01-16 01:36:09 +07:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error(`unrecognized platform "${platform}"`);
|
|
|
|
}
|
|
|
|
initMsg.setOperatingSystem(operatingSystem);
|
2019-01-29 00:14:06 +07:00
|
|
|
if (process.env.SHELL) {
|
|
|
|
initMsg.setShell(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
|
|
|
}
|
|
|
|
|
|
|
|
private handleMessage(message: ClientMessage): void {
|
|
|
|
if (message.hasNewEval()) {
|
2019-01-26 07:18:21 +07:00
|
|
|
const evalMessage = message.getNewEval()!;
|
2019-01-30 07:23:30 +07:00
|
|
|
logger.debug(() => [
|
|
|
|
"EvalMessage",
|
|
|
|
field("id", evalMessage.getId()),
|
|
|
|
field("args", evalMessage.getArgsList()),
|
|
|
|
field("function", evalMessage.getFunction()),
|
|
|
|
]);
|
2019-01-30 07:48:02 +07:00
|
|
|
const resp = evaluate(this.connection, evalMessage, () => {
|
|
|
|
this.evals.delete(evalMessage.getId());
|
|
|
|
});
|
|
|
|
if (resp) {
|
|
|
|
this.evals.set(evalMessage.getId(), resp);
|
|
|
|
}
|
|
|
|
} else if (message.hasEvalEvent()) {
|
|
|
|
const e = this.evals.get(message.getEvalEvent()!.getId());
|
|
|
|
if (!e) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
e.onEvent(message.getEvalEvent()!);
|
2019-01-13 02:44:29 +07:00
|
|
|
} else if (message.hasNewSession()) {
|
2019-01-25 04:11:50 +07:00
|
|
|
const sessionMessage = message.getNewSession()!;
|
2019-01-26 07:18:21 +07:00
|
|
|
logger.debug("NewSession", field("id", sessionMessage.getId()));
|
2019-01-25 04:11:50 +07:00
|
|
|
const session = handleNewSession(this.connection, sessionMessage, this.options, () => {
|
|
|
|
this.sessions.delete(sessionMessage.getId());
|
2019-01-13 02:44:29 +07:00
|
|
|
});
|
2019-01-26 07:18:21 +07:00
|
|
|
this.sessions.set(sessionMessage.getId(), session);
|
2019-01-13 02:44:29 +07:00
|
|
|
} else if (message.hasCloseSessionInput()) {
|
2019-01-26 07:18:21 +07:00
|
|
|
const closeSessionMessage = message.getCloseSessionInput()!;
|
|
|
|
logger.debug("CloseSessionInput", field("id", closeSessionMessage.getId()));
|
|
|
|
const s = this.getSession(closeSessionMessage.getId());
|
2019-01-13 02:44:29 +07:00
|
|
|
if (!s || !s.stdin) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
s.stdin.end();
|
|
|
|
} else if (message.hasResizeSessionTty()) {
|
2019-01-26 07:18:21 +07:00
|
|
|
const resizeSessionTtyMessage = message.getResizeSessionTty()!;
|
|
|
|
logger.debug("ResizeSessionTty", field("id", resizeSessionTtyMessage.getId()));
|
|
|
|
const s = this.getSession(resizeSessionTtyMessage.getId());
|
2019-01-13 02:44:29 +07:00
|
|
|
if (!s || !s.resize) {
|
|
|
|
return;
|
|
|
|
}
|
2019-01-26 07:18:21 +07:00
|
|
|
const tty = resizeSessionTtyMessage.getTtyDimensions()!;
|
2019-01-13 02:44:29 +07:00
|
|
|
s.resize(tty.getWidth(), tty.getHeight());
|
|
|
|
} else if (message.hasShutdownSession()) {
|
2019-01-26 07:18:21 +07:00
|
|
|
const shutdownSessionMessage = message.getShutdownSession()!;
|
|
|
|
logger.debug("ShutdownSession", field("id", shutdownSessionMessage.getId()));
|
|
|
|
const s = this.getSession(shutdownSessionMessage.getId());
|
2019-01-13 02:44:29 +07:00
|
|
|
if (!s) {
|
|
|
|
return;
|
|
|
|
}
|
2019-01-26 07:18:21 +07:00
|
|
|
s.kill(shutdownSessionMessage.getSignal());
|
2019-01-13 02:44:29 +07:00
|
|
|
} else if (message.hasWriteToSession()) {
|
2019-01-26 07:18:21 +07:00
|
|
|
const writeToSessionMessage = message.getWriteToSession()!;
|
|
|
|
logger.debug("WriteToSession", field("id", writeToSessionMessage.getId()));
|
|
|
|
const s = this.getSession(writeToSessionMessage.getId());
|
2019-01-13 02:44:29 +07:00
|
|
|
if (!s) {
|
|
|
|
return;
|
|
|
|
}
|
2019-01-26 07:18:21 +07:00
|
|
|
const data = new TextDecoder().decode(writeToSessionMessage.getData_asU8());
|
|
|
|
const source = writeToSessionMessage.getSource();
|
2019-01-24 00:52:58 +07:00
|
|
|
if (source === WriteToSessionMessage.Source.IPC) {
|
2019-01-30 07:23:30 +07:00
|
|
|
if (!s.send) {
|
2019-01-24 00:52:58 +07:00
|
|
|
throw new Error("Cannot send message via IPC to process without IPC");
|
|
|
|
}
|
2019-01-30 07:23:30 +07:00
|
|
|
s.send(JSON.parse(data));
|
2019-01-24 00:52:58 +07:00
|
|
|
} else {
|
|
|
|
s.write(data);
|
|
|
|
}
|
2019-01-19 04:46:40 +07:00
|
|
|
} else if (message.hasNewConnection()) {
|
2019-01-25 04:11:50 +07:00
|
|
|
const connectionMessage = message.getNewConnection()!;
|
2019-01-26 07:18:21 +07:00
|
|
|
logger.debug("NewConnection", field("id", connectionMessage.getId()));
|
|
|
|
if (this.connections.has(connectionMessage.getId())) {
|
|
|
|
throw new Error(`connect EISCONN ${connectionMessage.getPath() || connectionMessage.getPort()}`);
|
|
|
|
}
|
2019-01-25 04:11:50 +07:00
|
|
|
const socket = handleNewConnection(this.connection, connectionMessage, () => {
|
|
|
|
this.connections.delete(connectionMessage.getId());
|
2019-01-19 04:46:40 +07:00
|
|
|
});
|
2019-01-25 04:11:50 +07:00
|
|
|
this.connections.set(connectionMessage.getId(), socket);
|
2019-01-19 04:46:40 +07:00
|
|
|
} else if (message.hasConnectionOutput()) {
|
2019-01-26 07:18:21 +07:00
|
|
|
const connectionOutputMessage = message.getConnectionOutput()!;
|
|
|
|
logger.debug("ConnectionOuput", field("id", connectionOutputMessage.getId()));
|
|
|
|
const c = this.getConnection(connectionOutputMessage.getId());
|
2019-01-19 04:46:40 +07:00
|
|
|
if (!c) {
|
|
|
|
return;
|
|
|
|
}
|
2019-01-26 07:18:21 +07:00
|
|
|
c.write(Buffer.from(connectionOutputMessage.getData_asU8()));
|
2019-01-19 04:46:40 +07:00
|
|
|
} else if (message.hasConnectionClose()) {
|
2019-01-26 07:18:21 +07:00
|
|
|
const connectionCloseMessage = message.getConnectionClose()!;
|
|
|
|
logger.debug("ConnectionClose", field("id", connectionCloseMessage.getId()));
|
|
|
|
const c = this.getConnection(connectionCloseMessage.getId());
|
2019-01-19 04:46:40 +07:00
|
|
|
if (!c) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
c.end();
|
2019-01-24 07:00:38 +07:00
|
|
|
} else if (message.hasNewServer()) {
|
2019-01-25 04:11:50 +07:00
|
|
|
const serverMessage = message.getNewServer()!;
|
2019-01-26 07:18:21 +07:00
|
|
|
logger.debug("NewServer", field("id", serverMessage.getId()));
|
|
|
|
if (this.servers.has(serverMessage.getId())) {
|
|
|
|
throw new Error("multiple listeners not supported");
|
|
|
|
}
|
2019-01-25 04:11:50 +07:00
|
|
|
const s = handleNewServer(this.connection, serverMessage, (socket) => {
|
2019-01-24 07:00:38 +07:00
|
|
|
const id = this.connectionId--;
|
|
|
|
this.connections.set(id, socket);
|
2019-01-25 04:11:50 +07:00
|
|
|
|
2019-01-24 07:00:38 +07:00
|
|
|
return id;
|
|
|
|
}, () => {
|
2019-01-25 04:11:50 +07:00
|
|
|
this.connections.delete(serverMessage.getId());
|
2019-01-26 07:18:21 +07:00
|
|
|
}, (id) => {
|
|
|
|
this.connections.delete(id);
|
2019-01-24 07:00:38 +07:00
|
|
|
});
|
2019-01-25 04:11:50 +07:00
|
|
|
this.servers.set(serverMessage.getId(), s);
|
2019-01-24 07:00:38 +07:00
|
|
|
} else if (message.hasServerClose()) {
|
2019-01-26 07:18:21 +07:00
|
|
|
const serverCloseMessage = message.getServerClose()!;
|
|
|
|
logger.debug("ServerClose", field("id", serverCloseMessage.getId()));
|
|
|
|
const s = this.getServer(serverCloseMessage.getId());
|
2019-01-24 07:00:38 +07:00
|
|
|
if (!s) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
s.close();
|
2019-01-26 07:18:21 +07:00
|
|
|
} else {
|
|
|
|
logger.debug("Received unknown message type");
|
2019-01-12 02:33:44 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-24 07:00:38 +07:00
|
|
|
private getServer(id: number): net.Server | undefined {
|
|
|
|
return this.servers.get(id);
|
|
|
|
}
|
|
|
|
|
2019-01-19 04:46:40 +07:00
|
|
|
private getConnection(id: number): net.Socket | undefined {
|
|
|
|
return this.connections.get(id);
|
|
|
|
}
|
|
|
|
|
2019-01-13 02:44:29 +07:00
|
|
|
private getSession(id: number): Process | undefined {
|
|
|
|
return this.sessions.get(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|