User Defined Runtime Configuration Modification
Background
Building or Modifying the Configuration on Server Start Up (Configuration Decoration)
Key Functions
The start of of the configuration decoration hook is to override method overridableConfigure. The virtual method is defined in class BaseQuasarServer, and should be overridden in class QuasarServer.
virtual bool overridableConfigure(const std::string& fileName, AddressSpace::ASNodeManager *nm);
typedef std::function<bool (Configuration::Configuration&)> ConfigXmlDecoratorFunction;
bool configure (std::string fileName,
AddressSpace::ASNodeManager *nm, ConfigXmlDecoratorFunction
configXmlDecoratorFunction = ConfigXmlDecoratorFunction()); // 'empty' function by default.
/**
* push_back is a configuration decoration helper function. This function *MUST*
* be used when decorating the configuration object tree (i.e. adding new
* object instances). This function handles both
* 1. Addition: adding the object in the specified tree location.
* 2. Content Ordering: maintaining the ordering mechanism quasar uses to process
* configuration elements in the correct order.
*
* @param parent: Parent tree node.
* @param children: The collection of children (owned by the parent) to which the
* new child will be appended.
* @param child: The new child object
* @param childTypeId: The ordering type ID (as gen'd by xsdcxx) of the new child
* object
*/
template< typename TParent, typename TChildren, typename TChild >
void push_back(TParent& parent, TChildren& children, const TChild& child, const size_t childTypeId)
Example Code
Server Design
<?xml version="1.0" encoding="UTF-8"?>
<d:design projectShortName="" xmlns:d="http://cern.ch/quasar/Design" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://cern.ch/quasar/Design Design.xsd ">
<d:class name="InnerClass">
<d:devicelogic/>
<d:configentry dataType="OpcUa_UInt32" name="configInstanceId"/>
<d:cachevariable initializeWith="valueAndStatus" name="runtimeInstanceId" ...etc.../>
</d:class>
<d:class name="AnotherInnerClass">
<d:devicelogic/>
<d:configentry dataType="OpcUa_UInt32" name="configInstanceId"/>
<d:cachevariable initializeWith="valueAndStatus" name="runtimeInstanceId" ...etc.../>
</d:class>
<d:class name="ConfiguredClass">
<d:devicelogic/>
<d:configentry dataType="OpcUa_UInt32" name="configInstanceId"/>
<d:cachevariable initializeWith="valueAndStatus" name="runtimeInstanceId" ...etc.../>
<d:hasobjects instantiateUsing="configuration" class="InnerClass"/>
<d:hasobjects instantiateUsing="configuration" class="AnotherInnerClass"/>
</d:class>
<d:root>
<d:hasobjects instantiateUsing="configuration" class="ConfiguredClass"/>
</d:root>
</d:design>
Function decorateConfiguration()
bool QuasarServer::decorateConfiguration(Configuration::Configuration& theConfig) const
{
LOG(Log::INF) <<__FUNCTION__<< " starting server specific configuration decoration";
// Goal: extend given configuration (theConfig) AT RUNTIME (initialisation) as follows
//
// theConfig (contains contents of config.xml)
// +
// |_ConfiguredClass(1000)
// |_InnerClass(1001)
// |_AnotherInnerClass(1002)
// |_InnerClass(1003)
// |_AnotherInnerClass(1004)
// |_ConfiguredClass(2000)
// |_InnerClass(2001)
// |_AnotherInnerClass(2002)
// |_InnerClass(2003)
// |_AnotherInnerClass(2004)
// |_ConfiguredClass(3000)
// |_InnerClass(3001)
// |_AnotherInnerClass(3002)
// |_InnerClass(3003)
// |_AnotherInnerClass(3004)
// Create & populate objects locally, then add to theConfig tree using quasar decoration utility function
for(int i=1000; i<=3000; i+=1000)
{
Configuration::ConfiguredClass parent("parentDevice"+std::to_string(i), i);
for(int j = i+1; j<=i+4; ++j)
{
if(j%2)
{
Configuration::InnerClass child("childDevice"+std::to_string(j), j);
Configuration::DecorationUtils::push_back(parent, parent.InnerClass(), child,
Configuration::ConfiguredClass::InnerClass_id);
}
else
{
Configuration::AnotherInnerClass child("anotherChildDevice"+std::to_string(j), j);
Configuration::DecorationUtils::push_back(parent, parent.AnotherInnerClass(), child,
Configuration::ConfiguredClass::AnotherInnerClass_id);
}
}
Configuration::DecorationUtils::push_back(theConfig, theConfig.ConfiguredClass(), parent,
Configuration::Configuration::ConfiguredClass_id);
}
LOG(Log::INF) <<__FUNCTION__<< " completed server specific configuration decoration";
return true;
}
Configuration::DecorationUtils::clear(theConfig, theConfig.ConfiguredClass(),
Configuration::Configuration::ConfiguredClass_id);
Function validateContentOrder
This section is here for information: There is no action required by server developers. Internally, quasar calls validateContentOrder during initialisation to verify the configuration object tree is valid with respect to the content order mechanism, including any additional objects that were added to the tree during configuration decoration. Below is some deliberately erroneous code and the corresponding error message quasar logs as it exist (due to validateContentOrder failing).
if(j%2)
{
Configuration::InnerClass child("childDevice"+std::to_string(j), j);
parent.InnerClass().push_back(child); // WRONG ! INVALIDATES CONTENT ORDER
}
2020-05-19 17:37.04.106695 [BaseQuasarServer.cpp:156, ERR] Exception caught in BaseQuasarServer::serverRun: [validateContentOrder ERROR parent has [2] child objects unregistered in content order]
Wiring in the ConfigXmlDecoratorFunction instance
The final step is to ensure that the function configure
above is
called with the correct arguments; namely with the developer’s
implementation of ConfigXmlDecoratorFunction as the 3rd argument. As is
often the case in quasar, injecting user specifc code involves
overriding a virtual function. In this case, the virtual function to
override is:
bool BaseQuasarServer::overridableConfigure(const std::string& fileName, AddressSpace::ASNodeManager *nm);
A typical developer override of this function would be along the lines of the following pseudo code
bool QuasarServer::overridableConfigure(const std::string& fileName, AddressSpace::ASNodeManager *nm)
{
if([command line switch active for discovery mode])
{
LOG(Log::INF) <<__FUNCTION__<< " server specific override invoked, configuration will be decorated";
ConfigXmlDecoratorFunction decoratorFn = std::bind(&QuasarServer::decorateConfiguration, this, std::placeholders::_1);
return configure(fileName, nm, decoratorFn);
}
else
{
LOG(Log::INF) <<__FUNCTION__<< " server running in regular mode, configuration will be as per config.xml";
return configure(fileName, nm);
}
}
Persisting the Configuration in an XML file
As described above, type definition ConfigXmlDecoratorFunction
describes a single parameter Configuration::Configuration&
, this
parameter is an instance of an XSD generated C++ class which handles
both in-memory object loading from an XML file (parsing/deserialization)
and, the key point here, writing the contents of the in-memory object to
an XML file (serialization). To persist an in-memory configuration then,
we need simply only call serialization methods from xsd-cxx. Once
in-memory configuration decoration is complete; it can be written to an
XML file by calling (something like) the following pseudo-code excerpt:
std::ofstream configurationFile;
try
{
configurationFile.open(some command line specified path for the config serialization, ofstream::out | ofstream::trunc);
}
catch(...all sorts of errors....)
{ ...and handle... }
try
{
xml_schema::namespace_infomap nsMap;
nsMap[""].name = "http://cern.ch/quasar/Configuration";
nsMap[""].schema = "../Configuration/Configuration.xsd";
Configuration::configuration(configurationFile, theConfiguration, nsMap); // actual write executed on this line
}
catch(...all sorts of errors....)
{ ...and handle... }