Skip to content

Conversation

@pgaskin
Copy link
Owner

@pgaskin pgaskin commented Jun 18, 2020

This PR implements config file reloading.

Every time the menu is opened, the config files are scanned. If the number of files or the modification time of one changes, they are re-parsed and re-generated. If there is a config error or I/O error while parsing or scanning the files, an item is added showing it like usual. If there aren't any config entries, the usual one referring to the documentation is shown.

There should not be any noticeable delay in opening the menu, except around another 200ms (up to 500ms) when opening it after a config change.

closes #42


For the config parser improvements, see #47 (comment).

closes #48

pgaskin added 8 commits June 16, 2020 20:15
This will be used when re-parsing the config.
I still need to fix the item order and persistent separators.
Previously, it would add items to the end of the list every second update. This
was because we kept track of where to add the item based on the index of the
*original* menu, but this changes when we add our items. To fix this, we get
the action to add items before *after* we remove our own ones.
It's not actually needed.

This reverts commit 979d6e5.
@pgaskin pgaskin added this to the v0.2.0 milestone Jun 18, 2020
@pgaskin pgaskin requested review from NiLuJe, jackiew1 and shermp June 18, 2020 04:16
@pgaskin
Copy link
Owner Author

pgaskin commented Jun 18, 2020

This is ready for testing.

In particular, I'd like someone to try and break the update mechanism (try and make it incorrectly detect an update or lack thereof, also ensure the items are in the correct order, make sure it still works correctly after a USBMS session), I want to make sure I didn't leave any edge cases (no menu items, etc) unhandled, and I would like another pair of eyes on the pointer/memory handling in nm_global_* in init.c (to make sure I free things properly and swap things out under the right conditions).

In addition, you shouldn't be able to cause it to crash at any point (e.g. I tested that it wouldn't crash if the config files were modified while a menu was open and an item was pressed, and it worked fine because we only check for config updates when the menu is opened).

Also, @NiLuJe, can you ensure that your KFMon generator works with KFMon running, with KFMon changes (you'll need to touch a config file for now, but I'd be happy to hear ideas to do it automatically), without KFMon running, and with KFMon unresponsive?

P.S. A code review would also be appreciated.

@pgaskin
Copy link
Owner Author

pgaskin commented Jun 18, 2020

I do have one idea for automatic updates for KFMon: In another PR, I could implement generator updates and the KFMon one could check the mtime of the socket (which KFMon can touch every time there is a change).

@shermp
Copy link
Collaborator

shermp commented Jun 18, 2020

I'm trying to figure my way through the code, but I have to admit, first impressions are... convoluted. Mainly around the nm_global_config_update and nm_global_config_items

More thoughts when I can (hopefully) figure most things out.

@shermp
Copy link
Collaborator

shermp commented Jun 18, 2020

The code in config.c seems fine to me. What perplexes me is the global config management. To me, it seems a mess of global functions operating on a mess of global variables.

I would have thought a better way would be to have a single global struct that contains all the required state, with the required updating functions updating it directly, preferably via a pointer parameter (to make it clear what's being modified).

The menu ejecting code could then just check needed flags, and any error messages etc.

What am I missing?

@shermp
Copy link
Collaborator

shermp commented Jun 18, 2020

Just had another thought. You might need to go back through the config parser code and check for memory leaks, now that config parsing is no longer a one-time affair.

(I'm specifically thinking about returning on error without freeing the calloc'ed nm_menu_item_t variable.)

@NiLuJe
Copy link
Collaborator

NiLuJe commented Jun 18, 2020

I won't have time to give this a proper look-see until this week-end, but it's on top of the list ;).

@pgaskin
Copy link
Owner Author

pgaskin commented Jun 18, 2020

What perplexes me is the global config management. To me, it seems a mess of global functions operating on a mess of global variables.

Yes, it is somewhat convoluted. I ended up on the current design after thinking of how I can keep pointers valid for as long as possible, free stuff as soon as possible, and be the most efficient.

The first thing I did was to split the file scanning from the config. The other option I was considering was to add a file/date field to each config item (or just the first one from each file), but I realized that would limit future flexibility, it isn't that clean, and nothing uses it (or should use it) except during the initial parsing. This led to nm_config_files which returns a nm_config_file_t, which, for efficiency (we append quite a bit and don't know the size ahead of time, but we loop in order), was a linked list.

Then, I was thinking about how to update it in a way which would allow returning errors while leaving the original one untouched, and also handle freeing the memory. This led to nm_config_files_update, which is relatively straightforward (note that the bit after the loop which checks op || np is to check if the sizes are different, as one would still point to an item after the loop exits while the other is null at the end of the list).

