5259a17025075f7f5c3f16a566040a687cfbaa2d
[ric-plt/xapp-frame-cpp.git] / src / config / config.cpp
1 // vi: ts=4 sw=4 noet:
2 /*
3 ==================================================================================
4     Copyright (c) 2020 AT&T Intellectual Property.
5     Copyright (c) 2020 Nokia
6
7    Licensed under the Apache License, Version 2.0 (the "License");
8    you may not use this file except in compliance with the License.
9    You may obtain a copy of the License at
10
11        http://www.apache.org/licenses/LICENSE-2.0
12
13    Unless required by applicable law or agreed to in writing, software
14    distributed under the License is distributed on an "AS IS" BASIS,
15    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16    See the License for the specific language governing permissions and
17    limitations under the License.
18 ==================================================================================
19 */
20
21 /*
22     Mnemonic:   config.cpp
23     Abstract:   Support for reading config json.
24
25                                 The config structure allows simplified parsing of the json and
26                                 easy access to the things we belive all xAPPs will use (port
27                                 digging form the named "interface" and control settings). This
28                                 also supports the watching on the file and driving the user
29                                 callback when the config file appears to have changed (write
30                                 close on the file).
31
32                                 Accessing information from the json is serialised (mutex) as
33                                 with the random nature of file updates, we must ensure that
34                                 we don't change the json as we're trying to read from it. Locking
35                                 should be transparent to the user xAPP.
36
37     Date:       27 July 2020
38     Author:     E. Scott Daniels
39 */
40
41 #include <errno.h>
42 #include <poll.h>
43 #include <stdlib.h>
44 #include <sys/inotify.h>
45 #include <unistd.h>
46 #include <string.h>
47
48 #include <iostream>
49 #include <sstream>
50 #include <fstream>
51 #include <thread>
52 #include <memory>
53
54 #include "jhash.hpp"
55 #include "config.hpp"
56 #include "config_cb.hpp"
57
58 namespace xapp {
59
60
61 // ----- private things --------
62 /*
63         Notification listener. This function is started in a thread and listens for
64         changes to the config file. When it sees an interesting change (write close)
65         to the file, then it will read the new set of json, parse it, and drive the
66         user callback.
67
68         We must watch the directory containing the file as if the file is edited and
69         replaced it's likely saved with a different referencing inode. We'd see the
70         first change, but not any subsequent changes as the inotify is based on inodes
71         and not directory entries.
72 */
73 void xapp::Config::Listener( ) {
74         const struct inotify_event*     ie;     // event that popped
75         int ifd;                                                // the inotify file des
76         int     wfd;                                            // the watched file des
77         ssize_t n;
78         char    rbuf[4096];                             // large read buffer as the event is var len
79         char*   dname;                                  // directory name
80         char*   bname;                                  // basename
81         char*   tok;
82
83         ifd = inotify_init1( 0 );               // initialise watcher setting blocking read (no option)
84         if( ifd < 0 ) {
85                 fprintf( stderr, "<XFCPP> ### ERR ### unable to initialise file watch %s\n", strerror( errno ) );
86                 return;
87         }
88
89         dname = strdup( fname.c_str() );                                        // defrock the file name into dir and basename
90         if( (tok = strrchr( dname, '/' )) != NULL ) {
91                 *tok = 0;
92                 bname = strdup( tok+1 );
93         } else {
94                 free (dname);
95                 dname = strdup( "." );
96                 bname = strdup( fname.c_str() );
97         }
98
99         wfd = inotify_add_watch( ifd, dname, IN_MOVED_TO | IN_CLOSE_WRITE );            // we only care about close write changes
100         free (dname);
101
102         if( wfd < 0 ) {
103                 fprintf( stderr, "<XFCPP> ### ERR ### unable to add watch on config file %s: %s\n", fname.c_str(), strerror( errno ) );
104                 free( bname );
105                 return;
106         }
107
108         while( true ) {
109                 n = read( ifd, rbuf, sizeof( rbuf ) );                          // read the event
110                 if( n < 0  ) {
111                         if( errno == EAGAIN ) {
112                                 continue;
113                         } else {
114                                 fprintf( stderr, "<XFCPP ### CRIT ### config listener read err: %s\n", strerror( errno ) );
115                                 return;
116                         }
117                 }
118
119                 ie = (inotify_event *) rbuf;
120                 if( ie->len > 0 && strcmp( bname, ie->name ) == 0  ) {
121                         // future: lock
122                         auto njh = jparse( fname );                                                     // reparse the file
123                         // future: unlock
124
125                         if( njh != NULL && cb != NULL ) {                               // good parse, save and drive user callback
126                                 jh = njh;
127                                 cb->Drive_cb( *this, user_cb_data );
128                         }
129                 }
130         }
131 }
132
133
134 /*
135         Read a file containing json and parse into a framework Jhash.
136
137         Using C i/o will speed this up, but I can't imagine that we need
138         speed reading the config file once in a while.
139         The file read comes from a stack overflow example:
140                 stackoverflow.com/questions/2912520/read-file-contents-into-a-string-in-c
141 */
142 std::shared_ptr<xapp::Jhash> xapp::Config::jparse( std::string ufname ) {
143         fname = ufname;
144
145         std::ifstream ifs( fname );
146         std::string st( (std::istreambuf_iterator<char>( ifs ) ), (std::istreambuf_iterator<char>() ) );
147
148         auto new_jh = std::shared_ptr<xapp::Jhash>( new xapp::Jhash( st.c_str() ) );
149         return  new_jh->Parse_errors() ? NULL : new_jh;
150 }
151
152 /*
153         Read the configuration file from what we find as the filename in the environment (assumed
154         to be referenced by $XAPP_DESCRIPTOR_PATH/config-file.json. If the env var is not
155         defined we assume ./.  The data is then parsed with the assumption that it's json.
156
157         The actual meaning of the environment variable is confusing. The name is "path" which
158         should mean that this is the directory in which the config file lives, but the examples
159         elsewhere suggest that this is a filename (either fully qualified or relative). For now
160         we will assume that it's a file name, though we could add some intelligence to determine
161         if it's a directory name or not if it comes to it.
162 */
163 std::shared_ptr<xapp::Jhash> xapp::Config::jparse( ) {
164         const char*     data;
165
166         if( (data = getenv( (const char *) "XAPP_DESCRIPTOR_PATH" )) == NULL ) {
167                 data = (const char *) "./config-file.json";
168         }
169
170         return jparse( std::string( data ) );
171 }
172
173 // --------------------- construction, destruction -------------------------------------------
174
175 /*
176         By default expect to find XAPP_DESCRIPTOR_PATH in the environment and assume that it
177         is the directory name where we can find config-file.json. The build process will
178         read and parse the json allowing the user xAPP to invoke the supplied  "obvious"
179         functions to retrieve data.  If there is something in an xAPP's config that isn't
180         standard, it can get the raw Jhash object and go at it directly. The idea is that
181         the common things should be fairly painless to extract from the json goop.
182 */
183 xapp::Config::Config() :
184         jh( jparse() )
185 { /* empty body */ }
186
187 /*
188         Similar, except that it allows the xAPP to supply the filename (testing?)
189 */
190 xapp::Config::Config( const std::string& fname) :
191         jh( jparse( fname ) )
192 { /* empty body */ }
193
194
195 /*
196         Read and return the raw file blob as a single string. User can parse, or do
197         whatever they need (allows non-json things if necessary).
198 */
199 std::string xapp::Config::Get_contents( ) const {
200         std::string rv = "";
201
202         if( ! fname.empty() ) {
203                 std::ifstream ifs( fname );
204                 std::string st( (std::istreambuf_iterator<char>( ifs ) ), (std::istreambuf_iterator<char>() ) );
205                 rv = st;
206         }
207
208         return rv;
209 }
210
211 // ----- convience function for things we think an xAPP will likely need to pull from the config
212
213
214 /*
215         Suss out the port for the named "interface". The interface is likely the application
216         name.
217 */
218 std::string xapp::Config::Get_port( const std::string& name ) const {
219         int i;
220         int     nele = 0;
221         double value;
222         std::string rv = "";            // result value
223         std::string pname;                      // element port name in the json
224
225         if( jh == NULL ) {
226                 return rv;
227         }
228
229         jh->Unset_blob();
230         if( jh->Set_blob( (const char *) "messaging" ) ) {
231                 nele = jh->Array_len( (const char *) "ports" );
232                 for( i = 0; i < nele; i++ ) {
233                         if( jh->Set_blob_ele( (const char *) "ports", i ) ) {
234                                 pname = jh->String( (const char *) "name" );
235                                 if( pname.compare( name ) == 0 ) {                              // this element matches the name passed in
236                                         value = jh->Value( (const char *) "port" );
237                                         rv = std::to_string( (int) value );
238                                         jh->Unset_blob( );                                                      // leave hash in a known state
239                                         return rv;
240                                 }
241                         }
242
243                         jh->Unset_blob( );                                                              // Jhash requires bump to root, and array reselct to move to next ele
244                         jh->Set_blob( (const char *) "messaging" );
245                 }
246         }
247
248         jh->Unset_blob();
249         return rv;
250 }
251
252 /*
253         Suss out the named string from the controls object. If the resulting value is
254         missing or "", then the default is returned.
255 */
256 std::string xapp::Config::Get_control_str( const std::string& name, const std::string& defval ) const {
257         std::string value;
258         std::string rv;                         // result value
259
260         rv = defval;
261         if( jh == NULL ) {
262                 return rv;
263         }
264
265         jh->Unset_blob();
266         if(    jh->Set_blob( (const char *) "controls" ) && jh->Exists( name.c_str() ) )  {
267                 value = jh->String( name.c_str() );
268                 if( value.compare( "" ) != 0 ) {
269                         rv = value;
270                 }
271         }
272
273         jh->Unset_blob();
274         return rv;
275 }
276
277 /*
278         Convenience funciton without default. "" returned if not found.
279         No default value; returns "" if not set.
280 */
281 std::string xapp::Config::Get_control_str( const std::string& name ) const {
282         return Get_control_str( name, "" );
283 }
284
285 /*
286         Suss out the named field from the controls object with the assumption that it is a boolean.
287         If the resulting value is missing then the defval is used.
288 */
289 bool xapp::Config::Get_control_bool( const std::string& name, bool defval ) const {
290         bool rv;                                // result value
291
292         rv = defval;
293         if( jh == NULL ) {
294                 return rv;
295         }
296
297         jh->Unset_blob();
298         if( jh->Set_blob( (const char *) "controls" )  &&  jh->Exists( name.c_str() ) )  {
299                 rv = jh->Bool( name.c_str() );
300         }
301
302         jh->Unset_blob();
303         return rv;
304 }
305
306
307 /*
308         Convenience function without default.
309 */
310 bool xapp::Config::Get_control_bool( const std::string& name ) const {
311         return Get_control_bool( name, false );
312 }
313
314
315 /*
316         Suss out the named field from the controls object with the assumption that it is a value (float/int).
317         If the resulting value is missing then the defval is used.
318 */
319 double xapp::Config::Get_control_value( const std::string& name, double defval ) const {
320
321         auto rv = defval;                               // return value; set to default
322         if( jh == NULL ) {
323                 return rv;
324         }
325
326         jh->Unset_blob();
327         if( jh->Set_blob( (const char *) "controls" )  && jh->Exists( name.c_str() ) )  {
328                 rv = jh->Value( name.c_str() );
329         }
330
331         jh->Unset_blob();
332         return rv;
333 }
334
335
336 /*
337         Convenience function. If value is undefined, then 0 is returned.
338 */
339 double xapp::Config::Get_control_value( const std::string& name ) const {
340         return Get_control_value( name, 0.0 );
341 }
342
343
344 // ---- notification support ---------------------------------------------------------------
345
346
347 /*
348         Accept the user's notification function, and data that it needs (pointer to
349         something unknown), and stash that as a callback.
350
351         The fact that the user xAPP registers a callback also triggers the creation
352         of a thread to listen for changes on the config file.
353 */
354 void xapp::Config::Set_callback( notify_callback usr_func, void* usr_data ) {
355         cb = std::unique_ptr<Config_cb>( new Config_cb( usr_func, usr_data ) );
356         user_cb_data = usr_data;
357
358         if( listener == NULL ) {                                // start thread if needed
359                 listener = new std::thread( &xapp::Config::Listener, this );
360         }
361 }
362
363 } // namespace