Path: blob/master/src/packages/jupyter/nbgrader/jupyter-run.ts
1447 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import {6type JupyterNotebook,7type RunNotebookOptions,8type Limits,9DEFAULT_LIMITS,10} from "@cocalc/util/jupyter/nbgrader-types";11import type { JupyterKernelInterface as JupyterKernel } from "@cocalc/jupyter/types/project-interface";12import { is_object, len, uuid, trunc_middle } from "@cocalc/util/misc";13import { retry_until_success } from "@cocalc/util/async-utils";14import { kernel } from "@cocalc/jupyter/kernel";15import getLogger from "@cocalc/backend/logger";16export type { Limits };1718const logger = getLogger("jupyter:nbgrader:jupyter-run");1920function global_timeout_exceeded(limits: Limits): boolean {21if (limits.timeout_ms == null || limits.start_time == null) return false;22return Date.now() - limits.start_time >= limits.timeout_ms;23}2425export async function jupyter_run_notebook(26opts: RunNotebookOptions,27): Promise<string> {28const log = (...args) => {29logger.debug("jupyter_run_notebook", ...args);30};31log(trunc_middle(JSON.stringify(opts)));32const notebook: JupyterNotebook = JSON.parse(opts.ipynb);3334let limits: Limits = {35timeout_ms: opts.limits?.max_total_time_ms ?? 0,36timeout_ms_per_cell: opts.limits?.max_time_per_cell_ms ?? 0,37max_output: opts.limits?.max_output ?? 0,38max_output_per_cell: opts.limits?.max_output_per_cell ?? 0,39start_time: Date.now(),40total_output: 0,41};4243const name = notebook.metadata.kernelspec.name;44let jupyter: JupyterKernel | undefined = undefined;4546/* We use retry_until_success to spawn the kernel, since47it makes people's lives much easier if this works even48if there is a temporary issue. Also, in testing, I've49found that sometimes if you try to spawn two kernels at50the exact same time as the same user things can fail51This is possibly an upstream Jupyter bug, but let's52just work around it since we want extra reliability53anyways.54*/55async function init_jupyter0(): Promise<void> {56log("init_jupyter", jupyter != null);57jupyter?.close();58jupyter = undefined;59// path is random so it doesn't randomly conflict with60// something else running at the same time.61const path = opts.path + `/${uuid()}.ipynb`;62jupyter = kernel({ name, path });63log("init_jupyter: spawning");64// for Python, we suppress all warnings65// they end up as stderr-output and hence would imply 0 points66const env = { PYTHONWARNINGS: "ignore" };67await jupyter.spawn({ env });68log("init_jupyter: spawned");69}7071async function init_jupyter(): Promise<void> {72await retry_until_success({73f: init_jupyter0,74start_delay: 1000,75max_delay: 5000,76factor: 1.4,77max_time: 30000,78log: function (...args) {79log("init_jupyter - retry_until_success", ...args);80},81});82}8384try {85log("init_jupyter...");86await init_jupyter();87log("init_jupyter: done");88for (const cell of notebook.cells) {89try {90if (jupyter == null) {91log("BUG: jupyter==null");92throw Error("jupyter can't be null since it was initialized above");93}94log("run_cell...");95await run_cell(jupyter, limits, cell); // mutates cell by putting in outputs96log("run_cell: done");97} catch (err) {98// fatal error occured, e.g,. timeout, broken kernel, etc.99if (cell.outputs == null) {100cell.outputs = [];101}102cell.outputs.push({ traceback: [`${err}`] });103if (!global_timeout_exceeded(limits)) {104// close existing jupyter and spawn new one, so we can robustly run more cells.105// Obviously, only do this if we are not out of time.106log("timeout exceeded so restarting...");107await init_jupyter();108log("timeout exceeded restart done");109}110}111}112} finally {113log("in finally");114if (jupyter != null) {115log("jupyter != null so closing");116// @ts-ignore117jupyter.close();118jupyter = undefined;119}120}121log("returning result");122return JSON.stringify(notebook);123}124125export async function run_cell(126jupyter: JupyterKernel,127limits0: Partial<Limits>,128cell,129): Promise<void> {130if (jupyter == null) {131throw Error("jupyter must be defined");132}133const limits = { ...DEFAULT_LIMITS, ...limits0 };134135if (limits.timeout_ms && global_timeout_exceeded(limits)) {136// the total time has been exceeded -- this will mark outputs as error137// for each cell in the rest of the notebook.138throw Error(139`Total time limit (=${Math.round(140limits.timeout_ms / 1000,141)} seconds) exceeded`,142);143}144145if (cell.cell_type != "code") {146// skip all non-code cells -- nothing to run147return;148}149const code = cell.source.join("");150if (cell.outputs == null) {151// shouldn't happen, since this would violate nbformat, but let's ensure152// it anyways, just in case.153cell.outputs = [];154}155156const result = await jupyter.execute_code_now({157code,158timeout_ms: limits.timeout_ms_per_cell,159});160161let cell_output_chars = 0;162for (const x of result) {163if (x == null) continue;164if (x["msg_type"] == "clear_output") {165cell.outputs = [];166}167const mesg: any = x["content"];168if (mesg == null) continue;169if (mesg.comm_id != null) {170// ignore any comm/widget related messages171continue;172}173delete mesg.execution_state;174delete mesg.execution_count;175delete mesg.payload;176delete mesg.code;177delete mesg.status;178delete mesg.source;179for (const k in mesg) {180const v = mesg[k];181if (is_object(v) && len(v) === 0) {182delete mesg[k];183}184}185if (len(mesg) == 0) continue;186const n = JSON.stringify(mesg).length;187limits.total_output += n;188if (limits.max_output_per_cell) {189cell_output_chars += n;190}191if (mesg["traceback"] != null) {192// always include tracebacks193cell.outputs.push(mesg);194} else {195if (196limits.max_output_per_cell &&197cell_output_chars > limits.max_output_per_cell198) {199// Use stdout stream -- it's not an *error* that there is200// truncated output; just something we want to mention.201cell.outputs.push({202name: "stdout",203output_type: "stream",204text: [205`Output truncated since it exceeded the cell output limit of ${limits.max_output_per_cell} characters`,206],207});208} else if (limits.max_output && limits.total_output > limits.max_output) {209cell.outputs.push({210name: "stdout",211output_type: "stream",212text: [213`Output truncated since it exceeded the global output limit of ${limits.max_output} characters`,214],215});216} else {217cell.outputs.push(mesg);218}219}220}221}222223224