@jupyterlite/terminal 0.1.6 → 0.2.0-a0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/manager.ts CHANGED
@@ -1,82 +1,207 @@
1
- // Copyright (c) Jupyter Development Team.
2
- // Distributed under the terms of the Modified BSD License.
1
+ import { BaseManager, Terminal, TerminalManager } from '@jupyterlab/services';
2
+ import { ISignal, Signal } from '@lumino/signaling';
3
+ import { LiteTerminalConnection } from './terminal';
3
4
 
4
- import { PageConfig } from '@jupyterlab/coreutils';
5
- import { TerminalAPI } from '@jupyterlab/services';
5
+ /**
6
+ * Interface for Lite terminal manager, supports setting browserContextId.
7
+ */
8
+ interface ILiteTerminalManager extends Terminal.IManager {
9
+ browsingContextId: string;
10
+ }
6
11
 
7
- import { Terminal } from './terminal';
8
- import { ITerminalManager } from './tokens';
12
+ /**
13
+ * Type guard for ILiteTerminalManager.
14
+ */
15
+ export function isILiteTerminalManager(
16
+ obj: Terminal.IManager
17
+ ): obj is ILiteTerminalManager {
18
+ return 'browsingContextId' in obj;
19
+ }
9
20
 
10
21
  /**
11
- * A class to handle requests to /api/terminals.
12
- * Although this looks similar to a JupyterLab TerminalManager, it is really a class that
13
- * implements the terminal REST API.
22
+ * A terminal session manager.
14
23
  */
