diff --git a/config.mk b/config.mk index c1d41bd87..753797946 100644 --- a/config.mk +++ b/config.mk @@ -35,7 +35,7 @@ ENABLE_WAYLAND= -DENABLE_WAYLAND endif # flags -DEFAULT_CPPFLAGS = -D_DEFAULT_SOURCE -DVERSION=\"${VERSION}\" -DSYSCONFDIR=\"${SYSCONFDIR}\" +DEFAULT_CPPFLAGS = -Wno-gnu-zero-variadic-macro-arguments -D_DEFAULT_SOURCE -DVERSION=\"${VERSION}\" -DSYSCONFDIR=\"${SYSCONFDIR}\" DEFAULT_CFLAGS = -g -std=gnu99 -pedantic -Wall -Wno-overlength-strings -Os ${ENABLE_WAYLAND} ${EXTRA_CFLAGS} DEFAULT_LDFLAGS = -lm -lrt diff --git a/docs/dunst.1.pod b/docs/dunst.1.pod index 9c85db33f..232bed423 100644 --- a/docs/dunst.1.pod +++ b/docs/dunst.1.pod @@ -100,38 +100,57 @@ missed notifications after returning to the computer. =head1 FILES These are the base directories dunst searches for configuration files in -descending order of imortance: +I: -=over 4 +=over 8 + +=item C<$XDG_CONFIG_HOME> + +This is the most important directory. (C<$HOME/.config> if unset or empty) -$XDG_CONFIG_HOME ($HOME/.config if unset or empty) +=item C<$XDG_CONFIG_DIRS> -$XDG_CONFIG_DIRS (:-separated list of base directories in descending order -of importance; ##SYSCONFDIR## if unset or empty) +This, like C<$PATH> for instance, is a :-separated list of base directories +in I. +(F<##SYSCONFDIR##> if unset or empty) =back -Dunst will search these directories for the following relative path: +Dunst will search these directories for the following relative file paths: -=over 4 +=over 8 -dunst/dunstrc +=item F -=back +This is the base config and as such the least important in a particular base +directory. -All settings from all files get applied. Settings in more important files -override those in less important ones. Settings not present in more imortant -files are taken from a less important one or from the internal defaults if they -are not found in any file. +=item F -=over 4 +These are "drop-ins" (mind the ".d" suffix of the directory). +They are more important than the base dunstrc in the parent directory, as they +are considered to be small snippets to override settings. +The last in lexical order is the most important one, so you can easily change +the order by renaming them. +A common approach to naming drop-ins is to prefix them with numbers, i.e.: -=item This is where the default (least important) config file is located: + 00-least-important.conf + 01-foo.conf + 20-bar.conf + 99-most-important.conf -##SYSCONFDIR##/dunst/dunstrc +The only requirements are that the name of the file must have the correct +suffix and the first non-blank line that is not a comment must be a section or +rule identifier. =back +All settings from all files get applied. Settings in more important files +override those in less important ones. +Settings not present in more imortant files get their value from a less +important one or from the internal defaults if respective key is not found in +any file. + =head1 AUTHORS Written by Sascha Kruse diff --git a/src/icon.c b/src/icon.c index 7e25f1cf3..e897c382f 100644 --- a/src/icon.c +++ b/src/icon.c @@ -13,11 +13,6 @@ #include "utils.h" #include "icon-lookup.h" -static bool is_readable_file(const char *filename) -{ - return (access(filename, R_OK) != -1); -} - /** * Reassemble the data parts of a GdkPixbuf into a cairo_surface_t's data field. * diff --git a/src/ini.c b/src/ini.c index ad4d9010b..3db157ed6 100644 --- a/src/ini.c +++ b/src/ini.c @@ -2,6 +2,7 @@ #include "utils.h" #include "log.h" +#include "settings.h" struct section *get_section(struct ini *ini, const char *name) { @@ -107,7 +108,7 @@ struct ini *load_ini_file(FILE *fp) *end = '\0'; g_free(current_section); - current_section = (g_strdup(start + 1)); + current_section = g_strdup(start + 1); continue; } diff --git a/src/log.h b/src/log.h index 7819b660b..54290d10c 100644 --- a/src/log.h +++ b/src/log.h @@ -17,24 +17,23 @@ * @... are the arguments to above format string. * * This requires -Wno-gnu-zero-variadic-macro-arguments with clang - * because __VA_OPTS__ seems not to be officially supported yet. - * However, the result is the same with both gcc and clang. + * because of token pasting ',' and %__VA_ARGS__ being a GNU extension. + * However, the result is the same with both gcc and clang and since we are + * compiling with '-std=gnu99', this should be fine. */ -#if __GNUC__ >= 8 - -#define MSG(format, ...) "[%s:%s:%04d] " format, __FILE__, __func__, __LINE__, ## __VA_ARGS__ +#if __GNUC__ >= 8 || __clang_major__ >= 6 +#define MSG(format, ...) "[%16s:%04d] " format, __func__, __LINE__, ## __VA_ARGS__ +#endif +#ifdef MSG // These should benefit from more context #define LOG_E(...) g_error(MSG(__VA_ARGS__)) #define LOG_C(...) g_critical(MSG(__VA_ARGS__)) #define LOG_D(...) g_debug(MSG(__VA_ARGS__)) - #else - #define LOG_E g_error #define LOG_C g_critical #define LOG_D g_debug - #endif #define LOG_W g_warning diff --git a/src/option_parser.c b/src/option_parser.c index 041107f98..902430678 100644 --- a/src/option_parser.c +++ b/src/option_parser.c @@ -213,7 +213,6 @@ int string_parse_bool(const void *data, const char *s, void *ret) return success; } - int get_setting_id(const char *key, const char *section) { int error_code = 0; int partial_match_id = -1; diff --git a/src/settings.c b/src/settings.c index 17cda7365..80642b2a9 100644 --- a/src/settings.c +++ b/src/settings.c @@ -1,7 +1,14 @@ /* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +/** @file src/settings.c + * @brief Take care of the settings. + */ + #include "settings.h" +#include +#include +#include #include #include #include @@ -16,91 +23,166 @@ #include "x11/x.h" #include "output.h" -#ifdef SYSCONFDIR -#define XDG_CONFIG_DIRS_DEFAULT SYSCONFDIR // alternative default -#else -#define XDG_CONFIG_DIRS_DEFAULT "/etc/xdg" +#ifndef SYSCONFDIR +/** @brief Fallback for doxygen, mostly. + * + * Since this gets defined by $DEFAULT_CPPFLAGS at compile time doxygen seems to + * miss the correct value. + */ +#define SYSCONFDIR "/usr/local/etc/xdg" #endif -// path to dunstrc, relative to config directory -#define RPATH_RC "dunst/dunstrc" -#define RPATH_RC_D RPATH_RC ".d/*.conf" +/** @brief Alternative default for XDG_CONFIG_DIRS + * + * Fix peculiar behaviour if installed to a local PREFIX, i.e. + * /usr/local, when SYSCONFDIR should be /usr/local/etc/xdg and not + * /etc/xdg, hence use SYSCONFDIR (defined at compile time, see + * config.mk) as default for XDG_CONFIG_DIRS. The spec says 'should' + * and not 'must' use /etc/xdg. Users/admins can override this by + * explicitly setting XDG_CONFIG_DIRS to their liking at runtime or by + * setting SYSCONFDIR=/etc/xdg at compile time. + */ +#define XDG_CONFIG_DIRS_DEFAULT SYSCONFDIR + +/** @brief Generate path to base config 'dunstrc' in a base directory + * + * @returns a newly-allocated gchar* string that must be freed with g_free(). + */ +#define BASE_RC(basedir) g_build_filename(basedir, "dunst", "dunstrc", NULL) + +/** @brief Generate drop-in directory path for a base directory + * + * @returns a newly-allocated gchar* string that must be freed with g_free(). + */ +#define DROP_IN_DIR(basedir) g_strconcat(BASE_RC(basedir), ".d", NULL) + +/** @brief Match pattern for drop-in file names */ +#define DROP_IN_PATTERN "*.conf" struct settings settings; -/** - * Tries to open all existing config files in *descending* order of importance. - * If cmdline_config_path is not NULL return early after trying to open the - * referenced file. +static const char * const *get_xdg_conf_basedirs(void); + +static int is_drop_in(const struct dirent *dent); + +static void get_conf_files(GQueue *config_files); + +/** @brief Filter for scandir(). * - * @param cmdline_config_path - * @returns GQueue* of FILE* to config files - * @retval empty GQueue* if no file could be opened + * @returns @brief An integer indicating success * - * Use g_queue_pop_tail() to retrieve FILE* in *ascending* order of importance + * @retval @brief 1 if file name matches #DROP_IN_PATTERN + * @retval @brief 0 otherwise * - * Use g_queue_free() to free if not NULL + * @param dent [in] @brief directory entry */ -static GQueue *open_conf_files(char *cmdline_config_path) { - FILE *config_file; - - // Used as stack, least important pushed last but popped first. - GQueue *config_files = g_queue_new(); - - if (cmdline_config_path) { - config_file = STR_EQ(cmdline_config_path, "-") - ? stdin - : fopen(cmdline_config_path, "r"); - - if (config_file) { - LOG_I(MSG_FOPEN_SUCCESS(cmdline_config_path, config_file)); - g_queue_push_tail(config_files, config_file); - } else { - // warn because we exit early - LOG_W(MSG_FOPEN_FAILURE(cmdline_config_path)); - } +static int is_drop_in(const struct dirent *dent) { + return 0 == fnmatch(DROP_IN_PATTERN, dent->d_name, FNM_PATHNAME | FNM_PERIOD) + ? 1 // success + : 0; +} - return config_files; // ignore other config files if '-conf' given +/** @brief Get all relevant config base directories + * + * Returns an array of all XDG config base directories, @e most @e important @e + * first. + * + * @returns A %NULL-terminated array of gchar* strings representing the paths + * of all XDG base directories in @e descending order of importance. + * + * The result @e must @e not be freed! The array is cached in a static variable, + * so it is OK to call this again instead of caching its return value. + */ +static const char * const *get_xdg_conf_basedirs() { + static const char * const *xdg_bd_arr = NULL; + if (!xdg_bd_arr) { + char * xdg_basedirs; + + const char * const xcd_env = getenv("XDG_CONFIG_DIRS"); + const char * const xdg_config_dirs = xcd_env && strnlen(xcd_env, 1) + ? xcd_env + : XDG_CONFIG_DIRS_DEFAULT; + + /* + * Prepend XDG_CONFIG_HOME, most important first because + * XDG_CONFIG_DIRS is already ordered that way. + */ + xdg_basedirs = g_strconcat(g_get_user_config_dir(), + ":", + xdg_config_dirs, + NULL); + LOG_D("Config directories: '%s'", xdg_basedirs); + + xdg_bd_arr = (const char * const *) string_to_array(xdg_basedirs, ":"); + g_free(xdg_basedirs); } + return xdg_bd_arr; +} - /* - * Fix peculiar behaviour if installed to a local PREFIX, i.e. - * /usr/local, when SYSCONFDIR should be /usr/local/etc/xdg and not - * /etc/xdg, hence use SYSCONFDIR (defined at compile time, see - * config.mk) as default for XDG_CONFIG_DIRS. The spec says 'should' and - * not 'must' use /etc/xdg. - * Users/admins can override this by explicitly setting XDG_CONFIG_DIRS - * to their liking at runtime or by setting SYSCONFDIR=/etc/xdg at - * compile time. - */ - gchar * const xdg_cdirs = g_strdup(g_getenv("XDG_CONFIG_DIRS")); - gchar * const xdg_config_dirs = xdg_cdirs && strnlen((gchar *) xdg_cdirs, 1) - ? g_strdup(xdg_cdirs) - : g_strdup(XDG_CONFIG_DIRS_DEFAULT); - g_free(xdg_cdirs); - - /* - * Prepend XDG_CONFIG_HOME, most important first because XDG_CONFIG_DIRS - * is already ordered that way. - */ - gchar * const all_conf_dirs = g_strconcat(g_get_user_config_dir(), ":", - g_strdup(xdg_config_dirs), NULL); - g_free(xdg_config_dirs); - LOG_D("Config directories: '%s'", all_conf_dirs); - - for (gchar * const *d = string_to_array(all_conf_dirs, ":"); *d; d++) { - gchar * const path = string_to_path(g_strconcat(*d, "/", RPATH_RC, NULL)); - if ((config_file = fopen(path, "r"))) { - LOG_I(MSG_FOPEN_SUCCESS(path, config_file)); - g_queue_push_tail(config_files, config_file); +/** @brief Find all config files. + * + * Searches all XDG config base directories for config files and drop-ins and + * puts them in a GQueue, @e least important last. + * + * @param config_files [in|out] A pointer to a GQueue of gchar* strings + * representing config file paths + * + * Use g_queue_pop_tail() to retrieve paths in @e ascending order of + * importance, or use g_queue_reverse() before iterating over it with + * g_queue_for_each(). + * + * Use g_free() to free the retrieved elements of the queue. + * + * Use g_queue_free() to free after emptying it or g_queue_free_full() for a + * non-empty queue. + */ +static void get_conf_files(GQueue *config_files) { + struct dirent **drop_ins; + for (const char * const *d = get_xdg_conf_basedirs(); *d; d++) { + /* absolute path to the base rc-file */ + gchar * const base_rc = BASE_RC(*d); + /* absolute path to the corresponding drop-in directory */ + gchar * const drop_in_dir = DROP_IN_DIR(*d); + + int n = scandir(drop_in_dir, &drop_ins, is_drop_in, alphasort); + /* reverse order to get most important first */ + while (n-- > 0) { + gchar * const drop_in = g_strconcat(drop_in_dir, + "/", + drop_ins[n]->d_name, + NULL); + free(drop_ins[n]); + + if (is_readable_file(drop_in)) { + LOG_D("Adding drop-in '%s'", drop_in); + g_queue_push_tail(config_files, drop_in); + } else + g_free(drop_in); + } + g_free(drop_in_dir); + + /* base rc-file last, least important */ + if (is_readable_file(base_rc)) { + LOG_D("Adding base config '%s'", base_rc); + g_queue_push_tail(config_files, base_rc); } else - // debug level because of low relevance - LOG_D(MSG_FOPEN_FAILURE(path)); - g_free(path); + g_free(base_rc); } - g_free(all_conf_dirs); + free(drop_ins); +} + +FILE *fopen_conf(char * const path) +{ + FILE *f = NULL; + char *real_path = string_to_path(strdup(path)); - return config_files; + if (is_readable_file(real_path) && (f = fopen(real_path, "r"))) + LOG_I(MSG_FOPEN_SUCCESS(path, f)); + else + LOG_W(MSG_FOPEN_FAILURE(path)); + + free(real_path); + return f; } void settings_init() { @@ -178,34 +260,52 @@ void check_and_correct_settings(struct settings *s) { } -void load_settings(char *cmdline_config_path) { - FILE *config_file; - GQueue *config_files; +static void process_conf_file(const gpointer conf_fname, gpointer n_success) { + const gchar * const p = conf_fname; + + LOG_D("Trying to open '%s'", p); + /* Check for "-" here, so the file handling stays in one place */ + FILE *f = STR_EQ(p, "-") ? stdin : fopen_verbose(p); + if (!f) + return; + LOG_I("Parsing config, fd: '%d'", fileno(f)); + struct ini *ini = load_ini_file(f); + LOG_D("Closing config, fd: '%d'", fileno(f)); + fclose(f); + + LOG_D("Loading settings"); + save_settings(ini); + + LOG_D("Checking/correcting settings"); + check_and_correct_settings(&settings); + + finish_ini(ini); + free(ini); + + ++(*(int *) n_success); +} + +void load_settings(const char * const path) { settings_init(); LOG_D("Setting defaults"); set_defaults(); - config_files = open_conf_files(cmdline_config_path); - if (g_queue_is_empty(config_files)) { - LOG_W("No configuration file, using defaults"); - } else { // Add entries from all files, most important last - while ((config_file = g_queue_pop_tail(config_files))) { - LOG_I("Parsing config, fd: '%d'", fileno(config_file)); - struct ini *ini = load_ini_file(config_file); - fclose(config_file); + GQueue *conf_files = g_queue_new(); - LOG_D("Loading settings"); - save_settings(ini); + if (path) /** If @p path [in] was supplied it will be the only one tried. */ + g_queue_push_tail(conf_files, g_strdup(path)); + else + get_conf_files(conf_files); - LOG_D("Checking/correcting settings"); - check_and_correct_settings(&settings); + /* Load all conf files and drop-ins, least important first. */ + g_queue_reverse(conf_files); /* so we can iterate from least to most important */ + int n_loaded_confs = 0; + g_queue_foreach(conf_files, process_conf_file, &n_loaded_confs); + g_queue_free_full(conf_files, g_free); - finish_ini(ini); - free(ini); - } - } - g_queue_free(config_files); + if (0 == n_loaded_confs) + LOG_W("No configuration file, using defaults"); for (GSList *iter = rules; iter; iter = iter->next) { struct rule *r = iter->data; diff --git a/src/settings.h b/src/settings.h index 80d84927c..d23585483 100644 --- a/src/settings.h +++ b/src/settings.h @@ -161,7 +161,7 @@ struct settings { extern struct settings settings; -void load_settings(char *cmdline_config_path); +void load_settings(const char * const path); #endif /* vim: set ft=c tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/src/utils.c b/src/utils.c index 07ca9189b..f337cb6fd 100644 --- a/src/utils.c +++ b/src/utils.c @@ -6,8 +6,11 @@ #include #include #include +#include #include #include +#include +#include #include #include @@ -406,4 +409,40 @@ char *string_strip_brackets(const char* s) { } +/* see utils.h */ +bool is_readable_file(const char * const path) +{ + struct stat statbuf; + bool result = false; + + if (0 == stat(path, &statbuf)) { + /** See what intersting stuff can be done with FIFOs */ + if (!(statbuf.st_mode & (S_IFIFO | S_IFREG))) { + /** Sets errno if stat() was successful but @p path [in] + * does not point to a regular file or FIFO. This + * just in case someone queries errno which would + * otherwise indicate success. */ + errno = EINVAL; + } else if (0 == access(path, R_OK)) { /* must also be readable */ + result = true; + } + } + + return result; +} + +FILE *fopen_verbose(const char * const path) +{ + FILE *f = NULL; + char *real_path = string_to_path(strdup(path)); + + if (is_readable_file(real_path) && (f = fopen(real_path, "r"))) + LOG_I(MSG_FOPEN_SUCCESS(path, f)); + else + LOG_W(MSG_FOPEN_FAILURE(path)); + + free(real_path); + return f; +} + /* vim: set ft=c tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/src/utils.h b/src/utils.h index 84bc660aa..70d1db132 100644 --- a/src/utils.h +++ b/src/utils.h @@ -3,8 +3,9 @@ #define DUNST_UTILS_H #include -#include #include +#include +#include //! Test if a string is NULL or empty #define STR_EMPTY(s) (!s || (*s == '\0')) @@ -198,5 +199,37 @@ char *string_strip_brackets(const char* s); * Returns the length of a string array, -1 if the input is NULL. */ int string_array_length(char **s); + +/** @brief Check if file is readable + * + * This is a convenience function to check if @p path can be resolved and makes + * sense to try and open, like regular files and FIFOs (named pipes). Finally, + * a preliminary check is done to see if read permission is granted. + * + * Do not rely too hard on the result, though, since this is racy. A case can + * be made that these conditions might not be true anymore by the time the file + * is acutally opened for reading. + * + * Also, no tilde expansion is done. Use the result of `string_to_path()` for + * @p path. + * + * @param path [in] A string representing a path. + * @retval true in case of success. + * @retval false in case of failure, errno will be set appropriately. + */ +bool is_readable_file(const char * const path); + +/** @brief Open files verbosely. + * + * This is a wrapper around fopen() and is_readable_file() that does some + * preliminary checks and sends log messages. + * + * @p path [in] A char* string representing a filesystem path + * @returns The result of the fopen() call. + * @retval NULL if the fopen() call failed or @p path does not satisfy the + * conditions of is_readable_file(). + */ +FILE * fopen_verbose(const char * const path); + #endif /* vim: set ft=c tabstop=8 shiftwidth=8 expandtab textwidth=0: */