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 }