diff --git a/examples/server/public/index.html.gz b/examples/server/public/index.html.gz
index 9b322bbb6..73f2707b8 100644
Binary files a/examples/server/public/index.html.gz and b/examples/server/public/index.html.gz differ
diff --git a/examples/server/webui/src/components/CanvasPyInterpreter.tsx b/examples/server/webui/src/components/CanvasPyInterpreter.tsx
index 46169bbc4..b53474984 100644
--- a/examples/server/webui/src/components/CanvasPyInterpreter.tsx
+++ b/examples/server/webui/src/components/CanvasPyInterpreter.tsx
@@ -1,53 +1,103 @@
import { useEffect, useState } from 'react';
import { useAppContext } from '../utils/app.context';
-import { XCloseButton } from '../utils/common';
-import { delay } from '../utils/misc';
-import StorageUtils from '../utils/storage';
+import { OpenInNewTab, XCloseButton } from '../utils/common';
import { CanvasType } from '../utils/types';
-import { PlayIcon } from '@heroicons/react/24/outline';
+import { PlayIcon, StopIcon } from '@heroicons/react/24/outline';
-const PyodideWrapper = {
- load: async function () {
- // load pyodide from CDN
- // @ts-expect-error experimental pyodide
- if (window.addedScriptPyodide) return;
- // @ts-expect-error experimental pyodide
- window.addedScriptPyodide = true;
- const scriptElem = document.createElement('script');
- scriptElem.src = 'https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js';
- document.body.appendChild(scriptElem);
- },
+const canInterrupt = typeof SharedArrayBuffer === 'function';
- run: async function (code: string) {
- PyodideWrapper.load();
+// adapted from https://pyodide.org/en/stable/usage/webworker.html
+const WORKER_CODE = `
+importScripts("https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js");
- // wait for pyodide to be loaded
- // @ts-expect-error experimental pyodide
- while (!window.loadPyodide) {
- await delay(100);
- }
- const stdOutAndErr: string[] = [];
- // @ts-expect-error experimental pyodide
- const pyodide = await window.loadPyodide({
- stdout: (data: string) => stdOutAndErr.push(data),
- stderr: (data: string) => stdOutAndErr.push(data),
- });
- try {
- const result = await pyodide.runPythonAsync(code);
- if (result) {
- stdOutAndErr.push(result.toString());
- }
- } catch (e) {
- console.error(e);
- stdOutAndErr.push((e as Error).toString());
- }
- return stdOutAndErr.join('\n');
- },
+let stdOutAndErr = [];
+
+let pyodideReadyPromise = loadPyodide({
+ stdout: (data) => stdOutAndErr.push(data),
+ stderr: (data) => stdOutAndErr.push(data),
+});
+
+let alreadySetBuff = false;
+
+self.onmessage = async (event) => {
+ stdOutAndErr = [];
+
+ // make sure loading is done
+ const pyodide = await pyodideReadyPromise;
+ const { id, python, context, interruptBuffer } = event.data;
+
+ if (interruptBuffer && !alreadySetBuff) {
+ pyodide.setInterruptBuffer(interruptBuffer);
+ alreadySetBuff = true;
+ }
+
+ // Now load any packages we need, run the code, and send the result back.
+ await pyodide.loadPackagesFromImports(python);
+
+ // make a Python dictionary with the data from content
+ const dict = pyodide.globals.get("dict");
+ const globals = dict(Object.entries(context));
+ try {
+ self.postMessage({ id, running: true });
+ // Execute the python code in this context
+ const result = pyodide.runPython(python, { globals });
+ self.postMessage({ result, id, stdOutAndErr });
+ } catch (error) {
+ self.postMessage({ error: error.message, id });
+ }
+ interruptBuffer[0] = 0;
};
+`;
-if (StorageUtils.getConfig().pyIntepreterEnabled) {
- PyodideWrapper.load();
-}
+let worker: Worker;
+const interruptBuffer = canInterrupt
+ ? new Uint8Array(new SharedArrayBuffer(1))
+ : null;
+
+const runCodeInWorker = (
+ pyCode: string,
+ callbackRunning: () => void
+): {
+ donePromise: Promise;
+ interrupt: () => void;
+} => {
+ if (!worker) {
+ worker = new Worker(
+ URL.createObjectURL(new Blob([WORKER_CODE], { type: 'text/javascript' }))
+ );
+ }
+ const id = Math.random() * 1e8;
+ const context = {};
+ if (interruptBuffer) {
+ interruptBuffer[0] = 0;
+ }
+
+ const donePromise = new Promise((resolve) => {
+ worker.onmessage = (event) => {
+ const { error, stdOutAndErr, running } = event.data;
+ if (id !== event.data.id) return;
+ if (running) {
+ callbackRunning();
+ return;
+ } else if (error) {
+ resolve(error.toString());
+ } else {
+ resolve(stdOutAndErr.join('\n'));
+ }
+ };
+ worker.postMessage({ id, python: pyCode, context, interruptBuffer });
+ });
+
+ const interrupt = () => {
+ console.log('Interrupting...');
+ console.trace();
+ if (interruptBuffer) {
+ interruptBuffer[0] = 2;
+ }
+ };
+
+ return { donePromise, interrupt };
+};
export default function CanvasPyInterpreter() {
const { canvasData, setCanvasData } = useAppContext();
@@ -55,13 +105,22 @@ export default function CanvasPyInterpreter() {
const [code, setCode] = useState(canvasData?.content ?? ''); // copy to avoid direct mutation
const [running, setRunning] = useState(false);
const [output, setOutput] = useState('');
+ const [interruptFn, setInterruptFn] = useState<() => void>();
+ const [showStopBtn, setShowStopBtn] = useState(false);
const runCode = async (pycode: string) => {
+ interruptFn?.();
setRunning(true);
- setOutput('Running...');
- const out = await PyodideWrapper.run(pycode);
+ setOutput('Loading Pyodide...');
+ const { donePromise, interrupt } = runCodeInWorker(pycode, () => {
+ setOutput('Running...');
+ setShowStopBtn(canInterrupt);
+ });
+ setInterruptFn(() => interrupt);
+ const out = await donePromise;
setOutput(out);
setRunning(false);
+ setShowStopBtn(false);
};
// run code on mount
@@ -98,9 +157,21 @@ export default function CanvasPyInterpreter() {
onClick={() => runCode(code)}
disabled={running}
>
- {' '}
- {running ? 'Running...' : 'Run'}
+ Run
+ {showStopBtn && (
+
+ )}
+
+
+ Report a bug
+
+