diff --git a/src/hooks/usePython.ts b/src/hooks/usePython.ts index ae16ec79..99ebbe69 100644 --- a/src/hooks/usePython.ts +++ b/src/hooks/usePython.ts @@ -42,6 +42,7 @@ export default function usePython(props?: UsePythonProps) { const workerRef = useRef() const runnerRef = useRef>() + const interruptBuffer = useRef(new Uint8Array(new SharedArrayBuffer(1))) const { readFile, @@ -111,6 +112,7 @@ export default function usePython(props?: UsePythonProps) { console.debug('Loaded pyodide version:', version) }), 'standard', + interruptBuffer.current, allPackages ) } catch (error) { @@ -200,6 +202,7 @@ del sys autoImportPackages ) } + interruptBuffer.current[0] = 0 await runnerRef.current.run(code, autoImportPackages) // eslint-disable-next-line } catch (error: any) { @@ -213,13 +216,8 @@ del sys ) const interruptExecution = () => { - cleanup() - setIsRunning(false) - setRunnerId(undefined) - setOutput([]) - - // Spawn new worker - createWorker() + // 2 stands for SIGINT. + interruptBuffer.current[0] = 2 } const cleanup = () => { diff --git a/src/hooks/usePythonConsole.ts b/src/hooks/usePythonConsole.ts index dd158ef8..6a91ac7c 100644 --- a/src/hooks/usePythonConsole.ts +++ b/src/hooks/usePythonConsole.ts @@ -18,7 +18,7 @@ interface UsePythonConsoleProps { packages?: Packages } -export default function usePythonConsole(props?: UsePythonConsoleProps) { +function usePython(props?: UsePythonConsoleProps) { const { packages = {} } = props ?? {} const [runnerId, setRunnerId] = useState() @@ -43,6 +43,7 @@ export default function usePythonConsole(props?: UsePythonConsoleProps) { const workerRef = useRef() const runnerRef = useRef>() + const interruptBuffer = useRef(new Uint8Array(new SharedArrayBuffer(1))) const { readFile, @@ -120,6 +121,7 @@ export default function usePythonConsole(props?: UsePythonConsoleProps) { console.debug('Loaded pyodide version:', version) }), 'console', + interruptBuffer.current, allPackages ) } catch (error) { @@ -191,6 +193,7 @@ del sys autoImportPackages ) } + interruptBuffer.current[0] = 0 const runResult = await runnerRef.current.run(code, autoImportPackages) const { state, error } = runResult ?? {} setConsoleState(ConsoleState[state as keyof typeof ConsoleState]) @@ -209,14 +212,8 @@ del sys ) const interruptExecution = () => { - cleanup() - setIsRunning(false) - setRunnerId(undefined) - setBanner(undefined) - setConsoleState(undefined) - - // Spawn new worker - createWorker() + // 2 stands for SIGINT. + interruptBuffer.current[0] = 2 } const cleanup = () => { @@ -241,6 +238,7 @@ del sys return { runPython, stdout, + output, stderr, isLoading, isReady, @@ -259,3 +257,30 @@ del sys prompt: runnerId ? getPrompt(runnerId) : '' } } + +export default function usePythonConsole(props?: UsePythonConsoleProps) { + const { stderr, stdout, runPython, consoleState, ...pyconsole } = + usePython(props) + const [output, setOutput] = useState(['']) + function getPrompt() { + return consoleState === ConsoleState.incomplete ? '... ' : '>>> ' + } + + function run(input: string) { + setOutput((prev) => [...prev, getPrompt() + input + '\n']) + runPython(input) + } + + useEffect(() => { + if (stdout === pyconsole.output.join('')) { + setOutput((prev) => [...prev, stdout, stderr ? stderr + '\n' : '']) + } + }, [stdout, pyconsole.output, stderr]) + + return { + ...pyconsole, + output, + run, + getPrompt + } +} diff --git a/src/providers/PythonProvider.tsx b/src/providers/PythonProvider.tsx index 19e7028e..484ec779 100644 --- a/src/providers/PythonProvider.tsx +++ b/src/providers/PythonProvider.tsx @@ -17,13 +17,13 @@ const PythonContext = createContext({ export const suppressedMessages = ['Python initialization complete'] interface PythonProviderProps { + workerUrl?: string packages?: Packages timeout?: number lazy?: boolean terminateOnCompletion?: boolean autoImportPackages?: boolean - // eslint-disable-next-line - children: any + children: React.ReactNode } function PythonProvider(props: PythonProviderProps) { @@ -48,10 +48,13 @@ function PythonProvider(props: PythonProviderProps) { const registerServiceWorker = async () => { if ('serviceWorker' in navigator) { try { - const url = new URL('../workers/service-worker', import.meta.url) + const url = new URL( + props.workerUrl ?? '../workers/service-worker', + props.workerUrl ? window.location.origin : import.meta.url + ) const registration = await navigator.serviceWorker.register(url) if (registration.active) { - console.debug('Service worker active') + console.debug('Service worker active with url', url) swRef.current = registration.active } @@ -61,7 +64,7 @@ function PythonProvider(props: PythonProviderProps) { console.debug('Installing new service worker') installingWorker.addEventListener('statechange', () => { if (installingWorker.state === 'installed') { - console.debug('New service worker installed') + console.debug('New service worker installed with url', url) swRef.current = installingWorker } }) @@ -95,6 +98,7 @@ function PythonProvider(props: PythonProviderProps) { }, []) const sendInput = (id: string, value: string): void => { + console.debug(`sending input ${id} ${value}`) if (!workerAwaitingInputIds.has(id)) { console.error('Worker not awaiting input') return @@ -122,7 +126,6 @@ function PythonProvider(props: PythonProviderProps) { return next }) } - return ( void, @@ -11,6 +13,7 @@ export interface Runner { banner?: string }) => void, mode: 'standard' | 'console', + interruptBuffer: TypedArray, packages?: string[][] ) => Promise interruptExecution: () => void diff --git a/src/workers/python-worker.ts b/src/workers/python-worker.ts index 30c20bc8..c9b7b150 100644 --- a/src/workers/python-worker.ts +++ b/src/workers/python-worker.ts @@ -1,7 +1,11 @@ importScripts('https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.js') import { expose } from 'comlink' -import { loadPyodide as loadPyodideType, PyodideInterface } from 'pyodide' +import { + loadPyodide as loadPyodideType, + PyodideInterface, + TypedArray +} from 'pyodide' declare global { interface Window { @@ -34,7 +38,9 @@ const reactPyModule = { // Synchronous request to be intercepted by service worker request.open( 'GET', - `/react-py-get-input/?id=${id}&prompt=${encodeURIComponent(prompt)}`, + `${ + location.origin + }/react-py-get-input/?id=${id}&prompt=${encodeURIComponent(prompt)}`, false ) request.send(null) @@ -70,10 +76,13 @@ const python = { banner?: string }) => void, mode: 'standard' | 'console', + interruptBuffer: TypedArray, packages?: string[][] ) { self.pyodide = await self.loadPyodide({ stdout }) + self.pyodide.setInterruptBuffer(interruptBuffer) + // Enable debug mode // self.pyodide.setDebug(true) @@ -96,11 +105,11 @@ const python = { const initCode = ` import sys -import pyodide_http +#import pyodide_http sys.tracebacklimit = 0 -pyodide_http.patch_all() +#pyodide_http.patch_all() ` await self.pyodide.runPythonAsync(initCode)