8742cb25be
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
107 lines
3.4 KiB
Go
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
|
|
}
|