feat(conflict): gestione interattiva conflitti profilo/porta attiva su browser_start

This commit is contained in:
Matteo Benedetto
2026-08-22 12:33:07 +02:00
parent 9f9fe046dd
commit c3eea04bf1
+87 -14
View File
@@ -163,8 +163,25 @@ async function withRetry<T>(fn: () => Promise<T>, 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<ConflictCheck> {
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<boolean> {
try {
const status = await bridge("/status", undefined, STATUS_TIMEOUT_MS);
@@ -174,14 +191,55 @@ async function bridgeAlive(): Promise<boolean> {
}
}
async function startFirefox(profile: "dedicated" | "main"): Promise<void> {
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<void> {
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<boolean> {
async function ensureBridge(
profile: "dedicated" | "main" = "dedicated",
onConflict: ConflictAction = "ask",
ctx?: any,
): Promise<void> {
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)}` }],