diff options
Diffstat (limited to 'src/inotifywait.c')
-rw-r--r-- | src/inotifywait.c | 760 |
1 files changed, 760 insertions, 0 deletions
diff --git a/src/inotifywait.c b/src/inotifywait.c new file mode 100644 index 0000000..98aadd4 --- /dev/null +++ b/src/inotifywait.c @@ -0,0 +1,760 @@ +#include "../config.h" +#include "common.h" + +#include <sys/types.h> +#include <sys/select.h> +#include <sys/stat.h> +#include <sys/time.h> + +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <regex.h> +#include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <syslog.h> +#include <unistd.h> + +#include <inotifytools/inotifytools.h> +#include <inotifytools/inotify.h> + +extern char *optarg; +extern int optind, opterr, optopt; + +#define MAX_STRLEN 4096 +#define EXCLUDE_CHUNK 1024 + +#define nasprintf(...) niceassert( -1 != asprintf(__VA_ARGS__), "out of memory") + +// METHODS +bool parse_opts( + int * argc, + char *** argv, + int * events, + bool * monitor, + int * quiet, + unsigned long int * timeout, + int * recursive, + bool * csv, + bool * daemon, + bool * syslog, + char ** format, + char ** timefmt, + char ** fromfile, + char ** outfile, + char ** regex, + char ** iregex +); + +void print_help(); + + +char * csv_escape( char * string ) { + static char csv[MAX_STRLEN+1]; + static unsigned int i, ind; + + if ( strlen(string) > MAX_STRLEN ) { + return NULL; + } + + if ( strlen(string) == 0 ) { + return NULL; + } + + // May not need escaping + if ( !strchr(string, '"') && !strchr(string, ',') && !strchr(string, '\n') + && string[0] != ' ' && string[strlen(string)-1] != ' ' ) { + strcpy( csv, string ); + return csv; + } + + // OK, so now we _do_ need escaping. + csv[0] = '"'; + ind = 1; + for ( i = 0; i < strlen(string); ++i ) { + if ( string[i] == '"' ) { + csv[ind++] = '"'; + } + csv[ind++] = string[i]; + } + csv[ind++] = '"'; + csv[ind] = '\0'; + + return csv; +} + + +void validate_format( char * fmt ) { + // Make a fake event + struct inotify_event * event = + (struct inotify_event *)malloc(sizeof(struct inotify_event) + 4); + if ( !event ) { + fprintf( stderr, "Seem to be out of memory... yikes!\n" ); + exit(EXIT_FAILURE); + } + event->wd = 0; + event->mask = IN_ALL_EVENTS; + event->len = 3; + strcpy( event->name, "foo" ); + FILE * devnull = fopen( "/dev/null", "a" ); + if ( !devnull ) { + fprintf( stderr, "Couldn't open /dev/null: %s\n", strerror(errno) ); + free( event ); + return; + } + if ( -1 == inotifytools_fprintf( devnull, event, fmt ) ) { + fprintf( stderr, "Something is wrong with your format string.\n" ); + exit(EXIT_FAILURE); + } + free( event ); + fclose(devnull); +} + + +void output_event_csv( struct inotify_event * event ) { + char *filename = csv_escape(inotifytools_filename_from_wd(event->wd)); + if (filename != NULL) + printf("%s,", csv_escape(filename)); + + printf("%s,", csv_escape( inotifytools_event_to_str( event->mask ) ) ); + if ( event->len > 0 ) + printf("%s", csv_escape( event->name ) ); + printf("\n"); +} + + +void output_error( bool syslog, char* fmt, ... ) { + va_list va; + va_start(va, fmt); + if ( syslog ) { + vsyslog(LOG_INFO, fmt, va); + } else { + vfprintf(stderr, fmt, va); + } + va_end(va); +} + + +int main(int argc, char ** argv) +{ + int events = 0; + int orig_events; + bool monitor = false; + int quiet = 0; + unsigned long int timeout = 0; + int recursive = 0; + bool csv = false; + bool daemon = false; + bool syslog = false; + char * format = NULL; + char * timefmt = NULL; + char * fromfile = NULL; + char * outfile = NULL; + char * regex = NULL; + char * iregex = NULL; + pid_t pid; + int fd; + + // Parse commandline options, aborting if something goes wrong + if ( !parse_opts(&argc, &argv, &events, &monitor, &quiet, &timeout, + &recursive, &csv, &daemon, &syslog, &format, &timefmt, + &fromfile, &outfile, ®ex, &iregex) ) { + return EXIT_FAILURE; + } + + if ( !inotifytools_initialize() ) { + fprintf(stderr, "Couldn't initialize inotify. Are you running Linux " + "2.6.13 or later, and was the\n" + "CONFIG_INOTIFY option enabled when your kernel was " + "compiled? If so, \n" + "something mysterious has gone wrong. Please e-mail " + PACKAGE_BUGREPORT "\n" + " and mention that you saw this message.\n"); + return EXIT_FAILURE; + } + + if ( timefmt ) inotifytools_set_printf_timefmt( timefmt ); + if ( + (regex && !inotifytools_ignore_events_by_regex(regex, REG_EXTENDED) ) || + (iregex && !inotifytools_ignore_events_by_regex(iregex, REG_EXTENDED| + REG_ICASE)) + ) { + fprintf(stderr, "Error in `exclude' regular expression.\n"); + return EXIT_FAILURE; + } + + + if ( format ) validate_format(format); + + // Attempt to watch file + // If events is still 0, make it all events. + if (events == 0) + events = IN_ALL_EVENTS; + orig_events = events; + if ( monitor && recursive ) { + events = events | IN_CREATE | IN_MOVED_TO | IN_MOVED_FROM; + } + + FileList list = construct_path_list( argc, argv, fromfile ); + + if (0 == list.watch_files[0]) { + fprintf(stderr, "No files specified to watch!\n"); + return EXIT_FAILURE; + } + + + // Daemonize - BSD double-fork approach + if ( daemon ) { + + pid = fork(); + if (pid < 0) { + fprintf(stderr, "Failed to fork1 whilst daemonizing!\n"); + return EXIT_FAILURE; + } + if (pid > 0) { + _exit(0); + } + if (setsid() < 0) { + fprintf(stderr, "Failed to setsid whilst daemonizing!\n"); + return EXIT_FAILURE; + } + signal(SIGHUP,SIG_IGN); + pid = fork(); + if (pid < 0) { + fprintf(stderr, "Failed to fork2 whilst daemonizing!\n"); + return EXIT_FAILURE; + } + if (pid > 0) { + _exit(0); + } + if (chdir("/") < 0) { + fprintf(stderr, "Failed to chdir whilst daemonizing!\n"); + return EXIT_FAILURE; + } + + // Redirect stdin from /dev/null + fd = open("/dev/null", O_RDONLY); + if (fd != fileno(stdin)) { + dup2(fd, fileno(stdin)); + close(fd); + } + + // Redirect stdout to a file + fd = open(outfile, O_WRONLY | O_CREAT | O_APPEND, 0600); + if (fd < 0) { + fprintf( stderr, "Failed to open output file %s\n", outfile ); + return EXIT_FAILURE; + } + if (fd != fileno(stdout)) { + dup2(fd, fileno(stdout)); + close(fd); + } + + // Redirect stderr to /dev/null + fd = open("/dev/null", O_WRONLY); + if (fd != fileno(stderr)) { + dup2(fd, fileno(stderr)); + close(fd); + } + + } else if (outfile != NULL) { // Redirect stdout to a file if specified + fd = open(outfile, O_WRONLY | O_CREAT | O_APPEND, 0600); + if (fd < 0) { + fprintf( stderr, "Failed to open output file %s\n", outfile ); + return EXIT_FAILURE; + } + if (fd != fileno(stdout)) { + dup2(fd, fileno(stdout)); + close(fd); + } + } + + if ( syslog ) { + openlog ("inotifywait", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_DAEMON); + } + + if ( !quiet ) { + if ( recursive ) { + output_error( syslog, "Setting up watches. Beware: since -r " + "was given, this may take a while!\n" ); + } else { + output_error( syslog, "Setting up watches.\n" ); + } + } + + // now watch files + for ( int i = 0; list.watch_files[i]; ++i ) { + char const *this_file = list.watch_files[i]; + if ( (recursive && !inotifytools_watch_recursively_with_exclude( + this_file, + events, + list.exclude_files )) + || (!recursive && !inotifytools_watch_file( this_file, events )) ){ + if ( inotifytools_error() == ENOSPC ) { + output_error( syslog, "Failed to watch %s; upper limit on inotify " + "watches reached!\n", this_file ); + output_error( syslog, "Please increase the amount of inotify watches " + "allowed per user via `/proc/sys/fs/inotify/" + "max_user_watches'.\n"); + } + else { + output_error( syslog, "Couldn't watch %s: %s\n", this_file, + strerror( inotifytools_error() ) ); + } + return EXIT_FAILURE; + } + } + + if ( !quiet ) { + output_error( syslog, "Watches established.\n" ); + } + + // Now wait till we get event + struct inotify_event * event; + char * moved_from = 0; + + do { + event = inotifytools_next_event( timeout ); + if ( !event ) { + if ( !inotifytools_error() ) { + return EXIT_TIMEOUT; + } + else { + output_error( syslog, "%s\n", strerror( inotifytools_error() ) ); + return EXIT_FAILURE; + } + } + + if ( quiet < 2 && (event->mask & orig_events) ) { + if ( csv ) { + output_event_csv( event ); + } + else if ( format ) { + inotifytools_printf( event, format ); + } + else { + inotifytools_printf( event, "%w %,e %f\n" ); + } + } + + // if we last had MOVED_FROM and don't currently have MOVED_TO, + // moved_from file must have been moved outside of tree - so unwatch it. + if ( moved_from && !(event->mask & IN_MOVED_TO) ) { + if ( !inotifytools_remove_watch_by_filename( moved_from ) ) { + output_error( syslog, "Error removing watch on %s: %s\n", + moved_from, strerror(inotifytools_error()) ); + } + free( moved_from ); + moved_from = 0; + } + + if ( monitor && recursive ) { + if ((event->mask & IN_CREATE) || + (!moved_from && (event->mask & IN_MOVED_TO))) { + // New file - if it is a directory, watch it + static char * new_file; + + nasprintf( &new_file, "%s%s", + inotifytools_filename_from_wd( event->wd ), + event->name ); + + if ( isdir(new_file) && + !inotifytools_watch_recursively( new_file, events ) ) { + output_error( syslog, "Couldn't watch new directory %s: %s\n", + new_file, strerror( inotifytools_error() ) ); + } + free( new_file ); + } // IN_CREATE + else if (event->mask & IN_MOVED_FROM) { + nasprintf( &moved_from, "%s%s/", + inotifytools_filename_from_wd( event->wd ), + event->name ); + // if not watched... + if ( inotifytools_wd_from_filename(moved_from) == -1 ) { + free( moved_from ); + moved_from = 0; + } + } // IN_MOVED_FROM + else if (event->mask & IN_MOVED_TO) { + if ( moved_from ) { + static char * new_name; + nasprintf( &new_name, "%s%s/", + inotifytools_filename_from_wd( event->wd ), + event->name ); + inotifytools_replace_filename( moved_from, new_name ); + free( moved_from ); + moved_from = 0; + } // moved_from + } + } + + fflush( NULL ); + + } while ( monitor ); + + // If we weren't trying to listen for this event... + if ( (events & event->mask) == 0 ) { + // ...then most likely something bad happened, like IGNORE etc. + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + + +bool parse_opts( + int * argc, + char *** argv, + int * events, + bool * monitor, + int * quiet, + unsigned long int * timeout, + int * recursive, + bool * csv, + bool * daemon, + bool * syslog, + char ** format, + char ** timefmt, + char ** fromfile, + char ** outfile, + char ** regex, + char ** iregex +) { + assert( argc ); assert( argv ); assert( events ); assert( monitor ); + assert( quiet ); assert( timeout ); assert( csv ); assert( daemon ); + assert( syslog ); assert( format ); assert( timefmt ); assert( fromfile ); + assert( outfile ); assert( regex ); assert( iregex ); + + // Short options + char * opt_string = "mrhcdsqt:fo:e:"; + + // Construct array + struct option long_opts[17]; + + // --help + long_opts[0].name = "help"; + long_opts[0].has_arg = 0; + long_opts[0].flag = NULL; + long_opts[0].val = (int)'h'; + // --event + long_opts[1].name = "event"; + long_opts[1].has_arg = 1; + long_opts[1].flag = NULL; + long_opts[1].val = (int)'e'; + int new_event; + // --monitor + long_opts[2].name = "monitor"; + long_opts[2].has_arg = 0; + long_opts[2].flag = NULL; + long_opts[2].val = (int)'m'; + // --quiet + long_opts[3].name = "quiet"; + long_opts[3].has_arg = 0; + long_opts[3].flag = NULL; + long_opts[3].val = (int)'q'; + // --timeout + long_opts[4].name = "timeout"; + long_opts[4].has_arg = 1; + long_opts[4].flag = NULL; + long_opts[4].val = (int)'t'; + char * timeout_end = NULL; + // --filename + long_opts[5].name = "filename"; + long_opts[5].has_arg = 0; + long_opts[5].flag = NULL; + long_opts[5].val = (int)'f'; + // --recursive + long_opts[6].name = "recursive"; + long_opts[6].has_arg = 0; + long_opts[6].flag = NULL; + long_opts[6].val = (int)'r'; + // --csv + long_opts[7].name = "csv"; + long_opts[7].has_arg = 0; + long_opts[7].flag = NULL; + long_opts[7].val = (int)'c'; + // --daemon + long_opts[8].name = "daemon"; + long_opts[8].has_arg = 0; + long_opts[8].flag = NULL; + long_opts[8].val = (int)'d'; + // --syslog + long_opts[9].name = "syslog"; + long_opts[9].has_arg = 0; + long_opts[9].flag = NULL; + long_opts[9].val = (int)'s'; + // --format + long_opts[10].name = "format"; + long_opts[10].has_arg = 1; + long_opts[10].flag = NULL; + long_opts[10].val = (int)'n'; + // format with trailing newline + static char * newlineformat; + // --timefmt + long_opts[11].name = "timefmt"; + long_opts[11].has_arg = 1; + long_opts[11].flag = NULL; + long_opts[11].val = (int)'i'; + // --fromfile + long_opts[12].name = "fromfile"; + long_opts[12].has_arg = 1; + long_opts[12].flag = NULL; + long_opts[12].val = (int)'z'; + // --outfile + long_opts[13].name = "outfile"; + long_opts[13].has_arg = 1; + long_opts[13].flag = NULL; + long_opts[13].val = (int)'o'; + // --exclude + long_opts[14].name = "exclude"; + long_opts[14].has_arg = 1; + long_opts[14].flag = NULL; + long_opts[14].val = (int)'a'; + // --excludei + long_opts[15].name = "excludei"; + long_opts[15].has_arg = 1; + long_opts[15].flag = NULL; + long_opts[15].val = (int)'b'; + // Empty last element + long_opts[16].name = 0; + long_opts[16].has_arg = 0; + long_opts[16].flag = 0; + long_opts[16].val = 0; + + // Get first option + char curr_opt = getopt_long(*argc, *argv, opt_string, long_opts, NULL); + + // While more options exist... + while ( (curr_opt != '?') && (curr_opt != (char)-1) ) + { + switch ( curr_opt ) + { + // --help or -h + case 'h': + print_help(); + // Shouldn't process any further... + return false; + break; + + // --monitor or -m + case 'm': + *monitor = true; + break; + + // --quiet or -q + case 'q': + (*quiet)++; + break; + + // --recursive or -r + case 'r': + (*recursive)++; + break; + + // --csv or -c + case 'c': + (*csv) = true; + break; + + // --daemon or -d + case 'd': + (*daemon) = true; + (*monitor) = true; + (*syslog) = true; + break; + + // --syslog or -s + case 's': + (*syslog) = true; + break; + + // --filename or -f + case 'f': + fprintf(stderr, "The '--filename' option no longer exists. " + "The option it enabled in earlier\nversions of " + "inotifywait is now turned on by default.\n"); + return false; + break; + + // --format + case 'n': + newlineformat = (char *)malloc(strlen(optarg)+2); + strcpy( newlineformat, optarg ); + strcat( newlineformat, "\n" ); + (*format) = newlineformat; + break; + + // --timefmt + case 'i': + (*timefmt) = optarg; + break; + + // --exclude + case 'a': + (*regex) = optarg; + break; + + // --excludei + case 'b': + (*iregex) = optarg; + break; + + // --fromfile + case 'z': + if (*fromfile) { + fprintf(stderr, "Multiple --fromfile options given.\n"); + return false; + } + (*fromfile) = optarg; + break; + + // --outfile + case 'o': + if (*outfile) { + fprintf(stderr, "Multiple --outfile options given.\n"); + return false; + } + (*outfile) = optarg; + break; + + // --timeout or -t + case 't': + *timeout = strtoul(optarg, &timeout_end, 10); + if ( *timeout_end != '\0' ) + { + fprintf(stderr, "'%s' is not a valid timeout value.\n" + "Please specify an integer of value 0 or " + "greater.\n", + optarg); + return false; + } + break; + + // --event or -e + case 'e': + // Get event mask from event string + new_event = inotifytools_str_to_event(optarg); + + // If optarg was invalid, abort + if ( new_event == -1 ) + { + fprintf(stderr, "'%s' is not a valid event! Run with the " + "'--help' option to see a list of " + "events.\n", optarg); + return false; + } + + // Add the new event to the event mask + (*events) = ( (*events) | new_event ); + + break; + + + } + + curr_opt = getopt_long(*argc, *argv, opt_string, long_opts, NULL); + + } + + if ( *monitor && *timeout != 0 ) { + fprintf(stderr, "-m and -t cannot both be specified.\n"); + return false; + } + + if ( *regex && *iregex ) { + fprintf(stderr, "--exclude and --excludei cannot both be specified.\n"); + return false; + } + + if ( *format && *csv ) { + fprintf(stderr, "-c and --format cannot both be specified.\n"); + return false; + } + + if ( !*format && *timefmt ) { + fprintf(stderr, "--timefmt cannot be specified without --format.\n"); + return false; + } + + if ( *format && strstr( *format, "%T" ) && !*timefmt ) { + fprintf(stderr, "%%T is in --format string, but --timefmt was not " + "specified.\n"); + return false; + } + + if ( *daemon && *outfile == NULL ) { + fprintf(stderr, "-o must be specified with -d.\n"); + return false; + } + + (*argc) -= optind; + *argv = &(*argv)[optind]; + + // If ? returned, invalid option + return (curr_opt != '?'); +} + + +void print_help() +{ + printf("inotifywait %s\n", PACKAGE_VERSION); + printf("Wait for a particular event on a file or set of files.\n"); + printf("Usage: inotifywait [ options ] file1 [ file2 ] [ file3 ] " + "[ ... ]\n"); + printf("Options:\n"); + printf("\t-h|--help \tShow this help text.\n"); + printf("\t@<file> \tExclude the specified file from being " + "watched.\n"); + printf("\t--exclude <pattern>\n" + "\t \tExclude all events on files matching the\n" + "\t \textended regular expression <pattern>.\n"); + printf("\t--excludei <pattern>\n" + "\t \tLike --exclude but case insensitive.\n"); + printf("\t-m|--monitor \tKeep listening for events forever. Without\n" + "\t \tthis option, inotifywait will exit after one\n" + "\t \tevent is received.\n"); + printf("\t-d|--daemon \tSame as --monitor, except run in the background\n" + "\t \tlogging events to a file specified by --outfile.\n" + "\t \tImplies --syslog.\n"); + printf("\t-r|--recursive\tWatch directories recursively.\n"); + printf("\t--fromfile <file>\n" + "\t \tRead files to watch from <file> or `-' for " + "stdin.\n"); + printf("\t-o|--outfile <file>\n" + "\t \tPrint events to <file> rather than stdout.\n"); + printf("\t-s|--syslog \tSend errors to syslog rather than stderr.\n"); + printf("\t-q|--quiet \tPrint less (only print events).\n"); + printf("\t-qq \tPrint nothing (not even events).\n"); + printf("\t--format <fmt>\tPrint using a specified printf-like format\n" + "\t \tstring; read the man page for more details.\n"); + printf("\t--timefmt <fmt>\tstrftime-compatible format string for use with\n" + "\t \t%%T in --format string.\n"); + printf("\t-c|--csv \tPrint events in CSV format.\n"); + printf("\t-t|--timeout <seconds>\n" + "\t \tWhen listening for a single event, time out " + "after\n" + "\t \twaiting for an event for <seconds> seconds.\n" + "\t \tIf <seconds> is 0, inotifywait will never time " + "out.\n"); + printf("\t-e|--event <event1> [ -e|--event <event2> ... ]\n" + "\t\tListen for specific event(s). If omitted, all events are \n" + "\t\tlistened for.\n\n"); + printf("Exit status:\n"); + printf("\t%d - An event you asked to watch for was received.\n", + EXIT_SUCCESS ); + printf("\t%d - An event you did not ask to watch for was received\n", + EXIT_FAILURE); + printf("\t (usually delete_self or unmount), or some error " + "occurred.\n"); + printf("\t%d - The --timeout option was given and no events occurred\n", + EXIT_TIMEOUT); + printf("\t in the specified interval of time.\n\n"); + printf("Events:\n"); + print_event_descriptions(); +} + |