AUDIT / src /lib /runtime /RuntimeManager.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 4)
641b62c verified
Raw
History Blame Contribute Delete
9.35 kB
/**
* 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();