#include "filewatcher.h" #include #include #include #include #include #include #include #include #include #include "array/array.h" #include "array/sc_array.h" #include "common.h" #include "filewatch.h" #include "filewatch_context.h" #include "filewatch_type.h" #include "logger/logger.h" #include "types.h" sc_array_def(hiFileEvent, fwevent); typedef struct hiFileWatcher { thrd_t thread; mtx_t mutex; cnd_t cond; int running; struct hiFileWatcherContext context; struct sc_array_fwevent events; } hiFileWatcher; /* * Watch Params */ static inline hiWatchParams watch_params_create(const char *path, u32 mask) { return (hiWatchParams){.path = strdup(path), .mask = mask}; } static inline void watch_params_destroy(hiWatchParams *params) { free((char *)params->path); } static hiWatchParams *watch_params_find_by_path(const char *path, hiFileWatcherContext *ctx, size_t *index) { const struct sc_array_fwparam *params = &ctx->params; for (size_t i = 0; i < sc_array_size(params); ++i) { hiWatchParams *param = &sc_array_at(params, i); if (strcmp(path, param->path) == 0) { if (index) *index = i; return param; } } return NULL; } /* * File Events */ #define NULL_EVENT (hiFileEvent){0}; static hiFileEvent file_event_create(const struct inotify_event *inevent, hiFileWatcherContext *ctx) { hiFileEvent event = {0}; if (!inevent) { return event; } const hiFileWatch *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 < sc_array_size(&fw->files); ++i) { const char *watched_file = sc_array_at(&fw->files, i); if (strcmp(watched_file, filename) == 0) { // check if event mask matches the watcher mask hiWatchParams *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; } hiFileEvent hi_file_event_pop(hiFileWatcher *fw) { mtx_lock(&fw->mutex); hiFileEvent e = NULL_EVENT; if (sc_array_size(&fw->events) > 0) { e = sc_array_last(&fw->events); sc_array_del_last(&fw->events); } mtx_unlock(&fw->mutex); return e; } /* * File Watcher */ static HiloadResult file_watcher_ctx_init(hiFileWatcherContext *ctx) { ctx->fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); if (ctx->fd == -1) { return HILOAD_FAIL; } sc_array_init(&ctx->watches); sc_array_init(&ctx->params); return HILOAD_OK; } static void file_watcher_ctx_destroy(hiFileWatcherContext *ctx) { if (ctx) { file_watch_destroy_watches(ctx); } } // Declare the thread func int file_watcher_watch(void *arg); hiFileWatcher *hi_file_watcher_create() { // Allocate context hiFileWatcher *fw = malloc(sizeof(hiFileWatcher)); memset(fw, 0, sizeof(hiFileWatcher)); 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; } sc_array_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; } HiloadResult hi_file_watcher_add(struct hiFileWatcher *fw, const char *filename, u32 flags) { if (!fw) { sc_log_error("Attempted to add file watcher for '%s' with null context\n", filename); return HILOAD_FAIL; } mtx_lock(&fw->mutex); if (!HIOK(file_watch_add(&fw->context, flags, filename))) { mtx_unlock(&fw->mutex); return HILOAD_FAIL; } { // Add watch param as well hiWatchParams *params = watch_params_find_by_path(filename, &fw->context, NULL); if (!params) { hiWatchParams params = watch_params_create(filename, flags); sc_array_add(&fw->context.params, params); } else { params->mask |= flags; } } mtx_unlock(&fw->mutex); return HILOAD_OK; } HiloadResult hi_file_watcher_remove(struct hiFileWatcher *fw, const char *filename) { if (!fw) { return HILOAD_FAIL; } mtx_lock(&fw->mutex); if (!HIOK(file_watch_remove(&fw->context, filename))) { mtx_unlock(&fw->mutex); return HILOAD_FAIL; } { // remove watchparam as well size_t i = 0; hiWatchParams *params = watch_params_find_by_path(filename, &fw->context, &i); watch_params_destroy(params); sc_array_del(&fw->context.params, i); } mtx_unlock(&fw->mutex); sc_log_info("Removed file: '%s' from watch, filename"); return HILOAD_OK; } void hi_file_watcher_notify(hiFileWatcher *fw) { if (fw && fw->running) { cnd_signal(&fw->cond); } } void hi_file_watcher_destroy(hiFileWatcher *fw) { if (!fw) return; if (fw->running) { fw->running = false; hi_file_watcher_notify(fw); thrd_join(fw->thread, NULL); } file_watcher_ctx_destroy(&fw->context); free(fw); } int file_watcher_watch(void *arg) { hiFileWatcher *ctx = (hiFileWatcher *)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; hiFileEvent e = file_event_create(event, &ctx->context); if (e.type != HI_FILE_NONE) { sc_array_add(&ctx->events, e); if (event->len) { sc_log_debug("Event created: queue-size: %u, %s %s\n", sc_array_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; }