@jupyterlite/terminal 0.1.3 → 0.1.5
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 +4 -3
- package/lib/index.d.ts +2 -2
- package/lib/index.js +22 -10
- package/lib/manager.d.ts +32 -0
- package/lib/manager.js +67 -0
- package/lib/terminal.d.ts +8 -0
- package/lib/terminal.js +45 -7
- package/lib/tokens.d.ts +14 -5
- package/lib/tokens.js +1 -1
- package/package.json +10 -10
- package/src/index.ts +28 -12
- package/src/manager.ts +82 -0
- package/src/terminal.ts +54 -9
- package/src/tokens.ts +17 -6
- package/lib/terminals.d.ts +0 -22
- package/lib/terminals.js +0 -48
- package/src/terminals.ts +0 -60
package/README.md
CHANGED
|
@@ -53,8 +53,8 @@ If you would like to deploy a JupyterLite site with the terminal extension, you
|
|
|
53
53
|
As an example, this repository deploys the JupyterLite terminal to [Vercel](https://vercel.com), using the following files:
|
|
54
54
|
|
|
55
55
|
- `vercel.json`: configure the COOP / COEP server headers
|
|
56
|
-
- `requirements-deploy.txt`: dependencies for the JupyterLite deployment
|
|
57
|
-
- `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
|
|
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
58
|
|
|
59
59
|
For more information, have a look at the JupyterLite documentation: https://jupyterlite.readthedocs.io/
|
|
60
60
|
|
|
@@ -91,7 +91,8 @@ jupyter lab
|
|
|
91
91
|
Then build a JupyterLite distribution with the extension installed:
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
|
-
|
|
94
|
+
cd deploy
|
|
95
|
+
jupyter lite build --contents contents
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
And serve it either using:
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { JupyterLiteServerPlugin } from '@jupyterlite/server';
|
|
2
|
-
import {
|
|
3
|
-
declare const _default: (JupyterLiteServerPlugin<
|
|
2
|
+
import { ITerminalManager } from './tokens';
|
|
3
|
+
declare const _default: (JupyterLiteServerPlugin<ITerminalManager> | JupyterLiteServerPlugin<void>)[];
|
|
4
4
|
export default _default;
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Jupyter Development Team.
|
|
2
2
|
// Distributed under the terms of the Modified BSD License.
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { TerminalManager } from './manager';
|
|
4
|
+
import { ITerminalManager } from './tokens';
|
|
5
5
|
/**
|
|
6
6
|
* The terminals service plugin.
|
|
7
7
|
*/
|
|
@@ -9,9 +9,9 @@ const terminalsPlugin = {
|
|
|
9
9
|
id: '@jupyterlite/terminal:plugin',
|
|
10
10
|
description: 'A terminal for JupyterLite',
|
|
11
11
|
autoStart: true,
|
|
12
|
-
provides:
|
|
12
|
+
provides: ITerminalManager,
|
|
13
13
|
activate: async (app) => {
|
|
14
|
-
console.log('
|
|
14
|
+
console.log('JupyterLite extension @jupyterlite/terminal:plugin is activated!');
|
|
15
15
|
const { serviceManager } = app;
|
|
16
16
|
const { serverSettings, terminals } = serviceManager;
|
|
17
17
|
console.log('terminals available:', terminals.isAvailable());
|
|
@@ -20,7 +20,7 @@ const terminalsPlugin = {
|
|
|
20
20
|
// Not sure this is necessary?
|
|
21
21
|
await terminals.ready;
|
|
22
22
|
console.log('terminals ready after await:', terminals.isReady); // Ready
|
|
23
|
-
return new
|
|
23
|
+
return new TerminalManager(serverSettings.wsUrl);
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
/**
|
|
@@ -29,21 +29,33 @@ const terminalsPlugin = {
|
|
|
29
29
|
const terminalsRoutesPlugin = {
|
|
30
30
|
id: '@jupyterlite/terminal:routes-plugin',
|
|
31
31
|
autoStart: true,
|
|
32
|
-
requires: [
|
|
33
|
-
activate: (app,
|
|
34
|
-
console.log('
|
|
32
|
+
requires: [ITerminalManager],
|
|
33
|
+
activate: (app, terminalManager) => {
|
|
34
|
+
console.log('JupyterLite extension @jupyterlite/terminal:routes-plugin is activated!', terminalManager);
|
|
35
35
|
// GET /api/terminals - List the running terminals
|
|
36
36
|
app.router.get('/api/terminals', async (req) => {
|
|
37
|
-
const res = await
|
|
37
|
+
const res = await terminalManager.listRunning();
|
|
38
38
|
// Should return last_activity for each too,
|
|
39
39
|
return new Response(JSON.stringify(res));
|
|
40
40
|
});
|
|
41
41
|
// POST /api/terminals - Start a terminal
|
|
42
42
|
app.router.post('/api/terminals', async (req) => {
|
|
43
|
-
const res = await
|
|
43
|
+
const res = await terminalManager.startNew();
|
|
44
44
|
// Should return last_activity too.
|
|
45
45
|
return new Response(JSON.stringify(res));
|
|
46
46
|
});
|
|
47
|
+
// DELETE /api/terminals/{terminal name} - Delete a terminal
|
|
48
|
+
app.router.delete('/api/terminals/(.+)', async (req, name) => {
|
|
49
|
+
const exists = terminalManager.has(name);
|
|
50
|
+
if (exists) {
|
|
51
|
+
await terminalManager.shutdownTerminal(name);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const msg = `The terminal session "${name}"" does not exist`;
|
|
55
|
+
console.warn(msg);
|
|
56
|
+
}
|
|
57
|
+
return new Response(null, { status: exists ? 204 : 404 });
|
|
58
|
+
});
|
|
47
59
|
}
|
|
48
60
|
};
|
|
49
61
|
export default [terminalsPlugin, terminalsRoutesPlugin];
|
package/lib/manager.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { TerminalAPI } from '@jupyterlab/services';
|
|
2
|
+
import { ITerminalManager } from './tokens';
|
|
3
|
+
/**
|
|
4
|
+
* A class to handle requests to /api/terminals.
|
|
5
|
+
* Although this looks similar to a JupyterLab TerminalManager, it is really a class that
|
|
6
|
+
* implements the terminal REST API.
|
|
7
|
+
*/
|
|
8
|
+
export declare class TerminalManager implements ITerminalManager {
|
|
9
|
+
/**
|
|
10
|
+
* Construct a new TerminalManager object.
|
|
11
|
+
*/
|
|
12
|
+
constructor(wsUrl: string);
|
|
13
|
+
/**
|
|
14
|
+
* Return whether the named terminal exists.
|
|
15
|
+
*/
|
|
16
|
+
has(name: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* List the running terminals.
|
|
19
|
+
*/
|
|
20
|
+
listRunning(): Promise<TerminalAPI.IModel[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Shutdown a terminal by name.
|
|
23
|
+
*/
|
|
24
|
+
shutdownTerminal(name: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Start a new kernel.
|
|
27
|
+
*/
|
|
28
|
+
startNew(): Promise<TerminalAPI.IModel>;
|
|
29
|
+
private _nextAvailableName;
|
|
30
|
+
private _wsUrl;
|
|
31
|
+
private _terminals;
|
|
32
|
+
}
|
package/lib/manager.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
import { PageConfig } from '@jupyterlab/coreutils';
|
|
4
|
+
import { Terminal } from './terminal';
|
|
5
|
+
/**
|
|
6
|
+
* A class to handle requests to /api/terminals.
|
|
7
|
+
* Although this looks similar to a JupyterLab TerminalManager, it is really a class that
|
|
8
|
+
* implements the terminal REST API.
|
|
9
|
+
*/
|
|
10
|
+
export class TerminalManager {
|
|
11
|
+
/**
|
|
12
|
+
* Construct a new TerminalManager object.
|
|
13
|
+
*/
|
|
14
|
+
constructor(wsUrl) {
|
|
15
|
+
this._terminals = new Map();
|
|
16
|
+
this._wsUrl = wsUrl;
|
|
17
|
+
console.log('==> TerminalManager.constructor', this._wsUrl);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Return whether the named terminal exists.
|
|
21
|
+
*/
|
|
22
|
+
has(name) {
|
|
23
|
+
return this._terminals.has(name);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* List the running terminals.
|
|
27
|
+
*/
|
|
28
|
+
async listRunning() {
|
|
29
|
+
const ret = [...this._terminals.values()].map(terminal => ({
|
|
30
|
+
name: terminal.name
|
|
31
|
+
}));
|
|
32
|
+
return ret;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Shutdown a terminal by name.
|
|
36
|
+
*/
|
|
37
|
+
async shutdownTerminal(name) {
|
|
38
|
+
const terminal = this._terminals.get(name);
|
|
39
|
+
if (terminal !== undefined) {
|
|
40
|
+
console.log('==> TerminalManager.shutdownTerminal', name);
|
|
41
|
+
this._terminals.delete(name);
|
|
42
|
+
terminal.dispose();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Start a new kernel.
|
|
47
|
+
*/
|
|
48
|
+
async startNew() {
|
|
49
|
+
const name = this._nextAvailableName();
|
|
50
|
+
console.log('==> TerminalManager.startNew', name);
|
|
51
|
+
const baseUrl = PageConfig.getBaseUrl();
|
|
52
|
+
const terminal = new Terminal({ name, baseUrl });
|
|
53
|
+
this._terminals.set(name, terminal);
|
|
54
|
+
terminal.disposed.connect(() => this.shutdownTerminal(name));
|
|
55
|
+
const url = `${this._wsUrl}terminals/websocket/${name}`;
|
|
56
|
+
await terminal.wsConnect(url);
|
|
57
|
+
return { name };
|
|
58
|
+
}
|
|
59
|
+
_nextAvailableName() {
|
|
60
|
+
for (let i = 1;; ++i) {
|
|
61
|
+
const name = `${i}`;
|
|
62
|
+
if (!this._terminals.has(name)) {
|
|
63
|
+
return name;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
package/lib/terminal.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ISignal } from '@lumino/signaling';
|
|
1
2
|
import { ITerminal } from './tokens';
|
|
2
3
|
export declare class Terminal implements ITerminal {
|
|
3
4
|
readonly options: ITerminal.IOptions;
|
|
@@ -6,11 +7,18 @@ export declare class Terminal implements ITerminal {
|
|
|
6
7
|
*/
|
|
7
8
|
constructor(options: ITerminal.IOptions);
|
|
8
9
|
private _outputCallback;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
get disposed(): ISignal<this, void>;
|
|
12
|
+
get isDisposed(): boolean;
|
|
9
13
|
/**
|
|
10
14
|
* Get the name of the terminal.
|
|
11
15
|
*/
|
|
12
16
|
get name(): string;
|
|
13
17
|
wsConnect(url: string): Promise<void>;
|
|
18
|
+
private _disposed;
|
|
19
|
+
private _isDisposed;
|
|
20
|
+
private _server?;
|
|
14
21
|
private _socket?;
|
|
15
22
|
private _shell;
|
|
23
|
+
private _running;
|
|
16
24
|
}
|
package/lib/terminal.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Jupyter Development Team.
|
|
2
2
|
// Distributed under the terms of the Modified BSD License.
|
|
3
3
|
import { Shell } from '@jupyterlite/cockle';
|
|
4
|
+
import { Signal } from '@lumino/signaling';
|
|
4
5
|
import { Server as WebSocketServer } from 'mock-socket';
|
|
5
6
|
export class Terminal {
|
|
6
7
|
/**
|
|
@@ -8,19 +9,48 @@ export class Terminal {
|
|
|
8
9
|
*/
|
|
9
10
|
constructor(options) {
|
|
10
11
|
this.options = options;
|
|
12
|
+
this._disposed = new Signal(this);
|
|
13
|
+
this._isDisposed = false;
|
|
14
|
+
this._running = false;
|
|
11
15
|
this._shell = new Shell({
|
|
12
16
|
mountpoint: '/drive',
|
|
13
17
|
driveFsBaseUrl: options.baseUrl,
|
|
14
18
|
wasmBaseUrl: options.baseUrl + 'extensions/@jupyterlite/terminal/static/wasm/',
|
|
15
19
|
outputCallback: this._outputCallback.bind(this)
|
|
16
20
|
});
|
|
21
|
+
this._shell.disposed.connect(() => this.dispose());
|
|
17
22
|
}
|
|
18
|
-
|
|
23
|
+
_outputCallback(text) {
|
|
19
24
|
if (this._socket) {
|
|
20
25
|
const ret = JSON.stringify(['stdout', text]);
|
|
21
26
|
this._socket.send(ret);
|
|
22
27
|
}
|
|
23
28
|
}
|
|
29
|
+
dispose() {
|
|
30
|
+
if (this._isDisposed) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.log('Terminal.dispose');
|
|
34
|
+
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
|
+
this._shell.dispose();
|
|
46
|
+
this._disposed.emit();
|
|
47
|
+
}
|
|
48
|
+
get disposed() {
|
|
49
|
+
return this._disposed;
|
|
50
|
+
}
|
|
51
|
+
get isDisposed() {
|
|
52
|
+
return this._isDisposed;
|
|
53
|
+
}
|
|
24
54
|
/**
|
|
25
55
|
* Get the name of the terminal.
|
|
26
56
|
*/
|
|
@@ -29,9 +59,14 @@ export class Terminal {
|
|
|
29
59
|
}
|
|
30
60
|
async wsConnect(url) {
|
|
31
61
|
console.log('==> Terminal.wsConnect', url);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
console.log('==> server connection'
|
|
62
|
+
this._server = new WebSocketServer(url);
|
|
63
|
+
this._server.on('connection', async (socket) => {
|
|
64
|
+
console.log('==> server connection');
|
|
65
|
+
if (this._socket !== undefined) {
|
|
66
|
+
this._socket.send(JSON.stringify(['disconnect']));
|
|
67
|
+
this._socket.close();
|
|
68
|
+
this._socket = undefined;
|
|
69
|
+
}
|
|
35
70
|
this._socket = socket;
|
|
36
71
|
socket.on('message', async (message) => {
|
|
37
72
|
const data = JSON.parse(message);
|
|
@@ -47,17 +82,20 @@ export class Terminal {
|
|
|
47
82
|
await this._shell.setSize(rows, columns);
|
|
48
83
|
}
|
|
49
84
|
});
|
|
50
|
-
socket.on('close',
|
|
85
|
+
socket.on('close', () => {
|
|
51
86
|
console.log('==> socket close');
|
|
52
87
|
});
|
|
53
|
-
socket.on('error',
|
|
88
|
+
socket.on('error', () => {
|
|
54
89
|
console.log('==> socket error');
|
|
55
90
|
});
|
|
56
91
|
// Return handshake.
|
|
57
92
|
const res = JSON.stringify(['setup']);
|
|
58
93
|
console.log('==> Returning handshake via socket', res);
|
|
59
94
|
socket.send(res);
|
|
60
|
-
|
|
95
|
+
if (!this._running) {
|
|
96
|
+
this._running = true;
|
|
97
|
+
await this._shell.start();
|
|
98
|
+
}
|
|
61
99
|
});
|
|
62
100
|
}
|
|
63
101
|
}
|
package/lib/tokens.d.ts
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { TerminalAPI } from '@jupyterlab/services';
|
|
2
2
|
import { Token } from '@lumino/coreutils';
|
|
3
|
+
import { IObservableDisposable } from '@lumino/disposable';
|
|
3
4
|
/**
|
|
4
5
|
* The token for the Terminals service.
|
|
5
6
|
*/
|
|
6
|
-
export declare const
|
|
7
|
+
export declare const ITerminalManager: Token<ITerminalManager>;
|
|
7
8
|
/**
|
|
8
|
-
* An interface for the
|
|
9
|
+
* An interface for the TerminalManager service.
|
|
9
10
|
*/
|
|
10
|
-
export interface
|
|
11
|
+
export interface ITerminalManager {
|
|
12
|
+
/**
|
|
13
|
+
* Return whether the named terminal exists.
|
|
14
|
+
*/
|
|
15
|
+
has(name: string): boolean;
|
|
11
16
|
/**
|
|
12
17
|
* List the running terminals.
|
|
13
18
|
*/
|
|
14
|
-
|
|
19
|
+
listRunning: () => Promise<TerminalAPI.IModel[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Shutdown a terminal by name.
|
|
22
|
+
*/
|
|
23
|
+
shutdownTerminal: (name: string) => Promise<void>;
|
|
15
24
|
/**
|
|
16
25
|
* Start a new kernel.
|
|
17
26
|
*/
|
|
@@ -20,7 +29,7 @@ export interface ITerminals {
|
|
|
20
29
|
/**
|
|
21
30
|
* An interface for a server-side terminal running in the browser.
|
|
22
31
|
*/
|
|
23
|
-
export interface ITerminal {
|
|
32
|
+
export interface ITerminal extends IObservableDisposable {
|
|
24
33
|
/**
|
|
25
34
|
* The name of the server-side terminal.
|
|
26
35
|
*/
|
package/lib/tokens.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyterlite/terminal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A terminal for JupyterLite",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -57,19 +57,19 @@
|
|
|
57
57
|
"watch:labextension": "jupyter labextension watch ."
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@jupyterlab/coreutils": "^6.
|
|
61
|
-
"@jupyterlab/services": "^7.
|
|
62
|
-
"@jupyterlab/terminal": "^4.
|
|
63
|
-
"@jupyterlab/terminal-extension": "^4.
|
|
64
|
-
"@jupyterlite/cockle": "^0.0.
|
|
65
|
-
"@jupyterlite/contents": "^0.
|
|
66
|
-
"@jupyterlite/server": "^0.
|
|
60
|
+
"@jupyterlab/coreutils": "^6.3.5",
|
|
61
|
+
"@jupyterlab/services": "^7.3.5",
|
|
62
|
+
"@jupyterlab/terminal": "^4.3.5",
|
|
63
|
+
"@jupyterlab/terminal-extension": "^4.3.5",
|
|
64
|
+
"@jupyterlite/cockle": "^0.0.15",
|
|
65
|
+
"@jupyterlite/contents": "^0.5.1",
|
|
66
|
+
"@jupyterlite/server": "^0.5.1",
|
|
67
67
|
"@lumino/coreutils": "^2.2.0",
|
|
68
68
|
"mock-socket": "^9.3.1"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@jupyterlab/builder": "^4.
|
|
72
|
-
"@jupyterlab/testutils": "^4.
|
|
71
|
+
"@jupyterlab/builder": "^4.3.5",
|
|
72
|
+
"@jupyterlab/testutils": "^4.3.5",
|
|
73
73
|
"@types/jest": "^29.2.0",
|
|
74
74
|
"@types/json-schema": "^7.0.11",
|
|
75
75
|
"@types/react": "^18.0.26",
|
package/src/index.ts
CHANGED
|
@@ -7,20 +7,20 @@ import {
|
|
|
7
7
|
Router
|
|
8
8
|
} from '@jupyterlite/server';
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { TerminalManager } from './manager';
|
|
11
|
+
import { ITerminalManager } from './tokens';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* The terminals service plugin.
|
|
15
15
|
*/
|
|
16
|
-
const terminalsPlugin: JupyterLiteServerPlugin<
|
|
16
|
+
const terminalsPlugin: JupyterLiteServerPlugin<ITerminalManager> = {
|
|
17
17
|
id: '@jupyterlite/terminal:plugin',
|
|
18
18
|
description: 'A terminal for JupyterLite',
|
|
19
19
|
autoStart: true,
|
|
20
|
-
provides:
|
|
20
|
+
provides: ITerminalManager,
|
|
21
21
|
activate: async (app: JupyterLiteServer) => {
|
|
22
22
|
console.log(
|
|
23
|
-
'
|
|
23
|
+
'JupyterLite extension @jupyterlite/terminal:plugin is activated!'
|
|
24
24
|
);
|
|
25
25
|
|
|
26
26
|
const { serviceManager } = app;
|
|
@@ -33,7 +33,7 @@ const terminalsPlugin: JupyterLiteServerPlugin<ITerminals> = {
|
|
|
33
33
|
await terminals.ready;
|
|
34
34
|
console.log('terminals ready after await:', terminals.isReady); // Ready
|
|
35
35
|
|
|
36
|
-
return new
|
|
36
|
+
return new TerminalManager(serverSettings.wsUrl);
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
39
|
|
|
@@ -43,26 +43,42 @@ const terminalsPlugin: JupyterLiteServerPlugin<ITerminals> = {
|
|
|
43
43
|
const terminalsRoutesPlugin: JupyterLiteServerPlugin<void> = {
|
|
44
44
|
id: '@jupyterlite/terminal:routes-plugin',
|
|
45
45
|
autoStart: true,
|
|
46
|
-
requires: [
|
|
47
|
-
activate: (app: JupyterLiteServer,
|
|
46
|
+
requires: [ITerminalManager],
|
|
47
|
+
activate: (app: JupyterLiteServer, terminalManager: ITerminalManager) => {
|
|
48
48
|
console.log(
|
|
49
|
-
'
|
|
50
|
-
|
|
49
|
+
'JupyterLite extension @jupyterlite/terminal:routes-plugin is activated!',
|
|
50
|
+
terminalManager
|
|
51
51
|
);
|
|
52
52
|
|
|
53
53
|
// GET /api/terminals - List the running terminals
|
|
54
54
|
app.router.get('/api/terminals', async (req: Router.IRequest) => {
|
|
55
|
-
const res = await
|
|
55
|
+
const res = await terminalManager.listRunning();
|
|
56
56
|
// Should return last_activity for each too,
|
|
57
57
|
return new Response(JSON.stringify(res));
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
// POST /api/terminals - Start a terminal
|
|
61
61
|
app.router.post('/api/terminals', async (req: Router.IRequest) => {
|
|
62
|
-
const res = await
|
|
62
|
+
const res = await terminalManager.startNew();
|
|
63
63
|
// Should return last_activity too.
|
|
64
64
|
return new Response(JSON.stringify(res));
|
|
65
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 });
|
|
80
|
+
}
|
|
81
|
+
);
|
|
66
82
|
}
|
|
67
83
|
};
|
|
68
84
|
|
package/src/manager.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import { PageConfig } from '@jupyterlab/coreutils';
|
|
5
|
+
import { TerminalAPI } from '@jupyterlab/services';
|
|
6
|
+
|
|
7
|
+
import { Terminal } from './terminal';
|
|
8
|
+
import { ITerminalManager } from './tokens';
|
|
9
|
+
|
|
10
|
+
/**
|
|
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.
|
|
14
|
+
*/
|
|
15
|
+
export class TerminalManager implements ITerminalManager {
|
|
16
|
+
/**
|
|
17
|
+
* Construct a new TerminalManager object.
|
|
18
|
+
*/
|
|
19
|
+
constructor(wsUrl: string) {
|
|
20
|
+
this._wsUrl = wsUrl;
|
|
21
|
+
console.log('==> TerminalManager.constructor', this._wsUrl);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Return whether the named terminal exists.
|
|
26
|
+
*/
|
|
27
|
+
has(name: string): boolean {
|
|
28
|
+
return this._terminals.has(name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* List the running terminals.
|
|
33
|
+
*/
|
|
34
|
+
async listRunning(): Promise<TerminalAPI.IModel[]> {
|
|
35
|
+
const ret = [...this._terminals.values()].map(terminal => ({
|
|
36
|
+
name: terminal.name
|
|
37
|
+
}));
|
|
38
|
+
return ret;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Shutdown a terminal by name.
|
|
43
|
+
*/
|
|
44
|
+
async shutdownTerminal(name: string): Promise<void> {
|
|
45
|
+
const terminal = this._terminals.get(name);
|
|
46
|
+
if (terminal !== undefined) {
|
|
47
|
+
console.log('==> TerminalManager.shutdownTerminal', name);
|
|
48
|
+
this._terminals.delete(name);
|
|
49
|
+
terminal.dispose();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Start a new kernel.
|
|
55
|
+
*/
|
|
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);
|
|
62
|
+
|
|
63
|
+
terminal.disposed.connect(() => this.shutdownTerminal(name));
|
|
64
|
+
|
|
65
|
+
const url = `${this._wsUrl}terminals/websocket/${name}`;
|
|
66
|
+
await terminal.wsConnect(url);
|
|
67
|
+
|
|
68
|
+
return { name };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private _nextAvailableName(): string {
|
|
72
|
+
for (let i = 1; ; ++i) {
|
|
73
|
+
const name = `${i}`;
|
|
74
|
+
if (!this._terminals.has(name)) {
|
|
75
|
+
return name;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private _wsUrl: string;
|
|
81
|
+
private _terminals: Map<string, Terminal> = new Map();
|
|
82
|
+
}
|
package/src/terminal.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { Shell } from '@jupyterlite/cockle';
|
|
5
5
|
import { JSONPrimitive } from '@lumino/coreutils';
|
|
6
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
Server as WebSocketServer,
|
|
@@ -23,15 +24,48 @@ export class Terminal implements ITerminal {
|
|
|
23
24
|
options.baseUrl + 'extensions/@jupyterlite/terminal/static/wasm/',
|
|
24
25
|
outputCallback: this._outputCallback.bind(this)
|
|
25
26
|
});
|
|
27
|
+
this._shell.disposed.connect(() => this.dispose());
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
private
|
|
30
|
+
private _outputCallback(text: string): void {
|
|
29
31
|
if (this._socket) {
|
|
30
32
|
const ret = JSON.stringify(['stdout', text]);
|
|
31
33
|
this._socket.send(ret);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
dispose(): void {
|
|
38
|
+
if (this._isDisposed) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log('Terminal.dispose');
|
|
43
|
+
this._isDisposed = true;
|
|
44
|
+
|
|
45
|
+
if (this._socket !== undefined) {
|
|
46
|
+
// Disconnect from frontend.
|
|
47
|
+
this._socket.send(JSON.stringify(['disconnect']));
|
|
48
|
+
this._socket.close();
|
|
49
|
+
this._socket = undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (this._server !== undefined) {
|
|
53
|
+
this._server.close();
|
|
54
|
+
this._server = undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this._shell.dispose();
|
|
58
|
+
this._disposed.emit();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get disposed(): ISignal<this, void> {
|
|
62
|
+
return this._disposed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get isDisposed(): boolean {
|
|
66
|
+
return this._isDisposed;
|
|
67
|
+
}
|
|
68
|
+
|
|
35
69
|
/**
|
|
36
70
|
* Get the name of the terminal.
|
|
37
71
|
*/
|
|
@@ -41,11 +75,15 @@ export class Terminal implements ITerminal {
|
|
|
41
75
|
|
|
42
76
|
async wsConnect(url: string) {
|
|
43
77
|
console.log('==> Terminal.wsConnect', url);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
78
|
+
this._server = new WebSocketServer(url);
|
|
79
|
+
|
|
80
|
+
this._server.on('connection', async (socket: WebSocketClient) => {
|
|
81
|
+
console.log('==> server connection');
|
|
82
|
+
if (this._socket !== undefined) {
|
|
83
|
+
this._socket.send(JSON.stringify(['disconnect']));
|
|
84
|
+
this._socket.close();
|
|
85
|
+
this._socket = undefined;
|
|
86
|
+
}
|
|
49
87
|
this._socket = socket;
|
|
50
88
|
|
|
51
89
|
socket.on('message', async (message: any) => {
|
|
@@ -63,11 +101,11 @@ export class Terminal implements ITerminal {
|
|
|
63
101
|
}
|
|
64
102
|
});
|
|
65
103
|
|
|
66
|
-
socket.on('close',
|
|
104
|
+
socket.on('close', () => {
|
|
67
105
|
console.log('==> socket close');
|
|
68
106
|
});
|
|
69
107
|
|
|
70
|
-
socket.on('error',
|
|
108
|
+
socket.on('error', () => {
|
|
71
109
|
console.log('==> socket error');
|
|
72
110
|
});
|
|
73
111
|
|
|
@@ -76,10 +114,17 @@ export class Terminal implements ITerminal {
|
|
|
76
114
|
console.log('==> Returning handshake via socket', res);
|
|
77
115
|
socket.send(res);
|
|
78
116
|
|
|
79
|
-
|
|
117
|
+
if (!this._running) {
|
|
118
|
+
this._running = true;
|
|
119
|
+
await this._shell.start();
|
|
120
|
+
}
|
|
80
121
|
});
|
|
81
122
|
}
|
|
82
123
|
|
|
124
|
+
private _disposed = new Signal<this, void>(this);
|
|
125
|
+
private _isDisposed = false;
|
|
126
|
+
private _server?: WebSocketServer;
|
|
83
127
|
private _socket?: WebSocketClient;
|
|
84
128
|
private _shell: Shell;
|
|
129
|
+
private _running = false;
|
|
85
130
|
}
|
package/src/tokens.ts
CHANGED
|
@@ -4,22 +4,33 @@
|
|
|
4
4
|
import { TerminalAPI } from '@jupyterlab/services';
|
|
5
5
|
|
|
6
6
|
import { Token } from '@lumino/coreutils';
|
|
7
|
+
import { IObservableDisposable } from '@lumino/disposable';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The token for the Terminals service.
|
|
10
11
|
*/
|
|
11
|
-
export const
|
|
12
|
-
'@jupyterlite/terminal:
|
|
12
|
+
export const ITerminalManager = new Token<ITerminalManager>(
|
|
13
|
+
'@jupyterlite/terminal:ITerminalManager'
|
|
13
14
|
);
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
* An interface for the
|
|
17
|
+
* An interface for the TerminalManager service.
|
|
17
18
|
*/
|
|
18
|
-
export interface
|
|
19
|
+
export interface ITerminalManager {
|
|
20
|
+
/**
|
|
21
|
+
* Return whether the named terminal exists.
|
|
22
|
+
*/
|
|
23
|
+
has(name: string): boolean;
|
|
24
|
+
|
|
19
25
|
/**
|
|
20
26
|
* List the running terminals.
|
|
21
27
|
*/
|
|
22
|
-
|
|
28
|
+
listRunning: () => Promise<TerminalAPI.IModel[]>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Shutdown a terminal by name.
|
|
32
|
+
*/
|
|
33
|
+
shutdownTerminal: (name: string) => Promise<void>;
|
|
23
34
|
|
|
24
35
|
/**
|
|
25
36
|
* Start a new kernel.
|
|
@@ -30,7 +41,7 @@ export interface ITerminals {
|
|
|
30
41
|
/**
|
|
31
42
|
* An interface for a server-side terminal running in the browser.
|
|
32
43
|
*/
|
|
33
|
-
export interface ITerminal {
|
|
44
|
+
export interface ITerminal extends IObservableDisposable {
|
|
34
45
|
/**
|
|
35
46
|
* The name of the server-side terminal.
|
|
36
47
|
*/
|
package/lib/terminals.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { TerminalAPI } from '@jupyterlab/services';
|
|
2
|
-
import { ITerminals } from './tokens';
|
|
3
|
-
/**
|
|
4
|
-
* A class to handle requests to /api/terminals
|
|
5
|
-
*/
|
|
6
|
-
export declare class Terminals implements ITerminals {
|
|
7
|
-
/**
|
|
8
|
-
* Construct a new Terminals object.
|
|
9
|
-
*/
|
|
10
|
-
constructor(wsUrl: string);
|
|
11
|
-
/**
|
|
12
|
-
* List the running terminals.
|
|
13
|
-
*/
|
|
14
|
-
list(): Promise<TerminalAPI.IModel[]>;
|
|
15
|
-
/**
|
|
16
|
-
* Start a new kernel.
|
|
17
|
-
*/
|
|
18
|
-
startNew(): Promise<TerminalAPI.IModel>;
|
|
19
|
-
private _nextAvailableName;
|
|
20
|
-
private _wsUrl;
|
|
21
|
-
private _terminals;
|
|
22
|
-
}
|
package/lib/terminals.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Jupyter Development Team.
|
|
2
|
-
// Distributed under the terms of the Modified BSD License.
|
|
3
|
-
import { PageConfig } from '@jupyterlab/coreutils';
|
|
4
|
-
import { Terminal } from './terminal';
|
|
5
|
-
/**
|
|
6
|
-
* A class to handle requests to /api/terminals
|
|
7
|
-
*/
|
|
8
|
-
export class Terminals {
|
|
9
|
-
/**
|
|
10
|
-
* Construct a new Terminals object.
|
|
11
|
-
*/
|
|
12
|
-
constructor(wsUrl) {
|
|
13
|
-
this._terminals = new Map();
|
|
14
|
-
this._wsUrl = wsUrl;
|
|
15
|
-
console.log('==> Terminals.constructor', this._wsUrl);
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* List the running terminals.
|
|
19
|
-
*/
|
|
20
|
-
async list() {
|
|
21
|
-
const ret = [...this._terminals.values()].map(terminal => ({
|
|
22
|
-
name: terminal.name
|
|
23
|
-
}));
|
|
24
|
-
console.log('==> Terminals.list', ret);
|
|
25
|
-
return ret;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Start a new kernel.
|
|
29
|
-
*/
|
|
30
|
-
async startNew() {
|
|
31
|
-
const name = this._nextAvailableName();
|
|
32
|
-
console.log('==> Terminals.new', name);
|
|
33
|
-
const baseUrl = PageConfig.getBaseUrl();
|
|
34
|
-
const term = new Terminal({ name, baseUrl });
|
|
35
|
-
this._terminals.set(name, term);
|
|
36
|
-
const url = `${this._wsUrl}terminals/websocket/${name}`;
|
|
37
|
-
await term.wsConnect(url);
|
|
38
|
-
return { name };
|
|
39
|
-
}
|
|
40
|
-
_nextAvailableName() {
|
|
41
|
-
for (let i = 1;; ++i) {
|
|
42
|
-
const name = `${i}`;
|
|
43
|
-
if (!this._terminals.has(name)) {
|
|
44
|
-
return name;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
package/src/terminals.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Jupyter Development Team.
|
|
2
|
-
// Distributed under the terms of the Modified BSD License.
|
|
3
|
-
|
|
4
|
-
import { PageConfig } from '@jupyterlab/coreutils';
|
|
5
|
-
import { TerminalAPI } from '@jupyterlab/services';
|
|
6
|
-
|
|
7
|
-
import { Terminal } from './terminal';
|
|
8
|
-
import { ITerminals } from './tokens';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* A class to handle requests to /api/terminals
|
|
12
|
-
*/
|
|
13
|
-
export class Terminals implements ITerminals {
|
|
14
|
-
/**
|
|
15
|
-
* Construct a new Terminals object.
|
|
16
|
-
*/
|
|
17
|
-
constructor(wsUrl: string) {
|
|
18
|
-
this._wsUrl = wsUrl;
|
|
19
|
-
console.log('==> Terminals.constructor', this._wsUrl);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* List the running terminals.
|
|
24
|
-
*/
|
|
25
|
-
async list(): Promise<TerminalAPI.IModel[]> {
|
|
26
|
-
const ret = [...this._terminals.values()].map(terminal => ({
|
|
27
|
-
name: terminal.name
|
|
28
|
-
}));
|
|
29
|
-
console.log('==> Terminals.list', ret);
|
|
30
|
-
return ret;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Start a new kernel.
|
|
35
|
-
*/
|
|
36
|
-
async startNew(): Promise<TerminalAPI.IModel> {
|
|
37
|
-
const name = this._nextAvailableName();
|
|
38
|
-
console.log('==> Terminals.new', name);
|
|
39
|
-
const baseUrl = PageConfig.getBaseUrl();
|
|
40
|
-
const term = new Terminal({ name, baseUrl });
|
|
41
|
-
this._terminals.set(name, term);
|
|
42
|
-
|
|
43
|
-
const url = `${this._wsUrl}terminals/websocket/${name}`;
|
|
44
|
-
await term.wsConnect(url);
|
|
45
|
-
|
|
46
|
-
return { name };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private _nextAvailableName(): string {
|
|
50
|
-
for (let i = 1; ; ++i) {
|
|
51
|
-
const name = `${i}`;
|
|
52
|
-
if (!this._terminals.has(name)) {
|
|
53
|
-
return name;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
private _wsUrl: string;
|
|
59
|
-
private _terminals: Map<string, Terminal> = new Map();
|
|
60
|
-
}
|