Browse Source

feat: code execution time limit

Timothy J. Baek 1 year ago
parent
commit
a4630a9825
2 changed files with 157 additions and 51 deletions
  1. 102 51
      src/lib/components/chat/Messages/CodeBlock.svelte
  2. 55 0
      static/pyodide-worker.js

+ 102 - 51
src/lib/components/chat/Messages/CodeBlock.svelte

@@ -139,73 +139,124 @@
 	};
 
 	const executePython = async (code) => {
-		result = null;
-		stdout = null;
-		stderr = null;
-
-		executing = true;
-
-		let pyodide = await loadPyodide({
-			indexURL: '/pyodide/',
-			stdout: (text) => {
-				console.log('Python output:', text);
-
-				if (stdout) {
-					stdout += `${text}\n`;
-				} else {
-					stdout = `${text}\n`;
-				}
-			},
-			stderr: (text) => {
-				console.log('An error occured:', text);
-				if (stderr) {
-					stderr += `${text}\n`;
-				} else {
-					stderr = `${text}\n`;
+		if (!code.includes('input')) {
+			executePythonAsWorker(code);
+		} else {
+			result = null;
+			stdout = null;
+			stderr = null;
+
+			executing = true;
+
+			let pyodide = await loadPyodide({
+				indexURL: '/pyodide/',
+				stdout: (text) => {
+					console.log('Python output:', text);
+
+					if (stdout) {
+						stdout += `${text}\n`;
+					} else {
+						stdout = `${text}\n`;
+					}
+				},
+				stderr: (text) => {
+					console.log('An error occured:', text);
+					if (stderr) {
+						stderr += `${text}\n`;
+					} else {
+						stderr = `${text}\n`;
+					}
 				}
-			}
-		});
+			});
 
-		try {
-			const res = await pyodide.loadPackage('micropip');
-			console.log(res);
+			try {
+				const res = await pyodide.loadPackage('micropip');
+				console.log(res);
 
-			// pyodide.setStdin({ stdin: () => prompt() });
+				const micropip = pyodide.pyimport('micropip');
 
-			const micropip = pyodide.pyimport('micropip');
+				await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
 
-			await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
+				let packages = [
+					code.includes('requests') ? 'requests' : null,
+					code.includes('bs4') ? 'beautifulsoup4' : null,
+					code.includes('numpy') ? 'numpy' : null,
+					code.includes('pandas') ? 'pandas' : null
+				].filter(Boolean);
 
-			let packages = [
-				code.includes('requests') ? 'requests' : null,
-				code.includes('bs4') ? 'beautifulsoup4' : null,
-				code.includes('numpy') ? 'numpy' : null,
-				code.includes('pandas') ? 'pandas' : null
-			].filter(Boolean);
+				console.log(packages);
+				await micropip.install(packages);
 
-			console.log(packages);
-			await micropip.install(packages);
-
-			result = pyodide.runPython(`from js import prompt
+				result = await pyodide.runPythonAsync(`from js import prompt
 def input(p):
     return prompt(p)
 __builtins__.input = input`);
 
-			result = pyodide.runPython(code);
+				result = await pyodide.runPython(code);
+
+				if (!result) {
+					result = '[NO OUTPUT]';
+				}
 
-			if (!result) {
-				result = '[NO OUTPUT]';
+				console.log(result);
+				console.log(stdout);
+				console.log(stderr);
+			} catch (error) {
+				console.error('Error:', error);
+				stderr = error;
 			}
 
-			console.log(result);
-			console.log(stdout);
-			console.log(stderr);
-		} catch (error) {
-			console.error('Error:', error);
-			stderr = error;
+			executing = false;
 		}
+	};
+
+	const executePythonAsWorker = async (code) => {
+		result = null;
+		stdout = null;
+		stderr = null;
+
+		executing = true;
+
+		let packages = [
+			code.includes('requests') ? 'requests' : null,
+			code.includes('bs4') ? 'beautifulsoup4' : null,
+			code.includes('numpy') ? 'numpy' : null,
+			code.includes('pandas') ? 'pandas' : null
+		].filter(Boolean);
+
+		const pyodideWorker = new Worker('/pyodide-worker.js');
+
+		pyodideWorker.postMessage({
+			id: id,
+			code: code,
+			packages: packages
+		});
+
+		setTimeout(() => {
+			if (executing) {
+				executing = false;
+				stderr = 'Execution Time Limit Exceeded';
+				pyodideWorker.terminate();
+			}
+		}, 10000);
+
+		pyodideWorker.onmessage = (event) => {
+			console.log('pyodideWorker.onmessage', event);
+			const { id, ...data } = event.data;
+
+			console.log(id, data);
+
+			data['stdout'] && (stdout = data['stdout']);
+			data['stderr'] && (stderr = data['stderr']);
+			data['result'] && (result = data['result']);
+
+			executing = false;
+		};
 
-		executing = false;
+		pyodideWorker.onerror = (event) => {
+			console.log('pyodideWorker.onerror', event);
+			executing = false;
+		};
 	};
 
 	$: highlightedCode = code ? hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value : '';

+ 55 - 0
static/pyodide-worker.js

@@ -0,0 +1,55 @@
+// webworker.js
+// Setup your project to serve `py-worker.js`. You should also serve
+// `pyodide.js`, and all its associated `.asm.js`, `.json`,
+// and `.wasm` files as well:
+importScripts('/pyodide/pyodide.js');
+
+async function loadPyodideAndPackages(packages = []) {
+	self.stdout = null;
+	self.stderr = null;
+	self.result = null;
+
+	self.pyodide = await loadPyodide({
+		indexURL: '/pyodide/',
+		stdout: (text) => {
+			console.log('Python output:', text);
+
+			if (self.stdout) {
+				self.stdout += `${text}\n`;
+			} else {
+				self.stdout = `${text}\n`;
+			}
+		},
+		stderr: (text) => {
+			console.log('An error occured:', text);
+			if (self.stderr) {
+				self.stderr += `${text}\n`;
+			} else {
+				self.stderr = `${text}\n`;
+			}
+		}
+	});
+
+	await self.pyodide.loadPackage('micropip');
+	const micropip = self.pyodide.pyimport('micropip');
+
+	await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
+	await micropip.install(packages);
+}
+
+self.onmessage = async (event) => {
+	const { id, code, ...context } = event.data;
+
+	console.log(event.data)
+
+	// The worker copies the context in its own "memory" (an object mapping name to values)
+	for (const key of Object.keys(context)) {
+		self[key] = context[key];
+	}
+
+	// make sure loading is done
+	await loadPyodideAndPackages(self.packages);
+
+	self.result = await self.pyodide.runPythonAsync(code);
+	self.postMessage({ id, result: self.result, stdout: self.stdout, stderr: self.stderr });
+};