From 25bd9ca88bb69a524c710d0c768c41494bdb4711 Mon Sep 17 00:00:00 2001 From: shirou Date: Sat, 17 Feb 2024 11:31:22 +0000 Subject: [PATCH] [sensors] move Temperatures from host to sensors. --- host/host.go | 16 ---- host/host_darwin_cgo.go | 47 ----------- host/host_darwin_nocgo.go | 14 ---- host/host_fallback.go | 4 - host/host_freebsd.go | 4 - host/host_linux.go | 148 ---------------------------------- host/host_netbsd.go | 4 - host/host_openbsd.go | 4 - host/host_solaris.go | 41 +--------- host/host_test.go | 13 --- host/host_windows.go | 36 --------- host/smc_darwin.c | 170 ---------------------------------------- host/smc_darwin.h | 33 -------- sensors/sensors.go | 30 +++++++ sensors/sensors_darwin_cgo.go | 47 +++++++++++ sensors/sensors_darwin_nocgo.go | 14 ++++ sensors/sensors_fallback.go | 14 ++++ sensors/sensors_freebsd.go | 14 ++++ sensors/sensors_linux.go | 163 ++++++++++++++++++++++++++++++++++++++ sensors/sensors_netbsd.go | 14 ++++ sensors/sensors_openbsd.go | 14 ++++ sensors/sensors_solaris.go | 49 ++++++++++++ sensors/sensors_test.go | 21 +++++ sensors/sensors_windows.go | 46 +++++++++++ sensors/smc_darwin.c | 170 ++++++++++++++++++++++++++++++++++++++++ sensors/smc_darwin.h | 33 ++++++++ 26 files changed, 631 insertions(+), 532 deletions(-) delete mode 100644 host/host_darwin_cgo.go delete mode 100644 host/host_darwin_nocgo.go delete mode 100644 host/smc_darwin.c delete mode 100644 host/smc_darwin.h create mode 100644 sensors/sensors.go create mode 100644 sensors/sensors_darwin_cgo.go create mode 100644 sensors/sensors_darwin_nocgo.go create mode 100644 sensors/sensors_fallback.go create mode 100644 sensors/sensors_freebsd.go create mode 100644 sensors/sensors_linux.go create mode 100644 sensors/sensors_netbsd.go create mode 100644 sensors/sensors_openbsd.go create mode 100644 sensors/sensors_solaris.go create mode 100644 sensors/sensors_test.go create mode 100644 sensors/sensors_windows.go create mode 100644 sensors/smc_darwin.c create mode 100644 sensors/smc_darwin.h diff --git a/host/host.go b/host/host.go index 1e401a2..b69d2f6 100644 --- a/host/host.go +++ b/host/host.go @@ -41,13 +41,6 @@ type UserStat struct { Started int `json:"started"` } -type TemperatureStat struct { - SensorKey string `json:"sensorKey"` - Temperature float64 `json:"temperature"` - High float64 `json:"sensorHigh"` - Critical float64 `json:"sensorCritical"` -} - func (h InfoStat) String() string { s, _ := json.Marshal(h) return string(s) @@ -58,11 +51,6 @@ func (u UserStat) String() string { return string(s) } -func (t TemperatureStat) String() string { - s, _ := json.Marshal(t) - return string(s) -} - var enableBootTimeCache bool // EnableBootTimeCache change cache behavior of BootTime. If true, cache BootTime value. Default is false. @@ -158,10 +146,6 @@ func KernelVersion() (string, error) { return KernelVersionWithContext(context.Background()) } -func SensorsTemperatures() ([]TemperatureStat, error) { - return SensorsTemperaturesWithContext(context.Background()) -} - func timeSince(ts uint64) uint64 { return uint64(time.Now().Unix()) - ts } diff --git a/host/host_darwin_cgo.go b/host/host_darwin_cgo.go deleted file mode 100644 index 93c613e..0000000 --- a/host/host_darwin_cgo.go +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -//go:build darwin && cgo - -package host - -// #cgo LDFLAGS: -framework IOKit -// #include "smc_darwin.h" -import "C" -import "context" - -func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { - temperatureKeys := []string{ - C.AMBIENT_AIR_0, - C.AMBIENT_AIR_1, - C.CPU_0_DIODE, - C.CPU_0_HEATSINK, - C.CPU_0_PROXIMITY, - C.ENCLOSURE_BASE_0, - C.ENCLOSURE_BASE_1, - C.ENCLOSURE_BASE_2, - C.ENCLOSURE_BASE_3, - C.GPU_0_DIODE, - C.GPU_0_HEATSINK, - C.GPU_0_PROXIMITY, - C.HARD_DRIVE_BAY, - C.MEMORY_SLOT_0, - C.MEMORY_SLOTS_PROXIMITY, - C.NORTHBRIDGE, - C.NORTHBRIDGE_DIODE, - C.NORTHBRIDGE_PROXIMITY, - C.THUNDERBOLT_0, - C.THUNDERBOLT_1, - C.WIRELESS_MODULE, - } - var temperatures []TemperatureStat - - C.gopsutil_v3_open_smc() - defer C.gopsutil_v3_close_smc() - - for _, key := range temperatureKeys { - temperatures = append(temperatures, TemperatureStat{ - SensorKey: key, - Temperature: float64(C.gopsutil_v3_get_temperature(C.CString(key))), - }) - } - return temperatures, nil -} diff --git a/host/host_darwin_nocgo.go b/host/host_darwin_nocgo.go deleted file mode 100644 index d73a893..0000000 --- a/host/host_darwin_nocgo.go +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -//go:build darwin && !cgo - -package host - -import ( - "context" - - "github.com/shirou/gopsutil/v4/internal/common" -) - -func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { - return []TemperatureStat{}, common.ErrNotImplementedError -} diff --git a/host/host_fallback.go b/host/host_fallback.go index dcfc4bc..f6d9b79 100644 --- a/host/host_fallback.go +++ b/host/host_fallback.go @@ -41,10 +41,6 @@ func PlatformInformationWithContext(ctx context.Context) (string, string, string return "", "", "", common.ErrNotImplementedError } -func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { - return []TemperatureStat{}, common.ErrNotImplementedError -} - func KernelArch() (string, error) { return "", common.ErrNotImplementedError } diff --git a/host/host_freebsd.go b/host/host_freebsd.go index 884e079..97aa05a 100644 --- a/host/host_freebsd.go +++ b/host/host_freebsd.go @@ -141,10 +141,6 @@ func getUsersFromUtmp(utmpfile string) ([]UserStat, error) { return ret, nil } -func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { - return []TemperatureStat{}, common.ErrNotImplementedError -} - func KernelVersionWithContext(ctx context.Context) (string, error) { _, _, version, err := PlatformInformationWithContext(ctx) return version, err diff --git a/host/host_linux.go b/host/host_linux.go index f8a4aa2..04bda6c 100644 --- a/host/host_linux.go +++ b/host/host_linux.go @@ -10,9 +10,7 @@ import ( "fmt" "io" "os" - "path/filepath" "regexp" - "strconv" "strings" "golang.org/x/sys/unix" @@ -30,8 +28,6 @@ type lsbStruct struct { // from utmp.h const ( user_PROCESS = 7 - - hostTemperatureScale = 1000.0 ) func HostIDWithContext(ctx context.Context) (string, error) { @@ -392,147 +388,3 @@ func getSusePlatform(contents []string) string { func VirtualizationWithContext(ctx context.Context) (string, string, error) { return common.VirtualizationWithContext(ctx) } - -func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { - var err error - - var files []string - - temperatures := make([]TemperatureStat, 0) - - // Only the temp*_input file provides current temperature - // value in millidegree Celsius as reported by the temperature to the device: - // https://www.kernel.org/doc/Documentation/hwmon/sysfs-interface - if files, err = filepath.Glob(common.HostSysWithContext(ctx, "/class/hwmon/hwmon*/temp*_input")); err != nil { - return temperatures, err - } - - if len(files) == 0 { - // CentOS has an intermediate /device directory: - // https://github.com/giampaolo/psutil/issues/971 - if files, err = filepath.Glob(common.HostSysWithContext(ctx, "/class/hwmon/hwmon*/device/temp*_input")); err != nil { - return temperatures, err - } - } - - var warns Warnings - - if len(files) == 0 { // handle distributions without hwmon, like raspbian #391, parse legacy thermal_zone files - files, err = filepath.Glob(common.HostSysWithContext(ctx, "/class/thermal/thermal_zone*/")) - if err != nil { - return temperatures, err - } - for _, file := range files { - // Get the name of the temperature you are reading - name, err := os.ReadFile(filepath.Join(file, "type")) - if err != nil { - warns.Add(err) - continue - } - // Get the temperature reading - current, err := os.ReadFile(filepath.Join(file, "temp")) - if err != nil { - warns.Add(err) - continue - } - temperature, err := strconv.ParseInt(strings.TrimSpace(string(current)), 10, 64) - if err != nil { - warns.Add(err) - continue - } - - temperatures = append(temperatures, TemperatureStat{ - SensorKey: strings.TrimSpace(string(name)), - Temperature: float64(temperature) / 1000.0, - }) - } - return temperatures, warns.Reference() - } - - temperatures = make([]TemperatureStat, 0, len(files)) - - // example directory - // device/ temp1_crit_alarm temp2_crit_alarm temp3_crit_alarm temp4_crit_alarm temp5_crit_alarm temp6_crit_alarm temp7_crit_alarm - // name temp1_input temp2_input temp3_input temp4_input temp5_input temp6_input temp7_input - // power/ temp1_label temp2_label temp3_label temp4_label temp5_label temp6_label temp7_label - // subsystem/ temp1_max temp2_max temp3_max temp4_max temp5_max temp6_max temp7_max - // temp1_crit temp2_crit temp3_crit temp4_crit temp5_crit temp6_crit temp7_crit uevent - for _, file := range files { - var raw []byte - - var temperature float64 - - // Get the base directory location - directory := filepath.Dir(file) - - // Get the base filename prefix like temp1 - basename := strings.Split(filepath.Base(file), "_")[0] - - // Get the base path like /temp1 - basepath := filepath.Join(directory, basename) - - // Get the label of the temperature you are reading - label := "" - - if raw, _ = os.ReadFile(basepath + "_label"); len(raw) != 0 { - // Format the label from "Core 0" to "core_0" - label = strings.Join(strings.Split(strings.TrimSpace(strings.ToLower(string(raw))), " "), "_") - } - - // Get the name of the temperature you are reading - if raw, err = os.ReadFile(filepath.Join(directory, "name")); err != nil { - warns.Add(err) - continue - } - - name := strings.TrimSpace(string(raw)) - - if label != "" { - name = name + "_" + label - } - - // Get the temperature reading - if raw, err = os.ReadFile(file); err != nil { - warns.Add(err) - continue - } - - if temperature, err = strconv.ParseFloat(strings.TrimSpace(string(raw)), 64); err != nil { - warns.Add(err) - continue - } - - // Add discovered temperature sensor to the list - temperatures = append(temperatures, TemperatureStat{ - SensorKey: name, - Temperature: temperature / hostTemperatureScale, - High: optionalValueReadFromFile(basepath+"_max") / hostTemperatureScale, - Critical: optionalValueReadFromFile(basepath+"_crit") / hostTemperatureScale, - }) - } - - return temperatures, warns.Reference() -} - -func optionalValueReadFromFile(filename string) float64 { - var raw []byte - - var err error - - var value float64 - - // Check if file exists - if _, err := os.Stat(filename); os.IsNotExist(err) { - return 0 - } - - if raw, err = os.ReadFile(filename); err != nil { - return 0 - } - - if value, err = strconv.ParseFloat(strings.TrimSpace(string(raw)), 64); err != nil { - return 0 - } - - return value -} diff --git a/host/host_netbsd.go b/host/host_netbsd.go index 5bc338e..f3cddb7 100644 --- a/host/host_netbsd.go +++ b/host/host_netbsd.go @@ -45,10 +45,6 @@ func UsersWithContext(ctx context.Context) ([]UserStat, error) { return ret, common.ErrNotImplementedError } -func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { - return []TemperatureStat{}, common.ErrNotImplementedError -} - func KernelVersionWithContext(ctx context.Context) (string, error) { _, _, version, err := PlatformInformationWithContext(ctx) return version, err diff --git a/host/host_openbsd.go b/host/host_openbsd.go index 47db4ff..f21c5e8 100644 --- a/host/host_openbsd.go +++ b/host/host_openbsd.go @@ -95,10 +95,6 @@ func UsersWithContext(ctx context.Context) ([]UserStat, error) { return ret, nil } -func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { - return []TemperatureStat{}, common.ErrNotImplementedError -} - func KernelVersionWithContext(ctx context.Context) (string, error) { _, _, version, err := PlatformInformationWithContext(ctx) return version, err diff --git a/host/host_solaris.go b/host/host_solaris.go index ae4e710..371cc98 100644 --- a/host/host_solaris.go +++ b/host/host_solaris.go @@ -1,13 +1,13 @@ // SPDX-License-Identifier: BSD-3-Clause +//go:build solaris + package host import ( "bufio" "bytes" "context" - "encoding/csv" "fmt" - "io" "os" "regexp" "strconv" @@ -95,43 +95,6 @@ func UsersWithContext(ctx context.Context) ([]UserStat, error) { return []UserStat{}, common.ErrNotImplementedError } -func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { - var ret []TemperatureStat - - out, err := invoke.CommandWithContext(ctx, "ipmitool", "-c", "sdr", "list") - if err != nil { - return ret, err - } - - r := csv.NewReader(strings.NewReader(string(out))) - // Output may contain errors, e.g. "bmc_send_cmd: Permission denied", don't expect a consistent number of records - r.FieldsPerRecord = -1 - for { - record, err := r.Read() - if err == io.EOF { - break - } - if err != nil { - return ret, err - } - // CPU1 Temp,40,degrees C,ok - if len(record) < 3 || record[1] == "" || record[2] != "degrees C" { - continue - } - v, err := strconv.ParseFloat(record[1], 64) - if err != nil { - return ret, err - } - ts := TemperatureStat{ - SensorKey: strings.TrimSuffix(record[0], " Temp"), - Temperature: v, - } - ret = append(ret, ts) - } - - return ret, nil -} - func VirtualizationWithContext(ctx context.Context) (string, string, error) { return "", "", common.ErrNotImplementedError } diff --git a/host/host_test.go b/host/host_test.go index 5eda938..3e53ca7 100644 --- a/host/host_test.go +++ b/host/host_test.go @@ -137,19 +137,6 @@ func TestHostGuid(t *testing.T) { } } -func TestTemperatureStat_String(t *testing.T) { - v := TemperatureStat{ - SensorKey: "CPU", - Temperature: 1.1, - High: 30.1, - Critical: 0.1, - } - s := `{"sensorKey":"CPU","temperature":1.1,"sensorHigh":30.1,"sensorCritical":0.1}` - if s != fmt.Sprintf("%v", v) { - t.Errorf("TemperatureStat string is invalid, %v", fmt.Sprintf("%v", v)) - } -} - func TestVirtualization(t *testing.T) { wg := sync.WaitGroup{} testCount := 10 diff --git a/host/host_windows.go b/host/host_windows.go index 11783a6..1e02e01 100644 --- a/host/host_windows.go +++ b/host/host_windows.go @@ -6,7 +6,6 @@ package host import ( "context" "fmt" - "math" "strconv" "strings" "sync/atomic" @@ -16,7 +15,6 @@ import ( "github.com/shirou/gopsutil/v4/internal/common" "github.com/shirou/gopsutil/v4/process" - "github.com/yusufpapurcu/wmi" "golang.org/x/sys/windows" ) @@ -57,13 +55,6 @@ type systemInfo struct { wProcessorRevision uint16 } -type msAcpi_ThermalZoneTemperature struct { - Active bool - CriticalTripPoint uint32 - CurrentTemperature uint32 - InstanceName string -} - func HostIDWithContext(ctx context.Context) (string, error) { // there has been reports of issues on 32bit using golang.org/x/sys/windows/registry, see https://github.com/shirou/gopsutil/pull/312#issuecomment-277422612 // for rationale of using windows.RegOpenKeyEx/RegQueryValueEx instead of registry.OpenKey/GetStringValue @@ -232,33 +223,6 @@ func UsersWithContext(ctx context.Context) ([]UserStat, error) { return ret, common.ErrNotImplementedError } -func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { - var ret []TemperatureStat - var dst []msAcpi_ThermalZoneTemperature - q := wmi.CreateQuery(&dst, "") - if err := common.WMIQueryWithContext(ctx, q, &dst, nil, "root/wmi"); err != nil { - return ret, err - } - - for _, v := range dst { - ts := TemperatureStat{ - SensorKey: v.InstanceName, - Temperature: kelvinToCelsius(v.CurrentTemperature, 2), - } - ret = append(ret, ts) - } - - return ret, nil -} - -func kelvinToCelsius(temp uint32, n int) float64 { - // wmi return temperature Kelvin * 10, so need to divide the result by 10, - // and then minus 273.15 to get °Celsius. - t := float64(temp/10) - 273.15 - n10 := math.Pow10(n) - return math.Trunc((t+0.5/n10)*n10) / n10 -} - func VirtualizationWithContext(ctx context.Context) (string, string, error) { return "", "", common.ErrNotImplementedError } diff --git a/host/smc_darwin.c b/host/smc_darwin.c deleted file mode 100644 index f0c3f3a..0000000 --- a/host/smc_darwin.c +++ /dev/null @@ -1,170 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -#include -#include -#include "smc_darwin.h" - -#define IOSERVICE_SMC "AppleSMC" -#define IOSERVICE_MODEL "IOPlatformExpertDevice" - -#define DATA_TYPE_SP78 "sp78" - -typedef enum { - kSMCUserClientOpen = 0, - kSMCUserClientClose = 1, - kSMCHandleYPCEvent = 2, - kSMCReadKey = 5, - kSMCWriteKey = 6, - kSMCGetKeyCount = 7, - kSMCGetKeyFromIndex = 8, - kSMCGetKeyInfo = 9, -} selector_t; - -typedef struct { - unsigned char major; - unsigned char minor; - unsigned char build; - unsigned char reserved; - unsigned short release; -} SMCVersion; - -typedef struct { - uint16_t version; - uint16_t length; - uint32_t cpuPLimit; - uint32_t gpuPLimit; - uint32_t memPLimit; -} SMCPLimitData; - -typedef struct { - IOByteCount data_size; - uint32_t data_type; - uint8_t data_attributes; -} SMCKeyInfoData; - -typedef struct { - uint32_t key; - SMCVersion vers; - SMCPLimitData p_limit_data; - SMCKeyInfoData key_info; - uint8_t result; - uint8_t status; - uint8_t data8; - uint32_t data32; - uint8_t bytes[32]; -} SMCParamStruct; - -typedef enum { - kSMCSuccess = 0, - kSMCError = 1, - kSMCKeyNotFound = 0x84, -} kSMC_t; - -typedef struct { - uint8_t data[32]; - uint32_t data_type; - uint32_t data_size; - kSMC_t kSMC; -} smc_return_t; - -static const int SMC_KEY_SIZE = 4; // number of characters in an SMC key. -static io_connect_t conn; // our connection to the SMC. - -kern_return_t gopsutil_v3_open_smc(void) { - kern_return_t result; - io_service_t service; - - service = IOServiceGetMatchingService(0, IOServiceMatching(IOSERVICE_SMC)); - if (service == 0) { - // Note: IOServiceMatching documents 0 on failure - printf("ERROR: %s NOT FOUND\n", IOSERVICE_SMC); - return kIOReturnError; - } - - result = IOServiceOpen(service, mach_task_self(), 0, &conn); - IOObjectRelease(service); - - return result; -} - -kern_return_t gopsutil_v3_close_smc(void) { return IOServiceClose(conn); } - -static uint32_t to_uint32(char *key) { - uint32_t ans = 0; - uint32_t shift = 24; - - if (strlen(key) != SMC_KEY_SIZE) { - return 0; - } - - for (int i = 0; i < SMC_KEY_SIZE; i++) { - ans += key[i] << shift; - shift -= 8; - } - - return ans; -} - -static kern_return_t call_smc(SMCParamStruct *input, SMCParamStruct *output) { - kern_return_t result; - size_t input_cnt = sizeof(SMCParamStruct); - size_t output_cnt = sizeof(SMCParamStruct); - - result = IOConnectCallStructMethod(conn, kSMCHandleYPCEvent, input, input_cnt, - output, &output_cnt); - - if (result != kIOReturnSuccess) { - result = err_get_code(result); - } - return result; -} - -static kern_return_t read_smc(char *key, smc_return_t *result_smc) { - kern_return_t result; - SMCParamStruct input; - SMCParamStruct output; - - memset(&input, 0, sizeof(SMCParamStruct)); - memset(&output, 0, sizeof(SMCParamStruct)); - memset(result_smc, 0, sizeof(smc_return_t)); - - input.key = to_uint32(key); - input.data8 = kSMCGetKeyInfo; - - result = call_smc(&input, &output); - result_smc->kSMC = output.result; - - if (result != kIOReturnSuccess || output.result != kSMCSuccess) { - return result; - } - - result_smc->data_size = output.key_info.data_size; - result_smc->data_type = output.key_info.data_type; - - input.key_info.data_size = output.key_info.data_size; - input.data8 = kSMCReadKey; - - result = call_smc(&input, &output); - result_smc->kSMC = output.result; - - if (result != kIOReturnSuccess || output.result != kSMCSuccess) { - return result; - } - - memcpy(result_smc->data, output.bytes, sizeof(output.bytes)); - - return result; -} - -double gopsutil_v3_get_temperature(char *key) { - kern_return_t result; - smc_return_t result_smc; - - result = read_smc(key, &result_smc); - - if (!(result == kIOReturnSuccess) && result_smc.data_size == 2 && - result_smc.data_type == to_uint32(DATA_TYPE_SP78)) { - return 0.0; - } - - return (double)result_smc.data[0]; -} diff --git a/host/smc_darwin.h b/host/smc_darwin.h deleted file mode 100644 index 74ae55a..0000000 --- a/host/smc_darwin.h +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -#ifndef __SMC_H__ -#define __SMC_H__ 1 - -#include - -#define AMBIENT_AIR_0 "TA0P" -#define AMBIENT_AIR_1 "TA1P" -#define CPU_0_DIODE "TC0D" -#define CPU_0_HEATSINK "TC0H" -#define CPU_0_PROXIMITY "TC0P" -#define ENCLOSURE_BASE_0 "TB0T" -#define ENCLOSURE_BASE_1 "TB1T" -#define ENCLOSURE_BASE_2 "TB2T" -#define ENCLOSURE_BASE_3 "TB3T" -#define GPU_0_DIODE "TG0D" -#define GPU_0_HEATSINK "TG0H" -#define GPU_0_PROXIMITY "TG0P" -#define HARD_DRIVE_BAY "TH0P" -#define MEMORY_SLOT_0 "TM0S" -#define MEMORY_SLOTS_PROXIMITY "TM0P" -#define NORTHBRIDGE "TN0H" -#define NORTHBRIDGE_DIODE "TN0D" -#define NORTHBRIDGE_PROXIMITY "TN0P" -#define THUNDERBOLT_0 "TI0P" -#define THUNDERBOLT_1 "TI1P" -#define WIRELESS_MODULE "TW0P" - -kern_return_t gopsutil_v3_open_smc(void); -kern_return_t gopsutil_v3_close_smc(void); -double gopsutil_v3_get_temperature(char *); - -#endif // __SMC_H__ diff --git a/sensors/sensors.go b/sensors/sensors.go new file mode 100644 index 0000000..feb3996 --- /dev/null +++ b/sensors/sensors.go @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package sensors + +import ( + "context" + "encoding/json" + + "github.com/shirou/gopsutil/v4/internal/common" +) + +type Warnings = common.Warnings + +var invoke common.Invoker = common.Invoke{} + +type TemperatureStat struct { + SensorKey string `json:"sensorKey"` + Temperature float64 `json:"temperature"` + High float64 `json:"sensorHigh"` + Critical float64 `json:"sensorCritical"` +} + +func (t TemperatureStat) String() string { + s, _ := json.Marshal(t) + return string(s) +} + +func SensorsTemperatures() ([]TemperatureStat, error) { + return TemperaturesWithContext(context.Background()) +} diff --git a/sensors/sensors_darwin_cgo.go b/sensors/sensors_darwin_cgo.go new file mode 100644 index 0000000..6ac656b --- /dev/null +++ b/sensors/sensors_darwin_cgo.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build darwin && cgo + +package sensors + +// #cgo LDFLAGS: -framework IOKit +// #include "smc_darwin.h" +import "C" +import "context" + +func TemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { + temperatureKeys := []string{ + C.AMBIENT_AIR_0, + C.AMBIENT_AIR_1, + C.CPU_0_DIODE, + C.CPU_0_HEATSINK, + C.CPU_0_PROXIMITY, + C.ENCLOSURE_BASE_0, + C.ENCLOSURE_BASE_1, + C.ENCLOSURE_BASE_2, + C.ENCLOSURE_BASE_3, + C.GPU_0_DIODE, + C.GPU_0_HEATSINK, + C.GPU_0_PROXIMITY, + C.HARD_DRIVE_BAY, + C.MEMORY_SLOT_0, + C.MEMORY_SLOTS_PROXIMITY, + C.NORTHBRIDGE, + C.NORTHBRIDGE_DIODE, + C.NORTHBRIDGE_PROXIMITY, + C.THUNDERBOLT_0, + C.THUNDERBOLT_1, + C.WIRELESS_MODULE, + } + var temperatures []TemperatureStat + + C.gopsutil_v3_open_smc() + defer C.gopsutil_v3_close_smc() + + for _, key := range temperatureKeys { + temperatures = append(temperatures, TemperatureStat{ + SensorKey: key, + Temperature: float64(C.gopsutil_v3_get_temperature(C.CString(key))), + }) + } + return temperatures, nil +} diff --git a/sensors/sensors_darwin_nocgo.go b/sensors/sensors_darwin_nocgo.go new file mode 100644 index 0000000..45c8506 --- /dev/null +++ b/sensors/sensors_darwin_nocgo.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build darwin && !cgo + +package sensors + +import ( + "context" + + "github.com/shirou/gopsutil/v4/internal/common" +) + +func TemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { + return []TemperatureStat{}, common.ErrNotImplementedError +} diff --git a/sensors/sensors_fallback.go b/sensors/sensors_fallback.go new file mode 100644 index 0000000..40471d8 --- /dev/null +++ b/sensors/sensors_fallback.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build !darwin && !linux && !freebsd && !openbsd && !netbsd && !solaris && !windows + +package sensors + +import ( + "context" + + "github.com/shirou/gopsutil/v4/internal/common" +) + +func TemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { + return []TemperatureStat{}, common.ErrNotImplementedError +} diff --git a/sensors/sensors_freebsd.go b/sensors/sensors_freebsd.go new file mode 100644 index 0000000..f7bb29c --- /dev/null +++ b/sensors/sensors_freebsd.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build freebsd + +package freebsd + +import ( + "context" + + "github.com/shirou/gopsutil/v4/internal/common" +) + +func TemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { + return []TemperatureStat{}, common.ErrNotImplementedError +} diff --git a/sensors/sensors_linux.go b/sensors/sensors_linux.go new file mode 100644 index 0000000..85548f6 --- /dev/null +++ b/sensors/sensors_linux.go @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build linux + +package sensors + +import ( + "context" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/shirou/gopsutil/v4/internal/common" +) + +// from utmp.h +const ( + hostTemperatureScale = 1000.0 +) + +func TemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { + var err error + + var files []string + + temperatures := make([]TemperatureStat, 0) + + // Only the temp*_input file provides current temperature + // value in millidegree Celsius as reported by the temperature to the device: + // https://www.kernel.org/doc/Documentation/hwmon/sysfs-interface + if files, err = filepath.Glob(common.HostSysWithContext(ctx, "/class/hwmon/hwmon*/temp*_input")); err != nil { + return temperatures, err + } + + if len(files) == 0 { + // CentOS has an intermediate /device directory: + // https://github.com/giampaolo/psutil/issues/971 + if files, err = filepath.Glob(common.HostSysWithContext(ctx, "/class/hwmon/hwmon*/device/temp*_input")); err != nil { + return temperatures, err + } + } + + var warns Warnings + + if len(files) == 0 { // handle distributions without hwmon, like raspbian #391, parse legacy thermal_zone files + files, err = filepath.Glob(common.HostSysWithContext(ctx, "/class/thermal/thermal_zone*/")) + if err != nil { + return temperatures, err + } + for _, file := range files { + // Get the name of the temperature you are reading + name, err := os.ReadFile(filepath.Join(file, "type")) + if err != nil { + warns.Add(err) + continue + } + // Get the temperature reading + current, err := os.ReadFile(filepath.Join(file, "temp")) + if err != nil { + warns.Add(err) + continue + } + temperature, err := strconv.ParseInt(strings.TrimSpace(string(current)), 10, 64) + if err != nil { + warns.Add(err) + continue + } + + temperatures = append(temperatures, TemperatureStat{ + SensorKey: strings.TrimSpace(string(name)), + Temperature: float64(temperature) / 1000.0, + }) + } + return temperatures, warns.Reference() + } + + temperatures = make([]TemperatureStat, 0, len(files)) + + // example directory + // device/ temp1_crit_alarm temp2_crit_alarm temp3_crit_alarm temp4_crit_alarm temp5_crit_alarm temp6_crit_alarm temp7_crit_alarm + // name temp1_input temp2_input temp3_input temp4_input temp5_input temp6_input temp7_input + // power/ temp1_label temp2_label temp3_label temp4_label temp5_label temp6_label temp7_label + // subsystem/ temp1_max temp2_max temp3_max temp4_max temp5_max temp6_max temp7_max + // temp1_crit temp2_crit temp3_crit temp4_crit temp5_crit temp6_crit temp7_crit uevent + for _, file := range files { + var raw []byte + + var temperature float64 + + // Get the base directory location + directory := filepath.Dir(file) + + // Get the base filename prefix like temp1 + basename := strings.Split(filepath.Base(file), "_")[0] + + // Get the base path like /temp1 + basepath := filepath.Join(directory, basename) + + // Get the label of the temperature you are reading + label := "" + + if raw, _ = os.ReadFile(basepath + "_label"); len(raw) != 0 { + // Format the label from "Core 0" to "core_0" + label = strings.Join(strings.Split(strings.TrimSpace(strings.ToLower(string(raw))), " "), "_") + } + + // Get the name of the temperature you are reading + if raw, err = os.ReadFile(filepath.Join(directory, "name")); err != nil { + warns.Add(err) + continue + } + + name := strings.TrimSpace(string(raw)) + + if label != "" { + name = name + "_" + label + } + + // Get the temperature reading + if raw, err = os.ReadFile(file); err != nil { + warns.Add(err) + continue + } + + if temperature, err = strconv.ParseFloat(strings.TrimSpace(string(raw)), 64); err != nil { + warns.Add(err) + continue + } + + // Add discovered temperature sensor to the list + temperatures = append(temperatures, TemperatureStat{ + SensorKey: name, + Temperature: temperature / hostTemperatureScale, + High: optionalValueReadFromFile(basepath+"_max") / hostTemperatureScale, + Critical: optionalValueReadFromFile(basepath+"_crit") / hostTemperatureScale, + }) + } + + return temperatures, warns.Reference() +} + +func optionalValueReadFromFile(filename string) float64 { + var raw []byte + + var err error + + var value float64 + + // Check if file exists + if _, err := os.Stat(filename); os.IsNotExist(err) { + return 0 + } + + if raw, err = os.ReadFile(filename); err != nil { + return 0 + } + + if value, err = strconv.ParseFloat(strings.TrimSpace(string(raw)), 64); err != nil { + return 0 + } + + return value +} diff --git a/sensors/sensors_netbsd.go b/sensors/sensors_netbsd.go new file mode 100644 index 0000000..f2e2f7f --- /dev/null +++ b/sensors/sensors_netbsd.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build netbsd + +package sensors + +import ( + "context" + + "github.com/shirou/gopsutil/v4/internal/common" +) + +func TemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { + return []TemperatureStat{}, common.ErrNotImplementedError +} diff --git a/sensors/sensors_openbsd.go b/sensors/sensors_openbsd.go new file mode 100644 index 0000000..8ae3198 --- /dev/null +++ b/sensors/sensors_openbsd.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build openbsd + +package openbsd + +import ( + "context" + + "github.com/shirou/gopsutil/v4/internal/common" +) + +func TemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { + return []TemperatureStat{}, common.ErrNotImplementedError +} diff --git a/sensors/sensors_solaris.go b/sensors/sensors_solaris.go new file mode 100644 index 0000000..c5d1bce --- /dev/null +++ b/sensors/sensors_solaris.go @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build solaris + +package sensors + +import ( + "context" + "encoding/csv" + "io" + "strconv" + "strings" +) + +func TemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { + var ret []TemperatureStat + + out, err := invoke.CommandWithContext(ctx, "ipmitool", "-c", "sdr", "list") + if err != nil { + return ret, err + } + + r := csv.NewReader(strings.NewReader(string(out))) + // Output may contain errors, e.g. "bmc_send_cmd: Permission denied", don't expect a consistent number of records + r.FieldsPerRecord = -1 + for { + record, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + return ret, err + } + // CPU1 Temp,40,degrees C,ok + if len(record) < 3 || record[1] == "" || record[2] != "degrees C" { + continue + } + v, err := strconv.ParseFloat(record[1], 64) + if err != nil { + return ret, err + } + ts := TemperatureStat{ + SensorKey: strings.TrimSuffix(record[0], " Temp"), + Temperature: v, + } + ret = append(ret, ts) + } + + return ret, nil +} diff --git a/sensors/sensors_test.go b/sensors/sensors_test.go new file mode 100644 index 0000000..454316a --- /dev/null +++ b/sensors/sensors_test.go @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package sensors + +import ( + "fmt" + "testing" +) + +func TestTemperatureStat_String(t *testing.T) { + v := TemperatureStat{ + SensorKey: "CPU", + Temperature: 1.1, + High: 30.1, + Critical: 0.1, + } + s := `{"sensorKey":"CPU","temperature":1.1,"sensorHigh":30.1,"sensorCritical":0.1}` + if s != fmt.Sprintf("%v", v) { + t.Errorf("TemperatureStat string is invalid, %v", fmt.Sprintf("%v", v)) + } +} diff --git a/sensors/sensors_windows.go b/sensors/sensors_windows.go new file mode 100644 index 0000000..32b9ee4 --- /dev/null +++ b/sensors/sensors_windows.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build windows + +package sensors + +import ( + "context" + "math" + + "github.com/shirou/gopsutil/v4/internal/common" + "github.com/yusufpapurcu/wmi" +) + +type msAcpi_ThermalZoneTemperature struct { + Active bool + CriticalTripPoint uint32 + CurrentTemperature uint32 + InstanceName string +} + +func TemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { + var ret []TemperatureStat + var dst []msAcpi_ThermalZoneTemperature + q := wmi.CreateQuery(&dst, "") + if err := common.WMIQueryWithContext(ctx, q, &dst, nil, "root/wmi"); err != nil { + return ret, err + } + + for _, v := range dst { + ts := TemperatureStat{ + SensorKey: v.InstanceName, + Temperature: kelvinToCelsius(v.CurrentTemperature, 2), + } + ret = append(ret, ts) + } + + return ret, nil +} + +func kelvinToCelsius(temp uint32, n int) float64 { + // wmi return temperature Kelvin * 10, so need to divide the result by 10, + // and then minus 273.15 to get °Celsius. + t := float64(temp/10) - 273.15 + n10 := math.Pow10(n) + return math.Trunc((t+0.5/n10)*n10) / n10 +} diff --git a/sensors/smc_darwin.c b/sensors/smc_darwin.c new file mode 100644 index 0000000..f0c3f3a --- /dev/null +++ b/sensors/smc_darwin.c @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include "smc_darwin.h" + +#define IOSERVICE_SMC "AppleSMC" +#define IOSERVICE_MODEL "IOPlatformExpertDevice" + +#define DATA_TYPE_SP78 "sp78" + +typedef enum { + kSMCUserClientOpen = 0, + kSMCUserClientClose = 1, + kSMCHandleYPCEvent = 2, + kSMCReadKey = 5, + kSMCWriteKey = 6, + kSMCGetKeyCount = 7, + kSMCGetKeyFromIndex = 8, + kSMCGetKeyInfo = 9, +} selector_t; + +typedef struct { + unsigned char major; + unsigned char minor; + unsigned char build; + unsigned char reserved; + unsigned short release; +} SMCVersion; + +typedef struct { + uint16_t version; + uint16_t length; + uint32_t cpuPLimit; + uint32_t gpuPLimit; + uint32_t memPLimit; +} SMCPLimitData; + +typedef struct { + IOByteCount data_size; + uint32_t data_type; + uint8_t data_attributes; +} SMCKeyInfoData; + +typedef struct { + uint32_t key; + SMCVersion vers; + SMCPLimitData p_limit_data; + SMCKeyInfoData key_info; + uint8_t result; + uint8_t status; + uint8_t data8; + uint32_t data32; + uint8_t bytes[32]; +} SMCParamStruct; + +typedef enum { + kSMCSuccess = 0, + kSMCError = 1, + kSMCKeyNotFound = 0x84, +} kSMC_t; + +typedef struct { + uint8_t data[32]; + uint32_t data_type; + uint32_t data_size; + kSMC_t kSMC; +} smc_return_t; + +static const int SMC_KEY_SIZE = 4; // number of characters in an SMC key. +static io_connect_t conn; // our connection to the SMC. + +kern_return_t gopsutil_v3_open_smc(void) { + kern_return_t result; + io_service_t service; + + service = IOServiceGetMatchingService(0, IOServiceMatching(IOSERVICE_SMC)); + if (service == 0) { + // Note: IOServiceMatching documents 0 on failure + printf("ERROR: %s NOT FOUND\n", IOSERVICE_SMC); + return kIOReturnError; + } + + result = IOServiceOpen(service, mach_task_self(), 0, &conn); + IOObjectRelease(service); + + return result; +} + +kern_return_t gopsutil_v3_close_smc(void) { return IOServiceClose(conn); } + +static uint32_t to_uint32(char *key) { + uint32_t ans = 0; + uint32_t shift = 24; + + if (strlen(key) != SMC_KEY_SIZE) { + return 0; + } + + for (int i = 0; i < SMC_KEY_SIZE; i++) { + ans += key[i] << shift; + shift -= 8; + } + + return ans; +} + +static kern_return_t call_smc(SMCParamStruct *input, SMCParamStruct *output) { + kern_return_t result; + size_t input_cnt = sizeof(SMCParamStruct); + size_t output_cnt = sizeof(SMCParamStruct); + + result = IOConnectCallStructMethod(conn, kSMCHandleYPCEvent, input, input_cnt, + output, &output_cnt); + + if (result != kIOReturnSuccess) { + result = err_get_code(result); + } + return result; +} + +static kern_return_t read_smc(char *key, smc_return_t *result_smc) { + kern_return_t result; + SMCParamStruct input; + SMCParamStruct output; + + memset(&input, 0, sizeof(SMCParamStruct)); + memset(&output, 0, sizeof(SMCParamStruct)); + memset(result_smc, 0, sizeof(smc_return_t)); + + input.key = to_uint32(key); + input.data8 = kSMCGetKeyInfo; + + result = call_smc(&input, &output); + result_smc->kSMC = output.result; + + if (result != kIOReturnSuccess || output.result != kSMCSuccess) { + return result; + } + + result_smc->data_size = output.key_info.data_size; + result_smc->data_type = output.key_info.data_type; + + input.key_info.data_size = output.key_info.data_size; + input.data8 = kSMCReadKey; + + result = call_smc(&input, &output); + result_smc->kSMC = output.result; + + if (result != kIOReturnSuccess || output.result != kSMCSuccess) { + return result; + } + + memcpy(result_smc->data, output.bytes, sizeof(output.bytes)); + + return result; +} + +double gopsutil_v3_get_temperature(char *key) { + kern_return_t result; + smc_return_t result_smc; + + result = read_smc(key, &result_smc); + + if (!(result == kIOReturnSuccess) && result_smc.data_size == 2 && + result_smc.data_type == to_uint32(DATA_TYPE_SP78)) { + return 0.0; + } + + return (double)result_smc.data[0]; +} diff --git a/sensors/smc_darwin.h b/sensors/smc_darwin.h new file mode 100644 index 0000000..74ae55a --- /dev/null +++ b/sensors/smc_darwin.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +#ifndef __SMC_H__ +#define __SMC_H__ 1 + +#include + +#define AMBIENT_AIR_0 "TA0P" +#define AMBIENT_AIR_1 "TA1P" +#define CPU_0_DIODE "TC0D" +#define CPU_0_HEATSINK "TC0H" +#define CPU_0_PROXIMITY "TC0P" +#define ENCLOSURE_BASE_0 "TB0T" +#define ENCLOSURE_BASE_1 "TB1T" +#define ENCLOSURE_BASE_2 "TB2T" +#define ENCLOSURE_BASE_3 "TB3T" +#define GPU_0_DIODE "TG0D" +#define GPU_0_HEATSINK "TG0H" +#define GPU_0_PROXIMITY "TG0P" +#define HARD_DRIVE_BAY "TH0P" +#define MEMORY_SLOT_0 "TM0S" +#define MEMORY_SLOTS_PROXIMITY "TM0P" +#define NORTHBRIDGE "TN0H" +#define NORTHBRIDGE_DIODE "TN0D" +#define NORTHBRIDGE_PROXIMITY "TN0P" +#define THUNDERBOLT_0 "TI0P" +#define THUNDERBOLT_1 "TI1P" +#define WIRELESS_MODULE "TW0P" + +kern_return_t gopsutil_v3_open_smc(void); +kern_return_t gopsutil_v3_close_smc(void); +double gopsutil_v3_get_temperature(char *); + +#endif // __SMC_H__