tools

various tools
git clone git://deadbeef.fr/tools.git
Log | Files | Refs | README | LICENSE

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";