Afterwards, I had to figure out the best way to manage the config and errors with the least code duplication, specifically around the error items (I wanted to create those in one place during the parsing rather than in the menu for each error). This led to the 4 variables: nm_global_menu_config_files (for comparing against without affecting the config), nm_global_menu_config (so we can free it later), nm_global_menu_config_items (because we needed somewhere to store the pre-generated error items, which meant we couldn't just call nm_config_get_menu in the injection hook), and nm_global_menu_config_n (same reason). And, to be on the safe side, I made those private to init.c so other files can't touch it by accident, and added nm_global_config_items to get the items, and nm_global_config_update to update them.

For nm_global_config_update, I wanted to reduce the code duplication in error handling, and wanted to have the simplest conditions possible for freeing (i.e. avoid lots of nested if statements for each condition). This led to nm_global_config_replace, which just frees whatever will be replaced if it isn't NULL, and then replaces the items with either the error message or the new menu items. This also has the advantage of ensuring that there won't be any condition in which the menu items are invalid after the update function returns.

That's basically how I ended up with the current design.


I'll have a look at the config parsing itself and see if I can simplify the memory management in it (probably by making the struct on the stack first, then only allocating them memory on success rather than beforehand).

pgaskin added 3 commits June 18, 2020 20:13
* No more memory leaks on error.
* Properly handle memory allocation errors.
* Cleaner code.
* Less chance of making mistakes when adding features in the future.
* Also made if statements easier to read.

There are still a few things which could be simplified.
@pgaskin
Copy link
Owner Author

pgaskin commented Jun 19, 2020

I've basically rewritten the config parser over the last 3 commits. It should be faster, more stable, more efficient, easier to extend, and won't leak memory anywhere (even when errors occur). The only downside is that it is twice the lines of code.

I've already done some tests with it, but while testing the config reloading, also try out different config errors to make sure they are still handled properly.

If you have any suggestions for improving the config parser further, I'd be happy to hear them.

@pgaskin pgaskin changed the title Implement config file reloading Implement config file reloading and improve the config parser Jun 19, 2020
@NiLuJe
Copy link
Collaborator

NiLuJe commented Jun 22, 2020

And that's it for tonight, I think ;).

(I've also implemented and tested the mtime-on-kfmon-socket thing on KFMon's side, and that works beautifully :). Also made the config order alphabetical there, too).

@NiLuJe
Copy link
Collaborator

NiLuJe commented Jun 22, 2020

Also, random easy refactor idea: move the config filtering to the actual scandir filter feature? (That essentially means a smaller alloc for nl).

(And get rid of the logging on . & .. in the process ;)).

@pgaskin
Copy link
Owner Author

pgaskin commented Jun 22, 2020

generated items were inserted in reverse order (because the list is actually linked in reverse?).

Yes, thanks for catching that. The list used to be linked in reverse until I refactored the config parser. Then, that made the menu item adapter go in the wrong order, which, as you noticed, I fixed. But, I tested the generators before that fix, making me forget about the generator code.

I'll probably apply your first fix, as I feel it's a bit more concise than the second.

Apart from that, no adverse effects with the KFMon generator.
...

👍

Also made the config order alphabetical there, too

I noticed. Is there a particular case which is broken with alphasort that made you write an adapter for strcmp instead?

Also, random easy refactor idea: move the config filtering to the actual scandir filter feature? (That essentially means a smaller alloc for nl).

I can't remember why I decided against it, but upon looking at it again, I don't see why not.


Did you happen to test the config errors yet?

@NiLuJe
Copy link
Collaborator

NiLuJe commented Jun 22, 2020

Did you happen to test the config errors yet?

Hmm, which config errors? ^^

EDIT: The Clang SA stuff? If so, yep, the fixes did the trick, and I think I agree with you on the false-positives left ;).

@NiLuJe
Copy link
Collaborator

NiLuJe commented Jun 22, 2020

I noticed. Is there a particular case which is broken with alphasort that made you write an adapter for strcmp instead?

alphasort takes pointers to a dirent pointer, I'm using the BSD fts_* API instead of scandir (for, err, some reason? :D), so the function needs to take pointers to an FTSENT pointer instead ;).

@pgaskin
Copy link
Owner Author

pgaskin commented Jun 22, 2020

Hmm, which config errors? ^^

