/** * codeRunner.ts — Code runner tools * * Contiene: runCodeSandbox, runViaPyodideWorker, Pyodide worker via WorkerManager, * tool implementations: run_code, execute_shell, pip_install, run_python, * zip_project, profile_code, read_pdf, send_notification * * S135 — Integrazione runtime/WorkerManager per py worker: * - WorkerManager gestisce lifecycle completo: spawn, ping/pong health check (60s), * auto-respawn dopo 3 ping falliti, idle-terminate * - codeRunner.ts gestisce message routing via _pyListeners Map * - WorkerManager usa w.onmessage internamente (ping/pong) — * codeRunner usa addEventListener per coesistere senza conflitti * - onStatusChange subscription: re-attacca handler dopo ogni respawn * senza che codeRunner.ts gestisca alcun lifecycle manuale * - pyodideReady in RuntimeDiagnosticsPanel ora riporta stato reale */ import { vfsAsync } from "../vfsDb"; import { vfs } from "../vfs"; import { debugLoop } from "../debugLoop"; import { agentMemory } from "../agentMemory"; import { adaptiveSolver } from "../adaptiveSolver"; import { recordOutcomeAsync } from "../selfLearningAsync"; // FIX22: S649 — era dynamic import+sync (blocca UI ~200ms iPhone) import { usePythonVarsStore } from "@/store/pythonVarsStore"; import type { PythonVar } from "@/store/pythonVarsStore"; import { workerManager, WorkerManager } from "../runtime/WorkerManager"; import type { WorkerLike } from "../runtime/WorkerManager"; import { eventRuntime } from "@/core/events"; // S768: partial_output event // ─── Tool dispatcher ────────────────────────────────────────────────────────── export async function tryExecute(name: string, args: Record, signal?: AbortSignal): Promise { if (name === "run_code") { const { code, language, _retry = false } = args as { code: string; language?: string; _retry?: boolean }; // S74: Python/bash/ts → usa exec backend HF Space /api/exec se disponibile const _lang = (language ?? "javascript").toLowerCase(); if (!["javascript", "js"].includes(_lang)) { const { backend } = await import("../backendClient"); if (backend.isExecAvailable()) { // P1: Pre-exec Python dep detection — installa moduli non-stdlib prima dell'esecuzione // Elimina l'iterazione sprecata su ModuleNotFoundError if (_lang === "python" || _lang === "py") { const _PYLIB = new Set(["os","sys","json","re","math","datetime","collections","itertools", "functools","pathlib","typing","abc","io","time","random","hashlib","urllib","http", "threading","subprocess","traceback","inspect","copy","string","struct","base64", "contextlib","dataclasses","enum","logging","tempfile","shutil","glob","csv","xml", "html","email","socket","ssl","unittest","gc","weakref","queue","heapq","bisect", "builtins","operator","textwrap","warnings","keyword","ast","types","decimal", "statistics","cmath","numbers","array","zlib","pickle","shelve","sqlite3", "configparser","argparse","platform","signal","mimetypes","calendar", "__future__","typing_extensions","dataclasses","pprint","reprlib"]); const _pyPkgs = Array.from(new Set( (code.match(/^(?:import|from)\s+(\w+)/gm) ?? []) .map((m: string) => m.split(/\s+/)[1]) .filter((mod: string) => mod && !_PYLIB.has(mod)) )); if (_pyPkgs.length) { try { await backend.execCode(`pip install ${_pyPkgs.join(" ")} -q 2>&1 | tail -1`, "bash"); } catch { /* best-effort — non-blocking */ } } } try { const r = await backend.execCode(code, _lang); // S768: partial output — emetti evento // S769: persist al VFS per ispezione post-chip if (r.partial) { const _partialSnip = (r.stdout ?? "").slice(0, 300); try { eventRuntime.emit({ kind: "partial_output", tool: "run_code", partialOut: _partialSnip }); } catch { /* non-blocking */ } const _ts = Date.now(); vfsAsync.write( `.partial_out_${_ts}.txt`, `[S769] tool: run_code\ntime: ${new Date(_ts).toISOString()}\n\n${r.stdout ?? ""}`, ).catch(() => {}); } const out = [r.stdout, r.stderr].filter(Boolean).join("\n").trim(); return out || `[OK (exit ${r.exit_code}) — ${_lang}]`; } catch { /* fallback a browser sandbox */ } } } const result = await runCodeSandbox(code); if (result.startsWith("❌") && !_retry) { // RT-10: se il segnale è già abortito, non avviare auto-fix + secondo sandbox — // evita esecuzione fantasma di 70s in background dopo che il timeout esterno (65s) // ha già restituito "❌ [TIMEOUT]" al loop. if (signal?.aborted) return result; const taskId = crypto.randomUUID().slice(0, 8); const fixed = await debugLoop._askLLMFix(code, result.replace("❌ ", ""), "javascript", taskId); if (fixed && fixed.trim() !== code.trim()) { // RT-10: secondo check prima del secondo runCodeSandbox if (signal?.aborted) return result; const retryResult = await runCodeSandbox(fixed); if (!retryResult.startsWith("❌")) return `[auto-fix]→${retryResult}`; const solved = await adaptiveSolver.solve(retryResult, fixed, "run_code"); if (solved.success) { // S350: impara dai fix riusciti — pattern salvato per sessioni future try { recordOutcomeAsync({ task: `run_code:${solved.strategy}`, output: solved.output.slice(0, 300), success: true, confidence: 0.85, toolsUsed: ["run_code"] }); } catch { /* non-blocking */ } // FIX22 return `[adaptive-solver: ${solved.strategy}] ${solved.description}\n${solved.output}`; } return `${result}\n\n⚠️ Auto-fix tentato ma ancora in errore:\n${retryResult}\n\n💡 ${solved.description}`; } const solved = await adaptiveSolver.solve(result, code, "run_code"); if (solved.success) { // S350: impara dai fix riusciti — pattern salvato per sessioni future try { recordOutcomeAsync({ task: `run_code:${solved.strategy}`, output: solved.output.slice(0, 300), success: true, confidence: 0.85, toolsUsed: ["run_code"] }); } catch { /* non-blocking */ } // FIX22 return `[adaptive-solver: ${solved.strategy}] ${solved.description}\n${solved.output}`; } } return result; } if (name === "execute_shell") { const { command, timeout = 15, session_id } = args as { command: string; timeout?: number; session_id?: string }; const { backend } = await import("../backendClient"); // S144-Fix5: messaggio contestuale quando nessun backend è configurato. // EXEC_BACKEND fa già fallback su BACKEND (VITE_BACKEND_URL), quindi // isExecAvailable() è false SOLO se entrambe le env var sono vuote. if (!backend.isExecAvailable()) { const isPyCmd = /^\s*(python3?|pip3?)\s/.test(command); if (isPyCmd) return "💡 Backend non configurato. Per eseguire Python usa il tool **run_python** — funziona anche offline via Pyodide."; return "❌ execute_shell: nessun backend configurato. Imposta **VITE_BACKEND_URL** nelle env Vercel puntando all\'HF Space."; } try { // S694-persist: session_id da args (npm_install/npm_run propagano) o getExecSessionId() internamente // Cap 300s: npm install/build richiedono fino a 120s; il server HF Space ha i propri resource limits const r = await backend.executeShell(command, Math.min(Number(timeout) || 15, 300), session_id); // S768: partial output — emetti evento // S769: persist al VFS if (r.partial) { const _partialSnip = (r.stdout ?? "").slice(0, 300); try { eventRuntime.emit({ kind: "partial_output", tool: "execute_shell", partialOut: _partialSnip }); } catch { /* non-blocking */ } const _ts = Date.now(); vfsAsync.write( `.partial_out_${_ts}.txt`, `[S769] tool: execute_shell\ntime: ${new Date(_ts).toISOString()}\n\n${r.stdout ?? ""}`, ).catch(() => {}); } let out = "```\n"; if (r.stdout) out += r.stdout; if (r.stderr) out += `\n[stderr] ${r.stderr}`; out += `\n[exit ${r.exit_code}]` + "\n```"; return out; } catch (e) { const errMsg = e instanceof Error ? e.message : String(e); // S144-Fix5: se il backend HF Space non ha /api/execute-shell (404/405), // re-routing automatico per comandi Python via Pyodide. if (/404|405|not.?found|method.?not.?allowed/i.test(errMsg)) { // Caso: python3 -c "codice" → esegui via run_python const pyInlineMatch = command.match(/python3?\s+-c\s+["'\`]([\s\S]+)["'\`]/); if (pyInlineMatch) { return runViaPyodideWorker(pyInlineMatch[1]); } // Caso: comando shell generico non supportato return "❌ execute_shell: il backend HF Space non supporta comandi shell diretti. Usa **run_python** per codice Python, oppure configura un Exec Engine dedicato (VITE_EXEC_BACKEND_URL)."; } return `❌ execute_shell: ${errMsg}`; } } if (name === "pip_install") { const { packages } = args as { packages: string[] }; if (!Array.isArray(packages) || packages.length === 0) return "❌ pip_install: fornisci almeno un pacchetto."; const { backend } = await import("../backendClient"); // S144-Fix5: suggerisci micropip (Pyodide) se nessun backend disponibile const _micropipHint = (pkgs: string[]) => `💡 pip_install non disponibile senza backend. In Pyodide usa micropip:\n\`\`\`python\nimport micropip\nawait micropip.install([${pkgs.map(p => `"${p}"`).join(", ")}])\n\`\`\``; if (!backend.isExecAvailable()) return _micropipHint(packages); try { const r = await backend.pipInstall(packages); if (r.exit_code === 0) return `✅ Installati: ${(r.installed ?? packages).join(", ")}`; return `❌ pip install fallito (exit ${r.exit_code}):\n${r.stderr ?? r.error ?? "errore sconosciuto"}`; } catch (e) { const errMsg = e instanceof Error ? e.message : String(e); // S144-Fix5: 404 sul backend → suggerisci micropip if (/404|405|not.?found/i.test(errMsg)) return _micropipHint(packages); return `❌ pip_install: ${errMsg}`; } } if (name === "run_python") { const { code, packages = [] } = args as { code: string; packages?: string[] }; return await runViaPyodideWorker(code, packages, signal); } if (name === "zip_project") { const { paths, filename = "project.zip" } = args as { paths?: string[]; filename?: string }; const allFiles = await vfsAsync.list() as Array<{ path: string }>; const targets = paths && paths.length > 0 ? allFiles.filter(f => paths.some(p => f.path.startsWith(p) || f.path === p)) : allFiles; if (targets.length === 0) return "❌ zip_project: nessun file nel VFS da zippare."; const fileContentsRaw = await Promise.all(targets.map(async f => ({ path: f.path, content: await vfsAsync.read(f.path) }))); const fileContents = fileContentsRaw.filter((f): f is { path: string; content: string } => f.content !== null); const zipCode = ` const JSZip = (await importModule('jszip')).default; const zip = new JSZip(); const files = ${JSON.stringify(fileContents)}; for (const f of files) zip.file(f.path.replace(/^\\//, ''), f.content); const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = ${JSON.stringify(filename)}; a.click(); setTimeout(() => URL.revokeObjectURL(url), 5000); console.log('📦 ZIP scaricato: ${filename} (' + files.length + ' file)'); // check-console-log: ok `; return await runCodeSandbox(zipCode.trim()); } if (name === "profile_code") { const { code, iterations = 100, warmup = 10 } = args as { code: string; iterations?: number; warmup?: number }; const iters = Math.min(Number(iterations) || 100, 10000); const warmupN = Math.min(Number(warmup) || 10, 100); const profileCode = ` ${code} if (typeof main !== 'function') { console.log('❌ Definisci una funzione main() nel codice da profilare.'); } // check-console-log: ok else { const WARMUP = ${warmupN}; const ITERS = ${iters}; // Warmup for (let i = 0; i < WARMUP; i++) await main(); // Measure const times = []; for (let i = 0; i < ITERS; i++) { const t0 = performance.now(); await main(); times.push(performance.now() - t0); } times.sort((a,b) => a-b); const sum = times.reduce((a,b)=>a+b,0); const avg = sum/times.length; const min = times[0]; const max = times[times.length-1]; const p50 = times[Math.floor(times.length*0.5)]; const p95 = times[Math.floor(times.length*0.95)]; const p99 = times[Math.floor(times.length*0.99)]; const opsPerSec = avg > 0 ? (1000/avg).toFixed(0) : '∞'; console.log('⚡ Profiler Results (' + ITERS + ' iterazioni, ' + WARMUP + ' warmup)'); // check-console-log: ok console.log('avg: ' + avg.toFixed(4) + ' ms'); // check-console-log: ok console.log('min: ' + min.toFixed(4) + ' ms'); // check-console-log: ok console.log('max: ' + max.toFixed(4) + ' ms'); // check-console-log: ok console.log('p50: ' + p50.toFixed(4) + ' ms'); // check-console-log: ok console.log('p95: ' + p95.toFixed(4) + ' ms'); // check-console-log: ok console.log('p99: ' + p99.toFixed(4) + ' ms'); // check-console-log: ok console.log('ops/s: ' + opsPerSec); // check-console-log: ok } `; return await runCodeSandbox(profileCode.trim()); } if (name === "read_pdf") { const { url, max_pages = 10 } = args as { url: string; max_pages?: number }; const maxP = Math.min(Number(max_pages) || 10, 30); const pdfCode = ` const pdfjsLib = await importModule('pdfjs-dist/legacy/build/pdf'); pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.10.38/pdf.worker.min.mjs'; const pdf = await pdfjsLib.getDocument(${JSON.stringify(url)}).promise; const numPages = Math.min(pdf.numPages, ${maxP}); const pages = []; for (let i = 1; i <= numPages; i++) { const page = await pdf.getPage(i); const content = await page.getTextContent(); const text = content.items.map(item => ('str' in item ? item.str : '')).join(' ').trim(); if (text) pages.push('--- Pagina ' + i + ' ---\\n' + text); } if (!pages.length) { console.log('❌ Nessun testo estraibile (PDF scansionato o protetto).'); } // check-console-log: ok else { console.log('📄 PDF: ' + pdf.numPages + ' pagine totali, estratte: ' + numPages + '\\n\\n' + pages.join('\\n\\n').slice(0, 8000)); } // check-console-log: ok `; return await runCodeSandbox(pdfCode.trim()); } return null; } // ─── Code Sandbox ───────────────────────────────────────────────────────────── export async function runCodeSandbox(code: string): Promise { const logs: string[] = []; const logFn = (...a: unknown[]) => logs.push(a.map(x => typeof x === "string" ? x : JSON.stringify(x, null, 2)).join(" ")); const downloadFn = (content: string, filename = "file.txt", mimeType = "text/plain") => { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(url), 5000); logs.push(`📁 Download avviato: ${filename}`); }; // I-a fix S178: generatePDF scaricava HTML rinominato .pdf — ora apre finestra stampa // (browser → Stampa → "Salva come PDF" = vero PDF) e scarica anche l'HTML come fallback const generatePDF = async (html: string, filename = "documento.pdf") => { const fullHtml = `${filename} ${html} `; const blob = new Blob([fullHtml], { type: "text/html" }); const url = URL.createObjectURL(blob); const win = window.open(url, "_blank"); if (win) { setTimeout(() => win.print(), 800); } else { const a = document.createElement("a"); a.href = url; a.download = filename.replace(/\.pdf$/i, ".html"); a.click(); logs.push(`⚠️ generatePDF: popup bloccato — scaricato HTML (${filename.replace(/\.pdf$/i,".html")}). Apri il file e usa Stampa → Salva come PDF.`); } setTimeout(() => URL.revokeObjectURL(url), 30000); logs.push(`🖨️ PDF-print: "${filename}" — finestra stampa aperta. Scegli "Salva come PDF" nel dialogo.`); }; const globals: Record = { console: { log: logFn, warn: logFn, error: logFn, info: logFn }, fetch, vfs, memory: agentMemory, download: downloadFn, generatePDF, uuid: () => crypto.randomUUID(), hash: async (s: string, algo = "SHA-256") => { const buf = await crypto.subtle.digest(algo, new TextEncoder().encode(s)); return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, "0")).join(""); }, b64encode: (s: string) => btoa(unescape(encodeURIComponent(s))), b64decode: (s: string) => decodeURIComponent(escape(atob(s))), csvParse: (t: string) => t.trim().split(/\r?\n/).map(l => { const r: string[] = []; let f = "", q = false; for (const c of l) { if (c === '"') q = !q; else if (c === ',' && !q) { r.push(f); f = ""; } else f += c; } r.push(f); return r; }), importModule: (spec: string) => import(/* @vite-ignore */ `https://esm.sh/${spec}`), // OPT: require polyfill — redirige al sistema esm.sh con messaggio utile // Evita ReferenceError generico "require is not defined" che confonde l'LLM require: (spec: string) => { throw new Error( `require("${spec}") non disponibile nel browser sandbox.\n` + `Usa invece: const mod = await importModule("${spec}");` ); }, chart: (type: string, labels: string[], values: number[], title?: string) => { logs.push(`📊 Chart[${type}] "${title ?? ""}" — ${labels.slice(0,4).join(", ")}…`); }, sleep: (ms: number) => new Promise(r => setTimeout(r, ms)), atob, btoa, URL, Promise, JSON, Math, Date, Object, Array, String, Number, Boolean, parseInt, parseFloat, isNaN, isFinite, encodeURIComponent, decodeURIComponent, setTimeout, clearTimeout, setInterval, clearInterval, TextEncoder, TextDecoder, crypto, Blob, ArrayBuffer, Uint8Array, Int8Array, Float32Array, Float64Array, }; try { const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor as new (...args: string[]) => (...args: unknown[]) => Promise; const fn = new AsyncFunction(...Object.keys(globals), `"use strict";\n${code}`); const timeoutP = new Promise((_, rej) => setTimeout(() => rej(new Error("Sandbox timeout (30s)")), 30_000)); await Promise.race([fn(...Object.values(globals)), timeoutP]); const out = logs.join("\n") || "(nessun output)"; return out.length > 4000 ? out.slice(0, 4000) + "\n...(troncato)" : out; } catch (err) { return `❌ ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`; } } // ─── Pyodide Worker via runtime/WorkerManager ───────────────────────────────── // // S135: WorkerManager (runtime/WorkerManager.ts) gestisce il lifecycle completo: // • Spawn lazy alla prima run_python // • Ping/pong health check ogni 60s // • Auto-respawn automatico dopo 3 ping falliti (Safari iOS silent kill) // • Idle auto-terminate dopo 60s di inattività // • Status reporting per RuntimeDiagnosticsPanel (pyodideReady ora reale) // // Coesistenza con WorkerManager.onmessage: // WorkerManager usa w.onmessage = ... per ping/pong (ready/pong). // codeRunner usa w.addEventListener("message", ...) per coesistere senza // sovrascrivere l'handler interno. Entrambi i listener ricevono tutti i // messaggi; _pyMessageHandler ignora ready/pong, WorkerManager ignora il resto. // // Respawn handling: // onStatusChange subscription (module-level, registrata una volta): // - health "dead" → pulisce _pyWorkerRef e pending _pyListeners // - health "alive" → riattacca _pyMessageHandler al nuovo worker // // _pyStatus è settato da agentLoop via setPyodideStatus() prima di ogni runAgentLoop. type _PyWorkerMsg = { type: string; id?: string; text?: string; message?: string; exitCode?: number; progress?: number; vars?: PythonVar[]; }; let _pyStatus: ((s: string) => void) | null = null; const _pyListeners = new Map void>(); /** Riferimento al worker attivo corrente — null se non ancora spawned o dopo terminate. */ let _pyWorkerRef: WorkerLike | null = null; // S143-Bug4: tipo allineato a WorkerLike // ─── Message handler ────────────────────────────────────────────────────────── // Funzione named e stabile — addEventListener/removeEventListener richiedono // la stessa referenza. Non deve essere ricreata tra respawn. function _pyMessageHandler(e: MessageEvent<_PyWorkerMsg>): void { const msg = e.data; if (msg.type === "loading") { const pct = (msg as { type: string; progress?: number }).progress ?? 0; if (_pyStatus) { if (pct <= 0) _pyStatus("🐍 Avvio Python…"); else if (pct < 60) _pyStatus("🐍 Caricamento Python…"); else _pyStatus("🐍 Python quasi pronto…"); } return; } // "ready" e "pong" sono gestiti da WorkerManager.onmessage internamente — // questo handler li ignora per evitare doppia elaborazione. if (msg.type === "ready" || msg.type === "pong") return; // S71: variabili Python estratte post-run → aggiorna store if (msg.type === "vars" && (msg as { type: string; vars?: PythonVar[] }).vars?.length) { usePythonVarsStore.getState().updateVars( (msg as { type: string; vars: PythonVar[] }).vars, ); return; } // Messaggi per-esecuzione → routing per id if (msg.id) _pyListeners.get(msg.id)?.(msg); } // ─── Attacca handler al worker (idempotente) ────────────────────────────────── // Usa addEventListener → coesiste con WorkerManager.onmessage senza sovrascriverlo. // removeEventListener prima di add → garantisce unicità anche su chiamate ripetute. // S143-Bug4: WorkerLike ora include addEventListener/removeEventListener. // Rimosso il cast fragile "as Worker" — wl è già tipato correttamente. function _attachPyHandler(wl: WorkerLike): void { wl.removeEventListener("message", _pyMessageHandler); wl.addEventListener("message", _pyMessageHandler); _pyWorkerRef = wl; } // ─── Subscription unica ai lifecycle events del worker ─────────────────────── // Registrata a module load — non in una funzione — così esiste per tutta // la vita dell'app e sopravvive a respawn multipli. workerManager.onStatusChange((status) => { if (status.type !== "py") return; if (status.health === "dead") { // Worker morto (crash o idle terminate) → pulisci riferimento _pyWorkerRef = null; for (const [, cb] of _pyListeners) { cb({ type: "error", message: "Worker Pyodide terminato — riprova" }); } _pyListeners.clear(); return; } if (status.health === "alive") { // Worker spawned o respawned → riattacca handler se cambiato try { const wl = workerManager.ensureSpawned("py"); if (wl !== _pyWorkerRef) { _attachPyHandler(wl); } } catch { /* non-fatal: _getPyWorker lo ritenterà */ } } }); /** Chiamato da agentLoop.ts per agganciare onStatus al bridge Pyodide. */ export function setPyodideStatus(fn: ((s: string) => void) | null): void { _pyStatus = fn; } /** * Ritorna il worker Pyodide attivo, spawnandolo se necessario. * WorkerManager gestisce health check e respawn: questa funzione si * limita a ottenere il worker live e garantire che l'handler sia attaccato. */ function _getPyWorker(): WorkerLike | null { if (!WorkerManager.isWorkerSupported()) return null; try { const wl = workerManager.ensureSpawned("py"); // Attacca handler se non ancora fatto (prima use) o dopo respawn via onStatusChange if (wl !== _pyWorkerRef) { _attachPyHandler(wl); } return _pyWorkerRef; } catch { return null; } } // ─── Helper: Pyodide worker bridge (run_python tool) ───────────────────────── // S139-Fix3 — Safari iOS detection: module workers non supportati in modo affidabile. // Fallback trasparente su backend HF Space (già configurato come EXEC_BACKEND). function _isSafariIOS(): boolean { if (typeof navigator === "undefined") return false; return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as unknown as Record).MSStream; } /** * runViaPyodideWorker — entry point pubblico. * Su Safari iOS: delega l'esecuzione Python al backend HF Space (exec engine). * Su altri browser: usa il worker Pyodide locale. */ function runViaPyodideWorker(code: string, packages: string[] = [], signal?: AbortSignal): Promise { if (_isSafariIOS()) { return import("../backendClient").then(({ backend }) => { if (backend.isExecAvailable()) { const fullCode = packages.length > 0 ? `import subprocess\nsubprocess.run(["pip","install","-q",${packages.map(p => JSON.stringify(p)).join(",")}], capture_output=True)\n` + code : code; return backend.execCode(fullCode, "python", 30) .then(r => { // S768: partial output — emetti evento // S769: persist al VFS if (r.partial) { const _partialSnip = (r.stdout ?? "").slice(0, 300); try { eventRuntime.emit({ kind: "partial_output", tool: "run_python", partialOut: _partialSnip }); } catch { /* non-blocking */ } const _ts = Date.now(); vfsAsync.write( `.partial_out_${_ts}.txt`, `[S769] tool: run_python\ntime: ${new Date(_ts).toISOString()}\n\n${r.stdout ?? ""}`, ).catch(() => {}); } const out = [r.stdout, r.stderr ? `[stderr] ${r.stderr}` : ""].filter(Boolean).join("\n").trim(); return out || "(nessun output)"; }) .catch((e: Error) => `❌ Python (server): ${e.message}`); } // Backend non disponibile: tenta comunque il worker (Safari 16.4+ lo supporta parzialmente) return _runViaPyodideWorkerInternal(code, packages, signal); }); } return _runViaPyodideWorkerInternal(code, packages, signal); } function _runViaPyodideWorkerInternal(code: string, packages: string[] = [], signal?: AbortSignal): Promise { return new Promise((resolve) => { const worker = _getPyWorker(); if (!worker) { resolve("❌ Worker Pyodide non disponibile (Safari iOS potrebbe non supportare module workers — prova Chrome/Firefox)"); return; } let stdout = ""; let stderr = ""; const id = crypto.randomUUID(); const cleanup = () => { clearTimeout(timer); _pyListeners.delete(id); signal?.removeEventListener("abort", abortHandler); }; // RT-3: abort propagation — risolve immediatamente senza aspettare i 32s di timeout const abortHandler = () => { cleanup(); const partial = [stdout, stderr ? `[stderr] ${stderr}` : ""].filter(Boolean).join("\n"); resolve(`❌ [ABORT] Esecuzione Python interrotta dall'utente.${partial ? "\nOutput parziale:\n" + partial : ""}`); }; if (signal?.aborted) { resolve("❌ [ABORT] Esecuzione Python interrotta dall'utente."); return; } if (signal) signal.addEventListener("abort", abortHandler, { once: true }); const timer = setTimeout(() => { cleanup(); const partial = [stdout, stderr ? `[stderr] ${stderr}` : ""].filter(Boolean).join("\n"); resolve(`❌ Python timeout (30s)${partial ? "\nOutput parziale:\n" + partial : ""}`); }, 32_000); _pyListeners.set(id, (msg: _PyWorkerMsg) => { if (msg.type === "stdout") { const text = msg.text ?? ""; if (text.startsWith("[pip]")) { // S70: messaggi install → feedback live via onStatus const label = text.replace("[pip]", "").replace(/\n$/, "").trim(); if (label && _pyStatus) { if (label.startsWith("✓")) _pyStatus(`✅ Python: ${label.slice(1).trim()}`); else _pyStatus(`📦 ${label}`); } stdout += text; } else { stdout += text; } } else if (msg.type === "stderr") { stderr += msg.text ?? ""; } else if (msg.type === "done" || msg.type === "error") { cleanup(); const exitCode = msg.type === "done" ? (msg.exitCode ?? 0) : 1; // Rimuovi linee [pip] dall'output finale — l'utente le ha già viste live const cleanStdout = stdout .split("\n") .filter(l => !l.startsWith("[pip]")) .join("\n") .trim(); const out = [cleanStdout, stderr ? `[stderr] ${stderr.trim()}` : ""] .filter(Boolean) .join("\n") || "(nessun output)"; resolve(exitCode === 0 ? out : `❌ Python error (exit ${exitCode}):\n${out}`); } }); worker.postMessage({ type: "run", id, code, packages }); }); }