/* * icron: recursively monitor a dir and run a program when changes * are detected -- good rsync mirror replacement * * Written by Mike Frysinger * * Released into the Public Domain */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #define ICRON_WATCHES (IN_DONT_FOLLOW | IN_ONLYDIR | IN_CLOSE_WRITE | IN_CREATE | IN_MOVED_TO) /* Older headers/kernels don't have this function */ #ifndef IN_CLOEXEC static int inotify_init1(int flags) { int ret = inotify_init(); fcntl(ret, F_SETFD, fcntl(ret, F_GETFD) | FD_CLOEXEC); return ret; } # define IN_CLOEXEC 0 #endif #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(*arr)) #define PROG "icron" #ifndef LOGFILE # define LOGFILE "/var/log/icron.log" #endif static FILE *logfp; static const char *log_ts(void) { time_t t = time(0); static char buf[64]; ctime_r(&t, buf); buf[strlen(buf) - 1] = '\0'; return buf; } #define log(fmt, args...) do { fprintf(logfp, "[%s] " fmt "\n", log_ts(), ## args); fflush(logfp); } while (0) #define dlog(fmt, args...) log("%s:%i: " fmt, __func__, __LINE__, ## args) #define warnp(fmt, args...) log(fmt ": %s", ## args, strerror(errno)) #ifdef DEBUG #define quit() do { exit(1); } while (0) #else #define quit() do { system("tail " LOGFILE " | mail -s 'icron fell over' root"); exit(1); } while (0) #endif #define err(fmt, args...) do { log("ERROR: " fmt, ## args); quit(); } while (0) #define errp(fmt, args...) do { warnp("ERROR: " fmt, ## args); quit(); } while (0) typedef struct { const char *path; int wd; } watch_t; static watch_t *watches; static int total_watches, curr_watches, max_watches; static void check_max_watch(void) { static FILE *fp; if (total_watches < max_watches) return; if (!fp) { fp = fopen("/proc/sys/fs/inotify/max_user_watches", "w+"); if (fp) check_max_watch(); log("max_user_watches starting at %i", max_watches); return; } rewind(fp); if (fscanf(fp, "%i", &max_watches) != 1) return; /* In case some other instance updated the max */ if (total_watches < max_watches) return; printf("%i\n", max_watches); max_watches *= 2; log("increased max watches to %i", max_watches); fprintf(fp, "%i", max_watches); } static void queue_watch(int wd, const char *path) { int curr_watch = curr_watches++; if (total_watches <= curr_watches) { if (!total_watches) total_watches = 8192; else total_watches *= 2; watches = realloc(watches, sizeof(*watches) * total_watches); if (!watches) errp("realloc(%lu * %i = %lu)", sizeof(*watches), total_watches, sizeof(*watches) * total_watches); check_max_watch(); } watch_t *w = &watches[curr_watch]; w->wd = wd; w->path = strdup(path); } static const watch_t *lookup_watch(int wd) { int c; for (c = 0; c < curr_watches; ++c) if (watches[c].wd == wd) return &watches[c]; return NULL; } static char *lookup_event_watch_path(struct inotify_event *e) { const watch_t *w = lookup_watch(e->wd); char *path; asprintf(&path, "%s/%s", w->path, e->name); return path; } bool started = false; static void watch_it(int ifd, const char *path) { int wd = inotify_add_watch(ifd, path, ICRON_WATCHES); if (wd == -1) errp("inotify_add_watch(%s)", path); queue_watch(wd, path); if (started) log("now watching %s", path); } static void run_proc(char *path, char *prog) { /* Small hack to coalesce duplicate events */ static time_t last_time; static char *last_path; time_t now = time(NULL); if (now - last_time < 5 && last_path && !strcmp(path, last_path)) { last_time = now; log("ignoring dupe event for %s", path); return; } else { free(last_path); last_path = strdup(path); last_time = now; } /* Event is valid for a file, so run prog */ pid_t child = fork(); if (child == -1) errp("fork()"); else if (child) { log("pid %i running `%s '%s'`", child, prog, path); return; } char *child_argv[] = { prog, path, NULL }; signal(SIGCHLD, SIG_DFL); execvp(child_argv[0], child_argv); errp("execvp(%s)", child_argv[0]); } static void watch_dir(int ifd, const char *path, char *prog) { char *p, sub_path[8192]; if (!path) path = "."; strcpy(sub_path, path); p = sub_path + strlen(sub_path); *p++ = '/'; if (ifd < 0) { ifd = -ifd; watch_it(ifd, path); } DIR *dfd = opendir(path); if (!dfd) return; /* * We walk the tree twice to avoid race conditions: * http://mail.gnome.org/archives/dashboard-hackers/2004-October/msg00022.html */ struct dirent *de; while ((de = readdir(dfd))) { if (de->d_name[0] == '.') continue; if (de->d_type != DT_DIR) continue; strcpy(p, de->d_name); watch_it(ifd, sub_path); } rewinddir(dfd); while ((de = readdir(dfd))) { if (de->d_name[0] == '.') continue; if (de->d_type != DT_DIR) { if (prog) { strcpy(p, de->d_name); run_proc(sub_path, prog); } continue; } strcpy(p, de->d_name); watch_dir(ifd, sub_path, prog); } closedir(dfd); } #define elen(e) (sizeof(*e) + e->len) static void handle_event(int ifd, struct inotify_event *e, char *prog) { char *path = lookup_event_watch_path(e); log("processing %s (name:{%s} len:%zu wd:%i mask:%#x cookie:%#x)", path, e->len ? e->name : "(null)", elen(e), e->wd, e->mask, e->cookie); if (!e->len) { log("skipping event that lacks filename (delete?)"); goto done; } if (e->mask & IN_CREATE) { struct stat st; if (stat(path, &st) == 0) { if (S_ISDIR(st.st_mode)) { log("watching new dir %s", path); watch_it(ifd, path); watch_dir(ifd, path, prog); } else log("skipping file creation event"); } else warnp("file disappeared! stat(%s)", path); /* a created file will get a modify event */ goto done; } run_proc(path, prog); done: free(path); } static char **icron_argv; void reload(int sig) { exit(execvp(icron_argv[0], icron_argv)); } int main(int argc, char **argv) { char *prog = argv[argc - 1]; logfp = stderr; if (argc < 3) { puts("Usage: " PROG " "); return 1; } int ifd = inotify_init1(IN_CLOEXEC); if (ifd == -1) errp("inotify_init()"); int i; for (i = 1; i < argc - 1; ++i) watch_dir(-ifd, argv[i], NULL); #ifdef DEBUG if (daemon(0, 1)) #else logfp = fopen(LOGFILE, "ae"); if (daemon(0, 0)) #endif errp("daemon()"); /* don't care about the forked children */ signal(SIGCHLD, SIG_IGN); /* re-exec ourselves */ icron_argv = argv; signal(SIGHUP, reload); started = true; log("watching dir \"%s\" (with %i watches) via program \"%s\"", argv[1], curr_watches, prog); for (i = 2; i < argc - 1; ++i) log("\tadditional dirs: \"%s\"", argv[i]); while (1) { ssize_t len, i; char buf[32768]; len = read(ifd, buf, sizeof(buf)); if (len == -1) { /* things like ptrace() trigger this */ if (errno == EINTR) continue; errp("read()"); } log("read() returned %zi", len); i = 0; while (i < len) { struct inotify_event *e, *en; e = (void *)&buf[i]; i += elen(e); en = (void *)&buf[i]; if (e->wd == -1) { log("queue overflowed!"); break; } handle_event(ifd, e, prog); } } return 0; }