UNPKG

starboard-python

Version:
285 lines (245 loc) 8.84 kB
/// <reference no-default-lib="true"/> /// <reference lib="es2020" /> /// <reference lib="WebWorker" /> //@ts-ignore import {loadPyodide} from "../../dist/pyodide.js" import type { Pyodide as PyodideType } from "../pyodide/typings"; import type { KernelManagerType, WorkerKernel } from "./kernel"; import { PyodideWorkerOptions, PyodideWorkerResult } from "./worker-message"; import { EMFS } from "./emscripten-fs"; import {patchMatplotlib} from "../pyodide/matplotlib"; // TODO: We no longer need to do this. (globalThis as any).loadPyodide = loadPyodide; type LoadPyodideFunction = (config: { indexURL: string; stdin?: () => any | null; print?: (text: string) => void; printErr?: (text: string) => void; fullStdLib?: boolean; }) => Promise<PyodideType>; declare global { interface WorkerGlobalScope { /** * The object managing all the kernels in this web worker */ manager: KernelManagerType; loadPyodide: LoadPyodideFunction } } const manager: KernelManagerType = (globalThis as any)?.manager ?? (globalThis as any).manager; // const loadPyodide: LoadPyodideFunction = (self as any)?.loadPyodide ?? (globalThis as any).loadPyodide; class PyodideKernel implements WorkerKernel { kernelId: string; options: PyodideWorkerOptions; proxiedGlobalThis: undefined | any; proxiedDrawCanvas: (pixels: number[], width: number, height: number) => void = () => {}; pyodide: PyodideType | undefined = undefined; constructor(options: { id: string } & PyodideWorkerOptions) { this.kernelId = options.id; this.options = options; } async init(): Promise<any> { this.proxiedGlobalThis = this.proxyGlobalThis(this.options.globalThisId); this.proxiedDrawCanvas = manager.proxy && this.options.drawCanvasId ? manager.proxy.getObjectProxy(this.options.drawCanvasId) : () => {}; (globalThis as any).drawPyodideCanvas = (pixels: number[], width: number, height: number) => { if ((pixels as any).toJs) { pixels = (pixels as any).toJs(); } if (pixels instanceof Uint8ClampedArray || pixels instanceof Uint8Array) { pixels = Array.from(pixels); } // TODO: Handle the case when this.function gets called (this ends up being passed to the main thread, which won't work) this.proxiedDrawCanvas.apply({}, [pixels, width, height]); }; let artifactsURL = this.options.artifactsUrl || "https://cdn.jsdelivr.net/pyodide/v0.20.0/full/"; if (!artifactsURL.endsWith("/")) artifactsURL += "/"; if (!manager.proxy && !this.options.isMainThread) { console.warn("Missing object proxy, some Pyodide functionality will be restricted"); } this.pyodide = await loadPyodide({ indexURL: artifactsURL, stdin: this.createStdin(), print: (text: any) => { manager.log(this, text + ""); }, printErr: (text: any) => { manager.logError(this, text + ""); }, fullStdLib: false, })!; if (!this.pyodide) { throw new Error("Pyodide is undefined unexpectedly"); } (globalThis as any).pyodide = this.pyodide; if (manager.syncFs) { const FS = this.pyodide._module.FS; try { FS.mkdir("/mnt"); } catch (e) { console.warn(e); } try { FS.mkdir("/mnt/shared"); } catch (e) { console.warn(e); } try { FS.mount(new EMFS(FS, this.pyodide._module.ERRNO_CODES, manager.syncFs), {}, "/mnt/shared"); this.pyodide.runPython('import os\nos.chdir("/mnt/shared")'); } catch (e) { console.warn(e); } } if (this.proxiedGlobalThis) { // Fix "from js import ..." /* this.pyodide.unregisterJsModule("js"); // Not needed, since register conveniently overwrites existing things */ this.pyodide.registerJsModule("js", this.proxiedGlobalThis); // TODO: Or should we register a new module? Like js_main } } async runCode(code: string): Promise<any> { if (!this.pyodide) { console.warn("Worker has not yet been initialized"); return; } // Again: no clue why this is necessary and only doing it in init doesn't suffice (globalThis as any).pyodide = this.pyodide; // We prevent some spam, otherwise every time you run a cell with an import it will show // "Loading bla", "Bla was already loaded from default channel", "Loaded bla" let wasAlreadyLoaded: boolean | undefined = undefined; let msgBuffer: string[] = []; await this.pyodide.loadPackagesFromImports(code, (msg) => { if (wasAlreadyLoaded === true) return; if (msg.match(/Loaded.*\smatplotlib/)) { console.debug("Hooking matplotlib output to Starboard"); patchMatplotlib(this.pyodide!); } if (wasAlreadyLoaded === false) { if (msg.match(/already loaded from default channel$/)) { return; // This is not the main package being loaded but another dependency that is // already loaded - no need to list it. } console.debug(msg); } if (wasAlreadyLoaded === undefined) { if (msg.match(/already loaded from default channel$/)) { wasAlreadyLoaded = true; return; } if (msg.match(/^Loading [a-z\-, ]*/)) { wasAlreadyLoaded = false; msgBuffer.forEach(m => console.debug(m)); console.debug(msg); } } }); let result = await this.pyodide.runPythonAsync(code).catch((error) => error); let displayType: PyodideWorkerResult["display"]; if (this.pyodide.isPyProxy(result)) { if (result._repr_html_ !== undefined) { result = result._repr_html_(); displayType = "html"; } else if (result._repr_latex_ !== undefined) { result = result._repr_latex_(); displayType = "latex"; } else { result = result.__str__(); displayType = "default" } } else if (result instanceof this.pyodide.PythonError) { result = result + ""; } this.destroyToJsResult(result); return { display: displayType, value: result, } as PyodideWorkerResult; } customMessage(message: any): void { // No custom messages are supported nor used. return; } createStdin() { const encoder = new TextEncoder(); let input = new Uint8Array(); let inputIndex = -1; // -1 means that we just returned null function stdin() { if (inputIndex === -1) { const text = manager.input(); input = encoder.encode(text + (text.endsWith("\n") ? "" : "\n")); inputIndex = 0; } if (inputIndex < input.length) { let character = input[inputIndex]; inputIndex++; return character; } else { inputIndex = -1; return null; } } return stdin; } private proxyGlobalThis(id?: string) { // Special cases for the globalThis object. We don't need to proxy everything const noProxy = new Set<string | symbol>([ "location", // Proxy navigator, however, some navigator properties do not have to be proxied // "navigator", "self", "importScripts", "addEventListener", "removeEventListener", "caches", "crypto", "indexedDB", "isSecureContext", "origin", "performance", "atob", "btoa", "clearInterval", "clearTimeout", "createImageBitmap", "fetch", "queueMicrotask", "setInterval", "setTimeout", // Special cases for the pyodide globalThis "$$", "pyodide", "__name__", "__package__", "__path__", "__loader__", // Pyodide likes checking for lots of properties, like the .stack property to check if something is an error // https://github.com/pyodide/pyodide/blob/c8436c33a7fbee13e1ded97c0bbdaa7d635f2745/src/core/jsproxy.c#L1631 "stack", "get", "set", "has", "size", "length", "then", "includes", "next", Symbol.iterator, // Draw something to a canvas "drawPyodideCanvas", ]); return manager.proxy && id ? manager.proxy.wrapExcluderProxy(manager.proxy.getObjectProxy(id), globalThis, noProxy) : globalThis; } private destroyToJsResult(x: any) { if (!this.pyodide) return; if (!x) { return; } if (this.pyodide.isPyProxy(x)) { x.destroy(); return; } } } (globalThis as any).PyodideKernel = PyodideKernel;