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:
2026-06-06 17:06:45 +02:00
parent 029dbf57bd
commit 37aacfcfa9
3 changed files with 92 additions and 11 deletions
+44
View File
@@ -1,6 +1,7 @@
package browser
import (
"net"
"os"
"regexp"
"strings"
@@ -288,3 +289,46 @@ func TestCdpScreenshot(t *testing.T) {
t.Logf("Screenshot creado: %s (%d bytes)", outputPath, info.Size())
})
}
// TestCdpPortResponds verifica el sondeo TCP del puerto CDP sin Chrome real:
// un net.Listener local hace de "puerto ocupado" y, al cerrarlo, el puerto
// deja de responder.
func TestCdpPortResponds(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
port := ln.Addr().(*net.TCPAddr).Port
if !cdpPortResponds(port) {
t.Errorf("cdpPortResponds(%d) = false con listener vivo, want true", port)
}
if err := ln.Close(); err != nil {
t.Fatalf("close listener: %v", err)
}
if cdpPortResponds(port) {
t.Errorf("cdpPortResponds(%d) = true tras cerrar el listener, want false", port)
}
}
// TestChromeLaunchReuseExisting verifica que con ReuseExisting=true y un puerto
// ya ocupado, ChromeLaunch NO lanza Chrome y devuelve (0, nil). No requiere
// Chrome real: el listener simula un endpoint CDP vivo. Esto es la guarda
// anti-duplicado que evita el leak de procesos chromium huerfanos.
func TestChromeLaunchReuseExisting(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
defer ln.Close()
port := ln.Addr().(*net.TCPAddr).Port
pid, err := ChromeLaunch(ChromeLaunchOpts{Port: port, ReuseExisting: true})
if err != nil {
t.Fatalf("ChromeLaunch(ReuseExisting): %v", err)
}
if pid != 0 {
t.Errorf("pid = %d, want 0 (debe reusar el existente sin lanzar Chrome)", pid)
}
}