/*
* Copyright 2016-2017 China Mobile, Inc.
*
* This file is licensed to you under your choice of the GNU Lesser
* General Public License, version 2.1 or any later version (LGPLv2.1 or
* later), or the Apache License 2.0.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <ctype.h>
#include <stdbool.h>
#include "darray.h"
#include "libtcmu_config.h"
#include "libtcmu_log.h"
#include "libtcmu_common.h"
#include "ccan/list/list.h"
#define TCMU_CONFIG_DIR_DEFAULT "/etc/tcmu"
#define TCMU_CONFIG_FILE_DEFAULT TCMU_CONFIG_DIR_DEFAULT"/tcmu.conf"
typedef enum {
TCMU_OPT_NONE = 0,
TCMU_OPT_INT, /* type int */
TCMU_OPT_STR, /* type string */
TCMU_OPT_BOOL, /* type boolean */
TCMU_OPT_MAX,
} tcmu_option_type;
struct tcmu_conf_option {
struct list_node list;
char *key;
tcmu_option_type type;
union {
int opt_int;
bool opt_bool;
char *opt_str;
};
};
/*
* System config for TCMU, for now there are only 3 option types supported:
* 1, The "int type" option, for example:
* log_level = 2
*
* 2, The "string type" option, for example:
* tcmu_str = "Tom" --> Tom
* or
* tcmu_str = 'Tom' --> Tom
* or
* tcmu_str = 'Tom is a "boy"' ---> Tom is a "boy"
* or
* tcmu_str = "'T' is short for Tom" --> 'T' is short for Tom
*
* 3, The "boolean type" option, for example:
* tcmu_bool
*
* ========================
* How to add new options ?
*
* Using "log_level" as an example:
*
* 1, Add log_level member in:
* struct tcmu_config {
* int log_level;
* };
* in file libtcmu_config.h.
*
* 2, Add the following option in "tcmu.conf" file as default:
* log_level = 2
* or
* # log_level = 2
*
* Note: the option name in config file must be the same as in
* tcmu_config.
*
* 3, You should add your own set method in:
* static void tcmu_conf_set_options(struct tcmu_config *cfg)
* {
* TCMU_PARSE_CFG_INT(cfg, log_level);
* TCMU_CONF_CHECK_LOG_LEVEL(log_level);
* }
*
* Note: For now, if the options have been changed in config file, the
* system config reload thread daemon will try to update them for all the
* tcmu-runner, consumer and tcmu-synthesizer daemons.
*/
static LIST_HEAD(tcmu_options);
static struct tcmu_conf_option * tcmu_get_option(const char *key)
{
struct tcmu_conf_option *option;
list_for_each(&tcmu_options, option, list) {
if (!strcmp(option->key, key))
return option;
}
return NULL;
}
static struct tcmu_conf_option *
tcmu_register_option(char *key, tcmu_option_type type)
{
struct tcmu_conf_option *option;
option = calloc(1, sizeof(*option));
if (!option)
return NULL;
option->key = strdup(key);
if (!option->key)
goto free_option;
option->type = type;
list_node_init(&option->list);
list_add_tail(&tcmu_options, &option->list);
return option;
free_option:
free(option);
return NULL;
}
/* The default value should be specified here,
* so the next time when users comment out an
* option in config file, here it will set the
* default value back.
*/
#define TCMU_PARSE_CFG_INT(cfg, key) \
do { \
struct tcmu_conf_option *option; \
option = tcmu_get_option(#key); \
if (!option) { \
option = tcmu_register_option(#key, TCMU_OPT_INT); \
cfg->key = cfg->def_##key; \
} else { \
cfg->key = option->opt_int; \
} \
option->opt_int = cfg->def_##key;\
} while (0)
#define TCMU_PARSE_CFG_BOOL(cfg, key) \
do { \
struct tcmu_conf_option *option; \
option = tcmu_get_option(#key); \
if (!option) { \
option = tcmu_register_option(#key, TCMU_OPT_BOOL); \
cfg->key = cfg->def_##key; \
} else { \
cfg->key = option->opt_bool; \
} \
option->opt_bool = cfg->def_##key; \
} while (0)
#define TCMU_PARSE_CFG_STR(cfg, key) \
do { \
struct tcmu_conf_option *option; \
option = tcmu_get_option(#key); \
memset(cfg->key, 0, sizeof(cfg->key)); \
if (!option) { \
option = tcmu_register_option(#key, TCMU_OPT_STR); \
snprintf(cfg->key, sizeof(cfg->key), "%s", cfg->def_##key); \
} else { \
snprintf(cfg->key, sizeof(cfg->key), "%s", option->opt_str); \
if (option->opt_str) \
free(option->opt_str); \
} \
option->opt_str = strdup(cfg->def_##key); \
} while (0);
static void tcmu_conf_set_options(struct tcmu_config *cfg)
{
/* set log_level option */
TCMU_PARSE_CFG_INT(cfg, log_level);
tcmu_set_log_level(cfg->log_level);
/* set log_dir path option */
TCMU_PARSE_CFG_STR(cfg, log_dir);
tcmu_resetup_log_file(cfg, cfg->log_dir);
/* add your new config options */
}
#define TCMU_MAX_CFG_FILE_SIZE (2 * 1024 * 1024)
static int tcmu_read_config(int fd, char *buf, int count)
{
ssize_t len;
int save = errno;
do {
len = read(fd, buf, count);
} while (errno == EAGAIN);
errno = save;
return len;
}
/* end of line */
#define __EOL(c) (((c) == '\n') || ((c) == '\r'))
#define TCMU_TO_LINE_END(x, y) \
do { while ((x) < (y) && !__EOL(*(x))) { \
(x)++; } \
} while (0);
/* skip blank lines */
#define TCMU_SKIP_BLANK_LINES(x, y) \
do { while ((x) < (y) && (isblank(*(x)) || __EOL(*(x)))) { \
(x)++; } \
} while (0);
/* skip comment line with '#' */
#define TCMU_SKIP_COMMENT_LINE(x, y) \
do { while ((x) < (y) && !__EOL(*x)) { \
(x)++; } \
(x)++; \
} while (0);
/* skip comment lines with '#' */
#define TCMU_SKIP_COMMENT_LINES(x, y) \
do { while ((x) < (y) && *(x) == '#') { \
TCMU_SKIP_COMMENT_LINE((x), (y)); } \
} while (0);
static void tcmu_parse_option(char **cur, const char *end)
{
struct tcmu_conf_option *option;
tcmu_option_type type;
char *p = *cur, *q = *cur, *r, *s;
while (isblank(*p))
p++;
TCMU_TO_LINE_END(q, end);
*q = '\0';
*cur = q + 1;
/* parse the boolean type option */
s = r = strchr(p, '=');
if (!r) {
/* boolean type option at file end or line end */
r = p;
while (!isblank(*r) && r < q)
r++;
*r = '\0';
option = tcmu_get_option(p);
if (!option)
option = tcmu_register_option(p, TCMU_OPT_BOOL);
if (option)
option->opt_bool = true;
return;
}
/* skip character '=' */
s++;
r--;
while (isblank(*r))
r--;
r++;
*r = '\0';
option = tcmu_get_option(p);
if (!option) {
r = s;
while (isblank(*r))
r++;
if (isdigit(*r))
type = TCMU_OPT_INT;
else
type = TCMU_OPT_STR;
option = tcmu_register_option(p, type);
if (!option)
return;
}
/* parse the int/string type options */
switch (option->type) {
case TCMU_OPT_INT:
while (!isdigit(*s))
s++;
r = s;
while (isdigit(*r))
r++;
*r= '\0';
option->opt_int = atoi(s);
break;
case TCMU_OPT_STR:
while (isblank(*s))
s++;
/* skip first " or ' if exist */
if (*s == '"' || *s == '\'')
s++;
r = q - 1;
while (isblank(*r))
r--;
/* skip last " or ' if exist */
if (*r == '"' || *r == '\'')
*r = '\0';
if (option->opt_str)
/* free if this is reconfig */
free(option->opt_str);
option->opt_str = strdup(s);
break;
default:
tcmu_err("option type %d not supported!\n", option->type);
break;
}
}
static void tcmu_parse_options(struct tcmu_config *cfg, char *buf, int len)
{
char *cur = buf, *end = buf + len;
while (cur < end) {
/* skip blanks lines */
TCMU_SKIP_BLANK_LINES(cur, end);
/* skip comments with '#' */
TCMU_SKIP_COMMENT_LINES(cur, end);
if (cur >= end)
break;
if (!isalpha(*cur))
continue;
/* parse the options from config file to tcmu_options[] */
tcmu_parse_option(&cur, end);
}
/* parse the options from tcmu_options[] to struct tcmu_config */
tcmu_conf_set_options(cfg);
}
int tcmu_load_config(struct tcmu_config *cfg)
{
int ret = -1;
int fd, len;
char *buf;
int i;
buf = malloc(TCMU_MAX_CFG_FILE_SIZE);
if (!buf)
return -ENOMEM;
for (i = 0; i < 5; i++) {
if ((fd = open(TCMU_CONFIG_FILE_DEFAULT, O_RDONLY)) == -1) {
/* give a moment for editor to restore
* the conf-file after edit and save */
sleep(1);
continue;
}
break;
}
if (fd == -1) {
tcmu_err("Failed to open file '%s', %m\n",
TCMU_CONFIG_FILE_DEFAULT);
goto free_buf;
}
len = tcmu_read_config(fd, buf, TCMU_MAX_CFG_FILE_SIZE);
close(fd);
if (len < 0) {
tcmu_err("Failed to read file '%s'\n", TCMU_CONFIG_FILE_DEFAULT);
goto free_buf;
}
buf[len] = '\0';
tcmu_parse_options(cfg, buf, len);
ret = 0;
free_buf:
free(buf);
return ret;
}
#define BUF_LEN 1024
static void *dyn_config_start(void *arg)
{
struct tcmu_config *cfg = arg;
int monitor, wd, len;
char buf[BUF_LEN];
char *p;
tcmu_set_thread_name("dyn-config", NULL);
monitor = inotify_init();
if (monitor == -1) {
tcmu_err("Failed to init inotify %m\n");
return NULL;
}
/* Editors (vim, nano ..) follow different approaches to save conf file.
* The two commonly followed techniques are to overwrite the existing
* file, or to write to a new file (.swp, .tmp ..) and move it to actual
* file name later. In the later case, the inotify fails, because the
* file it's been intended to watch no longer exists, as the new file
* is a different file with just a same name.
* To handle both the file save approaches mentioned above, it is better
* we watch the directory and filter for MODIFY events.
*/
wd = inotify_add_watch(monitor, TCMU_CONFIG_DIR_DEFAULT, IN_MODIFY);
if (wd == -1) {
tcmu_err("Failed to add \"%s\" to inotify %m\n",
TCMU_CONFIG_DIR_DEFAULT);
return NULL;
}
tcmu_dbg("Inotify is watching \"%s\", wd: %d, mask: IN_MODIFY\n",
TCMU_CONFIG_DIR_DEFAULT, wd);
while (1) {
struct inotify_event *event;
len = read(monitor, buf, BUF_LEN);
if (len == -1) {
tcmu_warn("Failed to read inotify: %m\n");
continue;
}
for (p = buf; p < buf + len;
p += sizeof(struct inotify_event) + event->len) {
event = (struct inotify_event *)p;
tcmu_dbg("event->mask: 0x%x\n", event->mask);
if (event->wd != wd)
continue;
/* Try to reload the config file */
if (event->mask & IN_MODIFY)
tcmu_load_config(cfg);
}
}
return NULL;
}
int tcmu_watch_config(struct tcmu_config *cfg)
{
int ret;
ret = pthread_create(&cfg->thread_id, NULL, dyn_config_start, cfg);
if (ret)
return -ret;
return 0;
}
void tcmu_unwatch_config(struct tcmu_config *cfg)
{
tcmu_thread_cancel(cfg->thread_id);
}
struct tcmu_config* tcmu_initialize_config(void)
{
struct tcmu_config *cfg;
char *log_dir;
cfg = calloc(1, sizeof(*cfg));
if (cfg == NULL) {
tcmu_err("allocating TCMU config failed: %m\n");
errno = ENOMEM;
return NULL;
}
log_dir = getenv("TCMU_LOGDIR");
snprintf(cfg->def_log_dir, PATH_MAX, "%s",
log_dir ? log_dir : TCMU_LOG_DIR_DEFAULT);
cfg->def_log_level = TCMU_CONF_LOG_INFO;
return cfg;
}
void tcmu_free_config(struct tcmu_config *cfg)
{
struct tcmu_conf_option *option, *next;
if (!cfg)
return;
list_for_each_safe(&tcmu_options, option, next, list) {
list_del(&option->list);
if (option->type == TCMU_OPT_STR)
free(option->opt_str);
free(option->key);
free(option);
}
free(cfg);
}