Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/jupyter/kernel/launch-kernel.ts
5716 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 node:child_process spawn
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
// Store kernel info for tracking
173
running_kernel.connectionFile = connectionFile;
174
running_kernel.kernel_spec = kernel_spec;
175
176
spawned.push(running_kernel);
177
178
running_kernel.on("error", (code, signal) => {
179
logger.debug("launchKernelSpec: ERROR -- ", { argv, code, signal });
180
});
181
182
if (full_spawn_options.cleanupConnectionFile !== false) {
183
running_kernel.on("exit", (_code, _signal) => cleanup(connectionFile));
184
running_kernel.on("error", (_code, _signal) => cleanup(connectionFile));
185
}
186
return {
187
spawn: running_kernel,
188
connectionFile,
189
config,
190
kernel_spec,
191
};
192
}
193
194
// For a given kernel name and launch options: prepare the kernel file and launch the process
195
export default async function launchJupyterKernel(
196
name: string,
197
spawn_options: LaunchJupyterOpts,
198
): Promise<SpawnedKernel> {
199
const specs = await findAll();
200
const kernel_spec = specs[name];
201
if (kernel_spec == null) {
202
throw new Error(
203
`No spec available for kernel "${name}". Available specs: ${JSON.stringify(
204
Object.keys(specs),
205
)}`,
206
);
207
}
208
const { config, connectionFile } = await writeConnectionFile();
209
return await launchKernelSpec(
210
kernel_spec.spec,
211
config,
212
connectionFile,
213
spawn_options,
214
);
215
}
216
217
async function ensureDirectoryExists(path: string) {
218
try {
219
await mkdir(path, { recursive: true });
220
} catch (error) {
221
if (error.code !== "EEXIST") {
222
throw error;
223
}
224
}
225
}
226
227
// Clean up after any children created here
228
const spawned: any[] = [];
229
230
export interface RunningKernel {
231
pid: number;
232
connectionFile: string;
233
kernel_name?: string;
234
}
235
236
export function listRunningKernels(): RunningKernel[] {
237
return spawned
238
.filter((child) => child.pid)
239
.map((child) => ({
240
pid: child.pid,
241
connectionFile: child.connectionFile || "unknown",
242
kernel_name: child.kernel_spec?.name,
243
}));
244
}
245
246
export function stopKernel(pid: number): boolean {
247
const index = spawned.findIndex((child) => child.pid === pid);
248
if (index === -1) {
249
return false;
250
}
251
252
const child = spawned[index];
253
try {
254
// Try to kill the process group first (negative PID)
255
process.kill(-child.pid, "SIGKILL");
256
} catch (err) {
257
// If that fails, try killing the process directly
258
try {
259
child.kill("SIGKILL");
260
} catch (err2) {
261
logger.debug(`stopKernel: failed to kill ${child.pid}: ${err2}`);
262
return false;
263
}
264
}
265
266
// Remove from spawned array
267
spawned.splice(index, 1);
268
return true;
269
}
270
271
export function closeAll() {
272
for (const child of spawned) {
273
if (child.pid) {
274
process.kill(-child.pid, "SIGKILL");
275
child.kill("SIGKILL");
276
}
277
}
278
spawned.length = 0;
279
}
280
281
process.once("exit", () => {
282
closeAll();
283
});
284
285
["SIGINT", "SIGTERM", "SIGQUIT"].forEach((sig) => {
286
process.once(sig, () => {
287
closeAll();
288
});
289
});
290
291