package browser import ( "fmt" "net" "os" "os/exec" "time" ) // ChromeLaunchOpts configura el lanzamiento de Chrome con CDP. type ChromeLaunchOpts struct { // Port es el puerto de remote debugging. Por defecto 9222. Port int // UserDataDir es el directorio de perfil de Chrome. Por defecto /tmp/chrome-cdp-profile. UserDataDir string // Headless activa el modo headless (--headless=new). Por defecto false. 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 []string } // chromePaths lista los ejecutables de Chrome conocidos en WSL2/Linux. var chromePaths = []string{ "chrome.exe", "google-chrome", "chromium-browser", "chromium", "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe", "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe", "/mnt/c/Users/Public/Desktop/chrome.exe", } // findChrome localiza el ejecutable de Chrome en el sistema. func findChrome() (string, error) { for _, p := range chromePaths { if path, err := exec.LookPath(p); err == nil { return path, nil } if _, err := os.Stat(p); err == nil { return p, nil } } return "", fmt.Errorf("chrome: ejecutable no encontrado en PATH ni en rutas conocidas de Windows") } // waitCDPReady espera hasta que el puerto CDP responda conexiones TCP. // 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) addr := fmt.Sprintf("%s:%d", host, port) for time.Now().Before(deadline) { conn, err := net.DialTimeout("tcp", addr, 200*time.Millisecond) if err == nil { conn.Close() return nil } time.Sleep(200 * time.Millisecond) } 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. // Retorna el PID del proceso Chrome. Espera hasta 15s a que el puerto CDP este listo. // Busca chrome.exe en PATH (WSL2) o en rutas conocidas de Windows. func ChromeLaunch(opts ChromeLaunchOpts) (int, error) { if opts.Port == 0 { opts.Port = 9222 } if opts.UserDataDir == "" { opts.UserDataDir = "/tmp/chrome-cdp-profile" } chromePath := opts.ChromePath if chromePath == "" { var err error chromePath, err = findChrome() if err != nil { return 0, err } } args := []string{ fmt.Sprintf("--remote-debugging-port=%d", opts.Port), fmt.Sprintf("--user-data-dir=%s", opts.UserDataDir), "--no-first-run", "--no-default-browser-check", "--disable-background-networking", "--disable-client-side-phishing-detection", "--disable-default-apps", "--disable-extensions", "--disable-hang-monitor", "--disable-popup-blocking", "--disable-prompt-on-repost", "--disable-sync", "--disable-translate", "--metrics-recording-only", "--safebrowsing-disable-auto-update", } if opts.Headless { args = append(args, "--headless=new", "--disable-gpu") } args = append(args, opts.ExtraArgs...) cmd := exec.Command(chromePath, args...) // Chrome necesita que no haya stderr/stdout bloqueados cmd.Stdout = nil cmd.Stderr = nil if err := cmd.Start(); err != nil { return 0, fmt.Errorf("chrome: arrancar proceso: %w", err) } pid := cmd.Process.Pid // Esperar a que el puerto CDP este listo // Si Chrome escucha en 0.0.0.0 (ej: WSL2 -> Windows), el caller se encarga del wait skipWait := false for _, a := range opts.ExtraArgs { 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 }