CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/jupyter/kernel/launch-kernel.ts
Views: 687
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 for execa [1].
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
// Ref:
13
// [1] execa: https://github.com/sindresorhus/execa#readme
14
//
15
// History:
16
// This is a port of https://github.com/nteract/spawnteract/ to TypeScript (with minor changes).
17
// Original license: BSD-3-Clause and this file is also licensed under BSD-3-Clause!
18
// Author: Harald Schilly <[email protected]>
19
// Author: William Stein <[email protected]>
20
21
import * as path from "path";
22
import * as fs from "fs";
23
import * as uuid from "uuid";
24
import { mkdir } from "fs/promises";
25
import { spawn } from "node:child_process";
26
27
import { findAll } from "kernelspecs";
28
import * as jupyter_paths from "jupyter-paths";
29
30
import getPorts from "./get-ports";
31
import { writeFile } from "jsonfile";
32
import mkdirp from "mkdirp";
33
import shellEscape from "shell-escape";
34
import { envForSpawn } from "@cocalc/backend/misc";
35
import { getLogger } from "@cocalc/backend/logger";
36
37
const logger = getLogger("launch-kernel");
38
39
// This is temporary hack to import the latest execa, which is only
40
// available as an ES Module now. We will of course eventually switch
41
// to using esm modules instead of commonjs, but that's a big project.
42
import { dynamicImport } from "tsimportlib";
43
44
// this is passed to "execa", there are more options
45
// https://github.com/sindresorhus/execa#options
46
// https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio
47
type StdIO = "pipe" | "ignore" | "inherit" | undefined;
48
export interface LaunchJupyterOpts {
49
stdio?: StdIO | (StdIO | number)[];
50
env: { [key: string]: string };
51
cwd?: string;
52
cleanupConnectionFile?: boolean;
53
cleanup?: boolean;
54
preferLocal?: boolean;
55
localDir?: string;
56
execPath?: string;
57
buffer?: boolean;
58
reject?: boolean;
59
stripFinalNewline?: boolean;
60
shell?: boolean | string; // default false
61
// command line options for ulimit. You can launch a kernel
62
// but with these options set. Note that this uses the shell
63
// to wrap launching the kernel, so it's more complicated.
64
ulimit?: string;
65
}
66
67
export interface SpawnedKernel {
68
spawn; // output of execa
69
connectionFile: string;
70
config: ConnectionInfo;
71
kernel_spec;
72
initCode?: string[];
73
}
74
75
interface ConnectionInfo {
76
version: number;
77
key: string;
78
signature_scheme: "hmac-sha256";
79
transport: "tcp" | "ipc";
80
ip: string;
81
hb_port: number;
82
control_port: number;
83
shell_port: number;
84
stdin_port: number;
85
iopub_port: number;
86
}
87
88
function connectionInfo(ports): ConnectionInfo {
89
return {
90
version: 5,
91
key: uuid.v4(),
92
signature_scheme: "hmac-sha256",
93
transport: "tcp",
94
ip: "127.0.0.1",
95
hb_port: ports[0],
96
control_port: ports[1],
97
shell_port: ports[2],
98
stdin_port: ports[3],
99
iopub_port: ports[4],
100
};
101
}
102
103
const DEFAULT_PORT_OPTS = { port: 9000, host: "127.0.0.1" } as const;
104
105
// gather the connection information for a kernel, write it to a json file, and return it
106
async function writeConnectionFile(port_options?: {
107
port?: number;
108
host?: string;
109
}) {
110
const options = { ...DEFAULT_PORT_OPTS, ...port_options };
111
const ports = await getPorts(5, options);
112
113
// Make sure the kernel runtime dir exists before trying to write the kernel file.
114
const runtimeDir = jupyter_paths.runtimeDir();
115
await mkdirp(runtimeDir);
116
117
// Write the kernel connection file -- filename uses the UUID4 key
118
const config = connectionInfo(ports);
119
const connectionFile = path.join(runtimeDir, `kernel-${config.key}.json`);
120
121
await writeFile(connectionFile, config);
122
return { config, connectionFile };
123
}
124
125
// if spawn options' cleanupConnectionFile is true, the connection file is removed
126
function cleanup(connectionFile) {
127
try {
128
fs.unlinkSync(connectionFile);
129
} catch (e) {
130
return;
131
}
132
}
133
134
const DEFAULT_SPAWN_OPTIONS = {
135
cleanupConnectionFile: true,
136
env: {},
137
} as const;
138
139
// actually launch the kernel.
140
// the returning object contains all the configuration information and in particular,
141
// `spawn` is the running process started by "execa"
142
async function launchKernelSpec(
143
kernel_spec,
144
config: ConnectionInfo,
145
connectionFile: string,
146
spawn_options: LaunchJupyterOpts,
147
): Promise<SpawnedKernel> {
148
const argv = kernel_spec.argv.map((x) =>
149
x.replace("{connection_file}", connectionFile),
150
);
151
152
const full_spawn_options = {
153
...DEFAULT_SPAWN_OPTIONS,
154
...spawn_options,
155
detached: true, // for cocalc we always assume this
156
};
157
158
full_spawn_options.env = {
159
...envForSpawn(),
160
...kernel_spec.env,
161
...spawn_options.env,
162
};
163
164
const { execaCommand } = (await dynamicImport(
165
"execa",
166
module,
167
)) as typeof import("execa");
168
169
let running_kernel;
170
171
if (full_spawn_options.cwd != null) {
172
await ensureDirectoryExists(full_spawn_options.cwd);
173
}
174
175
if (spawn_options.ulimit) {
176
// Convert the ulimit arguments to a string
177
const ulimitCmd = `ulimit ${spawn_options.ulimit}`;
178
179
// Escape the command and arguments for safe usage in a shell command
180
const escapedCmd = shellEscape(argv);
181
182
// Prepend the ulimit command
183
const bashCmd = `${ulimitCmd} && ${escapedCmd}`;
184
185
// Execute the command with ulimit
186
running_kernel = execaCommand(bashCmd, {
187
...full_spawn_options,
188
shell: true,
189
});
190
} else {
191
// CRITICAL: I am *NOT* using execa, but instead spawn, because
192
// I hit bugs in execa. Namely, when argv[0] is a path that doesn't exist,
193
// no matter what, there is an uncaught exception emitted later. The exact
194
// same situation with execaCommand or node's spawn does NOT have an uncaught
195
// exception, so it's a bug.
196
//running_kernel = execa(argv[0], argv.slice(1), full_spawn_options); // NO!
197
running_kernel = spawn(argv[0], argv.slice(1), full_spawn_options);
198
}
199
200
running_kernel.on("error", (code, signal) => {
201
logger.debug("launchKernelSpec: ERROR -- ", { argv, code, signal });
202
});
203
204
if (full_spawn_options.cleanupConnectionFile !== false) {
205
running_kernel.on("exit", (_code, _signal) => cleanup(connectionFile));
206
running_kernel.on("error", (_code, _signal) => cleanup(connectionFile));
207
}
208
return {
209
spawn: running_kernel,
210
connectionFile,
211
config,
212
kernel_spec,
213
};
214
}
215
216
// For a given kernel name and launch options: prepare the kernel file and launch the process
217
export default async function launchJupyterKernel(
218
name: string,
219
spawn_options: LaunchJupyterOpts,
220
): Promise<SpawnedKernel> {
221
const specs = await findAll();
222
const kernel_spec = specs[name];
223
if (kernel_spec == null) {
224
throw new Error(
225
`No spec available for kernel "${name}". Available specs: ${JSON.stringify(
226
Object.keys(specs),
227
)}`,
228
);
229
}
230
const { config, connectionFile } = await writeConnectionFile();
231
return await launchKernelSpec(
232
kernel_spec.spec,
233
config,
234
connectionFile,
235
spawn_options,
236
);
237
}
238
239
async function ensureDirectoryExists(path: string) {
240
try {
241
await mkdir(path, { recursive: true });
242
} catch (error) {
243
if (error.code !== "EEXIST") {
244
throw error;
245
}
246
}
247
}
248
249