Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/jupyter/kernel/launch-kernel.ts
1447 views
1
// This file allows you to run a jupyter kernel via `launch_jupyter_kernel`.
2
// You have to provide the kernel name and (optionally) launch options.
3
//
4
// Example:
5
// import launchJupyterKernel from "./launch-jupyter-kernel";
6
// const kernel = await launchJupyterKernel("python3", {cwd: "/home/user"})
7
//
8
// * shell channel: `${kernel.config.ip}:${kernel.config.shell_port}`
9
// * `kernel.spawn` holds the process and you have to close it when finished.
10
// * Unless `cleanupConnectionFile` is false, the connection file will be deleted when finished.
11
//
12
// History:
13
// This is a port of https://github.com/nteract/spawnteract/ to TypeScript (with minor changes).
14
// Original license: BSD-3-Clause and this file is also licensed under BSD-3-Clause!
15
// Author: Harald Schilly <[email protected]>
16
// Author: William Stein <[email protected]>
17
18
import * as path from "path";
19
import * as fs from "fs";
20
import * as uuid from "uuid";
21
import { mkdir } from "fs/promises";
22
import { spawn } from "node:child_process";
23
import { findAll } from "kernelspecs";
24
import * as jupyter_paths from "jupyter-paths";
25
import bash from "@cocalc/backend/bash";
26
import { writeFile } from "jsonfile";
27
import mkdirp from "mkdirp";
28
import shellEscape from "shell-escape";
29
import { envForSpawn } from "@cocalc/backend/misc";
30
import { getLogger } from "@cocalc/backend/logger";
31
import { getPorts } from "@cocalc/backend/get-port";
32
33
const logger = getLogger("launch-kernel");
34
35
// this is passed to "execa", there are more options
36
// https://github.com/sindresorhus/execa#options
37
// https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio
38
type StdIO = "pipe" | "ignore" | "inherit" | undefined;
39
export interface LaunchJupyterOpts {
40
stdio?: StdIO | (StdIO | number)[];
41
env: { [key: string]: string };
42
cwd?: string;
43
cleanupConnectionFile?: boolean;
44
cleanup?: boolean;
45
preferLocal?: boolean;
46
localDir?: string;
47
execPath?: string;
48
buffer?: boolean;
49
reject?: boolean;
50
stripFinalNewline?: boolean;
51
shell?: boolean | string; // default false
52
// command line options for ulimit. You can launch a kernel
53
// but with these options set. Note that this uses the shell
54
// to wrap launching the kernel, so it's more complicated.
55
ulimit?: string;
56
}
57
58
export interface SpawnedKernel {
59
spawn; // output of execa
60
connectionFile: string;
61
config: ConnectionInfo;
62
kernel_spec;
63
initCode?: string[];
64
}
65
66
interface ConnectionInfo {
67
version: number;
68
key: string;
69
signature_scheme: "hmac-sha256";
70
transport: "tcp" | "ipc";
71
ip: string;
72
hb_port: number;
73
control_port: number;
74
shell_port: number;
75
stdin_port: number;
76
iopub_port: number;
77
}
78
79
function connectionInfo(ports): ConnectionInfo {
80
return {
81
version: 5,
82
key: uuid.v4(),
83
signature_scheme: "hmac-sha256",
84
transport: "tcp",
85
ip: "127.0.0.1",
86
hb_port: ports[0],
87
control_port: ports[1],
88
shell_port: ports[2],
89
stdin_port: ports[3],
90
iopub_port: ports[4],
91
};
92
}
93
94
// gather the connection information for a kernel, write it to a json file, and return it
95
async function writeConnectionFile() {
96
const ports = await getPorts(5);
97
// console.log("ports = ", ports);
98
99
// Make sure the kernel runtime dir exists before trying to write the kernel file.
100
const runtimeDir = jupyter_paths.runtimeDir();
101
await mkdirp(runtimeDir);
102
103
// Write the kernel connection file -- filename uses the UUID4 key
104
const config = connectionInfo(ports);
105
const connectionFile = path.join(runtimeDir, `kernel-${config.key}.json`);
106
107
await writeFile(connectionFile, config);
108
return { config, connectionFile };
109
}
110
111
// if spawn options' cleanupConnectionFile is true, the connection file is removed
112
function cleanup(connectionFile) {
113
try {
114
fs.unlinkSync(connectionFile);
115
} catch (e) {
116
return;
117
}
118
}
119
120
const DEFAULT_SPAWN_OPTIONS = {
121
cleanupConnectionFile: true,
122
env: {},
123
} as const;
124
125
// actually launch the kernel.
126
// the returning object contains all the configuration information and in particular,
127
// `spawn` is the running process started by "execa"
128
async function launchKernelSpec(
129
kernel_spec,
130
config: ConnectionInfo,
131
connectionFile: string,
132
spawn_options: LaunchJupyterOpts,
133
): Promise<SpawnedKernel> {
134
const argv = kernel_spec.argv.map((x) =>
135
x.replace("{connection_file}", connectionFile),
136
);
137
138
const full_spawn_options = {
139
...DEFAULT_SPAWN_OPTIONS,
140
...spawn_options,
141
detached: true, // for cocalc we always assume this
142
};
143
144
full_spawn_options.env = {
145
...envForSpawn(),
146
...kernel_spec.env,
147
...spawn_options.env,
148
};
149
150
let running_kernel;
151
152
if (full_spawn_options.cwd != null) {
153
await ensureDirectoryExists(full_spawn_options.cwd);
154
}
155
156
if (spawn_options.ulimit) {
157
// Convert the ulimit arguments to a string
158
const ulimitCmd = `ulimit ${spawn_options.ulimit}`;
159
160
// Escape the command and arguments for safe usage in a shell command
161
const escapedCmd = shellEscape(argv);
162
163
// Prepend the ulimit command
164
const bashCmd = `${ulimitCmd}\n\n${escapedCmd}`;
165
166
// Execute the command with ulimit
167
running_kernel = await bash(bashCmd, full_spawn_options);
168
} else {
169
running_kernel = spawn(argv[0], argv.slice(1), full_spawn_options);
170
}
171
172
running_kernel.on("error", (code, signal) => {
173
logger.debug("launchKernelSpec: ERROR -- ", { argv, code, signal });
174
});
175
176
if (full_spawn_options.cleanupConnectionFile !== false) {
177
running_kernel.on("exit", (_code, _signal) => cleanup(connectionFile));
178
running_kernel.on("error", (_code, _signal) => cleanup(connectionFile));
179
}
180
return {
181
spawn: running_kernel,
182
connectionFile,
183
config,
184
kernel_spec,
185
};
186
}
187
188
// For a given kernel name and launch options: prepare the kernel file and launch the process
189
export default async function launchJupyterKernel(
190
name: string,
191
spawn_options: LaunchJupyterOpts,
192
): Promise<SpawnedKernel> {
193
const specs = await findAll();
194
const kernel_spec = specs[name];
195
if (kernel_spec == null) {
196
throw new Error(
197
`No spec available for kernel "${name}". Available specs: ${JSON.stringify(
198
Object.keys(specs),
199
)}`,
200
);
201
}
202
const { config, connectionFile } = await writeConnectionFile();
203
return await launchKernelSpec(
204
kernel_spec.spec,
205
config,
206
connectionFile,
207
spawn_options,
208
);
209
}
210
211
async function ensureDirectoryExists(path: string) {
212
try {
213
await mkdir(path, { recursive: true });
214
} catch (error) {
215
if (error.code !== "EEXIST") {
216
throw error;
217
}
218
}
219
}
220
221