@jupyterlite/terminal 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts ADDED
@@ -0,0 +1,157 @@
1
+ import { PageConfig, URLExt } from '@jupyterlab/coreutils';
2
+ import { ServerConnection, Terminal } from '@jupyterlab/services';
3
+ import {
4
+ IExternalCommand,
5
+ IShellManager,
6
+ IStdinReply,
7
+ IStdinRequest,
8
+ ShellManager
9
+ } from '@jupyterlite/cockle';
10
+ import { JSONPrimitive } from '@lumino/coreutils';
11
+
12
+ import {
13
+ Server as WebSocketServer,
14
+ Client as WebSocketClient
15
+ } from 'mock-socket';
16
+
17
+ import { Shell } from './shell';
18
+ import { ILiteTerminalAPIClient } from './tokens';
19
+
20
+ export class LiteTerminalAPIClient implements ILiteTerminalAPIClient {
21
+ constructor(options: { serverSettings?: ServerConnection.ISettings } = {}) {
22
+ this.serverSettings =
23
+ options.serverSettings ?? ServerConnection.makeSettings();
24
+ this._shellManager = new ShellManager();
25
+ }
26
+
27
+ /**
28
+ * Set identifier for communicating with service worker.
29
+ */
30
+ set browsingContextId(browsingContextId: string) {
31
+ console.log('LiteTerminalAPIClient browsingContextId', browsingContextId);
32
+ this._browsingContextId = browsingContextId;
33
+ }
34
+
35
+ /**
36
+ * Function that handles stdin requests received from service worker.
37
+ */
38
+ async handleStdin(request: IStdinRequest): Promise<IStdinReply> {
39
+ return await this._shellManager.handleStdin(request);
40
+ }
41
+
42
+ get isAvailable(): boolean {
43
+ const available = String(PageConfig.getOption('terminalsAvailable'));
44
+ return available.toLowerCase() === 'true';
45
+ }
46
+
47
+ readonly serverSettings: ServerConnection.ISettings;
48
+
49
+ async startNew(
50
+ options?: Terminal.ITerminal.IOptions
51
+ ): Promise<Terminal.IModel> {
52
+ // Create shell.
53
+ const name = options?.name ?? this._nextAvailableName();
54
+ const { baseUrl, wsUrl } = this.serverSettings;
55
+ const shell = new Shell({
56
+ mountpoint: '/drive',
57
+ baseUrl,
58
+ wasmBaseUrl: URLExt.join(
59
+ baseUrl,
60
+ 'extensions/@jupyterlite/terminal/static/wasm/'
61
+ ),
62
+ browsingContextId: this._browsingContextId,
63
+ shellId: name,
64
+ shellManager: this._shellManager,
65
+ outputCallback: text => {
66
+ const msg = JSON.stringify(['stdout', text]);
67
+ shell.socket?.send(msg);
68
+ }
69
+ });
70
+ this._shells.set(name, shell);
71
+
72
+ for (const externalCommand of this._externalCommands) {
73
+ shell.registerExternalCommand(externalCommand);
74
+ }
75
+
76
+ // Hook to connect socket to shell.
77
+ const hook = async (
78
+ shell: Shell,
79
+ socket: WebSocketClient
80
+ ): Promise<void> => {
81
+ shell.socket = socket;
82
+
83
+ socket.on('message', async (message: any) => {
84
+ // Message from xtermjs to pass to shell.
85
+ const data = JSON.parse(message) as JSONPrimitive[];
86
+ const message_type = data[0];
87
+ const content = data.slice(1);
88
+ await shell.ready;
89
+ if (message_type === 'stdin') {
90
+ await shell.input(content[0] as string);
91
+ } else if (message_type === 'set_size') {
92
+ const rows = content[0] as number;
93
+ const columns = content[1] as number;
94
+ await shell.setSize(rows, columns);
95
+ }
96
+ });
97
+
98
+ // Return handshake.
99
+ const res = JSON.stringify(['setup']);
100
+ console.log('Terminal returning handshake via socket');
101
+ socket.send(res);
102
+
103
+ shell.start();
104
+ };
105
+
106
+ const url = URLExt.join(wsUrl, 'terminals', 'websocket', name);
107
+ const wsServer = new WebSocketServer(url);
108
+ wsServer.on('connection', (socket: WebSocketClient): void => {
109
+ hook(shell, socket);
110
+ });
111
+
112
+ shell.disposed.connect(() => {
113
+ this.shutdown(name);
114
+ wsServer.close();
115
+ });
116
+
117
+ return { name };
118
+ }
119
+
120
+ async listRunning(): Promise<Terminal.IModel[]> {
121
+ return this._models;
122
+ }
123
+
124
+ registerExternalCommand(options: IExternalCommand.IOptions): void {
125
+ this._externalCommands.push(options);
126
+ }
127
+
128
+ async shutdown(name: string): Promise<void> {
129
+ const shell = this._shells.get(name);
130
+ if (shell !== undefined) {
131
+ shell.socket?.send(JSON.stringify(['disconnect']));
132
+ shell.socket?.close();
133
+ this._shells.delete(name);
134
+ shell.dispose();
135
+ }
136
+ }
137
+
138
+ private get _models(): Terminal.IModel[] {
139
+ return Array.from(this._shells.keys(), name => {
140
+ return { name };
141
+ });
142
+ }
143
+
144
+ private _nextAvailableName(): string {
145
+ for (let i = 1; ; ++i) {
146
+ const name = `${i}`;
147
+ if (!this._shells.has(name)) {
148
+ return name;
149
+ }
150
+ }
151
+ }
152
+
153
+ private _browsingContextId?: string;
154
+ private _externalCommands: IExternalCommand.IOptions[] = [];
155
+ private _shellManager: IShellManager;
156
+ private _shells = new Map<string, Shell>();
157
+ }
package/src/index.ts CHANGED
@@ -2,84 +2,100 @@
2
2
  // Distributed under the terms of the Modified BSD License.
