Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/jupyter/execute/output-handler.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Class that handles output messages generated for evaluation of code7for a particular cell.89WARNING: For efficiency reasons (involving syncdb patch sizes),10outputs is a map from the (string representations of) the numbers11from 0 to n-1, where there are n messages. So watch out.1213OutputHandler emits two events:1415- 'change' -- (save), called when we change cell; if save=true, recommend16broadcasting this change to other users ASAP.1718- 'done' -- emited once when finished; after this, everything is cleaned up1920- 'more_output' -- If we exceed the message limit, emit more_output (mesg, mesg_length)21with extra messages.2223- 'process' -- Gets called on any incoming message; it may24**mutate** the message, e.g., removing images uses this.2526*/2728import { callback } from "awaiting";29import { EventEmitter } from "events";30import {31close,32defaults,33required,34server_time,35len,36to_json,37is_object,38} from "@cocalc/util/misc";3940const now = () => server_time().valueOf() - 0;4142const MIN_SAVE_INTERVAL_MS = 500;43const MAX_SAVE_INTERVAL_MS = 45000;4445export class OutputHandler extends EventEmitter {46private _opts: any;47private _n: number;48private _clear_before_next_output: boolean;49private _output_length: number;50private _in_more_output_mode: any;51private _state: any;52private _stdin_cb: any;5354// Never commit output to send to the frontend more frequently than this.saveIntervalMs55// Otherwise, we'll end up with a large number of patches.56// We start out with MIN_SAVE_INTERVAL_MS and exponentially back it off to57// MAX_SAVE_INTERVAL_MS.58private lastSave: number = 0;59private saveIntervalMs = MIN_SAVE_INTERVAL_MS;6061constructor(opts: any) {62super();63this._opts = defaults(opts, {64cell: required, // object; the cell whose output (etc.) will get mutated65max_output_length: undefined, // If given, used to truncate, discard output messages; extra66// messages are saved and made available.67report_started_ms: undefined, // If no messages for this many ms, then we update via set to indicate68// that cell is being run.69dbg: undefined,70});71const { cell } = this._opts;72cell.output = null;73cell.exec_count = null;74cell.state = "run";75cell.start = null;76cell.end = null;77// Internal state78this._n = 0;79this._clear_before_next_output = false;80this._output_length = 0;81this._in_more_output_mode = false;82this._state = "ready";83// Report that computation started if there is no output soon.84if (this._opts.report_started_ms != null) {85setTimeout(this._report_started, this._opts.report_started_ms);86}8788this.stdin = this.stdin.bind(this);89}9091close = (): void => {92if (this._state == "closed") return;93this._state = "closed";94this.emit("done");95this.removeAllListeners();96close(this, new Set(["_state", "close"]));97};9899_clear_output = (save?: any): void => {100if (this._state === "closed") {101return;102}103this._clear_before_next_output = false;104// clear output message -- we delete all the outputs105// reset the counter n, save, and are done.106// IMPORTANT: In Jupyter the clear_output message and everything107// before it is NOT saved in the notebook output itself108// (like in Sage worksheets).109this._opts.cell.output = null;110this._n = 0;111this._output_length = 0;112this.emit("change", save);113};114115_report_started = (): void => {116if (this._state == "closed" || this._n > 0) {117// do nothing -- already getting output or done.118return;119}120this.emit("change", true);121};122123// Call when computation starts124start = () => {125if (this._state === "closed") {126return;127}128this._opts.cell.start = (new Date() as any) - 0;129this._opts.cell.state = "busy";130this.emit("change", true);131};132133// Call error if an error occurs. An appropriate error message is generated.134// Computation is considered done.135error = (err: any): void => {136if (err === "closed") {137// See https://github.com/sagemathinc/cocalc/issues/2388138this.message({139data: {140"text/markdown":141"<font color='red'>**Jupyter Kernel terminated:**</font> This might be caused by running out of memory or hitting a bug in some library (e.g., forking too many processes, trying to access invalid memory, etc.). Consider restarting or upgrading your project or running the relevant code directly in a terminal to track down the cause, as [explained here](https://github.com/sagemathinc/cocalc/wiki/KernelTerminated).",142},143});144} else {145this.message({146text: `${err}`,147name: "stderr",148});149}150this.done();151};152153// Call done exactly once when done154done = (): void => {155if (this._state === "closed") {156return;157}158this._opts.cell.state = "done";159if (this._opts.cell.start == null) {160this._opts.cell.start = now();161}162this._opts.cell.end = now();163this.emit("change", true);164this.close();165};166167// Handle clear168clear = (wait: any): void => {169if (wait) {170// wait until next output before clearing.171this._clear_before_next_output = true;172return;173}174this._clear_output();175};176177_clean_mesg = (mesg: any): void => {178delete mesg.execution_state;179delete mesg.code;180delete mesg.status;181delete mesg.source;182for (const k in mesg) {183const v = mesg[k];184if (is_object(v) && len(v) === 0) {185delete mesg[k];186}187}188};189190private _push_mesg = (mesg: any, save?: boolean): void => {191if (this._state === "closed") {192return;193}194195if (save == null) {196const n = now();197if (n - this.lastSave > this.saveIntervalMs) {198save = true;199this.lastSave = n;200this.saveIntervalMs = Math.min(201MAX_SAVE_INTERVAL_MS,202this.saveIntervalMs * 1.1203);204}205} else if (save == true) {206this.lastSave = now();207}208209if (this._opts.cell.output === null) {210this._opts.cell.output = {};211}212this._opts.cell.output[`${this._n}`] = mesg;213this._n += 1;214this.emit("change", save);215};216217set_input = (input: any, save = true): void => {218if (this._state === "closed") {219return;220}221this._opts.cell.input = input;222this.emit("change", save);223};224225// Process incoming messages. This may mutate mesg.226message = (mesg: any): void => {227let has_exec_count: any;228if (this._state === "closed") {229return;230}231232if (this._opts.cell.end) {233// ignore any messages once we're done.234return;235}236237// record execution_count, if there.238if (mesg.execution_count != null) {239has_exec_count = true;240this._opts.cell.exec_count = mesg.execution_count;241delete mesg.execution_count;242} else {243has_exec_count = false;244}245246// delete useless fields247this._clean_mesg(mesg);248249if (len(mesg) === 0) {250// don't even bother saving this message; nothing useful here.251return;252}253254if (has_exec_count) {255// message that has an execution count256mesg.exec_count = this._opts.cell.exec_count;257}258259// hook to process message (e.g., this may mutate mesg,260// e.g., to remove big images)261this.emit("process", mesg);262263if (this._clear_before_next_output) {264this._clear_output(false);265}266267if (!this._opts.max_output_length) {268this._push_mesg(mesg);269return;270}271272// worry about length273const s = JSON.stringify(mesg);274const mesg_length = (s && s.length) || 0;275this._output_length += mesg_length;276277if (this._output_length <= this._opts.max_output_length) {278this._push_mesg(mesg);279return;280}281282// Check if we have entered the mode were output gets put in283// the set_more_output buffer.284if (!this._in_more_output_mode) {285this._push_mesg({ more_output: true });286this._in_more_output_mode = true;287}288this.emit("more_output", mesg, mesg_length);289};290291async stdin(prompt: string, password: boolean): Promise<string> {292// See docs for stdin option to execute_code in backend jupyter.coffee293this._push_mesg({ name: "input", opts: { prompt, password } });294// Now we wait until the output message we just included has its295// value set. Then we call cb with that value.296// This weird thing below sets this._stdin_cb, then297// waits for this._stdin_cb to be called, which happens298// when cell_changed gets called.299return await callback((cb) => (this._stdin_cb = cb));300}301302// Call this when the cell changes; only used for stdin right now.303cell_changed = (cell: any, get_password: any): void => {304if (this._state === "closed") {305return;306}307if (this._stdin_cb == null) {308return;309}310const output = cell != null ? cell.get("output") : undefined;311if (output == null) {312return;313}314const value = output.getIn([`${output.size - 1}`, "value"]);315if (value != null) {316let x = value;317if (this._opts.cell.output) {318const n = `${len(this._opts.cell.output) - 1}`;319if (320get_password != null &&321this._opts.cell.output[n] &&322this._opts.cell.output[n].opts != null &&323this._opts.cell.output[n].opts.password324) {325// In case of a password, the value is NEVER placed in the document.326// Instead the value is submitted to the backend via https, with327// a random identifier put in the value.328x = get_password(); // get actual password329}330if (this._opts.cell.output[`${n}`] != null) {331this._opts.cell.output[`${n}`].value = value;332} // sync output-handler view of output with syncdb333}334this._stdin_cb(undefined, x);335delete this._stdin_cb;336}337};338339payload = (payload: any): void => {340if (this._state === "closed") {341return;342}343if (payload.source === "set_next_input") {344this.set_input(payload.text);345} else if (payload.source === "page") {346// Just handle as a normal message; and we don't show in the pager,347// which doesn't make sense for multiple users.348// This happens when requesting help for r:349// https://github.com/sagemathinc/cocalc/issues/1933350this.message(payload);351} else {352// No idea what to do with this...353if (typeof this._opts.dbg === "function") {354this._opts.dbg(`Unknown PAYLOAD: ${to_json(payload)}`);355}356}357};358}359360361