Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use WASM commands running in webworker #15

Merged
merged 7 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@
"url": "https://github.com/jupyterlite/terminal.git"
},
"scripts": {
"build": "jlpm build:lib && jlpm build:labextension:dev",
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
"build": "jlpm build:lib && jlpm build:worker && jlpm build:labextension:dev",
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:worker && jlpm build:labextension",
"build:labextension": "jupyter labextension build .",
"build:labextension:dev": "jupyter labextension build --development True .",
"build:lib": "tsc --sourceMap",
"build:lib:prod": "tsc",
"build:worker": "webpack --config worker.webpack.config.js --mode=development",
"clean": "jlpm clean:lib",
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
"clean:lintcache": "rimraf .eslintcache .stylelintcache",
Expand All @@ -57,12 +58,15 @@
"watch:labextension": "jupyter labextension watch ."
},
"dependencies": {
"@jupyterlab/coreutils": "^6.2.2",
"@jupyterlab/services": "^7.2.0",
"@jupyterlab/terminal": "^4.2.0",
"@jupyterlab/terminal-extension": "^4.2.0",
"@jupyterlite/cockle": "^0.0.3",
"@jupyterlite/cockle": "^0.0.4",
"@jupyterlite/contents": "^0.3.0 || ^0.4.0-beta.0",
"@jupyterlite/server": "^0.3.0 || ^0.4.0-beta.0",
"@lumino/coreutils": "^2.1.2",
"comlink": "^4.4.1",
"mock-socket": "^9.3.1"
},
"devDependencies": {
Expand Down Expand Up @@ -90,6 +94,8 @@
"stylelint-csstree-validator": "^3.0.0",
"stylelint-prettier": "^4.0.0",
"typescript": "~5.0.2",
"webpack": "^5.87.0",
"webpack-cli": "^5.1.4",
"yjs": "^13.5.0"
},
"sideEffects": [
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const terminalsPlugin: JupyterLiteServerPlugin<ITerminals> = {
);

const { serviceManager } = app;
const { contents, serverSettings, terminals } = serviceManager;
const { serverSettings, terminals } = serviceManager;
console.log('terminals available:', terminals.isAvailable());
console.log('terminals ready:', terminals.isReady); // Not ready
console.log('terminals active:', terminals.isActive);
Expand All @@ -33,7 +33,7 @@ const terminalsPlugin: JupyterLiteServerPlugin<ITerminals> = {
await terminals.ready;
console.log('terminals ready after await:', terminals.isReady); // Ready

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

Expand Down
59 changes: 36 additions & 23 deletions src/terminal.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,78 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { JupyterFileSystem, Shell, IFileSystem } from '@jupyterlite/cockle';

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

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

import { ITerminal } from './tokens';
import { wrap } from 'comlink';

import { ITerminal, IRemoteWorkerTerminal } from './tokens';

export class Terminal implements ITerminal {
/**
* Construct a new Terminal.
*/
constructor(options: ITerminal.IOptions) {
this._name = options.name;
this._fs = new JupyterFileSystem(options.contentsManager);
console.log('==> new Terminal', this._name, this._fs);
constructor(readonly options: ITerminal.IOptions) {
this._initWorker();
}

private async _initWorker(): Promise<void> {
this._worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});

this._remote = wrap(this._worker);
const { baseUrl } = this.options;
await this._remote.initialize({ baseUrl });
}

/**
* Process a message coming from the JavaScript web worker.
*
* @param msg The worker message to process.
*/
private _processWorkerMessage(msg: any, socket: WebSocketClient): void {
if (msg.type === 'output') {
const ret = JSON.stringify(['stdout', msg.text]);
socket.send(ret);
}
}

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

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

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

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

const outputCallback = async (output: string) => {
console.log('==> recv from shell:', output);
const ret = JSON.stringify(['stdout', output]);
socket.send(ret);
this._worker!.onmessage = e => {
this._processWorkerMessage(e.data, socket);
};

this._shell = new Shell(this._fs, outputCallback);
console.log('==> shell', this._shell);

socket.on('message', async (message: any) => {
const data = JSON.parse(message) as JSONPrimitive[];
console.log('==> socket message', data);
//console.log('==> socket message', data);
const message_type = data[0];
const content = data.slice(1);

if (message_type === 'stdin') {
await this._shell!.input(content[0] as string);
await this._remote!.input(content[0] as string);
} else if (message_type === 'set_size') {
const rows = content[0] as number;
const columns = content[1] as number;
await this._shell!.setSize(rows, columns);
await this._remote!.setSize(rows, columns);
}
});

Expand All @@ -75,11 +89,10 @@ export class Terminal implements ITerminal {
console.log('==> Returning handshake via socket', res);
socket.send(res);

await this._shell!.start();
await this._remote!.start();
});
}

private _name: string;
private _fs: IFileSystem;
private _shell?: Shell;
private _worker?: Worker;
private _remote?: IRemoteWorkerTerminal;
}
16 changes: 6 additions & 10 deletions src/terminals.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

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

