9cfa420b9a95d5165c4f0acaf0d56821586e6d7a
[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         struct inotify_event*   ie;             // event that popped
75         int ifd;                                                // the inotify file des
76         int     wfd;                                            // the watched file des
77         int     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, (char *) dname, IN_MOVED_TO | IN_CLOSE_WRITE );           // we only care about close write changes
100
101         if( wfd < 0 ) {
102                 fprintf( stderr, "<XFCPP> ### ERR ### unable to add watch on config file %s: %s\n", fname.c_str(), strerror( errno ) );
103                 return;
104         }
105
106         while( true ) {
107                 n = read( ifd, rbuf, sizeof( rbuf ) );                          // read the event
108                 if( n < 0  ) {
109                         if( errno == EAGAIN ) {
110                                 continue;
111                         } else {
112                                 fprintf( stderr, "<XFCPP ### CRIT ### config listener read err: %s\n", strerror( errno ) );
113                                 return;
114                         }
115                 }
116
117                 ie = (inotify_event *) rbuf;
118                 if( ie->len > 0 && strcmp( bname, ie->name ) == 0  ) {
119                         // TODO: lock
120                         auto njh = jparse( fname );                                                     // reparse the file
121                         // TODO: unlock
122
123                         if( njh != NULL && cb != NULL ) {                               // good parse, save and drive user callback
124                                 jh = njh;
125                                 cb->Drive_cb( *this, user_cb_data );
126                         }
127                 }
128         }
129
130         free( dname );
131         free( bname );
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         std::string st( (std::istreambuf_iterator<char>( ifs ) ), (std::istreambuf_iterator<char>() ) );
148
149         auto new_jh = std::shared_ptr<xapp::Jhash>( new xapp::Jhash( st.c_str() ) );
150         return  new_jh->Parse_errors() ? NULL : new_jh;
151 }
152
153 /*
154         Read the configuration file from what we find as the filename in the environment (assumed
155         to be referenced by $XAPP_DESCRIPTOR_PATH/config-file.json. If the env var is not
156         defined we assume ./.  The data is then parsed with the assumption that it's json.
157
158         The actual meaning of the environment variable is confusing. The name is "path" which
159         should mean that this is the directory in which the config file lives, but the examples
160         elsewhere suggest that this is a filename (either fully qualified or relative). For now
161         we will assume that it's a file name, though we could add some intelligence to determine
162         if it's a directory name or not if it comes to it.
163 */
164 std::shared_ptr<xapp::Jhash> xapp::Config::jparse( ) {
165         char*   data;
166
167         if( (data = getenv( (char *) "XAPP_DESCRIPTOR_PATH" )) == NULL ) {
168                 data =  (char *) "./config-file.json";
169         }
170
171         return jparse( std::string( data ) );
172 }
173
174 // --------------------- construction, destruction -------------------------------------------
175
176 /*
177         By default expect to find XAPP_DESCRIPTOR_PATH in the environment and assume that it
178         is the directory name where we can find config-file.json. The build process will
179         read and parse the json allowing the user xAPP to invoke the supplied  "obvious"
180         functions to retrieve data.  If there is something in an xAPP's config that isn't
181         standard, it can get the raw Jhash object and go at it directly. The idea is that
182         the common things should be fairly painless to extract from the json goop.
183 */
184 xapp::Config::Config() :
185         jh( jparse() ),
186         listener( NULL )
187 { /* empty body */ }
188
189 /*
190         Similar, except that it allows the xAPP to supply the filename (testing?)
191 */
192 xapp::Config::Config( std::string fname) :
193         jh( jparse( fname ) ),
194         listener( NULL )
195 { /* empty body */ }
196
197
198 /*
199         Read and return the raw file blob as a single string. User can parse, or do
200         whatever they need (allows non-json things if necessary).
201 */
202 std::string xapp::Config::Get_contents( ) {
203         std::string rv = "";
204
205         if( ! fname.empty() ) {
206                 std::ifstream ifs( fname );
207                 std::string st( (std::istreambuf_iterator<char>( ifs ) ), (std::istreambuf_iterator<char>() ) );
208                 rv = st;
209         }
210
211         return rv;
212 }
213
214 // ----- convience function for things we think an xAPP will likely need to pull from the config
215
216
217 /*
218         Suss out the port for the named "interface". The interface is likely the application
219         name.
220 */
221 std::string xapp::Config::Get_port( std::string name ) {
222         int i;
223         int     nele = 0;
224         double value;
225         std::string rv = "";            // result value
226         std::string pname;                      // element port name in the json
227
228         if( jh == NULL ) {
229                 return rv;
230         }
231
232         jh->Unset_blob();
233         if( jh->Set_blob( (char *) "messaging" ) ) {
234                 nele = jh->Array_len( (char *) "ports" );
235                 for( i = 0; i < nele; i++ ) {
236                         if( jh->Set_blob_ele( (char *) "ports", i ) ) {
237                                 pname = jh->String( (char *) "name" );
238                                 if( pname.compare( name ) == 0 ) {                              // this element matches the name passed in
239                                         value = jh->Value( (char *) "port" );
240                                         rv = std::to_string( (int) value );
241                                         jh->Unset_blob( );                                                      // leave hash in a known state
242                                         return rv;
243                                 }
244                         }
245
246                         jh->Unset_blob( );                                                              // Jhash requires bump to root, and array reselct to move to next ele
247                         jh->Set_blob( (char *) "messaging" );
248                 }
249         }
250
251         jh->Unset_blob();
252         return rv;
253 }
254
255 /*
256         Suss out the named string from the controls object. If the resulting value is
257         missing or "", then the default is returned.
258 */
259 std::string xapp::Config::Get_control_str( std::string name, std::string defval ) {
260         std::string value;
261         std::string rv;                         // result value
262
263         rv = defval;
264         if( jh == NULL ) {
265                 return rv;
266         }
267
268         jh->Unset_blob();
269         if( jh->Set_blob( (char *) "controls" ) ) {
270                 if( jh->Exists( name.c_str() ) )  {
271                         value = jh->String( name.c_str() );
272                         if( value.compare( "" ) != 0 ) {
273                                 rv = value;
274                         }
275                 }
276         }
277
278         jh->Unset_blob();
279         return rv;
280 }
281
282 /*
283         Convenience funciton without default. "" returned if not found.
284         No default value; returns "" if not set.
285 */
286 std::string xapp::Config::Get_control_str( std::string name ) {
287         return Get_control_str( name, "" );
288 }
289
290 /*
291         Suss out the named field from the controls object with the assumption that it is a boolean.
292         If the resulting value is missing then the defval is used.
293 */
294 bool xapp::Config::Get_control_bool( std::string name, bool defval ) {
295         bool value;
296         bool rv;                                // result value
297
298         rv = defval;
299         if( jh == NULL ) {
300                 return rv;
301         }
302
303         jh->Unset_blob();
304         if( jh->Set_blob( (char *) "controls" ) ) {
305                 if( jh->Exists( name.c_str() ) )  {
306                         rv = jh->Bool( name.c_str() );
307                 }
308         }
309
310         jh->Unset_blob();
311         return rv;
312 }
313
314
315 /*
316         Convenience function without default.
317 */
318 bool xapp::Config::Get_control_bool( std::string name ) {
319         return Get_control_bool( name, false );
320 }
321
322
323 /*
324         Suss out the named field from the controls object with the assumption that it is a value (float/int).
325         If the resulting value is missing then the defval is used.
326 */
327 double xapp::Config::Get_control_value( std::string name, double defval ) {
328         double value;
329
330         auto rv = defval;                               // return value; set to default
331         if( jh == NULL ) {
332                 return rv;
333         }
334
335         jh->Unset_blob();
336         if( jh->Set_blob( (char *) "controls" ) ) {
337                 if( jh->Exists( name.c_str() ) )  {
338                         rv = jh->Value( name.c_str() );
339                 }
340         }
341
342         jh->Unset_blob();
343         return rv;
344 }
345
346
347 /*
348         Convenience function. If value is undefined, then 0 is returned.
349 */
350 double xapp::Config::Get_control_value( std::string name ) {
351         return Get_control_value( name, 0.0 );
352 }
353
354
355 // ---- notification support ---------------------------------------------------------------
356
357
358 /*
359         Accept the user's notification function, and data that it needs (pointer to
360         something unknown), and stash that as a callback.
361
362         The fact that the user xAPP registers a callback also triggers the creation
363         of a thread to listen for changes on the config file.
364 */
365 void xapp::Config::Set_callback( notify_callback usr_func, void* usr_data ) {
366         cb = std::unique_ptr<Config_cb>( new Config_cb( usr_func, usr_data ) );
367         user_cb_data = usr_data;
368
369         if( listener == NULL ) {                                // start thread if needed
370                 listener = new std::thread( &xapp::Config::Listener, this );
371         }
372 }
373
374 } // namespace