From fbff7d22ddc6f2188d76c498a688b994dfd8135e Mon Sep 17 00:00:00 2001 From: foxpy Date: Sun, 13 Sep 2026 19:04:49 +0300 Subject: [PATCH 1/4] add basic hwmon fan speed meter --- FanMeter.c | 44 ++++++++++++++++++++++++++ FanMeter.h | 15 +++++++++ Makefile.am | 2 ++ linux/Platform.c | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ linux/Platform.h | 4 +++ 5 files changed, 145 insertions(+) create mode 100644 FanMeter.c create mode 100644 FanMeter.h diff --git a/FanMeter.c b/FanMeter.c new file mode 100644 index 0000000000..2ae84718dd --- /dev/null +++ b/FanMeter.c @@ -0,0 +1,44 @@ +/* +htop - FanMeter.c +(C) 2026 Murad Karammaev +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "config.h" // IWYU pragma: keep + +#include "FanMeter.h" + +#include "CRT.h" +#include "Platform.h" +#include "XUtils.h" + + +static const int FanMeter_attributes[] = { + METER_VALUE +}; + +static void FanMeter_updateValues(Meter* this) { + int fanspeed = Platform_getFanSpeed(); + if (fanspeed < 0) { + xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "N/A"); + } else { + xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "%d rpm", fanspeed); + } +} + +const MeterClass FanMeter_class = { + .super = { + .extends = Class(Meter), + .delete = Meter_delete + }, + .updateValues = FanMeter_updateValues, + .defaultMode = TEXT_METERMODE, + .supportedModes = (1 << TEXT_METERMODE) | (1 << LED_METERMODE), + .maxItems = 0, + .total = 0.0, + .attributes = FanMeter_attributes, + .name = "Fan", + .uiName = "Fan speed", + .caption = "Fan speed: ", +}; diff --git a/FanMeter.h b/FanMeter.h new file mode 100644 index 0000000000..603072524d --- /dev/null +++ b/FanMeter.h @@ -0,0 +1,15 @@ +#ifndef HEADER_FanMeter +#define HEADER_FanMeter +/* +htop - FanMeter.h +(C) 2026 Murad Karammaev +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "Meter.h" + + +extern const MeterClass FanMeter_class; + +#endif diff --git a/Makefile.am b/Makefile.am index 115a07e891..4ba68d8ef0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -117,6 +117,7 @@ myhtopheaders = \ FileDescriptorMeter.h \ FunctionBar.h \ GPUMeter.h \ + FanMeter.h \ Hashtable.h \ Header.h \ HeaderLayout.h \ @@ -214,6 +215,7 @@ linux_platform_headers = \ linux_platform_sources = \ GPUMeter.c \ + FanMeter.c \ generic/gettime.c \ generic/hostname.c \ generic/uname.c \ diff --git a/linux/Platform.c b/linux/Platform.c index dd4121e8e6..33acac3940 100644 --- a/linux/Platform.c +++ b/linux/Platform.c @@ -29,6 +29,7 @@ in the source distribution for its full text. #include "DiskIOMeter.h" #include "FileDescriptorMeter.h" #include "GPUMeter.h" +#include "FanMeter.h" #include "HostnameMeter.h" #include "HugePageMeter.h" #include "LoadAverageMeter.h" @@ -280,6 +281,7 @@ const MeterClass* const Platform_meterTypes[] = { &OpenRCUserMeter_class, &FileDescriptorMeter_class, &GPUMeter_class, + &FanMeter_class, NULL }; @@ -441,6 +443,84 @@ void Platform_setGPUValues(Meter* this, double* totalUsage, unsigned long long* this->values[residueIndex] = residuePercentage; } +static int Platform_getFanSpeedFromHwmonDevice(int parent_dfd, char const* hwmon_device) { + DIR* dirp; + int dfd; + + dfd = openat(parent_dfd, hwmon_device, O_RDONLY | O_CLOEXEC | O_DIRECTORY); + if (dfd == -1) + return -1; + + dirp = fdopendir(dfd); + if (!dirp) { + close(dfd); + return -1; + } + + struct dirent* dir; + while ((dir = readdir(dirp)) != NULL) { + int fan_num; + if (sscanf(dir->d_name, "fan%d_input", &fan_num) != 1) + continue; + + int fd = openat(dfd, dir->d_name, O_RDONLY | O_CLOEXEC); + if (fd == -1) + continue; + FILE* fp = fdopen(fd, "r"); + if (!fp) { + close(fd); + continue; + } + + int fan_speed; + if (fscanf(fp, "%d", &fan_speed) != 1) { + fclose(fp); + continue; + } + + fclose(fp); + closedir(dirp); + return fan_speed; + } + + closedir(dirp); + return -1; +} + +/* +Traverse hwmon sensors, find first fan, read and return its speed. +Returns -1 on failure. +*/ +int Platform_getFanSpeed(void) { + DIR* dirp; + int dfd; + + dirp = opendir(HWMONDIR); + if (!dirp) + return -1; + + dfd = dirfd(dirp); + if (dfd == -1) { + closedir(dirp); + return -1; + } + + struct dirent* dir; + while ((dir = readdir(dirp)) != NULL) { + if (String_eq(dir->d_name, ".") || String_eq(dir->d_name, "..")) + continue; + + int fan_speed = Platform_getFanSpeedFromHwmonDevice(dfd, dir->d_name); + if (fan_speed >= 0) { + closedir(dirp); + return fan_speed; + } + } + + closedir(dirp); + return -1; +} + void Platform_setMemoryValues(Meter* this) { const Machine* host = this->host; const LinuxMachine* lhost = (const LinuxMachine*) host; diff --git a/linux/Platform.h b/linux/Platform.h index d918563429..8c9368703c 100644 --- a/linux/Platform.h +++ b/linux/Platform.h @@ -38,6 +38,8 @@ in the source distribution for its full text. #define PATH_MAX 4096 #endif +#define HWMONDIR "/sys/class/hwmon" + extern const ScreenDefaults Platform_defaultScreens[]; @@ -70,6 +72,8 @@ double Platform_setCPUValues(Meter* this, unsigned int cpu); void Platform_setGPUValues(Meter* this, double* totalUsage, unsigned long long* totalGPUTimeDiff); +int Platform_getFanSpeed(void); + void Platform_setMemoryValues(Meter* this); void Platform_setSwapValues(Meter* this); From 12cc640fc282f073b3bd757663fca92b7315d0ce Mon Sep 17 00:00:00 2001 From: foxpy Date: Sun, 4 Oct 2026 12:17:47 +0300 Subject: [PATCH 2/4] review fixes --- Makefile.am | 4 ++-- linux/Platform.c | 24 +++++++----------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/Makefile.am b/Makefile.am index 4ba68d8ef0..fe4c500e00 100644 --- a/Makefile.am +++ b/Makefile.am @@ -114,10 +114,10 @@ myhtopheaders = \ DynamicMeter.h \ DynamicScreen.h \ EnvScreen.h \ + FanMeter.h \ FileDescriptorMeter.h \ FunctionBar.h \ GPUMeter.h \ - FanMeter.h \ Hashtable.h \ Header.h \ HeaderLayout.h \ @@ -214,8 +214,8 @@ linux_platform_headers = \ zfs/ZfsCompressedArcMeter.h linux_platform_sources = \ - GPUMeter.c \ FanMeter.c \ + GPUMeter.c \ generic/gettime.c \ generic/hostname.c \ generic/uname.c \ diff --git a/linux/Platform.c b/linux/Platform.c index 33acac3940..8282ec69f3 100644 --- a/linux/Platform.c +++ b/linux/Platform.c @@ -443,11 +443,11 @@ void Platform_setGPUValues(Meter* this, double* totalUsage, unsigned long long* this->values[residueIndex] = residuePercentage; } -static int Platform_getFanSpeedFromHwmonDevice(int parent_dfd, char const* hwmon_device) { +static int Platform_getFanSpeedFromHwmonDevice(int parent_dfd, const char *hwmon_device) { DIR* dirp; int dfd; - dfd = openat(parent_dfd, hwmon_device, O_RDONLY | O_CLOEXEC | O_DIRECTORY); + dfd = Compat_openat(parent_dfd, hwmon_device, O_RDONLY | O_CLOEXEC | O_DIRECTORY); if (dfd == -1) return -1; @@ -463,22 +463,15 @@ static int Platform_getFanSpeedFromHwmonDevice(int parent_dfd, char const* hwmon if (sscanf(dir->d_name, "fan%d_input", &fan_num) != 1) continue; - int fd = openat(dfd, dir->d_name, O_RDONLY | O_CLOEXEC); - if (fd == -1) - continue; - FILE* fp = fdopen(fd, "r"); - if (!fp) { - close(fd); + char buffer[32]; + ssize_t ret = Compat_readfileat(dfd, dir->d_name, buffer, sizeof(buffer)); + if (ret <= 0 || (size_t)ret > sizeof(buffer)) continue; - } int fan_speed; - if (fscanf(fp, "%d", &fan_speed) != 1) { - fclose(fp); + if (sscanf(buffer, "%d", &fan_speed) != 1) continue; - } - fclose(fp); closedir(dirp); return fan_speed; } @@ -487,10 +480,7 @@ static int Platform_getFanSpeedFromHwmonDevice(int parent_dfd, char const* hwmon return -1; } -/* -Traverse hwmon sensors, find first fan, read and return its speed. -Returns -1 on failure. -*/ +/* Traverse hwmon sensors, find first fan, read and return its speed. */ int Platform_getFanSpeed(void) { DIR* dirp; int dfd; From d3922af23d99722895aafd3d14f4b12889e19b3b Mon Sep 17 00:00:00 2001 From: foxpy Date: Sun, 18 Oct 2026 16:03:07 +0300 Subject: [PATCH 3/4] use Compat_openat correctly --- linux/Platform.c | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/linux/Platform.c b/linux/Platform.c index 8282ec69f3..0a8ffc702d 100644 --- a/linux/Platform.c +++ b/linux/Platform.c @@ -443,11 +443,11 @@ void Platform_setGPUValues(Meter* this, double* totalUsage, unsigned long long* this->values[residueIndex] = residuePercentage; } -static int Platform_getFanSpeedFromHwmonDevice(int parent_dfd, const char *hwmon_device) { +static int Platform_getFanSpeedFromHwmonDevice(openat_arg_t parent_dfd, const char *hwmon_device) { DIR* dirp; - int dfd; - dfd = Compat_openat(parent_dfd, hwmon_device, O_RDONLY | O_CLOEXEC | O_DIRECTORY); +#ifdef HAVE_OPENAT + openat_arg_t dfd = Compat_openat(parent_dfd, hwmon_device, O_RDONLY | O_CLOEXEC | O_DIRECTORY); if (dfd == -1) return -1; @@ -456,6 +456,15 @@ static int Platform_getFanSpeedFromHwmonDevice(int parent_dfd, const char *hwmon close(dfd); return -1; } +#else + char dfd[4096]; + if (xSnprintf(dfd, sizeof(dfd), "%s/%s", parent_dfd, hwmon_device) < 0) + return -1; + + dirp = opendir(dfd); + if (!dirp) + return -1; +#endif struct dirent* dir; while ((dir = readdir(dirp)) != NULL) { @@ -483,17 +492,21 @@ static int Platform_getFanSpeedFromHwmonDevice(int parent_dfd, const char *hwmon /* Traverse hwmon sensors, find first fan, read and return its speed. */ int Platform_getFanSpeed(void) { DIR* dirp; - int dfd; + openat_arg_t dfd; dirp = opendir(HWMONDIR); if (!dirp) return -1; +#ifdef HAVE_OPENAT dfd = dirfd(dirp); if (dfd == -1) { closedir(dirp); return -1; } +#else + dfd = HWMONDIR; +#endif struct dirent* dir; while ((dir = readdir(dirp)) != NULL) { From 7ad0e9de8e2e7e5639453ca1eda5fe98ecc406a9 Mon Sep 17 00:00:00 2001 From: foxpy Date: Sun, 18 Oct 2026 16:38:15 +0300 Subject: [PATCH 4/4] validate string remainder when matching with sscanf --- linux/Platform.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/linux/Platform.c b/linux/Platform.c index 0a8ffc702d..a3e60c2e0d 100644 --- a/linux/Platform.c +++ b/linux/Platform.c @@ -468,8 +468,9 @@ static int Platform_getFanSpeedFromHwmonDevice(openat_arg_t parent_dfd, const ch struct dirent* dir; while ((dir = readdir(dirp)) != NULL) { - int fan_num; - if (sscanf(dir->d_name, "fan%d_input", &fan_num) != 1) + int fan_num = 0, scan_len = 0; + if (sscanf(dir->d_name, "fan%d%n", &fan_num, &scan_len) != 1 || + !String_eq(&dir->d_name[scan_len], "_input")) continue; char buffer[32];