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/kernel/kernel.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Jupyter Backend78For interactive testing:910$ node1112> j = require('./dist/kernel'); k = j.kernel({name:'python3', path:'x.ipynb'});13> console.log(JSON.stringify(await k.execute_code_now({code:'2+3'}),0,2))1415*/1617// const DEBUG = true; // only for extreme debugging.18const DEBUG = false; // normal mode19if (DEBUG) {20console.log("Enabling low level Jupyter kernel debugging.");21}2223// NOTE: we choose to use node-cleanup instead of the much more24// popular exit-hook, since node-cleanup actually works for us.25// https://github.com/jtlapp/node-cleanup/issues/1626// Also exit-hook is hard to import from commonjs.27import nodeCleanup from "node-cleanup";28import type { Channels, MessageType } from "@nteract/messaging";29import { reuseInFlight } from "@cocalc/util/reuse-in-flight";30import { callback, delay } from "awaiting";31import { createMainChannel } from "enchannel-zmq-backend";32import { EventEmitter } from "node:events";33import { unlink } from "@cocalc/backend/misc/async-utils-node";34import {35process as iframe_process,36is_likely_iframe,37} from "@cocalc/jupyter/blobs/iframe";38import { remove_redundant_reps } from "@cocalc/jupyter/ipynb/import-from-ipynb";39import { JupyterActions } from "@cocalc/jupyter/redux/project-actions";40import {41CodeExecutionEmitterInterface,42ExecOpts,43JupyterKernelInterface,44KernelInfo,45} from "@cocalc/jupyter/types/project-interface";46import { JupyterStore } from "@cocalc/jupyter/redux/store";47import { JUPYTER_MIMETYPES } from "@cocalc/jupyter/util/misc";48import type { SyncDB } from "@cocalc/sync/editor/db/sync";49import { retry_until_success } from "@cocalc/util/async-utils";50import createChdirCommand from "@cocalc/util/jupyter-api/chdir-commands";51import { key_value_store } from "@cocalc/util/key-value-store";52import {53copy,54deep_copy,55is_array,56len,57merge,58original_path,59path_split,60uuid,61} from "@cocalc/util/misc";62import { CodeExecutionEmitter } from "@cocalc/jupyter/execute/execute-code";63import { get_blob_store_sync } from "@cocalc/jupyter/blobs";64import {65getLanguage,66get_kernel_data_by_name,67} from "@cocalc/jupyter/kernel/kernel-data";68import launchJupyterKernel, {69LaunchJupyterOpts,70SpawnedKernel,71killKernel,72} from "@cocalc/jupyter/pool/pool";73import { getAbsolutePathFromHome } from "@cocalc/jupyter/util/fs";74import type { KernelParams } from "@cocalc/jupyter/types/kernel";75import { redux_name } from "@cocalc/util/redux/name";76import { redux } from "@cocalc/jupyter/redux/app";77import { VERSION } from "@cocalc/jupyter/kernel/version";78import type { NbconvertParams } from "@cocalc/jupyter/types/nbconvert";79import type { Client } from "@cocalc/sync/client/types";80import { getLogger } from "@cocalc/backend/logger";81import { base64ToBuffer } from "@cocalc/util/base64";8283const MAX_KERNEL_SPAWN_TIME = 120 * 1000;8485const logger = getLogger("jupyter:kernel");8687// We make it so nbconvert functionality can be dynamically enabled88// by calling this at runtime. The reason is because some users of89// this code (e.g., remote kernels) don't need to provide nbconvert90// functionality, and our implementation has some heavy dependencies,91// e.g., on a big chunk of the react frontend.92let nbconvert: (opts: NbconvertParams) => Promise<void> = async () => {93throw Error("nbconvert is not enabled");94};95export function initNbconvert(f) {96nbconvert = f;97}9899/*100We set a few extra user-specific options for the environment in which101Sage-based Jupyter kernels run; these are more multi-user friendly.102*/103const SAGE_JUPYTER_ENV = merge(copy(process.env), {104PYTHONUSERBASE: `${process.env.HOME}/.local`,105PYTHON_EGG_CACHE: `${process.env.HOME}/.sage/.python-eggs`,106R_MAKEVARS_USER: `${process.env.HOME}/.sage/R/Makevars.user`,107});108109// Initialize the actions and store for working with a specific110// Jupyter notebook. The syncdb is the syncdoc associated to111// the ipynb file, and this function creates the corresponding112// actions and store, which make it possible to work with this113// notebook.114export async function initJupyterRedux(syncdb: SyncDB, client: Client) {115const project_id = syncdb.project_id;116if (project_id == null) {117throw Error("project_id must be defined");118}119if (syncdb.get_state() == "closed") {120throw Error("syncdb must not be closed");121}122123// This path is the file we will watch for changes and save to, which is in the original124// official ipynb format:125const path = original_path(syncdb.get_path());126logger.debug("initJupyterRedux", path);127128const name = redux_name(project_id, path);129if (redux.getStore(name) != null && redux.getActions(name) != null) {130logger.debug(131"initJupyterRedux",132path,133" -- existing actions, so removing them",134);135// The redux info for this notebook already exists, so don't136// try to make it again without first deleting the existing one.137// Having two at once basically results in things feeling hung.138// This should never happen, but we ensure it139// See https://github.com/sagemathinc/cocalc/issues/4331140await removeJupyterRedux(path, project_id);141}142const store = redux.createStore(name, JupyterStore);143const actions = redux.createActions(name, JupyterActions);144145actions._init(project_id, path, syncdb, store, client);146147syncdb.once("error", (err) =>148logger.error("initJupyterRedux", path, "syncdb ERROR", err),149);150syncdb.once("ready", () =>151logger.debug("initJupyterRedux", path, "syncdb ready"),152);153}154155export async function getJupyterRedux(syncdb: SyncDB) {156const project_id = syncdb.project_id;157const path = original_path(syncdb.get_path());158const name = redux_name(project_id, path);159return { actions: redux.getActions(name), store: redux.getStore(name) };160}161162// Remove the store/actions for a given Jupyter notebook,163// and also close the kernel if it is running.164export async function removeJupyterRedux(165path: string,166project_id: string,167): Promise<void> {168logger.debug("removeJupyterRedux", path);169// if there is a kernel, close it170try {171await get_existing_kernel(path)?.close();172} catch (_err) {173// ignore174}175const name = redux_name(project_id, path);176const actions = redux.getActions(name);177if (actions != null) {178try {179await actions.close();180} catch (err) {181logger.debug(182"removeJupyterRedux",183path,184" WARNING -- issue closing actions",185err,186);187}188}189redux.removeStore(name);190redux.removeActions(name);191}192193export function kernel(opts: KernelParams): JupyterKernel {194return new JupyterKernel(opts.name, opts.path, opts.actions, opts.ulimit);195}196197/*198Jupyter Kernel interface.199200The kernel does *NOT* start up until either spawn is explicitly called, or201code execution is explicitly requested. This makes it possible to202call process_output without spawning an actual kernel.203*/204const _jupyter_kernels: { [path: string]: JupyterKernel } = {};205206// Ensure that the kernels all get killed when the process exits.207nodeCleanup(() => {208for (const kernelPath in _jupyter_kernels) {209// We do NOT await the close since that's not really210// supported or possible in general.211const { _kernel } = _jupyter_kernels[kernelPath] as any;212if (_kernel) {213killKernel(_kernel);214}215}216});217218// NOTE: keep JupyterKernel implementation private -- use the kernel function219// above, and the interface defined in types.220class JupyterKernel extends EventEmitter implements JupyterKernelInterface {221// name -- if undefined that means "no actual Jupyter kernel" (i.e., this JupyterKernel exists222// here, but there is no actual separate real Jupyter kernel process and one won't be created).223// Everything should work, except you can't *spawn* such a kernel.224public name: string | undefined;225226public store: any; // this is a key:value store used mainly for stdin support right now. NOTHING TO DO WITH REDUX!227public readonly identity: string = uuid();228229private stderr: string = "";230private ulimit?: string;231private _path: string;232private _actions?: JupyterActions;233private _state: string;234private _directory: string;235private _filename: string;236private _kernel?: SpawnedKernel;237private _kernel_info?: KernelInfo;238public _execute_code_queue: CodeExecutionEmitter[] = [];239public channel?: Channels;240private has_ensured_running: boolean = false;241242constructor(243name: string | undefined,244_path: string,245_actions: JupyterActions | undefined,246ulimit: string | undefined,247) {248super();249250this.ulimit = ulimit;251this.spawn = reuseInFlight(this.spawn.bind(this));252253this.kernel_info = reuseInFlight(this.kernel_info.bind(this));254this.nbconvert = reuseInFlight(this.nbconvert.bind(this));255this.ensure_running = reuseInFlight(this.ensure_running.bind(this));256257this.close = this.close.bind(this);258this.process_output = this.process_output.bind(this);259260this.name = name;261this._path = _path;262this._actions = _actions;263264this.store = key_value_store();265const { head, tail } = path_split(getAbsolutePathFromHome(this._path));266this._directory = head;267this._filename = tail;268this._set_state("off");269this._execute_code_queue = [];270if (_jupyter_kernels[this._path] !== undefined) {271// This happens when we change the kernel for a given file, e.g., from python2 to python3.272// Obviously, it is important to clean up after the old kernel.273_jupyter_kernels[this._path].close();274}275_jupyter_kernels[this._path] = this;276this.setMaxListeners(100);277const dbg = this.dbg("constructor");278dbg("done");279}280281public get_path() {282return this._path;283}284285// no-op if calling it doesn't change the state.286private _set_state(state: string): void {287// state = 'off' --> 'spawning' --> 'starting' --> 'running' --> 'closed'288if (this._state == state) return;289this._state = state;290this.emit("state", this._state);291this.emit(this._state); // we *SHOULD* use this everywhere, not above.292}293294get_state(): string {295return this._state;296}297298async spawn(spawn_opts?: { env?: { [key: string]: string } }): Promise<void> {299if (this._state === "closed") {300// game over!301throw Error("closed -- kernel spawn");302}303if (!this.name) {304// spawning not allowed.305throw Error("cannot spawn since no kernel is set");306}307if (["running", "starting"].includes(this._state)) {308// Already spawned, so no need to do it again.309return;310}311this._set_state("spawning");312const dbg = this.dbg("spawn");313dbg("spawning kernel...");314315// ****316// CRITICAL: anything added to opts better not be specific317// to the kernel path or it will completely break using a318// pool, which makes things massively slower.319// ****320321const opts: LaunchJupyterOpts = {322env: spawn_opts?.env ?? {},323...(this.ulimit != null ? { ulimit: this.ulimit } : undefined),324};325326try {327const kernelData = await get_kernel_data_by_name(this.name);328// This matches "sage", "sage-x.y", and Sage Python3 ("sage -python -m ipykernel")329if (kernelData.argv[0].startsWith("sage")) {330dbg("setting special environment for Sage kernels");331opts.env = merge(opts.env, SAGE_JUPYTER_ENV);332}333} catch (err) {334dbg(`No kernelData available for ${this.name}`);335}336337// Make cocalc default to the colab renderer for cocalc-jupyter, since338// this one happens to work best for us, and they don't have a custom339// one for us. See https://plot.ly/python/renderers/ and340// https://github.com/sagemathinc/cocalc/issues/4259341opts.env.PLOTLY_RENDERER = "colab";342opts.env.COCALC_JUPYTER_KERNELNAME = this.name;343344// !!! WARNING: do NOT add anything new here that depends on that path!!!!345// Otherwise the pool will switch to falling back to not being used, and346// cocalc would then be massively slower.347// Non-uniform customization.348// launchJupyterKernel is explicitly smart enough to deal with opts.cwd349if (this._directory) {350opts.cwd = this._directory;351}352// launchJupyterKernel is explicitly smart enough to deal with opts.env.COCALC_JUPYTER_FILENAME353opts.env.COCALC_JUPYTER_FILENAME = this._path;354// and launchJupyterKernel is NOT smart enough to deal with anything else!355356try {357dbg("launching kernel interface...");358this._kernel = await launchJupyterKernel(this.name, opts);359await this.finish_spawn();360} catch (err) {361dbg("ERROR spawning kernel", err);362if (this._state === "closed") {363throw Error("closed -- kernel spawn later");364}365this._set_state("off");366throw err;367}368369// NOW we do path-related customizations:370// TODO: we will set each of these after getting a kernel from the pool371// expose path of jupyter notebook -- https://github.com/sagemathinc/cocalc/issues/5165372//opts.env.COCALC_JUPYTER_FILENAME = this._path;373// if (this._directory !== "") {374// opts.cwd = this._directory;375// }376}377378get_spawned_kernel() {379return this._kernel;380}381382public get_connection_file(): string | undefined {383return this._kernel?.connectionFile;384}385386private finish_spawn = async () => {387const dbg = this.dbg("finish_spawn");388dbg("now finishing spawn of kernel...");389390if (DEBUG) {391this.low_level_dbg();392}393394if (!this._kernel) {395throw Error("_kernel must be defined");396}397this._kernel.spawn.on("error", (err) => {398const error = `${err}\n${this.stderr}`;399dbg("kernel error", error);400this.emit("kernel_error", error);401this._set_state("off");402});403404// Track stderr from the subprocess itself (the kernel).405// This is useful for debugging broken kernels, etc., and is especially406// useful since it exists even if the kernel sends nothing over any407// zmq channels (e.g., due to being very broken).408this.stderr = "";409this._kernel.spawn.stderr.on("data", (data) => {410const s = data.toString();411this.stderr += s;412if (this.stderr.length > 5000) {413// truncate if gets long for some reason -- only the end will414// be useful...415this.stderr = this.stderr.slice(this.stderr.length - 4000);416}417});418419this._kernel.spawn.stdout.on("data", (_data) => {420// NOTE: it is very important to read stdout (and stderr above)421// even if we **totally ignore** the data. Otherwise, execa saves422// some amount then just locks up and doesn't allow flushing the423// output stream. This is a "nice" feature of execa, since it means424// no data gets dropped. See https://github.com/sagemathinc/cocalc/issues/5065425});426427dbg("create main channel...", this._kernel.config);428429// This horrible code is becacuse createMainChannel will just "hang430// forever" if the kernel doesn't get spawned for some reason.431// Thus we do some tests, waiting for at least 2 seconds for there432// to be a pid. This is complicated and ugly, and I'm sorry about that,433// but sometimes that's life.434let i = 0;435while (i < 20 && this._state == "spawning" && !this._kernel?.spawn?.pid) {436i += 1;437await delay(100);438}439if (this._state != "spawning" || !this._kernel?.spawn?.pid) {440if (this._state == "spawning") {441this.emit("kernel_error", "Failed to start kernel process.");442this._set_state("off");443}444return;445}446const local = { success: false, gaveUp: false };447setTimeout(() => {448if (!local.success) {449local.gaveUp = true;450// it's been 30s and the channels didn't work. Let's give up.451// probably the kernel process just failed.452this.emit("kernel_error", "Failed to start kernel process.");453this._set_state("off");454// We can't "cancel" createMainChannel itself -- that will require455// rewriting that dependency.456// https://github.com/sagemathinc/cocalc/issues/7040457}458}, MAX_KERNEL_SPAWN_TIME);459const channel = await createMainChannel(460this._kernel.config,461"",462this.identity,463);464if (local.gaveUp) {465return;466}467this.channel = channel;468local.success = true;469dbg("created main channel");470471this.channel?.subscribe((mesg) => {472switch (mesg.channel) {473case "shell":474this._set_state("running");475this.emit("shell", mesg);476break;477case "stdin":478this.emit("stdin", mesg);479break;480case "iopub":481this._set_state("running");482if (mesg.content != null && mesg.content.execution_state != null) {483this.emit("execution_state", mesg.content.execution_state);484}485486if (mesg.content?.comm_id != null) {487// A comm message, which gets handled directly.488this.process_comm_message_from_kernel(mesg);489break;490}491492if (this._actions?.capture_output_message(mesg)) {493// captured an output message -- do not process further494break;495}496497this.emit("iopub", mesg);498break;499}500});501502this._kernel.spawn.on("exit", (exit_code, signal) => {503if (this._state === "closed") {504return;505}506this.dbg("kernel_exit")(507`spawned kernel terminated with exit code ${exit_code} (signal=${signal}); stderr=${this.stderr}`,508);509const stderr = this.stderr ? `\n...\n${this.stderr}` : "";510if (signal != null) {511this.emit(512"kernel_error",513`Kernel last terminated by signal ${signal}.${stderr}`,514);515} else if (exit_code != null) {516this.emit(517"kernel_error",518`Kernel last exited with code ${exit_code}.${stderr}`,519);520}521this.close();522});523524// so we can start sending code execution to the kernel, etc.525this._set_state("starting");526};527528// Signal should be a string like "SIGINT", "SIGKILL".529// See https://nodejs.org/api/process.html#process_process_kill_pid_signal530signal(signal: string): void {531const dbg = this.dbg("signal");532const spawn = this._kernel != null ? this._kernel.spawn : undefined;533const pid = spawn?.pid;534dbg(`pid=${pid}, signal=${signal}`);535if (pid == null) {536return;537}538try {539this.clear_execute_code_queue();540process.kill(-pid, signal); // negative to kill the process group541} catch (err) {542dbg(`error: ${err}`);543}544}545546// This is async, but the process.kill happens *before*547// anything async. That's important for cleaning these548// up when the project terminates.549async close(): Promise<void> {550this.dbg("close")();551if (this._state === "closed") {552return;553}554this._set_state("closed");555if (this.store != null) {556this.store.close();557delete this.store;558}559const kernel = _jupyter_kernels[this._path];560if (kernel != null && kernel.identity === this.identity) {561delete _jupyter_kernels[this._path];562}563this.removeAllListeners();564if (this._kernel != null) {565killKernel(this._kernel);566delete this._kernel;567delete this.channel;568}569if (this._execute_code_queue != null) {570for (const code_snippet of this._execute_code_queue) {571code_snippet.close();572}573this._execute_code_queue = [];574}575}576577// public, since we do use it from some other places...578dbg(f: string): Function {579return (...args) => {580//console.log(581logger.debug(582`jupyter.Kernel('${this.name ?? "no kernel"}',path='${583this._path584}').${f}`,585...args,586);587};588}589590low_level_dbg(): void {591const dbg = (...args) => logger.silly("low_level_debug", ...args);592dbg("Enabling");593if (this._kernel) {594this._kernel.spawn.all?.on("data", (data) =>595dbg("STDIO", data.toString()),596);597}598// for low level debugging only...599this.channel?.subscribe((mesg) => {600dbg(mesg);601});602}603604async ensure_running(): Promise<void> {605const dbg = this.dbg("ensure_running");606dbg(this._state);607if (this._state == "closed") {608throw Error("closed so not possible to ensure running");609}610if (this._state == "running") {611return;612}613dbg("spawning");614await this.spawn();615if (this._kernel?.initCode != null) {616for (const code of this._kernel?.initCode ?? []) {617dbg("initCode ", code);618await new CodeExecutionEmitter(this, { code }).go();619}620}621if (!this.has_ensured_running) {622this.has_ensured_running = true;623}624}625626execute_code(627opts: ExecOpts,628skipToFront = false,629): CodeExecutionEmitterInterface {630if (opts.halt_on_error === undefined) {631// if not specified, default to true.632opts.halt_on_error = true;633}634if (this._state === "closed") {635throw Error("closed -- kernel -- execute_code");636}637const code = new CodeExecutionEmitter(this, opts);638if (skipToFront) {639this._execute_code_queue.unshift(code);640} else {641this._execute_code_queue.push(code);642}643if (this._execute_code_queue.length == 1) {644// start it going!645this._process_execute_code_queue();646}647return code;648}649650cancel_execute(id: string): void {651if (this._state === "closed") {652return;653}654const dbg = this.dbg(`cancel_execute(id='${id}')`);655if (656this._execute_code_queue == null ||657this._execute_code_queue.length === 0658) {659dbg("nothing to do");660return;661}662if (this._execute_code_queue.length > 1) {663dbg(664"mutate this._execute_code_queue removing everything with the given id",665);666for (let i = this._execute_code_queue.length - 1; i--; i >= 1) {667const code = this._execute_code_queue[i];668if (code.id === id) {669dbg(`removing entry ${i} from queue`);670this._execute_code_queue.splice(i, 1);671code.cancel();672}673}674}675// if the currently running computation involves this id, send an676// interrupt signal (that's the best we can do)677if (this._execute_code_queue[0].id === id) {678dbg("interrupting running computation");679this.signal("SIGINT");680}681}682683async _process_execute_code_queue(): Promise<void> {684const dbg = this.dbg("_process_execute_code_queue");685dbg(`state='${this._state}'`);686if (this._state === "closed") {687dbg("closed");688return;689}690if (this._execute_code_queue == null) {691dbg("no queue");692return;693}694const n = this._execute_code_queue.length;695if (n === 0) {696dbg("queue is empty");697return;698}699dbg(700`queue has ${n} items; ensure kernel running`,701this._execute_code_queue,702);703try {704await this.ensure_running();705this._execute_code_queue[0].go();706} catch (err) {707dbg(`error running kernel -- ${err}`);708for (const code of this._execute_code_queue) {709code.throw_error(err);710}711this._execute_code_queue = [];712}713}714715public clear_execute_code_queue(): void {716const dbg = this.dbg("_clear_execute_code_queue");717// ensure no future queued up evaluation occurs (currently running718// one will complete and new executions could happen)719if (this._state === "closed") {720dbg("no op since state is closed");721return;722}723if (this._execute_code_queue == null) {724dbg("nothing to do since queue is null");725return;726}727dbg(`clearing queue of size ${this._execute_code_queue.length}`);728const mesg = { done: true };729for (const code_execution_emitter of this._execute_code_queue.slice(1)) {730code_execution_emitter.emit_output(mesg);731code_execution_emitter.close();732}733this._execute_code_queue = [];734}735736// This is like execute_code, but async and returns all the results,737// and does not use the internal execution queue.738// This is used for unit testing and interactive work at the terminal and nbgrader and the stateless api.739async execute_code_now(opts: ExecOpts): Promise<object[]> {740this.dbg("execute_code_now")();741if (this._state === "closed") {742throw Error("closed -- kernel -- execute_code_now");743}744if (opts.halt_on_error === undefined) {745// if not specified, default to true.746opts.halt_on_error = true;747}748await this.ensure_running();749return await new CodeExecutionEmitter(this, opts).go();750}751752process_output(content: any): void {753if (this._state === "closed") {754return;755}756const dbg = this.dbg("process_output");757if (content.data == null) {758// No data -- https://github.com/sagemathinc/cocalc/issues/6665759// NO do not do this sort of thing. This is exactly the sort of situation where760// content could be very large, and JSON.stringify could use huge amounts of memory.761// If you need to see this for debugging, uncomment it.762// dbg(trunc(JSON.stringify(content), 300));763// todo: FOR now -- later may remove large stdout, stderr, etc...764// dbg("no data, so nothing to do");765return;766}767768remove_redundant_reps(content.data);769770let saveToBlobStore;771try {772const blob_store = get_blob_store_sync();773saveToBlobStore = (774data: string,775type: string,776ipynb?: string,777): string => {778const sha1 = blob_store.save(data, type, ipynb);779if (this._actions?.is_compute_server) {780this._actions?.saveBlobToProject(data, type, ipynb);781}782return sha1;783};784} catch (err) {785dbg(`WARNING: Jupyter blob store is not available -- ${err}`);786// there is nothing to process without the blob store to save787// data in!788return;789}790791let type: string;792for (type of JUPYTER_MIMETYPES) {793if (content.data[type] == null) {794continue;795}796if (type.split("/")[0] === "image" || type === "application/pdf") {797// Store all images and PDF in the blob store:798content.data[type] = saveToBlobStore(content.data[type], type);799} else if (type === "text/html" && is_likely_iframe(content.data[type])) {800// Likely iframe, so we treat it as such. This is very important, e.g.,801// because of Sage's iframe 3d graphics. We parse802// and remove these and serve them from the backend.803// {iframe: sha1 of srcdoc}804content.data["iframe"] = iframe_process(805content.data[type],806saveToBlobStore,807);808delete content.data[type];809}810}811}812813async call(msg_type: string, content?: any): Promise<any> {814this.dbg("call")(msg_type);815if (!this.has_ensured_running) {816await this.ensure_running();817}818// Do a paranoid double check anyways...819if (this.channel == null || this._state == "closed") {820throw Error("not running, so can't call");821}822823const message = {824parent_header: {},825metadata: {},826channel: "shell",827content,828header: {829msg_id: uuid(),830username: "",831session: "",832msg_type: msg_type as MessageType,833version: VERSION,834date: new Date().toISOString(),835},836};837838// Send the message839this.channel?.next(message);840841// Wait for the response that has the right msg_id.842let the_mesg: any = undefined;843const wait_for_response = (cb) => {844const f = (mesg) => {845if (mesg.parent_header.msg_id === message.header.msg_id) {846this.removeListener("shell", f);847this.removeListener("closed", g);848mesg = deep_copy(mesg.content);849if (len(mesg.metadata) === 0) {850delete mesg.metadata;851}852the_mesg = mesg;853cb();854}855};856const g = () => {857this.removeListener("shell", f);858this.removeListener("closed", g);859cb("closed - jupyter - kernel - call");860};861this.on("shell", f);862this.on("closed", g);863};864await callback(wait_for_response);865return the_mesg;866}867868async complete(opts: { code: any; cursor_pos: any }): Promise<any> {869const dbg = this.dbg("complete");870dbg(`code='${opts.code}', cursor_pos='${opts.cursor_pos}'`);871return await this.call("complete_request", opts);872}873874async introspect(opts: {875code: any;876cursor_pos: any;877detail_level: any;878}): Promise<any> {879const dbg = this.dbg("introspect");880dbg(881`code='${opts.code}', cursor_pos='${opts.cursor_pos}', detail_level=${opts.detail_level}`,882);883return await this.call("inspect_request", opts);884}885886async kernel_info(): Promise<KernelInfo> {887if (this._kernel_info !== undefined) {888return this._kernel_info;889}890const info = await this.call("kernel_info_request");891info.nodejs_version = process.version;892if (this._actions != null) {893info.start_time = this._actions.store.get("start_time");894}895this._kernel_info = info;896return info;897}898899async save_ipynb_file(): Promise<void> {900if (this._actions != null) {901await this._actions.save_ipynb_file();902} else {903throw Error("save_ipynb_file -- ERROR: actions not known");904}905}906907more_output(id: string): any[] {908if (id == null) {909throw new Error("must specify id");910}911if (this._actions == null) {912throw new Error("must have redux actions");913}914return this._actions.store.get_more_output(id) || [];915}916917async nbconvert(args: string[], timeout?: number): Promise<void> {918if (timeout === undefined) {919timeout = 60; // seconds920}921if (!is_array(args)) {922throw new Error("args must be an array");923}924args = copy(args);925args.push("--");926args.push(this._filename);927await nbconvert({928args,929timeout,930directory: this._directory,931});932}933934// TODO: double check that this actually returns sha1935async load_attachment(path: string): Promise<string> {936const dbg = this.dbg("load_attachment");937dbg(`path='${path}'`);938if (path[0] !== "/") {939path = process.env.HOME + "/" + path;940}941async function f(): Promise<string> {942const bs = get_blob_store_sync();943if (bs == null) throw new Error("BlobStore not available");944return bs.readFile(path, "base64");945}946try {947return await retry_until_success({948f: f,949max_time: 30000,950});951} catch (err) {952unlink(path); // TODO: think through again if this is the right thing to do.953throw err;954}955}956957// This is called by project-actions when exporting the notebook958// to an ipynb file:959get_blob_store() {960return get_blob_store_sync();961}962963process_attachment(base64, mime): string | undefined {964const blob_store = get_blob_store_sync();965return blob_store?.save(base64, mime);966}967968process_comm_message_from_kernel(mesg): void {969if (this._actions == null) {970return;971}972const dbg = this.dbg("process_comm_message_from_kernel");973// This can be HUGE so don't print out the entire message; e.g., it could contain974// massive binary data!975dbg(mesg.header);976this._actions.process_comm_message_from_kernel(mesg);977}978979public ipywidgetsGetBuffer(980model_id: string,981// buffer_path is the string[] *or* the JSON of that.982buffer_path: string | string[],983): Buffer | undefined {984if (typeof buffer_path != "string") {985buffer_path = JSON.stringify(buffer_path);986}987return this._actions?.syncdb.ipywidgets_state?.getBuffer(988model_id,989buffer_path,990);991}992993public send_comm_message_to_kernel({994msg_id,995comm_id,996target_name,997data,998buffers64,999buffers,1000}: {1001msg_id: string;1002comm_id: string;1003target_name: string;1004data: any;1005buffers64?: string[];1006buffers?: Buffer[];1007}): void {1008const dbg = this.dbg("send_comm_message_to_kernel");1009dbg({ msg_id, comm_id, target_name, data, buffers64 });1010if (buffers64 != null && buffers64.length > 0) {1011buffers = buffers64?.map((x) => Buffer.from(base64ToBuffer(x))) ?? [];1012dbg(1013"buffers lengths = ",1014buffers.map((x) => x.byteLength),1015);1016if (this._actions?.syncdb.ipywidgets_state != null) {1017this._actions.syncdb.ipywidgets_state.setModelBuffers(1018comm_id,1019data.buffer_paths,1020buffers,1021false,1022);1023}1024}10251026const message = {1027parent_header: {},1028metadata: {},1029channel: "shell",1030content: { comm_id, target_name, data },1031header: {1032msg_id,1033username: "user",1034session: "",1035msg_type: "comm_msg" as MessageType,1036version: VERSION,1037date: new Date().toISOString(),1038},1039buffers,1040};10411042dbg(message);1043// "The Kernel listens for these messages on the Shell channel,1044// and the Frontend listens for them on the IOPub channel." -- docs1045this.channel?.next(message);1046}10471048async chdir(path: string): Promise<void> {1049if (!this.name) return; // no kernel, no current directory1050const dbg = this.dbg("chdir");1051dbg({ path });1052let lang;1053try {1054// using probably cached data, so likely very fast1055lang = await getLanguage(this.name);1056} catch (err) {1057dbg("WARNING ", err);1058const info = await this.kernel_info();1059lang = info.language_info?.name ?? "";1060}10611062const absPath = getAbsolutePathFromHome(path);1063const code = createChdirCommand(lang, absPath);1064if (code) {1065// returns '' if no command needed, e.g., for sparql.1066await this.execute_code_now({ code });1067}1068}1069}10701071export function get_existing_kernel(path: string): JupyterKernel | undefined {1072return _jupyter_kernels[path];1073}10741075export function get_kernel_by_pid(pid: number): JupyterKernel | undefined {1076for (const kernel of Object.values(_jupyter_kernels)) {1077if (kernel.get_spawned_kernel()?.spawn.pid === pid) {1078return kernel;1079}1080}1081return;1082}108310841085