import { Terminal } from './terminal';
import { ITerminals } from './tokens';
Expand All @@ -13,14 +14,9 @@ export class Terminals implements ITerminals {
/**
* Construct a new Terminals object.
*/
constructor(wsUrl: string, contentsManager: Contents.IManager) {
constructor(wsUrl: string) {
this._wsUrl = wsUrl;
this._contentsManager = contentsManager;
console.log(
'==> Terminals.constructor',
this._wsUrl,
this._contentsManager
);
console.log('==> Terminals.constructor', this._wsUrl);
}

/**
Expand All @@ -40,7 +36,8 @@ export class Terminals implements ITerminals {
async startNew(): Promise<TerminalAPI.IModel> {
const name = this._nextAvailableName();
console.log('==> Terminals.new', name);
const term = new Terminal({ name, contentsManager: this._contentsManager });
const baseUrl = PageConfig.getBaseUrl();
const term = new Terminal({ name, baseUrl });
this._terminals.set(name, term);

const url = `${this._wsUrl}terminals/websocket/${name}`;
Expand All @@ -59,6 +56,5 @@ export class Terminals implements ITerminals {
}

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

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

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

import { Remote } from 'comlink';

/**
* The token for the Terminals service.
*/
Expand Down Expand Up @@ -50,6 +52,33 @@ export namespace ITerminal {
*/
name: string;

contentsManager: Contents.IManager;
baseUrl: string;
}
}

export interface IWorkerTerminal {
input(text: string): Promise<void>;
setSize(rows: number, columns: number): Promise<void>;
start(): Promise<void>;
}

export namespace IWorkerTerminal {
/**
* Initialization options for a worker.
*/
export interface IOptions {
baseUrl: string;
}
}

export interface IRemote extends IWorkerTerminal {
/**
* Handle any lazy initialization activities.
*/
initialize(options: IWorkerTerminal.IOptions): Promise<void>;
}

/**
* An convenience interface for Pyodide workers wrapped by a comlink Remote.
*/
export type IRemoteWorkerTerminal = Remote<IRemote>;
63 changes: 63 additions & 0 deletions src/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Shell } from '@jupyterlite/cockle';
import { DriveFS } from '@jupyterlite/contents';

import { expose } from 'comlink';

import { IWorkerTerminal } from './tokens';

class WorkerTerminal implements IWorkerTerminal {
async initialize(options: IWorkerTerminal.IOptions): Promise<void> {
this._options = options;
console.log('WorkerTerminal.initialize', this._options, this._wantDriveFS);
}

async input(text: string): Promise<void> {
await this._shell!.input(text);
}

async setSize(rows: number, columns: number): Promise<void> {
await this._shell!.setSize(rows, columns);
}

async start(): Promise<void> {
this._shell = new Shell(this.output, this._mountpoint);
const { FS, PATH, ERRNO_CODES } = await this._shell.initFilesystem();

if (this._wantDriveFS) {
this._driveFS = new DriveFS({
FS,
PATH,
ERRNO_CODES,
baseUrl: this._options!.baseUrl,
driveName: '',
mountpoint: this._mountpoint
});
FS.mount(this._driveFS, {}, this._mountpoint);
FS.chdir(this._mountpoint);
} else {
// Add some dummy files if not using DriveFS.
FS.writeFile('file.txt', 'This is the contents of the file');
FS.writeFile('other.txt', 'Some other file');
FS.mkdir('dir');
}

await this._shell.start();
}

private async output(text: string): Promise<void> {
postMessage({
type: 'output',
text: text
});
}

private _options: IWorkerTerminal.IOptions | null = null;
private _shell?: Shell;
private _mountpoint: string = '/drive';
private _wantDriveFS: boolean = true;
private _driveFS?: DriveFS;
}

const obj = new WorkerTerminal();

expose(obj);
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"composite": true,
"declaration": true,
Expand Down
33 changes: 33 additions & 0 deletions worker.webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const path = require('path');
const rules = [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'source-map-loader'
}
];

const resolve = {
fallback: {
fs: false,
child_process: false,
crypto: false
},
extensions: ['.js']
};

module.exports = [
{
entry: './lib/worker.js',
output: {
filename: 'worker.js',
path: path.resolve(__dirname, 'lib'),
libraryTarget: 'amd'
},
module: {
rules
},
devtool: 'source-map',
resolve
}
];
Loading
Loading