taisei/src/filewatch/filewatch_inotify.c
Andrei Alexeyev fda8556a39
src: deprecate strdup(), use mem_strdup() explicitly
This fixes a nasty bug that manifests on windows when building without
precompiled headers. util/stringops.h used to silently replace strdup
with a macro that's compatible with mem_free(). This header would
typically be included everywhere due to PCH, but without it the strdup
from libc would sometimes be in scope. On most platforms mem_free() is
equivalent to free(), but not on windows, because we have to use
_aligned_free() there. Attempting to mem_free() the result of a libc
strdup() would segfault in such a configuration.

Avoid the footgun by banning strdup() entirely. Maybe redefining libc
names isn't such a great idea, who knew?
2024-09-10 14:31:55 +02:00

537 lines
12 KiB
C

/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
*/
#include "filewatch.h"
#include "events.h"
#include "hashtable.h"
#include "list.h"
#include "vfs/syspath_public.h"
#include <alloca.h>
#include <errno.h>
#include <stdio.h>
#include <sys/inotify.h>
#include <sys/resource.h>
#include <unistd.h>
// #define FW_DEBUG
#ifdef FW_DEBUG
#undef FW_DEBUG
#define FW_DEBUG(...) log_debug(__VA_ARGS__)
#define IF_FW_DEBUG(...) __VA_ARGS__
#else
#define FW_DEBUG(...) ((void)0)
#define IF_FW_DEBUG(...)
#endif
#ifndef IN_MASK_CREATE
// libinotify-kqueue doesn't have this
#define IN_MASK_CREATE (0)
#endif
#define WATCH_FLAGS ( \
IN_CLOSE_WRITE | \
IN_CREATE | \
IN_DELETE | \
IN_DELETE_SELF | \
IN_MODIFY | \
IN_MOVED_FROM | \
IN_MOVED_TO | \
IN_MOVE_SELF | \
\
IN_EXCL_UNLINK | \
IN_ONLYDIR | \
0)
#define INVALID_WD (-1)
#define FW (*_fw_globals)
#define EVENTS_BUF_SIZE 4096
typedef struct DirWatch DirWatch;
struct FileWatch {
LIST_INTERFACE(FileWatch);
char *filename;
DirWatch *dw;
bool updated;
bool deleted;
};
struct DirWatch {
LIST_INTERFACE(DirWatch);
FileWatch *filewatch_list;
char *path;
int wd;
};
typedef struct WDRecord {
DirWatch *dirwatch_list;
int32_t refs;
} WDRecord;
#define HT_SUFFIX int2wdrecord
#define HT_KEY_TYPE int32_t
#define HT_VALUE_TYPE WDRecord
#define HT_FUNC_HASH_KEY(key) htutil_hashfunc_uint32((uint32_t)(key))
#define HT_KEY_FMT PRIi32
#define HT_KEY_PRINTABLE(key) (key)
#define HT_VALUE_FMT "i"
#define HT_VALUE_PRINTABLE(val) ((val).wd)
#define HT_DECL
#define HT_IMPL
#include "hashtable_incproxy.inc.h"
#define HT_SUFFIX filewatchset
#define HT_KEY_TYPE FileWatch*
#define HT_VALUE_TYPE struct ht_empty
#define HT_FUNC_HASH_KEY(key) htutil_hashfunc_uint64((uint64_t)(key))
#define HT_KEY_FMT "p"
#define HT_KEY_PRINTABLE(key) ((void*)(key))
#define HT_VALUE_FMT "s"
#define HT_VALUE_PRINTABLE(val) "(empty)"
#define HT_DECL
#define HT_IMPL
#include "hashtable_incproxy.inc.h"
struct {
int inotify;
ht_int2wdrecord_t wd_records;
ht_str2ptr_t dirpath_to_dirwatch;
ht_filewatchset_t updated_watches;
SDL_mutex *modify_mtx;
} *_fw_globals;
static WDRecord *wdrecord_get(int wd, bool create) {
WDRecord *r = NULL;
if(!ht_int2wdrecord_get_ptr_unsafe(&FW.wd_records, wd, &r, create)) {
if(create) {
*r = (WDRecord) { 0 };
}
}
return r;
}
static void wdrecord_delete(int wd) {
ht_int2wdrecord_unset(&FW.wd_records, wd);
}
static int wdrecord_incref(WDRecord *r) {
return r->refs++;
}
static int wdrecord_decref(WDRecord *r) {
int32_t rc = --r->refs;
assert(rc >= 0);
return rc;
}
static DirWatch *dirwatch_get(const char *path, bool create) {
DirWatch *dw = ht_get(&FW.dirpath_to_dirwatch, path, NULL);
if(dw || !create) {
if(dw) {
assert(dw->path != NULL);
assert(!strcmp(dw->path, path));
}
return dw;
}
int wd = inotify_add_watch(FW.inotify, path, WATCH_FLAGS);
if(UNLIKELY(wd == INVALID_WD)) {
log_error("%s: %s", path, strerror(errno));
return NULL;
}
dw = ALLOC(typeof(*dw));
dw->path = mem_strdup(path);
dw->wd = wd;
WDRecord *wdrec = NOT_NULL(wdrecord_get(wd, true));
list_push(&wdrec->dirwatch_list, dw);
wdrecord_incref(wdrec);
ht_set(&FW.dirpath_to_dirwatch, path, dw);
return dw;
}
static void dirwatch_delete(DirWatch *dw) {
assert(dw->filewatch_list == NULL);
WDRecord *wdrec = NOT_NULL(wdrecord_get(dw->wd, false));
list_unlink(&wdrec->dirwatch_list, dw);
ht_unset(&FW.dirpath_to_dirwatch, dw->path);
mem_free(dw->path);
if(wdrecord_decref(wdrec) == 0) {
inotify_rm_watch(FW.inotify, dw->wd);
wdrecord_delete(dw->wd);
}
mem_free(dw);
}
static void dirwatch_invalidate(DirWatch *dw) {
log_warn("%s: Unimplemented (FIXME)", dw->path);
}
static bool filewatch_frame_event(SDL_Event *e, void *a);
attr_unused
static void dump_events(StringBuffer *sbuf, uint events) {
#define DELIM() do { \
if(sbuf->pos != sbuf->start) { \
strbuf_cat(sbuf, " | "); \
} \
} while(0)
#define HANDLE(evt) do { \
if(events & evt) { \
DELIM(); \
strbuf_cat(sbuf, #evt); \
events &= ~evt; \
} \
} while(0)
HANDLE(IN_ACCESS);
HANDLE(IN_MODIFY);
HANDLE(IN_ATTRIB);
HANDLE(IN_CLOSE_WRITE);
HANDLE(IN_CLOSE_NOWRITE);
HANDLE(IN_OPEN);
HANDLE(IN_MOVED_FROM);
HANDLE(IN_MOVED_TO);
HANDLE(IN_CREATE);
HANDLE(IN_DELETE);
HANDLE(IN_DELETE_SELF);
HANDLE(IN_MOVE_SELF);
HANDLE(IN_UNMOUNT);
HANDLE(IN_Q_OVERFLOW);
HANDLE(IN_IGNORED);
HANDLE(IN_ONLYDIR);
HANDLE(IN_DONT_FOLLOW);
HANDLE(IN_EXCL_UNLINK);
HANDLE(IN_MASK_CREATE);
HANDLE(IN_MASK_ADD);
HANDLE(IN_ISDIR);
HANDLE(IN_ONESHOT);
if(events) {
DELIM();
strbuf_printf(sbuf, "0x%08x", events);
}
#undef DELIM
#undef HANDLE
}
void filewatch_init(void) {
assert(_fw_globals == NULL);
// NOTE: This is for libinotify-kqueue (macOS/*BSD), since it will possibly have to open a file
// descriptor for every file inside a monitored directory. Linux is unaffected by this.
struct rlimit lim;
getrlimit(RLIMIT_NOFILE, &lim);
lim.rlim_cur = lim.rlim_max;
setrlimit(RLIMIT_NOFILE, &lim);
int inotify = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if(UNLIKELY(inotify < 0)) {
log_error("Failed to initialize inotify: %s", strerror(errno));
return;
}
_fw_globals = ALLOC(typeof(*_fw_globals));
FW.inotify = inotify;
ht_filewatchset_create(&FW.updated_watches);
ht_int2wdrecord_create(&FW.wd_records);
ht_create(&FW.dirpath_to_dirwatch);
FW.modify_mtx = SDL_CreateMutex();
if(UNLIKELY(FW.modify_mtx == NULL)) {
log_sdl_error(LOG_WARN, "SDL_CreateMutex");
}
events_register_handler(&(EventHandler) {
.proc = filewatch_frame_event,
.priority = EPRIO_SYSTEM,
.event_type = MAKE_TAISEI_EVENT(TE_FRAME),
});
}
void filewatch_shutdown(void) {
if(_fw_globals) {
events_unregister_handler(filewatch_frame_event);
close(FW.inotify);
ht_filewatchset_destroy(&FW.updated_watches);
ht_int2wdrecord_destroy(&FW.wd_records);
ht_destroy(&FW.dirpath_to_dirwatch);
SDL_DestroyMutex(FW.modify_mtx);
mem_free(_fw_globals);
_fw_globals = NULL;
}
}
static bool split_path(char *pathbuf, const char **dirpath, const char **filename) {
char *sep;
size_t plen = strlen(pathbuf);
// Strip trailing slash
// TODO: maybe guarantee this in vfs_syspath_normalize?
while(pathbuf[plen - 1] == '/') {
pathbuf[plen - 1] = 0;
if(--plen == 0) {
return false;
}
}
sep = memrchr(pathbuf, '/', plen);
if(UNLIKELY(sep == NULL)) {
return false;
}
*filename = sep + 1;
if(UNLIKELY(sep == pathbuf)) { // file in root directory?
*dirpath = "/";
} else {
*sep = 0;
*dirpath = pathbuf;
}
return true;
}
FileWatch *filewatch_watch(const char *syspath) {
if(!&FW) {
log_error("Subsystem not initialized");
return NULL;
}
if(*syspath != '/') {
log_error("%s: relative paths are not supported", syspath);
return NULL;
}
// NOTE: we want to normalize the path, but not resolve symlinks,
// so realpath(2) is not appropriate.
char pathbuf[strlen(syspath) + 1];
vfs_syspath_normalize(pathbuf, sizeof(pathbuf), syspath);
const char *dirpath, *filename;
if(UNLIKELY(!split_path(pathbuf, &dirpath, &filename))) {
log_error("%s: can't split path", syspath);
return NULL;
}
SDL_LockMutex(FW.modify_mtx);
DirWatch *dw = dirwatch_get(dirpath, true);
if(UNLIKELY(dw == NULL)) {
SDL_UnlockMutex(FW.modify_mtx);
log_error("%s: failed to watch parent directory", syspath);
return NULL;
}
auto fw = list_push(&dw->filewatch_list, ALLOC(FileWatch, {
.dw = dw,
.filename = mem_strdup(filename),
}));
SDL_UnlockMutex(FW.modify_mtx);
return fw;
}
void filewatch_unwatch(FileWatch *fw) {
SDL_LockMutex(FW.modify_mtx);
DirWatch *dw = NOT_NULL(fw->dw);
list_unlink(&dw->filewatch_list, fw);
mem_free(fw->filename);
mem_free(fw);
if(dw->filewatch_list == NULL) {
dirwatch_delete(dw);
}
SDL_UnlockMutex(FW.modify_mtx);
}
static uint32_t filewatch_process_event(
struct inotify_event *e
IF_FW_DEBUG(, StringBuffer *sbuf)
) {
if(UNLIKELY(e->wd == INVALID_WD)) {
log_error("inotify queue overflow");
return e->len;
}
WDRecord *rec = wdrecord_get(e->wd, false);
IF_FW_DEBUG(
strbuf_clear(sbuf);
dump_events(sbuf, e->mask);
FW_DEBUG("wd %i :: %s :: %s", e->wd, e->name, sbuf->start);
)
if(!rec) {
return e->len;
}
for(DirWatch *dw = rec->dirwatch_list; dw; dw = dw->next) {
if(dw->wd != e->wd) {
continue;
}
FW_DEBUG("Match dir %s", dw->path);
if(e->mask & IN_ISDIR) {
if(e->mask & (IN_MOVE_SELF | IN_DELETE_SELF | IN_IGNORED)) {
dirwatch_invalidate(dw);
}
continue;
}
for(FileWatch *fw = dw->filewatch_list; fw; fw = fw->next) {
if(strcmp(fw->filename, e->name)) {
FW_DEBUG("* %p %s != %s", fw, fw->filename, e->name);
continue;
}
FW_DEBUG("Match file %s/%s", dw->path, fw->filename);
if(e->mask & (IN_DELETE | IN_MOVED_FROM)) {
fw->deleted = true;
ht_filewatchset_set(&FW.updated_watches, fw, HT_EMPTY);
} else if(e->mask & (IN_CREATE | IN_MOVED_TO)) {
fw->deleted = false;
fw->updated = true;
ht_filewatchset_set(&FW.updated_watches, fw, HT_EMPTY);
} else if(e->mask & (IN_CLOSE_WRITE | IN_MODIFY)) {
fw->updated = true;
ht_filewatchset_set(&FW.updated_watches, fw, HT_EMPTY);
}
}
}
return e->len;
}
static uint32_t filewatch_process_event_misaligned(
size_t bufsize, char buf[bufsize]
IF_FW_DEBUG(, StringBuffer *sbuf)
) {
uint32_t sz;
memcpy(&sz, buf + offsetof(struct inotify_event, len), sizeof(sz));
assert(sz <= bufsize);
struct inotify_event *e = alloca(sizeof(*e) + sz);
memcpy(e, buf, sizeof(*e));
filewatch_process_event(e IF_FW_DEBUG(, sbuf));
return sz;
}
static void filewatch_process_events(ssize_t bufsize, char buf[bufsize]) {
IF_FW_DEBUG(
StringBuffer sbuf = {};
)
for(ssize_t i = 0; i < bufsize;) {
uintptr_t misalign = ((uintptr_t)(buf + i) & (alignof(struct inotify_event) - 1));
uint32_t nlen;
if(UNLIKELY(misalign)) {
IF_FW_DEBUG(log_warn("inotify_event pointer %p misaligned by %u",
buf + i, (uint)misalign));
nlen = filewatch_process_event_misaligned(
bufsize - i, buf + i
IF_FW_DEBUG(, &sbuf)
);
} else {
nlen = filewatch_process_event(
CASTPTR_ASSUME_ALIGNED(buf + i, struct inotify_event)
IF_FW_DEBUG(, &sbuf)
);
}
i += sizeof(struct inotify_event) + nlen;
}
IF_FW_DEBUG(
strbuf_free(&sbuf);
)
}
static void filewatch_process(void) {
SDL_LockMutex(FW.modify_mtx);
alignas(alignof(struct inotify_event)) char buf[EVENTS_BUF_SIZE];
for(;;) {
ssize_t r = read(FW.inotify, buf, sizeof(buf));
if(r == -1) {
if(errno != EAGAIN) {
log_error("%s", strerror(errno));
}
break;
}
FW_DEBUG("READ %ji", r);
filewatch_process_events(r, buf);
}
if(FW.updated_watches.num_elements_occupied == 0) {
SDL_UnlockMutex(FW.modify_mtx);
return;
}
ht_filewatchset_iter_t iter;
ht_filewatchset_iter_begin(&FW.updated_watches, &iter);
for(;iter.has_data; ht_filewatchset_iter_next(&iter)) {
FileWatch *fw = NOT_NULL(iter.key);
if(fw->deleted) {
events_emit(TE_FILEWATCH, FILEWATCH_FILE_DELETED, fw, NULL);
} else if(fw->updated) {
events_emit(TE_FILEWATCH, FILEWATCH_FILE_UPDATED, fw, NULL);
}
fw->deleted = fw->updated = false;
}
ht_filewatchset_iter_end(&iter);
ht_filewatchset_unset_all(&FW.updated_watches);
SDL_UnlockMutex(FW.modify_mtx);
}
static bool filewatch_frame_event(SDL_Event *e, void *a) {
filewatch_process();
return false;
}