From 96beadc8c9ed0d0fe1fd32ef56f3e899d34b0ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Dzi=C4=85s=C5=82o?= <184263826+JanDziaslo@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:34:41 +0200 Subject: [PATCH] fix(agent): add fallback for CPU model detection on MIPS architectures (#2138) gopsutil's cpu.Info() does not parse the 'cpu model' field from /proc/cpuinfo, which is the only source of CPU model names on MIPS. Add a fallback that reads /proc/cpuinfo directly and combines 'cpu model' (e.g. 'MIPS 1004Kc V2.15') with 'system type' (e.g. 'MediaTek MT7621 ver:1 eco:3') for a complete identifier. The fallback only triggers when gopsutil returns an empty ModelName, so x86/ARM/other architectures are unaffected. --- agent/fingerprint.go | 3 ++ agent/system.go | 60 +++++++++++++++++++++++++++++++++ agent/system_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) diff --git a/agent/fingerprint.go b/agent/fingerprint.go index 37920e943..567418648 100644 --- a/agent/fingerprint.go +++ b/agent/fingerprint.go @@ -50,6 +50,9 @@ func generateFingerprint(hostname, cpuModel string) string { if info, err := cpu.Info(); err == nil && len(info) > 0 { cpuModel = info[0].ModelName } + if cpuModel == "" { + cpuModel = getCpuModelFromCpuinfo() + } } fingerprint = hostname + cpuModel } diff --git a/agent/system.go b/agent/system.go index d19ffd14f..1649625aa 100644 --- a/agent/system.go +++ b/agent/system.go @@ -4,6 +4,7 @@ import ( "bufio" "errors" "fmt" + "io" "log/slog" "os" "runtime" @@ -78,6 +79,12 @@ func (a *Agent) refreshSystemDetails() { if info, err := cpu.Info(); err == nil && len(info) > 0 { a.systemDetails.CpuModel = info[0].ModelName } + // gopsutil doesn't parse the "cpu model" field from /proc/cpuinfo, which + // is the only source of the CPU model name on MIPS. Fall back to reading + // it directly when ModelName is empty. + if a.systemDetails.CpuModel == "" { + a.systemDetails.CpuModel = getCpuModelFromCpuinfo() + } // cores / threads cores, _ := cpu.Counts(false) threads := hostInfo.NCPU @@ -265,6 +272,59 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats { return systemStats } +// cpuModelFallbackKeys are the field names to look for in /proc/cpuinfo when +// gopsutil fails to return a ModelName. The "cpu model" key is used on MIPS +// (e.g. "MIPS 1004Kc V2.15"), while "system type" provides SoC information +// on various embedded architectures. +var cpuModelFallbackKeys = []string{"cpu model", "system type"} + +// getCpuModelFromCpuinfo reads /proc/cpuinfo and returns a CPU model string. +// This is a fallback for architectures where gopsutil's cpu.Info() does not +// populate ModelName, most notably MIPS. +func getCpuModelFromCpuinfo() string { + file, err := os.Open("/proc/cpuinfo") + if err != nil { + return "" + } + defer file.Close() + return parseCpuModel(file) +} + +// parseCpuModel scans r (expected to be /proc/cpuinfo content) and returns +// a combined CPU model string. It collects values from all matching keys +// and joins them with " / " when multiple are found. +func parseCpuModel(r io.Reader) string { + lines := readLines(r) + var parts []string + for _, key := range cpuModelFallbackKeys { + for _, line := range lines { + after, found := strings.CutPrefix(line, key) + if !found { + continue + } + after = strings.TrimSpace(after) + if len(after) < 2 || after[0] != ':' { + continue + } + if value := strings.TrimSpace(after[1:]); value != "" { + parts = append(parts, value) + break + } + } + } + return strings.Join(parts, " / ") +} + +// readLines reads all lines from r into a slice. +func readLines(r io.Reader) []string { + scanner := bufio.NewScanner(r) + var lines []string + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + return lines +} + // calculateHostMemoryUsage derives counters defensively because /proc/meminfo may // change while gopsutil reads it. Invalid unsigned subtractions saturate at zero. func calculateHostMemoryUsage(v *mem.VirtualMemoryStat, htop bool) (used, cacheBuff, swapUsed uint64) { diff --git a/agent/system_test.go b/agent/system_test.go index 98c21bac9..9500bd383 100644 --- a/agent/system_test.go +++ b/agent/system_test.go @@ -1,6 +1,7 @@ package agent import ( + "strings" "testing" "github.com/henrygd/beszel/internal/common" @@ -113,3 +114,81 @@ func TestUpdateSystemDetailsMarksDetailsDirty(t *testing.T) { assert.False(t, agent.detailsDirty) assert.Nil(t, original.Details) } + +func TestParseCpuModel(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "MIPS with both cpu model and system type", + input: `system type : MediaTek MT7621 ver:1 eco:3 +machine : ASUS RT-AX53U +processor : 0 +cpu model : MIPS 1004Kc V2.15 +BogoMIPS : 586.13 +wait instruction : yes`, + expected: "MIPS 1004Kc V2.15 / MediaTek MT7621 ver:1 eco:3", + }, + { + name: "MIPS with different SoC", + input: `system type : Atheros AR7161 rev 2 +machine : NETGEAR WNDR3700 +processor : 0 +cpu model : MIPS 24Kc V7.4 +BogoMIPS : 452.19`, + expected: "MIPS 24Kc V7.4 / Atheros AR7161 rev 2", + }, + { + name: "only system type when cpu model missing", + input: `system type : Broadcom BCM47xx +processor : 0 +BogoMIPS : 296.11`, + expected: "Broadcom BCM47xx", + }, + { + name: "only cpu model when system type missing", + input: `processor : 0 +cpu model : MIPS 34Kc V2.15 +BogoMIPS : 300.00`, + expected: "MIPS 34Kc V2.15", + }, + { + name: "x86 cpuinfo returns empty", + input: `processor : 0 +vendor_id : GenuineIntel +cpu family : 6 +model : 142 +model name : Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz +stepping : 10`, + expected: "", + }, + { + name: "empty input", + input: "", + expected: "", + }, + { + name: "cpu model with extra whitespace", + input: `processor : 0 +cpu model : MIPS 34Kc V2.15 +BogoMIPS : 300.00`, + expected: "MIPS 34Kc V2.15", + }, + { + name: "cpu model without value", + input: `processor : 0 +cpu model : +BogoMIPS : 300.00`, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parseCpuModel(strings.NewReader(tt.input)) + assert.Equal(t, tt.expected, result) + }) + } +}