docs(flows): DoD obligatorio con user-facing surface + abrir issues 0100-0103 (taxonomia, frontmatter migration, dev_console, work dashboard)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -13,6 +15,9 @@ 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.
|
||||
// En WSL2 con chrome.exe, si el valor comienza con /tmp/ o /home/ se traduce
|
||||
// automaticamente a una ruta Windows via wslpath. Pasar una ruta Windows
|
||||
// (ej: C:\Users\...) la deja intacta.
|
||||
UserDataDir string
|
||||
// Headless activa el modo headless (--headless=new). Por defecto false.
|
||||
Headless bool
|
||||
@@ -22,6 +27,53 @@ type ChromeLaunchOpts struct {
|
||||
ExtraArgs []string
|
||||
}
|
||||
|
||||
// reWindowsPath coincide con rutas absolutas de Windows (C:\... D:\... etc.).
|
||||
var reWindowsPath = regexp.MustCompile(`(?i)^[A-Z]:\\`)
|
||||
|
||||
// isWSL2 devuelve true si el proceso corre dentro de WSL2.
|
||||
// Lee /proc/version y busca "microsoft" o "WSL" (case-insensitive).
|
||||
func isWSL2() bool {
|
||||
b, err := os.ReadFile("/proc/version")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
lower := strings.ToLower(string(b))
|
||||
return strings.Contains(lower, "microsoft") || strings.Contains(lower, "wsl")
|
||||
}
|
||||
|
||||
// isWindowsExe devuelve true si la ruta del ejecutable corresponde a un .exe
|
||||
// (chrome.exe en Windows via WSL2: puede ser /mnt/c/... o resolverse en PATH).
|
||||
func isWindowsExe(path string) bool {
|
||||
return strings.HasSuffix(strings.ToLower(path), ".exe")
|
||||
}
|
||||
|
||||
// translateUserDataDirForWindows convierte una ruta Linux a ruta Windows via wslpath.
|
||||
// Ejemplo: "/tmp/foo" -> "C:\Users\lucas\AppData\Local\Temp\foo" (depende del sistema).
|
||||
// Devuelve error si wslpath no esta disponible.
|
||||
func translateUserDataDirForWindows(linuxPath string) (string, error) {
|
||||
out, err := exec.Command("wslpath", "-w", linuxPath).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("wslpath -w %q: %w", linuxPath, err)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
// defaultWindowsUserDataDir devuelve la ruta Windows del perfil CDP por defecto,
|
||||
// usando la variable de entorno USERNAME de Windows si esta disponible.
|
||||
func defaultWindowsUserDataDir() (string, error) {
|
||||
// Intentar leer el home de Windows via wslpath del home de usuario
|
||||
// Si falla, usar C:\Users\Public\fn-chrome-cdp-profile como fallback.
|
||||
user := os.Getenv("USERNAME") // Windows user via WSL env passthrough
|
||||
if user == "" {
|
||||
user = os.Getenv("USER")
|
||||
}
|
||||
if user == "" {
|
||||
user = "Public"
|
||||
}
|
||||
linuxPath := fmt.Sprintf("/mnt/c/Users/%s/AppData/Local/fn-chrome-cdp-profile", user)
|
||||
return translateUserDataDirForWindows(linuxPath)
|
||||
}
|
||||
|
||||
// chromePaths lista los ejecutables de Chrome conocidos en WSL2/Linux.
|
||||
var chromePaths = []string{
|
||||
"chrome.exe",
|
||||
@@ -47,13 +99,13 @@ func findChrome() (string, error) {
|
||||
}
|
||||
|
||||
// waitCDPReady espera hasta que el puerto CDP responda conexiones TCP.
|
||||
// host puede estar vacio (usa "localhost").
|
||||
// host puede estar vacio (usa "127.0.0.1").
|
||||
func waitCDPReady(host string, port int, timeout time.Duration) error {
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
for time.Now().Before(deadline) {
|
||||
conn, err := net.DialTimeout("tcp", addr, 200*time.Millisecond)
|
||||
if err == nil {
|
||||
@@ -62,19 +114,22 @@ func waitCDPReady(host string, port int, timeout time.Duration) error {
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
return fmt.Errorf("chrome: puerto CDP %s:%d no disponible despues de %s", host, port, timeout)
|
||||
return fmt.Errorf("chrome: puerto CDP %s no disponible despues de %s", addr, 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.
|
||||
//
|
||||
// WSL2 + chrome.exe: cuando se detecta WSL2 y el ejecutable es un .exe,
|
||||
// - El UserDataDir se traduce automaticamente a ruta Windows via wslpath
|
||||
// (si esta vacio o comienza con /tmp/ o /home/).
|
||||
// - Se inyecta --remote-debugging-address=0.0.0.0 para que Chrome sea
|
||||
// alcanzable desde WSL2 via 127.0.0.1 (el WSL networking reenvía localhost).
|
||||
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 == "" {
|
||||
@@ -85,9 +140,48 @@ func ChromeLaunch(opts ChromeLaunchOpts) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Detectar si estamos en WSL2 lanzando un exe de Windows
|
||||
wsl2WindowsMode := isWSL2() && isWindowsExe(chromePath)
|
||||
|
||||
// Resolver UserDataDir
|
||||
userDataDir := opts.UserDataDir
|
||||
if wsl2WindowsMode {
|
||||
switch {
|
||||
case userDataDir == "" ||
|
||||
strings.HasPrefix(userDataDir, "/tmp/") ||
|
||||
strings.HasPrefix(userDataDir, "/home/"):
|
||||
// Traducir a ruta Windows
|
||||
if userDataDir == "" {
|
||||
var err error
|
||||
userDataDir, err = defaultWindowsUserDataDir()
|
||||
if err != nil {
|
||||
// Fallback seguro: usar una ruta Windows fija
|
||||
userDataDir = `C:\Users\Public\fn-chrome-cdp-profile`
|
||||
}
|
||||
} else {
|
||||
translated, err := translateUserDataDirForWindows(userDataDir)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("chrome: traducir user-data-dir para Windows: %w", err)
|
||||
}
|
||||
userDataDir = translated
|
||||
}
|
||||
case reWindowsPath.MatchString(userDataDir):
|
||||
// Ya es una ruta Windows absoluta (C:\...), dejar intacta
|
||||
default:
|
||||
// Ruta que no es ni /tmp/ ni /home/ ni Windows absoluta:
|
||||
// intentar traducir igualmente.
|
||||
translated, err := translateUserDataDirForWindows(userDataDir)
|
||||
if err == nil {
|
||||
userDataDir = translated
|
||||
}
|
||||
}
|
||||
} else if userDataDir == "" {
|
||||
userDataDir = "/tmp/chrome-cdp-profile"
|
||||
}
|
||||
|
||||
args := []string{
|
||||
fmt.Sprintf("--remote-debugging-port=%d", opts.Port),
|
||||
fmt.Sprintf("--user-data-dir=%s", opts.UserDataDir),
|
||||
fmt.Sprintf("--user-data-dir=%s", userDataDir),
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--disable-background-networking",
|
||||
@@ -101,12 +195,26 @@ func ChromeLaunch(opts ChromeLaunchOpts) (int, error) {
|
||||
"--disable-translate",
|
||||
"--metrics-recording-only",
|
||||
"--safebrowsing-disable-auto-update",
|
||||
"--remote-allow-origins=*",
|
||||
}
|
||||
|
||||
if opts.Headless {
|
||||
args = append(args, "--headless=new", "--disable-gpu")
|
||||
}
|
||||
|
||||
// En WSL2+Windows: inyectar --remote-debugging-address=0.0.0.0 si no esta ya presente
|
||||
hasBindAll := false
|
||||
for _, a := range opts.ExtraArgs {
|
||||
if a == "--remote-debugging-address=0.0.0.0" {
|
||||
hasBindAll = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if wsl2WindowsMode && !hasBindAll {
|
||||
args = append(args, "--remote-debugging-address=0.0.0.0")
|
||||
hasBindAll = true
|
||||
}
|
||||
|
||||
args = append(args, opts.ExtraArgs...)
|
||||
|
||||
cmd := exec.Command(chromePath, args...)
|
||||
@@ -120,20 +228,14 @@ func ChromeLaunch(opts ChromeLaunchOpts) (int, error) {
|
||||
|
||||
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
|
||||
}
|
||||
// Esperar a que el puerto CDP este listo.
|
||||
// Siempre esperamos, incluyendo el caso WSL2+Windows donde Chrome escucha en
|
||||
// 0.0.0.0 — el WSL networking reenvía localhost:9222 → Windows:9222.
|
||||
// Usamos 127.0.0.1 explicitamente para evitar resolución IPv6 en algunos entornos.
|
||||
_ = hasBindAll // ya no se usa para skipWait
|
||||
if err := waitCDPReady("127.0.0.1", opts.Port, 15*time.Second); err != nil {
|
||||
cmd.Process.Kill()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return pid, nil
|
||||
|
||||
Reference in New Issue
Block a user