Files
fn_registry/functions/browser/cdp_handle_dialog.go
T
egutierrez 8742cb25be feat(browser): auto-commit con 60 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 11:42:31 +02:00

107 lines
3.4 KiB
Go

package browser
import (
"fmt"
"sync"
)
// DialogLog acumula lo que CdpHandleDialog auto-respondió. El worker lo rellena en
// cada diálogo; el caller lo lee con Snapshot() de forma segura (mutex interno).
// Los campos son públicos para inspección directa en tests controlados, pero en
// concurrencia usa siempre Snapshot() para evitar data races.
type DialogLog struct {
mu sync.Mutex
Count int // número de diálogos auto-respondidos
LastType string // tipo del último diálogo: alert|confirm|prompt|beforeunload
LastMessage string // mensaje del último diálogo
}
// record registra un diálogo auto-respondido. Es el núcleo puro (no toca CDP).
func (l *DialogLog) record(dialogType, message string) {
l.mu.Lock()
l.Count++
l.LastType = dialogType
l.LastMessage = message
l.mu.Unlock()
}
// Snapshot devuelve una copia consistente del estado actual del log.
func (l *DialogLog) Snapshot() (count int, lastType, lastMessage string) {
l.mu.Lock()
defer l.mu.Unlock()
return l.Count, l.LastType, l.LastMessage
}
// dialogJobBuffer es el tamaño del canal que desacopla el readLoop del worker
// que responde diálogos. Amplio para absorber ráfagas sin bloquear la lectura
// del WebSocket.
const dialogJobBuffer = 64
// CdpHandleDialog instala un auto-handler que responde automaticamente a todos
// los dialogos JS (alert, confirm, prompt, beforeunload) hasta que se llame la
// funcion cancel devuelta. Usa el evento Page.javascriptDialogOpening y
// Page.handleJavaScriptDialog del protocolo CDP.
//
// Devuelve, además del cancel, un *DialogLog que el handler rellena en cada
// diálogo: así el caller sabe cuántos diálogos se auto-respondieron y cuál fue
// el último (tipo + mensaje).
//
// Concurrencia: el handler de evento corre en la goroutine de lectura del
// WebSocket y NO puede llamar sendCDP de forma síncrona (deadlock). En vez de
// lanzar una goroutine nueva por diálogo (spawn ilimitado), encola el evento en
// un canal con buffer que consume UN único worker; el worker serializa las
// respuestas. cancel() detiene el worker y des-registra el handler; es
// idempotente (seguro llamarlo varias veces).
func CdpHandleDialog(c *CDPConn, accept bool, promptText string) (func(), *DialogLog, error) {
if c == nil {
return nil, nil, fmt.Errorf("cdp handle dialog: conexion nula")
}
if _, err := c.sendCDP("Page.enable", nil); err != nil {
return nil, nil, fmt.Errorf("cdp handle dialog: %w", err)
}
dlog := &DialogLog{}
jobs := make(chan map[string]any, dialogJobBuffer)
done := make(chan struct{})
// Worker único: serializa las respuestas a diálogos. Una sola goroutine para
// toda la vida del handler, no una por diálogo.
go func() {
for {
select {
case params := <-jobs:
dtype, _ := params["type"].(string)
msg, _ := params["message"].(string)
dlog.record(dtype, msg)
p := map[string]any{"accept": accept}
if promptText != "" {
p["promptText"] = promptText
}
_, _ = c.sendCDP("Page.handleJavaScriptDialog", p)
case <-done:
return
}
}
}()
cancelEvent := c.OnEvent("Page.javascriptDialogOpening", func(_ string, params map[string]any) {
// Encolar sin bloquear el readLoop. Si el buffer está lleno (tormenta de
// diálogos), descartamos ese evento para no colgar la conexión entera.
select {
case jobs <- params:
default:
}
})
var once sync.Once
cancel := func() {
once.Do(func() {
cancelEvent()
close(done)
})
}
return cancel, dlog, nil
}