UNPKG

@jupyterlab/services

Version:

Client APIs for the Jupyter services REST APIs

341 lines 12.4 kB
"use strict"; // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. Object.defineProperty(exports, "__esModule", { value: true }); exports.TerminalConnection = void 0; const coreutils_1 = require("@jupyterlab/coreutils"); const coreutils_2 = require("@lumino/coreutils"); const signaling_1 = require("@lumino/signaling"); const __1 = require(".."); const restapi_1 = require("./restapi"); /** * An implementation of a terminal interface. */ class TerminalConnection { /** * Construct a new terminal session. */ constructor(options) { var _a; /** * Create the terminal websocket connection and add socket status handlers. * * #### Notes * You are responsible for updating the connection status as appropriate. */ this._createSocket = () => { this._errorIfDisposed(); // Make sure the socket is clear this._clearSocket(); // Update the connection status to reflect opening a new connection. this._updateConnectionStatus('connecting'); const name = this._name; const settings = this.serverSettings; let url = coreutils_1.URLExt.join(settings.wsUrl, 'terminals', 'websocket', encodeURIComponent(name)); // If token authentication is in use. const token = settings.token; if (settings.appendToken && token !== '') { url = url + `?token=${encodeURIComponent(token)}`; } this._ws = new settings.WebSocket(url); this._ws.onmessage = this._onWSMessage; this._ws.onclose = this._onWSClose; this._ws.onerror = this._onWSClose; }; // Websocket messages events are defined as variables to bind `this` this._onWSMessage = (event) => { if (this._isDisposed) { return; } const data = JSON.parse(event.data); // Handle a disconnect message. if (data[0] === 'disconnect') { this.dispose(); } if (this._connectionStatus === 'connecting') { // After reconnection, ignore all messages until a 'setup' message // before we are truly connected. Setting the connection status to // connected only then means that if we do not get a setup message // before our retry timeout, we will delete the websocket and try again. if (data[0] === 'setup') { this._updateConnectionStatus('connected'); } return; } this._messageReceived.emit({ type: data[0], content: data.slice(1) }); }; this._onWSClose = (event) => { console.warn(`Terminal websocket closed: ${event.code}`); if (!this.isDisposed) { this._reconnect(); } }; this._connectionStatus = 'connecting'; this._connectionStatusChanged = new signaling_1.Signal(this); this._isDisposed = false; this._disposed = new signaling_1.Signal(this); this._messageReceived = new signaling_1.Signal(this); this._reconnectTimeout = null; this._ws = null; this._noOp = () => { /* no-op */ }; this._reconnectLimit = 7; this._reconnectAttempt = 0; this._pendingMessages = []; this._name = options.model.name; this.serverSettings = (_a = options.serverSettings) !== null && _a !== void 0 ? _a : __1.ServerConnection.makeSettings(); this._createSocket(); } /** * A signal emitted when the session is disposed. */ get disposed() { return this._disposed; } /** * A signal emitted when a message is received from the server. */ get messageReceived() { return this._messageReceived; } /** * Get the name of the terminal session. */ get name() { return this._name; } /** * Get the model for the terminal session. */ get model() { return { name: this._name }; } /** * Test whether the session is disposed. */ get isDisposed() { return this._isDisposed; } /** * Dispose of the resources held by the session. */ dispose() { if (this._isDisposed) { return; } this._isDisposed = true; this._disposed.emit(); this._updateConnectionStatus('disconnected'); this._clearSocket(); signaling_1.Signal.clearData(this); } /** * Send a message to the terminal session. * * #### Notes * If the connection is down, the message will be queued for sending when * the connection comes back up. */ send(message) { this._sendMessage(message); } /** * Send a message on the websocket, or possibly queue for later sending. * * @param queue - whether to queue the message if it cannot be sent */ _sendMessage(message, queue = true) { if (this._isDisposed || !message.content) { return; } if (this.connectionStatus === 'connected' && this._ws) { const msg = [message.type, ...message.content]; this._ws.send(JSON.stringify(msg)); } else if (queue) { this._pendingMessages.push(message); } else { throw new Error(`Could not send message: ${JSON.stringify(message)}`); } } /** * 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._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(); } } /** * Reconnect to a terminal. * * #### Notes * This may try multiple times to reconnect to a terminal, 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('Terminal 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; } /** * Attempt a connection if we have not exhausted connection attempts. */ _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.error(`Connection lost, reconnecting in ${Math.floor(timeout / 1000)} seconds.`); this._reconnectTimeout = setTimeout(this._createSocket, timeout); this._reconnectAttempt += 1; } else { this._updateConnectionStatus('disconnected'); } // Clear the websocket event handlers and the socket itself. this._clearSocket(); } /** * 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; } } /** * Shut down the terminal session. */ async shutdown() { await (0, restapi_1.shutdownTerminal)(this.name, this.serverSettings); this.dispose(); } /** * Clone the current terminal connection. */ clone() { return new TerminalConnection(this); } /** * Handle connection status changes. */ _updateConnectionStatus(connectionStatus) { if (this._connectionStatus === connectionStatus) { return; } this._connectionStatus = connectionStatus; // If we are not 'connecting', stop any reconnection attempts. if (connectionStatus !== 'connecting') { this._reconnectAttempt = 0; clearTimeout(this._reconnectTimeout); } // Send the pending messages if we just connected. if (connectionStatus === 'connected') { this._sendPending(); } // Notify others that the connection status changed. this._connectionStatusChanged.emit(connectionStatus); } /** * Utility function to throw an error if this instance is disposed. */ _errorIfDisposed() { if (this.isDisposed) { throw new Error('Terminal connection is disposed'); } } /** * A signal emitted when the terminal connection status changes. */ get connectionStatusChanged() { return this._connectionStatusChanged; } /** * The current connection status of the terminal connection. */ get connectionStatus() { return this._connectionStatus; } } exports.TerminalConnection = TerminalConnection; var Private; (function (Private) { /** * Get the url for a terminal. */ function getTermUrl(baseUrl, name) { return coreutils_1.URLExt.join(baseUrl, restapi_1.TERMINAL_SERVICE_URL, encodeURIComponent(name)); } Private.getTermUrl = getTermUrl; /** * Get a random integer between min and max, inclusive of both. * * #### Notes * From * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#Getting_a_random_integer_between_two_values_inclusive * * From the MDN page: It might be tempting to use Math.round() to accomplish * that, but doing so would cause your random numbers to follow a non-uniform * distribution, which may not be acceptable for your needs. */ function getRandomIntInclusive(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } Private.getRandomIntInclusive = getRandomIntInclusive; })(Private || (Private = {})); //# sourceMappingURL=default.js.map