Files
wehub-resource-sync 36b3af2e3d
PR Check / Code Quality: Format (push) Failing after 1s
PR Check / Code Quality: Lint (darwin) (push) Failing after 0s
PR Check / Code Quality: Lint (freebsd) (push) Failing after 1s
PR Check / Code Quality: Lint (windows) (push) Failing after 1s
PR Check / Code Quality: Lint (linux) (push) Failing after 1s
PR Check / Security: Vulnerability Scan (push) Failing after 0s
Update Documentation / update-docs (push) Failing after 2s
PR Check / Code Quality: Vendor (push) Failing after 1s
PR Check / Code Quality: Coverage (push) Failing after 0s
PR Check / Tests: Unit (macos-latest) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04-arm) (push) Has been cancelled
PR Check / Tests: Unit (windows-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:06 +08:00

194 lines
4.6 KiB
Go

//go:build linux
package proc
import (
"context"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/godbus/dbus/v5"
"github.com/pranshuparmar/witr/pkg/model"
)
// GetResourceContext returns resource usage context for a process
func GetResourceContext(pid int) *model.ResourceContext {
ctx := &model.ResourceContext{}
ctx.PreventsSleep = checkPreventsSleep(pid)
ctx.ThermalState = getThermalState()
ctx.AppNapped = getAppNapped(pid)
// Compute CPU% once and derive both the usage figure and the energy-impact
// label from it, instead of recomputing CPU via a second (slower) `top`.
if cpu, err := GetCPUPercent(pid, true); err == nil {
ctx.CPUUsage = cpu
ctx.EnergyImpact = energyImpactLabel(cpu)
}
return ctx
}
// thermal zone info from /sys/class/thermal
func getThermalState() string {
path := "/sys/class/thermal/thermal_zone0/temp"
if _, err := os.Stat(path); os.IsNotExist(err) {
return ""
}
readText, err := os.ReadFile(path)
if err != nil {
return ""
}
tempstr := strings.TrimSpace(string(readText))
temp, err := strconv.Atoi(tempstr)
if err != nil {
return ""
}
tempC := temp / 1000
switch {
case tempC > 90:
return fmt.Sprintf("Critical thermal pressure %d", tempC)
case tempC > 70:
return fmt.Sprintf("High thermal pressure %d", tempC)
case tempC > 60:
return fmt.Sprintf("Warm thermal state %d", tempC)
default:
return fmt.Sprintf("Normal thermal state %d", tempC)
}
}
// checkPreventsSleep checks if a process has sleep prevention assertions
func checkPreventsSleep(pid int) bool {
conn, err := dbus.SystemBus()
if err != nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// org.freedesktop.login1.Manager.ListInhibitors returns a(ssssuu):
// (what, who, why, mode, uid, pid). Reading it over D-Bus avoids forking
// `systemd-inhibit --list` on every resource lookup. SystemBus() returns a
// shared connection, so we don't close it here.
var inhibitors []struct {
What, Who, Why, Mode string
UID, PID uint32
}
obj := conn.Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1"))
if err := obj.CallWithContext(ctx, "org.freedesktop.login1.Manager.ListInhibitors", 0).Store(&inhibitors); err != nil {
return false
}
for _, in := range inhibitors {
if int(in.PID) != pid {
continue
}
what := strings.ToLower(in.What)
if strings.Contains(what, "sleep") || strings.Contains(what, "idle") || strings.Contains(what, "shutdown") {
return true
}
}
return false
}
// detect if process is in a stopped/suspended state
func getAppNapped(pid int) bool {
statFile := fmt.Sprintf("/proc/%d/stat", pid)
data, err := os.ReadFile(statFile)
if err != nil {
return false
}
dataStr := string(data)
lastParenIndex := strings.LastIndex(dataStr, ")")
if lastParenIndex == -1 || lastParenIndex+2 >= len(dataStr) {
return false
}
rest := dataStr[lastParenIndex+2:]
fields := strings.Fields(rest)
if len(fields) < 1 {
return false
}
state := fields[0]
return state == "T" || state == "t"
}
// energyImpactLabel maps a CPU-usage percentage to a coarse energy-impact band.
func energyImpactLabel(cpu float64) string {
switch {
case cpu > 50:
return "Very High"
case cpu > 25:
return "High"
case cpu > 10:
return "Medium"
case cpu > 2:
return "Low"
case cpu > 0:
return "Very Low"
default:
return ""
}
}
func GetCPUPercent(pid int, usePs ...bool) (float64, error) {
var cpu float64
shouldUsePs := len(usePs) > 0 && usePs[0]
if shouldUsePs {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "ps", "-p", strconv.Itoa(pid), "-o", "pcpu=").Output()
if err != nil {
return 0, err
}
cpuStr := strings.TrimSpace(string(out))
if cpuStr == "" {
return 0, fmt.Errorf("empty ps output")
}
cpu, err = strconv.ParseFloat(cpuStr, 64)
if err != nil {
return 0, err
}
} else {
// Use top (default)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "top", "-b", "-n", "1", "-p", strconv.Itoa(pid)).Output()
if err != nil {
return 0, err
}
lines := strings.Split(string(out), "\n")
found := false
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) >= 9 && fields[0] == strconv.Itoa(pid) {
cpuStr := strings.TrimSuffix(fields[8], "%")
cpu, err = strconv.ParseFloat(cpuStr, 64)
if err == nil {
found = true
break
}
}
}
if !found {
return 0, fmt.Errorf("process not found in top output")
}
}
return cpu, nil
}