A basic inotify utility for Linux 2.6.13+

a.k.a. Finding out when a file is accessed

I am using a rather daunting http server called Apache and have some alias directives set up. When I try to access these paths, however, I am confronted with a rude 4xx error.

If this was a smaller, more reasonable program, strace would come to mind. But the amount of data that apache would spew I'm sure could be used for a good laugh at hackathons, even with a context sensitive grep (-A and -B, look into it).

lsof loses here too; it doesn't know about file access history.

gdb may be a winner if I want to attach to the process, break on open, and then go from there. But I'm not a masochist like that, I swear.

inotify

(It's not a calendar app from Apple)

You've probably never heard of inotify unless you follow kernel news or are a +20 computer wizard that casts prosaic spells with every meta-command sequence.

However, if you are a mere mortal and have not heard of it yet, then check this:

inotify will tell you when a file has been accessed, modified, deleted etc.

It is so close to awesome, but unfortunately, it's just a kernel routine. This means you need to write program in order to use it.

I was hoping that a command line wrapper would exist, similar to how there is a command line version of the stat system call.

Psst! I actually found an extensive tool suite while researching this article. But hopefully, my simpleton implementation will be useful to more then just me. :-p

The Plan

(Always spec before you code, for the love of bob)

My usage requirement is to be able to run inotify with a file to watch, like so:

$ inotify /file/of/deep/interest

Then, if in another terminal, I do the following:

$ cat /file/of/deep/interest

I can switch back to my inotify window and see stuff on the screen. Hopefully, I will even be notified of what type of access occured.

"Pshh! This can't be to hard", I scoff with a grin of ignorant confidence.

The Code

Linux Journal did a nice writeup a while back on inotify. Since my requirements were simple, I just used a blocked read call, as described early on in the article.

I used much of the code from the article as my base. I've commented the purpose of my code below.

You can get the source code, with makefile here.

#include <sys/inotify.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

/* size of the event structure, not counting name */
#define EVENT_SIZE  (sizeof (struct inotify_event))

/* reasonable guess as to size of 1024 events */
#define BUF_LEN        (1024 * (EVENT_SIZE + 16))

char buf[BUF_LEN];

// We listen for all the events and do so in a
// one shot way.  After we are done we can just
// reregister.
#define ARGS IN_ONESHOT | IN_ALL_EVENTS

// We create a map with the name of the bits
// mapped to the bitfields as specified in the 
// inotify man page.
//
// If you haven't seen anything like this, it will
// allow us to iterate through the array and test each
// bit.
//
// If it's successful, then we print out the desc. The
// code that does this is below.
struct {
    int bit;
    char *desc;
} map[] = {
    { IN_ACCESS, "IN_ACCESS" },
    { IN_ATTRIB, "IN_ATTRIB" },
    { IN_CLOSE_WRITE, "IN_CLOSE_WRITE" },
    { IN_CLOSE_NOWRITE, "IN_CLOSE_NOWRITE" },
    { IN_CREATE, "IN_CREATE" },
    { IN_DELETE, "IN_DELETE" },
    { IN_DELETE_SELF, "IN_DELETE_SELF" },
    { IN_MODIFY, "IN_MODIFY" },
    { IN_MOVE_SELF, "IN_MOVE_SELF" },
    { IN_MOVED_FROM, "IN_MOVED_FROM" },
    { IN_MOVED_TO, "IN_MOVED_TO" },
    { IN_OPEN, "IN_OPEN" }
};
// Since the control_c function is called on a signal,
// the easiest way to make the file descriptors accessible
// to the control_c function is to make them global.
//
// According to the man page of inotify_rm_watch, the watch
// descriptor should be a uint32_t, so it has been made so below.
int g_fd;

uint32_t g_wd;

void control_c(int ignored) {
    printf("Removing watch\n");
    inotify_rm_watch(g_fd, g_wd);
    _exit (0);
}

int main(int argc, char*argv[]){
    int ret,
        len,
        ix,
        i,
        mapLen = sizeof(map) / (sizeof(int) + sizeof(char*));

    // Get the command line argument as the file to watch 
    argv++;
    if(*argv == 0) {
        printf ("No file name specified\n");
        return -1;
    }

    // Since we block on a read, we need some other way of cleaning up
    // our watch when the user wants to exit.
    //
    // I usually just do control+c to exit my program, so we listen for 
    // that signal and we deregister the watch there.
    signal(SIGINT, control_c);

    g_fd = inotify_init();
    if (g_fd < 0) {
            perror ("inotify_init");
    }

    g_wd = inotify_add_watch(g_fd,
        *argv,
        ARGS);

    if (g_wd < 0) {
            perror ("inotify_add_watch");
    }

    while(1) {
        len = read (g_fd, buf, BUF_LEN);

        if (len > 0) {
            i = 0;

            while (i < len) {
                struct inotify_event *event;

                event = (struct inotify_event *) &buf[i];

                // Output type of access by using the map above
                for (ix = 0; ix < mapLen; ix++) {
                    if(event->mask & map[ix].bit) {
                        printf("%s ", map[ix].desc);
                    }
                }

                if (event->len) {
                    printf ("name=%s", event->name);
                }

                i += EVENT_SIZE + event->len;
            }

	    printf("\n");
            fflush(0);
            // Saving files at least in vim seems to temporarily remove
            // the file completely, and then write a new file with the
            // same name.
            // 
            // This means that there is some blackout period where the
            // file actually does not exist.  This little sleep here
            // is a naive way of avoiding this problem.  However, it
	    // appears to work well.
            usleep(50000);

	    // Since are ARGS specifies ONE_SHOT, we need to reregister
	    // the watch each time.  This is fine.
            g_wd = inotify_add_watch(g_fd,
                *argv,
                ARGS);

            if (g_wd < 0) {
                perror ("inotify_add_watch");
            }
        }
    }
}