Asher 4a80bcb42c
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 10:17:03 -06:00

124 lines
3.6 KiB
TypeScript

import * as os from "os";
import * as path from "path";
import { mkdir } from "fs";
import { promisify } from "util";
import { logger, field } from "@coder/logger";
import { ClientMessage, WorkingInitMessage, ServerMessage } from "../proto";
import { evaluate, ActiveEvaluation } from "./evaluate";
import { ReadWriteConnection } from "../common/connection";
export interface ServerOptions {
readonly workingDirectory: string;
readonly dataDirectory: string;
readonly builtInExtensionsDirectory: string;
}
export class Server {
private readonly evals = new Map<number, ActiveEvaluation>();
public constructor(
private readonly connection: ReadWriteConnection,
private readonly options?: ServerOptions,
) {
connection.onMessage((data) => {
try {
this.handleMessage(ClientMessage.deserializeBinary(data));
} catch (ex) {
logger.error("Failed to handle client message", field("length", data.byteLength), field("exception", {
message: ex.message,
stack: ex.stack,
}));
}
});
connection.onClose(() => {
this.evals.forEach((e) => e.dispose());
});
if (!this.options) {
logger.warn("No server options provided. InitMessage will not be sent.");
return;
}
// 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(this.options.dataDirectory, "User", "workspaceStorage")) ]).then(() => {
logger.info("Created data directory");
}).catch((error) => {
logger.error(error.message, field("error", error));
});
const initMsg = new WorkingInitMessage();
initMsg.setDataDirectory(this.options.dataDirectory);
initMsg.setWorkingDirectory(this.options.workingDirectory);
initMsg.setBuiltinExtensionsDir(this.options.builtInExtensionsDirectory);
initMsg.setHomeDirectory(os.homedir());
initMsg.setTmpDirectory(os.tmpdir());
const platform = os.platform();
let operatingSystem: WorkingInitMessage.OperatingSystem;
switch (platform) {
case "win32":
operatingSystem = WorkingInitMessage.OperatingSystem.WINDOWS;
break;
case "linux":
operatingSystem = WorkingInitMessage.OperatingSystem.LINUX;
break;
case "darwin":
operatingSystem = WorkingInitMessage.OperatingSystem.MAC;
break;
default:
throw new Error(`unrecognized platform "${platform}"`);
}
initMsg.setOperatingSystem(operatingSystem);
if (global.process.env.SHELL) {
initMsg.setShell(global.process.env.SHELL);
}
const srvMsg = new ServerMessage();
srvMsg.setInit(initMsg);
connection.send(srvMsg.serializeBinary());
}
private handleMessage(message: ClientMessage): void {
if (message.hasNewEval()) {
const evalMessage = message.getNewEval()!;
logger.trace(() => [
"EvalMessage",
field("id", evalMessage.getId()),
field("args", evalMessage.getArgsList()),
field("function", evalMessage.getFunction()),
]);
const resp = evaluate(this.connection, evalMessage, () => {
this.evals.delete(evalMessage.getId());
logger.trace(() => [
`dispose ${evalMessage.getId()}, ${this.evals.size} left`,
]);
});
if (resp) {
this.evals.set(evalMessage.getId(), resp);
}
} else if (message.hasEvalEvent()) {
const evalEventMessage = message.getEvalEvent()!;
const e = this.evals.get(evalEventMessage.getId());
if (!e) {
return;
}
e.onEvent(evalEventMessage);
} else {
throw new Error("unknown message type");
}
}
}