Spaces:
Sleeping
Sleeping
| /** | |
| * Script Worker β Executes Python (Pyodide) and Lua (wasmoon) scripts | |
| * in a Web Worker to avoid blocking the UI thread. | |
| * | |
| * Receives: { type: 'execute', payload: { runtime, entryPoint, files } } | |
| * { type: 'abort' } | |
| * Posts: { type: 'stdout'|'stderr'|'status'|'error'|'complete'|'output-file', ... } | |
| */ | |
| /* global self, importScripts, postMessage */ | |
| let pyodide = null; | |
| let luaFactory = null; | |
| /** | |
| * Post a message back to the main thread. | |
| */ | |
| function send(type, data) { | |
| self.postMessage({ type, data }); | |
| } | |
| function sendFile(path, content) { | |
| self.postMessage({ type: 'output-file', path, content }); | |
| } | |
| // βββ Python (Pyodide) ββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function ensurePyodide() { | |
| if (pyodide) return pyodide; | |
| send('status', 'Loading Python runtime...'); | |
| try { | |
| importScripts('https://cdn.jsdelivr.net/pyodide/v0.27.4/full/pyodide.js'); | |
| } catch (err) { | |
| send('error', 'Failed to load Pyodide from CDN: ' + String(err)); | |
| throw err; | |
| } | |
| try { | |
| pyodide = await self.loadPyodide({ | |
| stdout: (msg) => send('stdout', msg), | |
| stderr: (msg) => send('stderr', msg), | |
| }); | |
| } catch (err) { | |
| send('error', 'Failed to initialize Pyodide: ' + String(err)); | |
| throw err; | |
| } | |
| // Pre-load micropip so users can install packages | |
| await pyodide.loadPackage('micropip'); | |
| send('status', 'Python runtime ready'); | |
| return pyodide; | |
| } | |
| async function executePython(entryPoint, files) { | |
| const py = await ensurePyodide(); | |
| // Mount VFS files into Pyodide's filesystem | |
| // Create /output/ directory for visual output | |
| try { py.FS.mkdir('/output'); } catch (_e) { /* exists */ } | |
| for (const [path, content] of Object.entries(files)) { | |
| // Skip dotfiles | |
| if (path.startsWith('/.')) continue; | |
| const dir = path.substring(0, path.lastIndexOf('/')) || '/'; | |
| // Ensure parent directories exist | |
| const parts = dir.split('/').filter(Boolean); | |
| let current = ''; | |
| for (const part of parts) { | |
| current += '/' + part; | |
| try { py.FS.mkdir(current); } catch (_e) { /* exists */ } | |
| } | |
| py.FS.writeFile(path, content); | |
| } | |
| // Run the entry point script | |
| const code = files[entryPoint]; | |
| if (!code) { | |
| send('error', `Entry point not found: ${entryPoint}`); | |
| return { exitCode: 1 }; | |
| } | |
| // Set up Python environment so module imports work: | |
| const entryDir = entryPoint.substring(0, entryPoint.lastIndexOf('/')) || '/'; | |
| try { | |
| await py.runPythonAsync(` | |
| import sys, os | |
| os.chdir(${JSON.stringify(entryDir)}) | |
| _ep_dir = ${JSON.stringify(entryDir)} | |
| if _ep_dir not in sys.path: | |
| sys.path.insert(0, _ep_dir) | |
| if '/' not in sys.path: | |
| sys.path.insert(0, '/') | |
| __file__ = ${JSON.stringify(entryPoint)} | |
| del _ep_dir | |
| `); | |
| } catch (_e) { /* best effort */ } | |
| try { | |
| await py.runPythonAsync(code); | |
| } catch (err) { | |
| send('stderr', String(err)); | |
| return { exitCode: 1 }; | |
| } | |
| // Scan /output/ for new files and send them back | |
| try { | |
| const outputFiles = py.FS.readdir('/output').filter(f => f !== '.' && f !== '..'); | |
| for (const filename of outputFiles) { | |
| const filePath = '/output/' + filename; | |
| try { | |
| const data = py.FS.readFile(filePath); | |
| const ext = filename.split('.').pop().toLowerCase(); | |
| if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].includes(ext)) { | |
| let binary = ''; | |
| for (let i = 0; i < data.length; i++) { | |
| binary += String.fromCharCode(data[i]); | |
| } | |
| const base64 = btoa(binary); | |
| sendFile(filePath, base64); | |
| } else { | |
| const decoder = new TextDecoder(); | |
| sendFile(filePath, decoder.decode(data)); | |
| } | |
| } catch (_e) { /* skip unreadable files */ } | |
| } | |
| } catch (_e) { /* /output/ may not have new files */ } | |
| return { exitCode: 0 }; | |
| } | |
| // βββ Lua (wasmoon) ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function ensureLuaFactory() { | |
| if (luaFactory) return luaFactory; | |
| send('status', 'Loading Lua runtime...'); | |
| try { | |
| // Dynamic import of wasmoon from CDN | |
| const wasmoon = await import('https://esm.sh/wasmoon@1'); | |
| luaFactory = new wasmoon.LuaFactory(); | |
| } catch (err) { | |
| send('error', 'Failed to load Lua runtime: ' + String(err)); | |
| throw err; | |
| } | |
| send('status', 'Lua runtime ready'); | |
| return luaFactory; | |
| } | |
| async function executeLua(entryPoint, files) { | |
| const factory = await ensureLuaFactory(); | |
| const engine = await factory.createEngine(); | |
| try { | |
| // Override print to capture stdout | |
| engine.global.set('print', function (...args) { | |
| send('stdout', args.map(String).join('\t')); | |
| }); | |
| // Pre-load module files so require() works | |
| const moduleFiles = {}; | |
| for (const [path, content] of Object.entries(files)) { | |
| if (path.endsWith('.lua') && path !== entryPoint) { | |
| const modName = path | |
| .replace(/^\//, '') | |
| .replace(/\.lua$/, '') | |
| .replace(/\//g, '.'); | |
| moduleFiles[modName] = content; | |
| } | |
| } | |
| // Register custom searcher for VFS modules | |
| engine.global.set('__vfs_modules', JSON.stringify(moduleFiles)); | |
| await engine.doString(` | |
| local vfs_modules = {} | |
| local json_str = __vfs_modules | |
| -- Simple JSON parse for module map (keys and string values only) | |
| for key, value in json_str:gmatch('"([^"]+)":"(.-[^\\\\])"') do | |
| -- Unescape basic sequences | |
| value = value:gsub('\\\\n', '\\n'):gsub('\\\\t', '\\t'):gsub('\\\\"', '"'):gsub('\\\\\\\\', '\\\\') | |
| vfs_modules[key] = value | |
| end | |
| __vfs_modules = nil | |
| table.insert(package.searchers, 2, function(modname) | |
| local source = vfs_modules[modname] | |
| if source then | |
| local fn, err = load(source, "@" .. modname .. ".lua") | |
| if fn then return fn | |
| else return "\\n\\tload error: " .. err end | |
| end | |
| return "\\n\\tno VFS module '" .. modname .. "'" | |
| end) | |
| `); | |
| // Run the entry point | |
| const code = files[entryPoint]; | |
| if (!code) { | |
| send('error', 'Entry point not found: ' + entryPoint); | |
| engine.global.close(); | |
| return { exitCode: 1 }; | |
| } | |
| await engine.doString(code); | |
| engine.global.close(); | |
| return { exitCode: 0 }; | |
| } catch (err) { | |
| send('stderr', String(err)); | |
| try { engine.global.close(); } catch (_e) { /* best effort */ } | |
| return { exitCode: 1 }; | |
| } | |
| } | |
| // βββ Message handler ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| self.onmessage = async function (event) { | |
| const msg = event.data; | |
| if (msg.type === 'execute') { | |
| const { runtime, entryPoint, files } = msg.payload; | |
| try { | |
| let result; | |
| if (runtime === 'python') { | |
| result = await executePython(entryPoint, files); | |
| } else if (runtime === 'lua') { | |
| result = await executeLua(entryPoint, files); | |
| } else { | |
| send('error', 'Unknown runtime: ' + runtime); | |
| self.postMessage({ type: 'complete', exitCode: 1 }); | |
| return; | |
| } | |
| self.postMessage({ type: 'complete', exitCode: result.exitCode }); | |
| } catch (err) { | |
| send('error', String(err)); | |
| self.postMessage({ type: 'complete', exitCode: 1 }); | |
| } | |
| } | |
| }; | |