132 lines
3.7 KiB
Go
132 lines
3.7 KiB
Go
|
|
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)
|
||
|
|
}
|