4a80bcb42c
* 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
133 lines
4.0 KiB
TypeScript
133 lines
4.0 KiB
TypeScript
import { EventEmitter } from "events";
|
|
import * as vm from "vm";
|
|
import { logger, field } from "@coder/logger";
|
|
import { NewEvalMessage, EvalFailedMessage, EvalDoneMessage, ServerMessage, EvalEventMessage } from "../proto";
|
|
import { SendableConnection } from "../common/connection";
|
|
import { stringify, parse } from "../common/util";
|
|
|
|
export interface ActiveEvaluation {
|
|
onEvent(msg: EvalEventMessage): void;
|
|
dispose(): void;
|
|
}
|
|
|
|
declare var __non_webpack_require__: typeof require;
|
|
export const evaluate = (connection: SendableConnection, message: NewEvalMessage, onDispose: () => void): ActiveEvaluation | void => {
|
|
const argStr: string[] = [];
|
|
message.getArgsList().forEach((value) => {
|
|
argStr.push(value);
|
|
});
|
|
|
|
/**
|
|
* Send the response and call onDispose.
|
|
*/
|
|
// tslint:disable-next-line no-any
|
|
const sendResp = (resp: any): void => {
|
|
const evalDone = new EvalDoneMessage();
|
|
evalDone.setId(message.getId());
|
|
evalDone.setResponse(stringify(resp));
|
|
|
|
const serverMsg = new ServerMessage();
|
|
serverMsg.setEvalDone(evalDone);
|
|
connection.send(serverMsg.serializeBinary());
|
|
|
|
onDispose();
|
|
};
|
|
|
|
/**
|
|
* Send an exception and call onDispose.
|
|
*/
|
|
const sendException = (error: Error): void => {
|
|
const evalFailed = new EvalFailedMessage();
|
|
evalFailed.setId(message.getId());
|
|
evalFailed.setReason(EvalFailedMessage.Reason.EXCEPTION);
|
|
evalFailed.setMessage(error.toString() + " " + error.stack);
|
|
|
|
const serverMsg = new ServerMessage();
|
|
serverMsg.setEvalFailed(evalFailed);
|
|
connection.send(serverMsg.serializeBinary());
|
|
|
|
onDispose();
|
|
};
|
|
|
|
let eventEmitter = message.getActive() ? new EventEmitter(): undefined;
|
|
const sandbox = {
|
|
eventEmitter: eventEmitter ? {
|
|
// tslint:disable no-any
|
|
on: (event: string, cb: (...args: any[]) => void): void => {
|
|
eventEmitter!.on(event, (...args: any[]) => {
|
|
logger.trace(() => [
|
|
`${event}`,
|
|
field("id", message.getId()),
|
|
field("args", args.map(stringify)),
|
|
]);
|
|
cb(...args);
|
|
});
|
|
},
|
|
emit: (event: string, ...args: any[]): void => {
|
|
logger.trace(() => [
|
|
`emit ${event}`,
|
|
field("id", message.getId()),
|
|
field("args", args.map(stringify)),
|
|
]);
|
|
const eventMsg = new EvalEventMessage();
|
|
eventMsg.setEvent(event);
|
|
eventMsg.setArgsList(args.map(stringify));
|
|
eventMsg.setId(message.getId());
|
|
const serverMsg = new ServerMessage();
|
|
serverMsg.setEvalEvent(eventMsg);
|
|
connection.send(serverMsg.serializeBinary());
|
|
},
|
|
// tslint:enable no-any
|
|
} : undefined,
|
|
_Buffer: Buffer,
|
|
require: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require,
|
|
setTimeout,
|
|
setInterval,
|
|
clearTimeout,
|
|
process: {
|
|
env: process.env,
|
|
},
|
|
};
|
|
|
|
let value: any; // tslint:disable-line no-any
|
|
try {
|
|
const code = `(${message.getFunction()})(${eventEmitter ? "eventEmitter, " : ""}${argStr.join(",")});`;
|
|
value = vm.runInNewContext(code, sandbox, {
|
|
// If the code takes longer than this to return, it is killed and throws.
|
|
timeout: message.getTimeout() || 15000,
|
|
});
|
|
} catch (ex) {
|
|
sendException(ex);
|
|
}
|
|
|
|
// An evaluation completes when the value it returns resolves. An active
|
|
// evaluation completes when it is disposed. Active evaluations are required
|
|
// to return disposers so we can know both when it has ended (so we can clean
|
|
// up on our end) and how to force end it (for example when the client
|
|
// disconnects).
|
|
// tslint:disable-next-line no-any
|
|
const promise = !eventEmitter ? value as Promise<any> : new Promise((resolve): void => {
|
|
value.onDidDispose(resolve);
|
|
});
|
|
if (promise && promise.then) {
|
|
promise.then(sendResp).catch(sendException);
|
|
} else {
|
|
sendResp(value);
|
|
}
|
|
|
|
return eventEmitter ? {
|
|
onEvent: (eventMsg: EvalEventMessage): void => {
|
|
eventEmitter!.emit(eventMsg.getEvent(), ...eventMsg.getArgsList().map(parse));
|
|
},
|
|
dispose: (): void => {
|
|
if (eventEmitter) {
|
|
if (value && value.dispose) {
|
|
value.dispose();
|
|
}
|
|
eventEmitter.removeAllListeners();
|
|
eventEmitter = undefined;
|
|
}
|
|
},
|
|
} : undefined;
|
|
};
|