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`):
|
- **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
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user