From 75bfde84544c52754b32e085d64833fea3f53785 Mon Sep 17 00:00:00 2001 From: Chun Ho Lam <58981086+LamHo220@users.noreply.github.com> Date: Mon, 24 Feb 2025 20:20:58 +0800 Subject: [PATCH 1/4] fix safari not updating state correctly --- src/hooks/usePythonConsole.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/hooks/usePythonConsole.ts b/src/hooks/usePythonConsole.ts index 12f81dac..dd158ef8 100644 --- a/src/hooks/usePythonConsole.ts +++ b/src/hooks/usePythonConsole.ts @@ -26,6 +26,7 @@ export default function usePythonConsole(props?: UsePythonConsoleProps) { const [banner, setBanner] = useState() const [consoleState, setConsoleState] = useState() const [isRunning, setIsRunning] = useState(false) + const [output, setOutput] = useState([]) const [stdout, setStdout] = useState('') const [stderr, setStderr] = useState('') const [pendingCode, setPendingCode] = useState() @@ -72,6 +73,13 @@ export default function usePythonConsole(props?: UsePythonConsoleProps) { } }, []) + // Immediately set stdout upon receiving new input + useEffect(() => { + if (output.length > 0 && !isRunning) { + setStdout(output.join('')) + } + }, [output, isRunning]) + const allPackages = useMemo(() => { const official = [ ...new Set([ @@ -104,7 +112,7 @@ export default function usePythonConsole(props?: UsePythonConsoleProps) { if (suppressedMessages.includes(msg)) { return } - setStdout(msg) + setOutput((prev) => [...prev, msg]) }), proxy(({ id, version, banner }) => { setRunnerId(id) @@ -150,6 +158,7 @@ del sys const runPython = useCallback( async (code: string) => { // Clear stdout and stderr + setOutput([]) setStdout('') setStderr('') From 87d5db1c56383cf193daccd14eb45d21927281de Mon Sep 17 00:00:00 2001 From: Chun Ho Lam <58981086+LamHo220@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:19:52 +0800 Subject: [PATCH 2/4] fix input error for next js & better output for console --- src/hooks/usePythonConsole.ts | 30 +++++++++++++++++++++++++++++- src/providers/PythonProvider.tsx | 15 +++++++++------ src/workers/python-worker.ts | 4 +++- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/hooks/usePythonConsole.ts b/src/hooks/usePythonConsole.ts index dd158ef8..c66d3186 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() @@ -241,6 +241,7 @@ del sys return { runPython, stdout, + output, stderr, isLoading, isReady, @@ -259,3 +260,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 ( Date: Fri, 7 Mar 2025 14:19:52 +0800 Subject: [PATCH 3/4] fix input error for next js & better output for console --- src/hooks/usePythonConsole.ts | 30 +++++++++++++++++++++++++++++- src/providers/PythonProvider.tsx | 15 +++++++++------ src/workers/python-worker.ts | 4 +++- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/hooks/usePythonConsole.ts b/src/hooks/usePythonConsole.ts index dd158ef8..c66d3186 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() @@ -241,6 +241,7 @@ del sys return { runPython, stdout, + output, stderr, isLoading, isReady, @@ -259,3 +260,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 ( Date: Mon, 2 Jun 2025 10:55:16 +0800 Subject: [PATCH 4/4] update method to interrupt code --- src/hooks/usePython.ts | 12 +++++------- src/hooks/usePythonConsole.ts | 13 +++++-------- src/types/Runner.ts | 3 +++ src/workers/python-worker.ts | 13 ++++++++++--- 4 files changed, 23 insertions(+), 18 deletions(-) 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 c66d3186..6a91ac7c 100644 --- a/src/hooks/usePythonConsole.ts +++ b/src/hooks/usePythonConsole.ts @@ -43,6 +43,7 @@ function usePython(props?: UsePythonConsoleProps) { const workerRef = useRef() const runnerRef = useRef>() + const interruptBuffer = useRef(new Uint8Array(new SharedArrayBuffer(1))) const { readFile, @@ -120,6 +121,7 @@ function usePython(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 = () => { diff --git a/src/types/Runner.ts b/src/types/Runner.ts index 696a1cdb..74663c30 100644 --- a/src/types/Runner.ts +++ b/src/types/Runner.ts @@ -1,3 +1,5 @@ +import { TypedArray } from 'pyodide' + export interface Runner { init: ( stdout: (msg: string) => 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 036b4b90..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 { @@ -72,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) @@ -98,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)