//go:build linux package main import ( "os" "strconv" "strings" "sync" "time" ) // Linux facts for the origin walk, all from /proc: comm, ppid and tty_nr // from /proc//stat, the cgroup's leaf unit from /proc//cgroup, the // executable from /proc//exe. Read per pid on demand and memoised for // one scan by the closure procFactsReader returns. // parseProcStatFacts reads comm, ppid and tty_nr out of a stat line. The // comm is parenthesised and may hold spaces; fields are counted from the // LAST ')': state(0) ppid(1) pgrp(2) session(3) tty_nr(4). func parseProcStatFacts(stat string) (comm string, ppid int, tty int, ok bool) { i := strings.LastIndex(stat, ")") j := strings.Index(stat, "(") if i < 0 || j < 0 || j > i { return "", 0, 0, false } comm = stat[j+1 : i] f := strings.Fields(stat[i+1:]) if len(f) < 5 { return comm, 0, 0, false } ppid, _ = strconv.Atoi(f[1]) tty, _ = strconv.Atoi(f[4]) return comm, ppid, tty, true } // cgroupUnit returns the systemd unit a cgroup path ends in (a .service or a // user@N.service child), or "" for a login session scope and everything // else. "0::/system.slice/cron.service" -> "cron.service". func cgroupUnit(cgroup string) string { for _, line := range strings.Split(cgroup, "\n") { // v2: "0::/path"; v1: "N:controllers:/path" — the path is after the last ':' k := strings.LastIndex(line, ":") if k < 0 { continue } path := line[k+1:] leaf := path[strings.LastIndex(path, "/")+1:] if strings.HasSuffix(leaf, ".service") { return leaf } } return "" } func procFactsReader() func(int) (procFacts, bool) { memo := map[int]procFacts{} miss := map[int]bool{} return func(pid int) (procFacts, bool) { if f, ok := memo[pid]; ok { return f, true } if miss[pid] { return procFacts{}, false } p := "/proc/" + strconv.Itoa(pid) stat, err := os.ReadFile(p + "/stat") if err != nil { miss[pid] = true return procFacts{}, false } comm, ppid, tty, ok := parseProcStatFacts(string(stat)) if !ok { miss[pid] = true return procFacts{}, false } f := procFacts{PID: pid, PPID: ppid, Comm: binBase(comm), Interactive: tty != 0} if tty != 0 { f.Detail = "tty " + ttyName(tty) } if cg, err := os.ReadFile(p + "/cgroup"); err == nil { f.Unit = cgroupUnit(string(cg)) } if exe, err := os.Readlink(p + "/exe"); err == nil { f.Exe = strings.TrimSuffix(exe, " (deleted)") if f.Comm == "" { f.Comm = binBase(f.Exe) } } memo[pid] = f return f, true } } // ttyName renders a tty_nr the way `ps` does for the common cases: pts/N and // ttyN. Anything else is shown as the raw number — a fact, not a guess. func ttyName(n int) string { major, minor := (n>>8)&0xfff, (n&0xff)|((n>>12)&0xfff00) switch major { case 136, 137, 138, 139, 140, 141, 142, 143: return "pts/" + strconv.Itoa((major-136)*256+minor) case 4: return "tty" + strconv.Itoa(minor) } return strconv.Itoa(n) } // systemd timers, read once per scan window and shared with the scheduler // inventory. `systemctl` absent (a non-systemd box) yields an empty map. var timerCache = struct { sync.Mutex at time.Time m map[string]string }{} func systemdTimerUnits() map[string]string { timerCache.Lock() defer timerCache.Unlock() if timerCache.m != nil && time.Since(timerCache.at) < schedCacheFor { return timerCache.m } m := map[string]string{} if out, err := runTimed("systemctl", "list-timers", "--all", "--no-legend", "--plain"); err == nil { m = parseSystemdTimers(string(out)) } timerCache.at, timerCache.m = time.Now(), m return m } func originExtras() originExtra { return originExtra{TimerUnits: systemdTimerUnits()} }