diff --git a/tools_lifecycle.go b/tools_lifecycle.go index 30e8202..526900b 100644 --- a/tools_lifecycle.go +++ b/tools_lifecycle.go @@ -57,14 +57,31 @@ type chromiumMaster struct { HasCDP bool `json:"has_cdp"` } -// readProcCmdline reads /proc//cmdline and splits it on NUL into argv. -// Returns nil if the process is gone or unreadable. -func readProcCmdline(pid int) []string { - b, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "cmdline")) - if err != nil || len(b) == 0 { +// parseCmdline turns the raw bytes of /proc//cmdline into argv. +// +// Canonically the kernel separates arguments with NUL bytes. But Chromium (and +// other programs that rewrite their process title in place) collapse the argv +// region into a single space-separated string, losing the NUL separators. In +// that case splitting on NUL yields a single giant element holding the whole +// command line, which breaks argv[0] detection and "--flag=" prefix matching. +// +// So: if the data still carries NUL separators we split on NUL (the correct, +// space-safe path). Otherwise we fall back to splitting on whitespace. The +// fallback is best-effort and would mis-split a flag value containing spaces +// (e.g. a user-data-dir path with a space), but Chromium's own flags don't, so +// it recovers the master-detection flags (--user-data-dir, --type=, +// --remote-debugging-port, --profile-directory) reliably in practice. +func parseCmdline(b []byte) []string { + s := strings.TrimRight(string(b), "\x00") + if s == "" { return nil } - raw := strings.Split(string(b), "\x00") + var raw []string + if strings.Contains(s, "\x00") { + raw = strings.Split(s, "\x00") + } else { + raw = strings.Fields(s) + } args := make([]string, 0, len(raw)) for _, a := range raw { if a != "" { @@ -74,6 +91,16 @@ func readProcCmdline(pid int) []string { return args } +// readProcCmdline reads /proc//cmdline and parses it into argv. +// Returns nil if the process is gone or unreadable. +func readProcCmdline(pid int) []string { + b, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "cmdline")) + if err != nil || len(b) == 0 { + return nil + } + return parseCmdline(b) +} + // flagValue returns the value of a "--name=value" flag from argv, plus whether it // was present. Matches the exact "--name=" prefix; the first occurrence wins. func flagValue(args []string, name string) (string, bool) { diff --git a/tools_lifecycle_test.go b/tools_lifecycle_test.go index ff9f59c..21f4e78 100644 --- a/tools_lifecycle_test.go +++ b/tools_lifecycle_test.go @@ -150,3 +150,48 @@ func TestMatchMaster(t *testing.T) { t.Errorf("unknown profile should not match") } } + +// TestParseCmdline cubre el parsing de /proc//cmdline en sus dos formatos: +// el canonico separado por NUL y el colapsado por espacios que produce Chromium +// al reescribir su titulo de proceso in-place. El segundo caso es el que rompia +// browser_list (los flags quedaban dentro de un unico argv[0] gigante). +func TestParseCmdline(t *testing.T) { + // Caso canonico: argv separado por NUL (proceso normal). + nul := []byte("/usr/lib/chromium/chromium\x00--user-data-dir=/tmp/x\x00--remote-debugging-port=9333\x00") + got := parseCmdline(nul) + want := []string{"/usr/lib/chromium/chromium", "--user-data-dir=/tmp/x", "--remote-debugging-port=9333"} + if len(got) != len(want) { + t.Fatalf("NUL: got %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("NUL[%d]: got %q, want %q", i, got[i], want[i]) + } + } + + // Caso Chromium: cmdline colapsado a una sola cadena separada por espacios. + collapsed := []byte("/usr/lib/chromium/chromium --remote-debugging-port=9333 --user-data-dir=/tmp/browser_mcp_userdata --no-first-run https://www.alsa.es/") + args := parseCmdline(collapsed) + if len(args) == 1 { + t.Fatalf("space-collapsed: parse devolvio un unico elemento gigante: %q", args[0]) + } + if args[0] != "/usr/lib/chromium/chromium" { + t.Errorf("space-collapsed argv[0]: got %q, want chromium binary", args[0]) + } + + // El master debe detectarse a partir del cmdline colapsado (regresion de browser_list). + m, ok := parseChromiumMaster(18148, args) + if !ok { + t.Fatalf("space-collapsed: parseChromiumMaster no detecto el master") + } + if m.UserDataDir != "/tmp/browser_mcp_userdata" { + t.Errorf("space-collapsed udd: got %q, want /tmp/browser_mcp_userdata", m.UserDataDir) + } + if m.CDPPort != "9333" || !m.HasCDP { + t.Errorf("space-collapsed cdp: got port=%q hasCDP=%v, want 9333/true", m.CDPPort, m.HasCDP) + } + + if parseCmdline([]byte("")) != nil || parseCmdline([]byte("\x00\x00")) != nil { + t.Errorf("cmdline vacio debe devolver nil") + } +}