Spaces:
Sleeping
Sleeping
| /** | |
| * RuntimeManager.ts — Orchestrates all code execution runtimes | |
| * | |
| * Routing strategy: | |
| * python/py → Pyodide Worker via WorkerManager (browser, offline) | |
| * js/ts → jsRunner Worker via WorkerPool (browser sandbox) | |
| * others → Judge0 CE (free public instance, no key) | |
| * | |
| * Falls back to Judge0 if workers fail. | |
| * | |
| * S137 — pyPool rimpiazzato con workerManager.ensureSpawned("py") | |
| * così RuntimeDiagnosticsPanel legge lo stato reale via getStatus("py"). | |
| * | |
| * S246-Fix: WorkerPool NON usa più URL stringa runtime. | |
| * Vite richiede che new URL("...", import.meta.url) sia STATICO — | |
| * il path deve essere un literal string nel sorgente, non una variabile. | |
| * URL runtime → Vite non bundla il worker → 404 in produzione → HTML | |
| * → TypeError: 'text/html' is not a valid JavaScript MIME type. | |
| * | |
| * Fix: WorkerPool crea il Worker con URL statico direttamente in ensureWorker(). | |
| * | |
| * P6-1: JsWorkerPool rimpiazzato con workerManager.ensureSpawned("js"). | |
| * Su iOS, WorkerManager termina il Worker opposto prima di spawnare (js↔py swap) | |
| * → max 2 Worker (1 streamWorker + 1 code runner) → no crash iOS. | |
| */ | |
| import { executionQueue } from "./ExecutionQueue"; | |
| import { workerManager } from "./WorkerManager"; | |
| import { ff } from "@/lib/featureFlags"; | |
| import { makeTimedSignal } from "@/lib/agentLoop/networkConstants"; // Loop-16: iOS-safe | |
| export type RuntimeTarget = "browser-js" | "browser-py" | "judge0"; | |
| export interface RunOptions { | |
| language: string; | |
| code: string; | |
| stdin?: string; | |
| packages?: string[]; | |
| timeout?: number; | |
| } | |
| export interface RunResult { | |
| stdout: string; | |
| stderr: string; | |
| exitCode: number; | |
| time: number; | |
| runtime: RuntimeTarget; | |
| } | |
| type WorkerMsg = | |
| | { type: "stdout"; id: string; text: string } | |
| | { type: "stderr"; id: string; text: string } | |
| | { type: "done"; id: string; exitCode: number; time: number } | |
| | { type: "error"; id: string; message: string } | |
| | { type: "loading"; progress: number } | |
| | { type: "ready" }; | |
| const JUDGE0_CE = "https://ce.judge0.com"; | |
| const JUDGE0_LANG_IDS: Record<string, number> = { | |
| python: 71, py: 71, | |
| javascript: 63, js: 63, | |
| typescript: 74, ts: 74, | |
| go: 60, | |
| rust: 73, | |
| java: 62, | |
| cpp: 54, "c++": 54, | |
| c: 50, | |
| bash: 46, sh: 46, | |
| ruby: 72, | |
| php: 68, | |
| kotlin: 78, | |
| swift: 83, | |
| r: 80, | |
| }; | |
| function routeRuntime(lang: string): RuntimeTarget { | |
| const l = lang.toLowerCase(); | |
| if (l === "python" || l === "py") return "browser-py"; | |
| if (l === "javascript" || l === "js") return "browser-js"; | |
| return "judge0"; | |
| } | |
| // ─── JS worker helper via WorkerManager (P6-1) ─────────────────────────────── | |
| // JsWorkerPool rimossa: JS ora usa workerManager.ensureSpawned("js") come Python. | |
| // Su iOS, WorkerManager.spawn() termina automaticamente il Worker opposto prima | |
| // di spawnare → garantisce max 2 Worker totali (1 stream + 1 code runner). | |
| // L'URL jsRunner.worker.ts rimane statico nel _defaultFactory di WorkerManager.ts. | |
| async function runInJsWorker(id: string, msgPayload: object, timeout: number): Promise<RunResult> { | |
| const workerLike = workerManager.ensureSpawned("js"); | |
| const worker = workerLike as unknown as Worker; | |
| let stdout = ""; | |
| let stderr = ""; | |
| let exitCode = 0; | |
| let time = 0; | |
| return new Promise<RunResult>((resolve, reject) => { | |
| const timer = setTimeout(() => { | |
| worker.removeEventListener("message", handler); | |
| reject(new Error(`jsRunner timeout dopo ${timeout}ms`)); | |
| }, timeout + 5_000); | |
| function handler(e: MessageEvent<WorkerMsg>): void { | |
| const data = e.data; | |
| if (!("id" in data) || data.id !== id) return; | |
| if (data.type === "stdout") stdout += data.text; | |
| if (data.type === "stderr") stderr += data.text; | |
| if (data.type === "done") { exitCode = data.exitCode; time = data.time; } | |
| if (data.type === "done" || data.type === "error") { | |
| clearTimeout(timer); | |
| worker.removeEventListener("message", handler); | |
| if (data.type === "error") reject(new Error(data.message)); | |
| else resolve({ stdout, stderr, exitCode, time, runtime: "browser-js" }); | |
| } | |
| } | |
| worker.addEventListener("message", handler); | |
| worker.postMessage(msgPayload); | |
| }); | |
| } | |
| // ─── Py worker helper via WorkerManager ────────────────────────────────────── | |
| async function runInPyWorker(id: string, msgPayload: object, timeout: number): Promise<RunResult> { | |
| const workerLike = workerManager.ensureSpawned("py"); | |
| const worker = workerLike as unknown as Worker; | |
| let stdout = ""; | |
| let stderr = ""; | |
| let exitCode = 0; | |
| let time = 0; | |
| return new Promise<RunResult>((resolve, reject) => { | |
| const timer = setTimeout(() => { | |
| worker.removeEventListener("message", handler); | |
| reject(new Error(`Pyodide timeout dopo ${timeout}ms`)); | |
| }, timeout + 5_000); | |
| function handler(e: MessageEvent<WorkerMsg>): void { | |
| const data = e.data; | |
| if (!("id" in data) || data.id !== id) return; | |
| if (data.type === "stdout") stdout += data.text; | |
| if (data.type === "stderr") stderr += data.text; | |
| if (data.type === "done") { exitCode = data.exitCode; time = data.time; } | |
| if (data.type === "done" || data.type === "error") { | |
| clearTimeout(timer); | |
| worker.removeEventListener("message", handler); | |
| if (data.type === "error") reject(new Error(data.message)); | |
| else resolve({ stdout, stderr, exitCode, time, runtime: "browser-py" }); | |
| } | |
| } | |
| worker.addEventListener("message", handler); | |
| worker.postMessage(msgPayload); | |
| }); | |
| } | |
| async function runJudge0(code: string, lang: string, stdin = "", timeout = 30_000): Promise<RunResult> { | |
| const langId = JUDGE0_LANG_IDS[lang.toLowerCase()]; | |
| if (!langId) throw new Error(`Linguaggio non supportato da Judge0: ${lang}`); | |
| const start = Date.now(); | |
| const submitRes = await fetch(`${JUDGE0_CE}/submissions?base64_encoded=true&wait=true`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| signal: makeTimedSignal(timeout), | |
| body: JSON.stringify({ | |
| source_code: btoa(unescape(encodeURIComponent(code))), | |
| language_id: langId, | |
| stdin: btoa(unescape(encodeURIComponent(stdin))), | |
| base64_encoded: true, | |
| }), | |
| }); | |
| if (!submitRes.ok) { | |
| throw new Error(`Judge0 HTTP ${submitRes.status}`); | |
| } | |
| const result = await submitRes.json() as { | |
| stdout?: string; | |
| stderr?: string; | |
| compile_output?: string; | |
| status?: { description?: string }; | |
| time?: string; | |
| memory?: number; | |
| exit_code?: number; | |
| }; | |
| const decode = (s?: string): string => { | |
| if (!s) return ""; | |
| try { return decodeURIComponent(escape(atob(s))); } catch { return s; } | |
| }; | |
| const stderr = [decode(result.stderr), decode(result.compile_output)].filter(Boolean).join("\n"); | |
| return { | |
| stdout: decode(result.stdout), | |
| stderr, | |
| exitCode: result.exit_code ?? 0, | |
| time: Date.now() - start, | |
| runtime: "judge0", | |
| }; | |
| } | |
| export class RuntimeManager { | |
| private static idCounter = 0; | |
| private static nextId() { return `run-${Date.now()}-${++RuntimeManager.idCounter}`; } | |
| async execute(opts: RunOptions): Promise<RunResult> { | |
| const id = RuntimeManager.nextId(); | |
| const lang = opts.language.toLowerCase(); | |
| const target = routeRuntime(lang); | |
| const timeout = opts.timeout ?? 30_000; | |
| return executionQueue.enqueue<RunResult>(id, async () => { | |
| if (target === "browser-js") { | |
| try { | |
| const result = await runInJsWorker(id, { | |
| type: "run", id, | |
| code: opts.code, | |
| timeout, | |
| }, timeout); | |
| result.runtime = "browser-js"; | |
| return result; | |
| } catch { | |
| return runJudge0(opts.code, lang, opts.stdin, timeout); | |
| } | |
| } | |
| if (target === "browser-py") { | |
| if (ff("PYODIDE_WORKER")) { | |
| try { | |
| const result = await runInPyWorker(id, { | |
| type: "run", | |
| id, | |
| code: opts.code, | |
| packages: opts.packages ?? [], | |
| timeout, | |
| }, timeout); | |
| return result; | |
| } catch { | |
| // Worker crash → Judge0 fallback silenzioso | |
| } | |
| } | |
| return runJudge0(opts.code, "python", opts.stdin, timeout); | |
| } | |
| return runJudge0(opts.code, lang, opts.stdin, timeout); | |
| }, { timeout: timeout + 10_000 }); | |
| } | |
| cancel(id: string) { | |
| // P6-1: JS usa workerManager (rimossa JsWorkerPool) | |
| const jsStatus = workerManager.getStatus("js"); | |
| if (jsStatus.spawned) { | |
| try { | |
| const wjs = workerManager.ensureSpawned("js") as unknown as Worker; | |
| wjs.postMessage({ type: "cancel", id }); | |
| } catch { /* ignora */ } | |
| } | |
| const pyStatus = workerManager.getStatus("py"); | |
| if (pyStatus.spawned) { | |
| try { | |
| const w = workerManager.ensureSpawned("py") as unknown as Worker; | |
| w.postMessage({ type: "cancel", id }); | |
| } catch { /* ignora */ } | |
| } | |
| executionQueue.cancel(id); | |
| } | |
| } | |
| export const runtimeManager = new RuntimeManager(); | |