add threaded filewatcher

This commit is contained in:
2025-03-27 01:24:57 +02:00
parent 6d0cbbb011
commit 2015894068
20 changed files with 600 additions and 95 deletions

View File

@@ -0,0 +1,348 @@
#include "filewatcher.h"
#include <errno.h>
#include <stdatomic.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/select.h>
#include <threads.h>
#include <time.h>
#include <unistd.h>
#include "array/array.h"
#include "common.h"
#include "logger/logger.h"
sc_array_def(hiFileEvent, fevent);
typedef struct hiFileEvents {
struct sc_array_fevent events;
} hiFileEvents;
typedef struct {
int wd; // Watch descriptor
u32 mask; // watch mask used
const char *path; // Owning
} WatchPath;
sc_array_def(WatchPath, watch);
typedef struct hiWatchPaths {
int fd;
struct sc_array_watch watches;
} hiWatchPaths;
typedef struct hiFileWatcherContext {
thrd_t thread;
mtx_t mutex;
cnd_t cond;
int running;
struct hiWatchPaths watchpaths;
struct hiFileEvents events;
} hiFileWatcherContext;
/*
* File Events
*/
static hiFileEvent create_file_event(const struct inotify_event *event) {
hiFileEvent hievent = {0};
if (!event) {
return hievent;
}
if (event->len)
hievent.file = strdup(event->name);
if (has_mask(event->mask, IN_ACCESS)) {
hievent.type = HI_FILE_ACCESS;
} else if (has_mask(event->mask, IN_MODIFY)) {
hievent.type = HI_FILE_MODIFY;
} else if (has_mask(event->mask, IN_CREATE)) {
hievent.type = HI_FILE_CREATE;
} else if (has_mask(event->mask, IN_DELETE)) {
hievent.type = HI_FILE_DELETE;
}
return hievent;
}
static hiFileEvent copy_file_event(const hiFileEvent *event) {
hiFileEvent cpy = *event;
// Copy the file string, so this can be destroyed without destroying the
// original
cpy.file = strdup(event->file);
return cpy;
}
void hi_file_event_free(hiFileEvent *event) {
if (event) {
free((void *)event->file);
}
}
void hi_file_event_destroy(hiFileEvent *event) {
if (event) {
free((void *)event->file);
memset(event, 0, sizeof(*event));
}
}
hiFileEvent hi_file_event_pop(hiFileWatcherContext *ctx) {
mtx_lock(&ctx->mutex);
hiFileEvent *last = &sc_array_last(&ctx->events.events);
hiFileEvent e = *last;
// prevent destroy from freeing the file string
last->file = 0;
hi_file_event_destroy(last);
sc_array_del_last(&ctx->events.events);
mtx_unlock(&ctx->mutex);
return e;
}
static HiloadResult file_events_init(hiFileEvents *events) {
sc_array_init(&events->events);
return HILOAD_OK;
}
static void file_events_destroy(hiFileEvents *events) {
if (events) {
for (int i = 0; i < sc_array_size(&events->events); ++i) {
hi_file_event_free(&sc_array_at(&events->events, i));
}
sc_array_term(&events->events);
}
}
/*
* Watch paths
*/
static inline u32 create_watch_mask(u32 flags) {
u32 inmask = 0;
if (has_mask(flags, HI_FILE_ACCESS)) {
inmask |= IN_ACCESS;
}
if (has_mask(flags, HI_FILE_MODIFY)) {
inmask |= IN_MODIFY;
}
if (has_mask(flags, HI_FILE_CREATE)) {
inmask |= IN_CREATE;
}
if (has_mask(flags, HI_FILE_DELETE)) {
inmask |= IN_DELETE;
}
return inmask;
}
static HiloadResult watch_path_add(hiWatchPaths *paths, u32 mask,
const char *path) {
if (!paths || paths->fd == -1) {
sc_log_error("Invalid inotify context\n");
return HILOAD_FAIL;
}
int wd = inotify_add_watch(paths->fd, path, create_watch_mask(mask));
if (wd == -1) {
sc_log_error("Couldn't watch: %s: %s\n", path, strerror(errno));
return HILOAD_FAIL;
}
{
WatchPath wp = {.wd = wd, .mask = mask, .path = strdup(path)};
sc_array_add(&paths->watches, wp);
}
return HILOAD_OK;
}
static void watch_path_destroy(int fd, WatchPath *wf) {
inotify_rm_watch(fd, wf->wd);
free((void *)wf->path);
wf->wd = -1;
wf->path = NULL;
}
static HiloadResult watch_path_remove(hiWatchPaths *paths, const char *path) {
bool found = false;
{
for (int i = 0; i < sc_array_size(&paths->watches); ++i) {
WatchPath *wp = &sc_array_at(&paths->watches, i);
if (strcmp(wp->path, path) == 0) {
watch_path_destroy(paths->fd, wp);
sc_array_del(&paths->watches, i);
found = true;
break;
}
}
}
return found ? HILOAD_OK : HILOAD_FAIL;
}
static HiloadResult watch_paths_init(hiWatchPaths *paths) {
paths->fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK);
if (paths->fd == -1) {
return HILOAD_FAIL;
}
sc_array_init(&paths->watches);
return HILOAD_OK;
}
static void watch_paths_destroy(hiWatchPaths *paths) {
if (paths) {
for (int i = 0; i < sc_array_size(&paths->watches); i++) {
WatchPath *wp = &sc_array_at(&paths->watches, i);
watch_path_destroy(paths->fd, wp);
}
sc_array_term(&paths->watches);
}
}
/*
* File Watcher
*/
// Declare the thread func
int file_watcher_watch(void *arg);
hiFileWatcherContext *hi_file_watcher_create() {
// Allocate context
hiFileWatcherContext *context = malloc(sizeof(hiFileWatcherContext));
memset(context, 0, sizeof(hiFileWatcherContext));
if (context) {
if (!HILOADRES(watch_paths_init(&context->watchpaths))) {
sc_log_error("Failed to initialize watch paths\n");
if (context->watchpaths.fd == -1) {
sc_log_error("Couldn't initialize inotify\n");
}
free(context);
return NULL;
}
if (!HILOADRES(file_events_init(&context->events))) {
sc_log_error("Failed to initialize file events\n");
free(context);
return NULL;
}
mtx_init(&context->mutex, mtx_plain);
cnd_init(&context->cond);
context->running = true;
thrd_create(&context->thread, file_watcher_watch, context);
return context;
}
free(context);
return NULL;
}
HiloadResult hi_file_watcher_add(struct hiFileWatcherContext *ctx,
const char *filename, u32 flags) {
if (!ctx) {
sc_log_error("Attempted to add file watcher for '%s' with null context\n",
filename);
return HILOAD_FAIL;
}
mtx_lock(&ctx->mutex);
if (!HILOADRES(watch_path_add(&ctx->watchpaths, flags, filename))) {
mtx_unlock(&ctx->mutex);
return HILOAD_FAIL;
}
mtx_unlock(&ctx->mutex);
sc_log_info("Watching file: %s\n", filename);
return HILOAD_OK;
}
HiloadResult hi_file_watcher_remove(struct hiFileWatcherContext *ctx,
const char *filename) {
if (!ctx) {
return HILOAD_FAIL;
}
mtx_lock(&ctx->mutex);
if (!HILOADRES(watch_path_remove(&ctx->watchpaths, filename))) {
mtx_unlock(&ctx->mutex);
return HILOAD_FAIL;
}
mtx_unlock(&ctx->mutex);
sc_log_info("Removed file: '%s' from watch, filename");
return HILOAD_OK;
}
void hi_file_watcher_notify(hiFileWatcherContext *ctx) {
if (ctx && ctx->running) {
cnd_signal(&ctx->cond);
}
}
void hi_file_watcher_destroy(hiFileWatcherContext *ctx) {
if (!ctx)
return;
if (ctx->running) {
ctx->running = false;
hi_file_watcher_notify(ctx);
thrd_join(ctx->thread, NULL);
}
file_events_destroy(&ctx->events);
watch_paths_destroy(&ctx->watchpaths);
free(ctx);
}
int file_watcher_watch(void *arg) {
hiFileWatcherContext *ctx = (hiFileWatcherContext *)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->watchpaths.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 = create_file_event(event);
sc_array_add(&ctx->events.events, e);
}
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;
}