Files
op-packages/fancontrol/src/fancontrol.c
T

792 lines
20 KiB
C

#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <glob.h>
#include <limits.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>
#define PROGRAM_NAME "fancontrol"
#define PROGRAM_VERSION "2.0.0"
#define AUTO_VALUE "auto"
#define DEFAULT_SYSFS_ROOT "/sys"
#define DEFAULT_STATUS_FILE "/var/run/fancontrol.status"
#define MAX_HWMON_CHANNELS 32
struct fan_config {
char thermal_file[PATH_MAX];
char thermal_zone[128];
char fan_file[PATH_MAX];
char fan_hwmon[128];
char enable_file[PATH_MAX];
int start_temp;
int full_speed_temp;
int hysteresis;
int start_pwm;
int max_pwm;
int kick_pwm;
int kick_ms;
int fail_safe_pwm;
int exit_pwm;
int temp_div;
int interval;
bool debug;
bool once;
};
struct fan_runtime {
char thermal_file[PATH_MAX];
char fan_file[PATH_MAX];
char enable_file[PATH_MAX];
char rpm_file[PATH_MAX];
bool fan_active;
};
static const char *sysfs_root = DEFAULT_SYSFS_ROOT;
static const char *status_file = DEFAULT_STATUS_FILE;
static volatile sig_atomic_t stop_requested;
static void usage(FILE *stream, const char *name)
{
fprintf(stream,
"Usage: %s [options]\n"
" -T path temperature sysfs path or 'auto'\n"
" -Z type preferred thermal zone type or 'auto'\n"
" -F path PWM sysfs path or 'auto'\n"
" -N name preferred hwmon name or 'auto'\n"
" -E path pwm enable sysfs path or 'auto'\n"
" -t degrees fan start temperature\n"
" -x degrees full-speed temperature\n"
" -H degrees stop hysteresis\n"
" -s pwm minimum running PWM (1-255)\n"
" -m pwm maximum PWM (1-255)\n"
" -k pwm start kick PWM, 0 disables the kick\n"
" -K ms start kick duration in milliseconds\n"
" -f pwm PWM used after a sensor failure\n"
" -q pwm PWM used when the daemon exits\n"
" -d divisor temperature input divisor\n"
" -i seconds polling interval\n"
" -D enable debug logging\n"
" -1 run one control cycle and exit\n"
" -v print version\n"
" -h show this help\n",
name);
}
static int copy_string(char *dest, size_t size, const char *source)
{
int len;
if (!source || strpbrk(source, "\r\n"))
return -1;
len = snprintf(dest, size, "%s", source);
return len < 0 || (size_t)len >= size ? -1 : 0;
}
static bool is_auto(const char *value)
{
return !value[0] || strcmp(value, AUTO_VALUE) == 0;
}
static int parse_int(const char *text, int min, int max, const char *name)
{
char *end = NULL;
long value;
errno = 0;
value = strtol(text, &end, 10);
if (errno || !end || *end || value < min || value > max) {
fprintf(stderr, "Invalid %s: %s\n", name, text);
exit(EXIT_FAILURE);
}
return (int)value;
}
static int read_text_file(const char *path, char *buffer, size_t size)
{
ssize_t length;
int fd;
if (!size)
return -1;
fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0)
return -1;
length = read(fd, buffer, size - 1);
close(fd);
if (length <= 0)
return -1;
buffer[length] = '\0';
buffer[strcspn(buffer, "\r\n")] = '\0';
return 0;
}
static int read_long_file(const char *path, long *value)
{
char buffer[64];
char *end = NULL;
long parsed;
if (read_text_file(path, buffer, sizeof(buffer)))
return -1;
errno = 0;
parsed = strtol(buffer, &end, 10);
if (errno || end == buffer)
return -1;
while (*end == ' ' || *end == '\t')
end++;
if (*end)
return -1;
*value = parsed;
return 0;
}
static int write_long_file(const char *path, int value)
{
char buffer[32];
ssize_t written;
int length;
int fd;
length = snprintf(buffer, sizeof(buffer), "%d\n", value);
if (length < 0 || (size_t)length >= sizeof(buffer))
return -1;
fd = open(path, O_WRONLY | O_CLOEXEC);
if (fd < 0)
return -1;
written = write(fd, buffer, (size_t)length);
if (close(fd) || written != length)
return -1;
return 0;
}
static void sleep_ms(unsigned int milliseconds)
{
struct timespec remaining = {
.tv_sec = milliseconds / 1000,
.tv_nsec = (long)(milliseconds % 1000) * 1000000L,
};
while (!stop_requested && nanosleep(&remaining, &remaining) && errno == EINTR)
;
}
static int preferred_thermal_score(const char *type)
{
static const char * const preferred[] = {
"cpu-top-thermal", "cpu_top_thermal", "cpu-thermal",
"cpu_thermal", "soc-thermal", "soc_thermal",
};
size_t i;
for (i = 0; i < sizeof(preferred) / sizeof(preferred[0]); i++)
if (strcmp(type, preferred[i]) == 0)
return (int)i;
return 100;
}
static int preferred_hwmon_temp_score(const char *name)
{
if (strstr(name, "cpu") || strstr(name, "CPU"))
return 0;
if (strstr(name, "soc") || strstr(name, "SoC"))
return 1;
return 100;
}
static int discover_hwmon_temperature(char *result, size_t result_size)
{
char pattern[PATH_MAX];
char name_file[PATH_MAX];
char hwmon_name[128];
char candidate[PATH_MAX];
char best[PATH_MAX] = "";
glob_t paths = { 0 };
int best_score = INT_MAX;
size_t i;
if (snprintf(pattern, sizeof(pattern), "%s/class/hwmon/hwmon*", sysfs_root) >=
(int)sizeof(pattern))
return -1;
if (glob(pattern, 0, NULL, &paths))
return -1;
for (i = 0; i < paths.gl_pathc; i++) {
int channel;
int score;
if (snprintf(name_file, sizeof(name_file), "%s/name", paths.gl_pathv[i]) >=
(int)sizeof(name_file) ||
read_text_file(name_file, hwmon_name, sizeof(hwmon_name)))
hwmon_name[0] = '\0';
score = preferred_hwmon_temp_score(hwmon_name);
for (channel = 1; channel <= MAX_HWMON_CHANNELS; channel++) {
if (snprintf(candidate, sizeof(candidate), "%s/temp%d_input",
paths.gl_pathv[i], channel) >= (int)sizeof(candidate))
continue;
if (access(candidate, R_OK))
continue;
if (score < best_score) {
copy_string(best, sizeof(best), candidate);
best_score = score;
}
break;
}
}
globfree(&paths);
if (!best[0])
return -1;
return copy_string(result, result_size, best);
}
static int discover_thermal_file(const struct fan_config *config,
char *result, size_t result_size)
{
char pattern[PATH_MAX];
char type_file[PATH_MAX];
char type[128];
char best[PATH_MAX] = "";
glob_t paths = { 0 };
int best_score = INT_MAX;
size_t i;
if (snprintf(pattern, sizeof(pattern), "%s/class/thermal/thermal_zone*/temp",
sysfs_root) >= (int)sizeof(pattern))
return -1;
if (glob(pattern, 0, NULL, &paths)) {
if (is_auto(config->thermal_zone))
return discover_hwmon_temperature(result, result_size);
return -1;
}
for (i = 0; i < paths.gl_pathc; i++) {
char *slash;
int score;
if (access(paths.gl_pathv[i], R_OK))
continue;
if (copy_string(type_file, sizeof(type_file), paths.gl_pathv[i]))
continue;
slash = strrchr(type_file, '/');
if (!slash)
continue;
copy_string(slash + 1, sizeof(type_file) - (size_t)(slash + 1 - type_file),
"type");
if (read_text_file(type_file, type, sizeof(type)))
type[0] = '\0';
if (!is_auto(config->thermal_zone)) {
if (strcmp(config->thermal_zone, type) != 0)
continue;
score = 0;
} else {
score = preferred_thermal_score(type);
}
if (score < best_score) {
copy_string(best, sizeof(best), paths.gl_pathv[i]);
best_score = score;
}
}
globfree(&paths);
if (!best[0]) {
if (is_auto(config->thermal_zone))
return discover_hwmon_temperature(result, result_size);
return -1;
}
return copy_string(result, result_size, best);
}
static bool valid_pwm_basename(const char *name, int *channel)
{
char trailing;
int parsed;
if (sscanf(name, "pwm%d%c", &parsed, &trailing) != 1 ||
parsed < 1 || parsed > MAX_HWMON_CHANNELS)
return false;
*channel = parsed;
return true;
}
static int derive_fan_paths(const char *pwm_file, struct fan_runtime *runtime)
{
char directory[PATH_MAX];
const char *basename;
char *slash;
int channel;
int length;
if (copy_string(directory, sizeof(directory), pwm_file))
return -1;
slash = strrchr(directory, '/');
if (!slash)
return -1;
basename = slash + 1;
if (!valid_pwm_basename(basename, &channel))
return 0;
*slash = '\0';
if (is_auto(runtime->enable_file)) {
length = snprintf(runtime->enable_file, sizeof(runtime->enable_file),
"%s/pwm%d_enable", directory, channel);
if (length < 0 || (size_t)length >= sizeof(runtime->enable_file))
return -1;
if (access(runtime->enable_file, W_OK))
runtime->enable_file[0] = '\0';
}
length = snprintf(runtime->rpm_file, sizeof(runtime->rpm_file),
"%s/fan%d_input", directory, channel);
if (length < 0 || (size_t)length >= sizeof(runtime->rpm_file))
return -1;
if (access(runtime->rpm_file, R_OK))
runtime->rpm_file[0] = '\0';
return 0;
}
static int discover_fan_file(const struct fan_config *config,
struct fan_runtime *runtime)
{
char pattern[PATH_MAX];
char name_file[PATH_MAX];
char hwmon_name[128];
char candidate[PATH_MAX];
char best[PATH_MAX] = "";
glob_t paths = { 0 };
int best_score = INT_MAX;
size_t i;
if (snprintf(pattern, sizeof(pattern), "%s/class/hwmon/hwmon*", sysfs_root) >=
(int)sizeof(pattern))
return -1;
if (glob(pattern, 0, NULL, &paths))
return -1;
for (i = 0; i < paths.gl_pathc; i++) {
int channel;
int score;
snprintf(name_file, sizeof(name_file), "%s/name", paths.gl_pathv[i]);
if (read_text_file(name_file, hwmon_name, sizeof(hwmon_name)))
continue;
if (!is_auto(config->fan_hwmon)) {
if (strcmp(config->fan_hwmon, hwmon_name) != 0)
continue;
score = 0;
} else {
score = strcmp(hwmon_name, "pwmfan") == 0 ? 0 : 100;
}
for (channel = 1; channel <= MAX_HWMON_CHANNELS; channel++) {
snprintf(candidate, sizeof(candidate), "%s/pwm%d",
paths.gl_pathv[i], channel);
if (access(candidate, R_OK | W_OK))
continue;
if (score < best_score) {
copy_string(best, sizeof(best), candidate);
best_score = score;
}
break;
}
}
globfree(&paths);
if (!best[0])
return -1;
if (copy_string(runtime->fan_file, sizeof(runtime->fan_file), best))
return -1;
return derive_fan_paths(runtime->fan_file, runtime);
}
static bool is_cooling_state_path(const char *path)
{
return strstr(path, "/cooling_device") &&
strcmp(strrchr(path, '/') ? strrchr(path, '/') + 1 : path,
"cur_state") == 0;
}
static int resolve_paths(const struct fan_config *config,
struct fan_runtime *runtime)
{
memset(runtime, 0, sizeof(*runtime));
copy_string(runtime->enable_file, sizeof(runtime->enable_file),
config->enable_file);
if (is_auto(config->thermal_file)) {
if (discover_thermal_file(config, runtime->thermal_file,
sizeof(runtime->thermal_file))) {
syslog(LOG_ERR, "no readable thermal sensor was found");
return -1;
}
} else if (copy_string(runtime->thermal_file,
sizeof(runtime->thermal_file), config->thermal_file) ||
access(runtime->thermal_file, R_OK)) {
syslog(LOG_ERR, "temperature input is not readable: %s",
config->thermal_file);
return -1;
}
if (is_auto(config->fan_file)) {
if (discover_fan_file(config, runtime)) {
syslog(LOG_ERR, "no writable hwmon PWM output was found");
return -1;
}
} else {
if (is_cooling_state_path(config->fan_file)) {
syslog(LOG_ERR,
"%s is a discrete cooling state, not a continuous PWM output",
config->fan_file);
return -1;
}
if (copy_string(runtime->fan_file, sizeof(runtime->fan_file),
config->fan_file) || access(runtime->fan_file, R_OK | W_OK)) {
syslog(LOG_ERR, "PWM output is not readable and writable: %s",
config->fan_file);
return -1;
}
if (derive_fan_paths(runtime->fan_file, runtime)) {
syslog(LOG_ERR, "failed to derive controls for PWM output: %s",
runtime->fan_file);
return -1;
}
}
if (!is_auto(config->enable_file)) {
if (copy_string(runtime->enable_file, sizeof(runtime->enable_file),
config->enable_file) || access(runtime->enable_file, W_OK)) {
syslog(LOG_ERR, "PWM enable control is not writable: %s",
config->enable_file);
return -1;
}
}
return 0;
}
static int calculate_pwm(const struct fan_config *config, int64_t temp_mc,
bool fan_active)
{
int64_t start_mc = (int64_t)config->start_temp * 1000;
int64_t stop_mc = (int64_t)(config->start_temp - config->hysteresis) * 1000;
int64_t full_mc = (int64_t)config->full_speed_temp * 1000;
int64_t pwm;
if ((!fan_active && temp_mc < start_mc) ||
(fan_active && temp_mc <= stop_mc))
return 0;
if (temp_mc <= start_mc)
return config->start_pwm;
if (temp_mc >= full_mc)
return config->max_pwm;
pwm = config->start_pwm +
(temp_mc - start_mc) * (config->max_pwm - config->start_pwm) /
(full_mc - start_mc);
return (int)pwm;
}
static int set_pwm(const struct fan_runtime *runtime, int pwm)
{
if (write_long_file(runtime->fan_file, pwm)) {
syslog(LOG_ERR, "failed to write PWM %d to %s: %s", pwm,
runtime->fan_file, strerror(errno));
return -1;
}
return 0;
}
static void write_status(const struct fan_runtime *runtime, int64_t temp_mc,
int pwm, long rpm, const char *error)
{
char temporary[PATH_MAX];
FILE *stream;
if (snprintf(temporary, sizeof(temporary), "%s.%ld", status_file,
(long)getpid()) >= (int)sizeof(temporary))
return;
stream = fopen(temporary, "w");
if (!stream)
return;
fprintf(stream,
"running=1\n"
"fan_active=%d\n"
"temperature_mc=%lld\n"
"pwm=%d\n"
"rpm=%ld\n"
"thermal_file=%s\n"
"fan_file=%s\n"
"error=%s\n",
runtime->fan_active ? 1 : 0, (long long)temp_mc, pwm, rpm,
runtime->thermal_file, runtime->fan_file, error ? error : "");
if (fclose(stream) || rename(temporary, status_file))
unlink(temporary);
}
static void signal_handler(int signal_number)
{
(void)signal_number;
stop_requested = 1;
}
static int install_signal_handlers(void)
{
struct sigaction action = {
.sa_handler = signal_handler,
};
sigemptyset(&action.sa_mask);
return sigaction(SIGINT, &action, NULL) ||
sigaction(SIGTERM, &action, NULL);
}
static void set_defaults(struct fan_config *config)
{
memset(config, 0, sizeof(*config));
copy_string(config->thermal_file, sizeof(config->thermal_file), AUTO_VALUE);
copy_string(config->thermal_zone, sizeof(config->thermal_zone), AUTO_VALUE);
copy_string(config->fan_file, sizeof(config->fan_file), AUTO_VALUE);
copy_string(config->fan_hwmon, sizeof(config->fan_hwmon), AUTO_VALUE);
copy_string(config->enable_file, sizeof(config->enable_file), AUTO_VALUE);
config->start_temp = 45;
config->full_speed_temp = 85;
config->hysteresis = 3;
config->start_pwm = 64;
config->max_pwm = 255;
config->kick_pwm = 255;
config->kick_ms = 500;
config->fail_safe_pwm = 255;
config->exit_pwm = 255;
config->temp_div = 1000;
config->interval = 5;
}
static void parse_options(int argc, char **argv, struct fan_config *config)
{
int option;
while ((option = getopt(argc, argv, "T:Z:F:N:E:t:x:H:s:m:k:K:f:q:d:i:D1vh")) != -1) {
switch (option) {
case 'T':
if (copy_string(config->thermal_file,
sizeof(config->thermal_file), optarg))
exit(EXIT_FAILURE);
break;
case 'Z':
if (copy_string(config->thermal_zone, sizeof(config->thermal_zone), optarg))
exit(EXIT_FAILURE);
break;
case 'F':
if (copy_string(config->fan_file, sizeof(config->fan_file), optarg))
exit(EXIT_FAILURE);
break;
case 'N':
if (copy_string(config->fan_hwmon, sizeof(config->fan_hwmon), optarg))
exit(EXIT_FAILURE);
break;
case 'E':
if (copy_string(config->enable_file, sizeof(config->enable_file), optarg))
exit(EXIT_FAILURE);
break;
case 't':
config->start_temp = parse_int(optarg, -100, 200, "start temperature");
break;
case 'x':
config->full_speed_temp = parse_int(optarg, -99, 250,
"full-speed temperature");
break;
case 'H':
config->hysteresis = parse_int(optarg, 0, 50, "hysteresis");
break;
case 's':
config->start_pwm = parse_int(optarg, 1, 255, "start PWM");
break;
case 'm':
config->max_pwm = parse_int(optarg, 1, 255, "maximum PWM");
break;
case 'k':
config->kick_pwm = parse_int(optarg, 0, 255, "kick PWM");
break;
case 'K':
config->kick_ms = parse_int(optarg, 0, 10000, "kick duration");
break;
case 'f':
config->fail_safe_pwm = parse_int(optarg, 0, 255, "fail-safe PWM");
break;
case 'q':
config->exit_pwm = parse_int(optarg, 0, 255, "exit PWM");
break;
case 'd':
config->temp_div = parse_int(optarg, 1, 1000000,
"temperature divisor");
break;
case 'i':
config->interval = parse_int(optarg, 1, 300, "polling interval");
break;
case 'D':
config->debug = true;
break;
case '1':
config->once = true;
break;
case 'v':
printf("%s %s\n", PROGRAM_NAME, PROGRAM_VERSION);
exit(EXIT_SUCCESS);
case 'h':
usage(stdout, argv[0]);
exit(EXIT_SUCCESS);
default:
usage(stderr, argv[0]);
exit(EXIT_FAILURE);
}
}
}
static int validate_config(const struct fan_config *config)
{
if (config->full_speed_temp <= config->start_temp) {
fprintf(stderr, "Full-speed temperature must exceed start temperature\n");
return -1;
}
if (config->start_pwm > config->max_pwm) {
fprintf(stderr, "Start PWM must not exceed maximum PWM\n");
return -1;
}
if (config->kick_ms && !config->kick_pwm) {
fprintf(stderr, "Kick PWM must be non-zero when kick duration is enabled\n");
return -1;
}
return 0;
}
int main(int argc, char **argv)
{
struct fan_config config;
struct fan_runtime runtime;
const char *environment;
bool sensor_failed = false;
int exit_code = EXIT_SUCCESS;
set_defaults(&config);
parse_options(argc, argv, &config);
if (validate_config(&config))
return EXIT_FAILURE;
environment = getenv("FANCONTROL_SYSFS_ROOT");
if (environment && environment[0])
sysfs_root = environment;
environment = getenv("FANCONTROL_STATUS_FILE");
if (environment && environment[0])
status_file = environment;
openlog(PROGRAM_NAME, LOG_PID | LOG_CONS, LOG_DAEMON);
if (install_signal_handlers()) {
syslog(LOG_ERR, "failed to install signal handlers: %s", strerror(errno));
return EXIT_FAILURE;
}
if (resolve_paths(&config, &runtime))
return EXIT_FAILURE;
if (runtime.enable_file[0] && write_long_file(runtime.enable_file, 1))
syslog(LOG_WARNING, "failed to select manual PWM mode through %s: %s",
runtime.enable_file, strerror(errno));
syslog(LOG_NOTICE, "using temperature input %s and PWM output %s",
runtime.thermal_file, runtime.fan_file);
while (!stop_requested) {
long raw_temp;
long actual_pwm = -1;
long rpm = -1;
int64_t temp_mc = -1;
int target_pwm;
if (read_long_file(runtime.thermal_file, &raw_temp)) {
target_pwm = config.fail_safe_pwm;
if (set_pwm(&runtime, target_pwm))
exit_code = EXIT_FAILURE;
runtime.fan_active = target_pwm > 0;
write_status(&runtime, temp_mc, target_pwm, rpm,
"temperature-read-failed");
if (!sensor_failed)
syslog(LOG_ERR,
"failed to read temperature from %s; using fail-safe PWM %d",
runtime.thermal_file, target_pwm);
sensor_failed = true;
} else {
if (sensor_failed)
syslog(LOG_NOTICE, "temperature input %s recovered",
runtime.thermal_file);
sensor_failed = false;
temp_mc = (int64_t)raw_temp * 1000 / config.temp_div;
target_pwm = calculate_pwm(&config, temp_mc, runtime.fan_active);
if (target_pwm > 0 && !runtime.fan_active &&
config.kick_pwm > 0 && config.kick_ms > 0) {
if (!set_pwm(&runtime, config.kick_pwm))
sleep_ms((unsigned int)config.kick_ms);
}
if (!stop_requested &&
(read_long_file(runtime.fan_file, &actual_pwm) ||
actual_pwm != target_pwm) && set_pwm(&runtime, target_pwm))
exit_code = EXIT_FAILURE;
runtime.fan_active = target_pwm > 0;
if (runtime.rpm_file[0])
read_long_file(runtime.rpm_file, &rpm);
write_status(&runtime, temp_mc, target_pwm, rpm, NULL);
if (config.debug)
syslog(LOG_DEBUG, "temperature=%lldmC pwm=%d rpm=%ld",
(long long)temp_mc, target_pwm, rpm);
}
if (config.once)
break;
sleep_ms((unsigned int)config.interval * 1000);
}
if (!config.once && set_pwm(&runtime, config.exit_pwm))
exit_code = EXIT_FAILURE;
unlink(status_file);
closelog();
return exit_code;
}