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/nbgrader/jupyter-run.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import type { RunNotebookOptions } from "@cocalc/jupyter/nbgrader/types";6import type { JupyterNotebook } from "@cocalc/jupyter/nbgrader/types";7import type { JupyterKernelInterface as JupyterKernel } from "@cocalc/jupyter/types/project-interface";8import { is_object, len, uuid, trunc_middle } from "@cocalc/util/misc";9import { retry_until_success } from "@cocalc/util/async-utils";10import { kernel } from "@cocalc/jupyter/kernel";1112// For tracking limits during the run:13export interface Limits {14timeout_ms_per_cell: number;15max_output_per_cell: number;16max_output: number;17total_output: number;18timeout_ms?: number;19start_time?: number;20}2122function global_timeout_exceeded(limits: Limits): boolean {23if (limits.timeout_ms == null || limits.start_time == null) return false;24return Date.now() - limits.start_time >= limits.timeout_ms;25}2627export async function jupyter_run_notebook(28logger,29opts: RunNotebookOptions30): Promise<string> {31const log = (...args) => {32logger.debug("jupyter_run_notebook", ...args);33};34log(trunc_middle(JSON.stringify(opts)));35const notebook: JupyterNotebook = JSON.parse(opts.ipynb);3637let limits: Limits = {38timeout_ms: opts.limits?.max_total_time_ms ?? 0,39timeout_ms_per_cell: opts.limits?.max_time_per_cell_ms ?? 0,40max_output: opts.limits?.max_output ?? 0,41max_output_per_cell: opts.limits?.max_output_per_cell ?? 0,42start_time: Date.now(),43total_output: 0,44};4546const name = notebook.metadata.kernelspec.name;47let jupyter: JupyterKernel | undefined = undefined;4849/* We use retry_until_success to spawn the kernel, since50it makes people's lives much easier if this works even51if there is a temporary issue. Also, in testing, I've52found that sometimes if you try to spawn two kernels at53the exact same time as the same user things can fail54This is possibly an upstream Jupyter bug, but let's55just work around it since we want extra reliability56anyways.57*/58async function init_jupyter0(): Promise<void> {59log("init_jupyter", jupyter != null);60jupyter?.close();61jupyter = undefined;62// path is random so it doesn't randomly conflict with63// something else running at the same time.64const path = opts.path + `/${uuid()}.ipynb`;65jupyter = kernel({ name, path });66log("init_jupyter: spawning");67// for Python, we suppress all warnings68// they end up as stderr-output and hence would imply 0 points69const env = { PYTHONWARNINGS: "ignore" };70await jupyter.spawn({ env });71log("init_jupyter: spawned");72}7374async function init_jupyter(): Promise<void> {75await retry_until_success({76f: init_jupyter0,77start_delay: 1000,78max_delay: 5000,79factor: 1.4,80max_time: 30000,81log: function (...args) {82log("init_jupyter - retry_until_success", ...args);83},84});85}8687try {88log("init_jupyter...");89await init_jupyter();90log("init_jupyter: done");91for (const cell of notebook.cells) {92try {93if (jupyter == null) {94log("BUG: jupyter==null");95throw Error("jupyter can't be null since it was initialized above");96}97log("run_cell...");98await run_cell(jupyter, limits, cell); // mutates cell by putting in outputs99log("run_cell: done");100} catch (err) {101// fatal error occured, e.g,. timeout, broken kernel, etc.102if (cell.outputs == null) {103cell.outputs = [];104}105cell.outputs.push({ traceback: [`${err}`] });106if (!global_timeout_exceeded(limits)) {107// close existing jupyter and spawn new one, so we can robustly run more cells.108// Obviously, only do this if we are not out of time.109log("timeout exceeded so restarting...");110await init_jupyter();111log("timeout exceeded restart done");112}113}114}115} finally {116log("in finally");117if (jupyter != null) {118log("jupyter != null so closing");119// @ts-ignore120jupyter.close();121jupyter = undefined;122}123}124log("returning result");125return JSON.stringify(notebook);126}127128export async function run_cell(129jupyter: JupyterKernel,130limits: Limits,131cell132): Promise<void> {133if (jupyter == null) {134throw Error("jupyter must be defined");135}136137if (limits.timeout_ms && global_timeout_exceeded(limits)) {138// the total time has been exceeded -- this will mark outputs as error139// for each cell in the rest of the notebook.140throw Error(141`Total time limit (=${Math.round(142limits.timeout_ms / 1000143)} seconds) exceeded`144);145}146147if (cell.cell_type != "code") {148// skip all non-code cells -- nothing to run149return;150}151const code = cell.source.join("");152if (cell.outputs == null) {153// shouldn't happen, since this would violate nbformat, but let's ensure154// it anyways, just in case.155cell.outputs = [];156}157158const result = await jupyter.execute_code_now({159code,160timeout_ms: limits.timeout_ms_per_cell,161});162163let cell_output_chars = 0;164for (const x of result) {165if (x == null) continue;166if (x["msg_type"] == "clear_output") {167cell.outputs = [];168}169const mesg: any = x["content"];170if (mesg == null) continue;171if (mesg.comm_id != null) {172// ignore any comm/widget related messages173continue;174}175delete mesg.execution_state;176delete mesg.execution_count;177delete mesg.payload;178delete mesg.code;179delete mesg.status;180delete mesg.source;181for (const k in mesg) {182const v = mesg[k];183if (is_object(v) && len(v) === 0) {184delete mesg[k];185}186}187if (len(mesg) == 0) continue;188const n = JSON.stringify(mesg).length;189limits.total_output += n;190if (limits.max_output_per_cell) {191cell_output_chars += n;192}193if (mesg["traceback"] != null) {194// always include tracebacks195cell.outputs.push(mesg);196} else {197if (198limits.max_output_per_cell &&199cell_output_chars > limits.max_output_per_cell200) {201// Use stdout stream -- it's not an *error* that there is202// truncated output; just something we want to mention.203cell.outputs.push({204name: "stdout",205output_type: "stream",206text: [207`Output truncated since it exceeded the cell output limit of ${limits.max_output_per_cell} characters`,208],209});210} else if (limits.max_output && limits.total_output > limits.max_output) {211cell.outputs.push({212name: "stdout",213output_type: "stream",214text: [215`Output truncated since it exceeded the global output limit of ${limits.max_output} characters`,216],217});218} else {219cell.outputs.push(mesg);220}221}222}223}224225226