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

Add files from the xeus-lite-loader extension #2

Closed
wants to merge 5 commits into from
Closed
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
3 changes: 2 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ jobs:

jupyter labextension list
jupyter labextension list 2>&1 | grep -ie "@jupyterlite/xeus.*OK"
python -m jupyterlab.browser_check
# TODO: re-enable?
# python -m jupyterlab.browser_check

- name: Package the extension
run: |
Expand Down
29 changes: 27 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@
"watch:labextension": "jupyter labextension watch ."
},
"dependencies": {
"@jupyterlab/application": "^4.0.0"
"@jupyterlab/coreutils": "^6",
"@jupyterlab/services": "^7",
"@jupyterlite/contents": "^0.2.0",
"@jupyterlite/kernel": "^0.2.0",
"@jupyterlite/server": "^0.2.0",
"@lumino/coreutils": "^2",
"@lumino/signaling": "^2",
"comlink": "^4.3.1"
},
"devDependencies": {
"@jupyterlab/builder": "^4.0.0",
Expand Down Expand Up @@ -91,7 +98,25 @@
},
"jupyterlab": {
"extension": true,
"outputDir": "jupyterlite_xeus/labextension"
"outputDir": "jupyterlite_xeus/labextension",
"webpackConfig": "./webpack.config.js",
"sharedPackages": {
"@jupyterlite/kernel": {
"bundled": false,
"singleton": true
},
"@jupyterlite/server": {
"bundled": false,
"singleton": true
},
"@jupyterlite/contents": {
"bundled": false,
"singleton": true
}
}
},
"jupyterlite": {
"liteExtension": true
},
"eslintIgnore": [
"node_modules",
Expand Down
113 changes: 96 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,97 @@
// Copyright (c) Thorsten Beier
// Copyright (c) JupyterLite Contributors
// Distributed under the terms of the Modified BSD License.

import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';

/**
* Initialization data for the @jupyterlite/xeus extension.
*/
const plugin: JupyterFrontEndPlugin<void> = {
id: '@jupyterlite/xeus:plugin',
description: 'JupyterLite loader for Xeus kernels',
autoStart: true,
activate: (app: JupyterFrontEnd) => {
console.log('JupyterLab extension @jupyterlite/xeus is activated!');
}
};

export default plugin;
IServiceWorkerManager,
JupyterLiteServer,
JupyterLiteServerPlugin
} from '@jupyterlite/server';
import { IBroadcastChannelWrapper } from '@jupyterlite/contents';
import { IKernel, IKernelSpecs } from '@jupyterlite/kernel';

import { WebWorkerKernel } from './web_worker_kernel';

const rel_path = '../extensions/@jupyterlite/xeus-python-kernel/static/';

// helper function to fetch json
function getPkgJson(url: string) {
const json_url = rel_path + url;
const xhr = new XMLHttpRequest();
xhr.open('GET', json_url, false);
xhr.send(null);
return JSON.parse(xhr.responseText);
}

let kernel_dir: string[] = [];
try {
kernel_dir = getPkgJson('kernels/kernels.json');
} catch (err) {
console.log(err);
console.log('could not fetch kernels.json');
kernel_dir = [];
throw err;
}
console.log(kernel_dir);

// fetch kernel spec for each kernel
const kernel_specs = kernel_dir.map(kernel_dir => {
const spec: any = getPkgJson('kernels/' + kernel_dir + '/kernel.json');
spec.name = kernel_dir;
spec.dir = kernel_dir;
spec.resources = {
'logo-32x32': rel_path + 'kernels/' + kernel_dir + '/logo-32x32.png',
'logo-64x64': rel_path + 'kernels/' + kernel_dir + '/logo-64x64.png'
};
return spec;
});

console.log(kernel_specs);

const server_kernels = kernel_specs.map(spec => {
const server_kernel: JupyterLiteServerPlugin<void> = {
// use name from spec
id: `@jupyterlite/${spec.name}-extension:kernel`,
autoStart: true,
requires: [IKernelSpecs],
optional: [IServiceWorkerManager, IBroadcastChannelWrapper],
activate: (
app: JupyterLiteServer,
kernelspecs: IKernelSpecs,
serviceWorker?: IServiceWorkerManager,
broadcastChannel?: IBroadcastChannelWrapper
) => {
kernelspecs.register({
spec: spec,
create: async (options: IKernel.IOptions): Promise<IKernel> => {
const mountDrive = !!(
serviceWorker?.enabled && broadcastChannel?.enabled
);

if (mountDrive) {
console.info(
`${spec.name} contents will be synced with Jupyter Contents`
);
} else {
console.warn(
`${spec.name} contents will NOT be synced with Jupyter Contents`
);
}

return new WebWorkerKernel(
{
...options,
mountDrive
},
spec
);
}
});
}
};
return server_kernel;
});

const plugins: JupyterLiteServerPlugin<any>[] = server_kernels;

