diff --git a/extensions/firefox-bidi.ts b/extensions/firefox-bidi.ts index 2e212a8..a7f476c 100644 --- a/extensions/firefox-bidi.ts +++ b/extensions/firefox-bidi.ts @@ -163,8 +163,25 @@ async function withRetry(fn: () => Promise, label: string, retries = 2): P } // --------------------------------------------------------------------------- -// Ciclo di vita: Firefox + bridge, con recupero da sessione orfana +// Ciclo di vita: Firefox + bridge, con recupero da sessione orfana e gestione conflitti // --------------------------------------------------------------------------- +export type ConflictAction = "close" | "wait" | "dedicated" | "ask"; + +interface ConflictCheck { + portBusy: boolean; + firefoxRunning: boolean; + mainProfile: string; +} + +async function checkFirefoxConflict(): Promise { + try { + const { stdout } = await execFileAsync(SCRIPT, ["check"], { timeout: 5_000 }); + return JSON.parse(stdout.trim()); + } catch { + return { portBusy: await isPortOpen(9222), firefoxRunning: false, mainProfile: "" }; + } +} + async function bridgeAlive(): Promise { try { const status = await bridge("/status", undefined, STATUS_TIMEOUT_MS); @@ -174,14 +191,55 @@ async function bridgeAlive(): Promise { } } -async function startFirefox(profile: "dedicated" | "main"): Promise { - const cmd = profile === "main" ? "start-main" : "start"; - // 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; +async function startFirefox( + profile: "dedicated" | "main", + action: ConflictAction = "ask", + ctx?: any, +): Promise { + let effectiveProfile = profile; + let resolvedAction = action; + + if (profile === "main") { + const conflict = await checkFirefoxConflict(); + if (conflict.firefoxRunning || conflict.portBusy) { + if (resolvedAction === "ask") { + if (ctx?.ui?.select) { + const prompt = + conflict.firefoxRunning + ? "Firefox o il profilo utente predefinito sono già attivi in un'altra sessione. Come desideri procedere?" + : "La porta WebDriver BiDi (9222) è già occupata da un'altra sessione. Come desideri procedere?"; + const choice = await ctx.ui.select(prompt, [ + "Chiudi il browser/sessione attiva e avvia con BiDi (profilo principale)", + "Attendi la fine dell'altra sessione", + "Apri un profilo separato/indipendente (bidi-profile)", + "Annulla operazione", + ]); + + if (!choice || choice.includes("Annulla")) { + throw new Error("Avvio Firefox BiDi annullato dall'utente."); + } else if (choice.includes("Chiudi")) { + resolvedAction = "close"; + } else if (choice.includes("Attendi")) { + resolvedAction = "wait"; + } else if (choice.includes("separato")) { + resolvedAction = "dedicated"; + effectiveProfile = "dedicated"; + } + } else { + throw new Error( + "Conflitto rilevato: Firefox o il profilo principale sono già in esecuzione in un'altra sessione. " + + "Specifica il parametro 'onConflict' ('close', 'wait', 'dedicated') per scegliere l'azione desiderata.", + ); + } + } + } + } + + const cmd = effectiveProfile === "main" ? "start-main" : "start"; + const env = { + ...process.env, + FIREFOX_BIDI_ACTION: resolvedAction, + }; await execFileAsync(SCRIPT, [cmd], { timeout: 90_000, env }); } @@ -304,10 +362,12 @@ function isPortOpen(port: number, host = "127.0.0.1"): Promise { async function ensureBridge( profile: "dedicated" | "main" = "dedicated", + onConflict: ConflictAction = "ask", + ctx?: any, ): Promise { if (await bridgeAlive()) return; if (!(await isPortOpen(9222))) { - await startFirefox(profile); + await startFirefox(profile, onConflict, ctx); } else { log("Firefox già attivo su 9222: avvio solo il bridge"); } @@ -343,8 +403,10 @@ export default function (pi: ExtensionAPI) { label: "Browser Start", description: "Avvia Firefox con WebDriver BiDi (porta 9222) e il Browser Bridge. " + - "profile=main usa il profilo principale (login/cookie intatti, chiude l'istanza attiva); " + - "profile=dedicated (default) usa il profilo di automazione bidi-profile.", + "profile=main usa il profilo principale (login/cookie reali); " + + "profile=dedicated (default) usa il profilo di automazione isolato bidi-profile. " + + "In caso di conflitto su profilo o porta già attivi, consente di chiedere all'utente o " + + "specificare l'azione desiderata (close, wait, dedicated).", parameters: Type.Object({ profile: Type.Optional( Type.Enum( @@ -352,9 +414,20 @@ export default function (pi: ExtensionAPI) { { description: "Profilo Firefox: dedicated (automazione) o main (login reali)", default: "dedicated" }, ), ), + onConflict: Type.Optional( + Type.Enum( + { close: "close", wait: "wait", dedicated: "dedicated", ask: "ask" }, + { + description: + "Azione se il profilo/browser è già attivo: 'close' (chiude l'istanza e avvia BiDi), " + + "'wait' (attende il rilascio), 'dedicated' (avvia un profilo indipendente), 'ask' (chiede conferma all'utente)", + default: "ask", + }, + ), + ), }), - async execute(_id, params) { - await ensureBridge(params.profile ?? "dedicated"); + async execute(_id, params, _signal, _onUpdate, ctx) { + await ensureBridge(params.profile ?? "dedicated", params.onConflict ?? "ask", ctx); const st = await bridge("/status"); return { content: [{ type: "text", text: `Firefox BiDi attivo: ${JSON.stringify(st)}` }],