From 9f9fe046dd9cc598ec37104d44dc322ee52abdf4 Mon Sep 17 00:00:00 2001 From: Matteo Benedetto Date: Thu, 20 Aug 2026 15:13:08 +0200 Subject: [PATCH] fix(browser): avvio main non interattivo affidabile --- README.md | 3 +- extensions/firefox-bidi.ts | 101 ++++++++++++++++++++----------------- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 5d0bfbb..b5c5a06 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ pi agent (questa estensione) ──HTTP JSON──► Browser Bridge (bridge.py) - **Script di avvio** (`~/Dev/firefox-bidi/start-firefox-bidi.sh`): - `start` → profilo dedicato `bidi-profile` (automazione pulita) - `start-main` → **profilo principale** (login/cookie intatti; chiude l'istanza - attiva se serve, lock esclusivo) + attiva se serve, lock esclusivo). Il tool `browser_start(profile="main")` + usa automaticamente `FIREFOX_BIDI_FORCE=1`: non può richiedere input interattivo. - `status` / `stop` / `foreground` ## Tool registrati diff --git a/extensions/firefox-bidi.ts b/extensions/firefox-bidi.ts index 9a24505..2e212a8 100644 --- a/extensions/firefox-bidi.ts +++ b/extensions/firefox-bidi.ts @@ -26,9 +26,9 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { spawn, execFile } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { closeSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { Socket } from "node:net"; import { promisify } from "node:util"; @@ -55,7 +55,9 @@ const EVAL_TIMEOUT_MS = 30_000; // fail-fast per operazioni veloci const SCREENSHOT_TIMEOUT_MS = 30_000; const EVENTS_TIMEOUT_MS = 10_000; const SEND_TIMEOUT_MS = 60_000; -const MAX_BRIDGE_CYCLES = 2; // cicli di riavvio bridge+firefox su sessione orfana +const BRIDGE_STABLE_MS = 3_000; +const BRIDGE_LOG = + process.env.FIREFOX_BIDI_BRIDGE_LOG ?? join(homedir(), ".local", "state", "pi-firefox-bidi", "bridge.log"); let bridgeProcess: ReturnType | null = null; @@ -165,8 +167,8 @@ async function withRetry(fn: () => Promise, label: string, retries = 2): P // --------------------------------------------------------------------------- async function bridgeAlive(): Promise { try { - await bridge("/status", undefined, STATUS_TIMEOUT_MS); - return true; + const status = await bridge("/status", undefined, STATUS_TIMEOUT_MS); + return status.firefox === true && status.sessionActive === true && Boolean(status.context); } catch { return false; } @@ -174,7 +176,13 @@ async function bridgeAlive(): Promise { async function startFirefox(profile: "dedicated" | "main"): Promise { const cmd = profile === "main" ? "start-main" : "start"; - await execFileAsync(SCRIPT, [cmd], { timeout: 90_000 }); + // I tool non dispongono di stdin interattivo: il profilo principale deve + // confermare esplicitamente la chiusura di un Firefox già attivo. Il tool + // documenta già tale effetto e conserva cookie/login al riavvio. + const env = profile === "main" + ? { ...process.env, FIREFOX_BIDI_FORCE: "1" } + : process.env; + await execFileAsync(SCRIPT, [cmd], { timeout: 90_000, env }); } /** @@ -231,29 +239,44 @@ async function ensureVenv(): Promise<{ py: string; env: NodeJS.ProcessEnv }> { async function startBridgeProcess(): Promise { const { py, env } = await ensureVenv(); - bridgeProcess = spawn(py, [BRIDGE_PY], { - stdio: "ignore", - detached: true, - env, + mkdirSync(dirname(BRIDGE_LOG), { recursive: true }); + const logFd = openSync(BRIDGE_LOG, "a"); + let child: ReturnType; + try { + writeSync(logFd, `\n[extension] ${new Date().toISOString()} start ${py} ${BRIDGE_PY}\n`); + child = spawn(py, [BRIDGE_PY], { + stdio: ["ignore", logFd, logFd], + detached: true, + env, + }); + } finally { + closeSync(logFd); + } + bridgeProcess = child; + child.once("error", (err) => log(`bridge spawn error: ${err.message}; log=${BRIDGE_LOG}`)); + child.once("exit", (code, signal) => { + log(`bridge terminato: code=${code ?? "null"} signal=${signal ?? "null"}; log=${BRIDGE_LOG}`); + if (bridgeProcess === child) bridgeProcess = null; }); - bridgeProcess.unref(); + child.unref(); } async function waitBridgeReady(timeoutMs = 15_000): Promise { const deadline = Date.now() + timeoutMs; + let stableSince = 0; while (Date.now() < deadline) { - if (await bridgeAlive()) return true; + if (await bridgeAlive()) { + if (stableSince === 0) stableSince = Date.now(); + if (Date.now() - stableSince >= BRIDGE_STABLE_MS) return true; + } else { + stableSince = 0; + } await new Promise((r) => setTimeout(r, 300)); } return false; } -/** - * Garantisce bridge attivo. Se non risponde: avvia Firefox (profilo scelto) + - * bridge. Se il bridge parte ma muore (es. sessione BiDi orfana dopo un kill - * senza session.end), riavvia Firefox e riprova fino a MAX_BRIDGE_CYCLES. - * Con forceRestart=true riavvia comunque (usato dopo errori di sessione). - */ +/** Garantisce un bridge stabile senza terminare o riavviare Firefox. */ /** * Verifica se Firefox Remote Agent è già in ascolto sulla porta BiDi (9222). * Usato da ensureBridge per NON riavviare un Firefox già attivo (istanza @@ -281,45 +304,33 @@ function isPortOpen(port: number, host = "127.0.0.1"): Promise { async function ensureBridge( profile: "dedicated" | "main" = "dedicated", - opts: { forceRestart?: boolean } = {}, ): Promise { - if (!opts.forceRestart && (await bridgeAlive())) return; - for (let cycle = 0; cycle <= MAX_BRIDGE_CYCLES; cycle++) { - if (cycle > 0 || opts.forceRestart) { - log(`ciclo ${cycle}: riavvio Firefox (possibile sessione BiDi orfana)`); - await execFileAsync(SCRIPT, ["stop"]).catch(() => {}); - await new Promise((r) => setTimeout(r, 1_500)); - await startFirefox(profile); - } else if (!(await isPortOpen(9222))) { - await startFirefox(profile); - } else { - log("Firefox già attivo su 9222: avvio solo il bridge"); - } - await startBridgeProcess(); - if (await waitBridgeReady()) return; - log(`ciclo ${cycle}: bridge non pronto, kill processo`); - bridgeProcess?.kill("SIGKILL"); - bridgeProcess = null; + if (await bridgeAlive()) return; + if (!(await isPortOpen(9222))) { + await startFirefox(profile); + } else { + log("Firefox già attivo su 9222: avvio solo il bridge"); } + await startBridgeProcess(); + if (await waitBridgeReady()) return; throw new Error( - "Bridge BiDi non raggiungibile dopo riavvii. Verifica: Firefox attivo? Porta 9222 libera? " + - "Se persiste, chiudi Firefox manualmente e riprova (sessione BiDi orfana).", + `Bridge BiDi non stabile per ${BRIDGE_STABLE_MS}ms. Nessun riavvio automatico di Firefox eseguito. ` + + `Verifica ${BRIDGE_LOG} e lo stato della porta 9222.`, ); } /** - * Esegue fn; se fallisce per sessione BiDi non valida (es. "session not - * created", "sessione BiDi chiusa"), riavvia Firefox+bridge e riprova una - * volta. Evita di lasciare l'utente con errori criptici dopo un crash. + * Non riavvia o termina Firefox automaticamente: una sessione BiDi chiusa + * viene riportata in modo esplicito e browser_start può ricreare il bridge. */ async function withSessionRecovery(fn: () => Promise, label: string): Promise { try { return await fn(); } catch (err) { - if (!isSessionError(err)) throw err; - log(`${label}: sessione BiDi non valida (${(err as Error).message}), riavvio Firefox+bridge`); - await ensureBridge("dedicated", { forceRestart: true }); - return await fn(); + if (isSessionError(err)) { + log(`${label}: sessione BiDi non valida (${(err as Error).message}); nessun auto-kill eseguito`); + } + throw err; } }