export default plugins;
217 changes: 217 additions & 0 deletions src/web_worker_kernel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// Copyright (c) Thorsten Beier
// Copyright (c) JupyterLite Contributors
// Distributed under the terms of the Modified BSD License.

import { wrap } from 'comlink';
import type { Remote } from 'comlink';

import { ISignal, Signal } from '@lumino/signaling';
import { PromiseDelegate } from '@lumino/coreutils';

import { PageConfig } from '@jupyterlab/coreutils';
import { KernelMessage } from '@jupyterlab/services';

import { IKernel } from '@jupyterlite/kernel';

interface IXeusKernel {
ready(): Promise<void>;

mount(driveName: string, mountpoint: string, baseUrl: string): Promise<void>;

cd(path: string): Promise<void>;

processMessage(msg: any): Promise<void>;
}

export class WebWorkerKernel implements IKernel {
/**
* Instantiate a new WebWorkerKernel
*
* @param options The instantiation options for a new WebWorkerKernel
*/
constructor(options: WebWorkerKernel.IOptions, spec: any) {
const { id, name, sendMessage, location } = options;
this._id = id;
this._name = name;
this._location = location;
this._spec = spec;
this._sendMessage = sendMessage;
this._worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});

this._worker.onmessage = e => {
this._processWorkerMessage(e.data);
};
this._remote = wrap(this._worker);
this._remote.processMessage({
msg: {
header: {
msg_type: 'initialize'
}
},
spec: this._spec
});
this.initFileSystem(options);
}

async handleMessage(msg: KernelMessage.IMessage): Promise<void> {
this._parent = msg;
this._parentHeader = msg.header;
await this._sendMessageToWorker(msg);
}

private async _sendMessageToWorker(msg: any): Promise<void> {
// TODO Remove this??
if (msg.header.msg_type !== 'input_reply') {
this._executeDelegate = new PromiseDelegate<void>();
}
await this._remote.processMessage({ msg, parent: this.parent });
if (msg.header.msg_type !== 'input_reply') {
return await this._executeDelegate.promise;
}
}

/**
* Get the last parent header
*/
get parentHeader():
| KernelMessage.IHeader<KernelMessage.MessageType>
| undefined {
return this._parentHeader;
}

/**
* Get the last parent message (mimick ipykernel's get_parent)
*/
get parent(): KernelMessage.IMessage | undefined {
return this._parent;
}

/**
* Get the kernel location
*/
get location(): string {
return this._location;
}

/**
* Process a message coming from the pyodide web worker.
*
* @param msg The worker message to process.
*/
private _processWorkerMessage(msg: any): void {
if (!msg.header) {
return;
}

msg.header.session = this._parentHeader?.session ?? '';
msg.session = this._parentHeader?.session ?? '';
this._sendMessage(msg);

// resolve promise
if (
msg.header.msg_type === 'status' &&
msg.content.execution_state === 'idle'
) {
this._executeDelegate.resolve();
}
}

/**
* A promise that is fulfilled when the kernel is ready.
*/
get ready(): Promise<void> {
return Promise.resolve();
}

/**
* Return whether the kernel is disposed.
*/
get isDisposed(): boolean {
return this._isDisposed;
}

/**
* A signal emitted when the kernel is disposed.
*/
get disposed(): ISignal<this, void> {
return this._disposed;
}

/**
* Dispose the kernel.
*/
dispose(): void {
if (this.isDisposed) {
return;
}
this._worker.terminate();
(this._worker as any) = null;
(this._remote as any) = null;
this._isDisposed = true;
this._disposed.emit(void 0);
}

/**
* Get the kernel id
*/
get id(): string {
return this._id;
}

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

private async initFileSystem(options: WebWorkerKernel.IOptions) {
let driveName: string;
let localPath: string;

if (options.location.includes(':')) {
const parts = options.location.split(':');
driveName = parts[0];
localPath = parts[1];
} else {
driveName = '';
localPath = options.location;
}

await this._remote.ready();

if (options.mountDrive) {
await this._remote.mount(driveName, '/drive', PageConfig.getBaseUrl());
await this._remote.cd(localPath);
}
}

private _spec: any;
private _id: string;
private _name: string;
private _location: string;
private _remote: Remote<IXeusKernel>;
private _isDisposed = false;
private _disposed = new Signal<this, void>(this);
private _worker: Worker;
private _sendMessage: IKernel.SendMessage;
private _executeDelegate = new PromiseDelegate<void>();
private _parentHeader:
| KernelMessage.IHeader<KernelMessage.MessageType>
| undefined = undefined;
private _parent: KernelMessage.IMessage | undefined = undefined;
}

/**
* A namespace for WebWorkerKernel statics.
*/
export namespace WebWorkerKernel {
/**
* The instantiation options for a Pyodide kernel
*/
export interface IOptions extends IKernel.IOptions {
mountDrive: boolean;
}
}
Loading
Loading