573 lines · c
1// SPDX-License-Identifier: GPL-2.0-only2// Copyright (C) 2022, Linaro Ltd - Daniel Lezcano <daniel.lezcano@linaro.org>3#define _GNU_SOURCE4#include <dirent.h>5#include <fcntl.h>6#include <getopt.h>7#include <regex.h>8#include <signal.h>9#include <stdio.h>10#include <stdlib.h>11#include <string.h>12#include <sys/stat.h>13#include <sys/signalfd.h>14#include <sys/timerfd.h>15#include <sys/types.h>16#include <sys/wait.h>17#include <time.h>18#include <unistd.h>19#include <linux/thermal.h>20 21#include <libconfig.h>22#include "thermal-tools.h"23 24#define CLASS_THERMAL "/sys/class/thermal"25 26enum {27 THERMOMETER_SUCCESS = 0,28 THERMOMETER_OPTION_ERROR,29 THERMOMETER_LOG_ERROR,30 THERMOMETER_CONFIG_ERROR,31 THERMOMETER_TIME_ERROR,32 THERMOMETER_INIT_ERROR,33 THERMOMETER_RUNTIME_ERROR34};35 36struct options {37 int loglvl;38 int logopt;39 int overwrite;40 int duration;41 const char *config;42 char postfix[PATH_MAX];43 char output[PATH_MAX];44};45 46struct tz_regex {47 regex_t regex;48 int polling;49};50 51struct configuration {52 struct tz_regex *tz_regex;53 int nr_tz_regex;54 55};56 57struct tz {58 FILE *file_out;59 int fd_temp;60 int fd_timer;61 int polling;62 const char *name;63};64 65struct thermometer {66 struct tz *tz;67 int nr_tz;68};69 70static struct tz_regex *configuration_tz_match(const char *expr,71 struct configuration *config)72{73 int i;74 75 for (i = 0; i < config->nr_tz_regex; i++) {76 77 if (!regexec(&config->tz_regex[i].regex, expr, 0, NULL, 0))78 return &config->tz_regex[i];79 }80 81 return NULL;82}83 84static int configuration_default_init(struct configuration *config)85{86 config->tz_regex = realloc(config->tz_regex, sizeof(*config->tz_regex) *87 (config->nr_tz_regex + 1));88 89 if (regcomp(&config->tz_regex[config->nr_tz_regex].regex, ".*",90 REG_NOSUB | REG_EXTENDED)) {91 ERROR("Invalid regular expression\n");92 return -1;93 }94 95 config->tz_regex[config->nr_tz_regex].polling = 250;96 config->nr_tz_regex = 1;97 98 return 0;99}100 101static int configuration_init(const char *path, struct configuration *config)102{103 config_t cfg;104 105 config_setting_t *tz;106 int i, length;107 108 if (path && access(path, F_OK)) {109 ERROR("'%s' is not accessible\n", path);110 return -1;111 }112 113 if (!path && !config->nr_tz_regex) {114 INFO("No thermal zones configured, using wildcard for all of them\n");115 return configuration_default_init(config);116 }117 118 config_init(&cfg);119 120 if (!config_read_file(&cfg, path)) {121 ERROR("Failed to parse %s:%d - %s\n", config_error_file(&cfg),122 config_error_line(&cfg), config_error_text(&cfg));123 124 return -1;125 }126 127 tz = config_lookup(&cfg, "thermal-zones");128 if (!tz) {129 ERROR("No thermal zone configured to be monitored\n");130 return -1;131 }132 133 length = config_setting_length(tz);134 135 INFO("Found %d thermal zone(s) regular expression\n", length);136 137 for (i = 0; i < length; i++) {138 139 config_setting_t *node;140 const char *name;141 int polling;142 143 node = config_setting_get_elem(tz, i);144 if (!node) {145 ERROR("Missing node name '%d'\n", i);146 return -1;147 }148 149 if (!config_setting_lookup_string(node, "name", &name)) {150 ERROR("Thermal zone name not found\n");151 return -1;152 }153 154 if (!config_setting_lookup_int(node, "polling", &polling)) {155 ERROR("Polling value not found");156 return -1;157 }158 159 config->tz_regex = realloc(config->tz_regex, sizeof(*config->tz_regex) *160 (config->nr_tz_regex + 1));161 162 if (regcomp(&config->tz_regex[config->nr_tz_regex].regex, name,163 REG_NOSUB | REG_EXTENDED)) {164 ERROR("Invalid regular expression '%s'\n", name);165 continue;166 }167 168 config->tz_regex[config->nr_tz_regex].polling = polling;169 config->nr_tz_regex++;170 171 INFO("Thermal zone regular expression '%s' with polling %d\n",172 name, polling);173 }174 175 return 0;176}177 178static void usage(const char *cmd)179{180 printf("%s Version: %s\n", cmd, VERSION);181 printf("Usage: %s [options]\n", cmd);182 printf("\t-h, --help\t\tthis help\n");183 printf("\t-o, --output <dir>\toutput directory for temperature capture\n");184 printf("\t-c, --config <file>\tconfiguration file\n");185 printf("\t-d, --duration <seconds>\tcapture duration\n");186 printf("\t-l, --loglevel <level>\tlog level: ");187 printf("DEBUG, INFO, NOTICE, WARN, ERROR\n");188 printf("\t-p, --postfix <string>\tpostfix to be happened at the end of the files\n");189 printf("\t-s, --syslog\t\toutput to syslog\n");190 printf("\t-w, --overwrite\t\toverwrite the temperature capture files if they exist\n");191 printf("\n");192 exit(0);193}194 195static int options_init(int argc, char *argv[], struct options *options)196{197 int opt;198 time_t now = time(NULL);199 200 struct option long_options[] = {201 { "help", no_argument, NULL, 'h' },202 { "config", required_argument, NULL, 'c' },203 { "duration", required_argument, NULL, 'd' },204 { "loglevel", required_argument, NULL, 'l' },205 { "postfix", required_argument, NULL, 'p' },206 { "output", required_argument, NULL, 'o' },207 { "syslog", required_argument, NULL, 's' },208 { "overwrite", no_argument, NULL, 'w' },209 { 0, 0, 0, 0 }210 };211 212 strftime(options->postfix, sizeof(options->postfix),213 "-%Y-%m-%d_%H:%M:%S", gmtime(&now));214 215 while (1) {216 217 int optindex = 0;218 219 opt = getopt_long(argc, argv, "ho:c:d:l:p:sw", long_options, &optindex);220 if (opt == -1)221 break;222 223 switch (opt) {224 case 'c':225 options->config = optarg;226 break;227 case 'd':228 options->duration = atoi(optarg) * 1000;229 break;230 case 'l':231 options->loglvl = log_str2level(optarg);232 break;233 case 'h':234 usage(basename(argv[0]));235 break;236 case 'p':237 strcpy(options->postfix, optarg);238 break;239 case 'o':240 strcpy(options->output, optarg);241 break;242 case 's':243 options->logopt = TO_SYSLOG;244 break;245 case 'w':246 options->overwrite = 1;247 break;248 default: /* '?' */249 ERROR("Usage: %s --help\n", argv[0]);250 return -1;251 }252 }253 254 return 0;255}256 257static int thermometer_add_tz(const char *path, const char *name, int polling,258 struct thermometer *thermometer)259{260 int fd;261 char tz_path[PATH_MAX];262 263 sprintf(tz_path, CLASS_THERMAL"/%s/temp", path);264 265 fd = open(tz_path, O_RDONLY);266 if (fd < 0) {267 ERROR("Failed to open '%s': %m\n", tz_path);268 return -1;269 }270 271 thermometer->tz = realloc(thermometer->tz,272 sizeof(*thermometer->tz) * (thermometer->nr_tz + 1));273 if (!thermometer->tz) {274 ERROR("Failed to allocate thermometer->tz\n");275 return -1;276 }277 278 thermometer->tz[thermometer->nr_tz].fd_temp = fd;279 thermometer->tz[thermometer->nr_tz].name = strdup(name);280 thermometer->tz[thermometer->nr_tz].polling = polling;281 thermometer->nr_tz++;282 283 INFO("Added thermal zone '%s->%s (polling:%d)'\n", path, name, polling);284 285 return 0;286}287 288static int thermometer_init(struct configuration *config,289 struct thermometer *thermometer)290{291 DIR *dir;292 struct dirent *dirent;293 struct tz_regex *tz_regex;294 const char *tz_dirname = "thermal_zone";295 296 if (mainloop_init()) {297 ERROR("Failed to start mainloop\n");298 return -1;299 }300 301 dir = opendir(CLASS_THERMAL);302 if (!dir) {303 ERROR("failed to open '%s'\n", CLASS_THERMAL);304 return -1;305 }306 307 while ((dirent = readdir(dir))) {308 char tz_type[THERMAL_NAME_LENGTH];309 char tz_path[PATH_MAX];310 FILE *tz_file;311 312 if (strncmp(dirent->d_name, tz_dirname, strlen(tz_dirname)))313 continue;314 315 sprintf(tz_path, CLASS_THERMAL"/%s/type", dirent->d_name);316 317 tz_file = fopen(tz_path, "r");318 if (!tz_file) {319 ERROR("Failed to open '%s': %m", tz_path);320 continue;321 }322 323 fscanf(tz_file, "%s", tz_type);324 325 fclose(tz_file);326 327 tz_regex = configuration_tz_match(tz_type, config);328 if (!tz_regex)329 continue;330 331 if (thermometer_add_tz(dirent->d_name, tz_type,332 tz_regex->polling, thermometer))333 continue;334 }335 336 closedir(dir);337 338 return 0;339}340 341static int timer_temperature_callback(int fd, void *arg)342{343 struct tz *tz = arg;344 char buf[16] = { 0 };345 346 pread(tz->fd_temp, buf, sizeof(buf), 0);347 348 fprintf(tz->file_out, "%ld %s", getuptimeofday_ms(), buf);349 350 read(fd, buf, sizeof(buf));351 352 return 0;353}354 355static int thermometer_start(struct thermometer *thermometer,356 struct options *options)357{358 struct itimerspec timer_it = { 0 };359 char *path;360 FILE *f;361 int i;362 363 INFO("Capturing %d thermal zone(s) temperature...\n", thermometer->nr_tz);364 365 if (access(options->output, F_OK) && mkdir(options->output, 0700)) {366 ERROR("Failed to create directory '%s'\n", options->output);367 return -1;368 }369 370 for (i = 0; i < thermometer->nr_tz; i++) {371 372 asprintf(&path, "%s/%s%s", options->output,373 thermometer->tz[i].name, options->postfix);374 375 if (!options->overwrite && !access(path, F_OK)) {376 ERROR("'%s' already exists\n", path);377 return -1;378 }379 380 f = fopen(path, "w");381 if (!f) {382 ERROR("Failed to create '%s':%m\n", path);383 return -1;384 }385 386 fprintf(f, "timestamp(ms) %s(°mC)\n", thermometer->tz[i].name);387 388 thermometer->tz[i].file_out = f;389 390 DEBUG("Created '%s' file for thermal zone '%s'\n", path, thermometer->tz[i].name);391 392 /*393 * Create polling timer394 */395 thermometer->tz[i].fd_timer = timerfd_create(CLOCK_MONOTONIC, 0);396 if (thermometer->tz[i].fd_timer < 0) {397 ERROR("Failed to create timer for '%s': %m\n",398 thermometer->tz[i].name);399 return -1;400 }401 402 DEBUG("Watching '%s' every %d ms\n",403 thermometer->tz[i].name, thermometer->tz[i].polling);404 405 timer_it.it_interval = timer_it.it_value =406 msec_to_timespec(thermometer->tz[i].polling);407 408 if (timerfd_settime(thermometer->tz[i].fd_timer, 0,409 &timer_it, NULL) < 0)410 return -1;411 412 if (mainloop_add(thermometer->tz[i].fd_timer,413 timer_temperature_callback,414 &thermometer->tz[i]))415 return -1;416 }417 418 return 0;419}420 421static int thermometer_execute(int argc, char *argv[], char *const envp[], pid_t *pid)422{423 if (!argc)424 return 0;425 426 *pid = fork();427 if (*pid < 0) {428 ERROR("Failed to fork process: %m");429 return -1;430 }431 432 if (!(*pid)) {433 execvpe(argv[0], argv, envp);434 exit(1);435 }436 437 return 0;438}439 440static int kill_process(__maybe_unused int fd, void *arg)441{442 pid_t pid = *(pid_t *)arg;443 444 if (kill(pid, SIGTERM))445 ERROR("Failed to send SIGTERM signal to '%d': %p\n", pid);446 else if (waitpid(pid, NULL, 0))447 ERROR("Failed to wait pid '%d': %p\n", pid);448 449 mainloop_exit();450 451 return 0;452}453 454static int exit_mainloop(__maybe_unused int fd, __maybe_unused void *arg)455{456 mainloop_exit();457 return 0;458}459 460static int thermometer_wait(struct options *options, pid_t pid)461{462 int fd;463 sigset_t mask;464 465 /*466 * If there is a duration specified, we will exit the mainloop467 * and gracefully close all the files which will flush the468 * file system cache469 */470 if (options->duration) {471 struct itimerspec timer_it = { 0 };472 473 timer_it.it_value = msec_to_timespec(options->duration);474 475 fd = timerfd_create(CLOCK_MONOTONIC, 0);476 if (fd < 0) {477 ERROR("Failed to create duration timer: %m\n");478 return -1;479 }480 481 if (timerfd_settime(fd, 0, &timer_it, NULL)) {482 ERROR("Failed to set timer time: %m\n");483 return -1;484 }485 486 if (mainloop_add(fd, pid < 0 ? exit_mainloop : kill_process, &pid)) {487 ERROR("Failed to set timer exit mainloop callback\n");488 return -1;489 }490 }491 492 /*493 * We want to catch any keyboard interrupt, as well as child494 * signals if any in order to exit properly495 */496 sigemptyset(&mask);497 sigaddset(&mask, SIGINT);498 sigaddset(&mask, SIGQUIT);499 sigaddset(&mask, SIGCHLD);500 501 if (sigprocmask(SIG_BLOCK, &mask, NULL)) {502 ERROR("Failed to set sigprocmask: %m\n");503 return -1;504 }505 506 fd = signalfd(-1, &mask, 0);507 if (fd < 0) {508 ERROR("Failed to set the signalfd: %m\n");509 return -1;510 }511 512 if (mainloop_add(fd, exit_mainloop, NULL)) {513 ERROR("Failed to set timer exit mainloop callback\n");514 return -1;515 }516 517 return mainloop(-1);518}519 520static int thermometer_stop(struct thermometer *thermometer)521{522 int i;523 524 INFO("Closing/flushing output files\n");525 526 for (i = 0; i < thermometer->nr_tz; i++)527 fclose(thermometer->tz[i].file_out);528 529 return 0;530}531 532int main(int argc, char *argv[], char *const envp[])533{534 struct options options = {535 .loglvl = LOG_DEBUG,536 .logopt = TO_STDOUT,537 .output = ".",538 };539 struct configuration config = { 0 };540 struct thermometer thermometer = { 0 };541 542 pid_t pid = -1;543 544 if (options_init(argc, argv, &options))545 return THERMOMETER_OPTION_ERROR;546 547 if (log_init(options.loglvl, argv[0], options.logopt))548 return THERMOMETER_LOG_ERROR;549 550 if (configuration_init(options.config, &config))551 return THERMOMETER_CONFIG_ERROR;552 553 if (uptimeofday_init())554 return THERMOMETER_TIME_ERROR;555 556 if (thermometer_init(&config, &thermometer))557 return THERMOMETER_INIT_ERROR;558 559 if (thermometer_start(&thermometer, &options))560 return THERMOMETER_RUNTIME_ERROR;561 562 if (thermometer_execute(argc - optind, &argv[optind], envp, &pid))563 return THERMOMETER_RUNTIME_ERROR;564 565 if (thermometer_wait(&options, pid))566 return THERMOMETER_RUNTIME_ERROR;567 568 if (thermometer_stop(&thermometer))569 return THERMOMETER_RUNTIME_ERROR;570 571 return THERMOMETER_SUCCESS;572}573