Path: blob/master/src/packages/jupyter/kernel/launch-kernel.ts
1447 views
// 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.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// History:12// This is a port of https://github.com/nteract/spawnteract/ to TypeScript (with minor changes).13// Original license: BSD-3-Clause and this file is also licensed under BSD-3-Clause!14// Author: Harald Schilly <[email protected]>15// Author: William Stein <[email protected]>1617import * as path from "path";18import * as fs from "fs";19import * as uuid from "uuid";20import { mkdir } from "fs/promises";21import { spawn } from "node:child_process";22import { findAll } from "kernelspecs";23import * as jupyter_paths from "jupyter-paths";24import bash from "@cocalc/backend/bash";25import { writeFile } from "jsonfile";26import mkdirp from "mkdirp";27import shellEscape from "shell-escape";28import { envForSpawn } from "@cocalc/backend/misc";29import { getLogger } from "@cocalc/backend/logger";30import { getPorts } from "@cocalc/backend/get-port";3132const logger = getLogger("launch-kernel");3334// this is passed to "execa", there are more options35// https://github.com/sindresorhus/execa#options36// https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio37type StdIO = "pipe" | "ignore" | "inherit" | undefined;38export interface LaunchJupyterOpts {39stdio?: StdIO | (StdIO | number)[];40env: { [key: string]: string };41cwd?: string;42cleanupConnectionFile?: boolean;43cleanup?: boolean;44preferLocal?: boolean;45localDir?: string;46execPath?: string;47buffer?: boolean;48reject?: boolean;49stripFinalNewline?: boolean;50shell?: boolean | string; // default false51// command line options for ulimit. You can launch a kernel52// but with these options set. Note that this uses the shell53// to wrap launching the kernel, so it's more complicated.54ulimit?: string;55}5657export interface SpawnedKernel {58spawn; // output of execa59connectionFile: string;60config: ConnectionInfo;61kernel_spec;62initCode?: string[];63}6465interface ConnectionInfo {66version: number;67key: string;68signature_scheme: "hmac-sha256";69transport: "tcp" | "ipc";70ip: string;71hb_port: number;72control_port: number;73shell_port: number;74stdin_port: number;75iopub_port: number;76}7778function connectionInfo(ports): ConnectionInfo {79return {80version: 5,81key: uuid.v4(),82signature_scheme: "hmac-sha256",83transport: "tcp",84ip: "127.0.0.1",85hb_port: ports[0],86control_port: ports[1],87shell_port: ports[2],88stdin_port: ports[3],89iopub_port: ports[4],90};91}9293// gather the connection information for a kernel, write it to a json file, and return it94async function writeConnectionFile() {95const ports = await getPorts(5);96// console.log("ports = ", ports);9798// Make sure the kernel runtime dir exists before trying to write the kernel file.99const runtimeDir = jupyter_paths.runtimeDir();100await mkdirp(runtimeDir);101102// Write the kernel connection file -- filename uses the UUID4 key103const config = connectionInfo(ports);104const connectionFile = path.join(runtimeDir, `kernel-${config.key}.json`);105106await writeFile(connectionFile, config);107return { config, connectionFile };108}109110// if spawn options' cleanupConnectionFile is true, the connection file is removed111function cleanup(connectionFile) {112try {113fs.unlinkSync(connectionFile);114} catch (e) {115return;116}117}118119const DEFAULT_SPAWN_OPTIONS = {120cleanupConnectionFile: true,121env: {},122} as const;123124// actually launch the kernel.125// the returning object contains all the configuration information and in particular,126// `spawn` is the running process started by "execa"127async function launchKernelSpec(128kernel_spec,129config: ConnectionInfo,130connectionFile: string,131spawn_options: LaunchJupyterOpts,132): Promise<SpawnedKernel> {133const argv = kernel_spec.argv.map((x) =>134x.replace("{connection_file}", connectionFile),135);136137const full_spawn_options = {138...DEFAULT_SPAWN_OPTIONS,139...spawn_options,140detached: true, // for cocalc we always assume this141};142143full_spawn_options.env = {144...envForSpawn(),145...kernel_spec.env,146...spawn_options.env,147};148149let running_kernel;150151if (full_spawn_options.cwd != null) {152await ensureDirectoryExists(full_spawn_options.cwd);153}154155if (spawn_options.ulimit) {156// Convert the ulimit arguments to a string157const ulimitCmd = `ulimit ${spawn_options.ulimit}`;158159// Escape the command and arguments for safe usage in a shell command160const escapedCmd = shellEscape(argv);161162// Prepend the ulimit command163const bashCmd = `${ulimitCmd}\n\n${escapedCmd}`;164165// Execute the command with ulimit166running_kernel = await bash(bashCmd, full_spawn_options);167} else {168running_kernel = spawn(argv[0], argv.slice(1), full_spawn_options);169}170171running_kernel.on("error", (code, signal) => {172logger.debug("launchKernelSpec: ERROR -- ", { argv, code, signal });173});174175if (full_spawn_options.cleanupConnectionFile !== false) {176running_kernel.on("exit", (_code, _signal) => cleanup(connectionFile));177running_kernel.on("error", (_code, _signal) => cleanup(connectionFile));178}179return {180spawn: running_kernel,181connectionFile,182config,183kernel_spec,184};185}186187// For a given kernel name and launch options: prepare the kernel file and launch the process188export default async function launchJupyterKernel(189name: string,190spawn_options: LaunchJupyterOpts,191): Promise<SpawnedKernel> {192const specs = await findAll();193const kernel_spec = specs[name];194if (kernel_spec == null) {195throw new Error(196`No spec available for kernel "${name}". Available specs: ${JSON.stringify(197Object.keys(specs),198)}`,199);200}201const { config, connectionFile } = await writeConnectionFile();202return await launchKernelSpec(203kernel_spec.spec,204config,205connectionFile,206spawn_options,207);208}209210async function ensureDirectoryExists(path: string) {211try {212await mkdir(path, { recursive: true });213} catch (error) {214if (error.code !== "EEXIST") {215throw error;216}217}218}219220221