Use inotify API to monitor file change
Tags: linux
Published: 2020-03-08 23:39:21

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:

```c
#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;
}
```

Compile and run it:
```bash
$ gcc main.c -o iwatch
$ ./iwatch
```

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
```

When `cat config.ini`, "iwatch" printed:
```
File has been opened
File has been accessed
File closed after read
```

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
```

When `cp config2.ini config.ini`, "iwatch" printed:
```
File has been modified
File has been opened
File closed after write
```

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
```

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:

```bash
# 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.
```

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
* http://man7.org/linux/man-pages/man7/inotify.7.html
* https://unix.stackexchange.com/questions/188873/using-inotifywait-along-with-vim