feat(browser): chrome_launch ReuseExisting — guarda anti-duplicado de Chrome
Añade el campo ReuseExisting a ChromeLaunchOpts. Con ReuseExisting=true, si el puerto CDP ya responde a una conexión TCP, ChromeLaunch NO lanza un Chrome nuevo y devuelve (0, nil) para que el caller se adjunte al existente. Evita acumular procesos chromium duplicados en el mismo puerto (cada uno ~789 MiB RSS), causa del leak de RAM del browser_mcp. Extrae el sondeo de puerto a dialCDP/cdpPortResponds (net.Dial con timeout), que waitCDPReady ahora reutiliza en su bucle. Tests sin Chrome real (TestCdpPortResponds, TestChromeLaunchReuseExisting) usando un net.Listener local como puerto ocupado. Bump a 1.4.0 + growth log + gotchas en el .md (pid 0 = no es nuestro, no matar). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,12 @@ type ChromeLaunchOpts struct {
|
||||
// Vacío = no se pasa el flag (Chrome usa su default o muestra el selector si hay varios perfiles).
|
||||
// Ej: "Default", "Automation".
|
||||
ProfileDirectory string
|
||||
// ReuseExisting, si es true y el puerto CDP ya responde a una conexion TCP,
|
||||
// NO lanza un Chrome nuevo: devuelve (0, nil) para que el caller reutilice el
|
||||
// navegador que ya está vivo en ese puerto. Evita acumular procesos chromium
|
||||
// duplicados (cada uno ~789 MiB RSS) cuando se llama repetidamente al mismo
|
||||
// puerto. El caller distingue el reuso por pid == 0.
|
||||
ReuseExisting bool
|
||||
}
|
||||
|
||||
// reWindowsPath coincide con rutas absolutas de Windows (C:\... D:\... etc.).
|
||||
@@ -137,6 +143,30 @@ func findChrome() (string, error) {
|
||||
return "", fmt.Errorf("chrome: ejecutable no encontrado en PATH ni en rutas conocidas")
|
||||
}
|
||||
|
||||
// dialCDP intenta una conexion TCP unica al puerto CDP. Devuelve true si el
|
||||
// puerto acepta la conexion (hay algo escuchando), false en caso contrario.
|
||||
// host vacio usa "127.0.0.1".
|
||||
func dialCDP(host string, port int, timeout time.Duration) bool {
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
conn, err := net.DialTimeout("tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
conn.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// cdpPortResponds indica si ya hay un proceso escuchando el puerto CDP en
|
||||
// 127.0.0.1. Es un sondeo TCP unico con timeout corto, usado por ChromeLaunch
|
||||
// (opts.ReuseExisting) para no relanzar un Chrome duplicado cuando el puerto ya
|
||||
// tiene uno vivo.
|
||||
func cdpPortResponds(port int) bool {
|
||||
return dialCDP("127.0.0.1", port, 300*time.Millisecond)
|
||||
}
|
||||
|
||||
// waitCDPReady espera hasta que el puerto CDP responda conexiones TCP.
|
||||
// host puede estar vacio (usa "127.0.0.1").
|
||||
func waitCDPReady(host string, port int, timeout time.Duration) error {
|
||||
@@ -144,16 +174,14 @@ func waitCDPReady(host string, port int, timeout time.Duration) error {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
for time.Now().Before(deadline) {
|
||||
conn, err := net.DialTimeout("tcp", addr, 200*time.Millisecond)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
if dialCDP(host, port, 200*time.Millisecond) {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
return fmt.Errorf("chrome: puerto CDP %s no disponible despues de %s", addr, timeout)
|
||||
return fmt.Errorf("chrome: puerto CDP %s no disponible despues de %s",
|
||||
net.JoinHostPort(host, fmt.Sprintf("%d", port)), timeout)
|
||||
}
|
||||
|
||||
// ChromeLaunch lanza Google Chrome con remote debugging habilitado en el puerto indicado.
|
||||
@@ -170,6 +198,13 @@ func ChromeLaunch(opts ChromeLaunchOpts) (int, error) {
|
||||
opts.Port = 9222
|
||||
}
|
||||
|
||||
// Anti-duplicado: si el caller pide reusar y ya hay un Chrome escuchando el
|
||||
// puerto CDP, no lanzamos otro. Devolvemos pid 0 para que el caller sepa que
|
||||
// debe adjuntarse al existente en vez de registrar un proceso nuevo.
|
||||
if opts.ReuseExisting && cdpPortResponds(opts.Port) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
chromePath := opts.ChromePath
|
||||
if chromePath == "" {
|
||||
var err error
|
||||
|
||||
Reference in New Issue
Block a user