Path: blob/master/tools/thermal/thermometer/thermometer.c
26288 views
// SPDX-License-Identifier: GPL-2.0-only1// Copyright (C) 2022, Linaro Ltd - Daniel Lezcano <[email protected]>2#define _GNU_SOURCE3#include <dirent.h>4#include <fcntl.h>5#include <getopt.h>6#include <regex.h>7#include <signal.h>8#include <stdio.h>9#include <stdlib.h>10#include <string.h>11#include <sys/stat.h>12#include <sys/signalfd.h>13#include <sys/timerfd.h>14#include <sys/types.h>15#include <sys/wait.h>16#include <time.h>17#include <unistd.h>18#include <linux/thermal.h>1920#include <libconfig.h>21#include "thermal-tools.h"2223#define CLASS_THERMAL "/sys/class/thermal"2425enum {26THERMOMETER_SUCCESS = 0,27THERMOMETER_OPTION_ERROR,28THERMOMETER_LOG_ERROR,29THERMOMETER_CONFIG_ERROR,30THERMOMETER_TIME_ERROR,31THERMOMETER_INIT_ERROR,32THERMOMETER_RUNTIME_ERROR33};3435struct options {36int loglvl;37int logopt;38int overwrite;39int duration;40const char *config;41char postfix[PATH_MAX];42char output[PATH_MAX];43};4445struct tz_regex {46regex_t regex;47int polling;48};4950struct configuration {51struct tz_regex *tz_regex;52int nr_tz_regex;5354};5556struct tz {57FILE *file_out;58int fd_temp;59int fd_timer;60int polling;61const char *name;62};6364struct thermometer {65struct tz *tz;66int nr_tz;67};6869static struct tz_regex *configuration_tz_match(const char *expr,70struct configuration *config)71{72int i;7374for (i = 0; i < config->nr_tz_regex; i++) {7576if (!regexec(&config->tz_regex[i].regex, expr, 0, NULL, 0))77return &config->tz_regex[i];78}7980return NULL;81}8283static int configuration_default_init(struct configuration *config)84{85config->tz_regex = realloc(config->tz_regex, sizeof(*config->tz_regex) *86(config->nr_tz_regex + 1));8788if (regcomp(&config->tz_regex[config->nr_tz_regex].regex, ".*",89REG_NOSUB | REG_EXTENDED)) {90ERROR("Invalid regular expression\n");91return -1;92}9394config->tz_regex[config->nr_tz_regex].polling = 250;95config->nr_tz_regex = 1;9697return 0;98}99100static int configuration_init(const char *path, struct configuration *config)101{102config_t cfg;103104config_setting_t *tz;105int i, length;106107if (path && access(path, F_OK)) {108ERROR("'%s' is not accessible\n", path);109return -1;110}111112if (!path && !config->nr_tz_regex) {113INFO("No thermal zones configured, using wildcard for all of them\n");114return configuration_default_init(config);115}116117config_init(&cfg);118119if (!config_read_file(&cfg, path)) {120ERROR("Failed to parse %s:%d - %s\n", config_error_file(&cfg),121config_error_line(&cfg), config_error_text(&cfg));122123return -1;124}125126tz = config_lookup(&cfg, "thermal-zones");127if (!tz) {128ERROR("No thermal zone configured to be monitored\n");129return -1;130}131132length = config_setting_length(tz);133134INFO("Found %d thermal zone(s) regular expression\n", length);135136for (i = 0; i < length; i++) {137138config_setting_t *node;139const char *name;140int polling;141142node = config_setting_get_elem(tz, i);143if (!node) {144ERROR("Missing node name '%d'\n", i);145return -1;146}147148if (!config_setting_lookup_string(node, "name", &name)) {149ERROR("Thermal zone name not found\n");150return -1;151}152153if (!config_setting_lookup_int(node, "polling", &polling)) {154ERROR("Polling value not found");155return -1;156}157158config->tz_regex = realloc(config->tz_regex, sizeof(*config->tz_regex) *159(config->nr_tz_regex + 1));160161if (regcomp(&config->tz_regex[config->nr_tz_regex].regex, name,162REG_NOSUB | REG_EXTENDED)) {163ERROR("Invalid regular expression '%s'\n", name);164continue;165}166167config->tz_regex[config->nr_tz_regex].polling = polling;168config->nr_tz_regex++;169170INFO("Thermal zone regular expression '%s' with polling %d\n",171name, polling);172}173174return 0;175}176177static void usage(const char *cmd)178{179printf("%s Version: %s\n", cmd, VERSION);180printf("Usage: %s [options]\n", cmd);181printf("\t-h, --help\t\tthis help\n");182printf("\t-o, --output <dir>\toutput directory for temperature capture\n");183printf("\t-c, --config <file>\tconfiguration file\n");184printf("\t-d, --duration <seconds>\tcapture duration\n");185printf("\t-l, --loglevel <level>\tlog level: ");186printf("DEBUG, INFO, NOTICE, WARN, ERROR\n");187printf("\t-p, --postfix <string>\tpostfix to be happened at the end of the files\n");188printf("\t-s, --syslog\t\toutput to syslog\n");189printf("\t-w, --overwrite\t\toverwrite the temperature capture files if they exist\n");190printf("\n");191exit(0);192}193194static int options_init(int argc, char *argv[], struct options *options)195{196int opt;197time_t now = time(NULL);198199struct option long_options[] = {200{ "help", no_argument, NULL, 'h' },201{ "config", required_argument, NULL, 'c' },202{ "duration", required_argument, NULL, 'd' },203{ "loglevel", required_argument, NULL, 'l' },204{ "postfix", required_argument, NULL, 'p' },205{ "output", required_argument, NULL, 'o' },206{ "syslog", required_argument, NULL, 's' },207{ "overwrite", no_argument, NULL, 'w' },208{ 0, 0, 0, 0 }209};210211strftime(options->postfix, sizeof(options->postfix),212"-%Y-%m-%d_%H:%M:%S", gmtime(&now));213214while (1) {215216int optindex = 0;217218opt = getopt_long(argc, argv, "ho:c:d:l:p:sw", long_options, &optindex);219if (opt == -1)220break;221222switch (opt) {223case 'c':224options->config = optarg;225break;226case 'd':227options->duration = atoi(optarg) * 1000;228break;229case 'l':230options->loglvl = log_str2level(optarg);231break;232case 'h':233usage(basename(argv[0]));234break;235case 'p':236strcpy(options->postfix, optarg);237break;238case 'o':239strcpy(options->output, optarg);240break;241case 's':242options->logopt = TO_SYSLOG;243break;244case 'w':245options->overwrite = 1;246break;247default: /* '?' */248ERROR("Usage: %s --help\n", argv[0]);249return -1;250}251}252253return 0;254}255256static int thermometer_add_tz(const char *path, const char *name, int polling,257struct thermometer *thermometer)258{259int fd;260char tz_path[PATH_MAX];261struct tz *tz;262263sprintf(tz_path, CLASS_THERMAL"/%s/temp", path);264265fd = open(tz_path, O_RDONLY);266if (fd < 0) {267ERROR("Failed to open '%s': %m\n", tz_path);268return -1;269}270271tz = realloc(thermometer->tz, sizeof(*thermometer->tz) * (thermometer->nr_tz + 1));272if (!tz) {273ERROR("Failed to allocate thermometer->tz\n");274return -1;275}276277thermometer->tz = tz;278thermometer->tz[thermometer->nr_tz].fd_temp = fd;279thermometer->tz[thermometer->nr_tz].name = strdup(name);280thermometer->tz[thermometer->nr_tz].polling = polling;281thermometer->nr_tz++;282283INFO("Added thermal zone '%s->%s (polling:%d)'\n", path, name, polling);284285return 0;286}287288static int thermometer_init(struct configuration *config,289struct thermometer *thermometer)290{291DIR *dir;292struct dirent *dirent;293struct tz_regex *tz_regex;294const char *tz_dirname = "thermal_zone";295296if (mainloop_init()) {297ERROR("Failed to start mainloop\n");298return -1;299}300301dir = opendir(CLASS_THERMAL);302if (!dir) {303ERROR("failed to open '%s'\n", CLASS_THERMAL);304return -1;305}306307while ((dirent = readdir(dir))) {308char tz_type[THERMAL_NAME_LENGTH];309char tz_path[PATH_MAX];310FILE *tz_file;311312if (strncmp(dirent->d_name, tz_dirname, strlen(tz_dirname)))313continue;314315sprintf(tz_path, CLASS_THERMAL"/%s/type", dirent->d_name);316317tz_file = fopen(tz_path, "r");318if (!tz_file) {319ERROR("Failed to open '%s': %m", tz_path);320continue;321}322323fscanf(tz_file, "%s", tz_type);324325fclose(tz_file);326327tz_regex = configuration_tz_match(tz_type, config);328if (!tz_regex)329continue;330331if (thermometer_add_tz(dirent->d_name, tz_type,332tz_regex->polling, thermometer))333continue;334}335336closedir(dir);337338return 0;339}340341static int timer_temperature_callback(int fd, void *arg)342{343struct tz *tz = arg;344char buf[16] = { 0 };345346pread(tz->fd_temp, buf, sizeof(buf), 0);347348fprintf(tz->file_out, "%ld %s", getuptimeofday_ms(), buf);349350read(fd, buf, sizeof(buf));351352return 0;353}354355static int thermometer_start(struct thermometer *thermometer,356struct options *options)357{358struct itimerspec timer_it = { 0 };359char *path;360FILE *f;361int i;362363INFO("Capturing %d thermal zone(s) temperature...\n", thermometer->nr_tz);364365if (access(options->output, F_OK) && mkdir(options->output, 0700)) {366ERROR("Failed to create directory '%s'\n", options->output);367return -1;368}369370for (i = 0; i < thermometer->nr_tz; i++) {371372asprintf(&path, "%s/%s%s", options->output,373thermometer->tz[i].name, options->postfix);374375if (!options->overwrite && !access(path, F_OK)) {376ERROR("'%s' already exists\n", path);377return -1;378}379380f = fopen(path, "w");381if (!f) {382ERROR("Failed to create '%s':%m\n", path);383return -1;384}385386fprintf(f, "timestamp(ms) %s(°mC)\n", thermometer->tz[i].name);387388thermometer->tz[i].file_out = f;389390DEBUG("Created '%s' file for thermal zone '%s'\n", path, thermometer->tz[i].name);391392/*393* Create polling timer394*/395thermometer->tz[i].fd_timer = timerfd_create(CLOCK_MONOTONIC, 0);396if (thermometer->tz[i].fd_timer < 0) {397ERROR("Failed to create timer for '%s': %m\n",398thermometer->tz[i].name);399return -1;400}401402DEBUG("Watching '%s' every %d ms\n",403thermometer->tz[i].name, thermometer->tz[i].polling);404405timer_it.it_interval = timer_it.it_value =406msec_to_timespec(thermometer->tz[i].polling);407408if (timerfd_settime(thermometer->tz[i].fd_timer, 0,409&timer_it, NULL) < 0)410return -1;411412if (mainloop_add(thermometer->tz[i].fd_timer,413timer_temperature_callback,414&thermometer->tz[i]))415return -1;416}417418return 0;419}420421static int thermometer_execute(int argc, char *argv[], char *const envp[], pid_t *pid)422{423if (!argc)424return 0;425426*pid = fork();427if (*pid < 0) {428ERROR("Failed to fork process: %m");429return -1;430}431432if (!(*pid)) {433execvpe(argv[0], argv, envp);434exit(1);435}436437return 0;438}439440static int kill_process(__maybe_unused int fd, void *arg)441{442pid_t pid = *(pid_t *)arg;443444if (kill(pid, SIGTERM))445ERROR("Failed to send SIGTERM signal to '%d': %p\n", pid);446else if (waitpid(pid, NULL, 0))447ERROR("Failed to wait pid '%d': %p\n", pid);448449mainloop_exit();450451return 0;452}453454static int exit_mainloop(__maybe_unused int fd, __maybe_unused void *arg)455{456mainloop_exit();457return 0;458}459460static int thermometer_wait(struct options *options, pid_t pid)461{462int fd;463sigset_t mask;464465/*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*/470if (options->duration) {471struct itimerspec timer_it = { 0 };472473timer_it.it_value = msec_to_timespec(options->duration);474475fd = timerfd_create(CLOCK_MONOTONIC, 0);476if (fd < 0) {477ERROR("Failed to create duration timer: %m\n");478return -1;479}480481if (timerfd_settime(fd, 0, &timer_it, NULL)) {482ERROR("Failed to set timer time: %m\n");483return -1;484}485486if (mainloop_add(fd, pid < 0 ? exit_mainloop : kill_process, &pid)) {487ERROR("Failed to set timer exit mainloop callback\n");488return -1;489}490}491492/*493* We want to catch any keyboard interrupt, as well as child494* signals if any in order to exit properly495*/496sigemptyset(&mask);497sigaddset(&mask, SIGINT);498sigaddset(&mask, SIGQUIT);499sigaddset(&mask, SIGCHLD);500501if (sigprocmask(SIG_BLOCK, &mask, NULL)) {502ERROR("Failed to set sigprocmask: %m\n");503return -1;504}505506fd = signalfd(-1, &mask, 0);507if (fd < 0) {508ERROR("Failed to set the signalfd: %m\n");509return -1;510}511512if (mainloop_add(fd, exit_mainloop, NULL)) {513ERROR("Failed to set timer exit mainloop callback\n");514return -1;515}516517return mainloop(-1);518}519520static int thermometer_stop(struct thermometer *thermometer)521{522int i;523524INFO("Closing/flushing output files\n");525526for (i = 0; i < thermometer->nr_tz; i++)527fclose(thermometer->tz[i].file_out);528529return 0;530}531532int main(int argc, char *argv[], char *const envp[])533{534struct options options = {535.loglvl = LOG_DEBUG,536.logopt = TO_STDOUT,537.output = ".",538};539struct configuration config = { 0 };540struct thermometer thermometer = { 0 };541542pid_t pid = -1;543544if (options_init(argc, argv, &options))545return THERMOMETER_OPTION_ERROR;546547if (log_init(options.loglvl, argv[0], options.logopt))548return THERMOMETER_LOG_ERROR;549550if (configuration_init(options.config, &config))551return THERMOMETER_CONFIG_ERROR;552553if (uptimeofday_init())554return THERMOMETER_TIME_ERROR;555556if (thermometer_init(&config, &thermometer))557return THERMOMETER_INIT_ERROR;558559if (thermometer_start(&thermometer, &options))560return THERMOMETER_RUNTIME_ERROR;561562if (thermometer_execute(argc - optind, &argv[optind], envp, &pid))563return THERMOMETER_RUNTIME_ERROR;564565if (thermometer_wait(&options, pid))566return THERMOMETER_RUNTIME_ERROR;567568if (thermometer_stop(&thermometer))569return THERMOMETER_RUNTIME_ERROR;570571return THERMOMETER_SUCCESS;572}573574575