From 16495b403b8b39e9fb37217bd39da4470153b589 Mon Sep 17 00:00:00 2001 From: alu Date: Tue, 4 Aug 2026 14:11:30 -0700 Subject: [PATCH] fix to power display --- app/app.go | 12 +++++-- app/cpu/cpu.go | 23 +++++-------- app/cpu/proc/proc.go | 4 +++ app/cpu/proc/vendor_amd.go | 67 ++++++++++++++++++++++---------------- go.mod | 12 ------- 5 files changed, 62 insertions(+), 56 deletions(-) diff --git a/app/app.go b/app/app.go index 61c0413..84ae9bc 100644 --- a/app/app.go +++ b/app/app.go @@ -2,6 +2,7 @@ package app import ( "finally-a-monolithic-linux-hw-monitor/app/cpu" + "finally-a-monolithic-linux-hw-monitor/app/cpu/proc" "fmt" "os" "os/signal" @@ -24,13 +25,20 @@ func Run() { os.Exit(0) }() + proc, err := proc.GetProc() + ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { console := &strings.Builder{} - cpu.RenderCPU(console) - console.WriteString("\nPress Ctrl+C to exit.\033[K\n") + if err != nil { + return + } + + cpu.RenderCPU(console, &proc) + + fmt.Fprint(console, "\nPress Ctrl+C to exit.\033[K\n") fmt.Print(console.String()) <-ticker.C diff --git a/app/cpu/cpu.go b/app/cpu/cpu.go index e08a14a..f0dd741 100644 --- a/app/cpu/cpu.go +++ b/app/cpu/cpu.go @@ -6,36 +6,31 @@ import ( "strings" ) -func RenderCPU(console *strings.Builder) { - proc, err := proc.GetProc() - if err != nil { - return - } - +func RenderCPU(console *strings.Builder, proc *proc.Proc) { // Move cursor to top-left (0,0) without clearing full buffer (flicker-free) - console.WriteString("\033[H") - console.WriteString(fmt.Sprintf("=== %s ===\n", proc.Model())) + fmt.Fprint(console, "\033[H") + fmt.Fprintf(console, "=== %s ===\n", proc.Model()) RenderTemperatureSensors(console, proc) RenderPowerSensors(console, proc) } -func RenderTemperatureSensors(console *strings.Builder, proc proc.Proc) { +func RenderTemperatureSensors(console *strings.Builder, proc *proc.Proc) { temps, _ := proc.SensorReader.ReadTemperatureSensors() - console.WriteString("=== Temperature ===\n") + fmt.Fprint(console, "--- Temperature ---\n") for _, sensor := range temps { tempC := sensor.TempC tempF := (tempC * 9 / 5) + 32 // \033[K clears from cursor to end of line (prevents lingering chars) - console.WriteString(fmt.Sprintf("Sensor: %-25s | %5.1f°C (%5.1f°F)\033[K\n", sensor.Name, tempC, tempF)) + fmt.Fprintf(console, "Sensor: %-25s | %5.1f°C (%5.1f°F)\033[K\n", sensor.Name, tempC, tempF) } } -func RenderPowerSensors(console *strings.Builder, proc proc.Proc) { - console.WriteString("=== Power ===\n") +func RenderPowerSensors(console *strings.Builder, proc *proc.Proc) { + fmt.Fprint(console, "--- Power ---\n") power, _ := proc.SensorReader.ReadPowerSensors() for _, sensor := range power { // \033[K clears from cursor to end of line (prevents lingering chars) - console.WriteString(fmt.Sprintf("Sensor: %-25s | %5.1fW\033[K\n", sensor.Name, sensor.Watts)) + fmt.Fprintf(console, "Sensor: %-25s | %5.1fW\033[K\n", sensor.Name, sensor.Watts) } } diff --git a/app/cpu/proc/proc.go b/app/cpu/proc/proc.go index 52cca4b..ba288b0 100644 --- a/app/cpu/proc/proc.go +++ b/app/cpu/proc/proc.go @@ -45,6 +45,10 @@ func GetProc() (Proc, error) { proc.Info = map[string]string{} scanner := bufio.NewScanner(procFile) + if scanner.Err() != nil { + return proc, scanner.Err() + } + for scanner.Scan() { line := scanner.Text() line = strings.ReplaceAll(line, "\t", "") diff --git a/app/cpu/proc/vendor_amd.go b/app/cpu/proc/vendor_amd.go index 2f09154..3ceb24e 100644 --- a/app/cpu/proc/vendor_amd.go +++ b/app/cpu/proc/vendor_amd.go @@ -2,6 +2,7 @@ package proc import ( "fmt" + "math" "os" "path/filepath" "strconv" @@ -12,7 +13,7 @@ import ( type AMDReader struct { hwmonDir string driver string - energyState map[string]EnergyState // tracks (path -> energy state) for energy*_input calculations + energyState map[string]EnergyState } type EnergyState struct { @@ -21,10 +22,9 @@ type EnergyState struct { } func NewAMDReader() (*AMDReader, error) { - // Locate AMD k10temp or zenpower hwmon path matches, err := filepath.Glob("/sys/class/hwmon/hwmon*") if err != nil { - return nil, err + return nil, fmt.Errorf("glob hwmon failed: %w", err) } for _, dir := range matches { @@ -35,8 +35,9 @@ func NewAMDReader() (*AMDReader, error) { drv := strings.TrimSpace(string(b)) if drv == "k10temp" || drv == "zenpower" { return &AMDReader{ - hwmonDir: dir, - driver: drv, + hwmonDir: dir, + driver: drv, + energyState: make(map[string]EnergyState), }, nil } } @@ -50,7 +51,7 @@ func (r *AMDReader) ReadTemperatureSensors() ([]TempSensor, error) { return nil, fmt.Errorf("no temperature inputs found in %s", r.hwmonDir) } - temps := []TempSensor{} + temps := make([]TempSensor, 0, len(inputs)) for _, inputPath := range inputs { valBytes, err := os.ReadFile(inputPath) @@ -90,7 +91,7 @@ func (r *AMDReader) ReadPowerSensors() ([]PowerSensor, error) { } func (r *AMDReader) readHwmonPower() []PowerSensor { - var powerSensors []PowerSensor + powerSensors := []PowerSensor{} getLabel := func(inputPath string) string { labelPath := strings.TrimSuffix(inputPath, "_input") + "_label" @@ -127,22 +128,23 @@ func (r *AMDReader) readHwmonPower() []PowerSensor { } func (r *AMDReader) readRaplPower() []PowerSensor { - var powerSensors []PowerSensor + powerSensors := []PowerSensor{} - // Match both top-level and nested RAPL zone directories patterns := []string{ "/sys/class/powercap/intel-rapl*/energy_uj", "/sys/class/powercap/intel-rapl/intel-rapl*/energy_uj", "/sys/class/powercap/intel-rapl/intel-rapl*/*/energy_uj", + "/sys/class/powercap/intel-rapl:*/energy_uj", + "/sys/class/powercap/intel-rapl:*/*/energy_uj", } - var energyInputs []string + energyInputs := []string{} seen := make(map[string]bool) for _, pattern := range patterns { matches, _ := filepath.Glob(pattern) + for _, match := range matches { - // Resolve symlinks to get real sysfs path realPath, err := filepath.EvalSymlinks(match) if err != nil { realPath = match @@ -172,18 +174,18 @@ func (r *AMDReader) readRaplPower() []PowerSensor { func (r *AMDReader) calculateEnergyDelta(inputs []string, getLabel func(string) string, out *[]PowerSensor) { now := time.Now() + if r.energyState == nil { r.energyState = make(map[string]EnergyState) } - for _, inputPath := range inputs { - // Canonicalize path to resolve symlinks and prevent map key duplication - realPath, err := filepath.EvalSymlinks(inputPath) + for _, rawPath := range inputs { + key, err := filepath.EvalSymlinks(rawPath) if err != nil { - realPath = inputPath + key = filepath.Clean(rawPath) } - valBytes, err := os.ReadFile(realPath) + valBytes, err := os.ReadFile(key) if err != nil { continue } @@ -193,26 +195,29 @@ func (r *AMDReader) calculateEnergyDelta(inputs []string, getLabel func(string) continue } - prevState, exists := r.energyState[realPath] - r.energyState[realPath] = EnergyState{ - lastEnergy: microJoules, - lastTime: now, - } + prevState, exists := r.energyState[key] - // Initial seed step: power delta requires two distinct reads if !exists { + r.energyState[key] = EnergyState{ + lastEnergy: microJoules, + lastTime: now, + } + *out = append(*out, PowerSensor{ + Name: getLabel(key), + Watts: 0.0, + }) continue } timeDiff := now.Sub(prevState.lastTime).Seconds() - if timeDiff <= 0 { + if timeDiff < 0.05 { continue } - maxRange := uint64(1<<64 - 1) - rangeBytes, err := os.ReadFile(filepath.Join(filepath.Dir(realPath), "max_energy_range_uj")) + maxRange := uint64(math.MaxUint64) + rangeBytes, err := os.ReadFile(filepath.Join(filepath.Dir(key), "max_energy_range_uj")) if err == nil { - if parsedMax, err := strconv.ParseUint(strings.TrimSpace(string(rangeBytes)), 10, 64); err == nil { + if parsedMax, err := strconv.ParseUint(strings.TrimSpace(string(rangeBytes)), 10, 64); err == nil && parsedMax > 0 { maxRange = parsedMax } } @@ -221,12 +226,18 @@ func (r *AMDReader) calculateEnergyDelta(inputs []string, getLabel func(string) if microJoules >= prevState.lastEnergy { energyDiff = microJoules - prevState.lastEnergy } else { - energyDiff = maxRange - prevState.lastEnergy + microJoules + energyDiff = (maxRange - prevState.lastEnergy) + microJoules } watts := (float64(energyDiff) / 1_000_000.0) / timeDiff + + r.energyState[key] = EnergyState{ + lastEnergy: microJoules, + lastTime: now, + } + *out = append(*out, PowerSensor{ - Name: getLabel(realPath), + Name: getLabel(key), Watts: watts, }) } diff --git a/go.mod b/go.mod index 04dd175..122bd61 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,3 @@ module finally-a-monolithic-linux-hw-monitor go 1.26.5 - -require ( - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/shirou/gopsutil/v3 v3.24.5 // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/sys v0.20.0 // indirect -)