UNPKG

@jupyterlab/services

Version:

Client APIs for the Jupyter services REST APIs

1,294 lines 55.5 kB
"use strict"; // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.KernelConnection = void 0; const coreutils_1 = require("@jupyterlab/coreutils"); const coreutils_2 = require("@lumino/coreutils"); const signaling_1 = require("@lumino/signaling"); const __1 = require(".."); const comm_1 = require("./comm"); const KernelMessage = __importStar(require("./messages")); const future_1 = require("./future"); const validate = __importStar(require("./validate")); const kernelspec_1 = require("../kernelspec"); const restapi = __importStar(require("./restapi")); 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. */ class KernelConnection { /** * Construct a kernel object. */ constructor(options) { var _a, _b, _c, _d; /** * Create the kernel websocket connection and add socket status handlers. */ this._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 = coreutils_1.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 = coreutils_1.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) => { var _a, _b; if (this._isDisposed) { return; } this._reason = ''; this._model = undefined; try { const model = await restapi.getKernelModel(this._id, settings); this._model = model; if ((model === null || model === void 0 ? void 0 : 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 __1.ServerConnection.NetworkError || ((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) === 503 || ((_b = err.response) === null || _b === void 0 ? void 0 : _b.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) => { // 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; }; // Make websocket callbacks arrow functions so they bind `this`. /** * Handle a websocket open event. */ this._onWSOpen = (evt) => { 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. */ this._onWSMessage = (evt) => { // Notify immediately if there is an error with the message. let msg; 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. */ this._onWSClose = (evt) => { if (!this.isDisposed) { this._reconnect(); } }; this._id = ''; this._name = ''; this._status = 'unknown'; this._connectionStatus = 'connecting'; this._kernelSession = ''; this._isDisposed = false; /** * Websocket to communicate with kernel. */ this._ws = null; this._username = ''; this._reconnectLimit = 7; this._reconnectAttempt = 0; this._reconnectTimeout = null; this._supportedProtocols = Object.values(KernelMessage.supportedKernelWebSocketProtocols); this._selectedProtocol = ''; this._futures = new Map(); this._comms = new Map(); this._targetRegistry = Object.create(null); this._info = new coreutils_2.PromiseDelegate(); this._pendingMessages = []; this._statusChanged = new signaling_1.Signal(this); this._connectionStatusChanged = new signaling_1.Signal(this); this._disposed = new signaling_1.Signal(this); this._iopubMessage = new signaling_1.Signal(this); this._anyMessage = new signaling_1.Signal(this); this._pendingInput = new signaling_1.Signal(this); this._unhandledMessage = new signaling_1.Signal(this); this._displayIdToParentIds = new Map(); this._msgIdToDisplayIds = new Map(); this._msgChain = Promise.resolve(); this._hasPendingInput = false; this._reason = ''; this._noOp = () => { /* no-op */ }; this._name = options.model.name; this._id = options.model.id; this.serverSettings = (_a = options.serverSettings) !== null && _a !== void 0 ? _a : __1.ServerConnection.makeSettings(); this._clientId = (_b = options.clientId) !== null && _b !== void 0 ? _b : coreutils_2.UUID.uuid4(); this._username = (_c = options.username) !== null && _c !== void 0 ? _c : ''; this.handleComms = (_d = options.handleComms) !== null && _d !== void 0 ? _d : true; this._createSocket(); } get disposed() { return this._disposed; } /** * A signal emitted when the kernel status changes. */ get statusChanged() { return this._statusChanged; } /** * A signal emitted when the kernel status changes. */ get connectionStatusChanged() { return this._connectionStatusChanged; } /** * A signal emitted for iopub kernel messages. * * #### Notes * This signal is emitted after the iopub message is handled asynchronously. */ get iopubMessage() { 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() { return this._unhandledMessage; } /** * The kernel model */ get model() { 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() { return this._anyMessage; } /** * A signal emitted when a kernel has pending inputs from the user. */ get pendingInput() { return this._pendingInput; } /** * The id of the server-side kernel. */ get id() { return this._id; } /** * The name of the server-side kernel. */ get name() { return this._name; } /** * The client username. */ get username() { return this._username; } /** * The client unique id. */ get clientId() { return this._clientId; } /** * The current status of the kernel. */ get status() { return this._status; } /** * The current connection status of the kernel connection. */ get connectionStatus() { return this._connectionStatus; } /** * Test whether the kernel has been disposed. */ get isDisposed() { return this._isDisposed; } /** * The cached kernel info. * * @returns A promise that resolves to the kernel info. */ get info() { return this._info.promise; } /** * The kernel spec. * * @returns A promise that resolves to the kernel spec. */ get spec() { if (this._specPromise) { return this._specPromise; } this._specPromise = kernelspec_1.KernelSpecAPI.getSpecs(this.serverSettings).then(specs => { return specs.kernelspecs[this._name]; }); return this._specPromise; } /** * Clone the current kernel with a new clientId. */ clone(options = {}) { 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() { if (this.isDisposed) { return; } this._isDisposed = true; this._disposed.emit(); this._updateConnectionStatus('disconnected'); this._clearKernelState(); this._pendingMessages = []; this._clearSocket(); // Clear Lumino signals signaling_1.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(msg, expectReply = false, disposeOnDone = true) { return this._sendKernelShellControl(future_1.KernelShellFutureHandler, msg, expectReply, disposeOnDone); } /** * 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(msg, expectReply = false, disposeOnDone = true) { return this._sendKernelShellControl(future_1.KernelControlFutureHandler, msg, expectReply, disposeOnDone); } _sendKernelShellControl(ctor, msg, expectReply = false, disposeOnDone = true) { 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. */ _sendMessage(msg, 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() { 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() { 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() { this._errorIfDisposed(); const result = new coreutils_2.PromiseDelegate(); // Set up a listener for the connection status changing, which accepts or // rejects after the retries are done. const fulfill = (sender, status) => { 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() { 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() { 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() { const msg = KernelMessage.createMessage({ msgType: 'kernel_info_request', channel: 'shell', username: this._username, session: this._clientId, content: {} }); let reply; try { reply = (await Private.handleShellMessage(this, msg)); } 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.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) { const msg = KernelMessage.createMessage({ msgType: 'complete_request', channel: 'shell', username: this._username, session: this._clientId, content }); return Private.handleShellMessage(this, msg); } /** * 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) { const msg = KernelMessage.createMessage({ msgType: 'inspect_request', channel: 'shell', username: this._username, session: this._clientId, content: content }); return Private.handleShellMessage(this, msg); } /** * 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) { const msg = KernelMessage.createMessage({ msgType: 'history_request', channel: 'shell', username: this._username, session: this._clientId, content }); return Private.handleShellMessage(this, msg); } /** * 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, disposeOnDone = true, metadata) { const defaults = { 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); } /** * 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, disposeOnDone = true) { const msg = KernelMessage.createMessage({ msgType: 'debug_request', channel: 'control', username: this._username, session: this._clientId, content }); return this.sendControlMessage(msg, true, disposeOnDone); } /** * 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) { const msg = KernelMessage.createMessage({ msgType: 'is_complete_request', channel: 'shell', username: this._username, session: this._clientId, content }); return Private.handleShellMessage(this, msg); } /** * Send a `comm_info_request` message. * * #### Notes * Fulfills with the `comm_info_reply` content when the shell reply is * received and validated. */ requestCommInfo(content) { const msg = KernelMessage.createMessage({ msgType: 'comm_info_request', channel: 'shell', username: this._username, session: this._clientId, content }); return Private.handleShellMessage(this, msg); } /** * 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, parent_header) { 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, commId = coreutils_2.UUID.uuid4()) { 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 comm_1.CommHandler(targetName, commId, this, () => { this._unregisterComm(commId); }); this._comms.set(commId, comm); return comm; } /** * Check if a comm exists. */ hasComm(commId) { 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, callback) { 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, callback) { 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, hook) { var _a; const future = (_a = this._futures) === null || _a === void 0 ? void 0 : _a.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, hook) { var _a; const future = (_a = this._futures) === null || _a === void 0 ? void 0 : _a.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. */ async _handleDisplayId(displayId, msg) { var _a, _b; const msgId = msg.parent_header.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 = { header: coreutils_2.JSONExt.deepCopy(msg.header), parent_header: coreutils_2.JSONExt.deepCopy(msg.parent_header), metadata: coreutils_2.JSONExt.deepCopy(msg.metadata), content: coreutils_2.JSONExt.deepCopy(msg.content), channel: msg.channel, buffers: msg.buffers ? msg.buffers.slice() : [] }; updateMsg.header.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 = (_a = this._displayIdToParentIds.get(displayId)) !== null && _a !== void 0 ? _a : []; 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 = (_b = this._msgIdToDisplayIds.get(msgId)) !== null && _b !== void 0 ? _b : []; 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. */ _clearSocket() { 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. */ _updateStatus(status) { 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. */ _sendPending() { // 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. */ _clearKernelState() { this._kernelSession = ''; this._pendingMessages = []; this._futures.forEach(future => { future.dispose(); }); this._comms.forEach(comm => { comm.dispose(); }); this._msgChain = Promise.resolve(); this._futures = new Map(); this._comms = new Map(); 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. */ _assertCurrentMessage(msg) { 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. */ async _handleCommOpen(msg) { this._assertCurrentMessage(msg); const content = msg.content; const comm = new comm_1.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. */ async _handleCommClose(msg) { 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.dispose(); } /** * Handle a 'comm_msg' kernel message. */ async _handleCommMsg(msg) { 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. */ _unregisterComm(commId) { this._comms.delete(commId); } /** * Handle connection status changes. */ _updateConnectionStatus(connectionStatus) { 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); } async _handleMessage(msg) { var _a, _b; 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 = ((_a = msg.content.transient) !== null && _a !== void 0 ? _a : {}); const displayId = transient['display_id']; 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; const future = (_b = this._futures) === null || _b === void 0 ? void 0 : _b.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.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); } break; case 'comm_msg': if (this.handleComms) { await this._handleCommMsg(msg); } break; case 'co