@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/lib/terminal.js CHANGED
@@ -1,101 +1,147 @@
1
- // Copyright (c) Jupyter Development Team.
2
- // Distributed under the terms of the Modified BSD License.
3
1
  import { Signal } from '@lumino/signaling';
4
- import { Server as WebSocketServer } from 'mock-socket';
5
2
  import { Shell } from './shell';
6
- export class Terminal {
3
+ /**
4
+ * An implementation of a terminal interface.
5
+ */
6
+ export class LiteTerminalConnection {
7
7
  /**
8
- * Construct a new Terminal.
8
+ * Construct a new terminal session.
9
9
  */
10
10
  constructor(options) {
11
- this.options = options;
12
- this._disposed = new Signal(this);
13
11
  this._isDisposed = false;
14
- this._running = false;
12
+ this._disposed = new Signal(this);
13
+ this._connectionStatus = 'connecting';
14
+ this._connectionStatusChanged = new Signal(this);
15
+ this._messageReceived = new Signal(this);
16
+ this._name = options.model.name;
17
+ this._serverSettings = options.serverSettings;
18
+ const { baseUrl } = this._serverSettings;
19
+ const { browsingContextId } = options;
15
20
  this._shell = new Shell({
16
21
  mountpoint: '/drive',
17
- driveFsBaseUrl: options.baseUrl,
18
- wasmBaseUrl: options.baseUrl + 'extensions/@jupyterlite/terminal/static/wasm/',
19
- outputCallback: this._outputCallback.bind(this)
22
+ driveFsBaseUrl: baseUrl,
23
+ wasmBaseUrl: baseUrl + 'extensions/@jupyterlite/terminal/static/wasm/',
24
+ outputCallback: this._outputCallback.bind(this),
25
+ browsingContextId
20
26
  });
21
27
  this._shell.disposed.connect(() => this.dispose());
28
+ this._shell.start().then(() => this._updateConnectionStatus('connected'));
22
29
  }
23
- _outputCallback(text) {
24
- if (this._socket) {
25
- const ret = JSON.stringify(['stdout', text]);
26
- this._socket.send(ret);
27
- }
30
+ /**
31
+ * The current connection status of the terminal connection.
32
+ */
33
+ get connectionStatus() {
34
+ return this._connectionStatus;
28
35
  }
36
+ /**
37
+ * A signal emitted when the terminal connection status changes.
38
+ */
39
+ get connectionStatusChanged() {
40
+ return this._connectionStatusChanged;
41
+ }
42
+ /**
43
+ * Dispose of the resources held by the session.
44
+ */
29
45
  dispose() {
30
46
  if (this._isDisposed) {
31
47
  return;
32
48
  }
33
- console.log('Terminal.dispose');
34
49
  this._isDisposed = true;
35
- if (this._socket !== undefined) {
36
- // Disconnect from frontend.
37
- this._socket.send(JSON.stringify(['disconnect']));
38
- this._socket.close();
39
- this._socket = undefined;
40
- }
41
- if (this._server !== undefined) {
42
- this._server.close();
43
- this._server = undefined;
44
- }
45
50
  this._shell.dispose();
46
51
  this._disposed.emit();
52
+ this._updateConnectionStatus('disconnected');
53
+ Signal.clearData(this);
47
54
  }
55
+ /**
56
+ * A signal emitted when the session is disposed.
57
+ */
48
58
  get disposed() {
49
59
  return this._disposed;
50
60
  }
61
+ /**
62
+ * Test whether the session is disposed.
63
+ */
51
64
  get isDisposed() {
52
65
  return this._isDisposed;
53
66
  }
54
67
  /**
55
- * Get the name of the terminal.
68
+ * A signal emitted when a message is received from the server.
69
+ */
70
+ get messageReceived() {
71
+ return this._messageReceived;
72
+ }
73
+ /**
74
+ * Get the model for the terminal session.
75
+ */
76
+ get model() {
77
+ return { name: this._name };
78
+ }
79
+ /**
80
+ * Get the name of the terminal session.
56
81
  */
57
82
  get name() {
58
- return this.options.name;
83
+ return this._name;
59
84
  }
60
- async wsConnect(url) {
61
- console.log('Terminal wsConnect', url);
62
- this._server = new WebSocketServer(url);
63
- this._server.on('connection', async (socket) => {
64
- console.log('Terminal server connection');
65
- if (this._socket !== undefined) {
66
- this._socket.send(JSON.stringify(['disconnect']));
67
- this._socket.close();
68
- this._socket = undefined;
69
- }
70
- this._socket = socket;
71
- socket.on('message', async (message) => {
72
- const data = JSON.parse(message);
73
- //console.log('==> socket message', data);
74
- const message_type = data[0];
75
- const content = data.slice(1);
76
- if (message_type === 'stdin') {
77
- await this._shell.input(content[0]);
78
- }
79
- else if (message_type === 'set_size') {
80
- const rows = content[0];
81
- const columns = content[1];
82
- await this._shell.setSize(rows, columns);
83
- }
84
- });
85
- socket.on('close', () => {
86
- console.log('Terminal socket close');
87
- });
88
- socket.on('error', () => {
89
- console.log('Terminal socket error');
90
- });
91
- // Return handshake.
92
- const res = JSON.stringify(['setup']);
93
- console.log('Terminal returning handshake via socket');
94
- socket.send(res);
95
- if (!this._running) {
96
- this._running = true;
97
- await this._shell.start();
85
+ /**
86
+ * Reconnect to a terminal.
87
+ *
88
+ * #### Notes
89
+ * This may try multiple times to reconnect to a terminal, and will sever
90
+ * any existing connection.
91
+ */
92
+ async reconnect() {
93
+ console.log('==> LiteTerminalConnection.reconnect not implemented');
94
+ }
95
+ /**
96
+ * Send a message to the terminal session.
97
+ *
98
+ * #### Notes
99
+ * If the connection is down, the message will be queued for sending when
100
+ * the connection comes back up.
101
+ */
102
+ send(message) {
103
+ const { content } = message;
104
+ if (content === undefined) {
105
+ return;
106
+ }
107
+ switch (message.type) {
108
+ case 'stdin':
109
+ this._shell.input(content[0]); // async
110
+ break;
111
+ case 'set_size': {
112
+ const rows = content[0];
113
+ const columns = content[1];
114
+ this._shell.setSize(rows, columns); // async
115
+ break;
98
116
  }
99
- });
117
+ }
118
+ }
119
+ /**
120
+ * The server settings for the session.
121
+ */
122
+ get serverSettings() {
123
+ return this._serverSettings;
124
+ }
125
+ /**
126
+ * Shut down the terminal session.
127
+ */
128
+ async shutdown() {
129
+ this.dispose();
130
+ }
131
+ _outputCallback(text) {
132
+ // 'stdout' or 'disconnect' as MessageType.
133
+ // Cockle is not yet using the 'disconnect'.
134
+ this._messageReceived.emit({ type: 'stdout', content: [text] });
135
+ }
136
+ /**
137
+ * Handle connection status changes.
138
+ */
139
+ _updateConnectionStatus(connectionStatus) {
140
+ if (this._connectionStatus === connectionStatus) {
141
+ return;
142
+ }
143
+ this._connectionStatus = connectionStatus;
144
+ // Notify others that the connection status changed.
145
+ this._connectionStatusChanged.emit(connectionStatus);
100
146
  }
101
147
  }
package/lib/worker.js CHANGED
@@ -1,34 +1,36 @@
1
1
  import { expose } from 'comlink';
2
2
  import { BaseShellWorker } from '@jupyterlite/cockle';
3
- import { DriveFS, ServiceWorkerContentsAPI } from '@jupyterlite/contents';
4
- /**
5
- * Custom DriveFS implementation using the service worker.
6
- */
7
- class MyDriveFS extends DriveFS {
8
- createAPI(options) {
9
- return new ServiceWorkerContentsAPI(options.baseUrl, options.driveName, options.mountpoint, options.FS, options.ERRNO_CODES);
10
- }
11
- }
3
+ import { DriveFS } from '@jupyterlite/contents';
12
4
  /**
13
5
  * Shell web worker that uses DriveFS via service worker.
14
6
  * Note that this is not exported as it is accessed from Shell via the filename.
15
7
  */
16
8
  class ShellWorker extends BaseShellWorker {
17
9
  /**
18
- * Initialize the DriveFS to mount an external file system.
10
+ * Initialize the DriveFS to mount an external file system, if available.
19
11
  */
20
- initDriveFS(driveFsBaseUrl, mountpoint, fileSystem) {
21
- console.log('Terminal initDriveFS', driveFsBaseUrl, mountpoint);
22
- const { FS, ERRNO_CODES, PATH } = fileSystem;
23
- const driveFS = new MyDriveFS({
24
- FS,
25
- PATH,
26
- ERRNO_CODES,
27
- baseUrl: driveFsBaseUrl,
28
- driveName: '',
29
- mountpoint
30
- });
31
- FS.mount(driveFS, {}, mountpoint);
12
+ initDriveFS(options) {
13
+ const { browsingContextId, driveFsBaseUrl, fileSystem, mountpoint } = options;
14
+ console.log('Terminal initDriveFS', driveFsBaseUrl, mountpoint, browsingContextId);
15
+ if (mountpoint !== '' &&
16
+ driveFsBaseUrl !== undefined &&
17
+ browsingContextId !== undefined) {
18
+ const { FS, ERRNO_CODES, PATH } = fileSystem;
19
+ const driveFS = new DriveFS({
20
+ FS,
21
+ PATH,
22
+ ERRNO_CODES,
23
+ baseUrl: driveFsBaseUrl,
24
+ driveName: '',
25
+ mountpoint,
26
+ browsingContextId
27
+ });
28
+ FS.mount(driveFS, {}, mountpoint);
29
+ console.log('Terminal connected to shared drive');
30
+ }
31
+ else {
32
+ console.warn('Terminal not connected to shared drive');
33
+ }
32
34
  }
33
35
  }
34
36
  const worker = new ShellWorker();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlite/terminal",
3
- "version": "0.1.6",
3
+ "version": "0.2.0-a0",
4
4
  "description": "A terminal for JupyterLite",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -59,19 +59,19 @@
59
59
  "watch:labextension": "jupyter labextension watch ."
60
60
  },
61
61
  "dependencies": {
62
- "@jupyterlab/coreutils": "^6.3.5",
63
- "@jupyterlab/services": "^7.3.5",
64
- "@jupyterlab/terminal": "^4.3.5",
65
- "@jupyterlab/terminal-extension": "^4.3.5",
66
- "@jupyterlite/cockle": "^0.0.16",
67
- "@jupyterlite/contents": "^0.5.1",
68
- "@jupyterlite/server": "^0.5.1",
62
+ "@jupyterlab/coreutils": "^6.4.2",
63
+ "@jupyterlab/services": "^7.4.2",
64
+ "@jupyterlab/terminal": "^4.4.2",
65
+ "@jupyterlab/terminal-extension": "^4.4.2",
66
+ "@jupyterlite/cockle": "^0.0.19",
67
+ "@jupyterlite/contents": "0.6.0-beta.0",
68
+ "@jupyterlite/server": "0.6.0-beta.0",
69
69
  "@lumino/coreutils": "^2.2.0",
70
70
  "mock-socket": "^9.3.1"
71
71
  },
72
72
  "devDependencies": {
73
- "@jupyterlab/builder": "^4.3.5",
74
- "@jupyterlab/testutils": "^4.3.5",
73
+ "@jupyterlab/builder": "^4.4.2",
74
+ "@jupyterlab/testutils": "^4.4.2",
75
75
  "@types/jest": "^29.2.0",
76
76
  "@types/json-schema": "^7.0.11",
77
77
  "@types/react": "^18.0.26",
@@ -111,9 +111,6 @@
111
111
  "extension": true,
112
112
  "outputDir": "jupyterlite_terminal/labextension"
113
113
  },
114
- "jupyterlite": {
115
- "liteExtension": true
116
- },
117
114
  "eslintIgnore": [
118
115
  "node_modules",
119
116
  "dist",
package/src/index.ts CHANGED
@@ -2,84 +2,58 @@
2
2
  // Distributed under the terms of the Modified BSD License.
3
3
 
4
4
  import {
5
- JupyterLiteServer,
6
- JupyterLiteServerPlugin,
7
- Router
8
- } from '@jupyterlite/server';
5
+ JupyterFrontEnd,
6
+ JupyterFrontEndPlugin
7
+ } from '@jupyterlab/application';
8
+ import {
9
+ ITerminalManager,
10
+ ServiceManagerPlugin,
11
+ Terminal
12
+ } from '@jupyterlab/services';
13
+ import { IServiceWorkerManager } from '@jupyterlite/server';
9
14
 
10
- import { TerminalManager } from './manager';
11
- import { ITerminalManager } from './tokens';
15
+ import { isILiteTerminalManager, LiteTerminalManager } from './manager';
12
16
 
13
17
  /**
14
- * The terminals service plugin.
18
+ * The terminal manager plugin, replacing the JupyterLab terminal manager.
15
19
  */
16
- const terminalsPlugin: JupyterLiteServerPlugin<ITerminalManager> = {
20
+ const terminalManagerPlugin: ServiceManagerPlugin<Terminal.IManager> = {
17
21
  id: '@jupyterlite/terminal:plugin',
18
- description: 'A terminal for JupyterLite',
22
+ description: 'A JupyterLite extension providing a custom terminal manager',
19
23
  autoStart: true,
20
24
  provides: ITerminalManager,
21
- activate: async (app: JupyterLiteServer) => {
25
+ activate: (_: null): Terminal.IManager => {
22
26
  console.log(
23
27
  'JupyterLite extension @jupyterlite/terminal:plugin is activated!'
24
28
  );
25
-
26
- const { serviceManager } = app;
27
- const { serverSettings, terminals } = serviceManager;
28
- console.log('terminals available:', terminals.isAvailable());
29
- console.log('terminals ready:', terminals.isReady); // Not ready
30
- console.log('terminals active:', terminals.isActive);
31
-
32
- // Not sure this is necessary?
33
- await terminals.ready;
34
- console.log('terminals ready after await:', terminals.isReady); // Ready
35
-
36
- return new TerminalManager(serverSettings.wsUrl);
29
+ return new LiteTerminalManager();
37
30
  }
38
31
  };
39
32
 
40
33
  /**
41
- * A plugin providing the routes for the terminals service
34
+ * A plugin that sets the browsingContextId of the terminal manager.
42
35
  */
43
- const terminalsRoutesPlugin: JupyterLiteServerPlugin<void> = {
44
- id: '@jupyterlite/terminal:routes-plugin',
36
+ const browsingContextIdSetter: JupyterFrontEndPlugin<void> = {
37
+ id: '@jupyterlite/terminal:browsing-context-id',
45
38
  autoStart: true,
39
+ optional: [IServiceWorkerManager],
46
40
  requires: [ITerminalManager],
47
- activate: (app: JupyterLiteServer, terminalManager: ITerminalManager) => {
48
- console.log(
49
- 'JupyterLite extension @jupyterlite/terminal:routes-plugin is activated!',
50
- terminalManager
51
- );
52
-
53
- // GET /api/terminals - List the running terminals
54
- app.router.get('/api/terminals', async (req: Router.IRequest) => {
55
- const res = await terminalManager.listRunning();
56
- // Should return last_activity for each too,
57
- return new Response(JSON.stringify(res));
58
- });
59
-
60
- // POST /api/terminals - Start a terminal
61
- app.router.post('/api/terminals', async (req: Router.IRequest) => {
62
- const res = await terminalManager.startNew();
63
- // Should return last_activity too.
64
- return new Response(JSON.stringify(res));
65
- });
66
-
67
- // DELETE /api/terminals/{terminal name} - Delete a terminal
68
- app.router.delete(
69
- '/api/terminals/(.+)',
70
- async (req: Router.IRequest, name: string) => {
71
- const exists = terminalManager.has(name);
72
- if (exists) {
73
- await terminalManager.shutdownTerminal(name);
74
- } else {
75
- const msg = `The terminal session "${name}"" does not exist`;
76
- console.warn(msg);
77
- }
78
-
79
- return new Response(null, { status: exists ? 204 : 404 });
41
+ activate: (
42
+ _: JupyterFrontEnd,
43
+ terminalManager: Terminal.IManager,
44
+ serviceWorkerManager?: IServiceWorkerManager
45
+ ): void => {
46
+ if (serviceWorkerManager !== undefined) {
47
+ if (isILiteTerminalManager(terminalManager)) {
48
+ const { browsingContextId } = serviceWorkerManager;
49
+ terminalManager.browsingContextId = browsingContextId;
50
+ } else {
51
+ console.warn(
52
+ 'Terminal manager does not support setting browsingContextId'
53
+ );
80
54
  }
81
- );
55
+ }
82
56
  }
83
57
  };
84
58
 
85
- export default [terminalsPlugin, terminalsRoutesPlugin];
59
+ export default [terminalManagerPlugin, browsingContextIdSetter];