DEV Community

BC
BC

Posted on

Use inotify API to monitor file change

It is not uncommon to write a daemon program and let the daemon process to watch its configuration file, if the file changed, then daemon will do a restart or reload.

To watch if file has changed, an easy solution is setting a timer to poll the config file info, like its atime, ctime and mtime, and file content's hash value, if there is a change b/t this time and next time polling, then we know there is a change happened.

Another solution is using Linux's inotify API (3 APIs and 1 structure are related to this):

3 APIs:

  • inotify_init
  • inotify_add_watch
  • inotify_rm_watch

1 structure:

  • struct inotify_event

So basically we need to init the inotify system (inotify_init), then add the file we want to watch to this system (inotify_add_watch). When adding this file we can specify what kind event to watch, if we want to watch all events, use IN_ALL_EVENTS, then we call read on the "inotify" fd, then function will block until the specified events happened.

Example code:

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


#define BUF_LEN (10 * (sizeof(struct inotify_event) + NAME_MAX + 1))


int main() {
    char buf[BUF_LEN];
    int inotify_fd = 0;
    struct inotify_event *event = NULL;
    char* pathname = "config.ini";

    inotify_fd = inotify_init();
    inotify_add_watch(inotify_fd, pathname, IN_ALL_EVENTS);
    while (1) {
        int n = read(inotify_fd, buf, BUF_LEN);
        char* p = buf;
        while (p < buf + n) {
            event = (struct inotify_event*)p;
            uint32_t mask = event->mask;
            if (mask & IN_ACCESS) {
                printf("File has been accessed\n");
            }
            if (mask & IN_ATTRIB) {
                printf("File meta data changed\n");
            }
            if (mask & IN_CLOSE_WRITE) {
                printf("File closed after write\n");
            }
            if (mask & IN_CLOSE_NOWRITE) {
                printf("File closed after read\n");
            }
            if (mask & IN_DELETE_SELF) {
                printf("File is deleted\n");
            }
            if (mask & IN_MODIFY) {
                printf("File has been modified\n");
            }
            if (mask & IN_MOVE_SELF) {
                printf("File has been moved\n");
            }
            if (mask & IN_OPEN) {
                printf("File has been opened\n");
            }
            if (mask & IN_IGNORED) {
                printf("File monitor has been removed\n");
            }
            p += sizeof(struct inotify_event) + event->len;
        }
    }
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Compile and run it:

$ gcc main.c -o iwatch
$ ./iwatch
Enter fullscreen mode Exit fullscreen mode

Now open another terminal, and change the file to see if our "iwatch" works:

When touch config.ini, "iwatch" printed:

File has been opened
File meta data changed
File closed after write
Enter fullscreen mode Exit fullscreen mode

When cat config.ini, "iwatch" printed:

File has been opened
File has been accessed
File closed after read
Enter fullscreen mode Exit fullscreen mode

When use nano to edit config.ini and close it, "iwatch" printed:

File has been opened
File has been accessed
File closed after read
File has been modified
File has been opened
File has been modified
File closed after write
Enter fullscreen mode Exit fullscreen mode

When cp config2.ini config.ini, "iwatch" printed:

File has been modified
File has been opened
File closed after write
Enter fullscreen mode Exit fullscreen mode

So far so good, but when vim config.ini, "iwatch" printed:

File has been opened
File closed after read
File has been opened
File has been accessed
File closed after read
File has been moved
Enter fullscreen mode Exit fullscreen mode

Weird there is no "File closed after write", and even worse, after we vim config.ini, if we touch config.ini, no further events got printed.

The reason is, when vim edits a file, it will open the file and create a temporary file, and write the content to that temporary file, after editing done, it will move the temporary file to the actual file. That's why we can see the message "File has been moved" in the end.

Because vim created a new temporary file to do the editing, the temporary file has its own inode number, and because inotify is inode-based API, after move the temporary file to the actual file, our config.ini file's inode has been changed, it is not the one we are watching anymore, that's why after vim, if we do touch config.ini, there are no more message printed out.

But remember we used nano to edit the file before, and it works as expected, that's because nano does the editing in a different way, it will open the actual file and overwrite its content, so our config.ini file's inode number stays unchange:

# get the inode number
$ ls -i config.ini 
1574489 config.ini
$ nano config.ini
$ ls -i config.ini
1574489 config.ini
# we can see after nano, inode number stays unchange
$
# now let's use vim
$ vim config.ini
$ ls -i config.ini
1574501 config.ini
# the file's inode number changed.
Enter fullscreen mode Exit fullscreen mode

Conclusion: if your program just need to monitor file change on "touch", "nano", "cp", you can use the inotify API to monitor that file, but "vim" won't trigger a file content change, and you will lose the future events since the inode number has been changed. An alternative way is to monitor file's folder instead of the file itself, then all editing will fire events. But as a fallback and probably easier solution, also because inotify API is only available in Linux, maybe just use the polling method is enough for your program.

Reference

Top comments (0)