|
@@ -1,9 +1,8 @@
|
|
|
// Copyright (c) Jupyter Development Team.
|
|
|
// Distributed under the terms of the Modified BSD License.
|
|
|
|
|
|
-import {
|
|
|
- getWsUrl
|
|
|
-} from 'jupyter-js-utils';
|
|
|
+import * as utils
|
|
|
+ from 'jupyter-js-utils';
|
|
|
|
|
|
import {
|
|
|
IBoxSizing, boxSizing
|
|
@@ -13,6 +12,10 @@ import {
|
|
|
Message, sendMessage
|
|
|
} from 'phosphor-messaging';
|
|
|
|
|
|
+import {
|
|
|
+ ISignal, Signal
|
|
|
+} from 'phosphor-signaling';
|
|
|
+
|
|
|
import {
|
|
|
ResizeMessage, Widget
|
|
|
} from 'phosphor-widget';
|
|
@@ -41,56 +44,83 @@ const DUMMY_ROWS = 24;
|
|
|
*/
|
|
|
const DUMMY_COLS = 80;
|
|
|
|
|
|
+/**
|
|
|
+ * The url for the terminal service.
|
|
|
+ */
|
|
|
+const TERMINAL_SERVICE_URL = 'api/terminals';
|
|
|
+
|
|
|
|
|
|
/**
|
|
|
* A widget which manages a terminal session.
|
|
|
*/
|
|
|
export
|
|
|
class TerminalWidget extends Widget {
|
|
|
-
|
|
|
- /**
|
|
|
- * The number of terminals started. Used to ensure unique sessions.
|
|
|
- */
|
|
|
- static nterms = 0;
|
|
|
-
|
|
|
/**
|
|
|
* Construct a new terminal widget.
|
|
|
*
|
|
|
* @param options - The terminal configuration options.
|
|
|
*/
|
|
|
- constructor(options?: TerminalWidget.IOptions) {
|
|
|
+ constructor(options: TerminalWidget.IOptions = {}) {
|
|
|
super();
|
|
|
- options = options || {};
|
|
|
this.addClass(TERMINAL_CLASS);
|
|
|
- let baseUrl = options.baseUrl || getWsUrl();
|
|
|
|
|
|
- TerminalWidget.nterms += 1;
|
|
|
- let url = baseUrl + 'terminals/websocket/' + TerminalWidget.nterms;
|
|
|
- this._ws = new WebSocket(url);
|
|
|
- this.id = `jp-TerminalWidget-${TerminalWidget.nterms}`;
|
|
|
+ // Initialize options.
|
|
|
+ this._baseUrl = options.baseUrl || utils.getBaseUrl();
|
|
|
+ this._ajaxSettings = options.ajaxSettings || {};
|
|
|
+ this._name = options.name;
|
|
|
|
|
|
- // Set the default title.
|
|
|
- this.title.text = 'Terminal ' + TerminalWidget.nterms;
|
|
|
+ // Create the xterm, dummy terminal, and private style sheet.
|
|
|
+ this._term = new Xterm(Private.getConfig(options));
|
|
|
+ this._initializeTerm();
|
|
|
+ this._dummyTerm = Private.createDummyTerm();
|
|
|
+ this._sheet = document.createElement('style');
|
|
|
+ this.node.appendChild(this._sheet);
|
|
|
|
|
|
+ // Initialize settings.
|
|
|
+ this.fontSize = options.fontSize || 14;
|
|
|
+ this.background = options.background || 'black';
|
|
|
+ this.color = options.color || 'white';
|
|
|
+ this.id = `jp-TerminalWidget-${Private.id++}`;
|
|
|
+ this.title.text = 'Terminal';
|
|
|
Xterm.brokenBold = true;
|
|
|
|
|
|
- this._term = new Xterm(Private.getConfig(options));
|
|
|
- this._fontSize = options.fontSize || 14;
|
|
|
- this._background = options.background || 'black';
|
|
|
- this._color = options.color || 'white';
|
|
|
-
|
|
|
- this._dummyTerm = Private.createDummyTerm();
|
|
|
+ // Handle websocket connection.
|
|
|
+ let wsUrl = options.wsUrl || utils.getWsUrl(this._baseUrl);
|
|
|
+ if (this._name) {
|
|
|
+ this._intializeSocket(wsUrl);
|
|
|
+ } else {
|
|
|
+ this._getName().then(name => {
|
|
|
+ this._name = name;
|
|
|
+ this._intializeSocket(wsUrl);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- this._ws.onopen = (event: MessageEvent) => {
|
|
|
- this._intializeTerm();
|
|
|
- };
|
|
|
+ /**
|
|
|
+ * A signal emitted when the terminal is fully connected.
|
|
|
+ */
|
|
|
+ get connected(): ISignal<TerminalWidget, void> {
|
|
|
+ return Private.connectedSignal.bind(this);
|
|
|
+ }
|
|
|
|
|
|
- this._ws.onmessage = (event: MessageEvent) => {
|
|
|
- this._handleWSMessage(event);
|
|
|
- };
|
|
|
+ /**
|
|
|
+ * Test whether the terminal session is connected.
|
|
|
+ *
|
|
|
+ * #### Notes
|
|
|
+ * This is a read-only property.
|
|
|
+ */
|
|
|
+ get isConnected(): boolean {
|
|
|
+ return this._connected;
|
|
|
+ }
|
|
|
|
|
|
- this._sheet = document.createElement('style');
|
|
|
- this.node.appendChild(this._sheet);
|
|
|
+ /**
|
|
|
+ * Get the name of the terminal session.
|
|
|
+ *
|
|
|
+ * #### Notes
|
|
|
+ * This is a read-only property.
|
|
|
+ */
|
|
|
+ get name(): string {
|
|
|
+ return this._name;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -105,9 +135,6 @@ class TerminalWidget extends Widget {
|
|
|
*/
|
|
|
set fontSize(size: number) {
|
|
|
this._fontSize = size;
|
|
|
- if (!this._intialized) {
|
|
|
- return;
|
|
|
- }
|
|
|
this._term.element.style.fontSize = `${size}px`;
|
|
|
this._snapTermSizing();
|
|
|
}
|
|
@@ -124,9 +151,7 @@ class TerminalWidget extends Widget {
|
|
|
*/
|
|
|
set background(value: string) {
|
|
|
this._background = value;
|
|
|
- if (this._intialized) {
|
|
|
- this.update();
|
|
|
- }
|
|
|
+ this.update();
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -141,9 +166,7 @@ class TerminalWidget extends Widget {
|
|
|
*/
|
|
|
set color(value: string) {
|
|
|
this._color = value;
|
|
|
- if (this._intialized) {
|
|
|
- this.update();
|
|
|
- }
|
|
|
+ this.update();
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -205,6 +228,21 @@ class TerminalWidget extends Widget {
|
|
|
super.dispose();
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Shut down the terminal session.
|
|
|
+ */
|
|
|
+ shutdown(): Promise<void> {
|
|
|
+ let url = utils.urlPathJoin(this._baseUrl, TERMINAL_SERVICE_URL);
|
|
|
+ let ajaxSettings = utils.copy(this._ajaxSettings);
|
|
|
+ ajaxSettings.method = 'DELETE';
|
|
|
+
|
|
|
+ return utils.ajaxRequest(url, ajaxSettings).then(success => {
|
|
|
+ if (success.xhr.status !== 204) {
|
|
|
+ throw new Error('Invalid Response: ' + success.xhr.status);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Process a message sent to the widget.
|
|
|
*
|
|
@@ -277,10 +315,51 @@ class TerminalWidget extends Widget {
|
|
|
sendMessage(this, resize);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Get a name for the terminal from the server.
|
|
|
+ */
|
|
|
+ private _getName(): Promise<string> {
|
|
|
+ let url = utils.urlPathJoin(this._baseUrl, TERMINAL_SERVICE_URL);
|
|
|
+ let ajaxSettings = utils.copy(this._ajaxSettings);
|
|
|
+ ajaxSettings.method = 'POST';
|
|
|
+ ajaxSettings.dataType = 'json';
|
|
|
+
|
|
|
+ return utils.ajaxRequest(url, ajaxSettings).then(success => {
|
|
|
+ if (success.xhr.status !== 200) {
|
|
|
+ throw new Error('Invalid Response: ' + success.xhr.status);
|
|
|
+ }
|
|
|
+ return (success.data as TerminalWidget.IModel).name;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Connect to the websocket.
|
|
|
+ */
|
|
|
+ private _intializeSocket(wsUrl: string): void {
|
|
|
+ let name = this._name;
|
|
|
+ let url = `${wsUrl}terminals/websocket/${name}`;
|
|
|
+ this._ws = new WebSocket(url);
|
|
|
+
|
|
|
+ // Set the default title.
|
|
|
+ this.title.text = `Terminal ${name}`;
|
|
|
+
|
|
|
+ this._ws.onopen = (event: MessageEvent) => {
|
|
|
+ this._connected = true;
|
|
|
+ if (this._dirty) {
|
|
|
+ this._resizeTerminal(-1, -1);
|
|
|
+ }
|
|
|
+ this.connected.emit(void 0);
|
|
|
+ };
|
|
|
+
|
|
|
+ this._ws.onmessage = (event: MessageEvent) => {
|
|
|
+ this._handleWSMessage(event);
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Create the terminal object.
|
|
|
*/
|
|
|
- private _intializeTerm(): void {
|
|
|
+ private _initializeTerm(): void {
|
|
|
this._term.open(this.node);
|
|
|
this._term.element.classList.add(TERMINAL_BODY_CLASS);
|
|
|
|
|
@@ -291,10 +370,6 @@ class TerminalWidget extends Widget {
|
|
|
this._term.on('title', (title: string) => {
|
|
|
this.title.text = title;
|
|
|
});
|
|
|
-
|
|
|
- // Update the font size, which snaps term sizing and resizes the terminal.
|
|
|
- this._intialized = true;
|
|
|
- this.fontSize = this.fontSize;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -334,7 +409,7 @@ class TerminalWidget extends Widget {
|
|
|
* The parent offset dimensions should be `-1` if unknown.
|
|
|
*/
|
|
|
private _resizeTerminal(offsetWidth: number, offsetHeight: number) {
|
|
|
- if (this._rowHeight === -1 || !this.isVisible) {
|
|
|
+ if (this._rowHeight === -1 || !this.isVisible || !this._connected) {
|
|
|
this._dirty = true;
|
|
|
return;
|
|
|
}
|
|
@@ -367,7 +442,10 @@ class TerminalWidget extends Widget {
|
|
|
private _background = '';
|
|
|
private _color = '';
|
|
|
private _box: IBoxSizing = null;
|
|
|
- private _intialized = false;
|
|
|
+ private _connected = false;
|
|
|
+ private _name: string;
|
|
|
+ private _baseUrl: string;
|
|
|
+ private _ajaxSettings: utils.IAjaxSettings = null;
|
|
|
}
|
|
|
|
|
|
|
|
@@ -382,10 +460,25 @@ namespace TerminalWidget {
|
|
|
export
|
|
|
interface IOptions {
|
|
|
/**
|
|
|
- * The base websocket url.
|
|
|
+ * The name of the terminal.
|
|
|
+ */
|
|
|
+ name?: string;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The base url.
|
|
|
*/
|
|
|
baseUrl?: string;
|
|
|
|
|
|
+ /**
|
|
|
+ * The base websocket url.
|
|
|
+ */
|
|
|
+ wsUrl?: string;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The Ajax settings used for server requests.
|
|
|
+ */
|
|
|
+ ajaxSettings?: utils.IAjaxSettings;
|
|
|
+
|
|
|
/**
|
|
|
* The font size of the terminal in pixels.
|
|
|
*/
|
|
@@ -421,6 +514,114 @@ namespace TerminalWidget {
|
|
|
*/
|
|
|
scrollback?: number;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The server model for a terminal widget.
|
|
|
+ */
|
|
|
+ export
|
|
|
+ interface IModel {
|
|
|
+ /**
|
|
|
+ * The name of the terminal session.
|
|
|
+ */
|
|
|
+ name: string;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+/**
|
|
|
+ * A terminal session manager.
|
|
|
+ */
|
|
|
+export
|
|
|
+class TerminalManager {
|
|
|
+ /**
|
|
|
+ * Construct a new terminal manager.
|
|
|
+ */
|
|
|
+ constructor(options: TerminalManager.IOptions) {
|
|
|
+ this._baseUrl = options.baseUrl || utils.getBaseUrl();
|
|
|
+ this._wsUrl = options.wsUrl || utils.getWsUrl(this._baseUrl);
|
|
|
+ this._ajaxSettings = utils.copy(options.ajaxSettings) || {};
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create a new terminal.
|
|
|
+ */
|
|
|
+ createNew(options?: TerminalWidget.IOptions): TerminalWidget {
|
|
|
+ options = options || {};
|
|
|
+ options.baseUrl = options.baseUrl || this._baseUrl;
|
|
|
+ options.wsUrl = options.wsUrl || this._wsUrl;
|
|
|
+ options.ajaxSettings = (
|
|
|
+ options.ajaxSettings || utils.copy(this._ajaxSettings)
|
|
|
+ );
|
|
|
+ return new TerminalWidget(options);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Shut down a terminal session by name.
|
|
|
+ */
|
|
|
+ shutdown(name: string): Promise<void> {
|
|
|
+ let url = utils.urlPathJoin(this._baseUrl, TERMINAL_SERVICE_URL);
|
|
|
+ let ajaxSettings = utils.copy(this._ajaxSettings) || {};
|
|
|
+ ajaxSettings.method = 'DELETE';
|
|
|
+
|
|
|
+ return utils.ajaxRequest(url, ajaxSettings).then(success => {
|
|
|
+ if (success.xhr.status !== 204) {
|
|
|
+ throw new Error('Invalid Response: ' + success.xhr.status);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get the list of models for the terminals running on the server.
|
|
|
+ */
|
|
|
+ listRunning(): Promise<TerminalWidget.IModel[]> {
|
|
|
+ let url = utils.urlPathJoin(this._baseUrl, TERMINAL_SERVICE_URL);
|
|
|
+ let ajaxSettings = utils.copy(this._ajaxSettings) || {};
|
|
|
+ ajaxSettings.method = 'GET';
|
|
|
+ ajaxSettings.dataType = 'json';
|
|
|
+
|
|
|
+ return utils.ajaxRequest(url, ajaxSettings).then(success => {
|
|
|
+ if (success.xhr.status !== 200) {
|
|
|
+ throw new Error('Invalid Response: ' + success.xhr.status);
|
|
|
+ }
|
|
|
+ let data = success.data as TerminalWidget.IModel[];
|
|
|
+ if (!Array.isArray(data)) {
|
|
|
+ throw new Error('Invalid terminal data');
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private _baseUrl = '';
|
|
|
+ private _wsUrl = '';
|
|
|
+ private _ajaxSettings: utils.IAjaxSettings = null;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+/**
|
|
|
+ * The namespace for TerminalManager statics.
|
|
|
+ */
|
|
|
+export
|
|
|
+namespace TerminalManager {
|
|
|
+ /**
|
|
|
+ * The options used to initialize a terminal manager.
|
|
|
+ */
|
|
|
+ export
|
|
|
+ interface IOptions {
|
|
|
+ /**
|
|
|
+ * The base url.
|
|
|
+ */
|
|
|
+ baseUrl?: string;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The base websocket url.
|
|
|
+ */
|
|
|
+ wsUrl?: string;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The Ajax settings used for server requests.
|
|
|
+ */
|
|
|
+ ajaxSettings?: utils.IAjaxSettings;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
|
|
@@ -428,6 +629,12 @@ namespace TerminalWidget {
|
|
|
* A namespace for private data.
|
|
|
*/
|
|
|
namespace Private {
|
|
|
+ /**
|
|
|
+ * A signal emitted when the terminal is fully connected.
|
|
|
+ */
|
|
|
+ export
|
|
|
+ const connectedSignal = new Signal<TerminalWidget, void>();
|
|
|
+
|
|
|
/**
|
|
|
* Get term.js options from ITerminalOptions.
|
|
|
*/
|
|
@@ -470,4 +677,10 @@ namespace Private {
|
|
|
(node.style as any)['white-space'] = 'nowrap';
|
|
|
return node;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * An incrementing counter for ids.
|
|
|
+ */
|
|
|
+ export
|
|
|
+ var id = 0;
|
|
|
}
|