feat: cdp_wait_load y mejoras en CDP connect/launch
Nueva función cdp_wait_load para esperar carga completa de página. CdpConnect ahora soporta host remoto via CdpConnectHost (útil para WSL2 donde Chrome Windows escucha en IP distinta). Mejoras en chrome_launch para configuración más flexible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,8 +20,11 @@ type cdpTarget struct {
|
|||||||
|
|
||||||
// cdpGetPageWSURL obtiene el webSocketDebuggerUrl de la primera tab de tipo "page"
|
// cdpGetPageWSURL obtiene el webSocketDebuggerUrl de la primera tab de tipo "page"
|
||||||
// via el endpoint /json. Si no hay ninguna, crea una nueva con /json/new.
|
// via el endpoint /json. Si no hay ninguna, crea una nueva con /json/new.
|
||||||
func cdpGetPageWSURL(port int) (string, error) {
|
func cdpGetPageWSURL(host string, port int) (string, error) {
|
||||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/json", port))
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
|
resp, err := http.Get(fmt.Sprintf("http://%s:%d/json", host, port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("cdp targets: %w", err)
|
return "", fmt.Errorf("cdp targets: %w", err)
|
||||||
}
|
}
|
||||||
@@ -40,7 +43,7 @@ func cdpGetPageWSURL(port int) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No hay tabs — crear una nueva via /json/new
|
// No hay tabs — crear una nueva via /json/new
|
||||||
newResp, err := http.Get(fmt.Sprintf("http://localhost:%d/json/new", port))
|
newResp, err := http.Get(fmt.Sprintf("http://%s:%d/json/new", host, port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("cdp new tab: %w", err)
|
return "", fmt.Errorf("cdp new tab: %w", err)
|
||||||
}
|
}
|
||||||
@@ -61,7 +64,16 @@ func cdpGetPageWSURL(port int) (string, error) {
|
|||||||
// Si no hay tabs disponibles, crea una nueva via /json/new.
|
// Si no hay tabs disponibles, crea una nueva via /json/new.
|
||||||
// Realiza el handshake WebSocket RFC 6455 sobre TCP puro (sin dependencias externas).
|
// Realiza el handshake WebSocket RFC 6455 sobre TCP puro (sin dependencias externas).
|
||||||
func CdpConnect(port int) (*CDPConn, error) {
|
func CdpConnect(port int) (*CDPConn, error) {
|
||||||
wsURL, err := cdpGetPageWSURL(port)
|
return CdpConnectHost("localhost", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CdpConnectHost es como CdpConnect pero permite especificar el host.
|
||||||
|
// Util para WSL2 donde Chrome Windows escucha en una IP distinta a localhost.
|
||||||
|
func CdpConnectHost(host string, port int) (*CDPConn, error) {
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
|
wsURL, err := cdpGetPageWSURL(host, port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cdp connect: %w", err)
|
return nil, fmt.Errorf("cdp connect: %w", err)
|
||||||
}
|
}
|
||||||
@@ -72,20 +84,20 @@ func CdpConnect(port int) (*CDPConn, error) {
|
|||||||
return nil, fmt.Errorf("cdp connect: parsear ws url %q: %w", wsURL, err)
|
return nil, fmt.Errorf("cdp connect: parsear ws url %q: %w", wsURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
host := u.Host
|
wsHost := u.Host
|
||||||
if !strings.Contains(host, ":") {
|
if !strings.Contains(wsHost, ":") {
|
||||||
host = host + ":80"
|
wsHost = wsHost + ":80"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abrir conexion TCP
|
// Abrir conexion TCP
|
||||||
tcpConn, err := net.Dial("tcp", host)
|
tcpConn, err := net.Dial("tcp", wsHost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cdp connect: tcp dial %s: %w", host, err)
|
return nil, fmt.Errorf("cdp connect: tcp dial %s: %w", wsHost, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Realizar handshake WebSocket
|
// Realizar handshake WebSocket
|
||||||
path := u.RequestURI()
|
path := u.RequestURI()
|
||||||
reader, err := wsHandshake(tcpConn, host, path)
|
reader, err := wsHandshake(tcpConn, wsHost, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tcpConn.Close()
|
tcpConn.Close()
|
||||||
return nil, fmt.Errorf("cdp connect: ws handshake: %w", err)
|
return nil, fmt.Errorf("cdp connect: ws handshake: %w", err)
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CdpWaitLoad espera a que la página actual termine de cargar completamente.
|
||||||
|
// Hace polling de document.readyState via Runtime.evaluate cada 200ms hasta
|
||||||
|
// que sea "complete", o hasta que se agote el timeout.
|
||||||
|
// Retorna error si el timeout se agota o si CdpEvaluate falla (conexion rota).
|
||||||
|
func CdpWaitLoad(c *CDPConn, timeout time.Duration) error {
|
||||||
|
if c == nil {
|
||||||
|
return fmt.Errorf("cdp wait load: conexion nula")
|
||||||
|
}
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 30 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
interval := 200 * time.Millisecond
|
||||||
|
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
result, err := CdpEvaluate(c, "document.readyState")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cdp wait load: error evaluando readyState: %w", err)
|
||||||
|
}
|
||||||
|
if result == "complete" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cdp wait load: pagina no cargo despues de %s", timeout)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: cdp_wait_load
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func CdpWaitLoad(c *CDPConn, timeout time.Duration) error"
|
||||||
|
description: "Espera a que la pagina actual termine de cargar completamente. Hace polling de document.readyState via Runtime.evaluate cada 200ms hasta que sea \"complete\", o hasta que se agote el timeout. Retorna error inmediato si CdpEvaluate falla (la conexion puede estar rota)."
|
||||||
|
tags: [chrome, cdp, browser, automation, wait, polling, devtools, readystate, load]
|
||||||
|
uses_functions: [cdp_evaluate_go_infra]
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt, time]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/cdp_wait_load.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
conn, _ := CdpConnect(9222)
|
||||||
|
CdpNavigate(conn, "https://example.com")
|
||||||
|
|
||||||
|
// Esperar hasta 30 segundos a que la pagina cargue por completo
|
||||||
|
if err := CdpWaitLoad(conn, 30*time.Second); err != nil {
|
||||||
|
log.Fatal("Timeout esperando carga:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
html, _ := CdpGetHTML(conn)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
A diferencia de `CdpWaitElement`, que ignora errores de `CdpEvaluate` durante el polling (la pagina puede aun no estar lista), `CdpWaitLoad` retorna el error inmediatamente porque un fallo en `document.readyState` indica una conexion rota, no una condicion transitoria.
|
||||||
|
|
||||||
|
Si `timeout <= 0` usa 30s por defecto (mas largo que `CdpWaitElement` porque la carga completa de red puede tardar mas que la aparicion de un elemento DOM).
|
||||||
@@ -16,6 +16,8 @@ type ChromeLaunchOpts struct {
|
|||||||
UserDataDir string
|
UserDataDir string
|
||||||
// Headless activa el modo headless (--headless=new). Por defecto false.
|
// Headless activa el modo headless (--headless=new). Por defecto false.
|
||||||
Headless bool
|
Headless bool
|
||||||
|
// ChromePath es la ruta al ejecutable de Chrome. Si esta vacio, se busca automaticamente.
|
||||||
|
ChromePath string
|
||||||
// ExtraArgs permite pasar flags adicionales a Chrome.
|
// ExtraArgs permite pasar flags adicionales a Chrome.
|
||||||
ExtraArgs []string
|
ExtraArgs []string
|
||||||
}
|
}
|
||||||
@@ -45,9 +47,13 @@ func findChrome() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// waitCDPReady espera hasta que el puerto CDP responda conexiones TCP.
|
// waitCDPReady espera hasta que el puerto CDP responda conexiones TCP.
|
||||||
func waitCDPReady(port int, timeout time.Duration) error {
|
// host puede estar vacio (usa "localhost").
|
||||||
|
func waitCDPReady(host string, port int, timeout time.Duration) error {
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
deadline := time.Now().Add(timeout)
|
deadline := time.Now().Add(timeout)
|
||||||
addr := fmt.Sprintf("localhost:%d", port)
|
addr := fmt.Sprintf("%s:%d", host, port)
|
||||||
for time.Now().Before(deadline) {
|
for time.Now().Before(deadline) {
|
||||||
conn, err := net.DialTimeout("tcp", addr, 200*time.Millisecond)
|
conn, err := net.DialTimeout("tcp", addr, 200*time.Millisecond)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -56,7 +62,7 @@ func waitCDPReady(port int, timeout time.Duration) error {
|
|||||||
}
|
}
|
||||||
time.Sleep(200 * time.Millisecond)
|
time.Sleep(200 * time.Millisecond)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("chrome: puerto CDP %d no disponible despues de %s", port, timeout)
|
return fmt.Errorf("chrome: puerto CDP %s:%d no disponible despues de %s", host, port, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChromeLaunch lanza Google Chrome con remote debugging habilitado en el puerto indicado.
|
// ChromeLaunch lanza Google Chrome con remote debugging habilitado en el puerto indicado.
|
||||||
@@ -70,9 +76,13 @@ func ChromeLaunch(opts ChromeLaunchOpts) (int, error) {
|
|||||||
opts.UserDataDir = "/tmp/chrome-cdp-profile"
|
opts.UserDataDir = "/tmp/chrome-cdp-profile"
|
||||||
}
|
}
|
||||||
|
|
||||||
chromePath, err := findChrome()
|
chromePath := opts.ChromePath
|
||||||
if err != nil {
|
if chromePath == "" {
|
||||||
return 0, err
|
var err error
|
||||||
|
chromePath, err = findChrome()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
@@ -111,10 +121,19 @@ func ChromeLaunch(opts ChromeLaunchOpts) (int, error) {
|
|||||||
pid := cmd.Process.Pid
|
pid := cmd.Process.Pid
|
||||||
|
|
||||||
// Esperar a que el puerto CDP este listo
|
// Esperar a que el puerto CDP este listo
|
||||||
if err := waitCDPReady(opts.Port, 15*time.Second); err != nil {
|
// Si Chrome escucha en 0.0.0.0 (ej: WSL2 -> Windows), el caller se encarga del wait
|
||||||
// Matar proceso si no arranco correctamente
|
skipWait := false
|
||||||
cmd.Process.Kill()
|
for _, a := range opts.ExtraArgs {
|
||||||
return 0, err
|
if a == "--remote-debugging-address=0.0.0.0" {
|
||||||
|
skipWait = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !skipWait {
|
||||||
|
if err := waitCDPReady("localhost", opts.Port, 15*time.Second); err != nil {
|
||||||
|
cmd.Process.Kill()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pid, nil
|
return pid, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user