mirror of
https://github.com/wavetermdev/waveterm
synced 2026-04-21 22:47:16 +00:00
144 lines
3.8 KiB
Go
144 lines
3.8 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)
|
|
}
|
|
|
|
ppid, _ := strconv.ParseInt(rest[1], 10, 32)
|
|
utime, _ := strconv.ParseUint(rest[11], 10, 64)
|
|
stime, _ := strconv.ParseUint(rest[12], 10, 64)
|
|
numThreads, _ := strconv.ParseInt(rest[17], 10, 32)
|
|
rssPages, _ := strconv.ParseInt(rest[21], 10, 64)
|
|
|
|
statusChar := rest[0]
|
|
status, ok := LinuxStatStatus[statusChar]
|
|
if !ok {
|
|
status = "unknown"
|
|
}
|
|
|
|
info := &ProcInfo{
|
|
Pid: int32(parsedPid),
|
|
Ppid: int32(ppid),
|
|
Command: comm,
|
|
Status: status,
|
|
CpuUser: float64(utime) / userHz,
|
|
CpuSys: float64(stime) / userHz,
|
|
VmRSS: uint64(rssPages * pageSize),
|
|
NumThreads: int32(numThreads),
|
|
}
|
|
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)
|
|
}
|