diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-25 07:14:17 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-25 07:14:17 +0000 |
commit | 531e7c3ed0d6ed15be0db22506e9a4dfbcfaf267 (patch) | |
tree | 346c213d4f3150d8334fcf924ddca53b6aacca54 /inotify-info.cpp | |
parent | Initial commit. (diff) | |
download | inotify-info-531e7c3ed0d6ed15be0db22506e9a4dfbcfaf267.tar.xz inotify-info-531e7c3ed0d6ed15be0db22506e9a4dfbcfaf267.zip |
Adding upstream version 0~20240209.upstream/0_20240209
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'inotify-info.cpp')
-rw-r--r-- | inotify-info.cpp | 1218 |
1 files changed, 1218 insertions, 0 deletions
diff --git a/inotify-info.cpp b/inotify-info.cpp new file mode 100644 index 0000000..71ed3a1 --- /dev/null +++ b/inotify-info.cpp @@ -0,0 +1,1218 @@ +/* + * Copyright 2021 Michael Sartain + * + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#define _GNU_SOURCE 1 + +#include <limits.h> +#include <dirent.h> +#include <fcntl.h> +#include <getopt.h> +#include <pthread.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <libgen.h> +#include <locale.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <syscall.h> +#include <unistd.h> +#include <sys/sysmacros.h> +#include <sys/statfs.h> + +#include <cstdint> +#include <string> +#include <vector> +#include <algorithm> +#include <unordered_set> +#include <unordered_map> + +#include "inotify-info.h" +#include "lfqueue/lfqueue.h" + +/* + * TODO + * - Comments + * - Disable color + */ + +static int g_verbose = 0; +static size_t g_numthreads = 32; + +/* true if at least one inotify watch is found in fdinfo files + * On a system with no active inotify watches, but which otherwise + * supports inotify watch info, this will prevent the watches column + * from being displayed. + * This case is indistinguishable from the case where the kernel does + * not support inotify watch info. + */ +static int g_kernel_provides_watches_info = 0; + +static char thousands_sep = ','; + +static std::vector< std::string > ignore_dirs; + +/* + * filename info + */ +struct filename_info_t +{ + ino64_t inode; // Inode number + dev_t dev; // Device ID containing file + std::string filename; +}; + +/* + * inotify process info + */ +struct procinfo_t +{ + pid_t pid = 0; + + // uid + uid_t uid = 0; + + // Count of inotify watches and instances + uint32_t watches = 0; + uint32_t instances = 0; + + // This appname or pid found in command line? + bool in_cmd_line = false; + + // Full executable path + std::string executable; + + // Executable basename + std::string appname; + + // Inotify fdset filenames + std::vector< std::string > fdset_filenames; + + // Device id map -> set of inodes for that device id + std::unordered_map< dev_t, std::unordered_set< ino64_t > > dev_map; +}; + +class lfqueue_wrapper_t +{ +public: + lfqueue_wrapper_t() { lfqueue_init( &queue ); } + ~lfqueue_wrapper_t() { lfqueue_destroy( &queue ); } + + void queue_directory( char *path ) { lfqueue_enq( &queue, path ); } + char *dequeue_directory() { return ( char * )lfqueue_deq( &queue ); } + +public: + typedef long long my_m256i __attribute__((__vector_size__(32), __aligned__(32))); + + union + { + lfqueue_t queue; + my_m256i align_buf[4]; // Align to 128 bytes + }; +}; + +/* + * shared thread data + */ +class thread_shared_data_t +{ +public: + bool init( uint32_t numthreads, const std::vector< procinfo_t > &inotify_proclist ); + +public: + // Array of queues - one per thread + std::vector< lfqueue_wrapper_t > dirqueues; + // Map of all inotify inodes watched to the set of devices they are on + std::unordered_map< ino64_t, std::unordered_set< dev_t > > inode_set; +}; + +/* + * thread info + */ +class thread_info_t +{ +public: + thread_info_t( thread_shared_data_t &tdata_in ) : tdata( tdata_in ) {} + ~thread_info_t() {} + + void queue_directory( char *path ); + char *dequeue_directory(); + + // Returns -1: queue empty, 0: open error, > 0 success + int parse_dirqueue_entry(); + + void add_filename( ino64_t inode, const char *path, const char *d_name, bool is_dir ); + +public: + uint32_t idx = 0; + pthread_t pthread_id = 0; + + thread_shared_data_t &tdata; + + // Total dirs scanned by this thread + uint32_t scanned_dirs = 0; + // Files found by this thread + std::vector< filename_info_t > found_files; +}; + +/* + * getdents64 syscall + */ +GCC_DIAG_PUSH_OFF( pedantic ) +struct linux_dirent64 +{ + uint64_t d_ino; // Inode number + int64_t d_off; // Offset to next linux_dirent + unsigned short d_reclen; // Length of this linux_dirent + unsigned char d_type; // File type + char d_name[]; // Filename (null-terminated) +}; +GCC_DIAG_POP() + +int sys_getdents64( int fd, char *dirp, int count ) +{ + return syscall( SYS_getdents64, fd, dirp, count ); +} + +static double gettime() +{ + struct timespec ts; + + clock_gettime( CLOCK_MONOTONIC, &ts ); + return ( double )ts.tv_sec + ( double )ts.tv_nsec / 1e9; +} + +std::string string_formatv( const char *fmt, va_list ap ) +{ + std::string str; + int size = 512; + + for ( ;; ) + { + str.resize( size ); + int n = vsnprintf( ( char * )str.c_str(), size, fmt, ap ); + + if ( ( n > -1 ) && ( n < size ) ) + { + str.resize( n ); + return str; + } + + size = ( n > -1 ) ? ( n + 1 ) : ( size * 2 ); + } +} + +std::string string_format( const char *fmt, ... ) +{ + va_list ap; + std::string str; + + va_start( ap, fmt ); + str = string_formatv( fmt, ap ); + va_end( ap ); + + return str; +} + +static std::string get_link_name( const char *pathname ) +{ + std::string Result; + char filename[ PATH_MAX + 1 ]; + + ssize_t ret = readlink( pathname, filename, sizeof( filename ) ); + if ( ( ret > 0 ) && ( ret < ( ssize_t )sizeof( filename ) ) ) + { + filename[ ret ] = 0; + Result = filename; + } + return Result; +} + +static uid_t get_uid(const char *pathname) +{ + int fd = open( pathname, O_RDONLY, 0 ); + + if ( fd >= 0 ) + { + char buf[ 16 * 1024 ]; + + ssize_t len = read( fd, buf, sizeof( buf ) ); + + close( fd ); + fd = -1; + + if ( len > 0 ) + { + buf[ len - 1 ] = 0; + + const char *uidstr = strstr( buf, "\nUid:" ); + if ( uidstr ) + { + return atoll( uidstr + 5 ); + } + } + } + + return -1; +} + +static uint64_t get_token_val( const char *line, const char *token ) +{ + const char *str = strstr( line, token ); + + return str ? strtoull( str + strlen( token ), nullptr, 16 ) : 0; +} + +static uint32_t inotify_parse_fdinfo_file( procinfo_t &procinfo, const char *fdset_name ) +{ + uint32_t watch_count = 0; + + FILE *fp = fopen( fdset_name, "r" ); + if ( fp ) + { + char line_buf[ 256 ]; + + procinfo.fdset_filenames.push_back( fdset_name ); + + for ( ;; ) + { + if ( !fgets( line_buf, sizeof( line_buf ), fp ) ) + break; + + /* sample fdinfo; inotify line added in linux 3.8, available if + * kernel compiled with CONFIG_INOTIFY_USER and CONFIG_PROC_FS + * pos: 0 + * flags: 00 + * mnt_id: 15 + * ino: 5865 + * inotify wd:1 ino:80001 sdev:800011 mask:100 ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:01000800bc1b8c7c + */ + if ( !strncmp( line_buf, "inotify ", 8 ) ) + { + watch_count++; + + uint64_t inode_val = get_token_val( line_buf, "ino:" ); + uint64_t sdev_val = get_token_val( line_buf, "sdev:" ); + + if ( inode_val ) + { + // https://unix.stackexchange.com/questions/645937/listing-the-files-that-are-being-watched-by-inotify-instances + // Assuming that the sdev field is encoded according to Linux's so-called "huge + // encoding", which uses 20 bits (instead of 8) for minor numbers, in bitwise + // parlance the major number is sdev >> 20 while the minor is sdev & 0xfffff. + unsigned int major = sdev_val >> 20; + unsigned int minor = sdev_val & 0xfffff; + + // Add inode to this device map + procinfo.dev_map[ makedev( major, minor ) ].insert( inode_val ); + } + } + } + + fclose( fp ); + } + + return watch_count; +} + +static void inotify_parse_fddir( procinfo_t &procinfo ) +{ + std::string filename = string_format( "/proc/%d/fd", procinfo.pid ); + + DIR *dir_fd = opendir( filename.c_str() ); + if ( !dir_fd ) + return; + + for ( ;; ) + { + struct dirent *dp_fd = readdir( dir_fd ); + if ( !dp_fd ) + break; + + if ( ( dp_fd->d_type == DT_LNK ) && isdigit( dp_fd->d_name[ 0 ] ) ) + { + filename = string_format( "/proc/%d/fd/%s", procinfo.pid, dp_fd->d_name ); + filename = get_link_name( filename.c_str() ); + + if ( filename == "anon_inode:inotify" || filename == "inotify" ) + { + filename = string_format( "/proc/%d/fdinfo/%s", procinfo.pid, dp_fd->d_name ); + + procinfo.instances++; + procinfo.watches += inotify_parse_fdinfo_file( procinfo, filename.c_str() ); + + /* If any watches have been found, enable the stats display */ + g_kernel_provides_watches_info |= !!procinfo.watches; + } + } + } + + closedir( dir_fd ); +} + +void thread_info_t::queue_directory( char *path ) +{ + tdata.dirqueues[ idx ].queue_directory( path ); +} + +char *thread_info_t::dequeue_directory() +{ + char *path = tdata.dirqueues[ idx ].dequeue_directory(); + + if ( !path ) + { + // Nothing on our queue, check queues on other threads + for ( lfqueue_wrapper_t &dirq : tdata.dirqueues ) + { + path = dirq.dequeue_directory(); + if ( path ) + break; + } + } + + return path; +} + +// statx() was added to Linux in kernel 4.11; library support was added in glibc 2.28. +#if defined( __linux__ ) && ( ( __GLIBC__ >= 2 && __GLIBC_MINOR__ >= 28 ) || ( __GLIBC__ > 2 ) ) + +struct statx mystatx( const char *filename, unsigned int mask = 0 ) +{ + struct statx statxbuf; + int flags = AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW | AT_STATX_DONT_SYNC; + + if ( statx( 0, filename, flags, mask, &statxbuf ) == -1 ) + { + printf( "ERROR: statx-ino( %s ) failed. Errno: %d\n", filename, errno ); + memset( &statxbuf, 0, sizeof( statxbuf ) ); + } + + return statxbuf; +} + +static dev_t stat_get_dev_t( const char *filename ) +{ + struct statx statxbuf = mystatx( filename ); + + return makedev( statxbuf.stx_dev_major, statxbuf.stx_dev_minor ); +} + +static uint64_t stat_get_ino( const char *filename ) +{ + return mystatx( filename, STATX_INO ).stx_ino; +} + +#else + +// Fall back to using stat() functions. Should work but be slower than using statx(). + +static dev_t stat_get_dev_t( const char *filename ) +{ + struct stat statbuf; + + int ret = stat( filename, &statbuf ); + if ( ret == -1 ) + { + printf( "ERROR: stat-dev_t( %s ) failed. Errno: %d\n", filename, errno ); + return 0; + } + return statbuf.st_dev; +} + +static uint64_t stat_get_ino( const char *filename ) +{ + struct stat statbuf; + + int ret = stat( filename, &statbuf ); + if ( ret == -1 ) + { + printf( "ERROR: stat-ino( %s ) failed. Errno: %d\n", filename, errno ); + return 0; + } + + return statbuf.st_ino; +} + +#endif + +void thread_info_t::add_filename( ino64_t inode, const char *path, const char *d_name, bool is_dir ) +{ + auto it = tdata.inode_set.find( inode ); + + if ( it != tdata.inode_set.end() ) + { + const std::unordered_set< dev_t > &dev_set = it->second; + + std::string filename = std::string( path ) + d_name; + dev_t dev = stat_get_dev_t( filename.c_str() ); + + // Make sure the inode AND device ID match before adding. + if ( dev_set.find( dev ) != dev_set.end() ) + { + filename_info_t fname; + + fname.filename = is_dir ? filename + "/" : filename; + fname.inode = inode; + fname.dev = dev; + + found_files.push_back( fname ); + } + } +} + +static bool is_dot_dir( const char *dname ) +{ + if ( dname[ 0 ] == '.' ) + { + if ( !dname[ 1 ] ) + return true; + + if ( ( dname[ 1 ] == '.' ) && !dname[ 2 ] ) + return true; + } + + return false; +} + +// From "linux/magic.h" +#define PROC_SUPER_MAGIC 0x9fa0 +#define SMB_SUPER_MAGIC 0x517B +#define CIFS_SUPER_MAGIC 0xFF534D42 /* the first four bytes of SMB PDUs */ +#define SMB2_SUPER_MAGIC 0xFE534D42 +#define FUSE_SUPER_MAGIC 0x65735546 + +// Detect proc and fuse directories and skip them. +// https://github.com/mikesart/inotify-info/issues/6 +// Could use setmntent("/proc/mounts", "r") + getmntent if speed is an issue? +static bool is_proc_dir( const char *path, const char *d_name ) +{ + struct statfs s; + std::string filename = std::string( path ) + d_name; + + if ( statfs(filename.c_str(), &s ) == 0 ) + { + switch ( s.f_type ) + { + case PROC_SUPER_MAGIC: + case FUSE_SUPER_MAGIC: + return true; + } + } + + return false; +} + +// Returns -1: queue empty, 0: open error, > 0 success +int thread_info_t::parse_dirqueue_entry() +{ + char __attribute__( ( aligned( 16 ) ) ) buf[ 1024 ]; + + char *path = dequeue_directory(); + if ( !path ) + { + return -1; + } + + for ( std::string &dname : ignore_dirs ) + { + if ( dname == path ) + { + if ( g_verbose > 1 ) + { + printf( "Ignoring '%s'\n", path ); + } + return 0; + } + } + + int fd = open( path, O_RDONLY | O_DIRECTORY, 0 ); + if ( fd < 0 ) + { + free( path ); + return 0; + } + + scanned_dirs++; + + size_t pathlen = strlen( path ); + + for ( ;; ) + { + int ret = sys_getdents64( fd, buf, sizeof( buf ) ); + + if ( ret < 0 ) + { + bool spew_error = true; + + if ( ( errno == 5 ) && !strncmp( path, "/sys/kernel/", 12 ) ) + { + // In docker container we can get permission denied errors in /sys/kernel. Ignore them. + // https://github.com/mikesart/inotify-info/issues/16 + spew_error = false; + } + + if ( spew_error ) + { + printf( "ERROR: sys_getdents64 failed on '%s': %d errno:%d\n", path, ret, errno ); + } + break; + } + if ( ret == 0 ) + break; + + for ( int bpos = 0; bpos < ret; ) + { + struct linux_dirent64 *dirp = ( struct linux_dirent64 * )( buf + bpos ); + const char *d_name = dirp->d_name; + + // DT_BLK This is a block device. + // DT_CHR This is a character device. + // DT_FIFO This is a named pipe (FIFO). + // DT_SOCK This is a UNIX domain socket. + // DT_UNKNOWN The file type could not be determined. + + // DT_REG This is a regular file. + // DT_LNK This is a symbolic link. + if ( dirp->d_type == DT_REG || dirp->d_type == DT_LNK ) + { + add_filename( dirp->d_ino, path, d_name, false ); + } + // DT_DIR This is a directory. + else if ( dirp->d_type == DT_DIR ) + { + if ( !is_dot_dir( d_name ) && !is_proc_dir( path, d_name ) ) + { + add_filename( dirp->d_ino, path, d_name, true ); + + size_t len = strlen( d_name ); + char *newpath = ( char * )malloc( pathlen + len + 2 ); + + if ( newpath ) + { + strcpy( newpath, path ); + strcpy( newpath + pathlen, d_name ); + newpath[ pathlen + len ] = '/'; + newpath[ pathlen + len + 1 ] = 0; + + queue_directory( newpath ); + } + } + } + + bpos += dirp->d_reclen; + } + } + + close( fd ); + free( path ); + return 1; +} + +static void *parse_dirqueue_threadproc( void *arg ) +{ + thread_info_t *pthread_info = ( thread_info_t * )arg; + + for ( ;; ) + { + // Loop until all the dequeue(s) fail + if ( pthread_info->parse_dirqueue_entry() == -1 ) + break; + } + + return nullptr; +} + +static bool is_proc_in_cmdline_applist( const procinfo_t &procinfo, std::vector< std::string > &cmdline_applist ) +{ + for ( const std::string &str : cmdline_applist ) + { + // Check if our command line string is a subset of this appname + if ( strstr( procinfo.appname.c_str(), str.c_str() ) ) + return true; + + // Check if the PIDs match + if ( atoll( str.c_str() ) == procinfo.pid ) + return true; + } + + return false; +} + +static bool watch_count_is_greater ( procinfo_t elem1, procinfo_t elem2 ) +{ + return elem1.watches > elem2.watches; +} + + +static bool init_inotify_proclist( std::vector< procinfo_t > &inotify_proclist ) +{ + DIR *dir_proc = opendir( "/proc" ); + + if ( !dir_proc ) + { + printf( "ERROR: opendir /proc failed: %d\n", errno ); + return false; + } + + for ( ;; ) + { + struct dirent *dp_proc = readdir( dir_proc ); + if ( !dp_proc ) + break; + + if ( ( dp_proc->d_type == DT_DIR ) && isdigit( dp_proc->d_name[ 0 ] ) ) + { + procinfo_t procinfo; + + procinfo.pid = atoll( dp_proc->d_name ); + + std::string executable = string_format( "/proc/%d/exe", procinfo.pid ); + std::string status = string_format( "/proc/%d/status", procinfo.pid ); + procinfo.uid = get_uid( status.c_str() ); + procinfo.executable = get_link_name( executable.c_str() ); + if ( !procinfo.executable.empty() ) + { + procinfo.appname = basename( (char*)procinfo.executable.c_str() ); + + inotify_parse_fddir( procinfo ); + + if ( procinfo.instances ) + { + inotify_proclist.push_back( procinfo ); + } + } + } + } + std::sort(inotify_proclist.begin(), inotify_proclist.end(), watch_count_is_greater); + + closedir( dir_proc ); + return true; +} + +// From: +// https://stackoverflow.com/questions/1449805/how-to-format-a-number-using-comma-as-thousands-separator-in-c +size_t str_format_uint32( char dst[16], uint32_t num ) +{ + if ( thousands_sep ) + { + char src[16]; + char *p_src = src; + char *p_dst = dst; + int num_len, commas; + + num_len = sprintf(src, "%u", num); + + for (commas = 2 - num_len % 3; *p_src; commas = (commas + 1) % 3) + { + *p_dst++ = *p_src++; + if (commas == 1) { + *p_dst++ = thousands_sep; + } + } + *--p_dst = '\0'; + + return (size_t)(p_dst - dst); + } + + return sprintf(dst, "%u", num); +} + +static void print_inotify_proclist( std::vector< procinfo_t > &inotify_proclist ) +{ +#if 0 + // test data + procinfo_t proc_info = {}; + proc_info.pid = 100; + proc_info.appname = "fsnotifier"; + proc_info.watches = 2; + proc_info.instances = 1; + inotify_proclist.push_back(proc_info); + + proc_info.pid = 1000; + proc_info.appname = "evolution-addressbook-factor"; + proc_info.watches = 116; + proc_info.instances = 10; + inotify_proclist.push_back(proc_info); + + proc_info.pid = 22154; + proc_info.appname = "evolution-addressbook-factor blah blah"; + proc_info.watches = 28200; + proc_info.instances = 100; + inotify_proclist.push_back(proc_info); + + proc_info.pid = 0x7fffffff; + proc_info.appname = "evolution-addressbook-factor blah blah2"; + proc_info.watches = 999999; + proc_info.instances = 999999999; + inotify_proclist.push_back(proc_info); +#endif + + int lenPid = 10; + int lenUid = 10; + int lenApp = 10; + int lenWatches = 8; + int lenInstances = 10; + + for ( procinfo_t &procinfo : inotify_proclist ) + lenApp = std::max<int>( procinfo.appname.length(), lenApp ); + + /* If the number of watches is negative, the kernel doesn't support this info. omit the header*/ + if (g_kernel_provides_watches_info) + printf( "%s%*s %-*s %-*s %*s %*s%s\n", + BCYAN, lenPid, "Pid", lenUid, "Uid", lenApp, "App", lenWatches, "Watches", lenInstances, "Instances", RESET); + else + printf( "%s%*s %-*s %*s %*s%s\n", + BCYAN, lenPid, "Pid", lenUid, "Uid", lenApp, "App", lenInstances, "Instances", RESET); + + + for ( procinfo_t &procinfo : inotify_proclist ) + { + char watches_str[16]; + + str_format_uint32(watches_str, procinfo.watches); + + if (g_kernel_provides_watches_info) + printf( "%*d %-*d %s%-*s%s %*s %*u\n", + lenPid, procinfo.pid, + lenUid, procinfo.uid, + BYELLOW, lenApp, procinfo.appname.c_str(), RESET, + lenWatches, watches_str, + lenInstances, procinfo.instances ); + else + printf( "%*d %-*d %s%-*s%s %*u\n", + lenPid, procinfo.pid, + lenUid, procinfo.uid, + BYELLOW, lenApp, procinfo.appname.c_str(), RESET, + lenInstances, procinfo.instances ); + + if ( g_verbose > 1 ) + { + for ( std::string &fname : procinfo.fdset_filenames ) + { + printf( " %s%s%s\n", CYAN, fname.c_str(), RESET ); + } + } + + if ( procinfo.in_cmd_line ) + { + for ( const auto &it1 : procinfo.dev_map ) + { + dev_t dev = it1.first; + + printf( "%s[%u.%u]:%s", BGRAY, major( dev ), minor( dev ), RESET ); + for ( const auto &it2 : it1.second ) + { + std::string inode_device_str = string_format( "%lu", it2 ); + + printf( " %s%s%s", BGRAY, inode_device_str.c_str(), RESET ); + } + printf( "\n" ); + } + } + } +} + +bool thread_shared_data_t::init( uint32_t numthreads, const std::vector< procinfo_t > &inotify_proclist ) +{ + for ( const procinfo_t &procinfo : inotify_proclist ) + { + if ( !procinfo.in_cmd_line ) + continue; + + for ( const auto &it1 : procinfo.dev_map ) + { + dev_t dev = it1.first; + + for ( const auto &inode : it1.second ) + { + inode_set[ inode ].insert( dev ); + } + } + } + + if ( !inode_set.empty() ) + { + dirqueues.resize( numthreads ); + } + + return !inode_set.empty(); +} + +static uint32_t find_files_in_inode_set( const std::vector< procinfo_t > &inotify_proclist, + std::vector< filename_info_t > &all_found_files ) +{ + thread_shared_data_t tdata; + + g_numthreads = std::max< size_t >( 1, g_numthreads ); + + if ( !tdata.init( g_numthreads, inotify_proclist ) ) + return 0; + + printf( "\n%sSearching '/' for listed inodes...%s (%lu threads)\n", BCYAN, RESET, g_numthreads ); + + // Initialize thread_info_t array + std::vector< class thread_info_t > thread_array( g_numthreads, thread_info_t( tdata ) ); + + for ( uint32_t idx = 0; idx < thread_array.size(); idx++ ) + { + thread_info_t &thread_info = thread_array[ idx ]; + + thread_info.idx = idx; + + if ( idx == 0 ) + { + // Add root dir in case someone is watching it + thread_info.add_filename( stat_get_ino( "/" ), "/", "", false ); + // Add and parse root + thread_info.queue_directory( strdup( "/" ) ); + thread_info.parse_dirqueue_entry(); + } + else if ( pthread_create( &thread_info.pthread_id, NULL, &parse_dirqueue_threadproc, &thread_info ) ) + { + printf( "Warning: pthread_create failed. errno: %d\n", errno ); + thread_info.pthread_id = 0; + } + } + + // Put main thread to work + parse_dirqueue_threadproc( &thread_array[ 0 ] ); + + uint32_t total_scanned_dirs = 0; + for ( const thread_info_t &thread_info : thread_array ) + { + if ( thread_info.pthread_id ) + { + if ( g_verbose > 1 ) + { + printf( "Waiting for thread #%zu\n", thread_info.pthread_id ); + } + + void *status = NULL; + int rc = pthread_join( thread_info.pthread_id, &status ); + + if ( g_verbose > 1 ) + { + printf( "Thread #%zu rc=%d status=%d\n", thread_info.pthread_id, rc, ( int )( intptr_t )status ); + } + } + + // Snag data from this thread + total_scanned_dirs += thread_info.scanned_dirs; + + all_found_files.insert( all_found_files.end(), + thread_info.found_files.begin(), thread_info.found_files.end() ); + + if ( g_verbose > 1 ) + { + printf( "Thread #%zu: %u dirs, %zu files found\n", + thread_info.pthread_id, thread_info.scanned_dirs, thread_info.found_files.size() ); + } + } + + struct + { + bool operator()( const filename_info_t &a, const filename_info_t &b ) const + { + if ( a.dev == b.dev ) + return a.inode < b.inode; + return a.dev < b.dev; + } + } filename_info_less_func; + + std::sort( all_found_files.begin(), all_found_files.end(), filename_info_less_func ); + + return total_scanned_dirs; +} + +static uint32_t get_inotify_procfs_value( const std::string &fname ) +{ + char buf[ 64 ]; + uint32_t val = 0; + std::string filename = "/proc/sys/fs/inotify/" + fname; + + int fd = open( filename.c_str(), O_RDONLY ); + if ( fd >= 0 ) + { + if ( read( fd, buf, sizeof( buf ) ) > 0 ) + { + val = strtoul( buf, nullptr, 10 ); + } + + close( fd ); + } + + return val; +} + +static void print_inotify_limits() +{ + const std::vector< std::string > filenames = + { + "max_queued_events", + "max_user_instances", + "max_user_watches" + }; + + printf( "%sINotify Limits:%s\n", BCYAN, RESET ); + for ( const std::string &fname : filenames ) + { + char str[16]; + uint32_t val = get_inotify_procfs_value( fname ); + + str_format_uint32(str, val); + + printf( " %-20s %s%s%s\n", fname.c_str(), BGREEN, str, RESET ); + } +} + +static uint32_t parse_config_file( const char *config_file ) +{ + uint32_t dir_count = 0; + + FILE *fp = fopen( config_file, "r" ); + if ( fp ) + { + char line_buf[ 8192 ]; + bool in_ignore_dirs_section = false; + + for ( ;; ) + { + if ( !fgets( line_buf, sizeof( line_buf ) - 1, fp ) ) + break; + + if ( line_buf[0] == '#' ) + { + // comment + } + else if ( !in_ignore_dirs_section ) + { + size_t len = strcspn( line_buf, "\r\n" ); + + if ( ( len == 12 ) && !strncmp( "[ignoredirs]", line_buf, 12 ) ) + { + in_ignore_dirs_section = true; + } + } + else if ( line_buf[ 0 ] == '[' ) + { + in_ignore_dirs_section = false; + } + else if ( in_ignore_dirs_section && ( line_buf[ 0 ] == '/' ) ) + { + size_t len = strcspn( line_buf, "\r\n" ); + + if ( len > 1 ) + { + line_buf[ len ] = 0; + if ( line_buf[ len - 1 ] != '/' ) + { + line_buf[ len ] = '/'; + line_buf[ len + 1 ] = '\0'; + } + + ignore_dirs.push_back( line_buf ); + dir_count++; + } + } + } + + fclose( fp ); + } + + return dir_count; +} + +static bool parse_ignore_dirs_file() +{ + const std::string filename = "inotify-info.config"; + + const char *xdg_config_dir = getenv( "XDG_CONFIG_HOME" ); + if ( xdg_config_dir ) + { + std::string config_file = std::string( xdg_config_dir ) + "/" + filename; + if ( parse_config_file( config_file.c_str() ) ) + return true; + + config_file = std::string( xdg_config_dir) + "/.config/" + filename; + if ( parse_config_file( config_file.c_str() ) ) + return true; + } + + const char *home_dir = getenv( "HOME" ); + if ( home_dir ) + { + std::string config_file = std::string( home_dir ) + "/" + filename; + if ( parse_config_file( config_file.c_str() ) ) + return true; + } + + std::string config_file = "/etc/" + filename; + if ( parse_config_file( config_file.c_str() ) ) + return true; + + return false; +} + +static void print_usage( const char *appname ) +{ + printf( "Usage: %s [--threads=##] [appname | pid...]\n", appname ); + printf( " [-vv]\n" ); + printf( " [-?|-h|--help]\n" ); + + exit( -1 ); +} + +static void parse_cmdline( int argc, char **argv, std::vector< std::string > &cmdline_applist ) +{ + static struct option long_opts[] = + { + { "verbose", no_argument, 0, 0 }, + { "threads", required_argument, 0, 0 }, + { "ignoredir", required_argument, 0, 0 }, + { 0, 0, 0, 0 } + }; + + // Let's pick the number of processors online (with a max of 32) for a default. + g_numthreads = std::min< uint32_t >( g_numthreads, sysconf( _SC_NPROCESSORS_ONLN ) ); + + int c; + int opt_ind = 0; + while ( ( c = getopt_long( argc, argv, "m:s:?hv", long_opts, &opt_ind ) ) != -1 ) + { + switch ( c ) + { + case 0: + if ( !strcasecmp( "verbose", long_opts[ opt_ind ].name ) ) + g_verbose++; + else if ( !strcasecmp( "threads", long_opts[ opt_ind ].name ) ) + g_numthreads = atoi( optarg ); + else if ( !strcasecmp( "ignoredir", long_opts[ opt_ind ].name ) ) + { + std::string dirname = optarg; + if ( dirname.size() > 1 ) + { + if ( optarg[ dirname.size() - 1 ] != '/' ) + dirname += "/"; + ignore_dirs.push_back( dirname ); + } + } + break; + case 'v': + g_verbose++; + break; + case 'h': + case '?': + default: + print_usage( argv[ 0 ] ); + break; + } + } + + for ( ; optind < argc; optind++ ) + { + cmdline_applist.push_back( argv[ optind ] ); + } + + parse_ignore_dirs_file(); + + if ( g_verbose > 1 ) + { + printf( "%lu ignore_dirs:\n", ignore_dirs.size() ); + + for ( std::string &dname : ignore_dirs ) + { + printf( " '%s'\n", dname.c_str() ); + } + } +} + +static void print_separator() +{ + printf( "%s%s%s\n", YELLOW, std::string( 78, '-' ).c_str(), RESET ); +} + +int main( int argc, char *argv[] ) +{ + std::vector< std::string > cmdline_applist; + std::vector< procinfo_t > inotify_proclist; + + struct lconv *env = localeconv(); + if (env && env->thousands_sep && env->thousands_sep[0]) + { + thousands_sep = env->thousands_sep[0]; + } + + parse_cmdline( argc, argv, cmdline_applist ); + print_separator(); + + print_inotify_limits(); + print_separator(); + + if ( init_inotify_proclist( inotify_proclist ) ) + { + uint32_t total_watches = 0; + uint32_t total_instances = 0; + std::vector< filename_info_t > all_found_files; + + for ( procinfo_t &procinfo : inotify_proclist ) + { + procinfo.in_cmd_line = is_proc_in_cmdline_applist( procinfo, cmdline_applist ); + + total_watches += procinfo.watches; + total_instances += procinfo.instances; + } + + if (inotify_proclist.size()) { + print_inotify_proclist( inotify_proclist ); + print_separator(); + } + + if (g_kernel_provides_watches_info) + printf( "Total inotify Watches: %s%u%s\n", BGREEN, total_watches, RESET ); + printf( "Total inotify Instances: %s%u%s\n", BGREEN, total_instances, RESET ); + print_separator(); + + double search_time = gettime(); + uint32_t total_scanned_dirs = find_files_in_inode_set( inotify_proclist, all_found_files ); + if ( total_scanned_dirs ) + { + search_time = gettime() - search_time; + + for ( const filename_info_t &fname_info : all_found_files ) + { + printf( "%s%9lu%s [%u:%u] %s\n", BGREEN, fname_info.inode, RESET, + major( fname_info.dev ), minor( fname_info.dev ), + fname_info.filename.c_str() ); + } + + setlocale( LC_NUMERIC, "" ); +GCC_DIAG_PUSH_OFF( format ) + printf( "\n%'u dirs scanned (%.2f seconds)\n", total_scanned_dirs, search_time ); +GCC_DIAG_POP() + } + } + + return 0; +} |