|
| 1 | +// Copyright 2026, Command Line Inc. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +package procinfo |
| 5 | + |
| 6 | +import ( |
| 7 | + "context" |
| 8 | + "errors" |
| 9 | + "fmt" |
| 10 | + "os" |
| 11 | + "strconv" |
| 12 | + "strings" |
| 13 | +) |
| 14 | + |
| 15 | +// userHz is USER_HZ, the kernel's timer frequency used in /proc/[pid]/stat CPU fields. |
| 16 | +// On Linux this is always 100. |
| 17 | +const userHz = 100.0 |
| 18 | + |
| 19 | +// pageSize is cached at init since it never changes at runtime. |
| 20 | +var pageSize int64 |
| 21 | + |
| 22 | +func init() { |
| 23 | + pageSize = int64(os.Getpagesize()) |
| 24 | + if pageSize <= 0 { |
| 25 | + pageSize = 4096 |
| 26 | + } |
| 27 | +} |
| 28 | + |
| 29 | +func MakeGlobalSnapshot() (any, error) { |
| 30 | + return nil, nil |
| 31 | +} |
| 32 | + |
| 33 | +// GetProcInfo reads process information for the given pid from /proc. |
| 34 | +// It reads /proc/[pid]/stat for most fields and /proc/[pid]/status for the UID. |
| 35 | +func GetProcInfo(_ context.Context, _ any, pid int32) (*ProcInfo, error) { |
| 36 | + info, err := readStat(pid) |
| 37 | + if err != nil { |
| 38 | + return nil, err |
| 39 | + } |
| 40 | + if uid, err := readUid(pid); err == nil { |
| 41 | + info.Uid = uid |
| 42 | + } else if errors.Is(err, ErrNotFound) { |
| 43 | + return nil, ErrNotFound |
| 44 | + } |
| 45 | + return info, nil |
| 46 | +} |
| 47 | + |
| 48 | +// readStat parses /proc/[pid]/stat. |
| 49 | +// |
| 50 | +// The comm field (field 2) is enclosed in parentheses and may contain spaces |
| 51 | +// and even parentheses itself, so we locate the last ')' to find the field |
| 52 | +// boundary rather than splitting on whitespace naively. |
| 53 | +func readStat(pid int32) (*ProcInfo, error) { |
| 54 | + path := fmt.Sprintf("/proc/%d/stat", pid) |
| 55 | + data, err := os.ReadFile(path) |
| 56 | + if err != nil { |
| 57 | + if errors.Is(err, os.ErrNotExist) { |
| 58 | + return nil, ErrNotFound |
| 59 | + } |
| 60 | + return nil, fmt.Errorf("procinfo: read %s: %w", path, err) |
| 61 | + } |
| 62 | + s := strings.TrimRight(string(data), "\n") |
| 63 | + |
| 64 | + // Locate comm: everything between first '(' and last ')'. |
| 65 | + lp := strings.Index(s, "(") |
| 66 | + rp := strings.LastIndex(s, ")") |
| 67 | + if lp < 0 || rp < 0 || rp <= lp { |
| 68 | + return nil, fmt.Errorf("procinfo: malformed stat for pid %d", pid) |
| 69 | + } |
| 70 | + |
| 71 | + pidStr := strings.TrimSpace(s[:lp]) |
| 72 | + comm := s[lp+1 : rp] |
| 73 | + rest := strings.Fields(s[rp+1:]) |
| 74 | + |
| 75 | + // rest[0] = field 3 (state), rest[1] = field 4 (ppid), ... |
| 76 | + // Fields after comm are numbered starting at 3, so rest[i] = field (i+3). |
| 77 | + // We need: |
| 78 | + // rest[0] = field 3 state |
| 79 | + // rest[1] = field 4 ppid |
| 80 | + // rest[11] = field 14 utime |
| 81 | + // rest[12] = field 15 stime |
| 82 | + // rest[17] = field 20 num_threads |
| 83 | + // rest[21] = field 24 rss (pages) |
| 84 | + if len(rest) < 22 { |
| 85 | + return nil, fmt.Errorf("procinfo: too few fields in stat for pid %d", pid) |
| 86 | + } |
| 87 | + |
| 88 | + parsedPid, err := strconv.ParseInt(pidStr, 10, 32) |
| 89 | + if err != nil { |
| 90 | + return nil, fmt.Errorf("procinfo: parse pid: %w", err) |
| 91 | + } |
| 92 | + |
| 93 | + ppid, _ := strconv.ParseInt(rest[1], 10, 32) |
| 94 | + utime, _ := strconv.ParseUint(rest[11], 10, 64) |
| 95 | + stime, _ := strconv.ParseUint(rest[12], 10, 64) |
| 96 | + numThreads, _ := strconv.ParseInt(rest[17], 10, 32) |
| 97 | + rssPages, _ := strconv.ParseInt(rest[21], 10, 64) |
| 98 | + |
| 99 | + statusChar := rest[0] |
| 100 | + status, ok := LinuxStatStatus[statusChar] |
| 101 | + if !ok { |
| 102 | + status = "unknown" |
| 103 | + } |
| 104 | + |
| 105 | + info := &ProcInfo{ |
| 106 | + Pid: int32(parsedPid), |
| 107 | + Ppid: int32(ppid), |
| 108 | + Command: comm, |
| 109 | + Status: status, |
| 110 | + CpuUser: float64(utime) / userHz, |
| 111 | + CpuSys: float64(stime) / userHz, |
| 112 | + VmRSS: uint64(rssPages * pageSize), |
| 113 | + NumThreads: int32(numThreads), |
| 114 | + } |
| 115 | + return info, nil |
| 116 | +} |
| 117 | + |
| 118 | +// readUid reads the real UID from /proc/[pid]/status. |
| 119 | +// The Uid line looks like: Uid: 1000 1000 1000 1000 |
| 120 | +func readUid(pid int32) (uint32, error) { |
| 121 | + path := fmt.Sprintf("/proc/%d/status", pid) |
| 122 | + data, err := os.ReadFile(path) |
| 123 | + if err != nil { |
| 124 | + if errors.Is(err, os.ErrNotExist) { |
| 125 | + return 0, ErrNotFound |
| 126 | + } |
| 127 | + return 0, fmt.Errorf("procinfo: read %s: %w", path, err) |
| 128 | + } |
| 129 | + for _, line := range strings.Split(string(data), "\n") { |
| 130 | + if !strings.HasPrefix(line, "Uid:") { |
| 131 | + continue |
| 132 | + } |
| 133 | + fields := strings.Fields(line) |
| 134 | + if len(fields) < 2 { |
| 135 | + break |
| 136 | + } |
| 137 | + uid, err := strconv.ParseUint(fields[1], 10, 32) |
| 138 | + if err != nil { |
| 139 | + break |
| 140 | + } |
| 141 | + return uint32(uid), nil |
| 142 | + } |
| 143 | + return 0, fmt.Errorf("procinfo: Uid line not found in %s", path) |
| 144 | +} |
0 commit comments