Add support for config file parsing and watching
[ric-plt/xapp-frame-cpp.git] / src / config / config.cpp
diff --git a/src/config/config.cpp b/src/config/config.cpp
new file mode 100644 (file)
index 0000000..16eb421
--- /dev/null
@@ -0,0 +1,371 @@
+// vi: ts=4 sw=4 noet:
+/*
+==================================================================================
+    Copyright (c) 2020 AT&T Intellectual Property.
+    Copyright (c) 2020 Nokia
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================================
+*/
+
+/*
+    Mnemonic:  config.cpp
+    Abstract:  Support for reading config json.
+
+                               The config structure allows simplified parsing of the json and
+                               easy access to the things we belive all xAPPs will use (port
+                               digging form the named "interface" and control settings). This
+                               also supports the watching on the file and driving the user
+                               callback when the config file appears to have changed (write
+                               close on the file).
+
+                               Accessing information from the json is serialised (mutex) as
+                               with the random nature of file updates, we must ensure that
+                               we don't change the json as we're trying to read from it. Locking
+                               should be transparent to the user xAPP.
+
+    Date:       27 July 2020
+    Author:     E. Scott Daniels
+*/
+
+#include <errno.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <sys/inotify.h>
+#include <unistd.h>
+#include <string.h>
+
+#include <iostream>
+#include <sstream>
+#include <fstream>
+#include <thread>
+#include <memory>
+
+#include "jhash.hpp"
+#include "config.hpp"
+#include "config_cb.hpp"
+
+namespace xapp {
+
+
+// ----- private things --------
+/*
+       Notification listener. This function is started in a thread and listens for
+       changes to the config file. When it sees an interesting change (write close)
+       to the file, then it will read the new set of json, parse it, and drive the
+       user callback.
+
+       We must watch the directory containing the file as if the file is edited and
+       replaced it's likely saved with a different referencing inode. We'd see the
+       first change, but not any subsequent changes as the inotify is based on inodes
+       and not directory entries.
+*/
+void xapp::Config::Listener( ) {
+       struct inotify_event*   ie;             // event that popped
+       int ifd;                                                // the inotify file des
+       int     wfd;                                            // the watched file des
+       int     n;
+       char    rbuf[4096];                             // large read buffer as the event is var len
+       char*   dname;                                  // directory name
+       char*   bname;                                  // basename
+       char*   tok;
+
+       ifd = inotify_init1( 0 );               // initialise watcher setting blocking read (no option)
+       if( ifd < 0 ) {
+               fprintf( stderr, "<XFCPP> ### ERR ### unable to initialise file watch %s\n", strerror( errno ) );
+               return;
+       }
+
+       dname = strdup( fname.c_str() );                                        // defrock the file name into dir and basename
+       if( (tok = strrchr( dname, '/' )) != NULL ) {
+               *tok = 0;
+               bname = strdup( tok+1 );
+       } else {
+               free( dname );
+               dname = strdup( "." );
+               bname = strdup( fname.c_str() );
+       }
+
+       wfd = inotify_add_watch( ifd, (char *) dname, IN_MOVED_TO | IN_CLOSE_WRITE );           // we only care about close write changes
+
+       if( wfd < 0 ) {
+               fprintf( stderr, "<XFCPP> ### ERR ### unable to add watch on config file %s: %s\n", fname.c_str(), strerror( errno ) );
+               return;
+       }
+
+       while( true ) {
+               n = read( ifd, rbuf, sizeof( rbuf ) );                          // read the event
+               if( n < 0  ) {
+                       if( errno == EAGAIN ) {
+                               continue;
+                       } else {
+                               fprintf( stderr, "<XFCPP ### CRIT ### config listener read err: %s\n", strerror( errno ) );
+                               return;
+                       }
+               }
+
+               ie = (inotify_event *) rbuf;
+               if( ie->len > 0 && strcmp( bname, ie->name ) == 0  ) {
+                       // TODO: lock
+                       auto njh = jparse( fname );                                                     // reparse the file
+                       // TODO: unlock
+
+                       if( njh != NULL && cb != NULL ) {                               // good parse, save and drive user callback
+                               jh = njh;
+                               cb->Drive_cb( *this, user_cb_data );
+                       }
+               }
+       }
+}
+
+
+/*
+       Read a file containing json and parse into a framework Jhash.
+
+       Using C i/o will speed this up, but I can't imagine that we need
+       speed reading the config file once in a while.
+       The file read comes from a stack overflow example:
+               stackoverflow.com/questions/2912520/read-file-contents-into-a-string-in-c
+*/
+std::shared_ptr<xapp::Jhash> xapp::Config::jparse( std::string ufname ) {
+       fname = ufname;
+
+       std::ifstream ifs( fname );
+       std::string st( (std::istreambuf_iterator<char>( ifs ) ), (std::istreambuf_iterator<char>() ) );
+
+       auto new_jh = std::shared_ptr<xapp::Jhash>( new xapp::Jhash( st.c_str() ) );
+       return  new_jh->Parse_errors() ? NULL : new_jh;
+}
+
+/*
+       Read the configuration file from what we find as the filename in the environment (assumed
+       to be referenced by $XAPP_DESCRIPTOR_PATH/config-file.json. If the env var is not
+       defined we assume ./.  The data is then parsed with the assumption that it's json.
+
+       The actual meaning of the environment variable is confusing. The name is "path" which
+       should mean that this is the directory in which the config file lives, but the examples
+       elsewhere suggest that this is a filename (either fully qualified or relative). For now
+       we will assume that it's a file name, though we could add some intelligence to determine
+       if it's a directory name or not if it comes to it.
+*/
+std::shared_ptr<xapp::Jhash> xapp::Config::jparse( ) {
+       char*   data;
+
+       if( (data = getenv( (char *) "XAPP_DESCRIPTOR_PATH" )) == NULL ) {
+               data =  (char *) "./config-file.json";
+       }
+
+       return jparse( std::string( data ) );
+}
+
+// --------------------- construction, destruction -------------------------------------------
+
+/*
+       By default expect to find XAPP_DESCRIPTOR_PATH in the environment and assume that it
+       is the directory name where we can find config-file.json. The build process will
+       read and parse the json allowing the user xAPP to invoke the supplied  "obvious"
+       functions to retrieve data.  If there is something in an xAPP's config that isn't
+       standard, it can get the raw Jhash object and go at it directly. The idea is that
+       the common things should be fairly painless to extract from the json goop.
+*/
+xapp::Config::Config() :
+       jh( jparse() ),
+       listener( NULL )
+{ /* empty body */ }
+
+/*
+       Similar, except that it allows the xAPP to supply the filename (testing?)
+*/
+xapp::Config::Config( std::string fname) :
+       jh( jparse( fname ) ),
+       listener( NULL )
+{ /* empty body */ }
+
+
+/*
+       Read and return the raw file blob as a single string. User can parse, or do
+       whatever they need (allows non-json things if necessary).
+*/
+std::string xapp::Config::Get_contents( ) {
+       std::string rv = "";
+
+       if( ! fname.empty() ) {
+               std::ifstream ifs( fname );
+               std::string st( (std::istreambuf_iterator<char>( ifs ) ), (std::istreambuf_iterator<char>() ) );
+               rv = st;
+       }
+
+       return rv;
+}
+
+// ----- convience function for things we think an xAPP will likely need to pull from the config
+
+
+/*
+       Suss out the port for the named "interface". The interface is likely the application
+       name.
+*/
+std::string xapp::Config::Get_port( std::string name ) {
+       int i;
+       int     nele = 0;
+       double value;
+       std::string rv = "";            // result value
+       std::string pname;                      // element port name in the json
+
+       if( jh == NULL ) {
+               return rv;
+       }
+
+       jh->Unset_blob();
+       if( jh->Set_blob( (char *) "messaging" ) ) {
+               nele = jh->Array_len( (char *) "ports" );
+               for( i = 0; i < nele; i++ ) {
+                       if( jh->Set_blob_ele( (char *) "ports", i ) ) {
+                               pname = jh->String( (char *) "name" );
+                               if( pname.compare( name ) == 0 ) {                              // this element matches the name passed in
+                                       value = jh->Value( (char *) "port" );
+                                       rv = std::to_string( (int) value );
+                                       jh->Unset_blob( );                                                      // leave hash in a known state
+                                       return rv;
+                               }
+                       }
+
+                       jh->Unset_blob( );                                                              // Jhash requires bump to root, and array reselct to move to next ele
+                       jh->Set_blob( (char *) "messaging" );
+               }
+       }
+
+       jh->Unset_blob();
+       return rv;
+}
+
+/*
+       Suss out the named string from the controls object. If the resulting value is
+       missing or "", then the default is returned.
+*/
+std::string xapp::Config::Get_control_str( std::string name, std::string defval ) {
+       std::string value;
+       std::string rv;                         // result value
+
+       rv = defval;
+       if( jh == NULL ) {
+               return rv;
+       }
+
+       jh->Unset_blob();
+       if( jh->Set_blob( (char *) "controls" ) ) {
+               if( jh->Exists( name.c_str() ) )  {
+                       value = jh->String( name.c_str() );
+                       if( value.compare( "" ) != 0 ) {
+                               rv = value;
+                       }
+               }
+       }
+
+       jh->Unset_blob();
+       return rv;
+}
+
+/*
+       Convenience funciton without default. "" returned if not found.
+       No default value; returns "" if not set.
+*/
+std::string xapp::Config::Get_control_str( std::string name ) {
+       return Get_control_str( name, "" );
+}
+
+/*
+       Suss out the named field from the controls object with the assumption that it is a boolean.
+       If the resulting value is missing then the defval is used.
+*/
+bool xapp::Config::Get_control_bool( std::string name, bool defval ) {
+       bool value;
+       bool rv;                                // result value
+
+       rv = defval;
+       if( jh == NULL ) {
+               return rv;
+       }
+
+       jh->Unset_blob();
+       if( jh->Set_blob( (char *) "controls" ) ) {
+               if( jh->Exists( name.c_str() ) )  {
+                       rv = jh->Bool( name.c_str() );
+               }
+       }
+
+       jh->Unset_blob();
+       return rv;
+}
+
+
+/*
+       Convenience function without default.
+*/
+bool xapp::Config::Get_control_bool( std::string name ) {
+       return Get_control_bool( name, false );
+}
+
+
+/*
+       Suss out the named field from the controls object with the assumption that it is a value (float/int).
+       If the resulting value is missing then the defval is used.
+*/
+double xapp::Config::Get_control_value( std::string name, double defval ) {
+       double value;
+
+       auto rv = defval;                               // return value; set to default
+       if( jh == NULL ) {
+               return rv;
+       }
+
+       jh->Unset_blob();
+       if( jh->Set_blob( (char *) "controls" ) ) {
+               if( jh->Exists( name.c_str() ) )  {
+                       rv = jh->Value( name.c_str() );
+               }
+       }
+
+       jh->Unset_blob();
+       return rv;
+}
+
+
+/*
+       Convenience function. If value is undefined, then 0 is returned.
+*/
+double xapp::Config::Get_control_value( std::string name ) {
+       return Get_control_value( name, 0.0 );
+}
+
+
+// ---- notification support ---------------------------------------------------------------
+
+
+/*
+       Accept the user's notification function, and data that it needs (pointer to
+       something unknown), and stash that as a callback.
+
+       The fact that the user xAPP registers a callback also triggers the creation
+       of a thread to listen for changes on the config file.
+*/
+void xapp::Config::Set_callback( notify_callback usr_func, void* usr_data ) {
+       cb = std::unique_ptr<Config_cb>( new Config_cb( usr_func, usr_data ) );
+       user_cb_data = usr_data;
+
+       if( listener == NULL ) {                                // start thread if needed
+               listener = new std::thread( &xapp::Config::Listener, this );
+       }
+}
+
+} // namespace