scripts/inotify/inotify.c (152 lines of code) (raw):

// Copyright 2022 Google LLC #include <errno.h> #include <limits.h> #include <poll.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/inotify.h> #include <sys/types.h> #include <sys/wait.h> #include <time.h> #include <unistd.h> extern char **environ; // poll() timeout for inotify events, in milliseconds; // run callback after this delay since the last run, even if there're no events. const int kPollTimeout = 2000; char buf[1024 * (sizeof(struct inotify_event) + NAME_MAX + 1)]; void run_callback_internal(char** callback) { fflush(stdout); // avoid mixing with callback output. pid_t pid = fork(); if (pid == -1) { perror("fork"); return; // let it retry at the next event or timeout. } if (pid == 0) { execve(callback[0], callback, environ); perror("execve"); exit(EXIT_FAILURE); } int status; do { int w = waitpid(pid, &status, 0); if (w == -1) { if (errno == EINTR) { continue; } perror("waitpid"); exit(EXIT_FAILURE); // shouldn't happen; don't risk leaking zombies. } } while (!WIFEXITED(status) && !WIFSIGNALED(status)); if (WIFEXITED(status)) { printf("inotify: %s exited with status %d (exit: %d)\n", callback[0], status, WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("inotify: %s exited with status %d (signal: %d)\n", callback[0], status, WTERMSIG(status)); } else { printf("inotify: %s exited with status %d\n", callback[0], status); } if (WIFEXITED(status) && WEXITSTATUS(status) == EXIT_SUCCESS) { exit(EXIT_SUCCESS); } } // run_callback runs the callback if always is true, or it has been kPollTimeout // milliseconds since 'last', which is updated after callback runs. void run_callback(char** callback, struct timespec *last, bool always) { if (!always) { struct timespec tp; int ret = clock_gettime(CLOCK_MONOTONIC, &tp); if (ret != 0) { perror("clock_gettime"); exit(EXIT_FAILURE); } int diff_milliseconds = (tp.tv_sec - last->tv_sec) * 1000 \ + (tp.tv_nsec - last->tv_nsec) / 1000000; if (diff_milliseconds < kPollTimeout) { return; } printf("inotify: calling %s after %dms since the last run\n", callback[0], diff_milliseconds); } run_callback_internal(callback); int ret = clock_gettime(CLOCK_MONOTONIC, last); if (ret != 0) { perror("clock_gettime"); exit(EXIT_FAILURE); } } int main(int argc, char* argv[]) { if (argc < 4) { printf("Usage: %s path file callback [callback-arg]...\n", argv[0]); printf("This utility watches inotify events at 'path', " "optionally filtered by file name 'file',\n" "and calls 'callback' with 'callback-arg' " "upon inotify events firing. Additionally,\n" "'callback' is also called at start up or " "every %d milliseconds if no inotify event fires.\n" "This program exits as success when 'callback' " "exits as success, or keeps running otherwise.\n", kPollTimeout); exit(EXIT_FAILURE); } char** callback = argv + 3; struct pollfd fds; fds.fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); if (fds.fd == -1) { perror("inotify_init1"); exit(EXIT_FAILURE); } fds.events = POLLIN; // flags taken from Golang fsnotify. uint32_t flags = IN_MOVED_TO | IN_MOVED_FROM | IN_CREATE | IN_ATTRIB | IN_MODIFY | IN_MOVE_SELF | IN_DELETE | IN_DELETE_SELF; int wd = inotify_add_watch(fds.fd, argv[1], flags); if (wd == -1) { perror("inotify_add_watch"); exit(EXIT_FAILURE); } struct timespec tp; printf("inotify: calling %s as initial run\n", callback[0]); run_callback(callback, &tp, true); for (;;) { // inotify(7) states several limitations and polling the filesystem (or // calling the callback in our case) in addition to inotify is required. // We utilize the 'timeout' parameter to poll(2) to achieve it by running // the callback at each time out event. int ret = poll(&fds, 1, kPollTimeout); if (ret == -1) { if (errno == EINTR) { run_callback(callback, &tp, false); continue; } perror("poll"); exit(EXIT_FAILURE); } if (ret == 0) { run_callback(callback, &tp, false); continue; } int matches = 0; for (;;) { ssize_t len = read(fds.fd, buf, sizeof(buf)); if (len == -1) { if (errno == EINTR) { continue; } if (errno == EAGAIN) { break; } perror("read"); exit(EXIT_FAILURE); } const struct inotify_event *event; for (char *ptr = buf; ptr < buf + len; ptr += sizeof(struct inotify_event) + event->len) { event = (const struct inotify_event *)ptr; if (argv[2][0] == '\0' || (event->len > 0 && strcmp(event->name, argv[2]) == 0)) { matches++; } } } if (matches == 0) { run_callback(callback, &tp, false); continue; } printf("inotify: calling %s for %d matching event(s)\n", callback[0], matches); run_callback(callback, &tp, true); } }