Skip to content

Commit 24c4c01

Browse files
committed
Set up a web worker to be used for skulp execution to prevent crashes of React core while letting users use it without hard limit in execution
1 parent 0353d06 commit 24c4c01

2 files changed

Lines changed: 123 additions & 79 deletions

File tree

src/components/InteractivePython/index.js

Lines changed: 73 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@ import { oneDark } from '@codemirror/theme-one-dark';
55
import styles from './styles.module.css';
66

77
export default function InteractivePython({ children }) {
8-
const initialCode = children.props.children.trim();
8+
const initialCode = children?.props?.children?.trim() || '';
99
const [code, setCode] = useState(initialCode);
10-
const [isSkulptReady, setIsSkulptReady] = useState(false);
1110
const [isRunning, setIsRunning] = useState(false);
1211

12+
const workerRef = useRef(null);
1313
const outputRef = useRef(null);
1414
const inputContainerRef = useRef(null);
1515
const inputPromptRef = useRef(null);
1616
const inputFieldRef = useRef(null);
17-
const resolveInputRef = useRef(null);
1817

1918
const appendOutput = (text) => {
2019
if (outputRef.current) outputRef.current.textContent += text;
@@ -42,90 +41,75 @@ export default function InteractivePython({ children }) {
4241
const prompt = inputPromptRef.current.textContent;
4342
hideInput();
4443
appendOutput(prompt + value + "\n");
45-
if (resolveInputRef.current) {
46-
const resolve = resolveInputRef.current;
47-
resolveInputRef.current = null;
48-
resolve(value);
44+
45+
// Send the input back to the worker to resume execution
46+
if (workerRef.current) {
47+
workerRef.current.postMessage({ type: 'INPUT_RESPONSE', payload: value });
4948
}
5049
};
5150

51+
// Cleanup worker on component unmount
5252
useEffect(() => {
53-
if (inputContainerRef.current) {
54-
inputContainerRef.current.style.display = 'none';
55-
}
53+
return () => stopWorker();
5654
}, []);
5755

58-
useEffect(() => {
59-
const loadScript = (src) => new Promise((resolve, reject) => {
60-
const script = document.createElement('script');
61-
script.src = src;
62-
script.async = true;
63-
script.onload = resolve;
64-
script.onerror = reject;
65-
document.body.appendChild(script);
66-
});
67-
68-
async function initSkulpt() {
69-
try {
70-
if (!window.Sk) {
71-
await loadScript("https://cdn.jsdelivr.net/npm/skulpt@1.2.0/dist/skulpt.min.js");
72-
await loadScript("https://cdn.jsdelivr.net/npm/skulpt@1.2.0/dist/skulpt-stdlib.js");
73-
}
74-
setIsSkulptReady(true);
75-
} catch (err) {
76-
console.error("Failed to load Skulpt scripts", err);
77-
}
56+
const stopWorker = () => {
57+
if (workerRef.current) {
58+
workerRef.current.terminate();
59+
workerRef.current = null;
60+
setIsRunning(false);
61+
hideInput();
62+
appendOutput("\n[Process Terminated]");
7863
}
79-
initSkulpt();
80-
}, []);
64+
};
8165

8266
const runCode = () => {
83-
if (!isSkulptReady || !window.Sk) return;
84-
8567
clearOutput();
8668
hideInput();
8769
setIsRunning(true);
88-
resolveInputRef.current = null;
89-
90-
window.Sk.configure({
91-
output: (text) => appendOutput(text),
92-
read: (x) => {
93-
if (window.Sk.builtinFiles === undefined || window.Sk.builtinFiles["files"][x] === undefined)
94-
throw "File not found: '" + x + "'";
95-
return window.Sk.builtinFiles["files"][x];
96-
},
97-
inputfunTakesPrompt: true,
98-
execLimit: 10000, // 10 sec
99-
yieldLimit: 100,
100-
inputfun: (prompt) => {
101-
return new Promise((resolve) => {
102-
resolveInputRef.current = resolve;
103-
showInput(prompt);
104-
});
105-
},
106-
__future__: window.Sk.python3
107-
});
108-
109-
window.Sk.misceval.asyncToPromise(() =>
110-
window.Sk.importMainWithBody("<stdin>", false, code, true)
111-
).then(
112-
() => { setIsRunning(false); hideInput(); },
113-
(err) => {
114-
appendOutput("\n" + err.toString());
115-
setIsRunning(false);
116-
hideInput();
117-
}
70+
71+
// Terminate any existing worker before starting a new one
72+
if (workerRef.current) {
73+
workerRef.current.terminate();
74+
}
75+
76+
// Initialize the Web Worker
77+
workerRef.current = new Worker(
78+
new URL('./skulpt.worker.js', import.meta.url)
11879
);
80+
81+
// Handle incoming messages from Skulpt
82+
workerRef.current.onmessage = (e) => {
83+
const { type, payload } = e.data;
84+
85+
switch (type) {
86+
case 'OUTPUT':
87+
appendOutput(payload);
88+
break;
89+
case 'INPUT_PROMPT':
90+
showInput(payload);
91+
break;
92+
case 'FINISHED':
93+
setIsRunning(false);
94+
appendOutput("\n[Program Finished]");
95+
break;
96+
case 'ERROR':
97+
setIsRunning(false);
98+
appendOutput("\n" + payload);
99+
break;
100+
default:
101+
break;
102+
}
103+
};
104+
105+
// Send the code to the worker to start execution
106+
workerRef.current.postMessage({ type: 'RUN_CODE', payload: code });
119107
};
120108

121109
return (
122110
<div className={styles.wrapper}>
123-
{/* ── Header ── */}
124111
<div className={styles.codeHeader}>
125-
<span className={styles.codeHeaderText}>Python Ledger Editor</span>
126-
<span className={`${styles.codeHeaderStatus} ${isSkulptReady ? styles.ready : ''}`}>
127-
{isSkulptReady ? '● READY' : '○ LOADING...'}
128-
</span>
112+
<span className={styles.codeHeaderText}>Python Sandbox</span>
129113
</div>
130114

131115
<CodeMirror
@@ -135,19 +119,29 @@ export default function InteractivePython({ children }) {
135119
onChange={(value) => setCode(value)}
136120
/>
137121

138-
{/* ── Run Button ── */}
139-
<button
140-
className={styles.runButton}
141-
onClick={runCode}
142-
disabled={!isSkulptReady || isRunning}
143-
>
144-
{isRunning ? '⏳ Running...' : '▶ Execute Program'}
145-
</button>
122+
<div style={{ display: 'flex', gap: '10px', margin: '10px 0' }}>
123+
<button
124+
className={styles.runButton}
125+
onClick={runCode}
126+
disabled={isRunning}
127+
>
128+
▶ Execute Program
129+
</button>
130+
131+
{/* The Kill Switch */}
132+
<button
133+
className={styles.runButton}
134+
onClick={stopWorker}
135+
disabled={!isRunning}
136+
style={{ backgroundColor: isRunning ? '#d9534f' : '#555' }}
137+
>
138+
■ Stop Execution
139+
</button>
140+
</div>
146141

147-
{/* ── Console ── */}
148142
<div className={styles.console}>
149143
<pre ref={outputRef} className={styles.output} />
150-
<div ref={inputContainerRef} className={styles.inputForm}>
144+
<div ref={inputContainerRef} className={styles.inputForm} style={{ display: 'none' }}>
151145
<span ref={inputPromptRef} className={styles.promptText} />
152146
<input
153147
ref={inputFieldRef}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// public/skulpt.worker.js
2+
3+
// 1. Load Skulpt directly into the worker
4+
importScripts("https://cdn.jsdelivr.net/npm/skulpt@1.2.0/dist/skulpt.min.js");
5+
importScripts("https://cdn.jsdelivr.net/npm/skulpt@1.2.0/dist/skulpt-stdlib.js");
6+
7+
let inputResolve = null;
8+
9+
self.onmessage = function(e) {
10+
const { type, payload } = e.data;
11+
12+
// Start execution
13+
if (type === 'RUN_CODE') {
14+
runPythonCode(payload);
15+
}
16+
// Receive input from the user (React UI) and resume Python execution
17+
else if (type === 'INPUT_RESPONSE' && inputResolve) {
18+
inputResolve(payload);
19+
inputResolve = null;
20+
}
21+
};
22+
23+
function runPythonCode(code) {
24+
self.Sk.configure({
25+
output: (text) => self.postMessage({ type: 'OUTPUT', payload: text }),
26+
read: (x) => {
27+
if (self.Sk.builtinFiles === undefined || self.Sk.builtinFiles["files"][x] === undefined) {
28+
throw "File not found: '" + x + "'";
29+
}
30+
return self.Sk.builtinFiles["files"][x];
31+
},
32+
inputfunTakesPrompt: true,
33+
inputfun: (prompt) => {
34+
return new Promise((resolve) => {
35+
inputResolve = resolve;
36+
// Ask React to show the input UI
37+
self.postMessage({ type: 'INPUT_PROMPT', payload: prompt });
38+
});
39+
},
40+
__future__: self.Sk.python3,
41+
yieldLimit: 100 // Keeps the worker responsive to messages
42+
});
43+
44+
self.Sk.misceval.asyncToPromise(() =>
45+
self.Sk.importMainWithBody("<stdin>", false, code, true)
46+
).then(
47+
() => self.postMessage({ type: 'FINISHED' }),
48+
(err) => self.postMessage({ type: 'ERROR', payload: err.toString() })
49+
);
50+
}

0 commit comments

Comments
 (0)