fix(browser): avvio main non interattivo affidabile

This commit is contained in:
Matteo Benedetto
2026-08-20 15:13:08 +02:00
parent 598491067b
commit 9f9fe046dd
2 changed files with 58 additions and 46 deletions
+2 -1
View File
@@ -19,7 +19,8 @@ pi agent (questa estensione) ──HTTP JSON──► Browser Bridge (bridge.py)
- **Script di avvio** (`~/Dev/firefox-bidi/start-firefox-bidi.sh`): - **Script di avvio** (`~/Dev/firefox-bidi/start-firefox-bidi.sh`):
- `start` → profilo dedicato `bidi-profile` (automazione pulita) - `start` → profilo dedicato `bidi-profile` (automazione pulita)
- `start-main`**profilo principale** (login/cookie intatti; chiude l'istanza - `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` - `status` / `stop` / `foreground`
## Tool registrati ## Tool registrati
+56 -45
View File
@@ -26,9 +26,9 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox"; import { Type } from "typebox";
import { spawn, execFile } from "node:child_process"; 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 { homedir } from "node:os";
import { join } from "node:path"; import { dirname, join } from "node:path";
import { Socket } from "node:net"; import { Socket } from "node:net";
import { promisify } from "node:util"; 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 SCREENSHOT_TIMEOUT_MS = 30_000;
const EVENTS_TIMEOUT_MS = 10_000; const EVENTS_TIMEOUT_MS = 10_000;
const SEND_TIMEOUT_MS = 60_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<typeof spawn> | null = null; let bridgeProcess: ReturnType<typeof spawn> | null = null;
@@ -165,8 +167,8 @@ async function withRetry<T>(fn: () => Promise<T>, label: string, retries = 2): P
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function bridgeAlive(): Promise<boolean> { async function bridgeAlive(): Promise<boolean> {
try { try {
await bridge("/status", undefined, STATUS_TIMEOUT_MS); const status = await bridge("/status", undefined, STATUS_TIMEOUT_MS);
return true; return status.firefox === true && status.sessionActive === true && Boolean(status.context);
} catch { } catch {
return false; return false;
} }
@@ -174,7 +176,13 @@ async function bridgeAlive(): Promise<boolean> {
async function startFirefox(profile: "dedicated" | "main"): Promise<void> { async function startFirefox(profile: "dedicated" | "main"): Promise<void> {
const cmd = profile === "main" ? "start-main" : "start"; 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<void> { async function startBridgeProcess(): Promise<void> {
const { py, env } = await ensureVenv(); const { py, env } = await ensureVenv();
bridgeProcess = spawn(py, [BRIDGE_PY], { mkdirSync(dirname(BRIDGE_LOG), { recursive: true });
stdio: "ignore", const logFd = openSync(BRIDGE_LOG, "a");
detached: true, let child: ReturnType<typeof spawn>;
env, 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<boolean> { async function waitBridgeReady(timeoutMs = 15_000): Promise<boolean> {
const deadline = Date.now() + timeoutMs; const deadline = Date.now() + timeoutMs;
let stableSince = 0;
while (Date.now() < deadline) { 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)); await new Promise((r) => setTimeout(r, 300));
} }
return false; return false;
} }
/** /** Garantisce un bridge stabile senza terminare o riavviare Firefox. */
* 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).
*/
/** /**
* Verifica se Firefox Remote Agent è già in ascolto sulla porta BiDi (9222). * Verifica se Firefox Remote Agent è già in ascolto sulla porta BiDi (9222).
* Usato da ensureBridge per NON riavviare un Firefox già attivo (istanza * 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<boolean> {
async function ensureBridge( async function ensureBridge(
profile: "dedicated" | "main" = "dedicated", profile: "dedicated" | "main" = "dedicated",
opts: { forceRestart?: boolean } = {},
): Promise<void> { ): Promise<void> {
if (!opts.forceRestart && (await bridgeAlive())) return; if (await bridgeAlive()) return;
for (let cycle = 0; cycle <= MAX_BRIDGE_CYCLES; cycle++) { if (!(await isPortOpen(9222))) {
if (cycle > 0 || opts.forceRestart) { await startFirefox(profile);
log(`ciclo ${cycle}: riavvio Firefox (possibile sessione BiDi orfana)`); } else {
await execFileAsync(SCRIPT, ["stop"]).catch(() => {}); log("Firefox già attivo su 9222: avvio solo il bridge");
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;
} }
await startBridgeProcess();
if (await waitBridgeReady()) return;
throw new Error( throw new Error(
"Bridge BiDi non raggiungibile dopo riavvii. Verifica: Firefox attivo? Porta 9222 libera? " + `Bridge BiDi non stabile per ${BRIDGE_STABLE_MS}ms. Nessun riavvio automatico di Firefox eseguito. ` +
"Se persiste, chiudi Firefox manualmente e riprova (sessione BiDi orfana).", `Verifica ${BRIDGE_LOG} e lo stato della porta 9222.`,
); );
} }
/** /**
* Esegue fn; se fallisce per sessione BiDi non valida (es. "session not * Non riavvia o termina Firefox automaticamente: una sessione BiDi chiusa
* created", "sessione BiDi chiusa"), riavvia Firefox+bridge e riprova una * viene riportata in modo esplicito e browser_start può ricreare il bridge.
* volta. Evita di lasciare l'utente con errori criptici dopo un crash.
*/ */
async function withSessionRecovery<T>(fn: () => Promise<T>, label: string): Promise<T> { async function withSessionRecovery<T>(fn: () => Promise<T>, label: string): Promise<T> {
try { try {
return await fn(); return await fn();
} catch (err) { } catch (err) {
if (!isSessionError(err)) throw err; if (isSessionError(err)) {
log(`${label}: sessione BiDi non valida (${(err as Error).message}), riavvio Firefox+bridge`); log(`${label}: sessione BiDi non valida (${(err as Error).message}); nessun auto-kill eseguito`);
await ensureBridge("dedicated", { forceRestart: true }); }
return await fn(); throw err;
} }
} }