waveterm/pkg/util/procinfo/procinfo_linux.go

154 lines
3.9 KiB
Go

// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package procinfo
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"strings"
)
// userHz is USER_HZ, the kernel's timer frequency used in /proc/[pid]/stat CPU fields.
// On Linux this is always 100.
const userHz = 100.0
// pageSize is cached at init since it never changes at runtime.
var pageSize int64
func init() {
pageSize = int64(os.Getpagesize())
if pageSize <= 0 {
pageSize = 4096
}
}
func MakeGlobalSnapshot() (any, error) {
return nil, nil
}
// GetProcInfo reads process information for the given pid from /proc.
// It reads /proc/[pid]/stat for most fields and /proc/[pid]/status for the UID.
func GetProcInfo(_ context.Context, _ any, pid int32) (*ProcInfo, error) {
info, err := readStat(pid)
if err != nil {
return nil, err
}
if uid, err := readUid(pid); err == nil {
info.Uid = uid
} else if errors.Is(err, ErrNotFound) {
return nil, ErrNotFound
}
return info, nil
}
// readStat parses /proc/[pid]/stat.
//
// The comm field (field 2) is enclosed in parentheses and may contain spaces
// and even parentheses itself, so we locate the last ')' to find the field
// boundary rather than splitting on whitespace naively.
func readStat(pid int32) (*ProcInfo, error) {
path := fmt.Sprintf("/proc/%d/stat", pid)
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("procinfo: read %s: %w", path, err)
}
s := strings.TrimRight(string(data), "\n")
// Locate comm: everything between first '(' and last ')'.
lp := strings.Index(s, "(")
rp := strings.LastIndex(s, ")")
if lp < 0 || rp < 0 || rp <= lp {
return nil, fmt.Errorf("procinfo: malformed stat for pid %d", pid)
}
pidStr := strings.TrimSpace(s[:lp])
comm := s[lp+1 : rp]
rest := strings.Fields(s[rp+1:])
// rest[0] = field 3 (state), rest[1] = field 4 (ppid), ...
// Fields after comm are numbered starting at 3, so rest[i] = field (i+3).
// We need:
// rest[0] = field 3 state
// rest[1] = field 4 ppid
// rest[11] = field 14 utime
// rest[12] = field 15 stime
// rest[17] = field 20 num_threads
// rest[21] = field 24 rss (pages)
if len(rest) < 22 {
return nil, fmt.Errorf("procinfo: too few fields in stat for pid %d", pid)
}
parsedPid, err := strconv.ParseInt(pidStr, 10, 32)
if err != nil {
return nil, fmt.Errorf("procinfo: parse pid: %w", err)
}
statusChar := rest[0]
status, ok := LinuxStatStatus[statusChar]
if !ok {
status = "unknown"
}
info := &ProcInfo{
Pid: int32(parsedPid),
Command: comm,
Status: status,
CpuUser: -1,
CpuSys: -1,
VmRSS: -1,
NumThreads: -1,
}
if ppid, err := strconv.ParseInt(rest[1], 10, 32); err == nil {
info.Ppid = int32(ppid)
}
if utime, err := strconv.ParseUint(rest[11], 10, 64); err == nil {
info.CpuUser = float64(utime) / userHz
}
if stime, err := strconv.ParseUint(rest[12], 10, 64); err == nil {
info.CpuSys = float64(stime) / userHz
}
if numThreads, err := strconv.ParseInt(rest[17], 10, 32); err == nil {
info.NumThreads = int32(numThreads)
}
if rssPages, err := strconv.ParseInt(rest[21], 10, 64); err == nil {
info.VmRSS = rssPages * pageSize
}
return info, nil
}
// readUid reads the real UID from /proc/[pid]/status.
// The Uid line looks like: Uid: 1000 1000 1000 1000
func readUid(pid int32) (uint32, error) {
path := fmt.Sprintf("/proc/%d/status", pid)
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return 0, ErrNotFound
}
return 0, fmt.Errorf("procinfo: read %s: %w", path, err)
}
for _, line := range strings.Split(string(data), "\n") {
if !strings.HasPrefix(line, "Uid:") {
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
break
}
uid, err := strconv.ParseUint(fields[1], 10, 32)
if err != nil {
break
}
return uint32(uid), nil
}
return 0, fmt.Errorf("procinfo: Uid line not found in %s", path)
}