Skip to content

Commit

Permalink
Basic framework
Browse files Browse the repository at this point in the history
  • Loading branch information
ianthomas23 committed May 17, 2024
1 parent 1076c3f commit b810b14
Show file tree
Hide file tree
Showing 9 changed files with 11,202 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,6 @@ dmypy.json

# Yarn cache
.yarn/

.jupyterlite.doit.db
_output/
6 changes: 6 additions & 0 deletions jupyter-lite.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"jupyter-lite-schema-version": 0,
"jupyter-config-data": {
"terminalsAvailable": true
}
}
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"keywords": [
"jupyter",
"jupyterlab",
"jupyterlab-extension"
"jupyterlite",
"jupyterlite-extension"
],
"homepage": "https://github.com/ianthomas23/jupyterlite-terminal",
"bugs": {
Expand Down Expand Up @@ -56,7 +57,11 @@
"watch:labextension": "jupyter labextension watch ."
},
"dependencies": {
"@jupyterlab/application": "^4.0.0"
"@jupyterlab/services": "^7.2.0",
"@jupyterlab/terminal": "^4.2.0",
"@jupyterlab/terminal-extension": "^4.2.0",
"@jupyterlite/server": "^0.3.0",
"@lumino/coreutils": "^2.1.2"
},
"devDependencies": {
"@jupyterlab/builder": "^4.0.0",
Expand Down Expand Up @@ -97,6 +102,9 @@
"extension": true,
"outputDir": "jupyterlite_terminal/labextension"
},
"jupyterlite": {
"liteExtension": true
},
"eslintIgnore": [
"node_modules",
"dist",
Expand Down
71 changes: 63 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,73 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
JupyterLiteServer,
JupyterLiteServerPlugin,
Router
} from '@jupyterlite/server';
import { ITerminalTracker } from '@jupyterlab/terminal';

import { ITerminals } from './tokens';
import { Terminals } from './terminals';

