Fixed by returning the original buffer from `fs.read` and then just using whatever encoding was passed in to iconv, so this should all work exactly the same now as it does on native Node.
152 lines
4.9 KiB
TypeScript
152 lines
4.9 KiB
TypeScript
import { fork as cpFork } from "child_process";
|
|
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 { ServerActiveEvalHelper, EvalHelper, ForkProvider } from "../common/helpers";
|
|
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, fork?: ForkProvider): ActiveEvaluation | void => {
|
|
/**
|
|
* Send the response and call onDispose.
|
|
*/
|
|
// tslint:disable-next-line no-any
|
|
const sendResp = (resp: any): void => {
|
|
logger.trace(() => [
|
|
"resolve",
|
|
field("id", message.getId()),
|
|
field("response", stringify(resp)),
|
|
]);
|
|
|
|
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 => {
|
|
logger.trace(() => [
|
|
"reject",
|
|
field("id", message.getId()),
|
|
field("response", stringify(error, true)),
|
|
]);
|
|
|
|
const evalFailed = new EvalFailedMessage();
|
|
evalFailed.setId(message.getId());
|
|
evalFailed.setResponse(stringify(error, true));
|
|
|
|
const serverMsg = new ServerMessage();
|
|
serverMsg.setEvalFailed(evalFailed);
|
|
connection.send(serverMsg.serializeBinary());
|
|
|
|
onDispose();
|
|
};
|
|
|
|
let eventEmitter = message.getActive() ? new EventEmitter(): undefined;
|
|
const sandbox = {
|
|
helper: eventEmitter ? new ServerActiveEvalHelper({
|
|
removeAllListeners: (event?: string): void => {
|
|
eventEmitter!.removeAllListeners(event);
|
|
},
|
|
// 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((a) => stringify(a))),
|
|
]);
|
|
cb(...args);
|
|
});
|
|
},
|
|
emit: (event: string, ...args: any[]): void => {
|
|
logger.trace(() => [
|
|
`emit ${event}`,
|
|
field("id", message.getId()),
|
|
field("args", args.map((a) => stringify(a))),
|
|
]);
|
|
const eventMsg = new EvalEventMessage();
|
|
eventMsg.setEvent(event);
|
|
eventMsg.setArgsList(args.map((a) => stringify(a)));
|
|
eventMsg.setId(message.getId());
|
|
const serverMsg = new ServerMessage();
|
|
serverMsg.setEvalEvent(eventMsg);
|
|
connection.send(serverMsg.serializeBinary());
|
|
},
|
|
// tslint:enable no-any
|
|
}, fork || cpFork) : new EvalHelper(),
|
|
_Buffer: Buffer,
|
|
// When the client is ran from Webpack, it will replace
|
|
// __non_webpack_require__ with require, which we then need to provide to
|
|
// the sandbox. Since the server might also be using Webpack, we need to set
|
|
// it to the non-Webpack version when that's the case. Then we need to also
|
|
// provide __non_webpack_require__ for when the client doesn't run through
|
|
// Webpack meaning it doesn't get replaced with require (Jest for example).
|
|
require: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require,
|
|
__non_webpack_require__: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require,
|
|
setTimeout,
|
|
setInterval,
|
|
clearTimeout,
|
|
process: {
|
|
env: process.env,
|
|
},
|
|
args: message.getArgsList().map(parse),
|
|
};
|
|
|
|
let value: any; // tslint:disable-line no-any
|
|
try {
|
|
const code = `(${message.getFunction()})(helper, ...args);`;
|
|
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;
|
|
};
|