UNPKG

@cocalc/project

Version:
285 lines (284 loc) 10.8 kB
"use strict"; /* * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CodeExecutionEmitter = void 0; /* Send code to a kernel to be evaluated, then wait for the results and gather them together. TODO: for easy testing/debugging, at an "async run() : Messages[]" method. */ const awaiting_1 = require("awaiting"); const events_1 = require("events"); const jupyter_1 = require("./jupyter"); const misc_1 = require("@cocalc/util/misc"); class CodeExecutionEmitter extends events_1.EventEmitter { constructor(kernel, opts) { super(); // DO NOT set iopub_done or shell_done directly; instead // set them using the function set_shell_done and set_iopub_done. // This ensures that we call _finish when both vars have been set. this.iopub_done = false; this.shell_done = false; this.state = "init"; this.all_output = []; this._go_cb = undefined; this.killing = ""; this.kernel = kernel; this.code = opts.code; this.id = opts.id; this.stdin = opts.stdin; this.halt_on_error = !!opts.halt_on_error; this.timeout_ms = opts.timeout_ms; this._message = { parent_header: {}, metadata: {}, channel: "shell", header: { msg_id: `execute_${(0, misc_1.uuid)()}`, username: "", session: "", msg_type: "execute_request", version: jupyter_1.VERSION, date: new Date().toISOString(), }, content: { code: this.code, silent: false, store_history: true, user_expressions: {}, allow_stdin: this.stdin != null, }, }; (0, misc_1.bind_methods)(this); } // Emits a valid result // result is https://jupyter-client.readthedocs.io/en/stable/messaging.html#python-api // Or an array of those when this.all is true emit_output(output) { this.all_output.push(output); this.emit("output", output); } // Call this to inform anybody listening that we've cancelled // this execution, and will NOT be doing it ever, and it // was explicitly cancelled. cancel() { this.emit("canceled"); } close() { if (this.state == "closed") return; if (this.timer != null) { clearTimeout(this.timer); delete this.timer; } this.state = "closed"; this.emit("closed"); this.removeAllListeners(); } throw_error(err) { this.emit("error", err); this.close(); } async _handle_stdin(mesg) { const dbg = this.kernel.dbg("_handle_stdin"); if (!this.stdin) { throw Error("BUG -- stdin handling not supported"); } dbg(`STDIN kernel --> server: ${JSON.stringify(mesg)}`); if (mesg.parent_header.msg_id !== this._message.header.msg_id) { dbg(`STDIN msg_id mismatch: ${mesg.parent_header.msg_id}!=${this._message.header.msg_id}`); return; } let response; try { response = await this.stdin(mesg.content.prompt ? mesg.content.prompt : "", !!mesg.content.password); } catch (err) { response = `ERROR -- ${err}`; } dbg(`STDIN client --> server ${JSON.stringify(response)}`); const m = { channel: "stdin", parent_header: this._message.header, metadata: {}, header: { msg_id: (0, misc_1.uuid)(), username: "", session: "", msg_type: "input_reply", version: jupyter_1.VERSION, date: new Date().toISOString(), }, content: { value: response, }, }; dbg(`STDIN server --> kernel: ${JSON.stringify(m)}`); this.kernel.channel?.next(m); } _handle_shell(mesg) { if (mesg.parent_header.msg_id !== this._message.header.msg_id) { return; } const dbg = this.kernel.dbg("_handle_shell"); dbg(`got SHELL message -- ${JSON.stringify(mesg)}`); if (mesg.content?.status == "error" || mesg.content?.status == "abort") { // NOTE: I'm adding support for "abort" status, since I was just reading // the kernel docs and it exists but is deprecated. Some old kernels // might use it and we should thus properly support it: // https://jupyter-client.readthedocs.io/en/stable/messaging.html#request-reply if (this.halt_on_error) { this.kernel.clear_execute_code_queue(); } this.set_shell_done(true); } else if (mesg.content?.status == "ok") { this._push_mesg(mesg); this.set_shell_done(true); } } set_shell_done(value) { this.shell_done = value; if (this.iopub_done && this.shell_done) { this._finish(); } } set_iopub_done(value) { this.iopub_done = value; if (this.iopub_done && this.shell_done) { this._finish(); } } _handle_iopub(mesg) { if (mesg.parent_header.msg_id !== this._message.header.msg_id) { // iopub message for a different execute request so ignore it. return; } const dbg = this.kernel.dbg("_handle_iopub"); dbg(`got IOPUB message -- ${JSON.stringify(mesg)}`); if (mesg.content?.comm_id != null) { // A comm message that is a result of execution of this code. // IGNORE here -- all comm messages are handles at a higher // level in jupyter.ts. Also, this case should never happen, since // we do not emit an event from jupyter.ts in this case anyways. } else { // A normal output message. this._push_mesg(mesg); } this.set_iopub_done(!!this.killing || mesg.content?.execution_state == "idle"); } // Called if the kernel is closed for some reason, e.g., crashing. handle_closed() { const dbg = this.kernel.dbg("CodeExecutionEmitter.handle_closed"); dbg("kernel closed"); this.killing = "kernel crashed"; this._finish(); } _finish() { if (this.state == "closed") { return; } this.kernel.removeListener("iopub", this._handle_iopub); if (this.stdin != null) { this.kernel.removeListener("stdin", this._handle_stdin); } this.kernel.removeListener("shell", this._handle_shell); if (this.kernel._execute_code_queue != null) { this.kernel._execute_code_queue.shift(); // finished this.kernel._process_execute_code_queue(); // start next exec } this.kernel.removeListener("close", this.handle_closed); this._push_mesg({ done: true }); this.close(); // Finally call the callback that was setup in this._go. // This is what makes it possible to await on the entire // execution. Also it is important to explicitly // signal an error if we had to kill execution due // to hitting a timeout, since the kernel may or may // not have randomly done so itself in output. this._go_cb?.(this.killing); this._go_cb = undefined; } _push_mesg(mesg) { // TODO: mesg isn't a normal javascript object; // it's **silently** immutable, which // is pretty annoying for our use. For now, we // just copy it, which is a waste. // const dbg = this.kernel.dbg(`_execute_code('${trunc(this.code, 15)}')`); // dbg("push_mesg", mesg); const header = mesg.header; mesg = (0, misc_1.copy_with)(mesg, ["metadata", "content", "buffers", "done"]); mesg = (0, misc_1.deep_copy)(mesg); if (header !== undefined) { mesg.msg_type = header.msg_type; } // dbg("push_mesg after copying msg_type", mesg); this.emit_output(mesg); } async go() { await (0, awaiting_1.callback)(this._go); return this.all_output; } _go(cb) { if (this.state != "init") { cb("may only run once"); return; } this.state = "running"; const dbg = this.kernel.dbg(`_execute_code('${(0, misc_1.trunc)(this.code, 15)}')`); dbg(`code='${this.code}'`); if (this.kernel.get_state() === "closed") { this.close(); cb("closed"); return; } this._go_cb = cb; // this._finish will call this. if (this.stdin != null) { this.kernel.on("stdin", this._handle_stdin); } this.kernel.on("shell", this._handle_shell); this.kernel.on("iopub", this._handle_iopub); dbg("send the message to get things rolling"); this.kernel.channel?.next(this._message); this.kernel.on("closed", this.handle_closed); if (this.timeout_ms) { // setup a timeout at which point things will get killed if they don't finish this.timer = setTimeout(this.timeout, this.timeout_ms); } } async timeout() { const dbg = this.kernel.dbg("CodeExecutionEmitter.timeout"); if (this.state == "closed") { dbg("already finished, so nothing to worry about"); return; } this.killing = "Timeout Error: execution time limit = " + Math.round((this.timeout_ms ?? 0) / 1000) + " seconds"; let tries = 3; let d = 1000; while (this.state != "closed" && tries > 0) { dbg("code still running, so try to interrupt it"); // Code still running but timeout reached. // Keep sending interrupt signal, which will hopefully do something to // stop running code (there is no guarantee, of course). We // try a few times... this.kernel.signal("SIGINT"); await (0, awaiting_1.delay)(d); d *= 1.3; tries -= 1; } if (this.state != "closed") { dbg("now try SIGKILL, which should kill things for sure."); this.kernel.signal("SIGKILL"); this._finish(); } } } exports.CodeExecutionEmitter = CodeExecutionEmitter; //# sourceMappingURL=execute-code.js.map