3
3
 
4
4
  import {
5
- JupyterLiteServer,
6
- JupyterLiteServerPlugin,
7
- Router
8
- } from '@jupyterlite/server';
5
+ JupyterFrontEnd,
6
+ JupyterFrontEndPlugin
7
+ } from '@jupyterlab/application';
8
+ import {
9
+ ITerminalManager,
10
+ ServiceManagerPlugin,
11
+ Terminal,
12
+ ServerConnection,
13
+ IServerSettings,
14
+ TerminalManager
15
+ } from '@jupyterlab/services';
16
+ import { IServiceWorkerManager } from '@jupyterlite/server';
17
+
18
+ import { WebSocket } from 'mock-socket';
19
+
20
+ import { LiteTerminalAPIClient } from './client';
21
+ import { ILiteTerminalAPIClient } from './tokens';
9
22
 
10
- import { TerminalManager } from './manager';
11
- import { ITerminalManager } from './tokens';
23
+ /**
24
+ * Plugin containing client for in-browser terminals.
25
+ */
26
+ const terminalClientPlugin: ServiceManagerPlugin<Terminal.ITerminalAPIClient> =
27
+ {
28
+ id: '@jupyterlite/terminal:client',
29
+ description: 'The client for Lite terminals',
30
+ autoStart: true,
31
+ provides: ILiteTerminalAPIClient,
32
+ optional: [IServerSettings],
33
+ activate: (
34
+ _: null,
35
+ serverSettings?: ServerConnection.ISettings
36
+ ): ILiteTerminalAPIClient => {
37
+ return new LiteTerminalAPIClient({
38
+ serverSettings: {
39
+ ...ServerConnection.makeSettings(),
40
+ ...serverSettings,
41
+ WebSocket
42
+ }
43
+ });
44
+ }
45
+ };
12
46
 
13
47
  /**
14
- * The terminals service plugin.
48
+ * Plugin containing manager for in-browser terminals.
15
49
  */
