Files
browser_mcp/pool.go
T
Egutierrez 71fdae9e35 fix: pool usa CdpClose(c,0) en vez de CdpDisconnect (evita drift falso en uses_functions)
El wrapper CdpDisconnect comparte entry de registry con CdpClose; el auditor
uses_functions no lo reconoce como mismo símbolo y marcaba cdp_close como
declared-but-unused. CdpClose(c,0) expresa lo mismo sin drift.
2026-06-06 15:41:01 +02:00

118 lines
3.2 KiB
Go

package main
import (
"strings"
"sync"
"fn-registry/functions/browser"
)
// connPool reusa conexiones CDP entre invocaciones de tools. Clave = puerto CDP.
// Una conexión = una sesión viva a una tab "page". Mantenerla evita pagar el
// handshake WebSocket en cada tool y preserva estado (event handlers, contexto).
type connPool struct {
mu sync.Mutex
conns map[int]*browser.CDPConn
cancels map[int]func() // cancels de handlers persistentes (handle_dialog)
dialogLogs map[int]*browser.DialogLog // log de diálogos auto-respondidos por puerto
}
func newConnPool() *connPool {
return &connPool{
conns: map[int]*browser.CDPConn{},
cancels: map[int]func(){},
dialogLogs: map[int]*browser.DialogLog{},
}
}
func (p *connPool) get(port int) (*browser.CDPConn, error) {
p.mu.Lock()
defer p.mu.Unlock()
if c, ok := p.conns[port]; ok && c != nil {
return c, nil
}
c, err := browser.CdpConnect(port)
if err != nil {
return nil, err
}
p.conns[port] = c
return c, nil
}
func (p *connPool) drop(port int) {
p.mu.Lock()
defer p.mu.Unlock()
if cancel, ok := p.cancels[port]; ok && cancel != nil {
cancel()
delete(p.cancels, port)
}
delete(p.dialogLogs, port)
if c, ok := p.conns[port]; ok && c != nil {
// pid=0: cerrar solo el WebSocket, sin matar Chrome (el navegador sigue
// vivo; solo soltamos la sesión pooled).
_ = browser.CdpClose(c, 0)
delete(p.conns, port)
}
}
// connectTarget descarta la conexión actual del puerto y reconecta a un target
// determinista (por id o substring de URL). Asegura que el agente opera sobre una
// pestaña conocida y no sobre "la primera al azar".
func (p *connPool) connectTarget(port int, match string) (*browser.CDPConn, error) {
p.drop(port)
c, err := browser.CdpConnectTarget("localhost", port, match)
if err != nil {
return nil, err
}
p.mu.Lock()
p.conns[port] = c
p.mu.Unlock()
return c, nil
}
// setDialog guarda el cancel y el DialogLog del auto-handler de diálogos del
// puerto. Si ya había uno armado, lo cancela primero.
func (p *connPool) setDialog(port int, cancel func(), dlog *browser.DialogLog) {
p.mu.Lock()
defer p.mu.Unlock()
if old := p.cancels[port]; old != nil {
old()
}
p.cancels[port] = cancel
p.dialogLogs[port] = dlog
}
// dialogSnapshot devuelve el estado del log de diálogos del puerto (0,"","" si
// no hay handler armado).
func (p *connPool) dialogSnapshot(port int) (int, string, string) {
p.mu.Lock()
defer p.mu.Unlock()
if dl := p.dialogLogs[port]; dl != nil {
return dl.Snapshot()
}
return 0, "", ""
}
func (p *connPool) closeAll() {
p.mu.Lock()
defer p.mu.Unlock()
for port, c := range p.conns {
if cancel := p.cancels[port]; cancel != nil {
cancel()
}
if c != nil {
_ = browser.CdpClose(c, 0)
}
}
p.conns = map[int]*browser.CDPConn{}
p.cancels = map[int]func(){}
p.dialogLogs = map[int]*browser.DialogLog{}
}
// isConnErr reconoce errores de conexión CDP muerta para reintentar UNA vez.
func isConnErr(err error) bool {
s := err.Error()
return strings.Contains(s, "connection close") || strings.Contains(s, "broken pipe") ||
strings.Contains(s, "use of closed") || strings.Contains(s, "ws read") || strings.Contains(s, "EOF")
}