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
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import * as vm from "vm";
|
||||
import { NewEvalMessage, TypedValue, EvalFailedMessage, EvalDoneMessage, ServerMessage, EvalEventMessage } from "../proto";
|
||||
import { SendableConnection } from "../common/connection";
|
||||
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;
|
||||
@@ -13,96 +16,117 @@ export const evaluate = (connection: SendableConnection, message: NewEvalMessage
|
||||
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());
|
||||
const tof = typeof resp;
|
||||
if (tof !== "undefined") {
|
||||
const tv = new TypedValue();
|
||||
let t: TypedValue.Type;
|
||||
switch (tof) {
|
||||
case "string":
|
||||
t = TypedValue.Type.STRING;
|
||||
break;
|
||||
case "boolean":
|
||||
t = TypedValue.Type.BOOLEAN;
|
||||
break;
|
||||
case "object":
|
||||
t = TypedValue.Type.OBJECT;
|
||||
break;
|
||||
case "number":
|
||||
t = TypedValue.Type.NUMBER;
|
||||
break;
|
||||
default:
|
||||
return sendErr(EvalFailedMessage.Reason.EXCEPTION, `unsupported response type ${tof}`);
|
||||
}
|
||||
tv.setValue(tof === "string" ? resp : JSON.stringify(resp));
|
||||
tv.setType(t);
|
||||
evalDone.setResponse(tv);
|
||||
}
|
||||
evalDone.setResponse(stringify(resp));
|
||||
|
||||
const serverMsg = new ServerMessage();
|
||||
serverMsg.setEvalDone(evalDone);
|
||||
connection.send(serverMsg.serializeBinary());
|
||||
|
||||
onDispose();
|
||||
};
|
||||
const sendErr = (reason: EvalFailedMessage.Reason, msg: string): void => {
|
||||
|
||||
/**
|
||||
* Send an exception and call onDispose.
|
||||
*/
|
||||
const sendException = (error: Error): void => {
|
||||
const evalFailed = new EvalFailedMessage();
|
||||
evalFailed.setId(message.getId());
|
||||
evalFailed.setReason(reason);
|
||||
evalFailed.setMessage(msg);
|
||||
evalFailed.setReason(EvalFailedMessage.Reason.EXCEPTION);
|
||||
evalFailed.setMessage(error.toString() + " " + error.stack);
|
||||
|
||||
const serverMsg = new ServerMessage();
|
||||
serverMsg.setEvalFailed(evalFailed);
|
||||
connection.send(serverMsg.serializeBinary());
|
||||
};
|
||||
let eventEmitter: EventEmitter | undefined;
|
||||
try {
|
||||
if (message.getActive()) {
|
||||
eventEmitter = new EventEmitter();
|
||||
}
|
||||
|
||||
const value = vm.runInNewContext(`(${message.getFunction()})(${eventEmitter ? `eventEmitter, ` : ""}${argStr.join(",")})`, {
|
||||
eventEmitter: eventEmitter ? {
|
||||
on: (event: string, cb: (...args: any[]) => void): void => {
|
||||
eventEmitter!.on(event, cb);
|
||||
},
|
||||
emit: (event: string, ...args: any[]): void => {
|
||||
const eventMsg = new EvalEventMessage();
|
||||
eventMsg.setEvent(event);
|
||||
eventMsg.setArgsList(args.filter(a => a).map(a => JSON.stringify(a)));
|
||||
eventMsg.setId(message.getId());
|
||||
const serverMsg = new ServerMessage();
|
||||
serverMsg.setEvalEvent(eventMsg);
|
||||
connection.send(serverMsg.serializeBinary());
|
||||
},
|
||||
} : undefined,
|
||||
_Buffer: Buffer,
|
||||
require: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require,
|
||||
_require: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require,
|
||||
setTimeout,
|
||||
}, {
|
||||
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,
|
||||
});
|
||||
if (eventEmitter) {
|
||||
// Is an active evaluation and should NOT be ended
|
||||
eventEmitter.on("close", () => onDispose());
|
||||
eventEmitter.on("error", () => onDispose());
|
||||
} else {
|
||||
if ((value as Promise<void>).then) {
|
||||
// Is promise
|
||||
(value as Promise<void>).then(r => sendResp(r)).catch(ex => sendErr(EvalFailedMessage.Reason.EXCEPTION, ex.toString()));
|
||||
} else {
|
||||
sendResp(value);
|
||||
}
|
||||
onDispose();
|
||||
}
|
||||
} catch (ex) {
|
||||
sendErr(EvalFailedMessage.Reason.EXCEPTION, ex.toString() + " " + ex.stack);
|
||||
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(a => JSON.parse(a)));
|
||||
eventEmitter!.emit(eventMsg.getEvent(), ...eventMsg.getArgsList().map(parse));
|
||||
},
|
||||
dispose: (): void => {
|
||||
if (eventEmitter) {
|
||||
if (value && value.dispose) {
|
||||
value.dispose();
|
||||
}
|
||||
eventEmitter.removeAllListeners();
|
||||
eventEmitter = undefined;
|
||||
}
|
||||
},
|
||||
} : undefined;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user