16
- const terminalsPlugin: JupyterLiteServerPlugin<ITerminalManager> = {
17
- id: '@jupyterlite/terminal:plugin',
18
- description: 'A terminal for JupyterLite',
50
+ const terminalManagerPlugin: ServiceManagerPlugin<Terminal.IManager> = {
51
+ id: '@jupyterlite/terminal:manager',
52
+ description: 'A JupyterLite extension providing a custom terminal manager',
19
53
  autoStart: true,
20
54
  provides: ITerminalManager,
21
- activate: async (app: JupyterLiteServer) => {
55
+ requires: [ILiteTerminalAPIClient],
56
+ activate: (
57
+ _: null,
58
+ terminalAPIClient: Terminal.ITerminalAPIClient
59
+ ): Terminal.IManager => {
22
60
  console.log(
23
- 'JupyterLite extension @jupyterlite/terminal:plugin is activated!'
61
+ 'JupyterLite extension @jupyterlite/terminal:manager activated'
24
62
  );
25
-
26
- const { serviceManager } = app;
27
- const { serverSettings, terminals } = serviceManager;
28
- console.log('terminals available:', terminals.isAvailable());
29
- console.log('terminals ready:', terminals.isReady); // Not ready
30
- console.log('terminals active:', terminals.isActive);
31
-
32
- // Not sure this is necessary?
33
- await terminals.ready;
34
- console.log('terminals ready after await:', terminals.isReady); // Ready
35
-
36
- return new TerminalManager(serverSettings.wsUrl);
63
+ return new TerminalManager({
64
+ terminalAPIClient,
65
+ serverSettings: terminalAPIClient.serverSettings
66
+ });
37
67
  }
38
68
  };
39
69
 
40
70
  /**
41
- * A plugin providing the routes for the terminals service
71
+ * Plugin that connects in-browser terminals and service worker.
42
72
  */
43
- const terminalsRoutesPlugin: JupyterLiteServerPlugin<void> = {
44
- id: '@jupyterlite/terminal:routes-plugin',
73
+ const terminalServiceWorkerPlugin: JupyterFrontEndPlugin<void> = {
74
+ id: '@jupyterlite/terminal:service-worker',
45
75
  autoStart: true,
46
- requires: [ITerminalManager],
47
- activate: (app: JupyterLiteServer, terminalManager: ITerminalManager) => {
48
- console.log(
49
- 'JupyterLite extension @jupyterlite/terminal:routes-plugin is activated!',
50
- terminalManager
51
- );
76
+ requires: [ILiteTerminalAPIClient],
77
+ optional: [IServiceWorkerManager],
78
+ activate: (
79
+ _: JupyterFrontEnd,
80
+ liteTerminalAPIClient: ILiteTerminalAPIClient,
81
+ serviceWorkerManager?: IServiceWorkerManager
82
+ ): void => {
83
+ if (serviceWorkerManager !== undefined) {
84
+ liteTerminalAPIClient.browsingContextId =
85
+ serviceWorkerManager.browsingContextId;
52
86
 
53
- // GET /api/terminals - List the running terminals
54
- app.router.get('/api/terminals', async (req: Router.IRequest) => {
55
- const res = await terminalManager.listRunning();
56
- // Should return last_activity for each too,
57
- return new Response(JSON.stringify(res));
58
- });
59
-
60
- // POST /api/terminals - Start a terminal
61
- app.router.post('/api/terminals', async (req: Router.IRequest) => {
62
- const res = await terminalManager.startNew();
63
- // Should return last_activity too.
64
- return new Response(JSON.stringify(res));
65
- });
66
-
67
- // DELETE /api/terminals/{terminal name} - Delete a terminal
68
- app.router.delete(
69
- '/api/terminals/(.+)',
70
- async (req: Router.IRequest, name: string) => {
71
- const exists = terminalManager.has(name);
72
- if (exists) {
73
- await terminalManager.shutdownTerminal(name);
74
- } else {
75
- const msg = `The terminal session "${name}"" does not exist`;
76
- console.warn(msg);
77
- }
78
-
79
- return new Response(null, { status: exists ? 204 : 404 });
80
- }
81
- );
87
+ serviceWorkerManager.registerStdinHandler(
88
+ 'terminal',
89
+ liteTerminalAPIClient.handleStdin.bind(liteTerminalAPIClient)
90
+ );
91
+ } else {
92
+ console.warn('Service worker is not available for terminals');
93
+ }
82
94
  }
83
95
  };
84
96
 
85
- export default [terminalsPlugin, terminalsRoutesPlugin];
97
+ export default [
98
+ terminalClientPlugin,
99
+ terminalManagerPlugin,
100
+ terminalServiceWorkerPlugin
101
+ ];
package/src/shell.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { BaseShell, IShell } from '@jupyterlite/cockle';
2
2
 
3
+ import { Client as WebSocketClient } from 'mock-socket';
4
+
3
5
  /**
4
6
  * Shell class that uses web worker that plugs into a DriveFS via the service worker.
5
7
  */
@@ -22,4 +24,6 @@ export class Shell extends BaseShell {
22
24
  type: 'module'
23
25
  });
24
26
  }
27
+
28
+ socket?: WebSocketClient;
25
29
  }
package/src/tokens.ts CHANGED
@@ -1,66 +1,28 @@
1
- // Copyright (c) Jupyter Development Team.
2
- // Distributed under the terms of the Modified BSD License.
3
-
4
- import { TerminalAPI } from '@jupyterlab/services';
5
-
1
+ import { Terminal } from '@jupyterlab/services';
2
+ import {
3
+ IExternalCommand,
4
+ IStdinReply,
5
+ IStdinRequest
6
+ } from '@jupyterlite/cockle';
6
7
  import { Token } from '@lumino/coreutils';
7
- import { IObservableDisposable } from '@lumino/disposable';
8
8
 
9
- /**
10
- * The token for the Terminals service.
11
- */
12
- export const ITerminalManager = new Token<ITerminalManager>(
13
- '@jupyterlite/terminal:ITerminalManager'
9
+ export const ILiteTerminalAPIClient = new Token<ILiteTerminalAPIClient>(
10
+ '@jupyterlite/terminal:client'
14
11
  );
15
12
 
16
- /**
17
- * An interface for the TerminalManager service.
18
- */
19
- export interface ITerminalManager {
20
- /**
21
- * Return whether the named terminal exists.
22
- */
23
- has(name: string): boolean;
24
-
25
- /**
26
- * List the running terminals.
27
- */
28
- listRunning: () => Promise<TerminalAPI.IModel[]>;
29
-
13
+ export interface ILiteTerminalAPIClient extends Terminal.ITerminalAPIClient {
30
14
  /**
31
- * Shutdown a terminal by name.
15
+ * Identifier for communicating with service worker.
32
16
  */
33
- shutdownTerminal: (name: string) => Promise<void>;
17
+ browsingContextId: string;
34
18
 
35
19
  /**
36
- * Start a new kernel.
20
+ * Function that handles stdin requests received from service worker.
37
21
  */
38
- startNew: () => Promise<TerminalAPI.IModel>;
39
- }
22
+ handleStdin(request: IStdinRequest): Promise<IStdinReply>;
40
23
 
41
- /**
42
- * An interface for a server-side terminal running in the browser.
43
- */
44
- export interface ITerminal extends IObservableDisposable {
45
24
  /**
46
- * The name of the server-side terminal.
25
+ * Register an external command that will be available in all terminals.
47
26
  */
48
- readonly name: string;
49
- }
50
-
51
- /**
52
- * A namespace for ITerminal statics.
53
- */
54
- export namespace ITerminal {
55
- /**
56
- * The instantiation options for an ITerminal.
57
- */
58
- export interface IOptions {
59
- /**
60
- * The name of the terminal.
61
- */
62
- name: string;
63
-
64
- baseUrl: string;
65
- }
27
+ registerExternalCommand(options: IExternalCommand.IOptions): void;
66
28
  }
package/src/worker.ts CHANGED
@@ -1,26 +1,7 @@
1
1
  import { expose } from 'comlink';
2
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
- }
3
+ import { BaseShellWorker, IDriveFSOptions } from '@jupyterlite/cockle';
4
+ import { DriveFS } from '@jupyterlite/contents';
24
5
 
25
6
  /**
26
7
  * Shell web worker that uses DriveFS via service worker.
@@ -28,24 +9,31 @@ class MyDriveFS extends DriveFS {
28
9
  */
29
10
  class ShellWorker extends BaseShellWorker {
30
11
  /**
31
- * Initialize the DriveFS to mount an external file system.
12
+ * Initialize the DriveFS to mount an external file system, if available.
32
13
  */
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);
14
+ protected override initDriveFS(options: IDriveFSOptions): void {
15
+ const { baseUrl, browsingContextId, fileSystem, mountpoint } = options;
16
+ console.log('Terminal initDriveFS', baseUrl, mountpoint, browsingContextId);
17
+ if (
18
+ mountpoint !== '' &&
19
+ baseUrl !== undefined &&
20
+ browsingContextId !== undefined
21
+ ) {
22
+ const { FS, ERRNO_CODES, PATH } = fileSystem;
23
+ const driveFS = new DriveFS({
24
+ FS,
25
+ PATH,
26
+ ERRNO_CODES,
27
+ baseUrl,
28
+ driveName: '',
29
+ mountpoint,
30
+ browsingContextId
31
+ });
32
+ FS.mount(driveFS, {}, mountpoint);
33
+ console.log('Terminal connected to shared drive');
34
+ } else {
35
+ console.warn('Terminal not connected to shared drive');
36
+ }
49
37
  }
50
38
  }
51
39
 
package/lib/manager.d.ts DELETED
@@ -1,32 +0,0 @@
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 DELETED
@@ -1,67 +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
- * 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 DELETED
@@ -1,24 +0,0 @@
1
- import { ISignal } from '@lumino/signaling';
2
- import { ITerminal } from './tokens';
3
- export declare class Terminal implements ITerminal {
4
- readonly options: ITerminal.IOptions;
5
- /**
6
- * Construct a new Terminal.
7
- */
8
- constructor(options: ITerminal.IOptions);
9
- private _outputCallback;
10
- dispose(): void;
11
- get disposed(): ISignal<this, void>;
12
- get isDisposed(): boolean;
13
- /**
14
- * Get the name of the terminal.
15
- */
16
- get name(): string;
17
- wsConnect(url: string): Promise<void>;
18
- private _disposed;
19
- private _isDisposed;
20
- private _server?;
21
- private _socket?;
22
- private _shell;
23
- private _running;
24
- }