package proc import ( "fmt" "os" "strconv" "strings" ) // ProcStat holds CPU-related information from /proc/[pid]/stat type ProcStat struct { PID int Comm string // Process name (executable filename) State byte // Process state (R, S, D, Z, T, etc.) PPID int // Parent PID UTime uint64 // User mode CPU time (in clock ticks) STime uint64 // Kernel mode CPU time (in clock ticks) CUTime int64 // Children user mode CPU time CSTime int64 // Children system mode CPU time NumThreads int64 // Number of threads StartTime uint64 // Time process started after boot (in clock ticks) } // ReadStat reads and parses /proc/[pid]/stat for the given PID func ReadStat(procPath string, pid int) (*ProcStat, error) { path := fmt.Sprintf("%s/%d/stat", procPath, pid) data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading stat file: %w", err) } return parseStat(pid, string(data)) } // parseStat parses the content of /proc/[pid]/stat // The format is complex because comm (field 2) can contain spaces and parentheses func parseStat(pid int, data string) (*ProcStat, error) { // Find the comm field which is enclosed in parentheses // This handles cases where comm contains spaces or other special characters start := strings.Index(data, "(") end := strings.LastIndex(data, ")") if start == -1 || end == -1 || end <= start { return nil, fmt.Errorf("invalid stat format: cannot find comm field") } comm := data[start+1 : end] // Fields after the comm field remainder := strings.TrimSpace(data[end+1:]) fields := strings.Fields(remainder) // We need at least 20 fields after comm (fields 3-22) if len(fields) < 20 { return nil, fmt.Errorf("invalid stat format: expected at least 20 fields after comm, got %d", len(fields)) } stat := &ProcStat{ PID: pid, Comm: comm, } // Field 3: state (index 0 after comm) if len(fields[0]) > 0 { stat.State = fields[0][0] } // Field 4: ppid (index 1) ppid, err := strconv.Atoi(fields[1]) if err != nil { return nil, fmt.Errorf("parsing ppid: %w", err) } stat.PPID = ppid // Field 14: utime (index 11) - user mode CPU time utime, err := strconv.ParseUint(fields[11], 10, 64) if err != nil { return nil, fmt.Errorf("parsing utime: %w", err) } stat.UTime = utime // Field 15: stime (index 12) - kernel mode CPU time stime, err := strconv.ParseUint(fields[12], 10, 64) if err != nil { return nil, fmt.Errorf("parsing stime: %w", err) } stat.STime = stime // Field 16: cutime (index 13) - children user mode CPU time cutime, err := strconv.ParseInt(fields[13], 10, 64) if err != nil { return nil, fmt.Errorf("parsing cutime: %w", err) } stat.CUTime = cutime // Field 17: cstime (index 14) - children system mode CPU time cstime, err := strconv.ParseInt(fields[14], 10, 64) if err != nil { return nil, fmt.Errorf("parsing cstime: %w", err) } stat.CSTime = cstime // Field 20: num_threads (index 17) numThreads, err := strconv.ParseInt(fields[17], 10, 64) if err != nil { return nil, fmt.Errorf("parsing num_threads: %w", err) } stat.NumThreads = numThreads // Field 22: starttime (index 19) - time process started after boot startTime, err := strconv.ParseUint(fields[19], 10, 64) if err != nil { return nil, fmt.Errorf("parsing starttime: %w", err) } stat.StartTime = startTime return stat, nil } // TotalTime returns the total CPU time (user + system) in clock ticks func (s *ProcStat) TotalTime() uint64 { return s.UTime + s.STime } // TotalTimeWithChildren returns total CPU time including children func (s *ProcStat) TotalTimeWithChildren() uint64 { total := int64(s.UTime) + int64(s.STime) + s.CUTime + s.CSTime if total < 0 { return 0 } return uint64(total) }