Merge quick/fix-cmdline-space-collapse: browser_list ve Chromium con cmdline colapsado

This commit is contained in:
2026-06-16 20:02:27 +02:00
2 changed files with 78 additions and 6 deletions
+33 -6
View File
@@ -57,14 +57,31 @@ type chromiumMaster struct {
HasCDP bool `json:"has_cdp"`
}
// readProcCmdline reads /proc/<pid>/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/<pid>/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/<pid>/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) {
+45
View File
@@ -150,3 +150,48 @@ func TestMatchMaster(t *testing.T) {
t.Errorf("unknown profile should not match")
}
}
// TestParseCmdline cubre el parsing de /proc/<pid>/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")
}
}