From 8146e336d7057d2064ed43e6684a11f601c84f47 Mon Sep 17 00:00:00 2001 From: Přemysl Janouch Date: Fri, 22 Jan 2016 08:56:34 +0100 Subject: Add fancontrol-ng --- CMakeLists.txt | 15 +- README.adoc | 5 +- fancontrol-ng.c | 680 +++++++++++++++++++++++++++++++++++++++++++++ fancontrol-ng.conf.example | 15 + fancontrol-ng.service.in | 9 + liberty | 2 +- 6 files changed, 722 insertions(+), 4 deletions(-) create mode 100644 fancontrol-ng.c create mode 100644 fancontrol-ng.conf.example create mode 100644 fancontrol-ng.service.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f6614b..75fdd8a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,11 +40,22 @@ add_threads (dwmstatus) add_executable (brightness brightness.c) target_link_libraries (brightness ${project_libraries}) -add_threads (brightness) + +add_executable (fancontrol-ng fancontrol-ng.c) +target_link_libraries (fancontrol-ng ${project_libraries}) # The files to be installed include (GNUInstallDirs) -install (TARGETS dwmstatus brightness DESTINATION ${CMAKE_INSTALL_BINDIR}) + +configure_file (${PROJECT_SOURCE_DIR}/fancontrol-ng.service.in + ${PROJECT_BINARY_DIR}/fancontrol-ng.service @ONLY) +install (FILES ${PROJECT_BINARY_DIR}/fancontrol-ng.service + DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/systemd/system) +install (FILES fancontrol-ng.conf.example + DESTINATION ${CMAKE_INSTALL_DATADIR}/fancontrol-ng) + +install (TARGETS dwmstatus brightness fancontrol-ng + DESTINATION ${CMAKE_INSTALL_BINDIR}) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) # CPack diff --git a/README.adoc b/README.adoc index b0f884b..26c99c1 100644 --- a/README.adoc +++ b/README.adoc @@ -7,8 +7,11 @@ to other people as well. - 'dwmstatus' does literally everything my dwm doesn't but I'd like it to. It includes PulseAudio volume management and hand-written NUT and MPD clients, - all in the name of liberation from GPL-licensed software of course + all in the name of liberation from GPL-licensed software of course. - 'brightness' allows me to change the brightness of w/e display device I have. + - 'fancontrol-ng' is a clone of fancontrol that can handle errors on resume + from suspend instead of setting fans to maximum speed and quitting; + in general it doesn't handle everything the original does Don't expect them to work under any OS that isn't Linux. diff --git a/fancontrol-ng.c b/fancontrol-ng.c new file mode 100644 index 0000000..d68f2f0 --- /dev/null +++ b/fancontrol-ng.c @@ -0,0 +1,680 @@ +/* + * fancontrol-ng.c: clone of fancontrol from lm_sensors + * + * Copyright (c) 2016, Přemysl Janouch + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#define LIBERTY_WANT_POLLER + +#include "config.h" +#undef PROGRAM_NAME +#define PROGRAM_NAME "fancontrol-ng" +#include "liberty/liberty.c" + +/// Shorthand to set an error and return failure from the function +#define FAIL(...) \ + BLOCK_START \ + error_set (e, __VA_ARGS__); \ + return false; \ + BLOCK_END + +// --- Main program ------------------------------------------------------------ + +struct device +{ + LIST_HEADER (struct device) + + struct app_context *ctx; ///< Application context + struct config_item *config; ///< Configuration root for the device + char *path; ///< Base path + + struct poller_timer timer; ///< Refresh timer +}; + +struct app_context +{ + struct poller poller; ///< Poller + bool polling; ///< The event loop is running + + struct config_item *config; ///< Program configuration + struct device *devices; ///< All devices +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +log_message_custom (void *user_data, const char *quote, const char *fmt, + va_list ap) +{ + (void) user_data; + FILE *stream = stdout; + + // TODO: sd-daemon.h log level prefixes? + fputs (quote, stream); + vfprintf (stream, fmt, ap); + fputs ("\n", stream); +} + +static char * +read_file_cstr (const char *path, struct error **e) +{ + struct str s; + str_init (&s); + if (read_file (path, &s, e)) + return str_steal (&s); + str_free (&s); + return NULL; +} + +static int64_t +read_file_unsigned (const char *path, struct error **e) +{ + char *s, *end; + if (!(s = read_file_cstr (path, e))) + return -1; + if ((end = strpbrk (s, "\r\n"))) + *end = 0; + + unsigned long num; + bool ok = xstrtoul (&num, s, 10); + free (s); + + if (!ok || num > INT64_MAX) + { + error_set (e, "error reading `%s': %s", path, "invalid integer value"); + return -1; + } + return num; +} + +static bool +write_file_printf (const char *path, struct error **e, const char *format, ...) + ATTRIBUTE_PRINTF (3, 4); + +static bool +write_file_printf (const char *path, struct error **e, const char *format, ...) +{ + struct str s; + str_init (&s); + + va_list ap; + va_start (ap, format); + str_append_vprintf (&s, format, ap); + va_end (ap); + + bool success = write_file (path, s.str, s.len, e); + str_free (&s); + return success; +} + +// --- Configuration ----------------------------------------------------------- + +static bool +config_validate_nonnegative (const struct config_item *item, struct error **e) +{ + if (item->type == CONFIG_ITEM_NULL) + return true; + + hard_assert (item->type == CONFIG_ITEM_INTEGER); + if (item->value.integer >= 0) + return true; + + error_set (e, "must be non-negative"); + return false; +} + +static struct config_schema g_config_device[] = +{ + { .name = "name", + .comment = "Device identifier", + .type = CONFIG_ITEM_STRING }, + { .name = "interval", + .comment = "Temperature checking interval", + .type = CONFIG_ITEM_INTEGER, + .validate = config_validate_nonnegative, + .default_ = "5" }, + {} +}; + +static struct config_schema g_config_pwm[] = +{ + { .name = "temp", + .comment = "Path to temperature sensor output", + .type = CONFIG_ITEM_STRING }, + { .name = "min_temp", + .comment = "Temperature for no fan operation", + .type = CONFIG_ITEM_INTEGER, + .validate = config_validate_nonnegative, + .default_ = "40" }, + { .name = "max_temp", + .comment = "Temperature for maximum fan operation", + .type = CONFIG_ITEM_INTEGER, + .validate = config_validate_nonnegative, + .default_ = "80" }, + { .name = "min_start", + .comment = "Minimum value for the fan to start spinning", + .type = CONFIG_ITEM_INTEGER, + .validate = config_validate_nonnegative, + .default_ = "0" }, + { .name = "min_stop", + .comment = "Mimimum value for the fan to stop spinning", + .type = CONFIG_ITEM_INTEGER, + .validate = config_validate_nonnegative, + .default_ = "0" }, + { .name = "pwm_min", + .comment = "Minimum PWM value to use", + .type = CONFIG_ITEM_INTEGER, + .validate = config_validate_nonnegative }, + { .name = "pwm_max", + .comment = "Maximum PWM value to use", + .type = CONFIG_ITEM_INTEGER, + .validate = config_validate_nonnegative }, + {} +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static int64_t +get_config_integer (struct config_item *root, const char *key) +{ + struct config_item *item = config_item_get (root, key, NULL); + hard_assert (item && item->type == CONFIG_ITEM_INTEGER); + return item->value.integer; +} + +static const char * +get_config_string (struct config_item *root, const char *key) +{ + struct config_item *item = config_item_get (root, key, NULL); + hard_assert (item); + if (item->type == CONFIG_ITEM_NULL) + return NULL; + hard_assert (config_item_type_is_string (item->type)); + return item->value.string.str; +} + +// --- Fan control ------------------------------------------------------------- + +// Consider this a failed attempt to avoid creating special PWM objects +// based on the configuration. The complexity just moved somewhere else. + +struct paths +{ + char *temp; ///< Current temperature + char *pwm; ///< Current PWM value + char *pwm_enable; ///< PWM control state + char *pwm_min; ///< Minimum PWM value + char *pwm_max; ///< Maximum PWM value +}; + +static struct paths * +paths_new (const char *device_path, const char *path, struct config_item *pwm) +{ + struct paths *self = xcalloc (1, sizeof *self); + self->temp = xstrdup_printf + ("%s/%s", device_path, get_config_string (pwm, "temp")); + + self->pwm = xstrdup_printf ("%s/%s", device_path, path); + self->pwm_enable = xstrdup_printf ("%s/%s_enable", device_path, path); + self->pwm_min = xstrdup_printf ("%s/%s_min", device_path, path); + self->pwm_max = xstrdup_printf ("%s/%s_max", device_path, path); + return self; +} + +static void +paths_destroy (struct paths *self) +{ + free (self->temp); + + free (self->pwm); + free (self->pwm_enable); + free (self->pwm_min); + free (self->pwm_max); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static bool +pwm_update (struct paths *paths, struct config_item *pwm, struct error **e) +{ + int64_t cur_enable, cur_temp, cur_pwm, pwm_min, pwm_max; + if ((cur_enable = read_file_unsigned (paths->pwm_enable, e)) < 0 + || (cur_temp = read_file_unsigned (paths->temp, e)) < 0 + || (cur_pwm = read_file_unsigned (paths->pwm, e)) < 0) + return false; + + struct config_item *pwm_min_item = config_item_get (pwm, "pwm_min", NULL); + if (pwm_min_item->type == CONFIG_ITEM_INTEGER) + pwm_min = pwm_min_item->value.integer; + else if ((pwm_min = read_file_unsigned (paths->pwm_min, NULL)) < 0) + pwm_min = 0; + + struct config_item *pwm_max_item = config_item_get (pwm, "pwm_max", NULL); + if (pwm_max_item->type == CONFIG_ITEM_INTEGER) + pwm_max = pwm_max_item->value.integer; + else if ((pwm_max = read_file_unsigned (paths->pwm_max, NULL)) < 0) + pwm_max = 255; + + int64_t min_temp = get_config_integer (pwm, "min_temp"); + int64_t max_temp = get_config_integer (pwm, "max_temp"); + int64_t min_start = get_config_integer (pwm, "min_start"); + int64_t min_stop = get_config_integer (pwm, "min_stop"); + + if (min_temp >= max_temp) FAIL ("min_temp must be less than max_temp"); + if (pwm_max > 255) FAIL ("pwm_max must be at most 255"); + if (min_stop >= pwm_max) FAIL ("min_stop must be less than pwm_max"); + if (min_stop < pwm_min) FAIL ("min_stop must be at least pwm_min"); + + // I'm not sure if this strangely complicated computation is justifiable + double where + = ((double) cur_temp / 1000 - min_temp) + / ((double) max_temp - min_temp); + + int64_t new_pwm; + if (where <= 0) new_pwm = pwm_min; + else if (where >= 1) new_pwm = pwm_max; + else + { + new_pwm = min_stop + where * (pwm_max - min_stop); + + // If needed, we start the fan until next iteration + if (cur_pwm <= min_stop) + new_pwm = MAX (new_pwm, min_start); + } + + new_pwm = MAX (new_pwm, pwm_min); + new_pwm = MIN (new_pwm, pwm_max); + + if (cur_enable != 1 && !write_file_printf (paths->pwm_enable, e, "1")) + return false; + if (!write_file_printf (paths->pwm, e, "%" PRId64, new_pwm)) + return false; + return true; +} + +static bool +pwm_set_enable (struct paths *paths, char value) +{ + struct error *e = NULL; + if (write_file (paths->pwm_enable, &value, 1, &e)) + return true; + + print_error ("failed to change PWM mode to %c: %s", + value, e->message); + error_free (e); + return false; +} + +static bool +pwm_give_up (struct paths *paths) +{ + // Try automatic control, and if that fails, go full speed + return pwm_set_enable (paths, '2') || pwm_set_enable (paths, '0'); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct pwm_iter +{ + struct str_map_iter object_iter; ///< Configuration iterator + struct device *device; ///< Device + + struct config_item *pwm; ///< PWM + const char *pwm_path; ///< PWM path + struct paths *paths; ///< Paths +}; + +static void +pwm_iter_init (struct pwm_iter *self, struct device *device) +{ + str_map_iter_init (&self->object_iter, + &config_item_get (device->config, "pwms", NULL)->value.object); + self->device = device; + self->paths = NULL; +} + +static void +pwm_iter_free (struct pwm_iter *self) +{ + if (self->paths) + { + paths_destroy (self->paths); + self->paths = NULL; + } +} + +static bool +pwm_iter_next (struct pwm_iter *self) +{ + pwm_iter_free (self); + if (!(self->pwm = str_map_iter_next (&self->object_iter))) + return false; + + self->pwm_path = self->object_iter.link->key; + self->paths = paths_new (self->device->path, self->pwm_path, self->pwm); + return true; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +device_run (struct device *self) +{ + struct pwm_iter iter; + pwm_iter_init (&iter, self); + + while (pwm_iter_next (&iter)) + { + struct error *e = NULL; + if (pwm_update (iter.paths, iter.pwm, &e)) + continue; + + print_error ("pwm `%s': %s", iter.pwm_path, e->message); + error_free (e); + pwm_give_up (iter.paths); + } + + pwm_iter_free (&iter); + + poller_timer_set (&self->timer, + 1000 * get_config_integer (self->config, "interval")); +} + +static void +device_stop (struct device *self) +{ + struct pwm_iter iter; + pwm_iter_init (&iter, self); + + while (pwm_iter_next (&iter)) + pwm_give_up (iter.paths); + + pwm_iter_free (&iter); +} + +static void +device_create (struct app_context *ctx, const char *path, + struct config_item *root) +{ + struct device *self = xcalloc (1, sizeof *self); + self->config = root; + self->path = xstrdup (path); + + poller_timer_init (&self->timer, &ctx->poller); + self->timer.dispatcher = (poller_timer_fn) device_run; + self->timer.user_data = self; + + LIST_PREPEND (ctx->devices, self); +} + +// --- Configuration ----------------------------------------------------------- + +// TODO: consider moving to liberty, +// degesch and json-rpc-shell have exactly the same function +static struct config_item * +load_configuration_file (const char *filename, struct error **e) +{ + struct config_item *root = NULL; + + struct str data; + str_init (&data); + if (!read_file (filename, &data, e)) + goto end; + + struct error *error = NULL; + if (!(root = config_item_parse (data.str, data.len, false, &error))) + { + error_set (e, "parse error: %s", error->message); + error_free (error); + } +end: + str_free (&data); + return root; +} + +// There is no room for errors in the configuration, everything must be valid. +// Thus the reset to defaults on invalid values is effectively disabled here. +static bool +apply_schema (struct config_schema *schema, struct config_item *object, + struct error **e) +{ + struct error *warning = NULL, *error = NULL; + config_schema_initialize_item (schema, object, NULL, &warning, &error); + + if (error && warning) + { + error_free (warning); + error_propagate (e, error); + return false; + } + if (error) + { + error_propagate (e, error); + return false; + } + if (warning) + { + // The standard warning is inappropriate here + error_free (warning); + FAIL ("invalid item `%s'", schema->name); + } + return true; +} + +static bool +check_device_configuration (struct config_item *subtree, struct error **e) +{ + // Check regular fields in the device object + for (struct config_schema *s = g_config_device; s->name; s++) + if (!apply_schema (s, subtree, e)) + return false; + + // Check for a subobject with PWMs to control + struct config_item *pwms; + if (!(pwms = config_item_get (subtree, "pwms", e))) + return false; + if (pwms->type != CONFIG_ITEM_OBJECT) + FAIL ("`%s' is not an object", "pwms"); + if (!pwms->value.object.len) + FAIL ("no PWMs defined"); + + // Check regular fields in all PWM subobjects + struct str_map_iter iter; + str_map_iter_init (&iter, &pwms->value.object); + + struct config_item *pwm; + struct error *error = NULL; + while ((pwm = str_map_iter_next (&iter))) + { + const char *subpath = iter.link->key; + for (struct config_schema *s = g_config_pwm; s->name; s++) + if (!apply_schema (s, pwm, &error)) + { + error_set (e, "PWM `%s': %s", subpath, error->message); + error_free (error); + return false; + } + if (!get_config_string (pwm, "temp")) + FAIL ("PWM `%s': %s", subpath, "`temp' cannot be null"); + } + return true; +} + +static void +load_configuration (struct app_context *ctx, const char *config_path) +{ + struct error *e = NULL; + struct config_item *root = load_configuration_file (config_path, &e); + + if (e) + { + print_error ("error loading configuration: %s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + + struct str_map_iter iter; + str_map_iter_init (&iter, &(ctx->config = root)->value.object); + + struct config_item *subtree; + while ((subtree = str_map_iter_next (&iter))) + { + const char *path = iter.link->key; + if (subtree->type != CONFIG_ITEM_OBJECT) + print_fatal ("device `%s' in configuration is not an object", path); + else if (!check_device_configuration (subtree, &e)) + print_fatal ("device `%s': %s", path, e->message); + else + device_create (ctx, path, subtree); + } +} + +// --- Signals ----------------------------------------------------------------- + +static int g_signal_pipe[2]; ///< A pipe used to signal... signals + +static void +sigterm_handler (int signum) +{ + (void) signum; + + int original_errno = errno; + if (write (g_signal_pipe[1], "", 1) == -1) + soft_assert (errno == EAGAIN); + errno = original_errno; +} + +static void +setup_signal_handlers (void) +{ + if (pipe (g_signal_pipe) == -1) + exit_fatal ("%s: %s", "pipe", strerror (errno)); + + set_cloexec (g_signal_pipe[0]); + set_cloexec (g_signal_pipe[1]); + + // So that the pipe cannot overflow; it would make write() block within + // the signal handler, which is something we really don't want to happen. + // The same holds true for read(). + set_blocking (g_signal_pipe[0], false); + set_blocking (g_signal_pipe[1], false); + + (void) signal (SIGPIPE, SIG_IGN); + + struct sigaction sa; + sa.sa_flags = SA_RESTART; + sa.sa_handler = sigterm_handler; + sigemptyset (&sa.sa_mask); + + if (sigaction (SIGINT, &sa, NULL) == -1 + || sigaction (SIGTERM, &sa, NULL) == -1) + exit_fatal ("sigaction: %s", strerror (errno)); +} + +// --- Main program ------------------------------------------------------------ + +static void +on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx) +{ + char id = 0; + (void) read (fd->fd, &id, 1); + + ctx->polling = false; +} + +static const char * +parse_program_arguments (int argc, char **argv) +{ + static const struct opt opts[] = + { + { 'd', "debug", NULL, 0, "run in debug mode" }, + { 'h', "help", NULL, 0, "display this help and exit" }, + { 'V', "version", NULL, 0, "output version information and exit" }, + { 0, NULL, NULL, 0, NULL } + }; + + struct opt_handler oh; + opt_handler_init (&oh, argc, argv, opts, "CONFIG", "Fan controller."); + + int c; + while ((c = opt_handler_get (&oh)) != -1) + switch (c) + { + case 'd': + g_debug_mode = true; + break; + case 'h': + opt_handler_usage (&oh, stdout); + exit (EXIT_SUCCESS); + case 'V': + printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); + exit (EXIT_SUCCESS); + default: + print_error ("wrong options"); + opt_handler_usage (&oh, stderr); + exit (EXIT_FAILURE); + } + + argc -= optind; + argv += optind; + + if (argc != 1) + { + opt_handler_usage (&oh, stderr); + exit (EXIT_FAILURE); + } + + opt_handler_free (&oh); + return argv[0]; +} + +int +main (int argc, char *argv[]) +{ + g_log_message_real = log_message_custom; + const char *config_path = parse_program_arguments (argc, argv); + + struct app_context ctx; + memset (&ctx, 0, sizeof ctx); + poller_init (&ctx.poller); + + setup_signal_handlers (); + + struct poller_fd signal_event; + poller_fd_init (&signal_event, &ctx.poller, g_signal_pipe[0]); + signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable; + signal_event.user_data = &ctx; + poller_fd_set (&signal_event, POLLIN); + + load_configuration (&ctx, config_path); + + if (!ctx.devices) + exit_fatal ("no devices present in configuration"); + LIST_FOR_EACH (struct device, iter, ctx.devices) + device_run (iter); + + ctx.polling = true; + while (ctx.polling) + poller_run (&ctx.poller); + + LIST_FOR_EACH (struct device, iter, ctx.devices) + device_stop (iter); + + config_item_destroy (ctx.config); + poller_free (&ctx.poller); + return EXIT_SUCCESS; +} diff --git a/fancontrol-ng.conf.example b/fancontrol-ng.conf.example new file mode 100644 index 0000000..c225669 --- /dev/null +++ b/fancontrol-ng.conf.example @@ -0,0 +1,15 @@ +"/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0" = { + name = "radeon" + interval = 10 + pwms = { + "hwmon/hwmon2/pwm1" = { + temp = "hwmon/hwmon2/temp1_input" + min_temp = 40 + max_temp = 80 + min_start = 50 + min_stop = 40 +# min_pwm = 0 +# max_pwm = 255 + } + } +} diff --git a/fancontrol-ng.service.in b/fancontrol-ng.service.in new file mode 100644 index 0000000..1a093a4 --- /dev/null +++ b/fancontrol-ng.service.in @@ -0,0 +1,9 @@ +[Unit] +Description=fancontrol-ng + +[Service] +ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/fancontrol-ng @CMAKE_INSTALL_FULL_SYSCONFDIR@/fancontrol-ng.conf +Restart=on-abort + +[Install] +WantedBy=default.target diff --git a/liberty b/liberty index 9e3cb2b..052d2ff 160000 --- a/liberty +++ b/liberty @@ -1 +1 @@ -Subproject commit 9e3cb2b6aa2db3ca0ea6a854fb3f89a163c84235 +Subproject commit 052d2ffc9a3141ef2bb771f70190ed7a0bb9da44 -- cgit v1.2.3-70-g09d2