/** * 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 = { 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 { 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((resolve, reject) => { const timer = setTimeout(() => { worker.removeEventListener("message", handler); reject(new Error(`jsRunner timeout dopo ${timeout}ms`)); }, timeout + 5_000); function handler(e: MessageEvent): 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 { 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((resolve, reject) => { const timer = setTimeout(() => { worker.removeEventListener("message", handler); reject(new Error(`Pyodide timeout dopo ${timeout}ms`)); }, timeout + 5_000); function handler(e: MessageEvent): 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 { 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 { const id = RuntimeManager.nextId(); const lang = opts.language.toLowerCase(); const target = routeRuntime(lang); const timeout = opts.timeout ?? 30_000; return executionQueue.enqueue(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();