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 b6ee9e0
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 10 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
67 changes: 59 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,69 @@
// 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);
}
};

export default plugin;
/**
* 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 [terminalsPlugin, terminalsRoutesPlugin];
54 changes: 54 additions & 0 deletions src/terminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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;
}
60 changes: 60 additions & 0 deletions src/terminals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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();
}
53 changes: 53 additions & 0 deletions src/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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;
}
}

0 comments on commit b6ee9e0

Please sign in to comment.