mirror of
https://github.com/henrygd/beszel.git
synced 2026-08-19 16:57:47 +02: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
|
package agent
|
||||||
|
|
||||||
|
|||||||
14
agent/sensors_freebsd.go
Normal file
14
agent/sensors_freebsd.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
81
agent/sensors_freebsd_common.go
Normal file
81
agent/sensors_freebsd_common.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
167
agent/sensors_freebsd_common_test.go
Normal file
167
agent/sensors_freebsd_common_test.go
Normal file
@@ -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