diff options
Diffstat (limited to 'libinotifytools/src/inotifytools.c')
-rw-r--r-- | libinotifytools/src/inotifytools.c | 2060 |
1 files changed, 2060 insertions, 0 deletions
diff --git a/libinotifytools/src/inotifytools.c b/libinotifytools/src/inotifytools.c new file mode 100644 index 0000000..b3feca3 --- /dev/null +++ b/libinotifytools/src/inotifytools.c @@ -0,0 +1,2060 @@ +// kate: replace-tabs off; space-indent off; + +/** + * @mainpage libinotifytools + * + * libinotifytools is a small C library to simplify the use of Linux's inotify + * interface. + * + * @link inotifytools/inotifytools.h Documentation for the library's public + * interface.@endlink + * + * @link todo.html TODO list.@endlink + */ + +#include "../../config.h" +#include "inotifytools/inotifytools.h" +#include "inotifytools_p.h" + +#include <string.h> +#include <strings.h> +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <sys/select.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/ioctl.h> +#include <unistd.h> +#include <dirent.h> +#include <time.h> +#include <regex.h> +#include <setjmp.h> + +#include "inotifytools/inotify.h" + +/** + * @file inotifytools/inotifytools.h + * @brief inotifytools library public interface. + * @author Rohan McGovern, \<rohan@mcgovern.id.au\> + * + * This library provides a thin layer on top of the basic inotify interface. + * The primary use is to easily set up watches on files, potentially many files + * at once, and read events without having to deal with low-level I/O. There + * are also several utility functions for inotify-related string formatting. + * + * To use this library, you must \c \#include the following headers accordingly: + * \li \c \<inotifytools/inotifytools.h\> - to use any functions declared in + * this file. + * \li \c \<inotifytools/inotify.h\> - to have the \c inotify_event type defined + * and the numeric IN_* event constants defined. If \c \<sys/inotify.h\> + * was present on your system at compile time, this header simply includes + * that. Otherwise it includes \c \<inotifytools/inotify-nosys.h\>. + * + * @section example Example + * This very simple program recursively watches the entire directory tree + * under its working directory for events, then prints them out with a + * timestamp. + * @include example.c + * + * @section events Events + * + * @note This section comes almost verbatim from the inotify(7) man page. + * + * @warning The information here applies to inotify in Linux 2.6.17. Older + * versions of Linux may not support all the events described here. + * + * The following numeric events can be specified to functions in inotifytools, + * and may be present in events returned through inotifytools: + * + * \li \c IN_ACCESS - File was accessed (read) \a * + * \li \c IN_ATTRIB - Metadata changed (permissions, timestamps, + * extended attributes, etc.) \a * + * \li \c IN_CLOSE_WRITE - File opened for writing was closed \a * + * \li \c IN_CLOSE_NOWRITE - File not opened for writing was closed \a * + * \li \c IN_CREATE - File/directory created in watched directory \a * + * \li \c IN_DELETE - File/directory deleted from watched directory \a * + * \li \c IN_DELETE_SELF - Watched file/directory was itself deleted + * \li \c IN_MODIFY - File was modified \a * + * \li \c IN_MOVE_SELF - Watched file/directory was itself moved + * \li \c IN_MOVED_FROM - File moved out of watched directory \a * + * \li \c IN_MOVED_TO - File moved into watched directory \a * + * \li \c IN_OPEN - File was opened \a * + * + * When monitoring a directory, the events marked with an asterisk \a * above + * can occur for files in the directory, in which case the name field in the + * returned inotify_event structure identifies the name of the file within the + * directory. + * + * The IN_ALL_EVENTS macro is defined as a bit mask of all of the above events. + * + * Two additional convenience macros are IN_MOVE, which equates to + * IN_MOVED_FROM|IN_MOVED_TO, and IN_CLOSE which equates to + * IN_CLOSE_WRITE|IN_CLOSE_NOWRITE. + * + * The following bitmasks can also be provided when creating a new watch: + * + * \li \c IN_DONT_FOLLOW - Don't dereference pathname if it is a symbolic link + * \li \c IN_MASK_ADD - Add (OR) events to watch mask for this pathname if + * it already exists (instead of replacing mask) + * \li \c IN_ONESHOT - Monitor pathname for one event, then remove from + * watch list + * \li \c IN_ONLYDIR - Only watch pathname if it is a directory + * + * The following bitmasks may occur in events generated by a watch: + * + * \li \c IN_IGNORED - Watch was removed explicitly + * (inotifytools_remove_watch_*) or automatically (file + * was deleted, or file system was unmounted) + * \li \c IN_ISDIR - Subject of this event is a directory + * \li \c IN_Q_OVERFLOW - Event queue overflowed (wd is -1 for this event) + * \li \c IN_UNMOUNT - File system containing watched object was unmounted + * + * @section TODO TODO list + * + * @todo Improve wd/filename mapping. Currently there is no explicit code for + * handling different filenames mapping to the same inode (and hence, wd). + * gamin's approach sounds good: let the user watch an inode using several + * different filenames, and when an event occurs on the inode, generate an + * event for each filename. + */ + +#define MAX_EVENTS 4096 +#define MAX_STRLEN 4096 +#define INOTIFY_PROCDIR "/proc/sys/fs/inotify/" +#define WATCHES_SIZE_PATH INOTIFY_PROCDIR "max_user_watches" +#define QUEUE_SIZE_PATH INOTIFY_PROCDIR "max_queued_watches" +#define INSTANCES_PATH INOTIFY_PROCDIR "max_user_instances" + +static int inotify_fd; +static unsigned num_access; +static unsigned num_modify; +static unsigned num_attrib; +static unsigned num_close_nowrite; +static unsigned num_close_write; +static unsigned num_open; +static unsigned num_move_self; +static unsigned num_moved_to; +static unsigned num_moved_from; +static unsigned num_create; +static unsigned num_delete; +static unsigned num_delete_self; +static unsigned num_unmount; +static unsigned num_total; +static int collect_stats = 0; + +struct rbtree *tree_wd = 0; +struct rbtree *tree_filename = 0; +static int error = 0; +static int init = 0; +static char* timefmt = 0; +static regex_t* regex = 0; + +int isdir( char const * path ); +void record_stats( struct inotify_event const * event ); +int onestr_to_event(char const * event); + +/** + * @internal + * Assert that a condition evaluates to true, and optionally output a message + * if the assertion fails. + * + * @param cond Integer; if 0, assertion fails, otherwise assertion succeeds. + * + * @param mesg A human-readable error message shown if assertion fails. + * + * @section example Example + * @code + * int upper = 100, lower = 50; + * int input = get_user_input(); + * niceassert( input <= upper && input >= lower, + * "input not in required range!"); + * @endcode + */ +#define niceassert(cond,mesg) _niceassert((long)cond, __LINE__, __FILE__, \ + #cond, mesg) + +#define nasprintf(...) niceassert( -1 != asprintf(__VA_ARGS__), "out of memory") + +/** + * @internal + * Assert that a condition evaluates to true, and optionally output a message + * if the assertion fails. + * + * You should use the niceassert() preprocessor macro instead. + * + * @param cond If 0, assertion fails, otherwise assertion succeeds. + * + * @param line Line number of source code where assertion is made. + * + * @param file Name of source file where assertion is made. + * + * @param condstr Stringified assertion expression. + * + * @param mesg A human-readable error message shown if assertion fails. + */ +void _niceassert( long cond, int line, char const * file, char const * condstr, + char const * mesg ) { + if ( cond ) return; + + if ( mesg ) { + fprintf(stderr, "%s:%d assertion ( %s ) failed: %s\n", file, line, + condstr, mesg ); + } + else { + fprintf(stderr, "%s:%d assertion ( %s ) failed.\n", file, line, condstr); + } +} + +/** + * @internal + * Construct a string from a character. + * + * @param ch A character. + * + * @return A string of length 1 consisting of the character @a ch. The string + * will be overwritten in subsequent calls. + */ +char * chrtostr(char ch) { + static char str[2] = { '\0', '\0' }; + str[0] = ch; + return str; +} + +/** + * @internal + */ +int read_num_from_file( char * filename, int * num ) { + FILE * file = fopen( filename, "r" ); + if ( !file ) { + error = errno; + return 0; + } + + if ( EOF == fscanf( file, "%d", num ) ) { + error = errno; + return 0; + } + + niceassert( 0 == fclose( file ), 0 ); + + return 1; +} + +int wd_compare(const void *d1, const void *d2, const void *config) { + if (!d1 || !d2) return d1 - d2; + return ((watch*)d1)->wd - ((watch*)d2)->wd; +} + +int filename_compare(const void *d1, const void *d2, const void *config) { + if (!d1 || !d2) return d1 - d2; + return strcmp(((watch*)d1)->filename, ((watch*)d2)->filename); +} + +/** + * @internal + */ +watch *watch_from_wd( int wd ) { + watch w; + w.wd = wd; + return (watch*)rbfind(&w, tree_wd); +} + +/** + * @internal + */ +watch *watch_from_filename( char const *filename ) { + watch w; + w.filename = (char*)filename; + return (watch*)rbfind(&w, tree_filename); +} + +/** + * Initialise inotify. + * + * You must call this function before using any function which adds or removes + * watches or attempts to access any information about watches. + * + * @return 1 on success, 0 on failure. On failure, the error can be + * obtained from inotifytools_error(). + */ +int inotifytools_initialize() { + if (init) return 1; + + error = 0; + // Try to initialise inotify + inotify_fd = inotify_init(); + if (inotify_fd < 0) { + error = inotify_fd; + return 0; + } + + collect_stats = 0; + init = 1; + tree_wd = rbinit(wd_compare, 0); + tree_filename = rbinit(filename_compare, 0); + timefmt = 0; + + return 1; +} + +/** + * @internal + */ +void destroy_watch(watch *w) { + if (w->filename) free(w->filename); + free(w); +} + +/** + * @internal + */ +void cleanup_tree(const void *nodep, + const VISIT which, + const int depth, void* arg) { + if (which != endorder && which != leaf) return; + watch *w = (watch*)nodep; + destroy_watch(w); +} + +/** + * Close inotify and free the memory used by inotifytools. + * + * If you call this function, you must call inotifytools_initialize() + * again before any other functions can be used. + */ +void inotifytools_cleanup() { + if (!init) return; + + init = 0; + close(inotify_fd); + collect_stats = 0; + error = 0; + timefmt = 0; + + if (regex) { + regfree(regex); + free(regex); + regex = 0; + } + + rbwalk(tree_wd, cleanup_tree, 0); + rbdestroy(tree_wd); tree_wd = 0; + rbdestroy(tree_filename); tree_filename = 0; +} + +/** + * @internal + */ +void empty_stats(const void *nodep, + const VISIT which, + const int depth, void *arg) { + if (which != endorder && which != leaf) return; + watch *w = (watch*)nodep; + w->hit_access = 0; + w->hit_modify = 0; + w->hit_attrib = 0; + w->hit_close_nowrite = 0; + w->hit_close_write = 0; + w->hit_open = 0; + w->hit_move_self = 0; + w->hit_moved_from = 0; + w->hit_moved_to = 0; + w->hit_create = 0; + w->hit_delete = 0; + w->hit_delete_self = 0; + w->hit_unmount = 0; + w->hit_total = 0; +} + +/** + * @internal + */ +void replace_filename(const void *nodep, + const VISIT which, + const int depth, void *arg) { + if (which != endorder && which != leaf) return; + watch *w = (watch*)nodep; + char *old_name = ((char**)arg)[0]; + char *new_name = ((char**)arg)[1]; + int old_len = *((int*)&((char**)arg)[2]); + char *name; + if ( 0 == strncmp( old_name, w->filename, old_len ) ) { + nasprintf( &name, "%s%s", new_name, &(w->filename[old_len]) ); + if (!strcmp( w->filename, new_name )) { + free(name); + } else { + rbdelete(w, tree_filename); + free( w->filename ); + w->filename = name; + rbsearch(w, tree_filename); + } + } +} + +/** + * @internal + */ +void get_num(const void *nodep, + const VISIT which, + const int depth, void *arg) { + if (which != endorder && which != leaf) return; + ++(*((int*)arg)); +} + + +/** + * Initialize or reset statistics. + * + * inotifytools_initialize() must be called before this function can + * be used. + * + * When this function is called, all subsequent events will be tallied. + * Statistics can then be obtained via the @a inotifytools_get_stat_* functions. + * + * After the first call, subsequent calls to this function will reset the + * event tallies to 0. + */ +void inotifytools_initialize_stats() { + niceassert( init, "inotifytools_initialize not called yet" ); + + // if already collecting stats, reset stats + if (collect_stats) { + rbwalk(tree_wd, empty_stats, 0); + } + + num_access = 0; + num_modify = 0; + num_attrib = 0; + num_close_nowrite = 0; + num_close_write = 0; + num_open = 0; + num_move_self = 0; + num_moved_from = 0; + num_moved_to = 0; + num_create = 0; + num_delete = 0; + num_delete_self = 0; + num_unmount = 0; + num_total = 0; + + collect_stats = 1; +} + +/** + * Convert character separated events from string form to integer form + * (as in inotify.h). + * + * @param event a sequence of events in string form as defined in + * inotify.h without leading IN_ prefix (e.g., MODIFY, + * ATTRIB), separated by the @a sep character. Case + * insensitive. Can be a single event. + * Can be empty or NULL. See section \ref events. + * + * @param sep Character used to separate events. @a sep must not be + * a character in a-z, A-Z, or _. + * + * @return integer representing the mask specified by @a event, or 0 + * if any string in @a event is empty or NULL, or -1 if + * any string in @a event does not match any event or + * @a sep is invalid. + * + * @section example Example + * @code + * char * eventstr = "MODIFY:CLOSE:CREATE"; + * int eventnum = inotifytools_str_to_event_sep( eventstr, ':' ); + * if ( eventnum == IN_MODIFY | IN_CLOSE | IN_CREATE ) { + * printf( "This code always gets executed!\n" ); + * } + * @endcode + */ +int inotifytools_str_to_event_sep(char const * event, char sep) { + if ( strchr( "_" "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", sep ) ) { + return -1; + } + + int ret, ret1, len; + char * event1, * event2; + char eventstr[4096]; + ret = 0; + + if ( !event || !event[0] ) return 0; + + event1 = (char *)event; + event2 = strchr( event1, sep ); + while ( event1 && event1[0] ) { + if ( event2 ) { + len = event2 - event1; + niceassert( len < 4096, "malformed event string (very long)" ); + } + else { + len = strlen(event1); + } + if ( len > 4095 ) len = 4095; + strncpy( eventstr, event1, len ); + eventstr[len] = 0; + + ret1 = onestr_to_event( eventstr ); + if ( 0 == ret1 || -1 == ret1 ) { + ret = ret1; + break; + } + ret |= ret1; + + event1 = event2; + if ( event1 && event1[0] ) { + // jump over 'sep' character + ++event1; + // if last character was 'sep'... + if ( !event1[0] ) return 0; + event2 = strchr( event1, sep ); + } + } + + return ret; +} + +/** + * Convert comma-separated events from string form to integer form + * (as in inotify.h). + * + * @param event a sequence of events in string form as defined in + * inotify.h without leading IN_ prefix (e.g., MODIFY, + * ATTRIB), comma-separated. Case + * insensitive. Can be a single event. + * Can be empty or NULL. See section \ref events. + * + * @return integer representing the mask specified by @a event, or 0 + * if any string in @a event is empty or NULL, or -1 if + * any string in @a event does not match any event. + * + * @section example Example + * @code + * char * eventstr = "MODIFY,CLOSE,CREATE"; + * int eventnum = inotifytools_str_to_event( eventstr ); + * if ( eventnum == IN_MODIFY | IN_CLOSE | IN_CREATE ) { + * printf( "This code always gets executed!\n" ); + * } + * @endcode + */ +int inotifytools_str_to_event(char const * event) { + return inotifytools_str_to_event_sep( event, ',' ); +} + +/** + * @internal + * Convert a single event from string form to integer form (as in inotify.h). + * + * @param event event in string form as defined in inotify.h without + * leading IN_ prefix (e.g., MODIFY, ATTRIB). Case + * insensitive. Can be empty or NULL. + * @return integer representing the mask specified by 'event', or 0 + * if @a event is empty or NULL, or -1 if string does not + * match any event. + */ +int onestr_to_event(char const * event) +{ + static int ret; + ret = -1; + + if ( !event || !event[0] ) + ret = 0; + else if ( 0 == strcasecmp(event, "ACCESS") ) + ret = IN_ACCESS; + else if ( 0 == strcasecmp(event, "MODIFY") ) + ret = IN_MODIFY; + else if ( 0 == strcasecmp(event, "ATTRIB") ) + ret = IN_ATTRIB; + else if ( 0 == strcasecmp(event, "CLOSE_WRITE") ) + ret = IN_CLOSE_WRITE; + else if ( 0 == strcasecmp(event, "CLOSE_NOWRITE") ) + ret = IN_CLOSE_NOWRITE; + else if ( 0 == strcasecmp(event, "OPEN") ) + ret = IN_OPEN; + else if ( 0 == strcasecmp(event, "MOVED_FROM") ) + ret = IN_MOVED_FROM; + else if ( 0 == strcasecmp(event, "MOVED_TO") ) + ret = IN_MOVED_TO; + else if ( 0 == strcasecmp(event, "CREATE") ) + ret = IN_CREATE; + else if ( 0 == strcasecmp(event, "DELETE") ) + ret = IN_DELETE; + else if ( 0 == strcasecmp(event, "DELETE_SELF") ) + ret = IN_DELETE_SELF; + else if ( 0 == strcasecmp(event, "UNMOUNT") ) + ret = IN_UNMOUNT; + else if ( 0 == strcasecmp(event, "Q_OVERFLOW") ) + ret = IN_Q_OVERFLOW; + else if ( 0 == strcasecmp(event, "IGNORED") ) + ret = IN_IGNORED; + else if ( 0 == strcasecmp(event, "CLOSE") ) + ret = IN_CLOSE; + else if ( 0 == strcasecmp(event, "MOVE_SELF") ) + ret = IN_MOVE_SELF; + else if ( 0 == strcasecmp(event, "MOVE") ) + ret = IN_MOVE; + else if ( 0 == strcasecmp(event, "ISDIR") ) + ret = IN_ISDIR; + else if ( 0 == strcasecmp(event, "ONESHOT") ) + ret = IN_ONESHOT; + else if ( 0 == strcasecmp(event, "ALL_EVENTS") ) + ret = IN_ALL_EVENTS; + + return ret; +} + +/** + * Convert event from integer form to string form (as in inotify.h). + * + * The returned string is from static storage; subsequent calls to this function + * or inotifytools_event_to_str_sep() will overwrite it. Don't free() it and + * make a copy if you want to keep it. + * + * @param events OR'd event(s) in integer form as defined in inotify.h. + * See section \ref events. + * + * @return comma-separated string representing the event(s), in no + * particular order + * + * @section example Example + * @code + * int eventnum == IN_MODIFY | IN_CLOSE | IN_CREATE; + * char * eventstr = inotifytools_event_to_str( eventnum ); + * printf( "%s\n", eventstr ); + * // outputs something like MODIFY,CLOSE,CREATE but order not guaranteed. + * @endcode + */ +char * inotifytools_event_to_str(int events) { + return inotifytools_event_to_str_sep(events, ','); +} + +/** + * Convert event from integer form to string form (as in inotify.h). + * + * The returned string is from static storage; subsequent calls to this function + * or inotifytools_event_to_str() will overwrite it. Don't free() it and + * make a copy if you want to keep it. + * + * @param events OR'd event(s) in integer form as defined in inotify.h + * + * @param sep character used to separate events + * + * @return @a sep separated string representing the event(s), in no + * particular order. If the integer is not made of OR'ed + * inotify events, the string returned will be a hexadecimal + * representation of the integer. + * + * @section example Example + * @code + * int eventnum == IN_MODIFY | IN_CLOSE | IN_CREATE; + * char * eventstr = inotifytools_event_to_str_sep( eventnum, '-' ); + * printf( "%s\n", eventstr ); + * // outputs something like MODIFY-CLOSE-CREATE but order not guaranteed. + * @endcode + */ +char * inotifytools_event_to_str_sep(int events, char sep) +{ + static char ret[1024]; + ret[0] = '\0'; + ret[1] = '\0'; + + if ( IN_ACCESS & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "ACCESS" ); + } + if ( IN_MODIFY & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "MODIFY" ); + } + if ( IN_ATTRIB & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "ATTRIB" ); + } + if ( IN_CLOSE_WRITE & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "CLOSE_WRITE" ); + } + if ( IN_CLOSE_NOWRITE & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "CLOSE_NOWRITE" ); + } + if ( IN_OPEN & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "OPEN" ); + } + if ( IN_MOVED_FROM & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "MOVED_FROM" ); + } + if ( IN_MOVED_TO & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "MOVED_TO" ); + } + if ( IN_CREATE & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "CREATE" ); + } + if ( IN_DELETE & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "DELETE" ); + } + if ( IN_DELETE_SELF & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "DELETE_SELF" ); + } + if ( IN_UNMOUNT & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "UNMOUNT" ); + } + if ( IN_Q_OVERFLOW & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "Q_OVERFLOW" ); + } + if ( IN_IGNORED & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "IGNORED" ); + } + if ( IN_CLOSE & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "CLOSE" ); + } + if ( IN_MOVE_SELF & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "MOVE_SELF" ); + } + if ( IN_ISDIR & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "ISDIR" ); + } + if ( IN_ONESHOT & events ) { + strcat( ret, chrtostr(sep) ); + strcat( ret, "ONESHOT" ); + } + + // Maybe we didn't match any... ? + if (ret[0] == '\0') { + niceassert( -1 != sprintf( ret, "%c0x%08x", sep, events ), 0 ); + } + + return &ret[1]; +} + +/** + * Get the filename used to establish a watch. + * + * inotifytools_initialize() must be called before this function can + * be used. + * + * @param wd watch descriptor. + * + * @return filename associated with watch descriptor @a wd, or NULL if @a wd + * is not associated with any filename. + * + * @note This always returns the filename which was used to establish a watch. + * This means the filename may be a relative path. If this isn't desired, + * then always use absolute paths when watching files. + * Also, this is not necessarily the filename which might have been used + * to cause an event on the file, since inotify is inode based and there + * can be many filenames mapping to a single inode. + * Finally, if a file is moved or renamed while being watched, the + * filename returned will still be the original name. + */ +char * inotifytools_filename_from_wd( int wd ) { + niceassert( init, "inotifytools_initialize not called yet" ); + watch *w = watch_from_wd(wd); + if (!w) + return NULL; + + return w->filename; +} + +/** + * Get the watch descriptor for a particular filename. + * + * inotifytools_initialize() must be called before this function can + * be used. + * + * @param filename file name to find watch descriptor for. + * + * @return watch descriptor associated with filename, or -1 if @a filename is + * not associated with any watch descriptor. + * + * @note The filename specified must always be the original name used to + * establish the watch. + */ +int inotifytools_wd_from_filename( char const * filename ) { + niceassert( init, "inotifytools_initialize not called yet" ); + watch *w = watch_from_filename(filename); + if (!w) return -1; + return w->wd; +} + +/** + * Set the filename for a particular watch descriptor. + * + * This function should be used to update a filename when a file is known to + * have been moved or renamed. At the moment, libinotifytools does not + * automatically handle this situation. + * + * inotifytools_initialize() must be called before this function can + * be used. + * + * @param wd Watch descriptor. + * + * @param filename New filename. + */ +void inotifytools_set_filename_by_wd( int wd, char const * filename ) { + niceassert( init, "inotifytools_initialize not called yet" ); + watch *w = watch_from_wd(wd); + if (!w) return; + if (w->filename) free(w->filename); + w->filename = strdup(filename); +} + +/** + * Set the filename for one or more watches with a particular existing filename. + * + * This function should be used to update a filename when a file is known to + * have been moved or renamed. At the moment, libinotifytools does not + * automatically handle this situation. + * + * inotifytools_initialize() must be called before this function can + * be used. + * + * @param oldname Current filename. + * + * @param newname New filename. + */ +void inotifytools_set_filename_by_filename( char const * oldname, + char const * newname ) { + watch *w = watch_from_filename(oldname); + if (!w) return; + if (w->filename) free(w->filename); + w->filename = strdup(newname); +} + +/** + * Replace a certain filename prefix on all watches. + * + * This function should be used to update filenames for an entire directory tree + * when a directory is known to have been moved or renamed. At the moment, + * libinotifytools does not automatically handle this situation. + * + * inotifytools_initialize() must be called before this function can + * be used. + * + * @param oldname Current filename prefix. + * + * @param newname New filename prefix. + * + * @section example Example + * @code + * // if /home/user1/original_dir is moved to /home/user2/new_dir, then to + * // update all watches: + * inotifytools_replace_filename( "/home/user1/original_dir", + * "/home/user2/new_dir" ); + * @endcode + */ +void inotifytools_replace_filename( char const * oldname, + char const * newname ) { + if ( !oldname || !newname ) return; + char *names[2+sizeof(int)/sizeof(char*)]; + names[0] = (char*)oldname; + names[1] = (char*)newname; + *((int*)&names[2]) = strlen(oldname); + rbwalk(tree_filename, replace_filename, (void*)names); +} + +/** + * @internal + */ +int remove_inotify_watch(watch *w) { + error = 0; + int status = inotify_rm_watch( inotify_fd, w->wd ); + if ( status < 0 ) { + fprintf(stderr, "Failed to remove watch on %s: %s\n", w->filename, + strerror(status) ); + error = status; + return 0; + } + return 1; +} + +/** + * @internal + */ +watch *create_watch(int wd, char *filename) { + if ( wd <= 0 || !filename) return 0; + + watch *w = (watch*)calloc(1, sizeof(watch)); + w->wd = wd; + w->filename = strdup(filename); + rbsearch(w, tree_wd); + rbsearch(w, tree_filename); +} + +/** + * Remove a watch on a file specified by watch descriptor. + * + * inotifytools_initialize() must be called before this function can + * be used. + * + * @param wd Watch descriptor of watch to be removed. + * + * @return 1 on success, 0 on failure. If the given watch doesn't exist, + * returns 1. On failure, the error can be + * obtained from inotifytools_error(). + */ +int inotifytools_remove_watch_by_wd( int wd ) { + niceassert( init, "inotifytools_initialize not called yet" ); + watch *w = watch_from_wd(wd); + if (!w) return 1; + + if (!remove_inotify_watch(w)) return 0; + rbdelete(w, tree_wd); + rbdelete(w, tree_filename); + destroy_watch(w); + return 1; +} + +/** + * Remove a watch on a file specified by filename. + * + * @param filename Name of file on which watch should be removed. + * + * @return 1 on success, 0 on failure. On failure, the error can be + * obtained from inotifytools_error(). + * + * @note The filename specified must always be the original name used to + * establish the watch. + */ +int inotifytools_remove_watch_by_filename( char const * filename ) { + niceassert( init, "inotifytools_initialize not called yet" ); + watch *w = watch_from_filename(filename); + if (!w) return 1; + + if (!remove_inotify_watch(w)) return 0; + rbdelete(w, tree_wd); + rbdelete(w, tree_filename); + destroy_watch(w); + return 1; +} + +/** + * Set up a watch on a file. + * + * @param filename Absolute or relative path of file to watch. + * + * @param events bitwise ORed inotify events to watch for. See section + * \ref events. + * + * @return 1 on success, 0 on failure. On failure, the error can be + * obtained from inotifytools_error(). + */ +int inotifytools_watch_file( char const * filename, int events ) { + static char const * filenames[2]; + filenames[0] = filename; + filenames[1] = NULL; + return inotifytools_watch_files( filenames, events ); +} + +/** + * Set up a watch on a list of files. + * + * inotifytools_initialize() must be called before this function can + * be used. + * + * @param filenames null-terminated array of absolute or relative paths of + * files to watch. + * + * @param events bitwise OR'ed inotify events to watch for. See section + * \ref events. + * + * @return 1 on success, 0 on failure. On failure, the error can be + * obtained from inotifytools_error(). + */ +int inotifytools_watch_files( char const * filenames[], int events ) { + niceassert( init, "inotifytools_initialize not called yet" ); + error = 0; + + static int i; + for ( i = 0; filenames[i]; ++i ) { + static int wd; + wd = inotify_add_watch( inotify_fd, filenames[i], events ); + if ( wd < 0 ) { + if ( wd == -1 ) { + error = errno; + return 0; + } // if ( wd == -1 ) + else { + fprintf( stderr, "Failed to watch %s: returned wd was %d " + "(expected -1 or >0 )", filenames[i], wd ); + // no appropriate value for error + return 0; + } // else + } // if ( wd < 0 ) + + char *filename; + // Always end filename with / if it is a directory + if ( !isdir(filenames[i]) + || filenames[i][strlen(filenames[i])-1] == '/') { + filename = strdup(filenames[i]); + } + else { + nasprintf( &filename, "%s/", filenames[i] ); + } + create_watch(wd, filename); + free(filename); + } // for + + return 1; +} + +/** + * Get the next inotify event to occur. + * + * inotifytools_initialize() must be called before this function can + * be used. + * + * @param timeout maximum amount of time, in seconds, to wait for an event. + * If @a timeout is 0, the function is non-blocking. If + * @a timeout is negative, the function will block until an + * event occurs. + * + * @return pointer to an inotify event, or NULL if function timed out before + * an event occurred. The event is located in static storage and it + * may be overwritten in subsequent calls; do not call free() on it, + * and make a copy if you want to keep it. + * + * @note Your program should call this function or + * inotifytools_next_events() frequently; between calls to this function, + * inotify events will be queued in the kernel, and eventually the queue + * will overflow and you will miss some events. + * + * @note If the function inotifytools_ignore_events_by_regex() has been called + * with a non-NULL parameter, this function will not return on events + * which match the regular expression passed to that function. However, + * the @a timeout period begins again each time a matching event occurs. + */ +struct inotify_event * inotifytools_next_event( int timeout ) { + return inotifytools_next_events( timeout, 1 ); +} + + +/** + * Get the next inotify events to occur. + * + * inotifytools_initialize() must be called before this function can + * be used. + * + * @param timeout maximum amount of time, in seconds, to wait for an event. + * If @a timeout is 0, the function is non-blocking. If + * @a timeout is negative, the function will block until an + * event occurs. + * + * @param num_events approximate number of inotify events to wait for until + * this function returns. Use this for buffering reads to + * inotify if you expect to receive large amounts of events. + * You are NOT guaranteed that this number of events will + * actually be read; instead, you are guaranteed that the + * number of bytes read from inotify is + * @a num_events * sizeof(struct inotify_event). Obviously + * the larger this number is, the greater the latency between + * when an event occurs and when you'll know about it. + * May not be larger than 4096. + * + * @return pointer to an inotify event, or NULL if function timed out before + * an event occurred or @a num_events < 1. The event is located in + * static storage and it may be overwritten in subsequent calls; do not + * call free() on it, and make a copy if you want to keep it. + * When @a num_events is greater than 1, this will return a pointer to + * the first event only, and you MUST call this function again to + * get pointers to subsequent events; don't try to add to the pointer + * to find the next events or you will run into trouble. + * + * @note You may actually get different events with different values of + * @a num_events. This is because inotify does some in-kernel filtering + * of duplicate events, meaning some duplicate events will not be + * reported if @a num_events > 1. For some purposes this is fine, but + * for others (such as gathering accurate statistics on numbers of event + * occurrences) you must call this function with @a num_events = 1, or + * simply use inotifytools_next_event(). + * + * @note Your program should call this function or + * inotifytools_next_events() frequently; between calls to this function, + * inotify events will be queued in the kernel, and eventually the queue + * will overflow and you will miss some events. + * + * @note If the function inotifytools_ignore_events_by_regex() has been called + * with a non-NULL parameter, this function will not return on events + * which match the regular expression passed to that function. However, + * the @a timeout period begins again each time a matching event occurs. + */ +struct inotify_event * inotifytools_next_events( int timeout, int num_events ) { + niceassert( init, "inotifytools_initialize not called yet" ); + niceassert( num_events <= MAX_EVENTS, "too many events requested" ); + + if ( num_events < 1 ) return NULL; + + static struct inotify_event event[MAX_EVENTS]; + static struct inotify_event * ret; + static int first_byte = 0; + static ssize_t bytes; + static jmp_buf jmp; + static char match_name[MAX_STRLEN]; + +#define RETURN(A) {\ + if (regex) {\ + inotifytools_snprintf(match_name, MAX_STRLEN, A, "%w%f");\ + if (0 == regexec(regex, match_name, 0, 0, 0)) {\ + longjmp(jmp,0);\ + }\ + }\ + if ( collect_stats ) {\ + record_stats( A );\ + }\ + return A;\ +} + + setjmp(jmp); + + error = 0; + + // first_byte is index into event buffer + if ( first_byte != 0 + && first_byte <= (int)(bytes - sizeof(struct inotify_event)) ) { + + ret = (struct inotify_event *)((char *)&event[0] + first_byte); + first_byte += sizeof(struct inotify_event) + ret->len; + + // if the pointer to the next event exactly hits end of bytes read, + // that's good. next time we're called, we'll read. + if ( first_byte == bytes ) { + first_byte = 0; + } + else if ( first_byte > bytes ) { + // oh... no. this can't be happening. An incomplete event. + // Copy what we currently have into first element, call self to + // read remainder. + // oh, and they BETTER NOT overlap. + // Boy I hope this code works. + // But I think this can never happen due to how inotify is written. + niceassert( (long)((char *)&event[0] + + sizeof(struct inotify_event) + + event[0].len) <= (long)ret, + "extremely unlucky user, death imminent" ); + // how much of the event do we have? + bytes = (char *)&event[0] + bytes - (char *)ret; + memcpy( &event[0], ret, bytes ); + return inotifytools_next_events( timeout, num_events ); + } + RETURN(ret); + + } + + else if ( first_byte == 0 ) { + bytes = 0; + } + + + static ssize_t this_bytes; + static unsigned int bytes_to_read; + static int rc; + static fd_set read_fds; + + static struct timeval read_timeout; + read_timeout.tv_sec = timeout; + read_timeout.tv_usec = 0; + static struct timeval * read_timeout_ptr; + read_timeout_ptr = ( timeout <= 0 ? NULL : &read_timeout ); + + FD_ZERO(&read_fds); + FD_SET(inotify_fd, &read_fds); + rc = select(inotify_fd + 1, &read_fds, + NULL, NULL, read_timeout_ptr); + if ( rc < 0 ) { + // error + error = errno; + return NULL; + } + else if ( rc == 0 ) { + // timeout + return NULL; + } + + // wait until we have enough bytes to read + do { + rc = ioctl( inotify_fd, FIONREAD, &bytes_to_read ); + } while ( !rc && + bytes_to_read < sizeof(struct inotify_event)*num_events ); + + if ( rc == -1 ) { + error = errno; + return NULL; + } + + this_bytes = read(inotify_fd, &event[0] + bytes, + sizeof(struct inotify_event)*MAX_EVENTS - bytes); + if ( this_bytes < 0 ) { + error = errno; + return NULL; + } + if ( this_bytes == 0 ) { + fprintf(stderr, "Inotify reported end-of-file. Possibly too many " + "events occurred at once.\n"); + return NULL; + } + bytes += this_bytes; + + ret = &event[0]; + first_byte = sizeof(struct inotify_event) + ret->len; + niceassert( first_byte <= bytes, "ridiculously long filename, things will " + "almost certainly screw up." ); + if ( first_byte == bytes ) { + first_byte = 0; + } + + RETURN(ret); + +#undef RETURN +} + +/** + * Set up recursive watches on an entire directory tree. + * + * inotifytools_initialize() must be called before this function can + * be used. + * + * @param path path of directory or file to watch. If the path is a directory, + * every subdirectory will also be watched for the same events up + * to the maximum readable depth. If the path is a file, the file + * is watched exactly as if inotifytools_watch_file() were used. + * + * @param events Inotify events to watch for. See section \ref events. + * + * @return 1 on success, 0 on failure. On failure, the error can be + * obtained from inotifytools_error(). Note that some errors on + * subdirectories will be ignored; for example, if you watch a directory + * tree which contains some directories which you do not have access to, + * those directories will not be watched, but this function will still + * return 1 if no other errors occur. + * + * @note This function does not attempt to work atomically. If you use this + * function to watch a directory tree and files or directories are being + * created or removed within that directory tree, there are no guarantees + * as to whether or not those files will be watched. + */ +int inotifytools_watch_recursively( char const * path, int events ) { + return inotifytools_watch_recursively_with_exclude( path, events, 0 ); +} + +/** + * Set up recursive watches on an entire directory tree, optionally excluding + * some directories. + * + * inotifytools_initialize() must be called before this function can + * be used. + * + * @author UH + * + * @param path path of directory or file to watch. If the path is a directory, + * every subdirectory will also be watched for the same events up + * to the maximum readable depth. If the path is a file, the file + * is watched exactly as if inotifytools_watch_file() were used. + * + * @param exclude_list NULL terminated path list of directories not to watch. + * Can be NULL if no paths are to be excluded. + * Directories may or may not include a trailing '/'. + * + * @param events Inotify events to watch for. See section \ref events. + * + * @return 1 on success, 0 on failure. On failure, the error can be + * obtained from inotifytools_error(). Note that some errors on + * subdirectories will be ignored; for example, if you watch a directory + * tree which contains some directories which you do not have access to, + * those directories will not be watched, but this function will still + * return 1 if no other errors occur. + * + * @note This function does not attempt to work atomically. If you use this + * function to watch a directory tree and files or directories are being + * created or removed within that directory tree, there are no guarantees + * as to whether or not those files will be watched. + */ +int inotifytools_watch_recursively_with_exclude( char const * path, int events, + char const ** exclude_list ) { + niceassert( init, "inotifytools_initialize not called yet" ); + + DIR * dir; + char * my_path; + error = 0; + dir = opendir( path ); + if ( !dir ) { + // If not a directory, don't need to do anything special + if ( errno == ENOTDIR ) { + return inotifytools_watch_file( path, events ); + } + else { + error = errno; + return 0; + } + } + + if ( path[strlen(path)-1] != '/' ) { + nasprintf( &my_path, "%s/", path ); + } + else { + my_path = (char *)path; + } + + static struct dirent * ent; + char * next_file; + static struct stat64 my_stat; + ent = readdir( dir ); + // Watch each directory within this directory + while ( ent ) { + if ( (0 != strcmp( ent->d_name, "." )) && + (0 != strcmp( ent->d_name, ".." )) ) { + nasprintf(&next_file,"%s%s", my_path, ent->d_name); + if ( -1 == lstat64( next_file, &my_stat ) ) { + error = errno; + free( next_file ); + if ( errno != EACCES ) { + error = errno; + if ( my_path != path ) free( my_path ); + closedir( dir ); + return 0; + } + } + else if ( S_ISDIR( my_stat.st_mode ) && + !S_ISLNK( my_stat.st_mode )) { + free( next_file ); + nasprintf(&next_file,"%s%s/", my_path, ent->d_name); + static unsigned int no_watch; + static char const ** exclude_entry; + + no_watch = 0; + for (exclude_entry = exclude_list; + exclude_entry && *exclude_entry && !no_watch; + ++exclude_entry) { + static int exclude_length; + + exclude_length = strlen(*exclude_entry); + if ((*exclude_entry)[exclude_length-1] == '/') { + --exclude_length; + } + if ( strlen(next_file) == (unsigned)(exclude_length + 1) && + !strncmp(*exclude_entry, next_file, exclude_length)) { + // directory found in exclude list + no_watch = 1; + } + } + if (!no_watch) { + static int status; + status = inotifytools_watch_recursively_with_exclude( + next_file, + events, + exclude_list ); + // For some errors, we will continue. + if ( !status && (EACCES != error) && (ENOENT != error) && + (ELOOP != error) ) { + free( next_file ); + if ( my_path != path ) free( my_path ); + closedir( dir ); + return 0; + } + } // if !no_watch + free( next_file ); + } // if isdir and not islnk + else { + free( next_file ); + } + } + ent = readdir( dir ); + error = 0; + } + + closedir( dir ); + + int ret = inotifytools_watch_file( my_path, events ); + if ( my_path != path ) free( my_path ); + return ret; +} + +/** + * @internal + */ +void record_stats( struct inotify_event const * event ) { + if (!event) return; + watch *w = watch_from_wd(event->wd); + if (!w) return; + if ( IN_ACCESS & event->mask ) { + ++w->hit_access; + ++num_access; + } + if ( IN_MODIFY & event->mask ) { + ++w->hit_modify; + ++num_modify; + } + if ( IN_ATTRIB & event->mask ) { + ++w->hit_attrib; + ++num_attrib; + } + if ( IN_CLOSE_WRITE & event->mask ) { + ++w->hit_close_write; + ++num_close_write; + } + if ( IN_CLOSE_NOWRITE & event->mask ) { + ++w->hit_close_nowrite; + ++num_close_nowrite; + } + if ( IN_OPEN & event->mask ) { + ++w->hit_open; + ++num_open; + } + if ( IN_MOVED_FROM & event->mask ) { + ++w->hit_moved_from; + ++num_moved_from; + } + if ( IN_MOVED_TO & event->mask ) { + ++w->hit_moved_to; + ++num_moved_to; + } + if ( IN_CREATE & event->mask ) { + ++w->hit_create; + ++num_create; + } + if ( IN_DELETE & event->mask ) { + ++w->hit_delete; + ++num_delete; + } + if ( IN_DELETE_SELF & event->mask ) { + ++w->hit_delete_self; + ++num_delete_self; + } + if ( IN_UNMOUNT & event->mask ) { + ++w->hit_unmount; + ++num_unmount; + } + if ( IN_MOVE_SELF & event->mask ) { + ++w->hit_move_self; + ++num_move_self; + } + + ++w->hit_total; + ++num_total; + +} + +int *stat_ptr(watch *w, int event) +{ + if ( IN_ACCESS == event ) + return &w->hit_access; + if ( IN_MODIFY == event ) + return &w->hit_modify; + if ( IN_ATTRIB == event ) + return &w->hit_attrib; + if ( IN_CLOSE_WRITE == event ) + return &w->hit_close_write; + if ( IN_CLOSE_NOWRITE == event ) + return &w->hit_close_nowrite; + if ( IN_OPEN == event ) + return &w->hit_open; + if ( IN_MOVED_FROM == event ) + return &w->hit_moved_from; + if ( IN_MOVED_TO == event ) + return &w->hit_moved_to; + if ( IN_CREATE == event ) + return &w->hit_create; + if ( IN_DELETE == event ) + return &w->hit_delete; + if ( IN_DELETE_SELF == event ) + return &w->hit_delete_self; + if ( IN_UNMOUNT == event ) + return &w->hit_unmount; + if ( IN_MOVE_SELF == event ) + return &w->hit_move_self; + if ( 0 == event ) + return &w->hit_total; + return 0; +} + +/** + * Get statistics by a particular watch descriptor. + * + * inotifytools_initialize_stats() must be called before this function can + * be used. + * + * @param wd watch descriptor to get stats for. + * + * @param event a single inotify event to get statistics for, or 0 for event + * total. See section \ref events. + * + * @return the number of times the event specified by @a event has occurred on + * the watch descriptor specified by @a wd since stats collection was + * enabled, or -1 if @a event or @a wd are invalid. + */ +int inotifytools_get_stat_by_wd( int wd, int event ) { + if (!collect_stats) return -1; + + watch *w = watch_from_wd(wd); + if (!w) return -1; + int *i = stat_ptr(w, event); + if (!i) return -1; + return *i; +} + +/** + * Get statistics aggregated across all watches. + * + * inotifytools_initialize_stats() must be called before this function can + * be used. + * + * @param event a single inotify event to get statistics for, or 0 for event + * total. See section \ref events. + * + * @return the number of times the event specified by @a event has occurred over + * all watches since stats collection was enabled, or -1 if @a event + * is not a valid event. + */ +int inotifytools_get_stat_total( int event ) { + if (!collect_stats) return -1; + if ( IN_ACCESS == event ) + return num_access; + if ( IN_MODIFY == event ) + return num_modify; + if ( IN_ATTRIB == event ) + return num_attrib; + if ( IN_CLOSE_WRITE == event ) + return num_close_write; + if ( IN_CLOSE_NOWRITE == event ) + return num_close_nowrite; + if ( IN_OPEN == event ) + return num_open; + if ( IN_MOVED_FROM == event ) + return num_moved_from; + if ( IN_MOVED_TO == event ) + return num_moved_to; + if ( IN_CREATE == event ) + return num_create; + if ( IN_DELETE == event ) + return num_delete; + if ( IN_DELETE_SELF == event ) + return num_delete_self; + if ( IN_UNMOUNT == event ) + return num_unmount; + if ( IN_MOVE_SELF == event ) + return num_move_self; + + if ( 0 == event ) + return num_total; + + return -1; +} + +/** + * Get statistics by a particular filename. + * + * inotifytools_initialize_stats() must be called before this function can + * be used. + * + * @param filename name of file to get stats for. + * + * @param event a single inotify event to get statistics for, or 0 for event + * total. See section \ref events. + * + * @return the number of times the event specified by @a event has occurred on + * the file specified by @a filename since stats collection was + * enabled, or -1 if the file is not being watched or @a event is + * invalid. + * + * @note The filename specified must always be the original name used to + * establish the watch. + */ +int inotifytools_get_stat_by_filename( char const * filename, + int event ) { + return inotifytools_get_stat_by_wd( inotifytools_wd_from_filename( + filename ), event ); +} + +/** + * Get the last error which occurred. + * + * When a function fails, call this to find out why. The returned value is + * a typical @a errno value, the meaning of which depends on context. For + * example, if inotifytools_watch_file() fails because you attempt to watch + * a file which doesn't exist, this function will return @a ENOENT. + * + * @return an error code. + */ +int inotifytools_error() { + return error; +} + +/** + * @internal + */ +int isdir( char const * path ) { + static struct stat64 my_stat; + + if ( -1 == lstat64( path, &my_stat ) ) { + if (errno == ENOENT) return 0; + fprintf(stderr, "Stat failed on %s: %s\n", path, strerror(errno)); + return 0; + } + + return S_ISDIR( my_stat.st_mode ) && !S_ISLNK( my_stat.st_mode ); +} + + +/** + * Get the number of watches set up through libinotifytools. + * + * @return number of watches set up by inotifytools_watch_file(), + * inotifytools_watch_files() and inotifytools_watch_recursively(). + */ +int inotifytools_get_num_watches() { + int ret = 0; + rbwalk(tree_filename, get_num, (void*)&ret); + return ret; +} + +/** + * Print a string to standard out using an inotify_event and a printf-like + * syntax. + * The string written will only ever be up to 4096 characters in length. + * + * @param event the event to use to construct a string. + * + * @param fmt the format string used to construct a string. + * + * @return number of characters written, or -1 if an error occurs. + * + * @section syntax Format string syntax + * The following tokens will be replaced with the specified string: + * \li \c \%w - This will be replaced with the name of the Watched file on + * which an event occurred. + * \li \c \%f - When an event occurs within a directory, this will be replaced + * with the name of the File which caused the event to occur. + * Otherwise, this will be replaced with an empty string. + * \li \c \%e - Replaced with the Event(s) which occurred, comma-separated. + * \li \c \%Xe - Replaced with the Event(s) which occurred, separated by + * whichever character is in the place of `X'. + * \li \c \%T - Replaced by the current Time in the format specified by the + * string previously passed to inotifytools_set_printf_timefmt(), + * or replaced with an empty string if that function has never + * been called. + * + * @section example Example + * @code + * // suppose this is the only file watched. + * inotifytools_watch_file( "mydir/", IN_CLOSE ); + * + * // wait until an event occurs + * struct inotify_event * event = inotifytools_next_event( -1 ); + * + * inotifytools_printf(stderr, event, "in %w, file %f had event(s): %.e\n"); + * // suppose the file 'myfile' in mydir was read from and closed. Then, + * // this prints to standard out something like: + * // "in mydir/, file myfile had event(s): CLOSE_NOWRITE.CLOSE.ISDIR\n" + * @endcode + */ +int inotifytools_printf( struct inotify_event* event, char* fmt ) { + return inotifytools_fprintf( stdout, event, fmt ); +} + +/** + * Print a string to a file using an inotify_event and a printf-like syntax. + * The string written will only ever be up to 4096 characters in length. + * + * @param file file to print to + * + * @param event the event to use to construct a string. + * + * @param fmt the format string used to construct a string. + * + * @return number of characters written, or -1 if an error occurs. + * + * @section syntax Format string syntax + * The following tokens will be replaced with the specified string: + * \li \c \%w - This will be replaced with the name of the Watched file on + * which an event occurred. + * \li \c \%f - When an event occurs within a directory, this will be replaced + * with the name of the File which caused the event to occur. + * Otherwise, this will be replaced with an empty string. + * \li \c \%e - Replaced with the Event(s) which occurred, comma-separated. + * \li \c \%Xe - Replaced with the Event(s) which occurred, separated by + * whichever character is in the place of `X'. + * \li \c \%T - Replaced by the current Time in the format specified by the + * string previously passed to inotifytools_set_printf_timefmt(), + * or replaced with an empty string if that function has never + * been called. + * + * @section example Example + * @code + * // suppose this is the only file watched. + * inotifytools_watch_file( "mydir/", IN_CLOSE ); + * + * // wait until an event occurs + * struct inotify_event * event = inotifytools_next_event( -1 ); + * + * inotifytools_fprintf(stderr, event, "in %w, file %f had event(s): %.e\n"); + * // suppose the file 'myfile' in mydir was read from and closed. Then, + * // this prints to standard error something like: + * // "in mydir/, file myfile had event(s): CLOSE_NOWRITE.CLOSE.ISDIR\n" + * @endcode + */ +int inotifytools_fprintf( FILE* file, struct inotify_event* event, char* fmt ) { + static char out[MAX_STRLEN+1]; + static int ret; + ret = inotifytools_sprintf( out, event, fmt ); + if ( -1 != ret ) fprintf( file, "%s", out ); + return ret; +} + +/** + * Construct a string using an inotify_event and a printf-like syntax. + * The string can only ever be up to 4096 characters in length. + * + * This function will keep writing until it reaches 4096 characters. If your + * allocated array is not large enough to hold the entire string, your program + * may crash. + * inotifytools_snprintf() is safer and you should use it where possible. + * + * @param out location in which to store string. + * + * @param event the event to use to construct a string. + * + * @param fmt the format string used to construct a string. + * + * @return number of characters written, or -1 if an error occurs. + * + * @section syntax Format string syntax + * The following tokens will be replaced with the specified string: + * \li \c \%w - This will be replaced with the name of the Watched file on + * which an event occurred. + * \li \c \%f - When an event occurs within a directory, this will be replaced + * with the name of the File which caused the event to occur. + * Otherwise, this will be replaced with an empty string. + * \li \c \%e - Replaced with the Event(s) which occurred, comma-separated. + * \li \c \%Xe - Replaced with the Event(s) which occurred, separated by + * whichever character is in the place of `X'. + * \li \c \%T - Replaced by the current Time in the format specified by the + * string previously passed to inotifytools_set_printf_timefmt(), + * or replaced with an empty string if that function has never + * been called. + * + * @section example Example + * @code + * // suppose this is the only file watched. + * inotifytools_watch_file( "mydir/", IN_CLOSE ); + * + * // wait until an event occurs + * struct inotify_event * event = inotifytools_next_event( -1 ); + * + * char mystring[1024]; + * // hope this doesn't crash - if filename is really long, might not fit into + * // mystring! + * inotifytools_sprintf(mystring, event, "in %w, file %f had event(s): %.e\n"); + * printf( mystring ); + * // suppose the file 'myfile' in mydir was written to and closed. Then, + * // this prints something like: + * // "in mydir/, file myfile had event(s): CLOSE_WRITE.CLOSE.ISDIR\n" + * @endcode + */ +int inotifytools_sprintf( char * out, struct inotify_event* event, char* fmt ) { + return inotifytools_snprintf( out, MAX_STRLEN, event, fmt ); +} + + +/** + * Construct a string using an inotify_event and a printf-like syntax. + * The string can only ever be up to 4096 characters in length. + * + * @param out location in which to store string. + * + * @param size maximum amount of characters to write. + * + * @param event the event to use to construct a string. + * + * @param fmt the format string used to construct a string. + * + * @return number of characters written, or -1 if an error occurs. + * + * @section syntax Format string syntax + * The following tokens will be replaced with the specified string: + * \li \c \%w - This will be replaced with the name of the Watched file on + * which an event occurred. + * \li \c \%f - When an event occurs within a directory, this will be replaced + * with the name of the File which caused the event to occur. + * Otherwise, this will be replaced with an empty string. + * \li \c \%e - Replaced with the Event(s) which occurred, comma-separated. + * \li \c \%Xe - Replaced with the Event(s) which occurred, separated by + * whichever character is in the place of `X'. + * \li \c \%T - Replaced by the current Time in the format specified by the + * string previously passed to inotifytools_set_printf_timefmt(), + * or replaced with an empty string if that function has never + * been called. + * + * @section example Example + * @code + * // suppose this is the only file watched. + * inotifytools_watch_file( "mydir/", IN_CLOSE ); + * + * // wait until an event occurs + * struct inotify_event * event = inotifytools_next_event( -1 ); + * + * char mystring[1024]; + * inotifytools_snprintf( mystring, 1024, event, + * "in %w, file %f had event(s): %.e\n" ); + * printf( mystring ); + * // suppose the file 'myfile' in mydir was written to and closed. Then, + * // this prints something like: + * // "in mydir/, file myfile had event(s): CLOSE_WRITE.CLOSE.ISDIR\n" + * @endcode + */ +int inotifytools_snprintf( char * out, int size, + struct inotify_event* event, char* fmt ) { + static char * filename, * eventname, * eventstr; + static unsigned int i, ind; + static char ch1; + static char timestr[MAX_STRLEN]; + static time_t now; + + + if ( event->len > 0 ) { + eventname = event->name; + } + else { + eventname = NULL; + } + + + filename = inotifytools_filename_from_wd( event->wd ); + + if ( !fmt || 0 == strlen(fmt) ) { + error = EINVAL; + return -1; + } + if ( strlen(fmt) > MAX_STRLEN || size > MAX_STRLEN) { + error = EMSGSIZE; + return -1; + } + + ind = 0; + for ( i = 0; i < strlen(fmt) && + (int)ind < size - 1; ++i ) { + if ( fmt[i] != '%' ) { + out[ind++] = fmt[i]; + continue; + } + + if ( i == strlen(fmt) - 1 ) { + // last character is %, invalid + error = EINVAL; + return ind; + } + + ch1 = fmt[i+1]; + + if ( ch1 == '%' ) { + out[ind++] = '%'; + ++i; + continue; + } + + if ( ch1 == 'w' ) { + if ( filename ) { + strncpy( &out[ind], filename, size - ind ); + ind += strlen(filename); + } + ++i; + continue; + } + + if ( ch1 == 'f' ) { + if ( eventname ) { + strncpy( &out[ind], eventname, size - ind ); + ind += strlen(eventname); + } + ++i; + continue; + } + + if ( ch1 == 'e' ) { + eventstr = inotifytools_event_to_str( event->mask ); + strncpy( &out[ind], eventstr, size - ind ); + ind += strlen(eventstr); + ++i; + continue; + } + + if ( ch1 == 'T' ) { + + if ( timefmt ) { + + now = time(0); + if ( 0 >= strftime( timestr, MAX_STRLEN-1, timefmt, + localtime( &now ) ) ) { + + // time format probably invalid + error = EINVAL; + return ind; + } + } + else { + timestr[0] = 0; + } + + strncpy( &out[ind], timestr, size - ind ); + ind += strlen(timestr); + ++i; + continue; + } + + // Check if next char in fmt is e + if ( i < strlen(fmt) - 2 && fmt[i+2] == 'e' ) { + eventstr = inotifytools_event_to_str_sep( event->mask, ch1 ); + strncpy( &out[ind], eventstr, size - ind ); + ind += strlen(eventstr); + i += 2; + continue; + } + + // OK, this wasn't a special format character, just output it as normal + if ( ind < MAX_STRLEN ) out[ind++] = '%'; + if ( ind < MAX_STRLEN ) out[ind++] = ch1; + ++i; + } + out[ind] = 0; + + return ind - 1; +} + +/** + * Set time format for printf functions. + * + * @param fmt A format string valid for use with strftime, or NULL. If NULL, + * time substitutions will no longer be made in printf functions. + * Note that this format string is not validated at all; using an + * incorrect format string will cause the printf functions to give + * incorrect results. + */ +void inotifytools_set_printf_timefmt( char * fmt ) { + timefmt = fmt; +} + +/** + * Get the event queue size. + * + * This setting can also be read or modified by accessing the file + * \a /proc/sys/fs/inotify/max_queued_events. + * + * @return the maximum number of events which will be queued in the kernel. + */ +int inotifytools_get_max_queued_events() { + int ret; + if ( !read_num_from_file( QUEUE_SIZE_PATH, &ret ) ) return -1; + return ret; +} + +/** + * Get the maximum number of user instances of inotify. + * + * This setting can also be read or modified by accessing the file + * \a /proc/sys/fs/inotify/max_user_instances. + * + * @return the maximum number of inotify file descriptors a single user can + * obtain. + */ +int inotifytools_get_max_user_instances() { + int ret; + if ( !read_num_from_file( INSTANCES_PATH, &ret ) ) return -1; + return ret; +} + +/** + * Get the maximum number of user watches. + * + * This setting can also be read or modified by accessing the file + * \a /proc/sys/fs/inotify/max_user_watches. + * + * @return the maximum number of inotify watches a single user can obtain per + * inotify instance. + */ +int inotifytools_get_max_user_watches() { + int ret; + if ( !read_num_from_file( WATCHES_SIZE_PATH, &ret ) ) return -1; + return ret; +} + +/** + * Ignore inotify events matching a particular regular expression. + * + * @a pattern is a regular expression and @a flags is a bitwise combination of + * POSIX regular expression flags. + * + * On future calls to inotifytools_next_events() or inotifytools_next_event(), + * the regular expression is executed on the filename of files on which + * events occur. If the regular expression matches, the matched event will be + * ignored. + */ +int inotifytools_ignore_events_by_regex( char const *pattern, int flags ) { + if (!pattern) { + if (regex) { + regfree(regex); + free(regex); + regex = 0; + } + return 1; + } + + if (regex) { regfree(regex); } + else { regex = (regex_t *)malloc(sizeof(regex_t)); } + + int ret = regcomp(regex, pattern, flags | REG_NOSUB); + if (0 == ret) return 1; + + regfree(regex); + free(regex); + regex = 0; + error = EINVAL; + return 0; +} + +int event_compare(const void *p1, const void *p2, const void *config) +{ + if (!p1 || !p2) return p1 - p2; + char asc = 1; + int sort_event = (int)config; + if (sort_event == -1) { + sort_event = 0; + asc = 0; + } else if (sort_event < 0) { + sort_event = -sort_event; + asc = 0; + } + int *i1 = stat_ptr((watch*)p1, sort_event); + int *i2 = stat_ptr((watch*)p2, sort_event); + if (0 == *i1 - *i2) { + return ((watch*)p1)->wd - ((watch*)p2)->wd; + } + if (asc) + return *i1 - *i2; + else + return *i2 - *i1; +} + +struct rbtree *inotifytools_wd_sorted_by_event(int sort_event) +{ + struct rbtree *ret = rbinit(event_compare, (void*)sort_event); + RBLIST *all = rbopenlist(tree_wd); + void const *p = rbreadlist(all); + while (p) { + void const *r = rbsearch(p, ret); + niceassert((int)(r == p), "Couldn't insert watch into new tree"); + p = rbreadlist(all); + } + rbcloselist(all); + return ret; +} |