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/launch-kernel.ts
Views: 687
// This file allows you to run a jupyter kernel via `launch_jupyter_kernel`.1// You have to provide the kernel name and (optionally) launch options for execa [1].2//3// Example:4// import launchJupyterKernel from "./launch-jupyter-kernel";5// const kernel = await launchJupyterKernel("python3", {cwd: "/home/user"})6//7// * shell channel: `${kernel.config.ip}:${kernel.config.shell_port}`8// * `kernel.spawn` holds the process and you have to close it when finished.9// * Unless `cleanupConnectionFile` is false, the connection file will be deleted when finished.10//11// Ref:12// [1] execa: https://github.com/sindresorhus/execa#readme13//14// History:15// This is a port of https://github.com/nteract/spawnteract/ to TypeScript (with minor changes).16// Original license: BSD-3-Clause and this file is also licensed under BSD-3-Clause!17// Author: Harald Schilly <[email protected]>18// Author: William Stein <[email protected]>1920import * as path from "path";21import * as fs from "fs";22import * as uuid from "uuid";23import { mkdir } from "fs/promises";24import { spawn } from "node:child_process";2526import { findAll } from "kernelspecs";27import * as jupyter_paths from "jupyter-paths";2829import getPorts from "./get-ports";30import { writeFile } from "jsonfile";31import mkdirp from "mkdirp";32import shellEscape from "shell-escape";33import { envForSpawn } from "@cocalc/backend/misc";34import { getLogger } from "@cocalc/backend/logger";3536const logger = getLogger("launch-kernel");3738// This is temporary hack to import the latest execa, which is only39// available as an ES Module now. We will of course eventually switch40// to using esm modules instead of commonjs, but that's a big project.41import { dynamicImport } from "tsimportlib";4243// this is passed to "execa", there are more options44// https://github.com/sindresorhus/execa#options45// https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio46type StdIO = "pipe" | "ignore" | "inherit" | undefined;47export interface LaunchJupyterOpts {48stdio?: StdIO | (StdIO | number)[];49env: { [key: string]: string };50cwd?: string;51cleanupConnectionFile?: boolean;52cleanup?: boolean;53preferLocal?: boolean;54localDir?: string;55execPath?: string;56buffer?: boolean;57reject?: boolean;58stripFinalNewline?: boolean;59shell?: boolean | string; // default false60// command line options for ulimit. You can launch a kernel61// but with these options set. Note that this uses the shell62// to wrap launching the kernel, so it's more complicated.63ulimit?: string;64}6566export interface SpawnedKernel {67spawn; // output of execa68connectionFile: string;69config: ConnectionInfo;70kernel_spec;71initCode?: string[];72}7374interface ConnectionInfo {75version: number;76key: string;77signature_scheme: "hmac-sha256";78transport: "tcp" | "ipc";79ip: string;80hb_port: number;81control_port: number;82shell_port: number;83stdin_port: number;84iopub_port: number;85}8687function connectionInfo(ports): ConnectionInfo {88return {89version: 5,90key: uuid.v4(),91signature_scheme: "hmac-sha256",92transport: "tcp",93ip: "127.0.0.1",94hb_port: ports[0],95control_port: ports[1],96shell_port: ports[2],97stdin_port: ports[3],98iopub_port: ports[4],99};100}101102const DEFAULT_PORT_OPTS = { port: 9000, host: "127.0.0.1" } as const;103104// gather the connection information for a kernel, write it to a json file, and return it105async function writeConnectionFile(port_options?: {106port?: number;107host?: string;108}) {109const options = { ...DEFAULT_PORT_OPTS, ...port_options };110const ports = await getPorts(5, options);111112// Make sure the kernel runtime dir exists before trying to write the kernel file.113const runtimeDir = jupyter_paths.runtimeDir();114await mkdirp(runtimeDir);115116// Write the kernel connection file -- filename uses the UUID4 key117const config = connectionInfo(ports);118const connectionFile = path.join(runtimeDir, `kernel-${config.key}.json`);119120await writeFile(connectionFile, config);121return { config, connectionFile };122}123124// if spawn options' cleanupConnectionFile is true, the connection file is removed125function cleanup(connectionFile) {126try {127fs.unlinkSync(connectionFile);128} catch (e) {129return;130}131}132133const DEFAULT_SPAWN_OPTIONS = {134cleanupConnectionFile: true,135env: {},136} as const;137138// actually launch the kernel.139// the returning object contains all the configuration information and in particular,140// `spawn` is the running process started by "execa"141async function launchKernelSpec(142kernel_spec,143config: ConnectionInfo,144connectionFile: string,145spawn_options: LaunchJupyterOpts,146): Promise<SpawnedKernel> {147const argv = kernel_spec.argv.map((x) =>148x.replace("{connection_file}", connectionFile),149);150151const full_spawn_options = {152...DEFAULT_SPAWN_OPTIONS,153...spawn_options,154detached: true, // for cocalc we always assume this155};156157full_spawn_options.env = {158...envForSpawn(),159...kernel_spec.env,160...spawn_options.env,161};162163const { execaCommand } = (await dynamicImport(164"execa",165module,166)) as typeof import("execa");167168let running_kernel;169170if (full_spawn_options.cwd != null) {171await ensureDirectoryExists(full_spawn_options.cwd);172}173174if (spawn_options.ulimit) {175// Convert the ulimit arguments to a string176const ulimitCmd = `ulimit ${spawn_options.ulimit}`;177178// Escape the command and arguments for safe usage in a shell command179const escapedCmd = shellEscape(argv);180181// Prepend the ulimit command182const bashCmd = `${ulimitCmd} && ${escapedCmd}`;183184// Execute the command with ulimit185running_kernel = execaCommand(bashCmd, {186...full_spawn_options,187shell: true,188});189} else {190// CRITICAL: I am *NOT* using execa, but instead spawn, because191// I hit bugs in execa. Namely, when argv[0] is a path that doesn't exist,192// no matter what, there is an uncaught exception emitted later. The exact193// same situation with execaCommand or node's spawn does NOT have an uncaught194// exception, so it's a bug.195//running_kernel = execa(argv[0], argv.slice(1), full_spawn_options); // NO!196running_kernel = spawn(argv[0], argv.slice(1), full_spawn_options);197}198199running_kernel.on("error", (code, signal) => {200logger.debug("launchKernelSpec: ERROR -- ", { argv, code, signal });201});202203if (full_spawn_options.cleanupConnectionFile !== false) {204running_kernel.on("exit", (_code, _signal) => cleanup(connectionFile));205running_kernel.on("error", (_code, _signal) => cleanup(connectionFile));206}207return {208spawn: running_kernel,209connectionFile,210config,211kernel_spec,212};213}214215// For a given kernel name and launch options: prepare the kernel file and launch the process216export default async function launchJupyterKernel(217name: string,218spawn_options: LaunchJupyterOpts,219): Promise<SpawnedKernel> {220const specs = await findAll();221const kernel_spec = specs[name];222if (kernel_spec == null) {223throw new Error(224`No spec available for kernel "${name}". Available specs: ${JSON.stringify(225Object.keys(specs),226)}`,227);228}229const { config, connectionFile } = await writeConnectionFile();230return await launchKernelSpec(231kernel_spec.spec,232config,233connectionFile,234spawn_options,235);236}237238async function ensureDirectoryExists(path: string) {239try {240await mkdir(path, { recursive: true });241} catch (error) {242if (error.code !== "EEXIST") {243throw error;244}245}246}247248249