//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 }