@jupyterlab/services
Version:
Client APIs for the Jupyter services REST APIs
1,643 lines (1,501 loc) • 54.2 kB
text/typescript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { URLExt } from '@jupyterlab/coreutils';
import { JSONExt, JSONObject, PromiseDelegate, UUID } from '@lumino/coreutils';
import { ISignal, Signal } from '@lumino/signaling';
import { ServerConnection } from '..';
import { CommHandler } from './comm';
import * as Kernel from './kernel';
import * as KernelMessage from './messages';
import {
KernelControlFutureHandler,
KernelFutureHandler,
KernelShellFutureHandler
} from './future';
import * as validate from './validate';
import { KernelSpec, KernelSpecAPI } from '../kernelspec';
import * as restapi from './restapi';
// Stub for requirejs.
declare let requirejs: any;
const KERNEL_INFO_TIMEOUT = 3000;
const RESTARTING_KERNEL_SESSION = '_RESTARTING_';
const STARTING_KERNEL_SESSION = '';
/**
* Implementation of the Kernel object.
*
* #### Notes
* Messages from the server are handled in the order they were received and
* asynchronously. Any message handler can return a promise, and message
* handling will pause until the promise is fulfilled.
*/
export class KernelConnection implements Kernel.IKernelConnection {
/**
* Construct a kernel object.
*/
constructor(options: Kernel.IKernelConnection.IOptions) {
this._name = options.model.name;
this._id = options.model.id;
this.serverSettings =
options.serverSettings ?? ServerConnection.makeSettings();
this._clientId = options.clientId ?? UUID.uuid4();
this._username = options.username ?? '';
this.handleComms = options.handleComms ?? true;
this._createSocket();
}
get disposed(): ISignal<this, void> {
return this._disposed;
}
/**
* The server settings for the kernel.
*/
readonly serverSettings: ServerConnection.ISettings;
/**
* Handle comm messages
*
* #### Notes
* The comm message protocol currently has implicit assumptions that only
* one kernel connection is handling comm messages. This option allows a
* kernel connection to opt out of handling comms.
*
* See https://github.com/jupyter/jupyter_client/issues/263
*/
readonly handleComms: boolean;
/**
* A signal emitted when the kernel status changes.
*/
get statusChanged(): ISignal<this, KernelMessage.Status> {
return this._statusChanged;
}
/**
* A signal emitted when the kernel status changes.
*/
get connectionStatusChanged(): ISignal<this, Kernel.ConnectionStatus> {
return this._connectionStatusChanged;
}
/**
* A signal emitted for iopub kernel messages.
*
* #### Notes
* This signal is emitted after the iopub message is handled asynchronously.
*/
get iopubMessage(): ISignal<this, KernelMessage.IIOPubMessage> {
return this._iopubMessage;
}
/**
* A signal emitted for unhandled kernel message.
*
* #### Notes
* This signal is emitted for a message that was not handled. It is emitted
* during the asynchronous message handling code.
*/
get unhandledMessage(): ISignal<this, KernelMessage.IMessage> {
return this._unhandledMessage;
}
/**
* The kernel model
*/
get model(): Kernel.IModel {
return (
this._model || {
id: this.id,
name: this.name,
reason: this._reason
}
);
}
/**
* A signal emitted for any kernel message.
*
* #### Notes
* This signal is emitted when a message is received, before it is handled
* asynchronously.
*
* This message is emitted when a message is queued for sending (either in
* the websocket buffer, or our own pending message buffer). The message may
* actually be sent across the wire at a later time.
*
* The message emitted in this signal should not be modified in any way.
*/
get anyMessage(): ISignal<this, Kernel.IAnyMessageArgs> {
return this._anyMessage;
}
/**
* A signal emitted when a kernel has pending inputs from the user.
*/
get pendingInput(): ISignal<this, boolean> {
return this._pendingInput;
}
/**
* The id of the server-side kernel.
*/
get id(): string {
return this._id;
}
/**
* The name of the server-side kernel.
*/
get name(): string {
return this._name;
}
/**
* The client username.
*/
get username(): string {
return this._username;
}
/**
* The client unique id.
*/
get clientId(): string {
return this._clientId;
}
/**
* The current status of the kernel.
*/
get status(): KernelMessage.Status {
return this._status;
}
/**
* The current connection status of the kernel connection.
*/
get connectionStatus(): Kernel.ConnectionStatus {
return this._connectionStatus;
}
/**
* Test whether the kernel has been disposed.
*/
get isDisposed(): boolean {
return this._isDisposed;
}
/**
* The cached kernel info.
*
* @returns A promise that resolves to the kernel info.
*/
get info(): Promise<KernelMessage.IInfoReply> {
return this._info.promise;
}
/**
* The kernel spec.
*
* @returns A promise that resolves to the kernel spec.
*/
get spec(): Promise<KernelSpec.ISpecModel | undefined> {
if (this._specPromise) {
return this._specPromise;
}
this._specPromise = KernelSpecAPI.getSpecs(this.serverSettings).then(
specs => {
return specs.kernelspecs[this._name];
}
);
return this._specPromise;
}
/**
* Clone the current kernel with a new clientId.
*/
clone(
options: Pick<
Kernel.IKernelConnection.IOptions,
'clientId' | 'username' | 'handleComms'
> = {}
): Kernel.IKernelConnection {
return new KernelConnection({
model: this.model,
username: this.username,
serverSettings: this.serverSettings,
// handleComms defaults to false since that is safer
handleComms: false,
...options
});
}
/**
* Dispose of the resources held by the kernel.
*/
dispose(): void {
if (this.isDisposed) {
return;
}
this._isDisposed = true;
this._disposed.emit();
this._updateConnectionStatus('disconnected');
this._clearKernelState();
this._pendingMessages = [];
this._clearSocket();
// Clear Lumino signals
Signal.clearData(this);
}
/**
* Send a shell message to the kernel.
*
* #### Notes
* Send a message to the kernel's shell channel, yielding a future object
* for accepting replies.
*
* If `expectReply` is given and `true`, the future is disposed when both a
* shell reply and an idle status message are received. If `expectReply`
* is not given or is `false`, the future is resolved when an idle status
* message is received.
* If `disposeOnDone` is not given or is `true`, the Future is disposed at this point.
* If `disposeOnDone` is given and `false`, it is up to the caller to dispose of the Future.
*
* All replies are validated as valid kernel messages.
*
* If the kernel status is `dead`, this will throw an error.
*/
sendShellMessage<T extends KernelMessage.ShellMessageType>(
msg: KernelMessage.IShellMessage<T>,
expectReply = false,
disposeOnDone = true
): Kernel.IShellFuture<KernelMessage.IShellMessage<T>> {
return this._sendKernelShellControl(
KernelShellFutureHandler,
msg,
expectReply,
disposeOnDone
) as Kernel.IShellFuture<KernelMessage.IShellMessage<T>>;
}
/**
* Send a control message to the kernel.
*
* #### Notes
* Send a message to the kernel's control channel, yielding a future object
* for accepting replies.
*
* If `expectReply` is given and `true`, the future is disposed when both a
* control reply and an idle status message are received. If `expectReply`
* is not given or is `false`, the future is resolved when an idle status
* message is received.
* If `disposeOnDone` is not given or is `true`, the Future is disposed at this point.
* If `disposeOnDone` is given and `false`, it is up to the caller to dispose of the Future.
*
* All replies are validated as valid kernel messages.
*
* If the kernel status is `dead`, this will throw an error.
*/
sendControlMessage<T extends KernelMessage.ControlMessageType>(
msg: KernelMessage.IControlMessage<T>,
expectReply = false,
disposeOnDone = true
): Kernel.IControlFuture<KernelMessage.IControlMessage<T>> {
return this._sendKernelShellControl(
KernelControlFutureHandler,
msg,
expectReply,
disposeOnDone
) as Kernel.IControlFuture<KernelMessage.IControlMessage<T>>;
}
private _sendKernelShellControl<
REQUEST extends KernelMessage.IShellControlMessage,
REPLY extends KernelMessage.IShellControlMessage,
KFH extends new (...params: any[]) => KernelFutureHandler<REQUEST, REPLY>,
T extends KernelMessage.IMessage
>(
ctor: KFH,
msg: T,
expectReply = false,
disposeOnDone = true
): Kernel.IFuture<
KernelMessage.IShellControlMessage,
KernelMessage.IShellControlMessage
> {
this._sendMessage(msg);
this._anyMessage.emit({ msg, direction: 'send' });
const future = new ctor(
() => {
const msgId = msg.header.msg_id;
this._futures.delete(msgId);
// Remove stored display id information.
const displayIds = this._msgIdToDisplayIds.get(msgId);
if (!displayIds) {
return;
}
displayIds.forEach(displayId => {
const msgIds = this._displayIdToParentIds.get(displayId);
if (msgIds) {
const idx = msgIds.indexOf(msgId);
if (idx === -1) {
return;
}
if (msgIds.length === 1) {
this._displayIdToParentIds.delete(displayId);
} else {
msgIds.splice(idx, 1);
this._displayIdToParentIds.set(displayId, msgIds);
}
}
});
this._msgIdToDisplayIds.delete(msgId);
},
msg,
expectReply,
disposeOnDone,
this
);
this._futures.set(msg.header.msg_id, future);
return future;
}
/**
* Send a message on the websocket.
*
* If queue is true, queue the message for later sending if we cannot send
* now. Otherwise throw an error.
*
* #### Notes
* As an exception to the queueing, if we are sending a kernel_info_request
* message while we think the kernel is restarting, we send the message
* immediately without queueing. This is so that we can trigger a message
* back, which will then clear the kernel restarting state.
*/
private _sendMessage(msg: KernelMessage.IMessage, queue = true) {
if (this.status === 'dead') {
throw new Error('Kernel is dead');
}
// If we have a kernel_info_request and we are starting or restarting, send the
// kernel_info_request immediately if we can, and if not throw an error so
// we can retry later. On restarting we do this because we must get at least one message
// from the kernel to reset the kernel session (thus clearing the restart
// status sentinel).
if (
(this._kernelSession === STARTING_KERNEL_SESSION ||
this._kernelSession === RESTARTING_KERNEL_SESSION) &&
KernelMessage.isInfoRequestMsg(msg)
) {
if (this.connectionStatus === 'connected') {
this._ws!.send(
this.serverSettings.serializer.serialize(msg, this._ws!.protocol)
);
return;
} else {
throw new Error('Could not send message: status is not connected');
}
}
// If there are pending messages, add to the queue so we keep messages in order
if (queue && this._pendingMessages.length > 0) {
this._pendingMessages.push(msg);
return;
}
// Send if the ws allows it, otherwise queue the message.
if (
this.connectionStatus === 'connected' &&
this._kernelSession !== RESTARTING_KERNEL_SESSION
) {
this._ws!.send(
this.serverSettings.serializer.serialize(msg, this._ws!.protocol)
);
} else if (queue) {
this._pendingMessages.push(msg);
} else {
throw new Error('Could not send message');
}
}
/**
* Interrupt a kernel.
*
* #### Notes
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/kernels).
*
* The promise is fulfilled on a valid response and rejected otherwise.
*
* It is assumed that the API call does not mutate the kernel id or name.
*
* The promise will be rejected if the kernel status is `Dead` or if the
* request fails or the response is invalid.
*/
async interrupt(): Promise<void> {
this.hasPendingInput = false;
if (this.status === 'dead') {
throw new Error('Kernel is dead');
}
return restapi.interruptKernel(this.id, this.serverSettings);
}
/**
* Request a kernel restart.
*
* #### Notes
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/kernels)
* and validates the response model.
*
* Any existing Future or Comm objects are cleared once the kernel has
* actually be restarted.
*
* The promise is fulfilled on a valid server response (after the kernel restarts)
* and rejected otherwise.
*
* It is assumed that the API call does not mutate the kernel id or name.
*
* The promise will be rejected if the request fails or the response is
* invalid.
*/
async restart(): Promise<void> {
if (this.status === 'dead') {
throw new Error('Kernel is dead');
}
this._updateStatus('restarting');
this._clearKernelState();
this._kernelSession = RESTARTING_KERNEL_SESSION;
await restapi.restartKernel(this.id, this.serverSettings);
// Reconnect to the kernel to address cases where kernel ports
// have changed during the restart.
await this.reconnect();
this.hasPendingInput = false;
}
/**
* Reconnect to a kernel.
*
* #### Notes
* This may try multiple times to reconnect to a kernel, and will sever any
* existing connection.
*/
reconnect(): Promise<void> {
this._errorIfDisposed();
const result = new PromiseDelegate<void>();
// Set up a listener for the connection status changing, which accepts or
// rejects after the retries are done.
const fulfill = (sender: this, status: Kernel.ConnectionStatus) => {
if (status === 'connected') {
result.resolve();
this.connectionStatusChanged.disconnect(fulfill, this);
} else if (status === 'disconnected') {
result.reject(new Error('Kernel connection disconnected'));
this.connectionStatusChanged.disconnect(fulfill, this);
}
};
this.connectionStatusChanged.connect(fulfill, this);
// Reset the reconnect limit so we start the connection attempts fresh
this._reconnectAttempt = 0;
// Start the reconnection process, which will also clear any existing
// connection.
this._reconnect();
// Return the promise that should resolve on connection or reject if the
// retries don't work.
return result.promise;
}
/**
* Shutdown a kernel.
*
* #### Notes
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/kernels).
*
* The promise is fulfilled on a valid response and rejected otherwise.
*
* On a valid response, disposes this kernel connection.
*
* If the kernel is already `dead`, disposes this kernel connection without
* a server request.
*/
async shutdown(): Promise<void> {
if (this.status !== 'dead') {
await restapi.shutdownKernel(this.id, this.serverSettings);
}
this.handleShutdown();
}
/**
* Handles a kernel shutdown.
*
* #### Notes
* This method should be called if we know from outside information that a
* kernel is dead (for example, we cannot find the kernel model on the
* server).
*/
handleShutdown(): void {
this._updateStatus('dead');
this.dispose();
}
/**
* Send a `kernel_info_request` message.
*
* #### Notes
* See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#kernel-info).
*
* Fulfills with the `kernel_info_response` content when the shell reply is
* received and validated.
*/
async requestKernelInfo(): Promise<KernelMessage.IInfoReplyMsg | undefined> {
const msg = KernelMessage.createMessage({
msgType: 'kernel_info_request',
channel: 'shell',
username: this._username,
session: this._clientId,
content: {}
});
let reply: KernelMessage.IInfoReplyMsg | undefined;
try {
reply = (await Private.handleShellMessage(this, msg)) as
| KernelMessage.IInfoReplyMsg
| undefined;
} catch (e) {
// If we rejected because the future was disposed, ignore and return.
if (this.isDisposed) {
return;
} else {
throw e;
}
}
this._errorIfDisposed();
if (!reply) {
return;
}
// Kernels sometimes do not include a status field on kernel_info_reply
// messages, so set a default for now.
// See https://github.com/jupyterlab/jupyterlab/issues/6760
if (reply.content.status === undefined) {
(reply.content as any).status = 'ok';
}
if (reply.content.status !== 'ok') {
this._info.reject('Kernel info reply errored');
return reply;
}
this._info.resolve(reply.content);
this._kernelSession = reply.header.session;
return reply;
}
/**
* Send a `complete_request` message.
*
* #### Notes
* See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#completion).
*
* Fulfills with the `complete_reply` content when the shell reply is
* received and validated.
*/
requestComplete(
content: KernelMessage.ICompleteRequestMsg['content']
): Promise<KernelMessage.ICompleteReplyMsg> {
const msg = KernelMessage.createMessage({
msgType: 'complete_request',
channel: 'shell',
username: this._username,
session: this._clientId,
content
});
return Private.handleShellMessage(
this,
msg
) as Promise<KernelMessage.ICompleteReplyMsg>;
}
/**
* Send an `inspect_request` message.
*
* #### Notes
* See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#introspection).
*
* Fulfills with the `inspect_reply` content when the shell reply is
* received and validated.
*/
requestInspect(
content: KernelMessage.IInspectRequestMsg['content']
): Promise<KernelMessage.IInspectReplyMsg> {
const msg = KernelMessage.createMessage({
msgType: 'inspect_request',
channel: 'shell',
username: this._username,
session: this._clientId,
content: content
});
return Private.handleShellMessage(
this,
msg
) as Promise<KernelMessage.IInspectReplyMsg>;
}
/**
* Send a `history_request` message.
*
* #### Notes
* See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#history).
*
* Fulfills with the `history_reply` content when the shell reply is
* received and validated.
*/
requestHistory(
content: KernelMessage.IHistoryRequestMsg['content']
): Promise<KernelMessage.IHistoryReplyMsg> {
const msg = KernelMessage.createMessage({
msgType: 'history_request',
channel: 'shell',
username: this._username,
session: this._clientId,
content
});
return Private.handleShellMessage(
this,
msg
) as Promise<KernelMessage.IHistoryReplyMsg>;
}
/**
* Send an `execute_request` message.
*
* #### Notes
* See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#execute).
*
* Future `onReply` is called with the `execute_reply` content when the
* shell reply is received and validated. The future will resolve when
* this message is received and the `idle` iopub status is received.
* The future will also be disposed at this point unless `disposeOnDone`
* is specified and `false`, in which case it is up to the caller to dispose
* of the future.
*
* **See also:** [[IExecuteReply]]
*/
requestExecute(
content: KernelMessage.IExecuteRequestMsg['content'],
disposeOnDone: boolean = true,
metadata?: JSONObject
): Kernel.IShellFuture<
KernelMessage.IExecuteRequestMsg,
KernelMessage.IExecuteReplyMsg
> {
const defaults: JSONObject = {
silent: false,
store_history: true,
user_expressions: {},
allow_stdin: true,
stop_on_error: false
};
const msg = KernelMessage.createMessage({
msgType: 'execute_request',
channel: 'shell',
username: this._username,
session: this._clientId,
content: { ...defaults, ...content },
metadata
});
return this.sendShellMessage(
msg,
true,
disposeOnDone
) as Kernel.IShellFuture<
KernelMessage.IExecuteRequestMsg,
KernelMessage.IExecuteReplyMsg
>;
}
/**
* Send an experimental `debug_request` message.
*
* @hidden
*
* #### Notes
* Debug messages are experimental messages that are not in the official
* kernel message specification. As such, this function is *NOT* considered
* part of the public API, and may change without notice.
*/
requestDebug(
content: KernelMessage.IDebugRequestMsg['content'],
disposeOnDone: boolean = true
): Kernel.IControlFuture<
KernelMessage.IDebugRequestMsg,
KernelMessage.IDebugReplyMsg
> {
const msg = KernelMessage.createMessage({
msgType: 'debug_request',
channel: 'control',
username: this._username,
session: this._clientId,
content
});
return this.sendControlMessage(
msg,
true,
disposeOnDone
) as Kernel.IControlFuture<
KernelMessage.IDebugRequestMsg,
KernelMessage.IDebugReplyMsg
>;
}
/**
* Send an `is_complete_request` message.
*
* #### Notes
* See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#code-completeness).
*
* Fulfills with the `is_complete_response` content when the shell reply is
* received and validated.
*/
requestIsComplete(
content: KernelMessage.IIsCompleteRequestMsg['content']
): Promise<KernelMessage.IIsCompleteReplyMsg> {
const msg = KernelMessage.createMessage({
msgType: 'is_complete_request',
channel: 'shell',
username: this._username,
session: this._clientId,
content
});
return Private.handleShellMessage(
this,
msg
) as Promise<KernelMessage.IIsCompleteReplyMsg>;
}
/**
* Send a `comm_info_request` message.
*
* #### Notes
* Fulfills with the `comm_info_reply` content when the shell reply is
* received and validated.
*/
requestCommInfo(
content: KernelMessage.ICommInfoRequestMsg['content']
): Promise<KernelMessage.ICommInfoReplyMsg> {
const msg = KernelMessage.createMessage({
msgType: 'comm_info_request',
channel: 'shell',
username: this._username,
session: this._clientId,
content
});
return Private.handleShellMessage(
this,
msg
) as Promise<KernelMessage.ICommInfoReplyMsg>;
}
/**
* Send an `input_reply` message.
*
* #### Notes
* See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#messages-on-the-stdin-router-dealer-sockets).
*/
sendInputReply(
content: KernelMessage.IInputReplyMsg['content'],
parent_header: KernelMessage.IInputReplyMsg['parent_header']
): void {
const msg = KernelMessage.createMessage({
msgType: 'input_reply',
channel: 'stdin',
username: this._username,
session: this._clientId,
content
});
msg.parent_header = parent_header;
this._sendMessage(msg);
this._anyMessage.emit({ msg, direction: 'send' });
this.hasPendingInput = false;
}
/**
* Create a new comm.
*
* #### Notes
* If a client-side comm already exists with the given commId, an error is thrown.
* If the kernel does not handle comms, an error is thrown.
*/
createComm(targetName: string, commId: string = UUID.uuid4()): Kernel.IComm {
if (!this.handleComms) {
throw new Error('Comms are disabled on this kernel connection');
}
if (this._comms.has(commId)) {
throw new Error('Comm is already created');
}
const comm = new CommHandler(targetName, commId, this, () => {
this._unregisterComm(commId);
});
this._comms.set(commId, comm);
return comm;
}
/**
* Check if a comm exists.
*/
hasComm(commId: string): boolean {
return this._comms.has(commId);
}
/**
* Register a comm target handler.
*
* @param targetName - The name of the comm target.
*
* @param callback - The callback invoked for a comm open message.
*
* @returns A disposable used to unregister the comm target.
*
* #### Notes
* Only one comm target can be registered to a target name at a time, an
* existing callback for the same target name will be overridden. A registered
* comm target handler will take precedence over a comm which specifies a
* `target_module`.
*
* If the callback returns a promise, kernel message processing will pause
* until the returned promise is fulfilled.
*/
registerCommTarget(
targetName: string,
callback: (
comm: Kernel.IComm,
msg: KernelMessage.ICommOpenMsg
) => void | PromiseLike<void>
): void {
if (!this.handleComms) {
return;
}
this._targetRegistry[targetName] = callback;
}
/**
* Remove a comm target handler.
*
* @param targetName - The name of the comm target to remove.
*
* @param callback - The callback to remove.
*
* #### Notes
* The comm target is only removed if the callback argument matches.
*/
removeCommTarget(
targetName: string,
callback: (
comm: Kernel.IComm,
msg: KernelMessage.ICommOpenMsg
) => void | PromiseLike<void>
): void {
if (!this.handleComms) {
return;
}
if (!this.isDisposed && this._targetRegistry[targetName] === callback) {
delete this._targetRegistry[targetName];
}
}
/**
* Register an IOPub message hook.
*
* @param msg_id - The parent_header message id the hook will intercept.
*
* @param hook - The callback invoked for the message.
*
* #### Notes
* The IOPub hook system allows you to preempt the handlers for IOPub
* messages that are responses to a given message id.
*
* The most recently registered hook is run first. A hook can return a
* boolean or a promise to a boolean, in which case all kernel message
* processing pauses until the promise is fulfilled. If a hook return value
* resolves to false, any later hooks will not run and the function will
* return a promise resolving to false. If a hook throws an error, the error
* is logged to the console and the next hook is run. If a hook is
* registered during the hook processing, it will not run until the next
* message. If a hook is removed during the hook processing, it will be
* deactivated immediately.
*
* See also [[IFuture.registerMessageHook]].
*/
registerMessageHook(
msgId: string,
hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>
): void {
const future = this._futures?.get(msgId);
if (future) {
future.registerMessageHook(hook);
}
}
/**
* Remove an IOPub message hook.
*
* @param msg_id - The parent_header message id the hook intercepted.
*
* @param hook - The callback invoked for the message.
*
*/
removeMessageHook(
msgId: string,
hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>
): void {
const future = this._futures?.get(msgId);
if (future) {
future.removeMessageHook(hook);
}
}
/**
* Remove the input guard, if any.
*/
removeInputGuard() {
this.hasPendingInput = false;
}
/**
* Handle a message with a display id.
*
* @returns Whether the message was handled.
*/
private async _handleDisplayId(
displayId: string,
msg: KernelMessage.IMessage
): Promise<boolean> {
const msgId = (msg.parent_header as KernelMessage.IHeader).msg_id;
let parentIds = this._displayIdToParentIds.get(displayId);
if (parentIds) {
// We've seen it before, update existing outputs with same display_id
// by handling display_data as update_display_data.
const updateMsg: KernelMessage.IMessage = {
header: JSONExt.deepCopy(
msg.header as unknown as JSONObject
) as unknown as KernelMessage.IHeader,
parent_header: JSONExt.deepCopy(
msg.parent_header as unknown as JSONObject
) as unknown as KernelMessage.IHeader,
metadata: JSONExt.deepCopy(msg.metadata),
content: JSONExt.deepCopy(msg.content as JSONObject),
channel: msg.channel,
buffers: msg.buffers ? msg.buffers.slice() : []
};
(updateMsg.header as any).msg_type = 'update_display_data';
await Promise.all(
parentIds.map(async parentId => {
const future = this._futures && this._futures.get(parentId);
if (future) {
await future.handleMsg(updateMsg);
}
})
);
}
// We're done here if it's update_display.
if (msg.header.msg_type === 'update_display_data') {
// It's an update, don't proceed to the normal display.
return true;
}
// Regular display_data with id, record it for future updating
// in _displayIdToParentIds for future lookup.
parentIds = this._displayIdToParentIds.get(displayId) ?? [];
if (parentIds.indexOf(msgId) === -1) {
parentIds.push(msgId);
}
this._displayIdToParentIds.set(displayId, parentIds);
// Add to our map of display ids for this message.
const displayIds = this._msgIdToDisplayIds.get(msgId) ?? [];
if (displayIds.indexOf(msgId) === -1) {
displayIds.push(msgId);
}
this._msgIdToDisplayIds.set(msgId, displayIds);
// Let the message propagate to the intended recipient.
return false;
}
/**
* Forcefully clear the socket state.
*
* #### Notes
* This will clear all socket state without calling any handlers and will
* not update the connection status. If you call this method, you are
* responsible for updating the connection status as needed and recreating
* the socket if you plan to reconnect.
*/
private _clearSocket(): void {
if (this._ws !== null) {
// Clear the websocket event handlers and the socket itself.
this._ws.onopen = this._noOp;
this._ws.onclose = this._noOp;
this._ws.onerror = this._noOp;
this._ws.onmessage = this._noOp;
this._ws.close();
this._ws = null;
}
}
/**
* Handle status iopub messages from the kernel.
*/
private _updateStatus(status: KernelMessage.Status): void {
if (this._status === status || this._status === 'dead') {
return;
}
this._status = status;
Private.logKernelStatus(this);
this._statusChanged.emit(status);
if (status === 'dead') {
this.dispose();
}
}
/**
* Send pending messages to the kernel.
*/
private _sendPending(): void {
// We check to make sure we are still connected each time. For
// example, if a websocket buffer overflows, it may close, so we should
// stop sending messages.
while (
this.connectionStatus === 'connected' &&
this._kernelSession !== RESTARTING_KERNEL_SESSION &&
this._pendingMessages.length > 0
) {
this._sendMessage(this._pendingMessages[0], false);
// We shift the message off the queue after the message is sent so that
// if there is an exception, the message is still pending.
this._pendingMessages.shift();
}
}
/**
* Clear the internal state.
*/
private _clearKernelState(): void {
this._kernelSession = '';
this._pendingMessages = [];
this._futures.forEach(future => {
future.dispose();
});
this._comms.forEach(comm => {
comm.dispose();
});
this._msgChain = Promise.resolve();
this._futures = new Map<
string,
KernelFutureHandler<
KernelMessage.IShellControlMessage,
KernelMessage.IShellControlMessage
>
>();
this._comms = new Map<string, Kernel.IComm>();
this._displayIdToParentIds.clear();
this._msgIdToDisplayIds.clear();
}
/**
* Check to make sure it is okay to proceed to handle a message.
*
* #### Notes
* Because we handle messages asynchronously, before a message is handled the
* kernel might be disposed or restarted (and have a different session id).
* This function throws an error in each of these cases. This is meant to be
* called at the start of an asynchronous message handler to cancel message
* processing if the message no longer is valid.
*/
private _assertCurrentMessage(msg: KernelMessage.IMessage) {
this._errorIfDisposed();
if (msg.header.session !== this._kernelSession) {
throw new Error(
`Canceling handling of old message: ${msg.header.msg_type}`
);
}
}
/**
* Handle a `comm_open` kernel message.
*/
private async _handleCommOpen(
msg: KernelMessage.ICommOpenMsg
): Promise<void> {
this._assertCurrentMessage(msg);
const content = msg.content;
const comm = new CommHandler(
content.target_name,
content.comm_id,
this,
() => {
this._unregisterComm(content.comm_id);
}
);
this._comms.set(content.comm_id, comm);
try {
const target = await Private.loadObject(
content.target_name,
content.target_module,
this._targetRegistry
);
await target(comm, msg);
} catch (e) {
// Close the comm asynchronously. We cannot block message processing on
// kernel messages to wait for another kernel message.
comm.close();
console.error('Exception opening new comm');
throw e;
}
}
/**
* Handle 'comm_close' kernel message.
*/
private async _handleCommClose(
msg: KernelMessage.ICommCloseMsg
): Promise<void> {
this._assertCurrentMessage(msg);
const content = msg.content;
const comm = this._comms.get(content.comm_id);
if (!comm) {
console.error('Comm not found for comm id ' + content.comm_id);
return;
}
this._unregisterComm(comm.commId);
const onClose = comm.onClose;
if (onClose) {
// tslint:disable-next-line:await-promise
await onClose(msg);
}
(comm as CommHandler).dispose();
}
/**
* Handle a 'comm_msg' kernel message.
*/
private async _handleCommMsg(msg: KernelMessage.ICommMsgMsg): Promise<void> {
this._assertCurrentMessage(msg);
const content = msg.content;
const comm = this._comms.get(content.comm_id);
if (!comm) {
return;
}
const onMsg = comm.onMsg;
if (onMsg) {
// tslint:disable-next-line:await-promise
await onMsg(msg);
}
}
/**
* Unregister a comm instance.
*/
private _unregisterComm(commId: string) {
this._comms.delete(commId);
}
/**
* Create the kernel websocket connection and add socket status handlers.
*/
private _createSocket = (useProtocols = true) => {
this._errorIfDisposed();
// Make sure the socket is clear
this._clearSocket();
// Update the connection status to reflect opening a new connection.
this._updateConnectionStatus('connecting');
const settings = this.serverSettings;
const partialUrl = URLExt.join(
settings.wsUrl,
restapi.KERNEL_SERVICE_URL,
encodeURIComponent(this._id)
);
// Strip any authentication from the display string.
const display = partialUrl.replace(/^((?:\w+:)?\/\/)(?:[^@\/]+@)/, '$1');
console.debug(`Starting WebSocket: ${display}`);
let url = URLExt.join(
partialUrl,
'channels?session_id=' + encodeURIComponent(this._clientId)
);
// If token authentication is in use.
const token = settings.token;
if (settings.appendToken && token !== '') {
url = url + `&token=${encodeURIComponent(token)}`;
}
// Try opening the websocket with our list of subprotocols.
// If the server doesn't handle subprotocols,
// the accepted protocol will be ''.
// But we cannot send '' as a subprotocol, so if connection fails,
// reconnect without subprotocols.
const supportedProtocols = useProtocols ? this._supportedProtocols : [];
this._ws = new settings.WebSocket(url, supportedProtocols);
// Ensure incoming binary messages are not Blobs
this._ws.binaryType = 'arraybuffer';
let alreadyCalledOnclose = false;
const getKernelModel = async (evt: Event) => {
if (this._isDisposed) {
return;
}
this._reason = '';
this._model = undefined;
try {
const model = await restapi.getKernelModel(this._id, settings);
this._model = model;
if (model?.execution_state === 'dead') {
this._updateStatus('dead');
} else {
this._onWSClose(evt);
}
} catch (err) {
// Try again, if there is a network failure
// Handle network errors, as well as cases where we are on a
// JupyterHub and the server is not running. JupyterHub returns a
// 503 (<2.0) or 424 (>2.0) in that case.
if (
err instanceof ServerConnection.NetworkError ||
err.response?.status === 503 ||
err.response?.status === 424
) {
const timeout = Private.getRandomIntInclusive(10, 30) * 1e3;
setTimeout(getKernelModel, timeout, evt);
} else {
this._reason = 'Kernel died unexpectedly';
this._updateStatus('dead');
}
}
return;
};
const earlyClose = async (evt: Event) => {
// If the websocket was closed early, that could mean
// that the kernel is actually dead. Try getting
// information about the kernel from the API call,
// if that fails, then assume the kernel is dead,
// otherwise just follow the typical websocket closed
// protocol.
if (alreadyCalledOnclose) {
return;
}
alreadyCalledOnclose = true;
await getKernelModel(evt);
return;
};
this._ws.onmessage = this._onWSMessage;
this._ws.onopen = this._onWSOpen;
this._ws.onclose = earlyClose;
this._ws.onerror = earlyClose;
};
/**
* Handle connection status changes.
*/
private _updateConnectionStatus(
connectionStatus: Kernel.ConnectionStatus
): void {
if (this._connectionStatus === connectionStatus) {
return;
}
this._connectionStatus = connectionStatus;
// If we are not 'connecting', reset any reconnection attempts.
if (connectionStatus !== 'connecting') {
this._reconnectAttempt = 0;
clearTimeout(this._reconnectTimeout);
}
if (this.status !== 'dead') {
if (connectionStatus === 'connected') {
let restarting = this._kernelSession === RESTARTING_KERNEL_SESSION;
// Send a kernel info request to make sure we send at least one
// message to get kernel status back. Always request kernel info
// first, to get kernel status back and ensure iopub is fully
// established. If we are restarting, this message will skip the queue
// and be sent immediately.
let p = this.requestKernelInfo();
// Send any pending messages after the kernelInfo resolves, or after a
// timeout as a failsafe.
let sendPendingCalled = false;
let sendPendingOnce = () => {
if (sendPendingCalled) {
return;
}
sendPendingCalled = true;
if (restarting && this._kernelSession === RESTARTING_KERNEL_SESSION) {
// We were restarting and a message didn't arrive to set the
// session, but we just assume the restart succeeded and send any
// pending messages.
// FIXME: it would be better to retry the kernel_info_request here
this._kernelSession = '';
}
clearTimeout(timeoutHandle);
if (this._pendingMessages.length > 0) {
this._sendPending();
}
};
void p.then(sendPendingOnce);
// FIXME: if sent while zmq subscriptions are not established,
// kernelInfo may not resolve, so use a timeout to ensure we don't hang forever.
// It may be preferable to retry kernelInfo rather than give up after one timeout.
let timeoutHandle = setTimeout(sendPendingOnce, KERNEL_INFO_TIMEOUT);
} else {
// If the connection is down, then we do not know what is happening
// with the kernel, so set the status to unknown.
this._updateStatus('unknown');
}
}
// Notify others that the connection status changed.
this._connectionStatusChanged.emit(connectionStatus);
}
private async _handleMessage(msg: KernelMessage.IMessage): Promise<void> {
let handled = false;
// Check to see if we have a display_id we need to reroute.
if (
msg.parent_header &&
msg.channel === 'iopub' &&
(KernelMessage.isDisplayDataMsg(msg) ||
KernelMessage.isUpdateDisplayDataMsg(msg) ||
KernelMessage.isExecuteResultMsg(msg))
) {
// display_data messages may re-route based on their display_id.
const transient = (msg.content.transient ?? {}) as JSONObject;
const displayId = transient['display_id'] as string;
if (displayId) {
handled = await this._handleDisplayId(displayId, msg);
// The await above may make this message out of date, so check again.
this._assertCurrentMessage(msg);
}
}
if (!handled && msg.parent_header) {
const parentHeader = msg.parent_header as KernelMessage.IHeader;
const future = this._futures?.get(parentHeader.msg_id);
if (future) {
await future.handleMsg(msg);
this._assertCurrentMessage(msg);
} else {
// If the message was sent by us and was not iopub, it is orphaned.
const owned = parentHeader.session === this.clientId;
if (msg.channel !== 'iopub' && owned) {
this._unhandledMessage.emit(msg);
}
}
}
if (msg.channel === 'iopub') {
switch (msg.header.msg_type) {
case 'status': {
// Updating the status is synchronous, and we call no async user code
const executionState = (msg as KernelMessage.IStatusMsg).content
.execution_state;
if (executionState === 'restarting') {
// The kernel has been auto-restarted by the server. After
// processing for this message is completely done, we want to
// handle this restart, so we don't await, but instead schedule
// the work as a microtask (i.e., in a promise resolution). We
// schedule this here so that it comes before any microtasks that
// might be scheduled in the status signal emission below.
void Promise.resolve().then(async () => {
this._updateStatus('autorestarting');
this._clearKernelState();
// We must reconnect since the kernel connection information may have
// changed, and the server only refreshes its zmq connection when a new
// websocket is opened.
await this.reconnect();
});
}
this._updateStatus(executionState);
break;
}
case 'comm_open':
if (this.handleComms) {
await this._handleCommOpen(msg as KernelMessage.ICommOpenMsg);
}
break;
case 'comm_msg':
if (this.handleComms) {
await this._handleCommMsg(msg as KernelMessage.ICommMsgMsg);
}
break;
case 'comm_close':
if (this.handleComms) {
await this._handleCommClose(msg as KernelMessage.ICommCloseMsg);
}
break;
default:
break;
}
// If the message was a status dead message, we might have disposed ourselves.
if (!this.isDisposed) {
this._assertCurrentMessage(msg);
// the message wouldn't be emitted if we were disposed anyway.
this._iopubMessage.emit(msg as KernelMessage.IIOPubMessage);
}
}
}
/**
* Attempt a connection if we have not exhausted connection attempts.
*/
private _reconnect() {
this._errorIfDisposed();
// Clear any existing reconnection attempt
clearTimeout(this._reconnectTimeout);
// Update the connection status and schedule a possible reconnection.
if (this._reconnectAttempt < this._reconnectLimit) {
this._updateConnectionStatus('connecting');
// The first reconnect attempt should happen immediately, and subsequent
// attempts should pick a random number in a growing range so that we
// don't overload the server with synchronized reconnection attempts
// across multiple kernels.
const timeout = Private.getRandomIntInclusive(
0,
1e3 * (Math.pow(2, this._reconnectAttempt) - 1)
);
console.warn(
`Connection lost, reconnecting in ${Math.floor(
timeout / 1000
)} seconds.`
);
// Try reconnection with subprotocols if the server had supported them.
// Otherwise, try reconnection without subprotocols.
const useProtocols = this._selectedProtocol !== '' ? true : false;
this._reconnectTimeout = setTimeout(
this._createSocket,
timeout,
useProtocols
);
this._reconnectAttempt += 1;
} else {
this._updateConnectionStatus('disconnected');
}
// Clear the websocket event handlers and the socket itself.
this._clearSocket();
}
/**
* Utility function to throw an error if this instance is disposed.
*/
private _errorIfDisposed() {
if (this.isDisposed) {
throw new Error('Kernel connection is disposed');
}
}
// Make websocket callbacks arrow functions so they bind `this`.
/**
* Handle a websocket open event.
*/
private _onWSOpen = (evt: Event) => {
if (
this._ws!.protocol !== '' &&
!this._supportedProtocols.includes(this._ws!.protocol)
) {
console.log(
'Server selected unknown kernel wire protocol:',
this._ws!.protocol
);
this._updateStatus('dead');
throw new Error(`Unknown kernel wire protocol: ${this._ws!.protocol}`);
}
// Remember the kernel wire protocol selected by the server.
this._selectedProtocol = this._ws!.protocol;
this._ws!.onclose = this._onWSClose;
this._ws!.onerror = this._onWSClose;
this._updateConnectionStatus('connected');
};
/**
* Handle a websocket message, validating and routing appropriately.
*/
private _onWSMessage = (evt: MessageEvent) => {
// Notify immediately if there is an error with the message.
let msg: KernelMessage.IMessage;
try {
msg = this.serverSettings.serializer.deserialize(
evt.data,
this._ws!.protocol
);
validate.validateMessage(msg);
} catch (error) {
error.message = `Kernel message validation error: ${error.message}`;
// We throw the error so that it bubbles up to the top, and displays the right stack.
throw error;
}
// Update the current kernel session id
this._kernelSession = msg.header.session;
// Handle the message asynchronously, in the order received.
this._msgChain = this._msgChain
.then(() => {
// Return so that any promises from handling a message are fulfilled
// before proceeding to the next message.
return this._handleMessage(msg);
})
.catch(error => {
// Log any errors in handling the message, thus resetting the _msgChain
// promise so we can process more messages.
// Ignore the "Canceled" errors that are thrown during kernel dispose.
if (error.message.startsWith('Canceled future for ')) {
console.error(error);
}
});
// Emit the message receive signal
this._anyMessage.emit({ msg, direction: 'recv' });
};
/**
* Handle a websocket close event.
*/
private _onWSClose = (evt: Event) => {
if (!this.isDisposed) {
this._reconnect();
}
};
get hasPendingInput(): boolean {
return this._hasPendingInput;
}
set hasPendingInput(value: boolean) {
this._hasPendingInput = value;
this._pendingInput.emit(value);
}
private _id = '';
private _name = '';
private _model: Kernel.IModel | undefined;
private _status: KernelMessage.Status = 'unknown';
private _connectionStatus: Kernel.ConnectionStatus = 'connecting';
private _kernelSession = '';
private _clientId: string;
private _isDisposed = false;
/**
* Websocket to communicate with kernel.
*/
private _ws: WebSocket | null = null;
private _username = '';
private _reconnectLimit = 7;
private _reconnectAttempt = 0;
private _reconnectTimeout: any = null;
private _supportedProtocols: string[] = Object.va