The usual ones (just to make sure I didn't mess them up during the refactor). I've already tested some of them.

@NiLuJe
Copy link
Collaborator

NiLuJe commented Jun 22, 2020

The usual ones (just to make sure I didn't mess them up during the refactor). I've already tested some of them.

Besides the action/generator fields no longer being validated, I haven't noticed anything new.

Bogus args are handled on a per-action basis, IIRC, so that shouldn't have been affected.
I tested the empty config case, that worked.

Not quite sure what else could go wrong ;).

@pgaskin pgaskin force-pushed the geek1011/config-reloading branch from 980a59a to 64b3822 Compare June 22, 2020 02:33
@pgaskin
Copy link
Owner Author

pgaskin commented Jun 22, 2020

I've tested the error handling with:

chain_success:dbg_syslog:bad
bad
bad:bad
menu_item:bad
menu_item:main:bad
menu_item:main:test:bad
menu_item:main:test:dbg_syslog:ok
chain_bad:bad
chain_success:bad
chain_success:dbg_syslog:ok
generator:bad
generator:main:kfmon
chain_success:dbg_syslog:bad
menu_item:main:test:dbg_syslog:ok
chain_failure:dbg_syslog:ok
chain_success:dbg_syslog:ok
chain_always:dbg_syslog:ok

I deleted the first line each time I tested it.

@pgaskin
Copy link
Owner Author

pgaskin commented Jun 22, 2020

I think this is pretty much ready now. The parser emits errors properly, and I think I've fixed all regressions from the refactor. I've also tested all the examples and my own config. I'll probably merge this, and I might post a beta version on MR later this week for testing.

@NiLuJe
Copy link
Collaborator

NiLuJe commented Jun 22, 2020

Another random thought: at boot, config parsing is bracketed between the failsafe mechanism.

That's no longer the case when updating configs. Should we care?

(I say that because I did manage to busy-loop nickel when breaking the linked list stuff ^^).

@pgaskin
Copy link
Owner Author

pgaskin commented Jun 22, 2020

That's no longer the case when updating configs. Should we care?

I originally thought of that (hence the now-reverted instant failsafe destruction commit), but I decided against it, as it would mean I need a mutex to ensure the failsafe isn't already running, and if it was going to crash, it would do it during the initial parsing, which would trigger the failsafe. And, if it did happen to cause issues only after the first parse (i.e. when the menu is opened), NM can be uninstalled manually by connecting via USB or telnet without using the menu. Does this seem reasonable?

@NiLuJe
Copy link
Collaborator

NiLuJe commented Jun 22, 2020

@geek1011: Nope, sounds good.

In any case, if an uncaught bug does cause an issue during a config update, Nickel will softlock, which means sickel will reboot the device. Since the device was essentially unusable, the configs won't have been touched, at which point they'll trigger the exact same issue during the newt boot, except this time it'll be during the failsafe window, so, everybody wins ;).

It essentially boils down to simply needing two reboots instead of one to "fix" it by self-destruct.

EDIT: Which is what you just said :D. It's late ^^.

@pgaskin pgaskin merged commit ee0eb7c into master Jun 23, 2020
pgaskin added a commit that referenced this pull request Jun 23, 2020
pgaskin added a commit that referenced this pull request Jun 26, 2020
…ting one (fixes #57)

This issue was found by @jackiew1. See #56 for more details.

This fixes an issue in ee0eb7c (#47).
pgaskin added a commit that referenced this pull request Jun 26, 2020
…ting one (fixes #57)

Instead of returning a bool from `nm_global_config_update` indicating whether
the menu items were modified, return an int indicating the current config
revision which can then be compared against one stored as a property of the
menu.

This issue was found by @jackiew1. See #56 for more details.

This fixes an issue in ee0eb7c (#47).
pgaskin added a commit that referenced this pull request Jun 26, 2020
…ting one (fixes #57) (#58)

Instead of returning a bool from `nm_global_config_update` indicating whether
the menu items were modified, return an int indicating the current config
revision which can then be compared against one stored as a property of the
menu.

This issue was found by @jackiew1. See #56 for more details.

This fixes an issue in ee0eb7c (#47).
@pgaskin pgaskin deleted the geek1011/config-reloading branch August 5, 2020 21:41
pgaskin added a commit that referenced this pull request Dec 4, 2020
This fixes a regression in ee0eb7c (#47) which caused vague error messages to be
returned instead of specific error messages from nm_config_parse__line_*. These
functions should have returned true on error, but were returning NULL (i.e.
false) instead, causing nm_config_parse to treat it as an unknown line type
instead of returning the specific issue with the line. This regression does not
cause any issues other than vague error messages being returned for certain
errors during config parsing.

In addition, this fixes another regression discovered by @NiLuJe where
configuration errors would persist in rare situations due to nm_config_files not
clearing existing errors as expected. This issue does not happen if the config
error menu item is pressed to see the error message since nm_menu_item_do clears
errors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Config parser improvements Config file reloading

4 participants