fix(browser): avvio main non interattivo affidabile
This commit is contained in:
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user