@jupyterlite/terminal 0.2.0-a0 → 0.2.0
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/README.md +13 -15
- package/lib/client.d.ts +28 -0
- package/lib/client.js +118 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +48 -20
- package/lib/shell.d.ts +2 -0
- package/lib/tokens.d.ts +18 -0
- package/lib/tokens.js +2 -0
- package/lib/worker.js +4 -4
- package/package.json +11 -10
- package/src/client.ts +157 -0
- package/src/index.ts +63 -21
- package/src/shell.ts +4 -0
- package/src/tokens.ts +28 -0
- package/src/worker.ts +4 -10
- package/lib/manager.d.ts +0 -93
- package/lib/manager.js +0 -167
- package/lib/terminal.d.ts +0 -88
- package/lib/terminal.js +0 -147
- package/src/manager.ts +0 -207
- package/src/terminal.ts +0 -195
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A terminal for JupyterLite.
|
|
|
11
11
|
|
|
12
12
|
## Requirements
|
|
13
13
|
|
|
14
|
-
- JupyterLite >= 0.
|
|
14
|
+
- JupyterLite >= 0.6.0
|
|
15
15
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
@@ -46,18 +46,6 @@ Then build a new JupyterLite site:
|
|
|
46
46
|
jupyter lite build
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
## Deployment
|
|
50
|
-
|
|
51
|
-
If you would like to deploy a JupyterLite site with the terminal extension, you will need to configure your server to add the `Cross-Origin-Embedder-Policy` and `Cross-Origin-Opener-Policy` headers.
|
|
52
|
-
|
|
53
|
-
As an example, this repository deploys the JupyterLite terminal to [Vercel](https://vercel.com), using the following files:
|
|
54
|
-
|
|
55
|
-
- `vercel.json`: configure the COOP / COEP server headers
|
|
56
|
-
- `deploy/requirements-deploy.txt`: dependencies for the JupyterLite deployment
|
|
57
|
-
- `deploy/deploy.sh`: script to deploy to Vercel, using micromamba to have full control on the Python versions and isolate the build in a virtual environment
|
|
58
|
-
|
|
59
|
-
For more information, have a look at the JupyterLite documentation: https://jupyterlite.readthedocs.io/
|
|
60
|
-
|
|
61
49
|
## Contributing
|
|
62
50
|
|
|
63
51
|
### Development install
|
|
@@ -97,6 +85,18 @@ jupyter lite build --contents contents
|
|
|
97
85
|
|
|
98
86
|
And serve it either using:
|
|
99
87
|
|
|
88
|
+
```bash
|
|
89
|
+
npx static-handler _output/
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
or:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
jupyter lite serve
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
To enable use of SharedArrayBuffer rather than ServiceWorker for `stdin` you will have to configure your server to add the `Cross-Origin-Embedder-Policy` and `Cross-Origin-Opener-Policy` headers. Do this using either:
|
|
99
|
+
|
|
100
100
|
```bash
|
|
101
101
|
npx static-handler --cors --coop --coep --corp _output/
|
|
102
102
|
```
|
|
@@ -107,8 +107,6 @@ or:
|
|
|
107
107
|
jupyter lite serve --LiteBuildConfig.extra_http_headers=Cross-Origin-Embedder-Policy=require-corp --LiteBuildConfig.extra_http_headers=Cross-Origin-Opener-Policy=same-origin
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
The extra HTTP headers are require to ensure that `SharedArrayBuffer` is available.
|
|
111
|
-
|
|
112
110
|
### Packaging the extension
|
|
113
111
|
|
|
114
112
|
See [RELEASE](RELEASE.md)
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ServerConnection, Terminal } from '@jupyterlab/services';
|
|
2
|
+
import { IExternalCommand, IStdinReply, IStdinRequest } from '@jupyterlite/cockle';
|
|
3
|
+
import { ILiteTerminalAPIClient } from './tokens';
|
|
4
|
+
export declare class LiteTerminalAPIClient implements ILiteTerminalAPIClient {
|
|
5
|
+
constructor(options?: {
|
|
6
|
+
serverSettings?: ServerConnection.ISettings;
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* Set identifier for communicating with service worker.
|
|
10
|
+
*/
|
|
11
|
+
set browsingContextId(browsingContextId: string);
|
|
12
|
+
/**
|
|
13
|
+
* Function that handles stdin requests received from service worker.
|
|
14
|
+
*/
|
|
15
|
+
handleStdin(request: IStdinRequest): Promise<IStdinReply>;
|
|
16
|
+
get isAvailable(): boolean;
|
|
17
|
+
readonly serverSettings: ServerConnection.ISettings;
|
|
18
|
+
startNew(options?: Terminal.ITerminal.IOptions): Promise<Terminal.IModel>;
|
|
19
|
+
listRunning(): Promise<Terminal.IModel[]>;
|
|
20
|
+
registerExternalCommand(options: IExternalCommand.IOptions): void;
|
|
21
|
+
shutdown(name: string): Promise<void>;
|
|
22
|
+
private get _models();
|
|
23
|
+
private _nextAvailableName;
|
|
24
|
+
private _browsingContextId?;
|
|
25
|
+
private _externalCommands;
|
|
26
|
+
private _shellManager;
|
|
27
|
+
private _shells;
|
|
28
|
+
}
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
2
|
+
import { ServerConnection } from '@jupyterlab/services';
|
|
3
|
+
import { ShellManager } from '@jupyterlite/cockle';
|
|
4
|
+
import { Server as WebSocketServer } from 'mock-socket';
|
|
5
|
+
import { Shell } from './shell';
|
|
6
|
+
export class LiteTerminalAPIClient {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
var _a;
|
|
9
|
+
this._externalCommands = [];
|
|
10
|
+
this._shells = new Map();
|
|
11
|
+
this.serverSettings =
|
|
12
|
+
(_a = options.serverSettings) !== null && _a !== void 0 ? _a : ServerConnection.makeSettings();
|
|
13
|
+
this._shellManager = new ShellManager();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Set identifier for communicating with service worker.
|
|
17
|
+
*/
|
|
18
|
+
set browsingContextId(browsingContextId) {
|
|
19
|
+
console.log('LiteTerminalAPIClient browsingContextId', browsingContextId);
|
|
20
|
+
this._browsingContextId = browsingContextId;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Function that handles stdin requests received from service worker.
|
|
24
|
+
*/
|
|
25
|
+
async handleStdin(request) {
|
|
26
|
+
return await this._shellManager.handleStdin(request);
|
|
27
|
+
}
|
|
28
|
+
get isAvailable() {
|
|
29
|
+
const available = String(PageConfig.getOption('terminalsAvailable'));
|
|
30
|
+
return available.toLowerCase() === 'true';
|
|
31
|
+
}
|
|
32
|
+
async startNew(options) {
|
|
33
|
+
var _a;
|
|
34
|
+
// Create shell.
|
|
35
|
+
const name = (_a = options === null || options === void 0 ? void 0 : options.name) !== null && _a !== void 0 ? _a : this._nextAvailableName();
|
|
36
|
+
const { baseUrl, wsUrl } = this.serverSettings;
|
|
37
|
+
const shell = new Shell({
|
|
38
|
+
mountpoint: '/drive',
|
|
39
|
+
baseUrl,
|
|
40
|
+
wasmBaseUrl: URLExt.join(baseUrl, 'extensions/@jupyterlite/terminal/static/wasm/'),
|
|
41
|
+
browsingContextId: this._browsingContextId,
|
|
42
|
+
shellId: name,
|
|
43
|
+
shellManager: this._shellManager,
|
|
44
|
+
outputCallback: text => {
|
|
45
|
+
var _a;
|
|
46
|
+
const msg = JSON.stringify(['stdout', text]);
|
|
47
|
+
(_a = shell.socket) === null || _a === void 0 ? void 0 : _a.send(msg);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
this._shells.set(name, shell);
|
|
51
|
+
for (const externalCommand of this._externalCommands) {
|
|
52
|
+
shell.registerExternalCommand(externalCommand);
|
|
53
|
+
}
|
|
54
|
+
// Hook to connect socket to shell.
|
|
55
|
+
const hook = async (shell, socket) => {
|
|
56
|
+
shell.socket = socket;
|
|
57
|
+
socket.on('message', async (message) => {
|
|
58
|
+
// Message from xtermjs to pass to shell.
|
|
59
|
+
const data = JSON.parse(message);
|
|
60
|
+
const message_type = data[0];
|
|
61
|
+
const content = data.slice(1);
|
|
62
|
+
await shell.ready;
|
|
63
|
+
if (message_type === 'stdin') {
|
|
64
|
+
await shell.input(content[0]);
|
|
65
|
+
}
|
|
66
|
+
else if (message_type === 'set_size') {
|
|
67
|
+
const rows = content[0];
|
|
68
|
+
const columns = content[1];
|
|
69
|
+
await shell.setSize(rows, columns);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// Return handshake.
|
|
73
|
+
const res = JSON.stringify(['setup']);
|
|
74
|
+
console.log('Terminal returning handshake via socket');
|
|
75
|
+
socket.send(res);
|
|
76
|
+
shell.start();
|
|
77
|
+
};
|
|
78
|
+
const url = URLExt.join(wsUrl, 'terminals', 'websocket', name);
|
|
79
|
+
const wsServer = new WebSocketServer(url);
|
|
80
|
+
wsServer.on('connection', (socket) => {
|
|
81
|
+
hook(shell, socket);
|
|
82
|
+
});
|
|
83
|
+
shell.disposed.connect(() => {
|
|
84
|
+
this.shutdown(name);
|
|
85
|
+
wsServer.close();
|
|
86
|
+
});
|
|
87
|
+
return { name };
|
|
88
|
+
}
|
|
89
|
+
async listRunning() {
|
|
90
|
+
return this._models;
|
|
91
|
+
}
|
|
92
|
+
registerExternalCommand(options) {
|
|
93
|
+
this._externalCommands.push(options);
|
|
94
|
+
}
|
|
95
|
+
async shutdown(name) {
|
|
96
|
+
var _a, _b;
|
|
97
|
+
const shell = this._shells.get(name);
|
|
98
|
+
if (shell !== undefined) {
|
|
99
|
+
(_a = shell.socket) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify(['disconnect']));
|
|
100
|
+
(_b = shell.socket) === null || _b === void 0 ? void 0 : _b.close();
|
|
101
|
+
this._shells.delete(name);
|
|
102
|
+
shell.dispose();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
get _models() {
|
|
106
|
+
return Array.from(this._shells.keys(), name => {
|
|
107
|
+
return { name };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
_nextAvailableName() {
|
|
111
|
+
for (let i = 1;; ++i) {
|
|
112
|
+
const name = `${i}`;
|
|
113
|
+
if (!this._shells.has(name)) {
|
|
114
|
+
return name;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
2
|
import { ServiceManagerPlugin, Terminal } from '@jupyterlab/services';
|
|
3
|
-
declare const _default: (ServiceManagerPlugin<Terminal.IManager> | JupyterFrontEndPlugin<void>)[];
|
|
3
|
+
declare const _default: (ServiceManagerPlugin<Terminal.ITerminalAPIClient> | ServiceManagerPlugin<Terminal.IManager> | JupyterFrontEndPlugin<void>)[];
|
|
4
4
|
export default _default;
|
package/lib/index.js
CHANGED
|
@@ -1,39 +1,67 @@
|
|
|
1
1
|
// Copyright (c) Jupyter Development Team.
|
|
2
2
|
// Distributed under the terms of the Modified BSD License.
|
|
3
|
-
import { ITerminalManager } from '@jupyterlab/services';
|
|
3
|
+
import { ITerminalManager, ServerConnection, IServerSettings, TerminalManager } from '@jupyterlab/services';
|
|
4
4
|
import { IServiceWorkerManager } from '@jupyterlite/server';
|
|
5
|
-
import {
|
|
5
|
+
import { WebSocket } from 'mock-socket';
|
|
6
|
+
import { LiteTerminalAPIClient } from './client';
|
|
7
|
+
import { ILiteTerminalAPIClient } from './tokens';
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
9
|
+
* Plugin containing client for in-browser terminals.
|
|
10
|
+
*/
|
|
11
|
+
const terminalClientPlugin = {
|
|
12
|
+
id: '@jupyterlite/terminal:client',
|
|
13
|
+
description: 'The client for Lite terminals',
|
|
14
|
+
autoStart: true,
|
|
15
|
+
provides: ILiteTerminalAPIClient,
|
|
16
|
+
optional: [IServerSettings],
|
|
17
|
+
activate: (_, serverSettings) => {
|
|
18
|
+
return new LiteTerminalAPIClient({
|
|
19
|
+
serverSettings: {
|
|
20
|
+
...ServerConnection.makeSettings(),
|
|
21
|
+
...serverSettings,
|
|
22
|
+
WebSocket
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Plugin containing manager for in-browser terminals.
|
|
8
29
|
*/
|
|
9
30
|
const terminalManagerPlugin = {
|
|
10
|
-
id: '@jupyterlite/terminal:
|
|
31
|
+
id: '@jupyterlite/terminal:manager',
|
|
11
32
|
description: 'A JupyterLite extension providing a custom terminal manager',
|
|
12
33
|
autoStart: true,
|
|
13
34
|
provides: ITerminalManager,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
35
|
+
requires: [ILiteTerminalAPIClient],
|
|
36
|
+
activate: (_, terminalAPIClient) => {
|
|
37
|
+
console.log('JupyterLite extension @jupyterlite/terminal:manager activated');
|
|
38
|
+
return new TerminalManager({
|
|
39
|
+
terminalAPIClient,
|
|
40
|
+
serverSettings: terminalAPIClient.serverSettings
|
|
41
|
+
});
|
|
17
42
|
}
|
|
18
43
|
};
|
|
19
44
|
/**
|
|
20
|
-
*
|
|
45
|
+
* Plugin that connects in-browser terminals and service worker.
|
|
21
46
|
*/
|
|
22
|
-
const
|
|
23
|
-
id: '@jupyterlite/terminal:
|
|
47
|
+
const terminalServiceWorkerPlugin = {
|
|
48
|
+
id: '@jupyterlite/terminal:service-worker',
|
|
24
49
|
autoStart: true,
|
|
50
|
+
requires: [ILiteTerminalAPIClient],
|
|
25
51
|
optional: [IServiceWorkerManager],
|
|
26
|
-
|
|
27
|
-
activate: (_, terminalManager, serviceWorkerManager) => {
|
|
52
|
+
activate: (_, liteTerminalAPIClient, serviceWorkerManager) => {
|
|
28
53
|
if (serviceWorkerManager !== undefined) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
54
|
+
liteTerminalAPIClient.browsingContextId =
|
|
55
|
+
serviceWorkerManager.browsingContextId;
|
|
56
|
+
serviceWorkerManager.registerStdinHandler('terminal', liteTerminalAPIClient.handleStdin.bind(liteTerminalAPIClient));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.warn('Service worker is not available for terminals');
|
|
36
60
|
}
|
|
37
61
|
}
|
|
38
62
|
};
|
|
39
|
-
export default [
|
|
63
|
+
export default [
|
|
64
|
+
terminalClientPlugin,
|
|
65
|
+
terminalManagerPlugin,
|
|
66
|
+
terminalServiceWorkerPlugin
|
|
67
|
+
];
|
package/lib/shell.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseShell, IShell } from '@jupyterlite/cockle';
|
|
2
|
+
import { Client as WebSocketClient } from 'mock-socket';
|
|
2
3
|
/**
|
|
3
4
|
* Shell class that uses web worker that plugs into a DriveFS via the service worker.
|
|
4
5
|
*/
|
|
@@ -13,4 +14,5 @@ export declare class Shell extends BaseShell {
|
|
|
13
14
|
* Load the web worker.
|
|
14
15
|
*/
|
|
15
16
|
protected initWorker(options: IShell.IOptions): Worker;
|
|
17
|
+
socket?: WebSocketClient;
|
|
16
18
|
}
|
package/lib/tokens.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Terminal } from '@jupyterlab/services';
|
|
2
|
+
import { IExternalCommand, IStdinReply, IStdinRequest } from '@jupyterlite/cockle';
|
|
3
|
+
import { Token } from '@lumino/coreutils';
|
|
4
|
+
export declare const ILiteTerminalAPIClient: Token<ILiteTerminalAPIClient>;
|
|
5
|
+
export interface ILiteTerminalAPIClient extends Terminal.ITerminalAPIClient {
|
|
6
|
+
/**
|
|
7
|
+
* Identifier for communicating with service worker.
|
|
8
|
+
*/
|
|
9
|
+
browsingContextId: string;
|
|
10
|
+
/**
|
|
11
|
+
* Function that handles stdin requests received from service worker.
|
|
12
|
+
*/
|
|
13
|
+
handleStdin(request: IStdinRequest): Promise<IStdinReply>;
|
|
14
|
+
/**
|
|
15
|
+
* Register an external command that will be available in all terminals.
|
|
16
|
+
*/
|
|
17
|
+
registerExternalCommand(options: IExternalCommand.IOptions): void;
|
|
18
|
+
}
|
package/lib/tokens.js
ADDED
package/lib/worker.js
CHANGED
|
@@ -10,17 +10,17 @@ class ShellWorker extends BaseShellWorker {
|
|
|
10
10
|
* Initialize the DriveFS to mount an external file system, if available.
|
|
11
11
|
*/
|
|
12
12
|
initDriveFS(options) {
|
|
13
|
-
const {
|
|
14
|
-
console.log('Terminal initDriveFS',
|
|
13
|
+
const { baseUrl, browsingContextId, fileSystem, mountpoint } = options;
|
|
14
|
+
console.log('Terminal initDriveFS', baseUrl, mountpoint, browsingContextId);
|
|
15
15
|
if (mountpoint !== '' &&
|
|
16
|
-
|
|
16
|
+
baseUrl !== undefined &&
|
|
17
17
|
browsingContextId !== undefined) {
|
|
18
18
|
const { FS, ERRNO_CODES, PATH } = fileSystem;
|
|
19
19
|
const driveFS = new DriveFS({
|
|
20
20
|
FS,
|
|
21
21
|
PATH,
|
|
22
22
|
ERRNO_CODES,
|
|
23
|
-
baseUrl
|
|
23
|
+
baseUrl,
|
|
24
24
|
driveName: '',
|
|
25
25
|
mountpoint,
|
|
26
26
|
browsingContextId
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyterlite/terminal",
|
|
3
|
-
"version": "0.2.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A terminal for JupyterLite",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -59,19 +59,17 @@
|
|
|
59
59
|
"watch:labextension": "jupyter labextension watch ."
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@jupyterlab/coreutils": "^6.4.
|
|
63
|
-
"@jupyterlab/services": "^7.4.
|
|
64
|
-
"@
|
|
65
|
-
"@
|
|
66
|
-
"@jupyterlite/
|
|
67
|
-
"@jupyterlite/contents": "0.6.0-beta.0",
|
|
68
|
-
"@jupyterlite/server": "0.6.0-beta.0",
|
|
62
|
+
"@jupyterlab/coreutils": "^6.4.3",
|
|
63
|
+
"@jupyterlab/services": "^7.4.3",
|
|
64
|
+
"@jupyterlite/cockle": "^0.1.0",
|
|
65
|
+
"@jupyterlite/contents": "0.6.0",
|
|
66
|
+
"@jupyterlite/server": "0.6.0",
|
|
69
67
|
"@lumino/coreutils": "^2.2.0",
|
|
70
68
|
"mock-socket": "^9.3.1"
|
|
71
69
|
},
|
|
72
70
|
"devDependencies": {
|
|
73
|
-
"@jupyterlab/builder": "^4.4.
|
|
74
|
-
"@jupyterlab/testutils": "^4.4.
|
|
71
|
+
"@jupyterlab/builder": "^4.4.3",
|
|
72
|
+
"@jupyterlab/testutils": "^4.4.3",
|
|
75
73
|
"@types/jest": "^29.2.0",
|
|
76
74
|
"@types/json-schema": "^7.0.11",
|
|
77
75
|
"@types/react": "^18.0.26",
|
|
@@ -99,6 +97,9 @@
|
|
|
99
97
|
"webpack-cli": "^5.1.4",
|
|
100
98
|
"yjs": "^13.5.0"
|
|
101
99
|
},
|
|
100
|
+
"resolutions": {
|
|
101
|
+
"parse5": "7.2.1"
|
|
102
|
+
},
|
|
102
103
|
"sideEffects": [
|
|
103
104
|
"style/*.css",
|
|
104
105
|
"style/index.js"
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
2
|
+
import { ServerConnection, Terminal } from '@jupyterlab/services';
|
|
3
|
+
import {
|
|
4
|
+
IExternalCommand,
|
|
5
|
+
IShellManager,
|
|
6
|
+
IStdinReply,
|
|
7
|
+
IStdinRequest,
|
|
8
|
+
ShellManager
|
|
9
|
+
} from '@jupyterlite/cockle';
|
|
10
|
+
import { JSONPrimitive } from '@lumino/coreutils';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
Server as WebSocketServer,
|
|
14
|
+
Client as WebSocketClient
|
|
15
|
+
} from 'mock-socket';
|
|
16
|
+
|
|
17
|
+
import { Shell } from './shell';
|
|
18
|
+
import { ILiteTerminalAPIClient } from './tokens';
|
|
19
|
+
|
|
20
|
+
export class LiteTerminalAPIClient implements ILiteTerminalAPIClient {
|
|
21
|
+
constructor(options: { serverSettings?: ServerConnection.ISettings } = {}) {
|
|
22
|
+
this.serverSettings =
|
|
23
|
+
options.serverSettings ?? ServerConnection.makeSettings();
|
|
24
|
+
this._shellManager = new ShellManager();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Set identifier for communicating with service worker.
|
|
29
|
+
*/
|
|
30
|
+
set browsingContextId(browsingContextId: string) {
|
|
31
|
+
console.log('LiteTerminalAPIClient browsingContextId', browsingContextId);
|
|
32
|
+
this._browsingContextId = browsingContextId;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Function that handles stdin requests received from service worker.
|
|
37
|
+
*/
|
|
38
|
+
async handleStdin(request: IStdinRequest): Promise<IStdinReply> {
|
|
39
|
+
return await this._shellManager.handleStdin(request);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get isAvailable(): boolean {
|
|
43
|
+
const available = String(PageConfig.getOption('terminalsAvailable'));
|
|
44
|
+
return available.toLowerCase() === 'true';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
readonly serverSettings: ServerConnection.ISettings;
|
|
48
|
+
|
|
49
|
+
async startNew(
|
|
50
|
+
options?: Terminal.ITerminal.IOptions
|
|
51
|
+
): Promise<Terminal.IModel> {
|
|
52
|
+
// Create shell.
|
|
53
|
+
const name = options?.name ?? this._nextAvailableName();
|
|
54
|
+
const { baseUrl, wsUrl } = this.serverSettings;
|
|
55
|
+
const shell = new Shell({
|
|
56
|
+
mountpoint: '/drive',
|
|
57
|
+
baseUrl,
|
|
58
|
+
wasmBaseUrl: URLExt.join(
|
|
59
|
+
baseUrl,
|
|
60
|
+
'extensions/@jupyterlite/terminal/static/wasm/'
|
|
61
|
+
),
|
|
62
|
+
browsingContextId: this._browsingContextId,
|
|
63
|
+
shellId: name,
|
|
64
|
+
shellManager: this._shellManager,
|
|
65
|
+
outputCallback: text => {
|
|
66
|
+
const msg = JSON.stringify(['stdout', text]);
|
|
67
|
+
shell.socket?.send(msg);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
this._shells.set(name, shell);
|
|
71
|
+
|
|
72
|
+
for (const externalCommand of this._externalCommands) {
|
|
73
|
+
shell.registerExternalCommand(externalCommand);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Hook to connect socket to shell.
|
|
77
|
+
const hook = async (
|
|
78
|
+
shell: Shell,
|
|
79
|
+
socket: WebSocketClient
|
|
80
|
+
): Promise<void> => {
|
|
81
|
+
shell.socket = socket;
|
|
82
|
+
|
|
83
|
+
socket.on('message', async (message: any) => {
|
|
84
|
+
// Message from xtermjs to pass to shell.
|
|
85
|
+
const data = JSON.parse(message) as JSONPrimitive[];
|
|
86
|
+
const message_type = data[0];
|
|
87
|
+
const content = data.slice(1);
|
|
88
|
+
await shell.ready;
|
|
89
|
+
if (message_type === 'stdin') {
|
|
90
|
+
await shell.input(content[0] as string);
|
|
91
|
+
} else if (message_type === 'set_size') {
|
|
92
|
+
const rows = content[0] as number;
|
|
93
|
+
const columns = content[1] as number;
|
|
94
|
+
await shell.setSize(rows, columns);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Return handshake.
|
|
99
|
+
const res = JSON.stringify(['setup']);
|
|
100
|
+
console.log('Terminal returning handshake via socket');
|
|
101
|
+
socket.send(res);
|
|
102
|
+
|
|
103
|
+
shell.start();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const url = URLExt.join(wsUrl, 'terminals', 'websocket', name);
|
|
107
|
+
const wsServer = new WebSocketServer(url);
|
|
108
|
+
wsServer.on('connection', (socket: WebSocketClient): void => {
|
|
109
|
+
hook(shell, socket);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
shell.disposed.connect(() => {
|
|
113
|
+
this.shutdown(name);
|
|
114
|
+
wsServer.close();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return { name };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async listRunning(): Promise<Terminal.IModel[]> {
|
|
121
|
+
return this._models;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
registerExternalCommand(options: IExternalCommand.IOptions): void {
|
|
125
|
+
this._externalCommands.push(options);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async shutdown(name: string): Promise<void> {
|
|
129
|
+
const shell = this._shells.get(name);
|
|
130
|
+
if (shell !== undefined) {
|
|
131
|
+
shell.socket?.send(JSON.stringify(['disconnect']));
|
|
132
|
+
shell.socket?.close();
|
|
133
|
+
this._shells.delete(name);
|
|
134
|
+
shell.dispose();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private get _models(): Terminal.IModel[] {
|
|
139
|
+
return Array.from(this._shells.keys(), name => {
|
|
140
|
+
return { name };
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private _nextAvailableName(): string {
|
|
145
|
+
for (let i = 1; ; ++i) {
|
|
146
|
+
const name = `${i}`;
|
|
147
|
+
if (!this._shells.has(name)) {
|
|
148
|
+
return name;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private _browsingContextId?: string;
|
|
154
|
+
private _externalCommands: IExternalCommand.IOptions[] = [];
|
|
155
|
+
private _shellManager: IShellManager;
|
|
156
|
+
private _shells = new Map<string, Shell>();
|
|
157
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,52 +8,94 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
ITerminalManager,
|
|
10
10
|
ServiceManagerPlugin,
|
|
11
|
-
Terminal
|
|
11
|
+
Terminal,
|
|
12
|
+
ServerConnection,
|
|
13
|
+
IServerSettings,
|
|
14
|
+
TerminalManager
|
|
12
15
|
} from '@jupyterlab/services';
|
|
13
16
|
import { IServiceWorkerManager } from '@jupyterlite/server';
|
|
14
17
|
|
|
15
|
-
import {
|
|
18
|
+
import { WebSocket } from 'mock-socket';
|
|
19
|
+
|
|
20
|
+
import { LiteTerminalAPIClient } from './client';
|
|
21
|
+
import { ILiteTerminalAPIClient } from './tokens';
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
|
-
*
|
|
24
|
+
* Plugin containing client for in-browser terminals.
|
|
25
|
+
*/
|
|
26
|
+
const terminalClientPlugin: ServiceManagerPlugin<Terminal.ITerminalAPIClient> =
|
|
27
|
+
{
|
|
28
|
+
id: '@jupyterlite/terminal:client',
|
|
29
|
+
description: 'The client for Lite terminals',
|
|
30
|
+
autoStart: true,
|
|
31
|
+
provides: ILiteTerminalAPIClient,
|
|
32
|
+
optional: [IServerSettings],
|
|
33
|
+
activate: (
|
|
34
|
+
_: null,
|
|
35
|
+
serverSettings?: ServerConnection.ISettings
|
|
36
|
+
): ILiteTerminalAPIClient => {
|
|
37
|
+
return new LiteTerminalAPIClient({
|
|
38
|
+
serverSettings: {
|
|
39
|
+
...ServerConnection.makeSettings(),
|
|
40
|
+
...serverSettings,
|
|
41
|
+
WebSocket
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Plugin containing manager for in-browser terminals.
|
|
19
49
|
*/
|
|
20
50
|
const terminalManagerPlugin: ServiceManagerPlugin<Terminal.IManager> = {
|
|
21
|
-
id: '@jupyterlite/terminal:
|
|
51
|
+
id: '@jupyterlite/terminal:manager',
|
|
22
52
|
description: 'A JupyterLite extension providing a custom terminal manager',
|
|
23
53
|
autoStart: true,
|
|
24
54
|
provides: ITerminalManager,
|
|
25
|
-
|
|
55
|
+
requires: [ILiteTerminalAPIClient],
|
|
56
|
+
activate: (
|
|
57
|
+
_: null,
|
|
58
|
+
terminalAPIClient: Terminal.ITerminalAPIClient
|
|
59
|
+
): Terminal.IManager => {
|
|
26
60
|
console.log(
|
|
27
|
-
'JupyterLite extension @jupyterlite/terminal:
|
|
61
|
+
'JupyterLite extension @jupyterlite/terminal:manager activated'
|
|
28
62
|
);
|
|
29
|
-
return new
|
|
63
|
+
return new TerminalManager({
|
|
64
|
+
terminalAPIClient,
|
|
65
|
+
serverSettings: terminalAPIClient.serverSettings
|
|
66
|
+
});
|
|
30
67
|
}
|
|
31
68
|
};
|
|
32
69
|
|
|
33
70
|
/**
|
|
34
|
-
*
|
|
71
|
+
* Plugin that connects in-browser terminals and service worker.
|
|
35
72
|
*/
|
|
36
|
-
const
|
|
37
|
-
id: '@jupyterlite/terminal:
|
|
73
|
+
const terminalServiceWorkerPlugin: JupyterFrontEndPlugin<void> = {
|
|
74
|
+
id: '@jupyterlite/terminal:service-worker',
|
|
38
75
|
autoStart: true,
|
|
76
|
+
requires: [ILiteTerminalAPIClient],
|
|
39
77
|
optional: [IServiceWorkerManager],
|
|
40
|
-
requires: [ITerminalManager],
|
|
41
78
|
activate: (
|
|
42
79
|
_: JupyterFrontEnd,
|
|
43
|
-
|
|
80
|
+
liteTerminalAPIClient: ILiteTerminalAPIClient,
|
|
44
81
|
serviceWorkerManager?: IServiceWorkerManager
|
|
45
82
|
): void => {
|
|
46
83
|
if (serviceWorkerManager !== undefined) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
84
|
+
liteTerminalAPIClient.browsingContextId =
|
|
85
|
+
serviceWorkerManager.browsingContextId;
|
|
86
|
+
|
|
87
|
+
serviceWorkerManager.registerStdinHandler(
|
|
88
|
+
'terminal',
|
|
89
|
+
liteTerminalAPIClient.handleStdin.bind(liteTerminalAPIClient)
|
|
90
|
+
);
|
|
91
|
+
} else {
|
|
92
|
+
console.warn('Service worker is not available for terminals');
|
|
55
93
|
}
|
|
56
94
|
}
|
|
57
95
|
};
|
|
58
96
|
|
|
59
|
-
export default [
|
|
97
|
+
export default [
|
|
98
|
+
terminalClientPlugin,
|
|
99
|
+
terminalManagerPlugin,
|
|
100
|
+
terminalServiceWorkerPlugin
|
|
101
|
+
];
|