|
@@ -1,48 +1,42 @@
|
|
|
// Copyright (c) Jupyter Development Team.
|
|
|
// Distributed under the terms of the Modified BSD License.
|
|
|
|
|
|
-import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
|
+import { URLExt } from '@jupyterlab/coreutils';
|
|
|
|
|
|
-import { ArrayExt, each, map, toArray } from '@lumino/algorithm';
|
|
|
-
|
|
|
-import { JSONPrimitive } from '@lumino/coreutils';
|
|
|
+import { JSONPrimitive, PromiseDelegate } from '@lumino/coreutils';
|
|
|
|
|
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
|
|
|
|
import { ServerConnection } from '..';
|
|
|
|
|
|
-import { TerminalSession } from './terminal';
|
|
|
-
|
|
|
-/**
|
|
|
- * The url for the terminal service.
|
|
|
- */
|
|
|
-const TERMINAL_SERVICE_URL = 'api/terminals';
|
|
|
+import * as Terminal from './terminal';
|
|
|
+import { shutdownTerminal, TERMINAL_SERVICE_URL } from './restapi';
|
|
|
|
|
|
/**
|
|
|
* An implementation of a terminal interface.
|
|
|
*/
|
|
|
-export class DefaultTerminalSession implements TerminalSession.ISession {
|
|
|
+export class TerminalConnection implements Terminal.ITerminalConnection {
|
|
|
/**
|
|
|
* Construct a new terminal session.
|
|
|
*/
|
|
|
- constructor(name: string, options: TerminalSession.IOptions = {}) {
|
|
|
- this._name = name;
|
|
|
+ constructor(options: Terminal.ITerminalConnection.IOptions) {
|
|
|
+ this._name = options.model.name;
|
|
|
this.serverSettings =
|
|
|
options.serverSettings ?? ServerConnection.makeSettings();
|
|
|
- this._readyPromise = this._initializeSocket();
|
|
|
+ this._createSocket();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * A signal emitted when the session is shut down.
|
|
|
+ * A signal emitted when the session is disposed.
|
|
|
*/
|
|
|
- get terminated(): Signal<this, void> {
|
|
|
- return this._terminated;
|
|
|
+ get disposed(): ISignal<this, void> {
|
|
|
+ return this._disposed;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* A signal emitted when a message is received from the server.
|
|
|
*/
|
|
|
- get messageReceived(): ISignal<this, TerminalSession.IMessage> {
|
|
|
+ get messageReceived(): ISignal<this, Terminal.IMessage> {
|
|
|
return this._messageReceived;
|
|
|
}
|
|
|
|
|
@@ -56,7 +50,7 @@ export class DefaultTerminalSession implements TerminalSession.ISession {
|
|
|
/**
|
|
|
* Get the model for the terminal session.
|
|
|
*/
|
|
|
- get model(): TerminalSession.IModel {
|
|
|
+ get model(): Terminal.IModel {
|
|
|
return { name: this._name };
|
|
|
}
|
|
|
|
|
@@ -65,20 +59,6 @@ export class DefaultTerminalSession implements TerminalSession.ISession {
|
|
|
*/
|
|
|
readonly serverSettings: ServerConnection.ISettings;
|
|
|
|
|
|
- /**
|
|
|
- * Test whether the session is ready.
|
|
|
- */
|
|
|
- get isReady(): boolean {
|
|
|
- return this._isReady;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * A promise that fulfills when the session is ready.
|
|
|
- */
|
|
|
- get ready(): Promise<void> {
|
|
|
- return this._readyPromise;
|
|
|
- }
|
|
|
-
|
|
|
/**
|
|
|
* Test whether the session is disposed.
|
|
|
*/
|
|
@@ -94,403 +74,335 @@ export class DefaultTerminalSession implements TerminalSession.ISession {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- this.terminated.emit(undefined);
|
|
|
this._isDisposed = true;
|
|
|
- if (this._ws) {
|
|
|
- this._ws.close();
|
|
|
- this._ws = null;
|
|
|
- }
|
|
|
- delete Private.running[this._url];
|
|
|
+ this._disposed.emit();
|
|
|
+
|
|
|
+ this._updateConnectionStatus('disconnected');
|
|
|
+ this._clearSocket();
|
|
|
+
|
|
|
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: TerminalSession.IMessage): void {
|
|
|
+ send(message: Terminal.IMessage): void {
|
|
|
+ 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: Terminal.IMessage, queue = true): void {
|
|
|
if (this._isDisposed || !message.content) {
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- const msg = [message.type, ...message.content];
|
|
|
- const socket = this._ws;
|
|
|
- const value = JSON.stringify(msg);
|
|
|
-
|
|
|
- if (this._isReady && socket) {
|
|
|
- socket.send(value);
|
|
|
- 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)}`);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- void this.ready.then(() => {
|
|
|
- const socket = this._ws;
|
|
|
-
|
|
|
- if (socket) {
|
|
|
- socket.send(value);
|
|
|
- }
|
|
|
- });
|
|
|
+ /**
|
|
|
+ * 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._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 the terminal.
|
|
|
+ * Reconnect to a terminal.
|
|
|
*
|
|
|
- * @returns A promise that resolves when the terminal has reconnected.
|
|
|
+ * #### Notes
|
|
|
+ * This may try multiple times to reconnect to a terminal, and will sever
|
|
|
+ * any existing connection.
|
|
|
*/
|
|
|
reconnect(): Promise<void> {
|
|
|
+ this._errorIfDisposed();
|
|
|
+ let result = new PromiseDelegate<void>();
|
|
|
+
|
|
|
+ // Set up a listener for the connection status changing, which accepts or
|
|
|
+ // rejects after the retries are done.
|
|
|
+ let fulfill = (sender: this, status: Terminal.ConnectionStatus) => {
|
|
|
+ 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;
|
|
|
- this._readyPromise = this._initializeSocket();
|
|
|
- return this._readyPromise;
|
|
|
- }
|
|
|
|
|
|
- /**
|
|
|
- * Shut down the terminal session.
|
|
|
- */
|
|
|
- shutdown(): Promise<void> {
|
|
|
- const { name, serverSettings } = this;
|
|
|
- return DefaultTerminalSession.shutdown(name, serverSettings);
|
|
|
+ // 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;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Clone the current session object.
|
|
|
+ * Attempt a connection if we have not exhausted connection attempts.
|
|
|
*/
|
|
|
- clone(): TerminalSession.ISession {
|
|
|
- const { name, serverSettings } = this;
|
|
|
- return new DefaultTerminalSession(name, { serverSettings });
|
|
|
+ _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
|
|
|
+ // attemps should pick a random number in a growing range so that we
|
|
|
+ // don't overload the server with synchronized reconnection attempts
|
|
|
+ // across multiple kernels.
|
|
|
+ let 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();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Connect to the websocket.
|
|
|
+ * 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 _initializeSocket(): Promise<void> {
|
|
|
- const name = this._name;
|
|
|
- let socket = this._ws;
|
|
|
-
|
|
|
- if (socket) {
|
|
|
+ private _clearSocket(): void {
|
|
|
+ if (this._ws !== null) {
|
|
|
// Clear the websocket event handlers and the socket itself.
|
|
|
- socket.onopen = this._noOp;
|
|
|
- socket.onclose = this._noOp;
|
|
|
- socket.onerror = this._noOp;
|
|
|
- socket.onmessage = this._noOp;
|
|
|
- socket.close();
|
|
|
+ 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;
|
|
|
}
|
|
|
- this._isReady = false;
|
|
|
+ }
|
|
|
|
|
|
- return new Promise<void>((resolve, reject) => {
|
|
|
- const settings = this.serverSettings;
|
|
|
- const token = this.serverSettings.token;
|
|
|
+ /**
|
|
|
+ * Shut down the terminal session.
|
|
|
+ */
|
|
|
+ async shutdown(): Promise<void> {
|
|
|
+ await shutdownTerminal(this.name, this.serverSettings);
|
|
|
+ this.dispose();
|
|
|
+ }
|
|
|
|
|
|
- this._url = Private.getTermUrl(settings.baseUrl, this._name);
|
|
|
- Private.running[this._url] = this;
|
|
|
+ /**
|
|
|
+ * Clone the current terminal connection.
|
|
|
+ */
|
|
|
+ clone(): Terminal.ITerminalConnection {
|
|
|
+ return new TerminalConnection(this);
|
|
|
+ }
|
|
|
|
|
|
- let wsUrl = URLExt.join(settings.wsUrl, `terminals/websocket/${name}`);
|
|
|
+ /**
|
|
|
+ * Create the terminal websocket connection and add socket status handlers.
|
|
|
+ *
|
|
|
+ * #### Notes
|
|
|
+ * You are responsible for updating the connection status as appropriate.
|
|
|
+ */
|
|
|
+ private _createSocket = () => {
|
|
|
+ this._errorIfDisposed();
|
|
|
|
|
|
- if (token) {
|
|
|
- wsUrl = wsUrl + `?token=${encodeURIComponent(token)}`;
|
|
|
- }
|
|
|
+ // Make sure the socket is clear
|
|
|
+ this._clearSocket();
|
|
|
|
|
|
- socket = this._ws = new settings.WebSocket(wsUrl);
|
|
|
-
|
|
|
- socket.onmessage = (event: MessageEvent) => {
|
|
|
- if (this._isDisposed) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const data = JSON.parse(event.data) as JSONPrimitive[];
|
|
|
-
|
|
|
- // Handle a disconnect message.
|
|
|
- if (data[0] === 'disconnect') {
|
|
|
- this._disconnected = true;
|
|
|
- }
|
|
|
-
|
|
|
- if (this._reconnectAttempt > 0) {
|
|
|
- // After reconnection, ignore all messages until a 'setup' message.
|
|
|
- if (data[0] === 'setup') {
|
|
|
- this._reconnectAttempt = 0;
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- this._messageReceived.emit({
|
|
|
- type: data[0] as TerminalSession.MessageType,
|
|
|
- content: data.slice(1)
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- socket.onopen = (event: MessageEvent) => {
|
|
|
- if (!this._isDisposed) {
|
|
|
- this._isReady = true;
|
|
|
- this._disconnected = false;
|
|
|
- resolve(undefined);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- socket.onerror = (event: Event) => {
|
|
|
- if (!this._isDisposed) {
|
|
|
- reject(event);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- socket.onclose = (event: CloseEvent) => {
|
|
|
- console.warn(`Terminal websocket closed: ${event.code}`);
|
|
|
- if (this._disconnected) {
|
|
|
- this.dispose();
|
|
|
- }
|
|
|
- this._reconnectSocket();
|
|
|
- };
|
|
|
- });
|
|
|
- }
|
|
|
+ // Update the connection status to reflect opening a new connection.
|
|
|
+ this._updateConnectionStatus('connecting');
|
|
|
|
|
|
- private _reconnectSocket(): void {
|
|
|
- if (this._isDisposed || !this._ws || this._disconnected) {
|
|
|
- return;
|
|
|
+ const name = this._name;
|
|
|
+ const settings = this.serverSettings;
|
|
|
+
|
|
|
+ let url = URLExt.join(
|
|
|
+ settings.wsUrl,
|
|
|
+ 'terminals',
|
|
|
+ 'websocket',
|
|
|
+ encodeURIComponent(name)
|
|
|
+ );
|
|
|
+
|
|
|
+ const token = settings.token;
|
|
|
+ if (token !== '') {
|
|
|
+ url = url + `?token=${encodeURIComponent(token)}`;
|
|
|
}
|
|
|
+ this._ws = new settings.WebSocket(url);
|
|
|
|
|
|
- const attempt = this._reconnectAttempt;
|
|
|
- const limit = this._reconnectLimit;
|
|
|
+ this._ws.onmessage = this._onWSMessage;
|
|
|
+ this._ws.onclose = this._onWSClose;
|
|
|
+ this._ws.onerror = this._onWSClose;
|
|
|
+ };
|
|
|
|
|
|
- if (attempt >= limit) {
|
|
|
- console.log(`Terminal reconnect aborted: ${attempt} attempts`);
|
|
|
+ // Websocket messages events are defined as variables to bind `this`
|
|
|
+ private _onWSMessage = (event: MessageEvent) => {
|
|
|
+ if (this._isDisposed) {
|
|
|
return;
|
|
|
}
|
|
|
+ const data = JSON.parse(event.data) as JSONPrimitive[];
|
|
|
|
|
|
- const timeout = Math.pow(2, attempt);
|
|
|
-
|
|
|
- console.log(`Terminal will attempt to reconnect in ${timeout}s`);
|
|
|
- this._isReady = false;
|
|
|
- this._reconnectAttempt += 1;
|
|
|
+ // Handle a disconnect message.
|
|
|
+ if (data[0] === 'disconnect') {
|
|
|
+ this.dispose();
|
|
|
+ }
|
|
|
|
|
|
- setTimeout(() => {
|
|
|
- if (this.isDisposed) {
|
|
|
- return;
|
|
|
+ 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');
|
|
|
}
|
|
|
- this._initializeSocket()
|
|
|
- .then(() => {
|
|
|
- console.log('Terminal reconnected');
|
|
|
- })
|
|
|
- .catch(reason => {
|
|
|
- console.warn(`Terminal reconnect failed`, reason);
|
|
|
- });
|
|
|
- }, 1e3 * timeout);
|
|
|
- }
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- private _isDisposed = false;
|
|
|
- private _isReady = false;
|
|
|
- private _messageReceived = new Signal<this, TerminalSession.IMessage>(this);
|
|
|
- private _terminated = new Signal<this, void>(this);
|
|
|
- private _name: string;
|
|
|
- private _readyPromise: Promise<void>;
|
|
|
- private _url: string;
|
|
|
- private _ws: WebSocket | null = null;
|
|
|
- private _noOp = () => {
|
|
|
- /* no-op */
|
|
|
+ this._messageReceived.emit({
|
|
|
+ type: data[0] as Terminal.MessageType,
|
|
|
+ content: data.slice(1)
|
|
|
+ });
|
|
|
};
|
|
|
- private _reconnectLimit = 7;
|
|
|
- private _reconnectAttempt = 0;
|
|
|
- private _disconnected = false;
|
|
|
-}
|
|
|
|
|
|
-/**
|
|
|
- * The static namespace for `DefaultTerminalSession`.
|
|
|
- */
|
|
|
-export namespace DefaultTerminalSession {
|
|
|
- /**
|
|
|
- * Whether the terminal service is available.
|
|
|
- */
|
|
|
- export function isAvailable(): boolean {
|
|
|
- let available = String(PageConfig.getOption('terminalsAvailable'));
|
|
|
- return available.toLowerCase() === 'true';
|
|
|
- }
|
|
|
+ private _onWSClose = (event: CloseEvent) => {
|
|
|
+ console.warn(`Terminal websocket closed: ${event.code}`);
|
|
|
+ if (!this.isDisposed) {
|
|
|
+ this._reconnect();
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
/**
|
|
|
- * Start a new terminal session.
|
|
|
- *
|
|
|
- * @param options - The session options to use.
|
|
|
- *
|
|
|
- * @returns A promise that resolves with the session instance.
|
|
|
+ * Handle connection status changes.
|
|
|
*/
|
|
|
- export function startNew(
|
|
|
- options: TerminalSession.IOptions = {}
|
|
|
- ): Promise<TerminalSession.ISession> {
|
|
|
- if (!TerminalSession.isAvailable()) {
|
|
|
- throw Private.unavailableMsg;
|
|
|
+ private _updateConnectionStatus(
|
|
|
+ connectionStatus: Terminal.ConnectionStatus
|
|
|
+ ): void {
|
|
|
+ if (this._connectionStatus === connectionStatus) {
|
|
|
+ return;
|
|
|
}
|
|
|
- let serverSettings =
|
|
|
- options.serverSettings ?? ServerConnection.makeSettings();
|
|
|
- let url = Private.getServiceUrl(serverSettings.baseUrl);
|
|
|
- let init = { method: 'POST' };
|
|
|
-
|
|
|
- return ServerConnection.makeRequest(url, init, serverSettings)
|
|
|
- .then(response => {
|
|
|
- if (response.status !== 200) {
|
|
|
- throw new ServerConnection.ResponseError(response);
|
|
|
- }
|
|
|
- return response.json();
|
|
|
- })
|
|
|
- .then((data: TerminalSession.IModel) => {
|
|
|
- let name = data.name;
|
|
|
- return new DefaultTerminalSession(name, { ...options, serverSettings });
|
|
|
- });
|
|
|
- }
|
|
|
|
|
|
- /*
|
|
|
- * Connect to a running session.
|
|
|
- *
|
|
|
- * @param name - The name of the target session.
|
|
|
- *
|
|
|
- * @param options - The session options to use.
|
|
|
- *
|
|
|
- * @returns A promise that resolves with the new session instance.
|
|
|
- *
|
|
|
- * #### Notes
|
|
|
- * If the session was already started via `startNew`, the existing
|
|
|
- * session object is used as the fulfillment value.
|
|
|
- *
|
|
|
- * Otherwise, if `options` are given, we resolve the promise after
|
|
|
- * confirming that the session exists on the server.
|
|
|
- *
|
|
|
- * If the session does not exist on the server, the promise is rejected.
|
|
|
- */
|
|
|
- export function connectTo(
|
|
|
- name: string,
|
|
|
- options: TerminalSession.IOptions = {}
|
|
|
- ): Promise<TerminalSession.ISession> {
|
|
|
- if (!TerminalSession.isAvailable()) {
|
|
|
- return Promise.reject(Private.unavailableMsg);
|
|
|
+ this._connectionStatus = connectionStatus;
|
|
|
+
|
|
|
+ // If we are not 'connecting', stop any reconnection attempts.
|
|
|
+ if (connectionStatus !== 'connecting') {
|
|
|
+ this._reconnectAttempt = 0;
|
|
|
+ clearTimeout(this._reconnectTimeout);
|
|
|
}
|
|
|
- let serverSettings =
|
|
|
- options.serverSettings ?? ServerConnection.makeSettings();
|
|
|
- let url = Private.getTermUrl(serverSettings.baseUrl, name);
|
|
|
- if (url in Private.running) {
|
|
|
- return Promise.resolve(Private.running[url].clone());
|
|
|
+
|
|
|
+ // Send the pending messages if we just connected.
|
|
|
+ if (connectionStatus === 'connected') {
|
|
|
+ this._sendPending();
|
|
|
}
|
|
|
- return listRunning(serverSettings).then(models => {
|
|
|
- let index = ArrayExt.findFirstIndex(models, model => {
|
|
|
- return model.name === name;
|
|
|
- });
|
|
|
- if (index !== -1) {
|
|
|
- let session = new DefaultTerminalSession(name, {
|
|
|
- ...options,
|
|
|
- serverSettings
|
|
|
- });
|
|
|
- return Promise.resolve(session);
|
|
|
- }
|
|
|
- return Promise.reject<TerminalSession.ISession>('Could not find session');
|
|
|
- });
|
|
|
+
|
|
|
+ // Notify others that the connection status changed.
|
|
|
+ this._connectionStatusChanged.emit(connectionStatus);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * List the running terminal sessions.
|
|
|
- *
|
|
|
- * @param settings - The server settings to use.
|
|
|
- *
|
|
|
- * @returns A promise that resolves with the list of running session models.
|
|
|
+ * Utility function to throw an error if this instance is disposed.
|
|
|
*/
|
|
|
- export function listRunning(
|
|
|
- settings: ServerConnection.ISettings = ServerConnection.makeSettings()
|
|
|
- ): Promise<TerminalSession.IModel[]> {
|
|
|
- if (!TerminalSession.isAvailable()) {
|
|
|
- return Promise.reject(Private.unavailableMsg);
|
|
|
+ private _errorIfDisposed() {
|
|
|
+ if (this.isDisposed) {
|
|
|
+ throw new Error('Terminal connection is disposed');
|
|
|
}
|
|
|
- let url = Private.getServiceUrl(settings.baseUrl);
|
|
|
- return ServerConnection.makeRequest(url, {}, settings)
|
|
|
- .then(response => {
|
|
|
- if (response.status !== 200) {
|
|
|
- throw new ServerConnection.ResponseError(response);
|
|
|
- }
|
|
|
- return response.json();
|
|
|
- })
|
|
|
- .then((data: TerminalSession.IModel[]) => {
|
|
|
- if (!Array.isArray(data)) {
|
|
|
- throw new Error('Invalid terminal data');
|
|
|
- }
|
|
|
- // Update the local data store.
|
|
|
- let urls = toArray(
|
|
|
- map(data, item => {
|
|
|
- return URLExt.join(url, item.name);
|
|
|
- })
|
|
|
- );
|
|
|
- each(Object.keys(Private.running), runningUrl => {
|
|
|
- if (urls.indexOf(runningUrl) === -1) {
|
|
|
- let session = Private.running[runningUrl];
|
|
|
- session.dispose();
|
|
|
- }
|
|
|
- });
|
|
|
- return data;
|
|
|
- });
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Shut down a terminal session by name.
|
|
|
- *
|
|
|
- * @param name - The name of the target session.
|
|
|
- *
|
|
|
- * @param settings - The server settings to use.
|
|
|
- *
|
|
|
- * @returns A promise that resolves when the session is shut down.
|
|
|
+ * A signal emitted when the terminal connection status changes.
|
|
|
*/
|
|
|
- export function shutdown(
|
|
|
- name: string,
|
|
|
- settings: ServerConnection.ISettings = ServerConnection.makeSettings()
|
|
|
- ): Promise<void> {
|
|
|
- if (!TerminalSession.isAvailable()) {
|
|
|
- return Promise.reject(Private.unavailableMsg);
|
|
|
- }
|
|
|
- let url = Private.getTermUrl(settings.baseUrl, name);
|
|
|
- let init = { method: 'DELETE' };
|
|
|
- return ServerConnection.makeRequest(url, init, settings).then(response => {
|
|
|
- if (response.status === 404) {
|
|
|
- return response.json().then(data => {
|
|
|
- console.warn(data['message']);
|
|
|
- });
|
|
|
- }
|
|
|
- if (response.status !== 204) {
|
|
|
- throw new ServerConnection.ResponseError(response);
|
|
|
- }
|
|
|
- });
|
|
|
+ get connectionStatusChanged(): ISignal<this, Terminal.ConnectionStatus> {
|
|
|
+ return this._connectionStatusChanged;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Shut down all terminal sessions.
|
|
|
- *
|
|
|
- * @param settings - The server settings to use.
|
|
|
- *
|
|
|
- * @returns A promise that resolves when all the sessions are shut down.
|
|
|
+ * The current connection status of the terminal connection.
|
|
|
*/
|
|
|
- export async function shutdownAll(
|
|
|
- settings: ServerConnection.ISettings = ServerConnection.makeSettings()
|
|
|
- ): Promise<void> {
|
|
|
- const running = await listRunning(settings);
|
|
|
- await Promise.all(running.map(s => shutdown(s.name, settings)));
|
|
|
+ get connectionStatus(): Terminal.ConnectionStatus {
|
|
|
+ return this._connectionStatus;
|
|
|
}
|
|
|
+
|
|
|
+ private _connectionStatus: Terminal.ConnectionStatus = 'connecting';
|
|
|
+ private _connectionStatusChanged = new Signal<
|
|
|
+ this,
|
|
|
+ Terminal.ConnectionStatus
|
|
|
+ >(this);
|
|
|
+ private _isDisposed = false;
|
|
|
+ private _disposed = new Signal<this, void>(this);
|
|
|
+ private _messageReceived = new Signal<this, Terminal.IMessage>(this);
|
|
|
+ private _name: string;
|
|
|
+ private _reconnectTimeout: any = null;
|
|
|
+ private _ws: WebSocket | null = null;
|
|
|
+ private _noOp = () => {
|
|
|
+ /* no-op */
|
|
|
+ };
|
|
|
+ private _reconnectLimit = 7;
|
|
|
+ private _reconnectAttempt = 0;
|
|
|
+ private _pendingMessages: Terminal.IMessage[] = [];
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * A namespace for private data.
|
|
|
- */
|
|
|
namespace Private {
|
|
|
- /**
|
|
|
- * A mapping of running terminals by url.
|
|
|
- */
|
|
|
- export const running: {
|
|
|
- [key: string]: DefaultTerminalSession;
|
|
|
- } = Object.create(null);
|
|
|
-
|
|
|
- /**
|
|
|
- * A promise returned for when terminals are unavailable.
|
|
|
- */
|
|
|
- export const unavailableMsg = 'Terminals Unavailable';
|
|
|
-
|
|
|
/**
|
|
|
* Get the url for a terminal.
|
|
|
*/
|
|
|
export function getTermUrl(baseUrl: string, name: string): string {
|
|
|
- return URLExt.join(baseUrl, TERMINAL_SERVICE_URL, name);
|
|
|
+ return URLExt.join(baseUrl, TERMINAL_SERVICE_URL, encodeURIComponent(name));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Get the base url.
|
|
|
+ * 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.
|
|
|
*/
|
|
|
- export function getServiceUrl(baseUrl: string): string {
|
|
|
- return URLExt.join(baseUrl, TERMINAL_SERVICE_URL);
|
|
|
+ export function getRandomIntInclusive(min: number, max: number) {
|
|
|
+ min = Math.ceil(min);
|
|
|
+ max = Math.floor(max);
|
|
|
+ return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
|
}
|
|
|
}
|