@nteract/fs-kernels
Version:
A manager for the filesystem aspects of Juyter kernels
263 lines (240 loc) • 7.38 kB
text/typescript
/**
* This module contains methods that allow you to launch ("spawn") Jupyter
* kernels. You can spawn kernels either by name, by a `kernelSpec` or
* by a `kernelSpec` and its connection information.
*
* Usage example:
* ```js
* // Spawn a kernel by name
* var spawnResults = require('spawnteract').launch('python3');
*
* // Print the ip address and port for the shell channel
* console.log(spawnResults.config.ip + ':' + spawnResults.config.shell_port);
* ```
*
* You'll need to close `spawnResults.spawn` yourself as well as delete
* `spawnResults.connectionFile` from disk when finished.
*
*/
/* eslint camelcase: 0 */
// ^--- #justjupyterthings
// tslint:disable:object-literal-sort-keys
// ^--- let keys for ports be in port id order
/**
*
*/
import fs from "fs";
import path from "path";
import util from "util";
import { getPorts as _getPorts } from "portfinder";
import uuid from "uuid";
import execa, { Options } from "execa";
import mkdirp from "mkdirp";
import { Subject } from "rxjs";
import jp from "./jupyter-paths";
import { findAll, KernelResourceByName, KernelSpec } from "./kernelspecs";
import { Channels, JupyterMessage, MessageType } from "@nteract/messaging";
import {
createMainChannel,
JupyterConnectionInfo
} from "enchannel-zmq-backend";
export function cleanup(connectionFile: fs.PathLike): void {
try {
fs.unlinkSync(connectionFile);
} catch (e) {
return;
}
}
/**
* Creates a JupyterConnectionInfo object for a kernel given an array of comm
* channel ports
*
* @private
* @param ports array of comm channel ports to use for the connection,
* [hb_port, control_port, shell_port, stdin_port, iopub_port]
* @return JupyterConnectionInfo object
*/
function createConnectionInfo(ports: number[]): JupyterConnectionInfo {
return {
version: 5,
key: uuid.v4(),
signature_scheme: "hmac-sha256",
transport: "tcp",
ip: "127.0.0.1",
hb_port: ports[0],
control_port: ports[1],
shell_port: ports[2],
stdin_port: ports[3],
iopub_port: ports[4]
};
}
export interface PortFinderOptions {
host: string;
port: number;
}
/**
* Write a connection file
* @public
* @param [portFinderOptions] connection options
* see {@link https://github.com/indexzero/node-portfinder/blob/master/lib/portfinder.js }
* @param [portFinderOptions.port]
* @param [portFinderOptions.host]
* @return configResults
* @return configResults.config connection info
* @return configResults.connectionFile path to the connection file
*/
async function writeConnectionFile(
portFinderOptions: PortFinderOptions = {
host: "127.0.0.1",
port: 9000
}
): Promise<{
config: JupyterConnectionInfo;
connectionFile: string;
}> {
const writeFile = util.promisify(fs.writeFile);
const getPorts = util.promisify(_getPorts);
try {
const ports: number[] = await getPorts(5, portFinderOptions);
const runtimeDir: string = await jp.runtimeDir();
mkdirp(runtimeDir, error => {
if (error) {
throw error;
}
});
// Write the kernel connection file.
const config: JupyterConnectionInfo = createConnectionInfo(ports);
const connectionFile: string = path.join(
await jp.runtimeDir(),
`kernel-${uuid.v4()}.json`
);
await writeFile(connectionFile, config);
return {
config,
connectionFile
};
} catch (error) {
console.log(error);
throw error;
}
}
/**
* Launch a kernel for a given kernelSpec
* @public
* @param kernelSpec describes a specific kernel
* @param [spawnOptions] `child_process`-like {@link https://github.com/sindresorhus/execa#options options for execa}
* use `{ cleanupConnectionFile: false }` to disable automatic connection file cleanup
*/
export async function launchSpec(
kernelSpec: KernelSpec,
spawnOptions?: Options
): Promise<LaunchedKernel> {
const info: {
config: JupyterConnectionInfo;
connectionFile: string;
} = await writeConnectionFile();
return launchSpecFromConnectionInfo(
kernelSpec,
info.config,
info.connectionFile,
spawnOptions
);
}
export interface LaunchedKernel {
spawn: execa.ExecaChildProcess;
connectionFile: string;
config: JupyterConnectionInfo;
kernelSpec: KernelSpec;
channels: Channels;
}
/**
* Launch a kernel for a given kernelSpec and connection info
* @public
* @param kernelSpec describes a specific kernel, see the npm package
* `kernelspecs`
* @param config connection config
* @param connectionFile path to the config file
* @param [spawnOptions] `child_process`-like options for
* [execa]{@link https://github.com/sindresorhus/execa#options}
* use `{ cleanupConnectionFile: false }` to disable
* automatic connection file cleanup
*
* @return spawnResults
* @return spawnResults.spawn spawned process
* @return spawnResults.connectionFile connection file path
* @return spawnResults.config connection info
*
*/
export async function launchSpecFromConnectionInfo(
kernelSpec: KernelSpec,
config: JupyterConnectionInfo,
connectionFile: string,
spawnOptions?: Options
): Promise<LaunchedKernel> {
const argv: string[] = kernelSpec.argv.map((x: string) =>
x === "{connection_file}" ? connectionFile : x
);
const defaultSpawnOptions: {
cleanupConnectionFile: boolean;
stdio: string;
} = {
cleanupConnectionFile: true,
stdio: "ignore"
};
const env: NodeJS.ProcessEnv & {
[varialbe: string]: string;
} = Object.assign({}, process.env, kernelSpec.env);
const fullSpawnOptions = Object.assign(
{},
defaultSpawnOptions,
// TODO: see if this interferes with what execa assigns to the env option
{ env },
spawnOptions
);
const runningKernel: execa.ExecaChildProcess = execa(
argv[0],
argv.slice(1),
fullSpawnOptions
);
if (fullSpawnOptions.cleanupConnectionFile !== false) {
runningKernel.on("exit", (code, signal) => cleanup(connectionFile));
runningKernel.on("error", error => cleanup(connectionFile));
}
const channels: Subject<
JupyterMessage<MessageType, any>
> = await createMainChannel(config);
return {
channels,
config,
connectionFile,
kernelSpec,
spawn: runningKernel
};
}
/**
* Launch a kernel by name
* @public
* @param kernelName
* @param [specs] array of kernelSpec objects to look through.
* See the npm package `kernelspecs`
* @param [spawnOptions] `child_process`-like options for
* [execa]{@link https://github.com/sindresorhus/execa#options}
* use `{ cleanupConnectionFile: false }` to disable
* automatic connection file cleanup
*/
export async function launch(
kernelName: string,
spawnOptions?: Options,
specs?: KernelResourceByName
): Promise<LaunchedKernel> {
// Let them pass in a cached specs file
if (!specs) {
const sp: KernelResourceByName = await findAll();
return launch(kernelName, spawnOptions, sp);
}
if (!specs[kernelName]) {
return Promise.reject(new Error(`No spec available for ${kernelName}`));
}
const spec: KernelSpec = specs[kernelName].spec;
return launchSpec(spec, spawnOptions);
}