@jupyterlite/terminal 0.1.4 → 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/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/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
- 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
  */
@@ -29,9 +59,14 @@ export class Terminal {
29
59
  }
30
60
  async wsConnect(url) {
31
61
  console.log('==> Terminal.wsConnect', url);
32
- const server = new WebSocketServer(url);
33
- server.on('connection', async (socket) => {
34
- console.log('==> server connection', this, socket);
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', async () => {
85
+ socket.on('close', () => {
51
86
  console.log('==> socket close');
52
87
  });
53
- socket.on('error', async () => {
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
- 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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlite/terminal",
3
- "version": "0.1.4",
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.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",
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.2.4",
72
- "@jupyterlab/testutils": "^4.2.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,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/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 async _outputCallback(text: string): Promise<void> {
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
- const server = new WebSocketServer(url);
46
-
47
- server.on('connection', async (socket: WebSocketClient) => {
48
- console.log('==> server connection', this, socket);
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', async () => {
104
+ socket.on('close', () => {
67
105
  console.log('==> socket close');
68
106
  });
69
107
 
70
- socket.on('error', async () => {
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
- await this._shell.start();
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 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
  */
@@ -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
- }