fix(browser_list): parse cmdline colapsado por espacios de Chromium

/proc/<pid>/cmdline normalmente separa argv por NUL, pero Chromium reescribe
su titulo de proceso in-place colapsando la region de argv a una sola cadena
separada por espacios. readProcCmdline asumia solo NUL, asi que para los
masters de Chromium devolvia un unico argv[0] gigante: isChromiumExe y el
prefijo --user-data-dir= fallaban y browser_list devolvia [] aunque hubiera
navegadores vivos.

Extrae parseCmdline (pura, testeable) con fallback a split por espacios cuando
no hay NUL. Test cubre ambos formatos + regresion de deteccion de master.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Egutierrez
2026-06-16 20:02:27 +02:00
parent 1c5b81f711
commit c2470f4f67
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) {