arepl-backend
Version:
JS interface to python evaluator for AREPL
221 lines • 8.9 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PythonExecutor = exports.PythonState = void 0;
const python_shell_1 = require("python-shell");
const os_1 = require("os");
const crypto_1 = require("crypto");
/**
* Starting = Starting or restarting.
* Ending = Process is exiting.
* Executing = Executing inputted code.
* DirtyFree = evaluator may have been polluted by side-effects from previous code, but is free for more code.
* FreshFree = evaluator is ready for the first run of code
*/
var PythonState;
(function (PythonState) {
PythonState[PythonState["Starting"] = 0] = "Starting";
PythonState[PythonState["Ending"] = 1] = "Ending";
PythonState[PythonState["Executing"] = 2] = "Executing";
PythonState[PythonState["DirtyFree"] = 3] = "DirtyFree";
PythonState[PythonState["FreshFree"] = 4] = "FreshFree";
})(PythonState || (exports.PythonState = PythonState = {}));
class PythonExecutor {
/**
* starts python_evaluator.py
* @param options Process / Python options. If not specified sensible defaults are inferred.
*/
constructor(options = {}) {
this.options = options;
this.state = PythonState.Starting;
if (!options.env)
options.env = {};
if (process.platform == "darwin") {
// needed for Mac to prevent ENOENT
options.env.PATH = ["/usr/local/bin", process.env.PATH].join(":");
}
else if (process.platform == "win32") {
// needed for windows for encoding to match what it would be in terminal
// https://docs.python.org/3/library/sys.html#sys.stdin
options.env.PYTHONIOENCODING = "utf8";
}
// python-shell buffers untill newline is reached in text mode
// so we use binary instead to skip python-shell buffering
// this lets user flush without newline
this.options.mode = 'binary';
this.options.stdio = ['pipe', 'pipe', 'pipe', 'pipe'];
if (!options.pythonPath)
this.options.pythonPath = python_shell_1.PythonShell.defaultPythonPath;
if (!options.scriptPath)
this.options.scriptPath = PythonExecutor.areplPythonBackendFolderPath;
this.evaluatorName = (0, crypto_1.randomBytes)(16).toString('hex');
}
/**
* does not do anything if program is currently executing code
*/
execCode(code) {
if (this.state == PythonState.Executing) {
console.error('Incoming code detected while process is still executing. \
This should never happen');
}
this.state = PythonState.Executing;
this.startTime = Date.now();
this.pyshell.send(JSON.stringify(code) + os_1.EOL);
}
/**
* @param {string} message
*/
sendStdin(message) {
this.pyshell.send(message);
}
/**
* kills python process and restarts. Force-kills if necessary after 50ms.
* After process restarts the callback passed in is invoked
*/
restart(callback = () => { }) {
this.state = PythonState.Ending;
// register callback for restart
// using childProcess callback instead of pyshell callback
// (pyshell callback only happens when process exits voluntarily)
this.pyshell.childProcess.on('exit', () => {
this.start(callback);
});
this.stop();
}
/**
* Kills python process. Force-kills if necessary after 50ms.
* You can check python_evaluator.running to see if process is dead yet
*/
stop(kill_immediately = false) {
this.state = PythonState.Ending;
const kill_signal = kill_immediately ? 'SIGKILL' : 'SIGTERM';
this.pyshell.childProcess.kill(kill_signal);
if (!kill_immediately) {
// pyshell has 50 ms to die gracefully
setTimeout(() => {
if (this.state == PythonState.Ending) {
// python didn't respect the SIGTERM, force-kill it
this.pyshell.childProcess.kill('SIGKILL');
}
}, PythonExecutor.GRACE_PERIOD);
}
}
/**
* starts python_evaluator.py.
*/
start(finishedStartingCallback) {
this.state = PythonState.Starting;
console.log("Starting Python...");
this.finishedStartingCallback = finishedStartingCallback;
this.startTime = Date.now();
this.pyshell = new python_shell_1.PythonShell('arepl_python_evaluator.py', this.options);
const resultPipe = this.pyshell.childProcess.stdio[3];
const newlineTransformer = new python_shell_1.NewlineTransformer();
resultPipe.pipe(newlineTransformer).on('data', this.handleResult.bind(this));
this.pyshell.stdout.on('data', (message) => {
this.onPrint(message.toString());
});
this.pyshell.stderr.on('data', (log) => {
this.onStderr(log.toString());
});
}
/**
* Overwrite this with your own handler.
* is called when program fails or completes
*/
onResult(foo) { }
/**
* Overwrite this with your own handler.
* Is called when program prints
* @param {string} foo
*/
onPrint(foo) { }
/**
* Overwrite this with your own handler.
* Is called when program logs stderr
* @param {string} foo
*/
onStderr(foo) { }
/**
* handles pyshell results and calls onResult / onPrint
* @param {string} results
*/
handleResult(results) {
let pyResult = {
userError: null,
userErrorMsg: "",
userVariables: {},
execTime: 0,
totalTime: 0,
totalPyTime: 0,
internalError: "",
caller: "",
lineno: -1,
done: true,
startResult: false,
evaluatorName: this.evaluatorName
};
try {
pyResult = JSON.parse(results);
if (pyResult.startResult) {
console.log(`Finished starting in ${Date.now() - this.startTime}`);
this.state = PythonState.FreshFree;
this.finishedStartingCallback();
return;
}
if (pyResult['done'] == true) {
this.state = PythonState.DirtyFree;
}
pyResult.execTime = pyResult.execTime * 1000; // convert into ms
pyResult.totalPyTime = pyResult.totalPyTime * 1000;
//@ts-ignore pyResult.userVariables is sent to as string, we convert to object
pyResult.userVariables = JSON.parse(pyResult.userVariables);
//@ts-ignore pyResult.userError is sent to as string, we convert to object
pyResult.userError = pyResult.userError ? JSON.parse(pyResult.userError) : {};
if (pyResult.userErrorMsg) {
pyResult.userErrorMsg = this.formatPythonException(pyResult.userErrorMsg);
}
pyResult.totalTime = Date.now() - this.startTime;
this.onResult(pyResult);
}
catch (err) {
if (err instanceof Error) {
err.message = err.message + "\nresults: " + results;
}
throw err;
}
}
/**
* checks syntax without executing code
* @param {string} code
* @returns {Promise} rejects w/ stderr if syntax failure
*/
checkSyntax(code) {
return __awaiter(this, void 0, void 0, function* () {
return python_shell_1.PythonShell.checkSyntax(code);
});
}
/**
* gets rid of unnecessary File "<string>" message in exception
* @example err:
* Traceback (most recent call last):\n File "<string>", line 1, in <module>\nNameError: name \'x\' is not defined\n
*/
formatPythonException(err) {
//replace File "<string>" (pointless)
err = err.replace(/File \"<string>\", /g, "");
return err;
}
}
exports.PythonExecutor = PythonExecutor;
PythonExecutor.areplPythonBackendFolderPath = __dirname + '/python/';
// how long between SIGTERM and SIGKILL, in ms
PythonExecutor.GRACE_PERIOD = 50;
//# sourceMappingURL=pythonExecutor.js.map