109 lines
3.1 KiB
Go
109 lines
3.1 KiB
Go
//go:build !windows
|
|
|
|
package client
|
|
|
|
import (
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// collectResourcePressure gathers disk, CPU frequency/temp, and GPU metrics on Linux/macOS.
|
|
func collectResourcePressure() *ResourcePressure {
|
|
r := &ResourcePressure{}
|
|
|
|
// ── Disk free space ───────────────────────────────────────────────────────
|
|
exe, _ := os.Executable()
|
|
if exe == "" {
|
|
exe = "/"
|
|
}
|
|
freeGB, totalGB, freePct := unixDiskStats(filepath.Dir(exe))
|
|
if freePct >= 0 {
|
|
r.DiskFreeGB = &freeGB
|
|
r.DiskTotalGB = &totalGB
|
|
r.DiskFreePct = &freePct
|
|
}
|
|
|
|
// ── CPU frequency ─────────────────────────────────────────────────────────
|
|
cur := readSysInt("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq")
|
|
max := readSysInt("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq")
|
|
if cur > 0 {
|
|
mhz := cur / 1000
|
|
r.CPUFreqMHz = &mhz
|
|
}
|
|
if max > 0 {
|
|
mhz := max / 1000
|
|
r.CPUMaxMHz = &mhz
|
|
if cur > 0 {
|
|
throttled := float64(cur) < float64(max)*0.80
|
|
r.CPUThrottle = &throttled
|
|
}
|
|
}
|
|
|
|
// ── CPU thermal ───────────────────────────────────────────────────────────
|
|
if temp := probeCPUTemp(); temp > 0 {
|
|
r.CPUTempC = &temp
|
|
}
|
|
|
|
// ── GPU (NVIDIA) ──────────────────────────────────────────────────────────
|
|
r.GPUTempC, r.GPUUsagePct = probeGPU()
|
|
|
|
return r
|
|
}
|
|
|
|
// unixDiskStats returns (freeGB, totalGB, freePct) for the filesystem at path.
|
|
func unixDiskStats(path string) (freeGB, totalGB float64, freePct int) {
|
|
var stat syscall.Statfs_t
|
|
if err := syscall.Statfs(path, &stat); err != nil {
|
|
return 0, 0, -1
|
|
}
|
|
if stat.Blocks == 0 {
|
|
return 0, 0, -1
|
|
}
|
|
bsize := uint64(stat.Bsize)
|
|
total := stat.Blocks * bsize
|
|
free := stat.Bavail * bsize
|
|
|
|
const gb = 1024 * 1024 * 1024
|
|
totalGB = math.Round(float64(total)/gb*100) / 100
|
|
freeGB = math.Round(float64(free)/gb*100) / 100
|
|
freePct = int(float64(free) / float64(total) * 100)
|
|
return
|
|
}
|
|
|
|
// probeCPUTemp reads from the thermal subsystem. Returns 0 when unavailable.
|
|
func probeCPUTemp() int {
|
|
// Try coretemp / k10temp style entries first (millidegrees)
|
|
for _, zone := range []string{
|
|
"/sys/class/thermal/thermal_zone0/temp",
|
|
"/sys/class/thermal/thermal_zone1/temp",
|
|
"/sys/class/hwmon/hwmon0/temp1_input",
|
|
"/sys/class/hwmon/hwmon1/temp1_input",
|
|
} {
|
|
if n := readSysInt(zone); n > 0 {
|
|
// values > 1000 are in millidegrees
|
|
if n > 1000 {
|
|
return n / 1000
|
|
}
|
|
return n
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// readSysInt reads a single integer from a sysfs/procfs file.
|
|
func readSysInt(path string) int {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
n, err := strconv.Atoi(strings.TrimSpace(string(b)))
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|