@jupyterlite/terminal 0.1.4 → 0.1.6

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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { JupyterLiteServerPlugin } from '@jupyterlite/server';
2
- import { ITerminals } from './tokens';
3
- declare const _default: (JupyterLiteServerPlugin<ITerminals> | JupyterLiteServerPlugin<void>)[];
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 { ITerminals } from './tokens';
4
- import { Terminals } from './terminals';
3
+ import { TerminalManager } from './manager';
4
+ import { ITerminalManager } from './tokens';
5
5
  /**
6
6
  * The terminals service plugin.
7
7
  */
@@ -9,7 +9,7 @@ const terminalsPlugin = {
9
9
  id: '@jupyterlite/terminal:plugin',
10
10
  description: 'A terminal for JupyterLite',
11
11
  autoStart: true,
12
- provides: ITerminals,
12
+ provides: ITerminalManager,
13
13
  activate: async (app) => {
14
14
  console.log('JupyterLite extension @jupyterlite/terminal:plugin is activated!');
15
15
  const { serviceManager } = app;
@@ -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 Terminals(serverSettings.wsUrl);
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: [ITerminals],
33
- activate: (app, terminals) => {
34
- console.log('JupyterLite extension @jupyterlite/terminal:routes-plugin is activated!', terminals);
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 terminals.list();
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 terminals.startNew();
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];
@@ -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/shell.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { BaseShell, IShell } from '@jupyterlite/cockle';
2
+ /**
3
+ * Shell class that uses web worker that plugs into a DriveFS via the service worker.
4
+ */
5
+ export declare class Shell extends BaseShell {
6
+ /**
7
+ * Instantiate a new Shell
8
+ *
9
+ * @param options The instantiation options for a new shell
10
+ */
11
+ constructor(options: IShell.IOptions);
12
+ /**
13
+ * Load the web worker.
14
+ */
15
+ protected initWorker(options: IShell.IOptions): Worker;
16
+ }
package/lib/shell.js ADDED
@@ -0,0 +1,23 @@
1
+ import { BaseShell } from '@jupyterlite/cockle';
2
+ /**
3
+ * Shell class that uses web worker that plugs into a DriveFS via the service worker.
4
+ */
5
+ export class Shell extends BaseShell {
6
+ /**
7
+ * Instantiate a new Shell
8
+ *
9
+ * @param options The instantiation options for a new shell
10
+ */
11
+ constructor(options) {
12
+ super(options);
13
+ }
14
+ /**
15
+ * Load the web worker.
16
+ */
17
+ initWorker(options) {
18
+ console.log('Terminal create webworker');
19
+ return new Worker(new URL('./worker.js', import.meta.url), {
20
+ type: 'module'
21
+ });
22
+ }
23
+ }
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,26 +1,56 @@
1
1
  // Copyright (c) Jupyter Development Team.
2
2
  // Distributed under the terms of the Modified BSD License.
3
- import { Shell } from '@jupyterlite/cockle';
3
+ import { Signal } from '@lumino/signaling';
4
4
  import { Server as WebSocketServer } from 'mock-socket';
5
+ import { Shell } from './shell';
5
6
  export class Terminal {
6
7
  /**
7
8
  * Construct a new 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
- async _outputCallback(text) {
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
  */
@@ -28,10 +58,15 @@ export class Terminal {
28
58
  return this.options.name;
29
59
  }
30
60
  async wsConnect(url) {
31
- console.log('==> Terminal.wsConnect', url);
32
- const server = new WebSocketServer(url);
33
- server.on('connection', async (socket) => {
34
- console.log('==> server connection', this, socket);
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
+ }
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', async () => {
51
- console.log('==> socket close');
85
+ socket.on('close', () => {
86
+ console.log('Terminal socket close');
52
87
  });
53
- socket.on('error', async () => {
54
- console.log('==> socket error');
88
+ socket.on('error', () => {
89
+ console.log('Terminal socket error');
55
90
  });
56
91
  // Return handshake.
57
92
  const res = JSON.stringify(['setup']);
58
- console.log('==> Returning handshake via socket', res);
93
+ console.log('Terminal returning handshake via socket');
59
94
  socket.send(res);
60
- await this._shell.start();
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 ITerminals: Token<ITerminals>;
7
+ export declare const ITerminalManager: Token<ITerminalManager>;
7
8
  /**
8
- * An interface for the Terminals service.
9
+ * An interface for the TerminalManager service.
9
10
  */
10
- export interface ITerminals {
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
- list: () => Promise<TerminalAPI.IModel[]>;
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
@@ -4,4 +4,4 @@ import { Token } from '@lumino/coreutils';
4
4
  /**
5
5
  * The token for the Terminals service.
6
6
  */
7
- export const ITerminals = new Token('@jupyterlite/terminal:ITerminals');
7
+ export const ITerminalManager = new Token('@jupyterlite/terminal:ITerminalManager');
@@ -0,0 +1 @@
1
+ export {};
package/lib/worker.js ADDED
@@ -0,0 +1,35 @@
1
+ import { expose } from 'comlink';
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
+ }
12
+ /**
13
+ * Shell web worker that uses DriveFS via service worker.
14
+ * Note that this is not exported as it is accessed from Shell via the filename.
15
+ */
16
+ class ShellWorker extends BaseShellWorker {
17
+ /**
18
+ * Initialize the DriveFS to mount an external file system.
19
+ */
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);
32
+ }
33
+ }
34
+ const worker = new ShellWorker();
35
+ expose(worker);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlite/terminal",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A terminal for JupyterLite",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -30,12 +30,14 @@
30
30
  "url": "https://github.com/jupyterlite/terminal.git"
31
31
  },
32
32
  "scripts": {
33
- "build": "jlpm build:lib && jlpm build:labextension:dev",
34
- "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
33
+ "build": "jlpm build:lib && jlpm build:labextension:dev && jlpm build:webworker",
34
+ "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension && jlpm build:webworker:prod",
35
35
  "build:labextension": "jupyter labextension build .",
36
36
  "build:labextension:dev": "jupyter labextension build --development True .",
37
37
  "build:lib": "tsc --sourceMap",
38
38
  "build:lib:prod": "tsc",
39
+ "build:webworker": "webpack -c webpack.worker.config.js --env=dev",
40
+ "build:webworker:prod": "webpack -c webpack.worker.config.js",
39
41
  "clean": "jlpm clean:lib",
40
42
  "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
41
43
  "clean:lintcache": "rimraf .eslintcache .stylelintcache",
@@ -57,19 +59,19 @@
57
59
  "watch:labextension": "jupyter labextension watch ."
58
60
  },
59
61
  "dependencies": {
60
- "@jupyterlab/coreutils": "^6.2.4",
61
- "@jupyterlab/services": "^7.2.4",
62
- "@jupyterlab/terminal": "^4.2.4",
63
- "@jupyterlab/terminal-extension": "^4.2.4",
64
- "@jupyterlite/cockle": "^0.0.12",
65
- "@jupyterlite/contents": "^0.4.0",
66
- "@jupyterlite/server": "^0.4.0",
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",
67
69
  "@lumino/coreutils": "^2.2.0",
68
70
  "mock-socket": "^9.3.1"
69
71
  },
70
72
  "devDependencies": {
71
- "@jupyterlab/builder": "^4.2.4",
72
- "@jupyterlab/testutils": "^4.2.4",
73
+ "@jupyterlab/builder": "^4.3.5",
74
+ "@jupyterlab/testutils": "^4.3.5",
73
75
  "@types/jest": "^29.2.0",
74
76
  "@types/json-schema": "^7.0.11",
75
77
  "@types/react": "^18.0.26",
@@ -91,6 +93,7 @@
91
93
  "stylelint-config-standard": "^34.0.0",
92
94
  "stylelint-csstree-validator": "^3.0.0",
93
95
  "stylelint-prettier": "^4.0.0",
96
+ "ts-loader": "^9.5.2",
94
97
  "typescript": "~5.0.2",
95
98
  "webpack": "^5.87.0",
96
99
  "webpack-cli": "^5.1.4",
@@ -106,8 +109,7 @@
106
109
  },
107
110
  "jupyterlab": {
108
111
  "extension": true,
109
- "outputDir": "jupyterlite_terminal/labextension",
110
- "webpackConfig": "./webpack.extra.config.js"
112
+ "outputDir": "jupyterlite_terminal/labextension"
111
113
  },
112
114
  "jupyterlite": {
113
115
  "liteExtension": true
package/src/index.ts CHANGED
@@ -7,17 +7,17 @@ import {
7
7
  Router
8
8
  } from '@jupyterlite/server';
9
9
 
10
- import { ITerminals } from './tokens';
11
- import { Terminals } from './terminals';
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<ITerminals> = {
16
+ const terminalsPlugin: JupyterLiteServerPlugin<ITerminalManager> = {
17
17
  id: '@jupyterlite/terminal:plugin',
18
18
  description: 'A terminal for JupyterLite',
19
19
  autoStart: true,
20
- provides: ITerminals,
20
+ provides: ITerminalManager,
21
21
  activate: async (app: JupyterLiteServer) => {
22
22
  console.log(
23
23
  'JupyterLite extension @jupyterlite/terminal:plugin is activated!'
@@ -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 Terminals(serverSettings.wsUrl);
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: [ITerminals],
47
- activate: (app: JupyterLiteServer, terminals: ITerminals) => {
46
+ requires: [ITerminalManager],
47
+ activate: (app: JupyterLiteServer, terminalManager: ITerminalManager) => {
48
48
  console.log(
49
49
  'JupyterLite extension @jupyterlite/terminal:routes-plugin is activated!',
50
- terminals
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 terminals.list();
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 terminals.startNew();
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/shell.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { BaseShell, IShell } from '@jupyterlite/cockle';
2
+
3
+ /**
4
+ * Shell class that uses web worker that plugs into a DriveFS via the service worker.
5
+ */
6
+ export class Shell extends BaseShell {
7
+ /**
8
+ * Instantiate a new Shell
9
+ *
10
+ * @param options The instantiation options for a new shell
11
+ */
12
+ constructor(options: IShell.IOptions) {
13
+ super(options);
14
+ }
15
+
16
+ /**
17
+ * Load the web worker.
18
+ */
19
+ protected override initWorker(options: IShell.IOptions): Worker {
20
+ console.log('Terminal create webworker');
21
+ return new Worker(new URL('./worker.js', import.meta.url), {
22
+ type: 'module'
23
+ });
24
+ }
25
+ }
package/src/terminal.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  // Copyright (c) Jupyter Development Team.
2
2
  // Distributed under the terms of the Modified BSD License.
3
3
 
4
- import { Shell } from '@jupyterlite/cockle';
4
+ import { IShell } 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,
9
10
  Client as WebSocketClient
10
11
  } from 'mock-socket';
11
12
 
13
+ import { Shell } from './shell';
12
14
  import { ITerminal } from './tokens';
13
15
 
14
16
  export class Terminal implements ITerminal {
@@ -23,15 +25,48 @@ export class Terminal implements ITerminal {
23
25
  options.baseUrl + 'extensions/@jupyterlite/terminal/static/wasm/',
24
26
  outputCallback: this._outputCallback.bind(this)
25
27
  });
28
+ this._shell.disposed.connect(() => this.dispose());
26
29
  }
27
30
 
28
- private async _outputCallback(text: string): Promise<void> {
31
+ private _outputCallback(text: string): void {
29
32
  if (this._socket) {
30
33
  const ret = JSON.stringify(['stdout', text]);
31
34
  this._socket.send(ret);
32
35
  }
33
36
  }
34
37
 
38
+ dispose(): void {
39
+ if (this._isDisposed) {
40
+ return;
41
+ }
42
+
43
+ console.log('Terminal.dispose');
44
+ 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
+ this._shell.dispose();
59
+ this._disposed.emit();
60
+ }
61
+
62
+ get disposed(): ISignal<this, void> {
63
+ return this._disposed;
64
+ }
65
+
66
+ get isDisposed(): boolean {
67
+ return this._isDisposed;
68
+ }
69
+
35
70
  /**
36
71
  * Get the name of the terminal.
37
72
  */
@@ -40,12 +75,16 @@ export class Terminal implements ITerminal {
40
75
  }
41
76
 
42
77
  async wsConnect(url: string) {
43
- console.log('==> Terminal.wsConnect', url);
44
-
45
- const server = new WebSocketServer(url);
46
-
47
- server.on('connection', async (socket: WebSocketClient) => {
48
- console.log('==> server connection', this, socket);
78
+ console.log('Terminal wsConnect', url);
79
+ this._server = new WebSocketServer(url);
80
+
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
+ }
49
88
  this._socket = socket;
50
89
 
51
90
  socket.on('message', async (message: any) => {
@@ -63,23 +102,30 @@ export class Terminal implements ITerminal {
63
102
  }
64
103
  });
65
104
 
66
- socket.on('close', async () => {
67
- console.log('==> socket close');
105
+ socket.on('close', () => {
106
+ console.log('Terminal socket close');
68
107
  });
69
108
 
70
- socket.on('error', async () => {
71
- console.log('==> socket error');
109
+ socket.on('error', () => {
110
+ console.log('Terminal socket error');
72
111
  });
73
112
 
74
113
  // Return handshake.
75
114
  const res = JSON.stringify(['setup']);
76
- console.log('==> Returning handshake via socket', res);
115
+ console.log('Terminal returning handshake via socket');
77
116
  socket.send(res);
78
117
 
79
- await this._shell.start();
118
+ if (!this._running) {
119
+ this._running = true;
120
+ await this._shell.start();
121
+ }
80
122
  });
81
123
  }
82
124
 
125
+ private _disposed = new Signal<this, void>(this);
126
+ private _isDisposed = false;
127
+ private _server?: WebSocketServer;
83
128
  private _socket?: WebSocketClient;
84
- private _shell: Shell;
129
+ private _shell: IShell;
130
+ private _running = false;
85
131
  }
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 ITerminals = new Token<ITerminals>(
12
- '@jupyterlite/terminal:ITerminals'
12
+ export const ITerminalManager = new Token<ITerminalManager>(
13
+ '@jupyterlite/terminal:ITerminalManager'
13
14
  );
14
15
 
15
16
  /**
16
- * An interface for the Terminals service.
17
+ * An interface for the TerminalManager service.
17
18
  */
18
- export interface ITerminals {
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
- list: () => Promise<TerminalAPI.IModel[]>;
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/src/worker.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { expose } from 'comlink';
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
+ }
24
+
25
+ /**
26
+ * Shell web worker that uses DriveFS via service worker.
27
+ * Note that this is not exported as it is accessed from Shell via the filename.
28
+ */
29
+ class ShellWorker extends BaseShellWorker {
30
+ /**
31
+ * Initialize the DriveFS to mount an external file system.
32
+ */
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);
49
+ }
50
+ }
51
+
52
+ const worker = new ShellWorker();
53
+ expose(worker);
@@ -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
- }