Monitor file changes with inotify API

Introduction

Most of us have used tools like hot reload in our development journey where the app listens for file changes and reloads itself. An example of these tools is nodemon which monitors file changes in the directory and reloads the server automatically.

I was always wondering about how this feature is implemented and I was sure that it must be provided by the underlying operating system somehow until I found this cool API provided by the Linux operating system called inotify API.

Let's take a look into this API and see the cool features it provides.

What's inotify API

If we take a look at the manual page of it we find the following:

man inotify

Screenshot from 2022-10-10 11-22-54.png

So inotify allows an application to monitor filesystem events from the directory changes (creating or deleting files) to file changes (modifying contents for example)

Mainly inotify API provides three functions:

  • inotify_init: initializes a new inotify instance.
  • inotify_add_watch: adds a new watch, or modifies an existing watch, for the file whose location is specified in the pathname (provided as an argument to this function ).
  • inotify_rm_watch: which removes an existing watch from an inotify instance.

When events occur for monitored files and directories, those events are made available to the application as structured data that can be read using the read function for example

Example

To demonstrate the use of this API we are going to write a simple application which listens for directory and file changes, here's the code for it

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

#define MAX_EVENTS 1024 /*Max. number of events to process at one go*/
#define LEN_NAME 1024 /*Assuming length of the filename won't exceed 16 bytes*/
#define EVENT_SIZE  ( sizeof (struct inotify_event) ) /*size of one event*/
#define BUF_LEN     ( MAX_EVENTS * ( EVENT_SIZE + LEN_NAME )) /*buffer to store the data of events*/

void listen (int fd) {
    char buffer[BUF_LEN];
    int length, i = 0;

    // 4. 
    length = read( fd, buffer, BUF_LEN );  
    if ( length < 0 ) {
        perror( "read" );
    }  

    while ( i < length ) {
        // 5.
        struct inotify_event *event = ( struct inotify_event * ) &buffer[ i ];
        if ( event->len ) {
            if ( event->mask & IN_CREATE) {
                if (event->mask & IN_ISDIR)
                    printf( "The directory %s was Created.\n", event->name );       
                else
                    printf( "The file %s was Created with WD %d\n", event->name, event->wd );       
            }

            if ( event->mask & IN_MODIFY) {
                if (event->mask & IN_ISDIR)
                    printf( "The directory %s was modified.\n", event->name );       
                else
                    printf( "The file %s was modified with WD %d\n", event->name, event->wd );       
            }

            if ( event->mask & IN_DELETE) {
                if (event->mask & IN_ISDIR)
                    printf( "The directory %s was deleted.\n", event->name );       
                else
                    printf( "The file %s was deleted with WD %d\n", event->name, event->wd );       
            }  
            i += EVENT_SIZE + event->len;
        }
    }
}

int main( int argc, char **argv ) {
    int watch_dec, file_desc;

    // 1. 
    fd = inotify_init();
    if ( file_desc < 0 ) {
        perror( "Couldn't initialize inotify");
    }

     // 2.   
    wd = inotify_add_watch(file_desc, argv[1], IN_CREATE | IN_MODIFY | IN_DELETE); 
    if (watch_dec == -1) {
        printf("Couldn't add watch to %s\n",argv[1]);
    } else {
        printf("Watching %s changes ... \n",argv[1]);
    }

   // 3
    while(1) {
        listen(fd); 
    } 

    /* Free up resources*/
    // 6.
    inotify_rm_watch( fd, wd );
    close( fd );

    return 0;
}

Let's go through the code an explain it

  • 1) First we initialize the inotify instance which returns a file descriptor associated with a new inotify event queue.
  • 2) Second, add a new watch for the directory specified as input to the application. Here we can specify some flags to listen for specific file change types (only deletion for example).
  • 3) Infinitely listen for file change events from the OS.
  • 4) Read the event from the event's file into the application buffer
  • 5) Create an event object based on the type of the event and perform actions.
  • 6) Free up kernel resources Now let's compile and run the application:
gcc -o program program.c

Here's a demo of the program execution asciicast