/*
* 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;
}