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
+8 -6
View File
@@ -3,7 +3,7 @@ name: chrome_launch
kind: function
lang: go
domain: browser
version: "1.3.0"
version: "1.4.0"
purity: impure
signature: "func ChromeLaunch(opts ChromeLaunchOpts) (int, error)"
description: "Lanza Google Chrome con remote debugging habilitado en el puerto indicado. En Linux nativo busca primero chromium/google-chrome/brave; en WSL2 busca chrome.exe primero. En WSL2+chrome.exe traduce UserDataDir a ruta Windows via wslpath e inyecta --remote-debugging-address=0.0.0.0. En Linux nativo setea Setpgid=true para crear grupo de proceso propio (permite matar el arbol completo con CdpClose). Espera hasta 15s a que el puerto CDP este listo. Retorna el PID del proceso."
@@ -16,10 +16,10 @@ error_type: "error_go_core"
imports: [fmt, net, os, os/exec, regexp, strings, syscall, time]
params:
- name: opts
desc: "opciones de lanzamiento: Port (defecto 9222), UserDataDir (defecto /tmp/chrome-cdp-profile en Linux, C:\\Users\\<USER>\\AppData\\Local\\fn-chrome-cdp-profile en WSL2+exe), Headless, ChromePath, ExtraArgs, KeepExtensions (si true no añade --disable-extensions, util para cargar extensiones del perfil), ProfileDirectory (selecciona el perfil con --profile-directory, ej: Default / Automation; vacío = no se pasa el flag)"
output: "int: PID del proceso Chrome lanzado"
desc: "opciones de lanzamiento: Port (defecto 9222), UserDataDir (defecto /tmp/chrome-cdp-profile en Linux, C:\\Users\\<USER>\\AppData\\Local\\fn-chrome-cdp-profile en WSL2+exe), Headless, ChromePath, ExtraArgs, KeepExtensions (si true no añade --disable-extensions, util para cargar extensiones del perfil), ProfileDirectory (selecciona el perfil con --profile-directory, ej: Default / Automation; vacío = no se pasa el flag), ReuseExisting (si true y el puerto CDP ya responde, no lanza Chrome nuevo y devuelve pid 0 — anti-duplicado)"
output: "int: PID del proceso Chrome lanzado, o 0 si ReuseExisting=true y ya había un Chrome vivo en el puerto"
tested: true
tests: ["TestIsWSL2", "TestTranslateUserDataDirForWindows", "TestIsWindowsExe", "TestFindChrome", "TestChromeLaunchAndConnect"]
tests: ["TestIsWSL2", "TestTranslateUserDataDirForWindows", "TestIsWindowsExe", "TestFindChrome", "TestChromeLaunchAndConnect", "TestCdpPortResponds", "TestChromeLaunchReuseExisting"]
test_file_path: "functions/browser/chrome_launch_test.go"
file_path: "functions/browser/chrome_launch.go"
---
@@ -71,8 +71,9 @@ Cuando necesites lanzar Chrome con CDP desde Go para automatizacion (scraping, t
- **KeepExtensions**: por defecto se añade `--disable-extensions`. Pasar `KeepExtensions: true` para omitir ese flag y mantener extensiones del perfil (útil con perfiles reales de usuario).
- **`wslpath` debe estar disponible** (WSL2 desde Windows 10 1903+): se invoca como subproceso en modo WSL2+exe. Si falla, `ChromeLaunch` retorna error.
- **ProfileDirectory obligatorio con múltiples perfiles**: sin `--profile-directory`, si el `user-data-dir` contiene varios perfiles (Default, Personal, Profile 1, Automation…) Chrome se queda atascado en el selector de perfil y no carga nada — el puerto CDP responde pero no hay perfil activo y las extensiones no se procesan. Pasar `ProfileDirectory: "Default"` (o el nombre exacto del subdirectorio) para evitarlo.
- **Chrome no cierra solo**: el PID devuelto es el proceso Chrome. Usar `CdpClose(nil, pid)` para terminar el arbol de procesos.
- **Puerto ocupado**: si el puerto ya está en uso por otra instancia de Chrome, `waitCDPReady` puede conectar al proceso previo. Usar puertos distintos por sesión.
- **Chrome no cierra solo**: el PID devuelto es el proceso Chrome. Usar `CdpClose(nil, pid)` para terminar el arbol de procesos. Quien lance debe guardar el pid; sin él, `CdpClose(c, 0)` solo cierra el WebSocket y deja Chrome huérfano (~789 MiB RSS cada uno). Acumular lanzamientos sin matar = leak de RAM.
- **Puerto ocupado**: si el puerto ya está en uso por otra instancia de Chrome, `waitCDPReady` puede conectar al proceso previo. Usar puertos distintos por sesión, o pasar `ReuseExisting: true` para que la función NO lance un duplicado y devuelva pid 0 (el caller se adjunta al Chrome existente).
- **ReuseExisting + pid 0**: con `ReuseExisting: true` un retorno `(0, nil)` significa "ya había un Chrome vivo en el puerto, no lancé otro". El caller NO debe registrar ni intentar matar ese pid 0; el proceso no es suyo (puede ser el navegador diario del usuario).
## Notas
@@ -95,3 +96,4 @@ El struct `ChromeLaunchOpts` se define en el mismo archivo.
- v1.1.0 (2026-05-16) — auto-handle WSL2→Windows chrome.exe: translate user-data-dir via wslpath + inject --remote-debugging-address=0.0.0.0
- v1.2.0 (2026-06-05) — Linux-first: reordena busqueda (chromium antes que chrome.exe) en Linux nativo; añade KeepExtensions; setea Setpgid=true en Linux para habilitar kill-by-group en CdpClose
- v1.3.0 (2026-06-05) — añade ProfileDirectory / --profile-directory para seleccionar perfil dentro del user-data-dir (evita quedarse atascado en el selector cuando hay varios perfiles)
- v1.4.0 (2026-06-06) — añade ReuseExisting: guarda anti-duplicado que devuelve (0, nil) sin lanzar cuando el puerto CDP ya responde. Extrae helper dialCDP/cdpPortResponds (sondeo TCP reutilizado por waitCDPReady). Cierra el leak de chromium huérfanos del browser_mcp (lanzamientos repetidos al mismo puerto)