Skip to content

IController - Persist and Restore #1920

Open
nxtum wants to merge 25 commits intordkcentral:masterfrom
nxtum:PersistConfigs
Open

IController - Persist and Restore #1920
nxtum wants to merge 25 commits intordkcentral:masterfrom
nxtum:PersistConfigs

Conversation

@nxtum
Copy link
Contributor

@nxtum nxtum commented Aug 14, 2025

Previously there was one Persist function that would save the configs for all plugins without the ability to undo.
Now, you can specify which plugin to persist, as well as remove any/all configs (restore to default)

@nxtum nxtum requested review from pwielders and sebaszm August 15, 2025 11:12
Copy link
Contributor

@pwielders pwielders left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An overall remark. I guess in the past, because all plugins where in 1 file, we added an empty config ('{}') for plugins that had no override or where destroyed. However now the configs are single entries per callsign. Whenever you "add" a file it probably takes up a sector on flash storage per file (much more than two byes to signal {} in the one file approach). I guess it is worth the effort to not store an override file if there are no changes to to config. So suggest to add this optimization in the code.
No changes on the plugin, no override file in the thunder/plugins directpry o persitent storage space to safe flash sectors...

#endif

/* static */ const TCHAR* Server::PluginOverrideFile = _T("PluginHost/override.json");
/* static */ const TCHAR* Server::PluginOverrideDirectory = _T("PluginHost/");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to move the directory now to Thunder/plugins and store the configs there, it is in the same line as the fixed configs are. Maybe you could even swap this line than with the nex line and use the default of the plugins config from there so place this:

/* static */ const TCHAR* Server::PluginOverrideDirectory = _T("Thunder/") + Server::PluginConfigDirectory;

on the line "after" the Server::PluginConfigDirectory declaration

// Create individual plugin configuration files
string filePath;
filePath.reserve(persistentFolder.size() + name.size() + sizeof("Override.json") - 1);
filePath.append(persistentFolder).append(name).append("Override.json");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we put them all in a folder (e.g. previous remark Thunder/plugins. I guess we could than use the same sematics as we do in thunder an only append the name with json, so:

filePath.append(persistentFolder).append(name).append(".json");

than it is consistent with the configs we find in /etc/Thunder/plugins as the startin point. Consistency is key ;-)

@pwielders pwielders self-requested a review September 6, 2025 11:04
Copy link
Contributor

@pwielders pwielders left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Questions/Remark:

  1. Why is in the save, the way to create an override a lamda i.s.o. a normal private method?
  2. Comparing if the configs are different might be a bit more tricky. Cause officially:
{ "fieldA": 1, "fieldB" : 2 } === { "fieldB": 2, "fieldA": 1}

This is now detected as different configs.
And probbaly even worse:

{     "field":   1 } === {"field":1}

Is already detected as a different config :-)

got 2 suggestions to avoid this (or make it more clear):

  1. Use a flag on the service that if the "config" is set through a method, flag it as dirty and consider the config to be changed (even if it was not changed :-)
  2. enhance the Core::JSON::VariantContainer class with an bool operator ==(const VariantContainer& ) const and load each string into the VariantContainer class and compare those (field availability and there corresponding values)

I have a preference for the second option enhance the JSON class :-) might be usefull in more places and the VariantContainer is already used anway, somewhere in the PluginServer code but aI am also interested in @sebaszm opinion here...


/* static */ const TCHAR* Server::PluginOverrideFile = _T("PluginHost/override.json");
/* static */ const TCHAR* Server::PluginConfigDirectory = _T("plugins/");
/* static */ const TCHAR* Server::PluginOverrideDirectory = _T("Thunder/plugins/");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use EXPAND_AND_QUOTE(NAMESPACE) instead of using framework name directly.


