mirror of
https://github.com/henrygd/beszel.git
synced 2026-08-19 08:47:46 +02:00
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.
This commit is contained in:
@@ -50,6 +50,9 @@ func generateFingerprint(hostname, cpuModel string) string {
|
|||||||
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
||||||
cpuModel = info[0].ModelName
|
cpuModel = info[0].ModelName
|
||||||
}
|
}
|
||||||
|
if cpuModel == "" {
|
||||||
|
cpuModel = getCpuModelFromCpuinfo()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fingerprint = hostname + cpuModel
|
fingerprint = hostname + cpuModel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -78,6 +79,12 @@ func (a *Agent) refreshSystemDetails() {
|
|||||||
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
||||||
a.systemDetails.CpuModel = info[0].ModelName
|
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 / threads
|
||||||
cores, _ := cpu.Counts(false)
|
cores, _ := cpu.Counts(false)
|
||||||
threads := hostInfo.NCPU
|
threads := hostInfo.NCPU
|
||||||
@@ -265,6 +272,59 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
|||||||
return systemStats
|
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
|
// calculateHostMemoryUsage derives counters defensively because /proc/meminfo may
|
||||||
// change while gopsutil reads it. Invalid unsigned subtractions saturate at zero.
|
// change while gopsutil reads it. Invalid unsigned subtractions saturate at zero.
|
||||||
func calculateHostMemoryUsage(v *mem.VirtualMemoryStat, htop bool) (used, cacheBuff, swapUsed uint64) {
|
func calculateHostMemoryUsage(v *mem.VirtualMemoryStat, htop bool) (used, cacheBuff, swapUsed uint64) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/internal/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
@@ -113,3 +114,81 @@ func TestUpdateSystemDetailsMarksDetailsDirty(t *testing.T) {
|
|||||||
assert.False(t, agent.detailsDirty)
|
assert.False(t, agent.detailsDirty)
|
||||||
assert.Nil(t, original.Details)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user