fbmon.cpp (13239B)
1 // Copyright (c) 2020 Morel BĂ©renger 2 // 3 // This software is provided 'as-is', without any express or implied 4 // warranty. In no event will the authors be held liable for any damages 5 // arising from the use of this software. 6 // 7 // Permission is granted to anyone to use this software for any purpose, 8 // including commercial applications, and to alter it and redistribute it 9 // freely, subject to the following restrictions: 10 // 11 // 1. The origin of this software must not be misrepresented; you must not 12 // claim that you wrote the original software. If you use this software 13 // in a product, an acknowledgment in the product documentation would be 14 // appreciated but is not required. 15 // 2. Altered source versions must be plainly marked as such, and must not be 16 // misrepresented as being the original software. 17 // 3. This notice may not be removed or altered from any source distribution. 18 19 #ifdef LIBCPP_MUSL_STATIC 20 #define __GLIBC_PREREQ(x,y) 0 21 #endif 22 23 #include <string.h> 24 #include <assert.h> 25 #include <math.h> 26 #include <limits.h> 27 28 #include <iterator> 29 #include <algorithm> 30 31 #include <sys/types.h> 32 #include <sys/stat.h> 33 #include <fcntl.h> 34 #include <unistd.h> 35 #include <poll.h> 36 37 #include <termios.h> 38 #include <sys/ioctl.h> 39 40 #include <utils.hpp> 41 #include <vector.hpp> 42 #include <optparser.hpp> 43 44 #include "service_info.hpp" 45 46 // compatibility: 47 // this code assumes (because of inotify API and behavior) a linux 48 // kernel, v2.6.21 minimum. Previous versions may contain bugs, but 49 // I won't hunt them. If you spot one, feel free to send the patch, 50 // *if* it does not makes the code more complex. 51 #include <linux/limits.h> 52 #include <sys/inotify.h> 53 54 void print_help( char const* pgm, FILE* target, opt_desc_t const* start, opt_desc_t const* end ); 55 56 static const char serv_infos_desc[] = 57 "description of a service to watch, separated with ':': path:filename:start_regex:death_regex:alive_delay"; 58 59 bool close_file( service_info_t& svc ); 60 bool linux_event_handling( size_t start, vector<service_info_t>& svcs, vector<pollfd>& pfds ); 61 void term_resize( int ); 62 int update_screen_info( void ); 63 64 static bool term_resized = true; 65 area_t g_screen_area; 66 67 int main( int argc, char **argv ) 68 { 69 vector<service_info_t> services; 70 vector<pollfd> pfds; 71 int refresh_delay = 1;//TODO: use. Or remove, not sure. Why keep? To show date? 72 //int fbdev; 73 //char* fbpath = nullptr; 74 //char* ftpath = nullptr; 75 76 opt_desc_t opts[] = 77 { 78 STD_HELP, 79 { "service", serv_infos_desc, 's', 0, &services, add_serv_info, show_serv_info }, 80 { "refresh_delay", "seconds between 2 refresh (r<0: on event only)", 'r', 0, &refresh_delay, set<int>, show<int> }, 81 //{ "framebuffer", "path to the framebuffer device", 'f', 0, &fbpath, set<char const**>, show<char*> }, 82 //{ "font", "path to the font to use (PSF formats)", 'F', 0, &ftpath, set<char const**>, show<char*> }, 83 }; 84 85 // adds the "master" inotify watched 86 // must be done before parsing arguments 87 g_inotify_watch_fd = inotify_init(); 88 if( -1 == g_inotify_watch_fd ) 89 { 90 syserr( "failed to initialise inotify" ); 91 return EXIT_FAILURE; 92 } 93 fputs( "inotify initialised\n", stderr ); 94 95 //TODO: catch resize events 96 if( -1 == update_screen_info() ) 97 { 98 syserr( "failed to retrieve screen's size (input term, really)" ); 99 return EXIT_FAILURE; 100 } 101 102 auto b_opts = std::begin( opts ); 103 auto e_opts = std::end( opts ); 104 105 char **arg = &argv[1]; 106 assert( argc > 0 ); 107 for( int iarg = 1; iarg != argc; ++iarg, ++arg ) 108 { 109 auto error = parse_cmd_opt( *arg, b_opts, e_opts ); 110 switch( error ) 111 { 112 case MAX_COUNT: 113 arg_warning( *arg, error ); 114 break; 115 case NONE: 116 case IGNORED: 117 break; 118 case SET_NO_VAL: 119 case SET_VAL_IGN: 120 case SET_FAIL: 121 case BAD_ARGS: 122 case BAD_SETTER: 123 print_help( argv[0], stderr, b_opts, e_opts ); 124 arg_error( *arg, error ); 125 return EXIT_FAILURE; 126 } 127 } 128 129 if( opts[0].count ) //check if --help was found 130 { 131 print_help( argv[0], stderr, b_opts, e_opts ); 132 return EXIT_SUCCESS; 133 } 134 if( services.empty() ) 135 { 136 print_help( argv[0], stderr, b_opts, e_opts ); 137 return EXIT_FAILURE; 138 } 139 140 // this program works with the assumption already opened FDs 141 // are "packed", the numbers do not have holes. This assumption 142 // is done for ease of code and performance reasons. 143 // reserve nb services pollfd instances plus: 144 // _ inotify fd 145 const size_t INOTIF_FD = pfds.size(); 146 pfds.reserve( pfds.size() + 1 + INOTIF_FD + services.size() ); 147 pfds.push_back( { g_inotify_watch_fd, POLLIN | POLLPRI, 0 } ); 148 149 uint8_t max_width = 0; 150 for( auto const &svc : services ) 151 { 152 pfds.push_back( { svc.file_fd, POLLIN|POLLPRI, 0 } ); 153 //compute max width to use with some margins 154 max_width = std::max( max_width, static_cast<uint8_t>( name_sz( svc ) + 3 ) ); 155 fprintf( stderr, "now polling fd %d\n", svc.file_fd ); 156 } 157 const uint16_t max_cols = static_cast<uint16_t>( ceil( g_screen_area.w / static_cast<float>( max_width ) ) ); 158 const uint16_t col_size = g_screen_area.w / max_cols; 159 const uint16_t row_size = 2; 160 161 uint16_t col = 0, row = 0; 162 for( auto &svc : services ) 163 { 164 svc.draw_area.x = 2 + col * col_size; 165 svc.draw_area.y = 1 + row * row_size; 166 //TODO: implement limits, so that a more graphical mode can be implemented 167 //svc.draw_area.w = ...; 168 //svc.draw_area.h = ...; 169 ++col; 170 row += col / max_cols; 171 col %= max_cols; 172 } 173 174 int nbev; 175 bool stop = false; 176 assert( g_inotify_watch_fd != 0 && "TODO: 0 can't be disabled in poll" ); 177 time_t prev_time = time( nullptr ); 178 time_t curr_time; 179 ssize_t dist_time = 0; 180 while( !stop ) 181 { 182 if( dist_time == 0 ) 183 { 184 dist_time = refresh_delay; 185 } 186 assert( dist_time * 1000 <= INT_MAX ); 187 nbev = poll( pfds.data(), pfds.size(), static_cast<int>( dist_time * 1000 ) ); 188 curr_time = time( nullptr ); 189 dist_time = curr_time - prev_time; 190 prev_time = curr_time; 191 //got interrupted or reached timeout 192 if( -1 == nbev || 0 == nbev ) 193 { 194 for( size_t i = 1; i < pfds.size(); ++i ) 195 { 196 refresh( services[i-1], dist_time ); 197 } 198 continue; 199 } 200 201 for( size_t i = 0; i < pfds.size(); ++i ) 202 { 203 short rev = pfds[i].revents; 204 if( 0 == rev ) 205 { 206 continue; 207 } 208 209 // dynamic FDs: target files 210 if( i > INOTIF_FD ) 211 { 212 if( rev & ( POLLIN | POLLPRI ) ) 213 { 214 update( services[i-1] ); 215 pfds[i].fd = -pfds[i].fd; 216 } 217 refresh( services[i-1], dist_time ); 218 } 219 else 220 { 221 assert( g_inotify_watch_fd >= 0 ); 222 if( INOTIF_FD == i && linux_event_handling( INOTIF_FD, services, pfds ) ) 223 { 224 abort(); 225 } 226 } 227 } 228 } 229 return EXIT_SUCCESS; 230 } 231 232 void term_resize( int ) 233 { 234 term_resized = true; 235 } 236 237 int update_screen_info( void ) 238 { 239 winsize wsz; 240 if( term_resized ) 241 { 242 int ioc = ioctl( STDIN_FILENO, TIOCGWINSZ, &wsz ); 243 //TODO move in loop 244 //TODO don't do anything as long as areas are not ok 245 //if( -1 == ioc && errno != EINTR ) 246 if( -1 == ioc && errno != EINTR ) 247 { 248 perror( "open" ); 249 return -1; 250 } 251 if( g_screen_area.w != wsz.ws_row || g_screen_area.h != wsz.ws_col ) 252 { 253 g_screen_area.x = 0; 254 g_screen_area.y = 0; 255 g_screen_area.w = wsz.ws_col; 256 g_screen_area.h = wsz.ws_row; 257 // TODO update all client's areas 258 } 259 return 0; 260 } 261 return -1; 262 } 263 264 void print_help( char const* pgm, FILE* target, opt_desc_t const* start, opt_desc_t const* end ) 265 { 266 fputs( "Usage: ", target ); 267 fputs( pgm, target ); 268 fputs( " [OPTIONS]\n" 269 "Description:\n" 270 "This program constantly watches log files to guess a service's state: starting, alive, dead.\n" 271 "Guessing is based on 3 things:\n" 272 "\t* a \"starting\" regex;\n" 273 "\t* a \"dead\" regex;\n" 274 "\t* a time-to-start delay, in seconds;\n" 275 "\n" 276 "Results are printed on choosen TTY.\n" 277 "For each watched file:\n" 278 "\t* full path of file\n" 279 "\t* current status\n" 280 "\t* timestamp of last known running state\n" 281 "\t* timestamp of last known death\n" 282 "\n" 283 "Regex syntax POSIX Extended (for now)\n" 284 //"Regex syntax is intentionnally trivial and a lot simpler that what you might know, only reserved characters are:\n" 285 //"\t* '*': matches the SMALLER possible string\n" 286 //"\t* '(': start the ONLY allowed capture\n" 287 //"\t* ')': ends the capture\n" 288 //"\t* '|': ends the regex\n" 289 //"\t* '\\': escape next character\n" 290 "TODO:\n" 291 "do not scoll, instead, update informations\n" 292 "show an even log\n" 293 "have colours to know more easily a service's state\n" 294 "reduce footprint and dependencies.\n" 295 "static build (monitors should be 100% independent)\n" 296 "only work when the correct TTY is \"focused\"\n" 297 "Options:\n" 298 , target ); 299 print_opts( target, start, end ); 300 } 301 302 // for each service, there can be 2 kind of events registered: 303 // * event on parent directory: 304 // only interesting if pdir is removed (unregister (0ed it?) the 305 // service, close file if open) or if affected file is watched file 306 // * event on watched file: 307 // enable polling of target FD 308 // but it's also possible that polling was enabled for file. If so, it 309 // should stay enabled as long as read() returns the maximum number of bytes. 310 // 311 // read a chunk (can't do better, number of bytes there can't be guessed) 312 // for each event in chunk: 313 // locate the service it belongs to 314 // if event affects directory 315 // linked pollfd is inotify_fd + distance( services.begin(), service ) 316 // if event implies a file change of name or deletion: 317 // close FD in service 318 // if event is appearance of target 319 // open file and store into service 320 // polling becomes FD 321 // if event affects target file 322 // enable polling 323 // 324 325 // pfds[start]: inotify's pollfd 326 // pfds[start+x]: service N's pollfd 327 bool linux_event_handling( size_t start, vector<service_info_t>& svcs, vector<pollfd>& pfds ) 328 { 329 //that code taken from manpage is *really* ugly 330 char buf[sizeof(inotify_event) + PATH_MAX] __attribute__ ((aligned(__alignof__(struct inotify_event)))); 331 assert( pfds[start].fd > 0 ); 332 ssize_t len = read( pfds[start].fd, buf, sizeof( buf ) ); 333 if( -1 == len && errno == EINVAL ) 334 { 335 fputs( "failed to read: inotify's API is fucked up.", stderr ); 336 return true; 337 } 338 const inotify_event* event; 339 for( const char * ptr = buf; ptr < buf + len; ptr += sizeof( inotify_event ) + event->len ) 340 { 341 event = reinterpret_cast<const inotify_event*>( ptr ); 342 343 auto svc_it = std::find_if( svcs.begin(), svcs.end(), 344 [&]( service_info_t const& s ) 345 { 346 return s.dir_wfd == event->wd || s.file_wfd == event->wd; 347 } ); 348 if( svc_it == svcs.end() ) 349 { 350 continue; 351 } 352 353 assert( svc_it >= svcs.begin() ); 354 pollfd& pfd = pfds[start + 1 + static_cast<size_t>( svc_it - svcs.begin() ) ]; 355 // work on dir 356 if( svc_it->dir_wfd == event->wd ) 357 { 358 assert( event->mask & ( 359 IN_DELETE_SELF | IN_MOVE_SELF | // disable service 360 IN_DELETE | IN_MOVE | // close target FD and update pollfd 361 IN_CREATE // open target and updated pollfd 362 ) ); 363 364 if( 0 != strcmp( svc_it->path + 1 + svc_it->filename, event->name ) ) 365 { 366 continue; 367 } 368 // this will happen when a file gets IN_MOVE, because it can be both 369 // IN_MOVED_TO and IN_MOVED_FROM 370 if( event->mask & ( IN_DELETE | IN_MOVE ) && svc_it->file_fd != -1 ) 371 { 372 if( close_file( *svc_it ) ) 373 { 374 fputs( "fucking API!\n", stderr ); 375 } 376 pfd.fd = -1; 377 } 378 if( event->mask & ( IN_CREATE ) ) 379 { 380 //TODO: this code is everything but safe. Should be moved out of here. 381 assert( svc_it->file_fd == -1 ); 382 svc_it->file_wfd = inotify_add_watch( g_inotify_watch_fd, svc_it->path, 0 383 | IN_MODIFY 384 //| IN_MOVE_SELF // would be dup of dir_wfd, I guess 385 //| IN_DELETE_SELF // would be dup of dir_wfd, I guess 386 | IN_EXCL_UNLINK // don't care about unlinked files 387 ); 388 if( -1 == svc_it->file_wfd ) 389 { 390 fprintf( stderr, HEADER_LOG ": failed to watch file: \"%s\" %s(0x%x)", 391 svc_it->path, strerror( errno ), errno ); 392 return true; 393 } 394 if( -1 == ( pfd.fd = svc_it->file_fd = open( svc_it->path, O_RDONLY ) ) ) 395 { 396 syserr( "unable to open service's file" ); 397 return true; 398 } 399 } 400 if( event->mask & ( IN_DELETE_SELF | IN_MOVE_SELF ) ) 401 { 402 // failure here is not that much of a problem... 403 404 //TODO: actually remove both pollfd and service instances 405 // from the vectors (swap with last element and resize) 406 if( close_file( *svc_it ) ) 407 { 408 fputs( "fucking API!\n", stderr ); 409 } 410 if( -1 == inotify_rm_watch( g_inotify_watch_fd, svc_it->dir_wfd ) ) 411 { 412 syserr( "failed to remove dir WD" ); 413 } 414 svc_it->dir_wfd = -1; 415 svc_it->birth_time = UINT16_MAX; 416 pfd.fd = -1; 417 } 418 } 419 else 420 { 421 assert( event->mask & IN_MODIFY ); 422 assert( svc_it->file_fd >= 0 ); 423 pfd.fd = svc_it->file_fd; 424 } 425 426 assert( -1 != event->wd ); 427 assert( svc_it->dir_wfd != svc_it->file_wfd ); 428 } 429 return false; 430 } 431 432 bool close_file( service_info_t& svc ) 433 { 434 bool ret = false; 435 if( -1 == close( svc.file_fd ) ) 436 { 437 syserr( "failed to close service FD" ); 438 ret = true; 439 } 440 svc.file_fd = -1; 441 if( -1 == inotify_rm_watch( g_inotify_watch_fd, svc.file_wfd ) ) 442 { 443 syserr( "failed to remove file WD" ); 444 ret = true; 445 } 446 svc.file_wfd = -1; 447 return ret; 448 } 449 450 // red 451 char const* g_color_down = "41"; 452 // green 453 char const* g_color_up = "42"; 454 // yellow 455 char const* g_color_wake = "43";