// See if the persitent path for our-selves exist, if not we will create it :-)
Core::File persistentPath(_config.PersistentPath() + _T("PluginHost"));
Core::File persistentPath(_config.PersistentPath() + _T("Thunder/plugins"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the constant instead of repeating the path name.

Comment on lines 1780 to 1790
// Create individual plugin configuration files
string filePath;
filePath.reserve(persistentFolder.size() + name.size() + sizeof(".json") - 1);
filePath.append(persistentFolder).append(name).append(".json");

_pluginOverrideFiles.emplace(
std::piecewise_construct,
std::forward_as_tuple(name),
std::forward_as_tuple(std::move(filePath))
);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to remember the filenames for each callsign? if we only stored the base folder having a callsing we can every time find the right path. I don't think configs are being overiden so frequent to justify this flamboyance with memory.

Comment on lines 1821 to 1827
// Clear all currently set values, they might be from the previous run.
Clear();

Callsigns::const_iterator current(_callsigns.find(index->Callsign()));
// Read the file and parse it into this object.
IElement::FromFile(storage);
_serverconfig.SetPrefix(Prefix.Value());
_serverconfig.SetIdleTime(IdleTime.Value());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part shouldn't be repeated for each plugin config. This clears and looks for prefix and idletime in each of the plugins' config while this is Thunder property.

uint32_t result = Core::ERROR_NONE;

auto CreateOverride = [this, &result](ServiceMap::Iterator& indexService) {
const string currentCallsign = indexService->Callsign();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A copy!!

}

Core::File storage(indexFile->second);
if (storage.Create() == true) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not write the override config at all if it's not changed! Let the absence of the override file mean there is no override.

Comment on lines +4859 to +4871
Override infoBlob(_config, _services, Configuration().PersistentPath() + PluginOverrideDirectory);
return (infoBlob.Save(callsign));
}

return (infoBlob.Save());
uint32_t Restore(const Core::OptionalType<string>& callsign)
{
Override infoBlob(_config, _services, Configuration().PersistentPath() + PluginOverrideDirectory);
return (infoBlob.Destroy(callsign));
}

uint32_t Load()
{
Override infoBlob(_config, _services, Configuration().PersistentPath() + PluginOverrideFile);

Override infoBlob(_config, _services, Configuration().PersistentPath() + PluginOverrideDirectory);
Copy link
Contributor

@sebaszm sebaszm Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this approach inefficient. Even if we are working with a single callsign we still create the Override object and prepare for handling all plugins (ie. saving callsiings, configs, filenames.... etc).

@sebaszm
Copy link
Contributor

sebaszm commented Sep 11, 2025

  1. Use a flag on the service that if the "config" is set through a method, flag it as dirty and consider the config to be changed (even if it was not changed :-)
  2. enhance the Core::JSON::VariantContainer class with an bool operator ==(const VariantContainer& ) const and load each string into the VariantContainer class and compare those (field availability and there corresponding values)

I have a preference for the second option enhance the JSON class :-) might be usefull in more places and the VariantContainer is already used anway, somewhere in the PluginServer code but aI am also interested in @sebaszm opinion here...

  1. does not solve the problem much if Store() is used with no callsign then all plugins will be marked dirty even if not changed.

Agree, to do this properly we would need to normalize the JSON first. JSON::Variant would be a perfect starting point to achieve this (it is though behind COMPLEMENTARY_CODE_SET flag). We however will encounter nuance like order of elements in array (is [a,b] same as [b,a]? depends), or explicitly setting a value that is same as a default. I think too that it would be a useful feature, but perhaps we should split this into another PR.

@sebaszm sebaszm self-requested a review October 1, 2025 13:22
Copilot AI review requested due to automatic review settings February 15, 2026 13:35
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds granular control to plugin configuration persistence by allowing users to persist or restore individual plugins rather than all plugins at once. The changes transform the single "persist all" functionality into a flexible system that supports persisting/restoring specific plugins, all plugins, or server-wide configuration.

Changes:

  • Extended Persist method to accept an optional callsign parameter, allowing selective persistence of individual plugins or all plugins
  • Added new Restore method to remove persisted configuration overrides and return plugins to their default state
  • Refactored the storage system from a single override.json file to individual per-plugin JSON files in a directory structure

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
Source/plugins/IController.h Updated IConfiguration interface with parameterized Persist method and new Restore method
Source/Thunder/PluginServer.h Major refactoring of Override class: changed from single-file to directory-based storage, implemented Save/Destroy methods with per-plugin file handling, added PluginHost-specific configuration methods
Source/Thunder/PluginServer.cpp Updated PluginOverrideFile constant to PluginOverrideDirectory, fixed typo in comment
Source/Thunder/Controller.h Added Restore method declaration and updated Persist signature
Source/Thunder/Controller.cpp Implemented new Restore method and updated Persist to forward callsign parameter

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if (callsign.IsSet() == true) {
bool found = false;
const string& raw = callsign.Value();
const string target = raw.empty() ? _T("Controller") : raw;
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an empty string is passed as callsign (callsign.IsSet() is true but callsign.Value() is empty), it gets mapped to "Controller" at line 1828. This special case should be documented in the function or the IController interface, as it's not obvious that an empty string means "Controller".

Copilot uses AI. Check for mistakes.
if (callsign.IsSet() == true) {
bool found = false;
const string& raw = callsign.Value();
const string target = raw.empty() ? _T("Controller") : raw;
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an empty string is passed as callsign (callsign.IsSet() is true but callsign.Value() is empty), it gets mapped to "Controller" at line 1866. This special case should be documented in the function or the IController interface, as it's not obvious that an empty string means "Controller". This duplicates the same logic from the Save method.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants