-
Notifications
You must be signed in to change notification settings - Fork 342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for config drop-ins (rudimentary includes as a bonus) #933
Conversation
Codecov Report
@@ Coverage Diff @@
## master #933 +/- ##
==========================================
- Coverage 60.33% 60.17% -0.16%
==========================================
Files 42 42
Lines 6416 6456 +40
==========================================
+ Hits 3871 3885 +14
- Misses 2545 2571 +26
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
Commit messages and code should now be in sync. Especially ini.c was missing the fix for section names surrounded by spaces, which fell by the wayside when resolving a conflict while rebasing against upstream. |
Added a minor fix for the doxygen comment of |
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't go through everything of the last commit yet, but here are my initial thoughts.
I think that a drop-in directory would be an OK alternative to @include. I'm not sure if it's much easier to implement, though. If we implement @include support, you don't need to do anything to implement a drop-in dir, since you can just write
@INCLUDE ./conf.d/*.conf
.
About the drop-in directory location: I believe that it should be basedir/dunst/conf.d/
. Most programs I see on my system are putting the conf.d directory in the same directory as their main config (in our case the config is in the directory dunst, so conf.d should be there as well).
In the current review comments I looked a bit too much at the implementation details, but I'll think some more about the big picture and get back to you about that. So don't look too much into the the small comments yet.
src/settings.c
Outdated
#define G_DROPIN_DIR(basedir) G_STRCONCAT(G_BASE_RC(basedir), ".d") | ||
|
||
/** @brief Match pattern for drop-in file names */ | ||
#define DROPIN_PAT "*.conf" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
spelling: DROPIN_PAT -> DROPIN_PATH. I would maybe call it DROP_IN_PATH, though, since dropin isn't a word
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
spelling: DROPIN_PAT -> DROPIN_PATH.
Nope, it is a pattern, not a path. ;) I thought it was obvious and wanted to save some line space and keystrokes.
I would maybe call it DROP_IN_PAT
H, though, since dropin isn't a word
Fair point, I had similar thoughts while I was at it. I will rename it. Should I use ..._PATTERN
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This macro isn't neccesary anyways. It's only used in 1 place. Please move the string to is_drop_in.
Yes, that is technically true, in a way, but the drop-in code hooks nicely into the already present implementation of #917, which is kind of the point. And, as I said, you cannot get headaches about loops because we rely on filesystem features which protect against those--thinking about symlink loops here.
But it is kind of a side effect of #917 to get an easy in for drop-ins. Includes are somewhat orthogonal to that, because you need to open and preprocess/parse the files to make them work. Hence my remark on robustness: drop-ins are basically just some filesystem 'magic'.
My experience is the opposite. Most of the time I see something like
👍 |
If you think it's easier to implement drop-in dirs that's fine. We can always add
I'm sorry, I was under the impression that the location in your code was for example |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the rest of my review. In short, please don't use macros when not neccesary. You may also drop the @INCLUDE
code. Once you've addressed my comments, I think this PR is in a pretty good state.
src/settings.c
Outdated
#define G_DROPIN_DIR(basedir) G_STRCONCAT(G_BASE_RC(basedir), ".d") | ||
|
||
/** @brief Match pattern for drop-in file names */ | ||
#define DROPIN_PAT "*.conf" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This macro isn't neccesary anyways. It's only used in 1 place. Please move the string to is_drop_in.
src/settings.c
Outdated
FILE *config_file; | ||
static int is_dropin(const struct dirent *dent) | ||
{ | ||
return 0 == fnmatch(DROPIN_PAT, dent->d_name, FNM_PATHNAME | FNM_PERIOD) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return 0 == fnmatch(DROPIN_PAT, dent->d_name, FNM_PATHNAME | FNM_PERIOD) | |
return 0 == fnmatch("*.conf", dent->d_name, FNM_PATHNAME | FNM_PERIOD) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This macro isn't neccesary anyways. It's only used in 1 place.
Technically, that is not true because it is also used in the doxygen comment. ;) I was just about to make the change you requested, when I realized that there are two places that need changing. I'd really rather use symbolic names wherever possible. Using a hardcoded string constant "buried" in a function just doesn't feel right.
Edit
Let me quote from the old K&R to drive this home ;)
C makes the use of functions easy, convenient and efficient; you will often see a short function defined
and called only once, just because it clarifies some piece of code.
The same goes for this symbolic name (not a macro, BTW), which only serves to hide the gritty details which are irrelevant in is_drop_in()
. So the fact that it is only used once, does not make it useless.
If it helps I renamed it to DROP_IN_PATTERN
in my local repo, already. :)
Edit2
It's bad practice to bury "magic numbers" like 300 and 20 in a program; they convey little information to someone who might have to read the program later, and they are hard to change In a systematic way. One way to deal with magic numbers is to give them meaningful names. A #define line defines a symbolic name or symbolic constant to be a particular string of characters:
#define name replacement text
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, the macro makes the function harder to read, since you have to scroll up to the definition of the macro to see what the actual search path is.
It's bad practice to bury "magic numbers" like 300 and 20 in a program; they convey little information to someone who might have to read the program later, and they are hard to change In a systematic way. One way to deal with magic numbers is to give them meaningful names. A #define line defines a symbolic name or symbolic constant to be a particular string of characters:
#define name replacement text
To use your example. In the case of numbers it's much less clear what's happening, since a number like 20 doesn't say much. The string "*.conf"
that's passed to the function fnmatch
makes it completely clear what's happening: you're trying to see if the filename ends with .conf
. When you replace it with DROP_IN_PATTERN, the reader might think 2 things:
- the function matches "*.conf"
- the function matches "dunstrc.d/*.conf"
In the same line there is an example of good usage of a macro (gcc calls them macros), namely the flags given to fnmatch
: FNM_PATHNAME | FNM_PERIOD
. You could also use some number instead, but that wouldn't say anything and you would have to look at some other place to see what those numbers mean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, the macro makes the function harder to read, since you have to scroll up to the definition of the macro to see what the actual search path is.
Again, not a path. ;)
And what you describe is kind of the point, actually. You should not need to look because it is irrelevant. All you need to know is that it is looking for the drop-in pattern (DROP_IN_PATTERN). The real value of that pattern is unimportant. You do not go searching for the the real values of symbolic constants like TRUE, FALSE, or EINVAL, or whatever, either, do you?
It's bad practice to bury "magic numbers" like 300 and 20 in a program; they convey little information to someone who might have to read the program later, and they are hard to change In a systematic way. One way to deal with magic numbers is to give them meaningful names. A #define line defines a symbolic name or symbolic constant to be a particular string of characters:
#define name replacement textTo use your example. In the case of numbers it's much less clear what's happening, since a number like 20 doesn't say much.
The example only talks about numbers because it is a verbatim copy from Kernighan's & Ritchie's "The C Programming Language".
The string
"*.conf"
that's passed to the functionfnmatch
makes it completely clear what's happening: you're trying to see if the filename ends with.conf
. When you replace it with DROP_IN_PATTERN, the reader might think 2 things:* the function matches "*.conf" * the function matches "dunstrc.d/*.conf"
Why? This is a filter for scandir
which cannot return relative paths but only the entries of a directory listing--it cannot look into sub directories. Again, that is nothing that the reader should be concerned about. Think of it in terms of encapsulation: you do not need to know. The function name and its documentation says it all, hopefully--if not, the documentation should be fixed, not the macro. Needing to know about the inner workings of a function is a symptom of bad encapsulation and smells. ;)
And if you had to go looking it up, try the documentation produced by doxygen, which neatly links to DROP_IN_PATTERN, which it couldn't if it wasn't defined this way. I know that you do not use it, but maybe you should consider doing so. At least take a look at it to see what I mean, please.
In the same line there is an example of good usage of a macro (gcc calls them macros)
OK, 'tomato tomahto'. ;)
namely the flags given to
fnmatch
:FNM_PATHNAME | FNM_PERIOD
. You could also use some number instead, but that wouldn't say anything and you would have to look at some other place to see what those numbers mean.
Yes, but why is my macro any less useful?
* 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This variable doesn't need to be defined file-wide. Please move it to the place where it's used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A similar issue here. If I move this to get_xdg_basedirs()
, the doxygen comment then becomes part of that function's comment. While I was writing that code it just seemed out of place and not necessary to have there, which is why I made it a symbolic constant.
Also, this being a preprocessor directive makes it file-wide anyways at least from the line of its definition onwards.
P.S.: vim
also wants to indent the comment, since it is inside a function. :/
About #947:
Did you mean this?
BTW, there is P.S.: One thing just sprang to mind, which I thought of while writing |
src/settings.c
Outdated
* 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_basedirs(void); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This definition isn't needed as far as I can tell
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See below.
src/settings.c
Outdated
|
||
static int is_dropin(const struct dirent *dent); | ||
|
||
static bool fexists(char * const path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These definitions aren't needed either
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are just the prototypes, and I took another file from Dunst's codebase as an example--can't remember which, though. Having these in there makes for more flexibility below, since one can write the actual function definitions in arbitrary order, as opposed to "define before call".
I'd argue that, since load_settings()
is the only real public function and somewhat the main()
of this file, it should be at the top, after the prototypes that is. But it is at the bottom now, because someone didn't bother to copy its signature to define the prototype. ;)
yes
So what I'm asking is, is to replace most of the
The environment variable itself doesn't default to some value. When it's empty, that value should indeed be used. To make it more confusing, when the spec says a file is located in
Yep that's definitely a good option. The only place where that's used (in master) is in icon.c to check if the icon file is readable. I don't expect many icon themes to include FIFO's, but I don't think it would hurt to allow them.
Yeah that's a good point. It isn't too big of a problem in the icon code for a few reasons, but it's better to not allow directories in the first place.
There isn't much of a security problem in this case, since dunst just runs as a regular user. It doesn't need to do privilege dropping/escalation either (although it might provide a very small security benefit). No privilege dropping is in the pipeline. |
I think you should reconsider, since you are basically reinventing the wheel with
No, not if you want to be compliant with the XDG BD spec. It states a recommended default which you are, in fact, using here. What's the point, if there are readily available functions for doing that? And what would you expect to find, if you were to actually use an empty
Not confusing at all, to me. |
Look at the old code WRT Edit: corrected the URL to the old code |
The problem is that I need a subdirectory of I'm also considering contributing the icon lookup code to mako, so I want to stay as independent from glib as possible in that part (the list is easy enough to replace). |
Yes, of course but you don't need to, and most probably should not, do that in one go. The spec is called "XDG Base Directory Specification" for a reason. You get the base directory and append the appropriate relative path(s) after. That is what
It was only meant as an example of what
Yes, that is one special case for backwards compatibility. You can just prepend it to the array and drop that part of the code, once backwards compatibility is no longer required, or whatever. All the other directories are specified in the XDG BD spec. I have cloned your fork of Dunst and was about to prepare a PR with a PoC but, for the time being, let me try some quick and dirty pseudo code to demonstrate:
(or something along those lines, haven't had time to flesh out the details and have not fully understood the spec yet, but you should get the gist of it, hopefully. The This way you can reuse
Why is it annoying? It gets used in other places as well. I find it quite convenient, so why not use it when it is a requirement for Dunst, anyway? Even your code for #947 uses it for other stuff, i.e. GPtrArray. Cannot have it both ways. ;)
That is a good point, but then you would also need to rewrite any code that uses Glib, to make it portable to non-Glib projects. It's not the worst idea, though. Let me think a little more about it. On a related note: is there another channel for communication? This does not seem like the right place. A mailing list would be great, or maybe IRC, even though I am not a frequent IRC user. |
I'll play around with the code some more, while finishing #947.
I'm about in the same timezone as you, so IRC could be an option for real-time communication. I'm not very often online on there, though, so you would have to message me beforehand. Important conclusions should be posted to github, however. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// debug level because of low relevance | ||
LOG_D(MSG_FOPEN_FAILURE(path)); | ||
g_free(path); | ||
g_free(base_rc); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: when the if statement uses brackets the else statement should as well
save_settings(ini); | ||
|
||
LOG_D("Checking/correcting settings"); | ||
check_and_correct_settings(&settings); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function should only be done once, after all the config files are loaded
fclose(f); | ||
|
||
LOG_D("Loading settings"); | ||
save_settings(ini); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
load_ini_file
may return NULL. You should handle that case correctly. I'd make save_settings
return immediately when the ini file is NULL.
* non-empty queue. | ||
*/ | ||
static void get_conf_files(GQueue *config_files) { | ||
struct dirent **drop_ins; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be initialized to NULL. Otherwise my linter (rightfully) complains about the free statement below.
I've reconsidered the decision to read every config file and apply them one after the other. Apart from if it conforms better to the standards, let's consider the user benefits.
Benefits of applying only the last config (let's ignore drop-ins for now):
To me the benefits of easy backward compatibility, while still providing the best defaults for new users is very nice, while I don't think there are many benefits with applying all configs. What are your thoughts?
Also, we need to think about this either way, since the drop-ins have this problem as well. We could make all rules from other files independent by adding something like a filename attribute to each rule. I think this is necessary to avoid confusion from users. |
I've also taken a look at the doxygen output and I do agree that the separate macros might make the doxygen output a bit clearer. But I don't think this should go over the code readability. I found myself scrolling back to the macro definitions way too often, even just while reviewing this PR. So I do stand by my point of removing at least the macros
The relevant comments may be moved to the function description or somewhere in the function. Indented comments inside functions are not a problem to me. |
Also, could you add some tests for the added functions, where possible. This comes with the added bonus of valgrind testing for memleaks. |
@WhitePeter have you had some time to look at this? |
@WhitePeter Since you don't seem to have the time to finish this, I'll go ahead an do that (keeping your commit attribution intact, of course). EDIT messed a bit with rebasing, but feel free to overwrite. I'll go ahead an continue in another branch. |
Dial back a little on the space requirement of the prefix for messages of levels "debug", "error" and "critical". The prefix now only includes the function name, padded to 16 characters so as to make for better vertical alignment, and the line number in the source file. Additionally updated feature test macro to make this work with recent enough versions of clang as well. config.mk: Disable warnings about empty __VA_ARGS__ Since compilation is done with '-std=gnu99' and token pasting of ',' and '__VA_ARGS__' is a GNU extension which also works with clang (>=6) this warning should be safe to disable, see also the changes to log.h above. The CI did not complain in my local tests. Only clang threw this warning before and only to inform that it is indeed a GNU extension, despite using the same '-std=gnu99'. This should not mask genuine errors when __VA_ARGS__ must not be empty, i.e. usage without token pasting it with ',', since those should be errors that lead to unsuccessful compilation, anyway.
This is basically a proof of concept, so not much official documentation just yet. Include syntax is a little more strict. The keyword is "@include", which is borrowed from doxygen. There MUST not be any blanks before the "@", like in C. Some reasoning for these preliminary decisions: Includes should stand out and thus should get some kind of special character prefix, but since '#' is already taken, they could be mistaken for comments, especially with syntax highlighting enabled. Using all capitol letters and not allowing spaces in front also serves to make them more distinguishable. Drive-by fix: Sections surrounded by spaces are no longer possible.
Also some refinements to the FILES section.
Refactoring for necessarily added complexity and push file handling into 'load_settings()', because otherwise there would be virtually no limit on how many files could be open at the same time, while we only read them sequentially, anyway.
Also reverted stripping of section names.
A more thorough check of what constitutes as *useable* and readable file.
Moved from settings and renamed, a small wrapper around `is_readable_file()` and `fopen()`.
Do away with the 'G_*' prefix of macros because it is misleading. The original intention was to make clear that they are derived from Glib functions but instead they might be mistaken for *actual* Glib content. Also added some more documentation for clarity.
Also fixed a double free issue with the provided path parameter. Removed leftover prototype of fopen_conf().
This PR is now in a non-compilable state, sorry for that. It doesn't really matter, though, since I am finishing it in #997. Closing this one, as it's not needed anymore. |
This PR introduces support for drop-ins--those files (or snippets) usually residing in a *.d directory to override (some or all settings of) the main/base config in the parent directory.
There is also some very rudimentary support for including additional files, inspired by #796. But it is more of an "anti-sale" I am trying here, since I believe that drop-ins are a sufficient alternative. They can be implemented in a cleaner way and do not have problems that might arise with including files, i.e. include loops as mention here. Things like that are simply impossible with drop-ins. So, with the right care one should be able to achieve the same (or similar) results by leveraging drop-ins. See FILES section in 'dunst(1)' for details on the drop-in support. There is, intentionally, no further documentation on including files, because I believe it is more trouble to get right than it is worth, given the rather robust alternative, which is a straight forward evolution of #917.
Technically, one would need to really preprocess all files and keep track of already included files, if you want to get include support right. At first glance, hooking this into 'load_ini_file()' does look nice, until you think about all the potential problems. From my perspective the right place to do this would probably be in 'load_settings()' where the file handling takes place now, because not only do you need to keep track of all files opened by include rules but the corresponding file containing the first include as well. That information is not available in 'load_ini_file()' unless you want to push file handling there. But if you do that there is one big gain from NOT supporting includes and solely relying on drop-ins: the key-value file parser provided by glib, which I stumbled upon while browsing the documentation, could be used to do the dirty work in 'load_ini_file()'. While there is no way to make that work with includes, it seems trivial to replace the cumbersome string parsing and drop includes altogether.
Oh, one last thing, I forgot to mention in the commit messages: 'load_settings()' now has a safeguard to not set defaults, if called again, with #719 in mind.Edit: This wasn't actually true, sinceset_defaults()
ran unconditionally, had overlooked that I moved it because of a failing test, and on second thought it does not make sense to not set defaults on subsequent calls ofload_settings()
P.S.: Also dialed back a little on the prefixes for debug messages etc. and made them work with clang as well.
I just realized that the commit message suggests otherwise, but only debug, error, and critical messages get this additional prefix.