Fix $XAPP_DESCRIPTOR_PATH parser bug
[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 #include <sys/stat.h>
48
49 #include <iostream>
50 #include <sstream>
51 #include <fstream>
52 #include <thread>
53 #include <memory>
54
55 #include "jhash.hpp"
56 #include "config.hpp"
57 #include "config_cb.hpp"
58
59 namespace xapp {
60
61
62 // ----- private things --------
63 /*
64         Notification listener. This function is started in a thread and listens for
65         changes to the config file. When it sees an interesting change (write close)
66         to the file, then it will read the new set of json, parse it, and drive the
67         user callback.
68
69         We must watch the directory containing the file as if the file is edited and
70         replaced it's likely saved with a different referencing inode. We'd see the
71         first change, but not any subsequent changes as the inotify is based on inodes
72         and not directory entries.
73 */
74 void xapp::Config::Listener( ) {
75         const struct inotify_event*     ie;     // event that popped
76         int ifd;                                                // the inotify file des
77         int     wfd;                                            // the watched file des
78         ssize_t n;
79         char    rbuf[4096];                             // large read buffer as the event is var len
80         char*   dname;                                  // directory name
81         char*   bname;                                  // basename
82         char*   tok;
83
84         ifd = inotify_init1( 0 );               // initialise watcher setting blocking read (no option)
85         if( ifd < 0 ) {
86                 fprintf( stderr, "<XFCPP> ### ERR ### unable to initialise file watch %s\n", strerror( errno ) );
87                 return;
88         }
89
90         dname = strdup( fname.c_str() );                                        // defrock the file name into dir and basename
91         if( (tok = strrchr( dname, '/' )) != NULL ) {
92                 *tok = 0;
93                 bname = strdup( tok+1 );
94         } else {
95                 free (dname);
96                 dname = strdup( "." );
97                 bname = strdup( fname.c_str() );
98         }
99
100         wfd = inotify_add_watch( ifd, dname, IN_MOVED_TO | IN_CLOSE_WRITE );            // we only care about close write changes
101         free (dname);
102
103         if( wfd < 0 ) {
104                 fprintf( stderr, "<XFCPP> ### ERR ### unable to add watch on config file %s: %s\n", fname.c_str(), strerror( errno ) );
105                 free( bname );
106                 return;
107         }
108
109         while( true ) {
110                 n = read( ifd, rbuf, sizeof( rbuf ) );                          // read the event
111                 if( n < 0  ) {
112                         if( errno == EAGAIN ) {
113                                 continue;
114                         } else {
115                                 fprintf( stderr, "<XFCPP> ### CRIT ### config listener read err: %s\n", strerror( errno ) );
116                                 return;
117                         }
118                 }
119
120                 ie = (inotify_event *) rbuf;
121                 if( ie->len > 0 && strcmp( bname, ie->name ) == 0  ) {
122                         // future: lock
123                         auto njh = jparse( fname );                                                     // reparse the file
124                         // future: unlock
125
126                         if( njh != NULL && cb != NULL ) {                               // good parse, save and drive user callback
127                                 jh = njh;
128                                 cb->Drive_cb( *this, user_cb_data );
129                         }
130                 }
131         }
132 }
133
134
135 /*
136         Read a file containing json and parse into a framework Jhash.
137
138         Using C i/o will speed this up, but I can't imagine that we need
139         speed reading the config file once in a while.
140         The file read comes from a stack overflow example:
141                 stackoverflow.com/questions/2912520/read-file-contents-into-a-string-in-c
142 */
143 std::shared_ptr<xapp::Jhash> xapp::Config::jparse( std::string ufname ) {
144         fname = ufname;
145
146         std::ifstream ifs( fname );
147         if( ! ifs.is_open() ) {
148                 fprintf( stderr, "<XFCPP> ### WARN ### unable to open %s; %s\n", fname.c_str(), strerror( errno ) );
149         }
150
151         std::string st( (std::istreambuf_iterator<char>( ifs ) ), (std::istreambuf_iterator<char>() ) );
152
153         auto new_jh = std::shared_ptr<xapp::Jhash>( new xapp::Jhash( st.c_str() ) );
154         return  new_jh->Parse_errors() ? NULL : new_jh;
155 }
156
157 /*
158         Read the configuration file from what we find as the filename in the environment (assumed
159         to be referenced by $XAPP_DESCRIPTOR_PATH/config-file.json. If the env var is not
160         defined we assume ./.  The data is then parsed with the assumption that it's json.
161
162         The actual meaning of the environment variable is confusing. The name is "path" which
163         should mean that this is the directory in which the config file lives, but the examples
164         elsewhere suggest that this is a filename (either fully qualified or relative). To prevent
165         errors, we use some intelligence to determine if it's a directory name or not if it comes to it.
166 */
167 std::shared_ptr<xapp::Jhash> xapp::Config::jparse( ) {
168         const char*     data;
169         std::string filename;
170         struct stat sb;
171
172         data = getenv( (const char *) "XAPP_DESCRIPTOR_PATH" );
173         if( data != NULL ) {
174                 filename = data;
175                 if( stat( data, &sb ) == 0 ) {
176                         if( S_ISDIR( sb.st_mode ) ) {
177                                 filename.append( "/config-file.json" );
178                         }
179                 } else {
180                         fprintf( stderr, "<XFCPP> ### ERR ### unable to stat env XAPP_DESCRIPTOR_PATH: %s\n", strerror( errno ) );
181                 }
182
183         } else {
184                 filename = "./config-file.json";
185         }
186
187         return jparse( filename );
188 }
189
190 // --------------------- construction, destruction -------------------------------------------
191
192 /*
193         By default expect to find XAPP_DESCRIPTOR_PATH in the environment and assume that it
194         is the directory name where we can find config-file.json. The build process will
195         read and parse the json allowing the user xAPP to invoke the supplied  "obvious"
196         functions to retrieve data.  If there is something in an xAPP's config that isn't
197         standard, it can get the raw Jhash object and go at it directly. The idea is that
198         the common things should be fairly painless to extract from the json goop.
199 */
200 xapp::Config::Config() :
201         jh( jparse() )
202 { /* empty body */ }
203
204 /*
205         Similar, except that it allows the xAPP to supply the filename (testing?)
206 */
207 xapp::Config::Config( const std::string& fname) :
208         jh( jparse( fname ) )
209 { /* empty body */ }
210
211
212 /*
213         Read and return the raw file blob as a single string. User can parse, or do
214         whatever they need (allows non-json things if necessary).
215 */
216 std::string xapp::Config::Get_contents( ) const {
217         std::string rv = "";
218
219         if( ! fname.empty() ) {
220                 std::ifstream ifs( fname );
221                 std::string st( (std::istreambuf_iterator<char>( ifs ) ), (std::istreambuf_iterator<char>() ) );
222                 rv = st;
223         }
224
225         return rv;
226 }
227
228 // ----- convience function for things we think an xAPP will likely need to pull from the config
229
230
231 /*
232         Suss out the port for the named "interface". The interface is likely the application
233         name.
234 */
235 std::string xapp::Config::Get_port( const std::string& name ) const {
236         int i;
237         int     nele = 0;
238         double value;
239         std::string rv = "";            // result value
240         std::string pname;                      // element port name in the json
241
242         if( jh == NULL ) {
243                 return rv;
244         }
245
246         jh->Unset_blob();
247         if( jh->Set_blob( (const char *) "messaging" ) ) {
248                 nele = jh->Array_len( (const char *) "ports" );
249                 for( i = 0; i < nele; i++ ) {
250                         if( jh->Set_blob_ele( (const char *) "ports", i ) ) {
251                                 pname = jh->String( (const char *) "name" );
252                                 if( pname.compare( name ) == 0 ) {                              // this element matches the name passed in
253                                         value = jh->Value( (const char *) "port" );
254                                         rv = std::to_string( (int) value );
255                                         jh->Unset_blob( );                                                      // leave hash in a known state
256                                         return rv;
257                                 }
258                         }
259
260                         jh->Unset_blob( );                                                              // Jhash requires bump to root, and array reselct to move to next ele
261                         jh->Set_blob( (const char *) "messaging" );
262                 }
263         }
264
265         jh->Unset_blob();
266         return rv;
267 }
268
269 /*
270         Suss out the named string from the controls object. If the resulting value is
271         missing or "", then the default is returned.
272 */
273 std::string xapp::Config::Get_control_str( const std::string& name, const std::string& defval ) const {
274         std::string value;
275         std::string rv;                         // result value
276
277         rv = defval;
278         if( jh == NULL ) {
279                 return rv;
280         }
281
282         jh->Unset_blob();
283         if(    jh->Set_blob( (const char *) "controls" ) && jh->Exists( name.c_str() ) )  {
284                 value = jh->String( name.c_str() );
285                 if( value.compare( "" ) != 0 ) {
286                         rv = value;
287                 }
288         }
289
290         jh->Unset_blob();
291         return rv;
292 }
293
294 /*
295         Convenience funciton without default. "" returned if not found.
296         No default value; returns "" if not set.
297 */
298 std::string xapp::Config::Get_control_str( const std::string& name ) const {
299         return Get_control_str( name, "" );
300 }
301
302 /*
303         Suss out the named field from the controls object with the assumption that it is a boolean.
304         If the resulting value is missing then the defval is used.
305 */
306 bool xapp::Config::Get_control_bool( const std::string& name, bool defval ) const {
307         bool rv;                                // result value
308
309         rv = defval;
310         if( jh == NULL ) {
311                 return rv;
312         }
313
314         jh->Unset_blob();
315         if( jh->Set_blob( (const char *) "controls" )  &&  jh->Exists( name.c_str() ) )  {
316                 rv = jh->Bool( name.c_str() );
317         }
318
319         jh->Unset_blob();
320         return rv;
321 }
322
323
324 /*
325         Convenience function without default.
326 */
327 bool xapp::Config::Get_control_bool( const std::string& name ) const {
328         return Get_control_bool( name, false );
329 }
330
331
332 /*
333         Suss out the named field from the controls object with the assumption that it is a value (float/int).
334         If the resulting value is missing then the defval is used.
335 */
336 double xapp::Config::Get_control_value( const std::string& name, double defval ) const {
337
338         auto rv = defval;                               // return value; set to default
339         if( jh == NULL ) {
340                 return rv;
341         }
342
343         jh->Unset_blob();
344         if( jh->Set_blob( (const char *) "controls" )  && jh->Exists( name.c_str() ) )  {
345                 rv = jh->Value( name.c_str() );
346         }
347
348         jh->Unset_blob();
349         return rv;
350 }
351
352
353 /*
354         Convenience function. If value is undefined, then 0 is returned.
355 */
356 double xapp::Config::Get_control_value( const std::string& name ) const {
357         return Get_control_value( name, 0.0 );
358 }
359
360
361 // ---- notification support ---------------------------------------------------------------
362
363
364 /*
365         Accept the user's notification function, and data that it needs (pointer to
366         something unknown), and stash that as a callback.
367
368         The fact that the user xAPP registers a callback also triggers the creation
369         of a thread to listen for changes on the config file.
370 */
371 void xapp::Config::Set_callback( notify_callback usr_func, void* usr_data ) {
372         cb = std::unique_ptr<Config_cb>( new Config_cb( usr_func, usr_data ) );
373         user_cb_data = usr_data;
374
375         if( listener == NULL ) {                                // start thread if needed
376                 listener = new std::thread( &xapp::Config::Listener, this );
377         }
378 }
379
380 } // namespace