/**
* Initialization data for the jupyterlite-terminal extension.
* The terminals service plugin.
*/
const plugin: JupyterFrontEndPlugin<void> = {
const terminalsPlugin: JupyterLiteServerPlugin<ITerminals> = {
id: 'jupyterlite-terminal:plugin',
description: 'A terminal for JupyterLite',
autoStart: true,
activate: (app: JupyterFrontEnd) => {
console.log('JupyterLab extension jupyterlite-terminal is activated!');
requires: [ITerminalTracker],
provides: ITerminals,
activate: async (app: JupyterLiteServer, tracker: ITerminalTracker) => {
console.log(
'JupyterLab extension jupyterlite-terminal:plugin is activated!'
);

console.log('==> ITerminalTracker', tracker);

const { serviceManager } = app;
const { contents, serverSettings, terminals } = serviceManager;
console.log('terminals available:', terminals.isAvailable());
console.log('terminals ready:', terminals.isReady); // Not ready
console.log('terminals active:', terminals.isActive);

// Not sure this is necessary?
await terminals.ready;
console.log('terminals ready after await:', terminals.isReady); // Ready

return new Terminals(serverSettings.wsUrl, contents);
}
};

/**
* A plugin providing the routes for the terminals service
*/
const terminalsRoutesPlugin: JupyterLiteServerPlugin<void> = {
id: 'jupyterlite-terminal:routes-plugin',
autoStart: true,
requires: [ITerminals],
activate: (app: JupyterLiteServer, terminals: ITerminals) => {
console.log(
'JupyterLab extension jupyterlite-terminal:routes-plugin is activated!',
terminals
);

// GET /api/terminals - List the running terminals
app.router.get('/api/terminals', async (req: Router.IRequest) => {
const res = terminals.list();
// Should return last_activity for each too,
return new Response(JSON.stringify(res));
});

// POST /api/terminals - Start a terminal
app.router.post('/api/terminals', async (req: Router.IRequest) => {
const res = await terminals.startNew();
// Should return last_activity too.
return new Response(JSON.stringify(res));
});
}
};

export default plugin;
export default [terminalsPlugin, terminalsRoutesPlugin];
57 changes: 57 additions & 0 deletions src/terminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { JSONPrimitive } from '@lumino/coreutils';

import {
Server as WebSocketServer,
Client as WebSocketClient
} from 'mock-socket';

import { ITerminal } from './tokens';

export class Terminal implements ITerminal {
/**
* Construct a new Terminal.
*/
constructor(options: ITerminal.IOptions) {
this._name = options.name;
}

/**
* Get the name of the terminal.
*/
get name(): string {
return this._name;
}

async wsConnect(url: string) {
console.log('==> Terminal.wsConnect', url);

const server = new WebSocketServer(url, { mock: false });

server.on('connection', async (socket: WebSocketClient) => {
console.log('==> server connection', this, socket);

socket.on('message', async (message: any) => {
const data = JSON.parse(message) as JSONPrimitive[];
console.log('==> socket message', data);
});

socket.on('close', async () => {
console.log('==> socket close');
});

socket.on('error', async () => {
console.log('==> socket error');
});

// Return handshake.
const res = JSON.stringify(['setup']);
console.log('==> Returning handshake via socket', res);
socket.send(res);
});
}

private _name: string;
}
64 changes: 64 additions & 0 deletions src/terminals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { Contents, TerminalAPI } from '@jupyterlab/services';

import { Terminal } from './terminal';
import { ITerminals } from './tokens';

/**
* A class to handle requests to /api/terminals
*/
export class Terminals implements ITerminals {
/**
* Construct a new Terminals object.
*/
constructor(wsUrl: string, contentsManager: Contents.IManager) {
this._wsUrl = wsUrl;
this._contentsManager = contentsManager;
console.log(
'==> Terminals.constructor',
this._wsUrl,
this._contentsManager
);
}

/**
* List the running terminals.
*/
async list(): Promise<TerminalAPI.IModel[]> {
const ret = [...this._terminals.values()].map(terminal => ({
name: terminal.name
}));
console.log('==> Terminals.list', ret);
return ret;
}

/**
* Start a new kernel.
*/
async startNew(): Promise<TerminalAPI.IModel> {
const name = this._nextAvailableName();
console.log('==> Terminals.new', name);
const term = new Terminal({ name, contentsManager: this._contentsManager });
this._terminals.set(name, term);

const url = `${this._wsUrl}terminals/websocket/${name}`;
await term.wsConnect(url);

return { name };
}

private _nextAvailableName(): string {
for (let i = 1; ; ++i) {
const name = `${i}`;
if (!this._terminals.has(name)) {
return name;
}
}
}

private _wsUrl: string;
private _contentsManager: Contents.IManager;
private _terminals: Map<string, Terminal> = new Map();
}
55 changes: 55 additions & 0 deletions src/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { Contents, TerminalAPI } from '@jupyterlab/services';

import { Token } from '@lumino/coreutils';

/**
* The token for the Terminals service.
*/
export const ITerminals = new Token<ITerminals>(
'@jupyterlite/terminal:ITerminals'
);

/**
* An interface for the Terminals service.
*/
export interface ITerminals {
/**
* List the running terminals.
*/
list: () => Promise<TerminalAPI.IModel[]>;

/**
* Start a new kernel.
*/
startNew: () => Promise<TerminalAPI.IModel>;
}

/**
* An interface for a server-side terminal running in the browser.
*/
export interface ITerminal {
/**
* The name of the server-side terminal.
*/
readonly name: string;
}

/**
* A namespace for ITerminal statics.
*/
export namespace ITerminal {
/**
* The instantiation options for an ITerminal.
*/
export interface IOptions {
/**
* The name of the terminal.
*/
name: string;

contentsManager: Contents.IManager;
}
}
4 changes: 3 additions & 1 deletion ui-tests/tests/jupyterlite_terminal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ test('should emit an activation console message', async ({ page }) => {
await page.goto();

expect(
logs.filter(s => s === 'JupyterLab extension jupyterlite-terminal is activated!')
logs.filter(
s => s === 'JupyterLab extension jupyterlite-terminal is activated!'
)
).toHaveLength(1);
});
Loading

0 comments on commit b810b14

Please sign in to comment.