15
- export class TerminalManager implements ITerminalManager {
24
+ export class LiteTerminalManager
25
+ extends BaseManager
26
+ implements ILiteTerminalManager
27
+ {
28
+ /**
29
+ * Construct a new terminal manager.
30
+ */
31
+ constructor(options: TerminalManager.IOptions = {}) {
32
+ super(options);
33
+
34
+ // Initialize internal data.
35
+ this._ready = (async () => {
36
+ this._isReady = true;
37
+ })();
38
+ }
39
+
40
+ /**
41
+ * Set identifier for communicating with service worker.
42
+ */
43
+ set browsingContextId(browsingContextId: string) {
44
+ console.log('==> LiteTerminalManager browsingContextId', browsingContextId);
45
+ this._browsingContextId = browsingContextId;
46
+ }
47
+
16
48
  /**
17
- * Construct a new TerminalManager object.
49
+ * A signal emitted when there is a connection failure.
18
50
  */
19
- constructor(wsUrl: string) {
20
- this._wsUrl = wsUrl;
21
- console.log('==> TerminalManager.constructor', this._wsUrl);
51
+ get connectionFailure(): ISignal<this, Error> {
52
+ return this._connectionFailure;
53
+ }
54
+
55
+ /*
56
+ * Connect to a running terminal.
57
+ *
58
+ * @param options - The options used to connect to the terminal.
59
+ *
60
+ * @returns The new terminal connection instance.
61
+ *
62
+ * #### Notes
63
+ * The manager `serverSettings` will be used.
64
+ */
65
+ connectTo(
66
+ options: Omit<Terminal.ITerminalConnection.IOptions, 'serverSettings'>
67
+ ): Terminal.ITerminalConnection {
68
+ const { model } = options;
69
+ const { name } = model;
70
+ console.log('==> LiteTerminalManager.connectTo', name);
71
+ const { serverSettings } = this;
72
+
73
+ const terminal = new LiteTerminalConnection({
74
+ browsingContextId: this._browsingContextId,
75
+ model,
76
+ serverSettings
77
+ });
78
+ terminal.disposed.connect(() => this.shutdown(name));
79
+ return terminal;
22
80
  }
23
81
 
24
82
  /**
25
- * Return whether the named terminal exists.
83
+ * Whether the terminal service is available.
26
84
  */
27
- has(name: string): boolean {
28
- return this._terminals.has(name);
85
+ isAvailable(): boolean {
86
+ return true;
29
87
  }
30
88
 
31
89
  /**
32
- * List the running terminals.
90
+ * Test whether the manager is ready.
33
91
  */
34
- async listRunning(): Promise<TerminalAPI.IModel[]> {
35
- const ret = [...this._terminals.values()].map(terminal => ({
36
- name: terminal.name
37
- }));
38
- return ret;
92
+ get isReady(): boolean {
93
+ return this._isReady;
39
94
  }
40
95
 
41
96
  /**
42
- * Shutdown a terminal by name.
97
+ * A promise that fulfills when the manager is ready.
43
98
  */
44
- async shutdownTerminal(name: string): Promise<void> {
45
- const terminal = this._terminals.get(name);
99
+ get ready(): Promise<void> {
100
+ return this._ready;
101
+ }
102
+
103
+ /**
104
+ * Force a refresh of the running terminals.
105
+ *
106
+ * @returns A promise that with the list of running terminals.
107
+ *
108
+ * #### Notes
109
+ * This is intended to be called only in response to a user action,
110
+ * since the manager maintains its internal state.
111
+ */
112
+ async refreshRunning(): Promise<void> {
113
+ this._runningChanged.emit(this._models);
114
+ }
115
+
116
+ /**
117
+ * Create an iterator over the most recent running terminals.
118
+ *
119
+ * @returns A new iterator over the running terminals.
120
+ */
121
+ running(): IterableIterator<Terminal.IModel> {
122
+ return this._models[Symbol.iterator]();
123
+ }
124
+
125
+ /**
126
+ * A signal emitted when the running terminals change.
127
+ */
128
+ get runningChanged(): ISignal<this, Terminal.IModel[]> {
129
+ return this._runningChanged;
130
+ }
131
+
132
+ /**
133
+ * Shut down a terminal session by name.
134
+ */
135
+ async shutdown(name: string): Promise<void> {
136
+ const terminal = this._terminalConnections.get(name);
46
137
  if (terminal !== undefined) {
47
- console.log('==> TerminalManager.shutdownTerminal', name);
48
- this._terminals.delete(name);
138
+ this._terminalConnections.delete(name);
49
139
  terminal.dispose();
140
+ this.refreshRunning();
50
141
  }
51
142
  }
52
143
 
53
144
  /**
54
- * Start a new kernel.
145
+ * Shut down all terminal sessions.
146
+ *
147
+ * @returns A promise that resolves when all of the sessions are shut down.
55
148
  */
56
- async startNew(): Promise<TerminalAPI.IModel> {
57
- const name = this._nextAvailableName();
58
- console.log('==> TerminalManager.startNew', name);
59
- const baseUrl = PageConfig.getBaseUrl();
60
- const terminal = new Terminal({ name, baseUrl });
61
- this._terminals.set(name, terminal);
149
+ async shutdownAll(): Promise<void> {
150
+ await Promise.all(this._models.map(model => this.shutdown(model.name)));
151
+ this.refreshRunning();
152
+ }
62
153
 
63
- terminal.disposed.connect(() => this.shutdownTerminal(name));
154
+ /**
155
+ * Create a new terminal session.
156
+ *
157
+ * @param options - The options used to create the terminal.
158
+ *
159
+ * @returns A promise that resolves with the terminal connection instance.
160
+ *
161
+ * #### Notes
162
+ * The manager `serverSettings` will be used unless overridden in the
163
+ * options.
164
+ */
165
+ async startNew(
166
+ options: Terminal.ITerminal.IOptions
167
+ ): Promise<Terminal.ITerminalConnection> {
168
+ const name = options.name ?? this._nextAvailableName();
169
+ const model: Terminal.IModel = { name };
170
+ const { serverSettings } = this;
64
171
 
65
- const url = `${this._wsUrl}terminals/websocket/${name}`;
66
- await terminal.wsConnect(url);
172
+ const terminal = new LiteTerminalConnection({
173
+ browsingContextId: this._browsingContextId,
174
+ model,
175
+ serverSettings
176
+ });
177
+ terminal.disposed.connect(() => this.shutdown(name));
178
+ this._terminalConnections.set(name, terminal);
179
+ await this.refreshRunning();
180
+ return terminal;
181
+ }
67
182
 
68
- return { name };
183
+ private get _models(): Terminal.IModel[] {
184
+ return Array.from(this._terminalConnections, ([name, value]) => {
185
+ return { name };
186
+ });
69
187
  }
70
188
 
71
189
  private _nextAvailableName(): string {
72
190
  for (let i = 1; ; ++i) {
73
191
  const name = `${i}`;
74
- if (!this._terminals.has(name)) {
192
+ if (!this._terminalConnections.has(name)) {
75
193
  return name;
76
194
  }
77
195
  }
78
196
  }
79
197
 
80
- private _wsUrl: string;
81
- private _terminals: Map<string, Terminal> = new Map();
198
+ private _browsingContextId?: string;
199
+ private _connectionFailure = new Signal<this, Error>(this);
200
+ private _isReady = false;
201
+ private _ready: Promise<void>;
202
+ private _runningChanged = new Signal<this, Terminal.IModel[]>(this);
203
+ private _terminalConnections = new Map<
204
+ string,
205
+ Terminal.ITerminalConnection
206
+ >();
82
207
  }
package/src/terminal.ts CHANGED
@@ -1,131 +1,195 @@
1
- // Copyright (c) Jupyter Development Team.
2
- // Distributed under the terms of the Modified BSD License.
3
-
4
- import { IShell } from '@jupyterlite/cockle';
5
- import { JSONPrimitive } from '@lumino/coreutils';
1
+ import { ServerConnection, Terminal } from '@jupyterlab/services';
6
2
  import { ISignal, Signal } from '@lumino/signaling';
7
-
8
- import {
9
- Server as WebSocketServer,
10
- Client as WebSocketClient
11
- } from 'mock-socket';
12
-
13
3
  import { Shell } from './shell';
14
- import { ITerminal } from './tokens';
4
+ import { IShell } from '@jupyterlite/cockle';
15
5
 
16
- export class Terminal implements ITerminal {
6
+ /**
7
+ * An implementation of a terminal interface.
8
+ */
9
+ export class LiteTerminalConnection implements Terminal.ITerminalConnection {
17
10
  /**
18
- * Construct a new Terminal.
11
+ * Construct a new terminal session.
19
12
  */
20
- constructor(readonly options: ITerminal.IOptions) {
13
+ constructor(options: LiteTerminalConnection.IOptions) {
14
+ this._name = options.model.name;
15
+ this._serverSettings = options.serverSettings!;
16
+ const { baseUrl } = this._serverSettings;
17
+ const { browsingContextId } = options;
18
+
21
19
  this._shell = new Shell({
22
20
  mountpoint: '/drive',
23
- driveFsBaseUrl: options.baseUrl,
24
- wasmBaseUrl:
25
- options.baseUrl + 'extensions/@jupyterlite/terminal/static/wasm/',
26
- outputCallback: this._outputCallback.bind(this)
21
+ driveFsBaseUrl: baseUrl,
22
+ wasmBaseUrl: baseUrl + 'extensions/@jupyterlite/terminal/static/wasm/',
23
+ outputCallback: this._outputCallback.bind(this),
24
+ browsingContextId
27
25
  });
28
26
  this._shell.disposed.connect(() => this.dispose());
27
+
28
+ this._shell.start().then(() => this._updateConnectionStatus('connected'));
29
29
  }
30
30
 
31
- private _outputCallback(text: string): void {
32
- if (this._socket) {
33
- const ret = JSON.stringify(['stdout', text]);
34
- this._socket.send(ret);
35
- }
31
+ /**
32
+ * The current connection status of the terminal connection.
33
+ */
34
+ get connectionStatus(): Terminal.ConnectionStatus {
35
+ return this._connectionStatus;
36
+ }
37
+
38
+ /**
39
+ * A signal emitted when the terminal connection status changes.
40
+ */
41
+ get connectionStatusChanged(): ISignal<this, Terminal.ConnectionStatus> {
42
+ return this._connectionStatusChanged;
36
43
  }
37
44
 
45
+ /**
46
+ * Dispose of the resources held by the session.
47
+ */
38
48
  dispose(): void {
39
49
  if (this._isDisposed) {
40
50
  return;
41
51
  }
42
52
 
43
- console.log('Terminal.dispose');
44
53
  this._isDisposed = true;
45
-
46
- if (this._socket !== undefined) {
47
- // Disconnect from frontend.
48
- this._socket.send(JSON.stringify(['disconnect']));
49
- this._socket.close();
50
- this._socket = undefined;
51
- }
52
-
53
- if (this._server !== undefined) {
54
- this._server.close();
55
- this._server = undefined;
56
- }
57
-
58
54
  this._shell.dispose();
59
55
  this._disposed.emit();
56
+
57
+ this._updateConnectionStatus('disconnected');
58
+
59
+ Signal.clearData(this);
60
60
  }
61
61
 
62
+ /**
63
+ * A signal emitted when the session is disposed.
64
+ */
62
65
  get disposed(): ISignal<this, void> {
63
66
  return this._disposed;
64
67
  }
65
68
 
69
+ /**
70
+ * Test whether the session is disposed.
71
+ */
66
72
  get isDisposed(): boolean {
67
73
  return this._isDisposed;
68
74
  }
69
75
 
70
76
  /**
71
- * Get the name of the terminal.
77
+ * A signal emitted when a message is received from the server.
78
+ */
79
+ get messageReceived(): ISignal<
80
+ Terminal.ITerminalConnection,
81
+ Terminal.IMessage
82
+ > {
83
+ return this._messageReceived;
84
+ }
85
+
86
+ /**
87
+ * Get the model for the terminal session.
88
+ */
89
+ get model(): Terminal.IModel {
90
+ return { name: this._name };
91
+ }
92
+
93
+ /**
94
+ * Get the name of the terminal session.
72
95
  */
73
96
  get name(): string {
74
- return this.options.name;
97
+ return this._name;
75
98
  }
76
99
 
77
- async wsConnect(url: string) {
78
- console.log('Terminal wsConnect', url);
79
- this._server = new WebSocketServer(url);
100
+ /**
101
+ * Reconnect to a terminal.
102
+ *
103
+ * #### Notes
104
+ * This may try multiple times to reconnect to a terminal, and will sever
105
+ * any existing connection.
106
+ */
107
+ async reconnect(): Promise<void> {
108
+ console.log('==> LiteTerminalConnection.reconnect not implemented');
109
+ }
80
110
 
81
- this._server.on('connection', async (socket: WebSocketClient) => {
82
- console.log('Terminal server connection');
83
- if (this._socket !== undefined) {
84
- this._socket.send(JSON.stringify(['disconnect']));
85
- this._socket.close();
86
- this._socket = undefined;
87
- }
88
- this._socket = socket;
89
-
90
- socket.on('message', async (message: any) => {
91
- const data = JSON.parse(message) as JSONPrimitive[];
92
- //console.log('==> socket message', data);
93
- const message_type = data[0];
94
- const content = data.slice(1);
95
-
96
- if (message_type === 'stdin') {
97
- await this._shell.input(content[0] as string);
98
- } else if (message_type === 'set_size') {
99
- const rows = content[0] as number;
100
- const columns = content[1] as number;
101
- await this._shell.setSize(rows, columns);
102
- }
103
- });
104
-
105
- socket.on('close', () => {
106
- console.log('Terminal socket close');
107
- });
108
-
109
- socket.on('error', () => {
110
- console.log('Terminal socket error');
111
- });
112
-
113
- // Return handshake.
114
- const res = JSON.stringify(['setup']);
115
- console.log('Terminal returning handshake via socket');
116
- socket.send(res);
117
-
118
- if (!this._running) {
119
- this._running = true;
120
- await this._shell.start();
111
+ /**
112
+ * Send a message to the terminal session.
113
+ *
114
+ * #### Notes
115
+ * If the connection is down, the message will be queued for sending when
116
+ * the connection comes back up.
117
+ */
118
+ send(message: Terminal.IMessage): void {
119
+ const { content } = message;
120
+ if (content === undefined) {
121
+ return;
122
+ }
123
+
124
+ switch (message.type) {
125
+ case 'stdin':
126
+ this._shell.input(content[0] as string); // async
127
+ break;
128
+ case 'set_size': {
129
+ const rows = content[0] as number;
130
+ const columns = content[1] as number;
131
+ this._shell.setSize(rows, columns); // async
132
+ break;
121
133
  }
122
- });
134
+ }
135
+ }
136
+
137
+ /**
138
+ * The server settings for the session.
139
+ */
140
+ get serverSettings(): ServerConnection.ISettings {
141
+ return this._serverSettings;
142
+ }
143
+
144
+ /**
145
+ * Shut down the terminal session.
146
+ */
147
+ async shutdown(): Promise<void> {
148
+ this.dispose();
149
+ }
150
+
151
+ private _outputCallback(text: string): void {
152
+ // 'stdout' or 'disconnect' as MessageType.
153
+ // Cockle is not yet using the 'disconnect'.
154
+ this._messageReceived.emit({ type: 'stdout', content: [text] });
155
+ }
156
+
157
+ /**
158
+ * Handle connection status changes.
159
+ */
160
+ private _updateConnectionStatus(
161
+ connectionStatus: Terminal.ConnectionStatus
162
+ ): void {
163
+ if (this._connectionStatus === connectionStatus) {
164
+ return;
165
+ }
166
+
167
+ this._connectionStatus = connectionStatus;
168
+
169
+ // Notify others that the connection status changed.
170
+ this._connectionStatusChanged.emit(connectionStatus);
123
171
  }
124
172
 
125
- private _disposed = new Signal<this, void>(this);
126
173
  private _isDisposed = false;
127
- private _server?: WebSocketServer;
128
- private _socket?: WebSocketClient;
174
+ private _disposed = new Signal<this, void>(this);
175
+
176
+ private _name: string;
177
+ private _serverSettings: ServerConnection.ISettings;
178
+ private _connectionStatus: Terminal.ConnectionStatus = 'connecting';
179
+ private _connectionStatusChanged = new Signal<
180
+ this,
181
+ Terminal.ConnectionStatus
182
+ >(this);
183
+ private _messageReceived = new Signal<this, Terminal.IMessage>(this);
184
+
129
185
  private _shell: IShell;
130
- private _running = false;
186
+ }
187
+
188
+ export namespace LiteTerminalConnection {
189
+ export interface IOptions extends Terminal.ITerminalConnection.IOptions {
190
+ /**
191
+ * The ID of the browsing context where the request originated.
192
+ */
193
+ browsingContextId?: string;
194
+ }
131
195
  }
package/src/worker.ts CHANGED
@@ -1,26 +1,7 @@
1
1
  import { expose } from 'comlink';
2
2
 
3
- import { BaseShellWorker, IFileSystem } from '@jupyterlite/cockle';
4
- import {
5
- ContentsAPI,
6
- DriveFS,
7
- ServiceWorkerContentsAPI
8
- } from '@jupyterlite/contents';
9
-
10
- /**
11
- * Custom DriveFS implementation using the service worker.
12
- */
13
- class MyDriveFS extends DriveFS {
14
- createAPI(options: DriveFS.IOptions): ContentsAPI {
15
- return new ServiceWorkerContentsAPI(
16
- options.baseUrl,
17
- options.driveName,
18
- options.mountpoint,
19
- options.FS,
20
- options.ERRNO_CODES
21
- );
22
- }
23
- }
3
+ import { BaseShellWorker, IDriveFSOptions } from '@jupyterlite/cockle';
4
+ import { DriveFS } from '@jupyterlite/contents';
24
5
 
25
6
  /**
26
7
  * Shell web worker that uses DriveFS via service worker.
@@ -28,24 +9,37 @@ class MyDriveFS extends DriveFS {
28
9
  */
29
10
  class ShellWorker extends BaseShellWorker {
30
11
  /**
31
- * Initialize the DriveFS to mount an external file system.
12
+ * Initialize the DriveFS to mount an external file system, if available.
32
13
  */
33
- protected override initDriveFS(
34
- driveFsBaseUrl: string,
35
- mountpoint: string,
36
- fileSystem: IFileSystem
37
- ): void {
38
- console.log('Terminal initDriveFS', driveFsBaseUrl, mountpoint);
39
- const { FS, ERRNO_CODES, PATH } = fileSystem;
40
- const driveFS = new MyDriveFS({
41
- FS,
42
- PATH,
43
- ERRNO_CODES,
44
- baseUrl: driveFsBaseUrl,
45
- driveName: '',
46
- mountpoint
47
- });
48
- FS.mount(driveFS, {}, mountpoint);
14
+ protected override initDriveFS(options: IDriveFSOptions): void {
15
+ const { browsingContextId, driveFsBaseUrl, fileSystem, mountpoint } =
16
+ options;
17
+ console.log(
18
+ 'Terminal initDriveFS',
19
+ driveFsBaseUrl,
20
+ mountpoint,
21
+ browsingContextId
22
+ );
23
+ if (
24
+ mountpoint !== '' &&
25
+ driveFsBaseUrl !== undefined &&
26
+ browsingContextId !== undefined
27
+ ) {
28
+ const { FS, ERRNO_CODES, PATH } = fileSystem;
29
+ const driveFS = new DriveFS({
30
+ FS,
31
+ PATH,
32
+ ERRNO_CODES,
33
+ baseUrl: driveFsBaseUrl,
34
+ driveName: '',
35
+ mountpoint,
36
+ browsingContextId
37
+ });
38
+ FS.mount(driveFS, {}, mountpoint);
39
+ console.log('Terminal connected to shared drive');
40
+ } else {
41
+ console.warn('Terminal not connected to shared drive');
42
+ }
49
43
  }
50
44
  }
51
45
 
package/lib/tokens.d.ts DELETED
@@ -1,52 +0,0 @@
1
- import { TerminalAPI } from '@jupyterlab/services';
2
- import { Token } from '@lumino/coreutils';
3
- import { IObservableDisposable } from '@lumino/disposable';
4
- /**
5
- * The token for the Terminals service.
6
- */
7
- export declare const ITerminalManager: Token<ITerminalManager>;
8
- /**
9
- * An interface for the TerminalManager service.
10
- */
11
- export interface ITerminalManager {
12
- /**
13
- * Return whether the named terminal exists.
14
- */
15
- has(name: string): boolean;
16
- /**
17
- * List the running terminals.
18
- */
19
- listRunning: () => Promise<TerminalAPI.IModel[]>;
20
- /**
21
- * Shutdown a terminal by name.
22
- */
23
- shutdownTerminal: (name: string) => Promise<void>;
24
- /**
25
- * Start a new kernel.
26
- */
27
- startNew: () => Promise<TerminalAPI.IModel>;
28
- }
29
- /**
30
- * An interface for a server-side terminal running in the browser.
31
- */
32
- export interface ITerminal extends IObservableDisposable {
33
- /**
34
- * The name of the server-side terminal.
35
- */
36
- readonly name: string;
37
- }
38
- /**
39
- * A namespace for ITerminal statics.
40
- */
41
- export declare namespace ITerminal {
42
- /**
43
- * The instantiation options for an ITerminal.
44
- */
45
- interface IOptions {
46
- /**
47
- * The name of the terminal.
48
- */
49
- name: string;
50
- baseUrl: string;
51
- }
52
- }
package/lib/tokens.js DELETED
@@ -1,7 +0,0 @@
1
- // Copyright (c) Jupyter Development Team.
2
- // Distributed under the terms of the Modified BSD License.
3
- import { Token } from '@lumino/coreutils';
4
- /**
5
- * The token for the Terminals service.
6
- */
7
- export const ITerminalManager = new Token('@jupyterlite/terminal:ITerminalManager');