mirror of
https://github.com/henrygd/beszel.git
synced 2026-08-17 04:36:17 +00:00
feat: Add support for reading CPU (dev.cpu.*.temperature) and ACPI thermal zone (hw.acpi.thermal.tz*) temperature sensors on FreeBSD systems. (#2227)
Co-authored-by: roib <roib@elsec.us>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//go:build !windows
|
||||
//go:build !windows && !freebsd
|
||||
|
||||
package agent
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//go:build freebsd
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var getSensorTemps = func(ctx context.Context) ([]sensors.TemperatureStat, error) {
|
||||
return getFreeBSDSensorTemps(ctx, unix.SysctlUint32)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//go:build freebsd || testing
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
)
|
||||
|
||||
const (
|
||||
freebsdZeroCelsiusDeciKelvin = 2731
|
||||
freebsdAcpiThermalZoneCount = 16
|
||||
)
|
||||
|
||||
type freebsdSysctlUintReader func(name string) (uint32, error)
|
||||
|
||||
func getFreeBSDSensorTemps(ctx context.Context, readSysctl freebsdSysctlUintReader) ([]sensors.TemperatureStat, error) {
|
||||
cpuCount, err := readSysctl("hw.ncpu")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
temps := make([]sensors.TemperatureStat, 0, int(cpuCount)+freebsdAcpiThermalZoneCount)
|
||||
for cpu := uint32(0); cpu < cpuCount; cpu++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return temps, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
sensorName := fmt.Sprintf("dev.cpu.%d.temperature", cpu)
|
||||
value, err := readSysctl(sensorName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
temp, ok := freebsdDeciKelvinToCelsius(value)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
temps = append(temps, sensors.TemperatureStat{
|
||||
SensorKey: sensorName,
|
||||
Temperature: temp,
|
||||
})
|
||||
}
|
||||
|
||||
for zone := 0; zone < freebsdAcpiThermalZoneCount; zone++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return temps, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
sensorName := fmt.Sprintf("hw.acpi.thermal.tz%d.temperature", zone)
|
||||
value, err := readSysctl(sensorName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
temp, ok := freebsdDeciKelvinToCelsius(value)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
temps = append(temps, sensors.TemperatureStat{
|
||||
SensorKey: sensorName,
|
||||
Temperature: temp,
|
||||
})
|
||||
}
|
||||
|
||||
return temps, nil
|
||||
}
|
||||
|
||||
func freebsdDeciKelvinToCelsius(value uint32) (float64, bool) {
|
||||
if value <= freebsdZeroCelsiusDeciKelvin {
|
||||
return 0, false
|
||||
}
|
||||
temp := float64(int64(value)-freebsdZeroCelsiusDeciKelvin) / 10
|
||||
if temp <= 0 || temp >= 200 {
|
||||
return 0, false
|
||||
}
|
||||
return temp, true
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//go:build testing
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var errFakeFreeBSDSysctlNotFound = errors.New("sysctl not found")
|
||||
|
||||
type fakeFreeBSDSysctls struct {
|
||||
values map[string]uint32
|
||||
errs map[string]error
|
||||
}
|
||||
|
||||
func (f fakeFreeBSDSysctls) read(name string) (uint32, error) {
|
||||
if err, ok := f.errs[name]; ok {
|
||||
return 0, err
|
||||
}
|
||||
if value, ok := f.values[name]; ok {
|
||||
return value, nil
|
||||
}
|
||||
return 0, errFakeFreeBSDSysctlNotFound
|
||||
}
|
||||
|
||||
func TestFreeBSDDeciKelvinToCelsius(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value uint32
|
||||
expected float64
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
name: "45 Celsius",
|
||||
value: 3181,
|
||||
expected: 45,
|
||||
ok: true,
|
||||
},
|
||||
{
|
||||
name: "fractional Celsius",
|
||||
value: 3186,
|
||||
expected: 45.5,
|
||||
ok: true,
|
||||
},
|
||||
{
|
||||
name: "zero deci-Kelvin",
|
||||
value: 0,
|
||||
ok: false,
|
||||
},
|
||||
{
|
||||
name: "zero Celsius",
|
||||
value: freebsdZeroCelsiusDeciKelvin,
|
||||
ok: false,
|
||||
},
|
||||
{
|
||||
name: "below zero Celsius",
|
||||
value: freebsdZeroCelsiusDeciKelvin - 1,
|
||||
ok: false,
|
||||
},
|
||||
{
|
||||
name: "invalid signed integer",
|
||||
value: 1<<32 - 1,
|
||||
ok: false,
|
||||
},
|
||||
{
|
||||
name: "unreasonably high Celsius",
|
||||
value: freebsdZeroCelsiusDeciKelvin + 2000,
|
||||
ok: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, ok := freebsdDeciKelvinToCelsius(tt.value)
|
||||
assert.Equal(t, tt.ok, ok)
|
||||
assert.InDelta(t, tt.expected, result, 0.001)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFreeBSDSensorTemps(t *testing.T) {
|
||||
reader := fakeFreeBSDSysctls{
|
||||
values: map[string]uint32{
|
||||
"hw.ncpu": 4,
|
||||
"dev.cpu.0.temperature": 3231,
|
||||
"dev.cpu.1.temperature": 3242,
|
||||
"dev.cpu.3.temperature": freebsdZeroCelsiusDeciKelvin,
|
||||
"hw.acpi.thermal.tz0.temperature": 3101,
|
||||
"hw.acpi.thermal.tz2.temperature": 3116,
|
||||
"hw.acpi.thermal.tz3.temperature": freebsdZeroCelsiusDeciKelvin,
|
||||
"unrelated.sensor.value": 9999,
|
||||
"dev.cpu.99.temperature": 9999,
|
||||
"dev.amdtemp.0.core0.foo": 9999,
|
||||
},
|
||||
}
|
||||
|
||||
temps, err := getFreeBSDSensorTemps(context.Background(), reader.read)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, temps, 4)
|
||||
assert.Equal(t, "dev.cpu.0.temperature", temps[0].SensorKey)
|
||||
assert.InDelta(t, 50.0, temps[0].Temperature, 0.001)
|
||||
assert.Equal(t, "dev.cpu.1.temperature", temps[1].SensorKey)
|
||||
assert.InDelta(t, 51.1, temps[1].Temperature, 0.001)
|
||||
assert.Equal(t, "hw.acpi.thermal.tz0.temperature", temps[2].SensorKey)
|
||||
assert.InDelta(t, 37.0, temps[2].Temperature, 0.001)
|
||||
assert.Equal(t, "hw.acpi.thermal.tz2.temperature", temps[3].SensorKey)
|
||||
assert.InDelta(t, 38.5, temps[3].Temperature, 0.001)
|
||||
}
|
||||
|
||||
func TestGetFreeBSDSensorTempsCpuCountError(t *testing.T) {
|
||||
reader := fakeFreeBSDSysctls{
|
||||
errs: map[string]error{
|
||||
"hw.ncpu": errors.New("permission denied"),
|
||||
},
|
||||
}
|
||||
|
||||
temps, err := getFreeBSDSensorTemps(context.Background(), reader.read)
|
||||
|
||||
assert.Nil(t, temps)
|
||||
assert.EqualError(t, err, "permission denied")
|
||||
}
|
||||
|
||||
func TestGetFreeBSDSensorTempsNoTemperatureSysctls(t *testing.T) {
|
||||
reader := fakeFreeBSDSysctls{
|
||||
values: map[string]uint32{"hw.ncpu": 2},
|
||||
}
|
||||
|
||||
temps, err := getFreeBSDSensorTemps(context.Background(), reader.read)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, temps)
|
||||
}
|
||||
|
||||
func TestGetFreeBSDSensorTempsAcpiOnly(t *testing.T) {
|
||||
reader := fakeFreeBSDSysctls{
|
||||
values: map[string]uint32{
|
||||
"hw.ncpu": 0,
|
||||
"hw.acpi.thermal.tz0.temperature": 3081,
|
||||
},
|
||||
}
|
||||
|
||||
temps, err := getFreeBSDSensorTemps(context.Background(), reader.read)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, temps, 1)
|
||||
assert.Equal(t, "hw.acpi.thermal.tz0.temperature", temps[0].SensorKey)
|
||||
assert.InDelta(t, 35.0, temps[0].Temperature, 0.001)
|
||||
}
|
||||
|
||||
func TestGetFreeBSDSensorTempsContextCancelled(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
reader := fakeFreeBSDSysctls{
|
||||
values: map[string]uint32{"hw.ncpu": 2},
|
||||
}
|
||||
|
||||
temps, err := getFreeBSDSensorTemps(ctx, reader.read)
|
||||
|
||||
assert.Empty(t, temps)
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
Reference in New Issue
Block a user