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`):
- `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
+49 -38
View File
@@ -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<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> {
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<boolean> {
async function startFirefox(profile: "dedicated" | "main"): Promise<void> {
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> {
const { py, env } = await ensureVenv();
bridgeProcess = spawn(py, [BRIDGE_PY], {
stdio: "ignore",
mkdirSync(dirname(BRIDGE_LOG), { recursive: true });
const logFd = openSync(BRIDGE_LOG, "a");
let child: ReturnType<typeof spawn>;
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,
});
bridgeProcess.unref();
} 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;
});
child.unref();
}
async function waitBridgeReady(timeoutMs = 15_000): Promise<boolean> {
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<boolean> {
async function ensureBridge(
profile: "dedicated" | "main" = "dedicated",
opts: { forceRestart?: boolean } = {},
): Promise<void> {
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))) {
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;
log(`ciclo ${cycle}: bridge non pronto, kill processo`);
bridgeProcess?.kill("SIGKILL");
bridgeProcess = null;
}
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<T>(fn: () => Promise<T>, label: string): Promise<T> {
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;
}
}