#include "filewatcher.h" #include "filewatch.h" #include "filewatch_context.h" #include "filewatch_type.h" #include "common.h" #include "logger.h" #include "types.h" #include "vector.h" #include #include #include #include #include #include #include #include #include vector_def(FileEvent, FileEvent); /** * All context and state required to run a file watcher. */ typedef struct FileWatcher { thrd_t thread; mtx_t mutex; cnd_t cond; int running; FileWatcherContext context; VectorFileEvent events; } FileWatcher; /* * Watch Params */ static inline WatchParams watch_params_create(const char *path, u32 mask) { return (WatchParams){.path = strdup(path), .mask = mask}; } static inline void watch_params_destroy(WatchParams *params) { free((char *)params->path); } static WatchParams *watch_params_find_by_path(const char *path, FileWatcherContext *ctx, size_t *index) { const VectorWatchParams *params = &ctx->params; for (size_t i = 0; i < vector_size(params); ++i) { WatchParams *param = &vector_at(params, i); if (strcmp(path, param->path) == 0) { if (index) *index = i; return param; } } return NULL; } /* * File Events */ #define NULL_EVENT (FileEvent){0}; static FileEvent file_event_create(const struct inotify_event *inevent, FileWatcherContext *ctx) { FileEvent event = {0}; if (!inevent) { return event; } const FileWatch *fw = file_watch_find_by_wd(ctx, inevent->wd, NULL); if (has_mask(inevent->mask, IN_IGNORED)) { sc_log_debug("IN_IGNORED received on %s\n", fw->path); return NULL_EVENT; } if (inevent->len) { #define BUFSIZE 1024 char filename[BUFSIZE]; size_t parent_name_size = strlen(fw->path); size_t event_path_size = strlen(inevent->name); size_t fullpath_size = parent_name_size + event_path_size; if (fullpath_size > BUFSIZE - 1) { sc_log_error("Pathname length over %u, please compile with smaller " "paths. Event discarded for %s\n.", BUFSIZE - 1, inevent->name); return NULL_EVENT; } #undef BUFSIZE strncpy(filename, fw->path, parent_name_size); strncpy(filename + parent_name_size, inevent->name, event_path_size); filename[fullpath_size] = '\0'; // find if file is associated with any path for (size_t i = 0; i < vector_size(&fw->files); ++i) { const char *watched_file = vector_at(&fw->files, i); if (strcmp(watched_file, filename) == 0) { // check if event mask matches the watcher mask WatchParams *params = watch_params_find_by_path(watched_file, ctx, NULL); if (!params) { return NULL_EVENT; } if (has_mask(inevent->mask, IN_MODIFY)) { event.type = HI_FILE_MODIFY; } else if (has_mask(inevent->mask, IN_CREATE)) { event.type = HI_FILE_CREATE; } else if (has_mask(inevent->mask, IN_DELETE)) { event.type = HI_FILE_DELETE; } else if (has_mask(inevent->mask, IN_DELETE_SELF)) { event.type = HI_FILE_DELETE; } else if (has_mask(inevent->mask, IN_MOVE)) { event.type = HI_FILE_MOVE; } // We aren't watching for this event on the file, discard if (!has_mask(params->mask, event.type)) { return NULL_EVENT; } // Use params path pointer, because that's what user has control over. event.pathname = params->path; // Event matched and we're watching for it return event; } } // Something happened with a file we're not watching return NULL_EVENT; } // Event on the folder itself return NULL_EVENT; } FileEvent file_event_pop(FileWatcher *fw) { mtx_lock(&fw->mutex); FileEvent e = NULL_EVENT; if (vector_size(&fw->events) > 0) { e = vector_last(&fw->events); vector_del_last(&fw->events); } mtx_unlock(&fw->mutex); return e; } /* * File Watcher */ static HiResult file_watcher_ctx_init(FileWatcherContext *ctx) { ctx->fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); if (ctx->fd == -1) { return HI_FAIL; } vector_init(&ctx->watches); vector_init(&ctx->params); return HI_OK; } static void file_watcher_ctx_destroy(FileWatcherContext *ctx) { if (ctx) { file_watch_destroy_watches(ctx); } } // Declare the thread func int file_watcher_watch(void *arg); FileWatcher *file_watcher_create() { // Allocate context FileWatcher *fw = malloc(sizeof(FileWatcher)); memset(fw, 0, sizeof(FileWatcher)); if (fw) { if (!HIOK(file_watcher_ctx_init(&fw->context))) { sc_log_error("Failed to initialize watch paths\n"); if (fw->context.fd == -1) { sc_log_error("Couldn't initialize inotify\n"); } free(fw); return NULL; } vector_init(&fw->events); mtx_init(&fw->mutex, mtx_plain); cnd_init(&fw->cond); fw->running = true; thrd_create(&fw->thread, file_watcher_watch, fw); return fw; } free(fw); return NULL; } HiResult file_watcher_add(FileWatcher *fw, const char *filename, u32 flags) { if (!fw) { sc_log_error("Attempted to add file watcher for '%s' with null context\n", filename); return HI_FAIL; } mtx_lock(&fw->mutex); if (!HIOK(file_watch_add(&fw->context, flags, filename))) { mtx_unlock(&fw->mutex); return HI_FAIL; } { // Add watch param as well WatchParams *params = watch_params_find_by_path(filename, &fw->context, NULL); if (!params) { WatchParams params = watch_params_create(filename, flags); vector_add(&fw->context.params, params); } else { params->mask |= flags; } } mtx_unlock(&fw->mutex); return HI_OK; } HiResult hi_file_watcher_remove(FileWatcher *fw, const char *filename) { if (!fw) { return HI_FAIL; } mtx_lock(&fw->mutex); if (!HIOK(file_watch_remove(&fw->context, filename))) { mtx_unlock(&fw->mutex); return HI_FAIL; } { // remove watchparam as well size_t i = 0; WatchParams *params = watch_params_find_by_path(filename, &fw->context, &i); watch_params_destroy(params); vector_del(&fw->context.params, i); } mtx_unlock(&fw->mutex); sc_log_info("Removed file: '%s' from watch, filename"); return HI_OK; } void file_watcher_notify(FileWatcher *fw) { if (fw && fw->running) { cnd_signal(&fw->cond); } } void file_watcher_destroy(FileWatcher *fw) { if (!fw) return; if (fw->running) { fw->running = false; file_watcher_notify(fw); thrd_join(fw->thread, NULL); } file_watcher_ctx_destroy(&fw->context); free(fw); } int file_watcher_watch(void *arg) { FileWatcher *ctx = (FileWatcher *)arg; sc_log_set_thread_name("File Watcher"); char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event)))); const struct inotify_event *event = NULL; mtx_lock(&ctx->mutex); while (ctx->running) { ssize_t n; n = read(ctx->context.fd, buf, sizeof(buf)); if (n == -1 && errno != EAGAIN) { sc_log_error("Failed to read inotify: %s. Exiting thread.\n", strerror(errno)); return 1; } if (n > 0) { for (char *ptr = buf; ptr < buf + n; ptr += sizeof(struct inotify_event) + event->len) { event = (const struct inotify_event *)ptr; FileEvent e = file_event_create(event, &ctx->context); if (e.type != HI_FILE_NONE) { vector_add(&ctx->events, e); if (event->len) { sc_log_debug("Event created: queue-size: %u, %s %s\n", vector_size(&ctx->events), e.pathname, hi_file_watch_type_to_str(e.type)); } } } continue; // read again without waiting } // wait for 100ms #define SEC_IN_NS 1000000000 struct timespec timeout; i64 wait_time_ns = 1000 * 1000 * 100; timespec_get(&timeout, TIME_UTC); timeout.tv_nsec += wait_time_ns; if (timeout.tv_nsec > SEC_IN_NS) { timeout.tv_sec += timeout.tv_nsec / SEC_IN_NS; timeout.tv_nsec = timeout.tv_nsec % SEC_IN_NS; } #undef SEC_IN_NS cnd_timedwait(&ctx->cond, &ctx->mutex, &timeout); } mtx_unlock(&ctx->mutex); return 0; }