From 74d7190c62821f708d07c3b7686805f98fddf424 Mon Sep 17 00:00:00 2001 From: veclavtalica Date: Thu, 30 Jan 2025 04:30:20 +0300 Subject: [PATCH] ilimination of system code, removal of x-watcher and replacement of it by dmon, fixes in audio code, dynamic asset reload --- CMakeLists.txt | 38 +- src/game_object/twn_dynamic_game_object.c | 98 ++ src/game_object/twn_linux_game_object.c | 161 -- src/game_object/twn_static_game_object.c | 14 - src/game_object/twn_win32_game_object.c | 90 -- src/system/linux/twn_timer.c | 76 - src/twn_amalgam.c | 1 + src/twn_audio.c | 46 +- src/twn_audio_c.h | 3 +- src/twn_filewatch.c | 160 +- src/twn_filewatch_c.h | 6 +- src/twn_game_object_c.h | 6 +- src/twn_loop.c | 21 +- include/twn_loop.h => src/twn_loop_c.h | 2 + src/twn_main.c | 2 +- src/twn_timer.c | 38 + src/{system/twn_timer.h => twn_timer_c.h} | 2 +- third-party/dmon/dmon.h | 1748 +++++++++++++++++++++ third-party/dmon/dmon_extra.h | 164 ++ third-party/x-watcher/array.h | 77 - third-party/x-watcher/x-watcher.h | 838 ---------- 21 files changed, 2164 insertions(+), 1427 deletions(-) create mode 100644 src/game_object/twn_dynamic_game_object.c delete mode 100644 src/game_object/twn_linux_game_object.c delete mode 100644 src/game_object/twn_win32_game_object.c delete mode 100644 src/system/linux/twn_timer.c rename include/twn_loop.h => src/twn_loop_c.h (79%) create mode 100644 src/twn_timer.c rename src/{system/twn_timer.h => twn_timer_c.h} (69%) create mode 100644 third-party/dmon/dmon.h create mode 100644 third-party/dmon/dmon_extra.h delete mode 100644 third-party/x-watcher/array.h delete mode 100644 third-party/x-watcher/x-watcher.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 03d82e9..30b79b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,18 +64,6 @@ set(PHYSFS_ARCHIVE_7Z OFF CACHE INTERNAL "") add_subdirectory(third-party/physfs ${CMAKE_CURRENT_BINARY_DIR}/third-party/physfs SYSTEM) add_subdirectory(third-party/libxm ${CMAKE_CURRENT_BINARY_DIR}/third-party/libxm SYSTEM) - -if(LINUX) - set(SYSTEM_SOURCE_FILES - src/system/linux/twn_timer.c - $<$:src/game_object/twn_linux_game_object.c>) -elseif(WIN32) - set(SYSTEM_SOURCE_FILES - $<$:src/game_object/twn_win32_game_object.c>) -else() - set(SYSTEM_SOURCE_FILES) -endif() - if(EMSCRIPTEN) set(TWN_RENDERING_API WEBGL1) else() @@ -96,15 +84,19 @@ set(TWN_THIRD_PARTY_SOURCE_FILES set(TWN_NONOPT_SOURCE_FILES src/twn_stb.c - src/twn_loop.c src/twn_main.c - src/twn_context.c include/twn_context.h - src/twn_audio.c include/twn_audio.h - src/twn_util.c include/twn_util.h - src/twn_input.c include/twn_input.h - src/twn_camera.c src/twn_camera_c.h - src/twn_textures.c src/twn_textures_c.h - src/twn_filewatch.c src/twn_filewatch_c.h + + src/twn_context.c include/twn_context.h + src/twn_audio.c include/twn_audio.h + src/twn_util.c include/twn_util.h + src/twn_input.c include/twn_input.h + + src/twn_loop.c src/twn_loop_c.h + src/twn_camera.c src/twn_camera_c.h + src/twn_textures.c src/twn_textures_c.h + src/twn_filewatch.c src/twn_filewatch_c.h + src/twn_filewatch.c src/twn_filewatch_c.h + src/twn_timer.c src/twn_timer_c.h src/rendering/twn_draw.c src/rendering/twn_draw_c.h src/rendering/twn_quads.c @@ -114,12 +106,14 @@ set(TWN_NONOPT_SOURCE_FILES src/rendering/twn_triangles.c src/rendering/twn_billboards.c src/rendering/twn_circles.c - src/rendering/twn_skybox.c) + src/rendering/twn_skybox.c +) set(TWN_SOURCE_FILES $,src/twn_amalgam.c src/twn_stb.c,${TWN_NONOPT_SOURCE_FILES}> # for dynamic load based solution main is compiled in a separate target + $<$:src/game_object/twn_dynamic_game_object.c> $<$>:src/twn_main.c src/game_object/twn_static_game_object.c> @@ -258,7 +252,7 @@ function(include_deps target) third-party/physfs/extras third-party/libxm/include third-party/stb - third-party/x-watcher + third-party/dmon third-party/tomlc99 $<$>:third-party/glad/include>) diff --git a/src/game_object/twn_dynamic_game_object.c b/src/game_object/twn_dynamic_game_object.c new file mode 100644 index 0000000..589e1e7 --- /dev/null +++ b/src/game_object/twn_dynamic_game_object.c @@ -0,0 +1,98 @@ +#include "twn_game_object_c.h" +#include "twn_engine_context_c.h" +#include "twn_filewatch_c.h" +#include "twn_util_c.h" +#include "twn_util.h" +#include "twn_loop_c.h" + +#include + +#ifdef _WIN32 +#define GAME_OBJECT_NAME "libgame.dll" +#else +#define GAME_OBJECT_NAME "libgame.so" +#endif + +static void (*game_tick_callback)(void); +static void (*game_end_callback)(void); + +static void *handle = NULL; + + +static void game_object_file_action(char const *path, enum FilewatchAction action) { + (void)action; + + if (action == FILEWATCH_ACTION_FILE_DELETED) + return; + + if (handle) { + SDL_UnloadObject(handle); + game_tick_callback = NULL; + game_end_callback = NULL; + handle = NULL; + } + + handle = SDL_LoadObject(path); + if (!handle) { + CRY_SDL("Hot Reload Error: Cannot open game code shared object"); + return; + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" + + game_tick_callback = (void (*)(void))SDL_LoadFunction(handle, "game_tick"); + if (!game_tick_callback) { + CRY("Hot Reload Error", "game_tick_callback() symbol wasn't found"); + goto ERR_GETTING_PROC; + } + + game_end_callback = (void (*)(void))SDL_LoadFunction(handle, "game_end"); + if (!game_end_callback) { + CRY("Hot Reload Error", "game_end_callback() symbol wasn't found"); + goto ERR_GETTING_PROC; + } + +#pragma GCC diagnostic pop + + if (fabsf(0.0f - ctx.game.frame_number) > 0.00001f) { + log_info("Game object was reloaded\n"); + reset_state(); + } + + return; + +ERR_GETTING_PROC: + SDL_UnloadObject(handle); + handle = NULL; + game_tick_callback = NULL; + game_end_callback = NULL; +} + + +void game_object_load(void) { + static bool filewatch_attached; + if (!filewatch_attached) { + char *game_object_path; + SDL_asprintf(&game_object_path, "%s%s", ctx.base_dir, GAME_OBJECT_NAME); + filewatch_add_file(game_object_path, game_object_file_action); + game_object_file_action(game_object_path, FILEWATCH_ACTION_FILE_MODIFIED); + SDL_free(game_object_path); + filewatch_attached = true; + } +} + + +void game_object_unload(void) { + game_end_callback(); + /* needs to be closed otherwise symbols aren't resolved again */ + SDL_UnloadObject(handle); + game_tick_callback = NULL; + game_end_callback = NULL; + handle = NULL; +} + + +void game_object_tick(void) { + game_tick_callback(); +} diff --git a/src/game_object/twn_linux_game_object.c b/src/game_object/twn_linux_game_object.c deleted file mode 100644 index b6ad120..0000000 --- a/src/game_object/twn_linux_game_object.c +++ /dev/null @@ -1,161 +0,0 @@ -#include "twn_game_object_c.h" -#include "twn_engine_context_c.h" -#include "twn_util_c.h" -#include "twn_util.h" - -#include -#include - -#include - - -#define GAME_OBJECT_NAME "libgame.so" -#define MODIFIED_TICKS_MERGED 10 - - -static void (*game_tick_callback)(void); -static void (*game_end_callback)(void); - -static x_watcher *watcher; -static void *handle = NULL; - -static float last_tick_modified; -static bool loaded_after_modification = true; -static SDL_mutex *lock; - -static char *game_object_path; - -static void load_game_object(void) { - /* needs to be closed otherwise symbols aren't resolved again */ - if (handle) { - dlclose(handle); - handle = NULL; - game_tick_callback = NULL; - game_end_callback = NULL; - } - - void *new_handle = dlopen(game_object_path, RTLD_LAZY); - if (!new_handle) { - log_critical("Hot Reload Error: Cannot open game code shared object (%s)", dlerror()); - goto ERR_OPENING_SO; - } - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpedantic" - - game_tick_callback = (void (*)(void))dlsym(new_handle, "game_tick"); - if (!game_tick_callback) { - CRY("Hot Reload Error", "game_tick_callback() symbol wasn't found"); - goto ERR_GETTING_PROC; - } - - game_end_callback = (void (*)(void))dlsym(new_handle, "game_end"); - if (!game_end_callback) { - CRY("Hot Reload Error", "game_end_callback() symbol wasn't found"); - goto ERR_GETTING_PROC; - } - -#pragma GCC diagnostic pop - - handle = new_handle; - - if (fabsf(0.0f - ctx.game.frame_number) > 0.00001f) - log_info("Game object was reloaded\n"); - - return; - -ERR_GETTING_PROC: - dlclose(new_handle); - game_tick_callback = NULL; - game_end_callback = NULL; - -ERR_OPENING_SO: - SDL_UnlockMutex(lock); - -} - -static void watcher_callback(XWATCHER_FILE_EVENT event, - const char *path, - int context, - void *data) -{ - (void)context; - (void)path; - (void)data; - - switch(event) { - case XWATCHER_FILE_CREATED: - case XWATCHER_FILE_MODIFIED: - SDL_LockMutex(lock); - last_tick_modified = ctx.game.frame_number; - loaded_after_modification = false; - SDL_UnlockMutex(lock); - break; - - case XWATCHER_FILE_UNSPECIFIED: - case XWATCHER_FILE_REMOVED: - case XWATCHER_FILE_OPENED: - case XWATCHER_FILE_ATTRIBUTES_CHANGED: - case XWATCHER_FILE_NONE: - case XWATCHER_FILE_RENAMED: - default: - break; - } -} - - -void game_object_load(void) { - SDL_asprintf(&game_object_path, "%s%s", ctx.base_dir, GAME_OBJECT_NAME); - - watcher = xWatcher_create(); - - xWatcher_reference dir; - dir.path = game_object_path; - dir.callback_func = watcher_callback; - - xWatcher_appendFile(watcher, &dir); - xWatcher_start(watcher); - - lock = SDL_CreateMutex(); - load_game_object(); -} - - -void game_object_unload(void) { - game_end_callback(); - xWatcher_destroy(watcher); - watcher = NULL; - dlclose(handle); - handle = NULL; - game_tick_callback = NULL; - game_end_callback = NULL; - SDL_DestroyMutex(lock); -} - - -bool game_object_try_reloading(void) { - bool result = false; - - /* only load the modified library after some time, as compilers make a lot of modifications */ - SDL_LockMutex(lock); - if (ctx.game.frame_number - last_tick_modified > MODIFIED_TICKS_MERGED && - !loaded_after_modification) { - load_game_object(); - loaded_after_modification = true; - result = true; - } SDL_UnlockMutex(lock); - - return result; -} - - -void game_object_tick(void) { - game_tick_callback(); -} - -void *game_object_get_game_tick_address(void) { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpedantic" - return (void *)&game_tick_callback; -#pragma GCC diagnostic pop -} diff --git a/src/game_object/twn_static_game_object.c b/src/game_object/twn_static_game_object.c index 931f142..a1db49a 100644 --- a/src/game_object/twn_static_game_object.c +++ b/src/game_object/twn_static_game_object.c @@ -5,24 +5,10 @@ void game_object_load(void) { } - void game_object_unload(void) { game_end(); } - -bool game_object_try_reloading(void) { - return false; -} - - void game_object_tick(void) { game_tick(); } - -void *game_object_get_game_tick_address(void) { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpedantic" - return (void *)&game_tick; -#pragma GCC diagnostic pop -} diff --git a/src/game_object/twn_win32_game_object.c b/src/game_object/twn_win32_game_object.c deleted file mode 100644 index 4958afa..0000000 --- a/src/game_object/twn_win32_game_object.c +++ /dev/null @@ -1,90 +0,0 @@ -#include "twn_game_object_c.h" -#include "twn_engine_context_c.h" -#include "twn_util_c.h" -#include "twn_util.h" - -#include -#include - - -#define GAME_OBJECT_PATH "libgame.dll" - - -static void (*game_tick_callback)(void); -static void (*game_end_callback)(void); - -static void *handle = NULL; - - -static void load_game_object(void) { - /* needs to be closed otherwise symbols aren't resolved again */ - if (handle) { - FreeLibrary(handle); - handle = NULL; - game_tick_callback = NULL; - game_end_callback = NULL; - } - - void *new_handle = LoadLibraryA(GAME_OBJECT_PATH); - if (!new_handle) { - log_critical("Hot Reload Error: Cannot open game code shared object (%s)", GetLastError()); - goto ERR_OPENING_SO; - } - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpedantic" - - game_tick_callback = (void (*)(void))GetProcAddress(new_handle, "game_tick"); - if (!game_tick_callback) { - CRY("Hot Reload Error", "game_tick_callback() symbol wasn't found"); - goto ERR_GETTING_PROC; - } - - game_end_callback = (void (*)(void))GetProcAddress(new_handle, "game_end"); - if (!game_end_callback) { - CRY("Hot Reload Error", "game_end_callback() symbol wasn't found"); - goto ERR_GETTING_PROC; - } - -#pragma GCC diagnostic pop - - handle = new_handle; - - if (ctx.game.frame_number != 0) - log_info("Game object was reloaded\n"); - - return; - -ERR_GETTING_PROC: - FreeLibrary(new_handle); - game_tick_callback = NULL; - game_end_callback = NULL; - -ERR_OPENING_SO: - die_abruptly(); -} - - -void game_object_load(void) { - load_game_object(); -} - - -void game_object_unload(void) { - game_end_callback(); - FreeLibrary(handle); - handle = NULL; - game_tick_callback = NULL; - game_end_callback = NULL; -} - - -/* doesn't support reloading because of problems with file watching */ -bool game_object_try_reloading(void) { - return false; -} - - -void game_object_tick(void) { - game_tick_callback(); -} diff --git a/src/system/linux/twn_timer.c b/src/system/linux/twn_timer.c deleted file mode 100644 index be26538..0000000 --- a/src/system/linux/twn_timer.c +++ /dev/null @@ -1,76 +0,0 @@ -#include "../twn_timer.h" -#include "twn_engine_context_c.h" - -#include - -#include -#include -#include -#include -#include - - -static timer_t timerid; -static struct sigaction sa; -static struct sigevent sev; - -static bool created; -static uint64_t used_milliseconds_to_expire; - -#define SANITY_TIMER_MESSAGE_FMT "Game tick exeeded its allocated time (%lu milliseconds), application is closing" - -/* stop application */ -static void sanity_timer_handler(int sig, siginfo_t *si, void *uc) { - (void)uc; (void)sig; (void)si; - size_t text_str_len = snprintf(NULL, 0, SANITY_TIMER_MESSAGE_FMT, used_milliseconds_to_expire) + 1; - char *text_str = SDL_malloc(text_str_len); - snprintf(text_str, text_str_len, SANITY_TIMER_MESSAGE_FMT, used_milliseconds_to_expire); - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "sanity timer", text_str, ctx.window); - SDL_free(text_str); - raise(SIGKILL); -} - - -bool start_sanity_timer(uint64_t milliseconds_to_expire) { - if (!created) { - sa.sa_flags = SA_SIGINFO; - sa.sa_sigaction = sanity_timer_handler; - sigemptyset(&sa.sa_mask); - if (sigaction(SIGRTMIN, &sa, NULL) == -1) - goto ERR_SIGACTION; - - sev.sigev_notify = SIGEV_SIGNAL; - sev.sigev_signo = SIGRTMIN; - sev.sigev_value.sival_ptr = &timerid; - if (timer_create(CLOCK_MONOTONIC, &sev, &timerid) == -1) - goto ERR_TIMERCREATE; - - created = true; - - ERR_TIMERCREATE: - // ERR_SIGPROCMASK: - ERR_SIGACTION: - return false; - } - - struct itimerspec its = {0}; - its.it_value.tv_sec = milliseconds_to_expire / 1000; - its.it_value.tv_nsec = (milliseconds_to_expire * 1000000 % 1000000000); - if (timer_settime(timerid, 0, &its, NULL) == -1) - return false; - - used_milliseconds_to_expire = milliseconds_to_expire; - - return true; -} - - -bool end_sanity_timer(void) { - struct itimerspec its = {0}; - its.it_value.tv_sec = 0; - its.it_value.tv_nsec = 0; - if (timer_settime(timerid, 0, &its, NULL) == -1) - return false; - - return false; -} diff --git a/src/twn_amalgam.c b/src/twn_amalgam.c index 09d7608..d941a87 100644 --- a/src/twn_amalgam.c +++ b/src/twn_amalgam.c @@ -10,6 +10,7 @@ #include "twn_textures.c" #include "twn_util.c" #include "twn_filewatch.c" +#include "twn_timer.c" #include "rendering/twn_circles.c" #include "rendering/twn_draw.c" diff --git a/src/twn_audio.c b/src/twn_audio.c index f4dd69f..af7e76a 100644 --- a/src/twn_audio.c +++ b/src/twn_audio.c @@ -29,6 +29,7 @@ static const uint8_t audio_exts_len[AUDIO_FILE_TYPE_COUNT] = { }; /* TODO: allow for vectorization and packed vectors (alignment care and alike) */ +/* TODO: free channel name duplicates */ /* TODO: count frames without use, free the memory when threshold is met */ /* TODO: count repeated usages for sound effect cases with rendering to ram? */ @@ -212,6 +213,7 @@ static void free_audio_channel(AudioChannel channel) { SDL_assert_always(false); break; } + SDL_free(channel.path); } @@ -283,32 +285,42 @@ void audio_play(const char *path, /* create a channel if it doesn't exist */ if (!pair) { AudioFileType const file_type = infer_audio_file_type(path); - AudioChannel new_channel = { + AudioChannel const new_channel = { .file_type = file_type, .context = init_audio_context(path, file_type), - .path = path, - .name = channel, + .path = SDL_strdup(path), .repeat = repeat, .volume = volume, .panning = panning, }; - shput(ctx.audio_channels, channel, new_channel); + shput(ctx.audio_channels, SDL_strdup(channel), new_channel); pair = shgetp_null(ctx.audio_channels, channel); } - /* TODO: destroy and create new context when channel is reused for different file */ - - /* works for both restarts and new audio */ - if (strcmp(pair->value.path, path) == 0) + /* repeat if so */ + else if (strcmp(pair->value.path, path) == 0) repeat_audio(&pair->value); + else { + /* start anew, reuse channel name */ + free_audio_channel(pair->value); + AudioFileType const file_type = infer_audio_file_type(path); + AudioChannel const new_channel = { + .file_type = file_type, + .context = init_audio_context(path, file_type), + .path = SDL_strdup(path), + .repeat = repeat, + .volume = volume, + .panning = panning, + }; + pair->value = new_channel; + } } else { /* audio without channel plays without repeat and ability to change parameters over its course, nor stop it */ AudioFileType const file_type = infer_audio_file_type(path); AudioChannel new_channel = { .file_type = file_type, .context = init_audio_context(path, file_type), - .path = path, - .name = NULL, + .path = SDL_strdup(path), .repeat = false, .volume = volume, .panning = panning, @@ -506,12 +518,12 @@ static void audio_sample_and_mixin_channel(AudioChannel *channel, } -static void sanity_check_channel(const AudioChannel *channel) { - if (channel->volume < 0.0f || channel->volume > 1.0f) - log_warn("Volume argument is out of range for channel (%s)", channel->name); +static void sanity_check_channel(const AudioChannelItem channel) { + if (channel.value.volume < 0.0f || channel.value.volume > 1.0f) + log_warn("Volume argument is out of range for channel (%s)", channel.key ? channel.key : "unnamed"); - if (channel->panning < -1.0f || channel->panning > 1.0f) - log_warn("Panning argument is out of range for channel (%s)", channel->name); + if (channel.value.panning < -1.0f || channel.value.panning > 1.0f) + log_warn("Panning argument is out of range for channel (%s)", channel.key ? channel.key : "unnamed"); } @@ -523,7 +535,7 @@ void audio_callback(void *userdata, uint8_t *stream, int len) { size_t const audio_channels_len = shlen(ctx.audio_channels); for (size_t i = 0; i < audio_channels_len; ++i) { - sanity_check_channel(&ctx.audio_channels[i].value); + sanity_check_channel(ctx.audio_channels[i]); audio_sample_and_mixin_channel(&ctx.audio_channels[i].value, stream, len, i == audio_channels_len - 1); @@ -531,7 +543,7 @@ void audio_callback(void *userdata, uint8_t *stream, int len) { size_t const unnamed_audio_channels_len = shlen(ctx.unnamed_audio_channels); for (size_t i = 0; i < unnamed_audio_channels_len; ++i) { - sanity_check_channel(&ctx.unnamed_audio_channels[i]); + sanity_check_channel((AudioChannelItem){NULL, ctx.unnamed_audio_channels[i]}); audio_sample_and_mixin_channel(&ctx.unnamed_audio_channels[i], stream, len, i == unnamed_audio_channels_len - 1); diff --git a/src/twn_audio_c.h b/src/twn_audio_c.h index a352442..ca597c1 100644 --- a/src/twn_audio_c.h +++ b/src/twn_audio_c.h @@ -48,8 +48,7 @@ union AudioContext { typedef struct AudioChannel { AudioFileType file_type; union AudioContext context; /* interpreted by `file_type` value */ - const char *path; - const char *name; + char *path; bool repeat; float volume; float panning; diff --git a/src/twn_filewatch.c b/src/twn_filewatch.c index a640fda..e24490b 100644 --- a/src/twn_filewatch.c +++ b/src/twn_filewatch.c @@ -2,169 +2,111 @@ #include "twn_util.h" #include "twn_engine_context_c.h" -#include +#define DMON_IMPL +#include #include #include -struct FilewatchPathToCallback { +struct FilewatchEntry { char *path; FileatchCallback callback; - float tick_last_reported; - enum FilewatchAction action_pending : 3; - bool action_processed : 1; + enum FilewatchAction *actions_pending; }; -static struct FilewatchPathToCallback *filewatch_directories; -static struct FilewatchPathToCallback *filewatch_files; +static struct FilewatchEntry *filewatch_directories; +static struct FilewatchEntry *filewatch_files; /* note: it gets rebuilt on every addition, as api is such */ /* TODO: test whether it's possible to miss on update while watcher is being rebuilt */ /* we might have to enumerate things, just to make sure */ -static x_watcher *filewatcher; static SDL_mutex *filewatcher_lock; +static bool filewatcher_initialized; -/* TODO: check whether path and p->path match? */ -static void filewatch_dispatch(XWATCHER_FILE_EVENT event, - const char *path, - int context, - void *additional_data -) { - (void)additional_data; (void)path; +static void filewatch_callback(dmon_watch_id watch_id, + dmon_action action, + const char* rootdir, + const char* filepath, + const char* oldfilepath, + void* user) +{ + enum FilewatchAction faction; - enum FilewatchAction action; - - switch (event) { - case XWATCHER_FILE_CREATED: - action = FILEWATCH_ACTION_FILE_CREATED; + switch (action) { + case DMON_ACTION_CREATE: + faction = FILEWATCH_ACTION_FILE_CREATED; break; - case XWATCHER_FILE_REMOVED: - action = FILEWATCH_ACTION_FILE_DELETED; + case DMON_ACTION_DELETE: + faction = FILEWATCH_ACTION_FILE_DELETED; break; - case XWATCHER_FILE_MODIFIED: - action = FILEWATCH_ACTION_FILE_MODIFIED; + case DMON_ACTION_MODIFY: + faction = FILEWATCH_ACTION_FILE_MODIFIED; break; - case XWATCHER_FILE_NONE: - case XWATCHER_FILE_OPENED: - case XWATCHER_FILE_RENAMED: - case XWATCHER_FILE_UNSPECIFIED: - case XWATCHER_FILE_ATTRIBUTES_CHANGED: default: return; } SDL_LockMutex(filewatcher_lock); - struct FilewatchPathToCallback *p; + intptr_t const context = (intptr_t)user; + struct FilewatchEntry *p; if (context < 0) p = &filewatch_files[-context - 1]; else p = &filewatch_directories[context]; - p->tick_last_reported = ctx.game.frame_number; - p->action_processed = false; - p->action_pending = action; - + arrpush(p->actions_pending, faction); SDL_UnlockMutex(filewatcher_lock); } -static bool filewatcher_rebuild(void) { - if (filewatcher) - xWatcher_destroy(filewatcher); - - filewatcher = xWatcher_create(); - if (!filewatcher) { - log_warn("Error creating xWatcher instance, no file watching is done"); - return false; - } - - int const filewatch_directories_len = arrlen(filewatch_directories); - for (int i = 0; i < filewatch_directories_len; ++i) { - xWatcher_reference ref = { - .path = filewatch_directories[i].path, - .callback_func = filewatch_dispatch, - .context = i, - }; - if (!xWatcher_appendDir(filewatcher, &ref)) - log_warn("Error watching dir contents: %s", filewatch_directories[i].path); - } - - int const filewatch_files_len = arrlen(filewatch_files); - for (int i = 0; i < filewatch_files_len; ++i) { - xWatcher_reference ref = { - .path = filewatch_files[i].path, - .callback_func = filewatch_dispatch, - .context = -(i + 1), /* in negative range to allow inrefence */ - }; - if (!xWatcher_appendDir(filewatcher, &ref)) - log_warn("Error watching file: %s", filewatch_files[i].path); - } - - if (!xWatcher_start(filewatcher)) { - log_warn("Error creating xWatcher instance, no file watching is done"); - xWatcher_destroy(filewatcher); - filewatcher = NULL; - return false; - } - - return true; -} - - -bool filewatch_add_directory(char *dir, FileatchCallback callback) { +bool filewatch_add_directory(char const *dir, FileatchCallback callback) { SDL_assert(dir && callback); - struct FilewatchPathToCallback const w = { + if (!filewatcher_initialized) { + dmon_init(); + filewatcher_initialized = true; + } + + struct FilewatchEntry const w = { .callback = callback, - .path = SDL_strdup(dir), - .action_processed = true, - .tick_last_reported = ctx.game.frame_number, + .path = SDL_strdup(dir), /* TODO: free */ }; arrpush(filewatch_directories, w); - - return filewatcher_rebuild(); + dmon_watch(dir, filewatch_callback, DMON_WATCHFLAGS_RECURSIVE, (void *)(intptr_t)(arrlen(filewatch_directories) - 1)); } -bool filewatch_add_file(char *filepath, FileatchCallback callback) { +bool filewatch_add_file(char const *filepath, FileatchCallback callback) { SDL_assert(filepath && callback); - struct FilewatchPathToCallback const f = { + if (!filewatcher_initialized) { + dmon_init(); + filewatcher_initialized = true; + } + + struct FilewatchEntry const f = { .callback = callback, - .path = SDL_strdup(filepath), - .action_processed = true, - .tick_last_reported = ctx.game.frame_number, + .path = SDL_strdup(filepath), /* TODO: free */ }; arrpush(filewatch_files, f); - - return filewatcher_rebuild(); + dmon_watch("./", filewatch_callback, 0, (void *)(intptr_t)(-arrlen(filewatch_files))); } void filewatch_poll(void) { SDL_LockMutex(filewatcher_lock); - int const filewatch_directories_len = arrlen(filewatch_directories); - for (int i = 0; i < filewatch_directories_len; ++i) { - if (!filewatch_directories[i].action_processed - && (ctx.game.frame_number - filewatch_directories[i].tick_last_reported > FILEWATCH_MODIFIED_TICKS_MERGED)) { - SDL_assert(filewatch_directories[i].action_pending != FILEWATCH_ACTION_FILE_NONE); - filewatch_directories[i].callback(filewatch_directories[i].path, filewatch_directories[i].action_pending); - filewatch_directories[i].action_pending = FILEWATCH_ACTION_FILE_NONE; - filewatch_directories[i].action_processed = true; - } + for (int i = 0; i < arrlen(filewatch_directories); ++i) { + for (int u = 0; u < arrlen(filewatch_directories[i].actions_pending); ++u) + filewatch_directories[i].callback(filewatch_directories[i].path, filewatch_directories[i].actions_pending[u]); + arrfree(filewatch_directories[i].actions_pending); } - int const filewatch_files_len = arrlen(filewatch_files); - for (int i = 0; i < filewatch_files_len; ++i) { - if (!filewatch_files[i].action_processed - && (ctx.game.frame_number - filewatch_files[i].tick_last_reported > FILEWATCH_MODIFIED_TICKS_MERGED)) { - SDL_assert(filewatch_files[i].action_pending != FILEWATCH_ACTION_FILE_NONE); - filewatch_files[i].callback(filewatch_files[i].path, filewatch_files[i].action_pending); - filewatch_files[i].action_pending = FILEWATCH_ACTION_FILE_NONE; - filewatch_files[i].action_processed = true; - } + for (int i = 0; i < arrlen(filewatch_files); ++i) { + for (int u = 0; u < arrlen(filewatch_files[i].actions_pending); ++u) + filewatch_files[i].callback(filewatch_files[i].path, filewatch_files[i].actions_pending[u]); + arrfree(filewatch_files[i].actions_pending); } SDL_UnlockMutex(filewatcher_lock); diff --git a/src/twn_filewatch_c.h b/src/twn_filewatch_c.h index 04a90fb..15c54cb 100644 --- a/src/twn_filewatch_c.h +++ b/src/twn_filewatch_c.h @@ -3,8 +3,6 @@ #include -#define FILEWATCH_MODIFIED_TICKS_MERGED 10 - enum FilewatchAction { FILEWATCH_ACTION_FILE_NONE, FILEWATCH_ACTION_FILE_CREATED, @@ -14,9 +12,9 @@ enum FilewatchAction { typedef void (*FileatchCallback)(char const *path, enum FilewatchAction action); -bool filewatch_add_directory(char *dir, FileatchCallback callback); +bool filewatch_add_directory(char const *dir, FileatchCallback callback); -bool filewatch_add_file(char *file, FileatchCallback callback); +bool filewatch_add_file(char const *file, FileatchCallback callback); void filewatch_poll(void); diff --git a/src/twn_game_object_c.h b/src/twn_game_object_c.h index 1d11cc9..5a5c8f1 100644 --- a/src/twn_game_object_c.h +++ b/src/twn_game_object_c.h @@ -11,13 +11,9 @@ void game_object_load(void); +/* note: it should be only called when application is closing */ void game_object_unload(void); -/* returns true if reload happened, otherwise false */ -bool game_object_try_reloading(void); - void game_object_tick(void); -void *game_object_get_game_tick_address(void); - #endif diff --git a/src/twn_loop.c b/src/twn_loop.c index 80a269b..12e2130 100644 --- a/src/twn_loop.c +++ b/src/twn_loop.c @@ -1,4 +1,4 @@ -#include "twn_loop.h" +#include "twn_loop_c.h" #include "twn_engine_context_c.h" #include "twn_filewatch_c.h" #include "twn_input_c.h" @@ -6,7 +6,7 @@ #include "twn_util_c.h" #include "twn_game_object_c.h" #include "twn_textures_c.h" -#include "system/twn_timer.h" +#include "twn_timer_c.h" #include #include @@ -747,15 +747,20 @@ static void clean_up(void) { } -static void reset_state(void) { +void reset_state(void) { + ctx.game.initialization_needed = true; input_reset_state(&ctx.input); textures_reset_state(); } static void pack_contents_modified(char const *path, enum FilewatchAction action) { - log_info("Pack contents invalidated: %s, action: %i", path, action); - reset_state(); + static uint32_t last_reset_tick; + if (last_reset_tick != (uint32_t)ctx.game.frame_number) { + log_info("Pack contents invalidated: %s, action: %i", path, action); + reset_state(); + last_reset_tick = (uint32_t)ctx.game.frame_number; + } } @@ -868,11 +873,7 @@ int enter_loop(int argc, char **argv) { profile_end("startup"); while (ctx.is_running) { - if (game_object_try_reloading()) { - ctx.game.initialization_needed = true; - reset_state(); - } - + /* dispatch all filewatch driven events, such as game object and asset pack reload */ filewatch_poll(); main_loop(); } diff --git a/include/twn_loop.h b/src/twn_loop_c.h similarity index 79% rename from include/twn_loop.h rename to src/twn_loop_c.h index 4eb5969..94a3f43 100644 --- a/include/twn_loop.h +++ b/src/twn_loop_c.h @@ -6,5 +6,7 @@ TWN_API int enter_loop(int argc, char **argv); +TWN_API void reset_state(void); + #endif diff --git a/src/twn_main.c b/src/twn_main.c index c892cd5..652749d 100644 --- a/src/twn_main.c +++ b/src/twn_main.c @@ -1,4 +1,4 @@ -#include "twn_loop.h" +#include "twn_loop_c.h" #ifndef EMSCRIPTEN #define SDL_MAIN_HANDLED diff --git a/src/twn_timer.c b/src/twn_timer.c new file mode 100644 index 0000000..1e14312 --- /dev/null +++ b/src/twn_timer.c @@ -0,0 +1,38 @@ +#include "twn_timer_c.h" +#include "twn_engine_context_c.h" + +#include + +static SDL_TimerID sanity_timer; + +#define SANITY_TIMER_MESSAGE_FMT "Game tick exeeded its allocated time (%u milliseconds), application is closing" + +/* stop application */ +static uint32_t sanity_timer_handler(uint32_t interval, void *data) { + (void)data; + + size_t text_str_len = snprintf(NULL, 0, SANITY_TIMER_MESSAGE_FMT, interval) + 1; + char *text_str = SDL_malloc(text_str_len); + snprintf(text_str, text_str_len, SANITY_TIMER_MESSAGE_FMT, interval); + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "sanity timer", text_str, ctx.window); + SDL_free(text_str); + + /* TODO: figure out the most portable way to do it */ + /* TODO: different type of behavior is possible, especially for debugging */ + quick_exit(EXIT_FAILURE); +} + + +bool start_sanity_timer(uint32_t milliseconds_to_expire) { + if (!sanity_timer) { + sanity_timer = SDL_AddTimer(milliseconds_to_expire, sanity_timer_handler, NULL); + } + + return sanity_timer != 0; +} + + +bool end_sanity_timer(void) { + SDL_RemoveTimer(sanity_timer); + sanity_timer = 0; +} diff --git a/src/system/twn_timer.h b/src/twn_timer_c.h similarity index 69% rename from src/system/twn_timer.h rename to src/twn_timer_c.h index 2cabcaf..292a8ad 100644 --- a/src/system/twn_timer.h +++ b/src/twn_timer_c.h @@ -4,7 +4,7 @@ #include #include -bool start_sanity_timer(uint64_t milliseconds_to_expire); +bool start_sanity_timer(uint32_t milliseconds_to_expire); bool end_sanity_timer(void); #endif // TWN_TIMER_H diff --git a/third-party/dmon/dmon.h b/third-party/dmon/dmon.h new file mode 100644 index 0000000..ead5824 --- /dev/null +++ b/third-party/dmon/dmon.h @@ -0,0 +1,1748 @@ +#ifndef __DMON_H__ +#define __DMON_H__ + +// +// Copyright 2023 Sepehr Taghdisian (septag@github). All rights reserved. +// License: https://github.com/septag/dmon#license-bsd-2-clause +// +// Portable directory monitoring library +// watches directories for file or directory changes. +// +// Usage: +// define DMON_IMPL and include this file to use it: +// #define DMON_IMPL +// #include "dmon.h" +// +// dmon_init(): +// Call this once at the start of your program. +// This will start a low-priority monitoring thread +// dmon_deinit(): +// Call this when your work with dmon is finished, usually on program terminate +// This will free resources and stop the monitoring thread +// dmon_watch: +// Watch for directories +// You can watch multiple directories by calling this function multiple times +// rootdir: root directory to monitor +// watch_cb: callback function to receive events. +// NOTE that this function is called from another thread, so you should +// beware of data races in your application when accessing data within this +// callback +// flags: watch flags, see dmon_watch_flags_t +// user_data: user pointer that is passed to callback function +// Returns the Id of the watched directory after successful call, or returns Id=0 if error +// dmon_unwatch: +// Remove the directory from watch list +// +// see test.c for the basic example +// +// Configuration: +// You can customize some low-level functionality like malloc and logging by overriding macros: +// +// DMON_MALLOC, DMON_FREE, DMON_REALLOC: +// define these macros to override memory allocations +// default is 'malloc', 'free' and 'realloc' +// DMON_ASSERT: +// define this to provide your own assert +// default is 'assert' +// DMON_LOG_ERROR: +// define this to provide your own logging mechanism +// default implementation logs to stdout and breaks the program +// DMON_LOG_DEBUG +// define this to provide your own extra debug logging mechanism +// default implementation logs to stdout in DEBUG and does nothing in other builds +// DMON_API_DECL, DMON_API_IMPL +// define these to provide your own API declarations. (for example: static) +// default is nothing (which is extern in C language ) +// DMON_MAX_PATH +// Maximum size of path characters +// default is 260 characters +// DMON_MAX_WATCHES +// Maximum number of watch directories +// default is 64 +// DMON_SLEEP_INTERVAL +// Number of milliseconds to pause between polling for file changes +// default is 10 ms +// +// TODO: +// - Use FSEventStreamSetDispatchQueue instead of FSEventStreamScheduleWithRunLoop on MacOS +// - DMON_WATCHFLAGS_FOLLOW_SYMLINKS does not resolve files +// - implement DMON_WATCHFLAGS_OUTOFSCOPE_LINKS +// - implement DMON_WATCHFLAGS_IGNORE_DIRECTORIES +// +// History: +// 1.0.0 First version. working Win32/Linux backends +// 1.1.0 MacOS backend +// 1.1.1 Minor fixes, eliminate gcc/clang warnings with -Wall +// 1.1.2 Eliminate some win32 dead code +// 1.1.3 Fixed select not resetting causing high cpu usage on linux +// 1.2.1 inotify (linux) fixes and improvements, added extra functionality header for linux +// to manually add/remove directories manually to the watch handle, in case of large file sets +// 1.2.2 Name refactoring +// 1.3.0 Fixing bugs and proper watch/unwatch handles with freelists. Lower memory consumption, especially on Windows backend +// 1.3.1 Fix in MacOS event grouping + +#include +#include + +#ifndef DMON_API_DECL +# define DMON_API_DECL +#endif + +#ifndef DMON_API_IMPL +# define DMON_API_IMPL +#endif + +typedef struct { uint32_t id; } dmon_watch_id; + +// Pass these flags to `dmon_watch` +typedef enum dmon_watch_flags_t { + DMON_WATCHFLAGS_RECURSIVE = 0x1, // monitor all child directories + DMON_WATCHFLAGS_FOLLOW_SYMLINKS = 0x2, // resolve symlinks (linux only) + DMON_WATCHFLAGS_OUTOFSCOPE_LINKS = 0x4, // TODO: not implemented yet + DMON_WATCHFLAGS_IGNORE_DIRECTORIES = 0x8 // TODO: not implemented yet +} dmon_watch_flags; + +// Action is what operation performed on the file. this value is provided by watch callback +typedef enum dmon_action_t { + DMON_ACTION_CREATE = 1, + DMON_ACTION_DELETE, + DMON_ACTION_MODIFY, + DMON_ACTION_MOVE +} dmon_action; + +#ifdef __cplusplus +extern "C" { +#endif + +DMON_API_DECL void dmon_init(void); +DMON_API_DECL void dmon_deinit(void); + +DMON_API_DECL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* rootdir, const char* filepath, + const char* oldfilepath, void* user), + uint32_t flags, void* user_data); +DMON_API_DECL void dmon_unwatch(dmon_watch_id id); + +#ifdef __cplusplus +} +#endif + +#ifdef DMON_IMPL + +#define DMON_OS_WINDOWS 0 +#define DMON_OS_MACOS 0 +#define DMON_OS_LINUX 0 + +#if defined(_WIN32) || defined(_WIN64) +# undef DMON_OS_WINDOWS +# define DMON_OS_WINDOWS 1 +#elif defined(__linux__) +# undef DMON_OS_LINUX +# define DMON_OS_LINUX 1 +#elif defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) +# undef DMON_OS_MACOS +# define DMON_OS_MACOS __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ +#else +# define DMON_OS 0 +# error "unsupported platform" +#endif + +#if DMON_OS_WINDOWS +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +# include +# ifdef _MSC_VER +# pragma intrinsic(_InterlockedExchange) +# endif +#elif DMON_OS_LINUX +# ifndef __USE_MISC +# define __USE_MISC +# endif +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#elif DMON_OS_MACOS +# include +# include +# include +# include +# include +#endif + +#ifndef DMON_MALLOC +# include +# define DMON_MALLOC(size) malloc(size) +# define DMON_FREE(ptr) free(ptr) +# define DMON_REALLOC(ptr, size) realloc(ptr, size) +#endif + +#ifndef DMON_ASSERT +# include +# define DMON_ASSERT(e) assert(e) +#endif + +#ifndef DMON_LOG_ERROR +# include +# define DMON_LOG_ERROR(s) do { puts(s); DMON_ASSERT(0); } while(0) +#endif + +#ifndef DMON_LOG_DEBUG +# ifndef NDEBUG +# include +# define DMON_LOG_DEBUG(s) do { puts(s); } while(0) +# else +# define DMON_LOG_DEBUG(s) +# endif +#endif + +#ifndef DMON_MAX_WATCHES +# define DMON_MAX_WATCHES 64 +#endif + +#ifndef DMON_MAX_PATH +# define DMON_MAX_PATH 260 +#endif + +#define _DMON_UNUSED(x) (void)(x) + +#ifndef _DMON_PRIVATE +# if defined(__GNUC__) || defined(__clang__) +# define _DMON_PRIVATE __attribute__((unused)) static +# else +# define _DMON_PRIVATE static +# endif +#endif + +#ifndef DMON_SLEEP_INTERVAL +# define DMON_SLEEP_INTERVAL 10 +#endif + +#include + +#ifndef _DMON_LOG_ERRORF +# define _DMON_LOG_ERRORF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_ERROR(msg); } while(0); +#endif + +#ifndef _DMON_LOG_DEBUGF +# define _DMON_LOG_DEBUGF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_DEBUG(msg); } while(0); +#endif + +#ifndef _dmon_min +# define _dmon_min(a, b) ((a) < (b) ? (a) : (b)) +#endif + +#ifndef _dmon_max +# define _dmon_max(a, b) ((a) > (b) ? (a) : (b)) +#endif + +#ifndef _dmon_swap +# define _dmon_swap(a, b, _type) \ + do { \ + _type tmp = a; \ + a = b; \ + b = tmp; \ + } while (0) +#endif + +#ifndef _dmon_make_id +# ifdef __cplusplus +# define _dmon_make_id(id) {id} +# else +# define _dmon_make_id(id) (dmon_watch_id) {id} +# endif +#endif // _dmon_make_id + +_DMON_PRIVATE bool _dmon_isrange(char ch, char from, char to) +{ + return (uint8_t)(ch - from) <= (uint8_t)(to - from); +} + +_DMON_PRIVATE bool _dmon_isupperchar(char ch) +{ + return _dmon_isrange(ch, 'A', 'Z'); +} + +_DMON_PRIVATE char _dmon_tolowerchar(char ch) +{ + return ch + (_dmon_isupperchar(ch) ? 0x20 : 0); +} + +_DMON_PRIVATE char* _dmon_tolower(char* dst, int dst_sz, const char* str) +{ + int offset = 0; + int dst_max = dst_sz - 1; + while (*str && offset < dst_max) { + dst[offset++] = _dmon_tolowerchar(*str); + ++str; + } + dst[offset] = '\0'; + return dst; +} + +_DMON_PRIVATE char* _dmon_strcpy(char* dst, int dst_sz, const char* src) +{ + DMON_ASSERT(dst); + DMON_ASSERT(src); + + const int32_t len = (int32_t)strlen(src); + const int32_t _max = dst_sz - 1; + const int32_t num = (len < _max ? len : _max); + memcpy(dst, src, num); + dst[num] = '\0'; + + return dst; +} + +_DMON_PRIVATE char* _dmon_unixpath(char* dst, int size, const char* path) +{ + size_t len = strlen(path), i; + len = _dmon_min(len, (size_t)size - 1); + + for (i = 0; i < len; i++) { + if (path[i] != '\\') + dst[i] = path[i]; + else + dst[i] = '/'; + } + dst[len] = '\0'; + return dst; +} + +#if DMON_OS_LINUX || DMON_OS_MACOS +_DMON_PRIVATE char* _dmon_strcat(char* dst, int dst_sz, const char* src) +{ + int len = (int)strlen(dst); + return _dmon_strcpy(dst + len, dst_sz - len, src); +} +#endif // DMON_OS_LINUX || DMON_OS_MACOS + +// stretchy buffer: https://github.com/nothings/stb/blob/master/stretchy_buffer.h +#define stb_sb_free(a) ((a) ? DMON_FREE(stb__sbraw(a)),0 : 0) +#define stb_sb_push(a,v) (stb__sbmaybegrow(a,1), (a)[stb__sbn(a)++] = (v)) +#define stb_sb_pop(a) (stb__sbn(a)--) +#define stb_sb_count(a) ((a) ? stb__sbn(a) : 0) +#define stb_sb_add(a,n) (stb__sbmaybegrow(a,n), stb__sbn(a)+=(n), &(a)[stb__sbn(a)-(n)]) +#define stb_sb_last(a) ((a)[stb__sbn(a)-1]) +#define stb_sb_reset(a) ((a) ? (stb__sbn(a) = 0) : 0) + +#define stb__sbraw(a) ((int *) (a) - 2) +#define stb__sbm(a) stb__sbraw(a)[0] +#define stb__sbn(a) stb__sbraw(a)[1] + +#define stb__sbneedgrow(a,n) ((a)==0 || stb__sbn(a)+(n) >= stb__sbm(a)) +#define stb__sbmaybegrow(a,n) (stb__sbneedgrow(a,(n)) ? stb__sbgrow(a,n) : 0) +#define stb__sbgrow(a,n) (*((void **)&(a)) = stb__sbgrowf((a), (n), sizeof(*(a)))) + +static void * stb__sbgrowf(void *arr, int increment, int itemsize) +{ + int dbl_cur = arr ? 2*stb__sbm(arr) : 0; + int min_needed = stb_sb_count(arr) + increment; + int m = dbl_cur > min_needed ? dbl_cur : min_needed; + int *p = (int *) DMON_REALLOC(arr ? stb__sbraw(arr) : 0, itemsize * m + sizeof(int)*2); + if (p) { + if (!arr) + p[1] = 0; + p[0] = m; + return p+2; + } else { + return (void *) (2*sizeof(int)); // try to force a NULL pointer exception later + } +} + +// watcher callback (same as dmon.h's declaration) +typedef void (_dmon_watch_cb)(dmon_watch_id, dmon_action, const char*, const char*, const char*, void*); + +#if DMON_OS_WINDOWS +// --------------------------------------------------------------------------------------------------------------------- +// @Windows +// IOCP +#ifdef UNICODE +# define _DMON_WINAPI_STR(name, size) wchar_t _##name[size]; MultiByteToWideChar(CP_UTF8, 0, name, -1, _##name, size) +#else +# define _DMON_WINAPI_STR(name, size) const char* _##name = name +#endif + +typedef struct dmon__win32_event { + char filepath[DMON_MAX_PATH]; + DWORD action; + dmon_watch_id watch_id; + bool skip; +} dmon__win32_event; + +typedef struct dmon__watch_state { + dmon_watch_id id; + OVERLAPPED overlapped; + HANDLE dir_handle; + uint8_t buffer[64512]; // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx + DWORD notify_filter; + _dmon_watch_cb* watch_cb; + uint32_t watch_flags; + void* user_data; + char rootdir[DMON_MAX_PATH]; + char old_filepath[DMON_MAX_PATH]; +} dmon__watch_state; + +typedef struct dmon__state { + int num_watches; + dmon__watch_state* watches[DMON_MAX_WATCHES]; + int freelist[DMON_MAX_WATCHES]; + HANDLE thread_handle; + CRITICAL_SECTION mutex; + volatile LONG modify_watches; + dmon__win32_event* events; + bool quit; +} dmon__state; + +static bool _dmon_init; +static dmon__state _dmon; + +_DMON_PRIVATE bool _dmon_refresh_watch(dmon__watch_state* watch) +{ + return ReadDirectoryChangesW(watch->dir_handle, watch->buffer, sizeof(watch->buffer), + (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) ? TRUE : FALSE, + watch->notify_filter, NULL, &watch->overlapped, NULL) != 0; +} + +_DMON_PRIVATE void _dmon_unwatch(dmon__watch_state* watch) +{ + CancelIo(watch->dir_handle); + CloseHandle(watch->overlapped.hEvent); + CloseHandle(watch->dir_handle); +} + +_DMON_PRIVATE void _dmon_win32_process_events(void) +{ + int i, c; + for (i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__win32_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + + if (ev->action == FILE_ACTION_MODIFIED || ev->action == FILE_ACTION_ADDED) { + // remove duplicate modifies on a single file + int j; + for (j = i + 1; j < c; j++) { + dmon__win32_event* check_ev = &_dmon.events[j]; + if (check_ev->action == FILE_ACTION_MODIFIED && + strcmp(ev->filepath, check_ev->filepath) == 0) { + check_ev->skip = true; + } + } + } + } + + // trigger user callbacks + for (i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__win32_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + dmon__watch_state* watch = _dmon.watches[ev->watch_id.id - 1]; + + if(watch == NULL || watch->watch_cb == NULL) { + continue; + } + + switch (ev->action) { + case FILE_ACTION_ADDED: + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + case FILE_ACTION_MODIFIED: + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + case FILE_ACTION_RENAMED_OLD_NAME: { + // find the first occurrence of the NEW_NAME + // this is somewhat API flaw that we have no reference for relating old and new files + int j; + for (j = i + 1; j < c; j++) { + dmon__win32_event* check_ev = &_dmon.events[j]; + if (check_ev->action == FILE_ACTION_RENAMED_NEW_NAME) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } break; + case FILE_ACTION_REMOVED: + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + } + } + stb_sb_reset(_dmon.events); +} + +_DMON_PRIVATE DWORD WINAPI _dmon_thread(LPVOID arg) +{ + _DMON_UNUSED(arg); + HANDLE wait_handles[DMON_MAX_WATCHES]; + dmon__watch_state* watch_states[DMON_MAX_WATCHES]; + + SYSTEMTIME starttm; + GetSystemTime(&starttm); + uint64_t msecs_elapsed = 0; + + while (!_dmon.quit) { + int i; + if (_dmon.modify_watches || !TryEnterCriticalSection(&_dmon.mutex)) { + Sleep(DMON_SLEEP_INTERVAL); + continue; + } + + if (_dmon.num_watches == 0) { + Sleep(DMON_SLEEP_INTERVAL); + LeaveCriticalSection(&_dmon.mutex); + continue; + } + + for (i = 0; i < DMON_MAX_WATCHES; i++) { + if (_dmon.watches[i]) { + dmon__watch_state* watch = _dmon.watches[i]; + watch_states[i] = watch; + wait_handles[i] = watch->overlapped.hEvent; + } + } + + DWORD wait_result = WaitForMultipleObjects(_dmon.num_watches, wait_handles, FALSE, 10); + DMON_ASSERT(wait_result != WAIT_FAILED); + if (wait_result != WAIT_TIMEOUT) { + dmon__watch_state* watch = watch_states[wait_result - WAIT_OBJECT_0]; + DMON_ASSERT(HasOverlappedIoCompleted(&watch->overlapped)); + + DWORD bytes; + if (GetOverlappedResult(watch->dir_handle, &watch->overlapped, &bytes, FALSE)) { + char filepath[DMON_MAX_PATH]; + PFILE_NOTIFY_INFORMATION notify; + size_t offset = 0; + + if (bytes == 0) { + _dmon_refresh_watch(watch); + LeaveCriticalSection(&_dmon.mutex); + continue; + } + + do { + notify = (PFILE_NOTIFY_INFORMATION)&watch->buffer[offset]; + + int count = WideCharToMultiByte(CP_UTF8, 0, notify->FileName, + notify->FileNameLength / sizeof(WCHAR), + filepath, DMON_MAX_PATH - 1, NULL, NULL); + filepath[count] = TEXT('\0'); + _dmon_unixpath(filepath, sizeof(filepath), filepath); + + // TODO: ignore directories if flag is set + + if (stb_sb_count(_dmon.events) == 0) { + msecs_elapsed = 0; + } + dmon__win32_event wev = { { 0 }, notify->Action, watch->id, false }; + _dmon_strcpy(wev.filepath, sizeof(wev.filepath), filepath); + stb_sb_push(_dmon.events, wev); + + offset += notify->NextEntryOffset; + } while (notify->NextEntryOffset > 0); + + if (!_dmon.quit) { + _dmon_refresh_watch(watch); + } + } + } // if (WaitForMultipleObjects) + + SYSTEMTIME tm; + GetSystemTime(&tm); + LONG dt =(tm.wSecond - starttm.wSecond) * 1000 + (tm.wMilliseconds - starttm.wMilliseconds); + starttm = tm; + msecs_elapsed += dt; + if (msecs_elapsed > 100 && stb_sb_count(_dmon.events) > 0) { + _dmon_win32_process_events(); + msecs_elapsed = 0; + } + + LeaveCriticalSection(&_dmon.mutex); + } + return 0; +} + + +DMON_API_IMPL void dmon_init(void) +{ + DMON_ASSERT(!_dmon_init); + InitializeCriticalSection(&_dmon.mutex); + + _dmon.thread_handle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_dmon_thread, NULL, 0, NULL); + DMON_ASSERT(_dmon.thread_handle); + + for (int i = 0; i < DMON_MAX_WATCHES; i++) + _dmon.freelist[i] = DMON_MAX_WATCHES - i - 1; + + _dmon_init = true; +} + + +DMON_API_IMPL void dmon_deinit(void) +{ + DMON_ASSERT(_dmon_init); + _dmon.quit = true; + if (_dmon.thread_handle != INVALID_HANDLE_VALUE) { + WaitForSingleObject(_dmon.thread_handle, INFINITE); + CloseHandle(_dmon.thread_handle); + } + + { + int i; + for (i = 0; i < DMON_MAX_WATCHES; i++) { + if (_dmon.watches[i]) { + _dmon_unwatch(_dmon.watches[i]); + DMON_FREE(_dmon.watches[i]); + } + } + } + + DeleteCriticalSection(&_dmon.mutex); + stb_sb_free(_dmon.events); + memset(&_dmon, 0x0, sizeof(_dmon)); + _dmon_init = false; +} + +DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* dirname, const char* filename, + const char* oldname, void* user), + uint32_t flags, void* user_data) +{ + DMON_ASSERT(_dmon_init); + DMON_ASSERT(watch_cb); + DMON_ASSERT(rootdir && rootdir[0]); + + _InterlockedExchange(&_dmon.modify_watches, 1); + EnterCriticalSection(&_dmon.mutex); + + DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); + if (_dmon.num_watches >= DMON_MAX_WATCHES) { + DMON_LOG_ERROR("Exceeding maximum number of watches"); + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return _dmon_make_id(0); + } + + int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches; + int index = _dmon.freelist[num_freelist - 1]; + uint32_t id = (uint32_t)(index + 1); + + if (_dmon.watches[index] == NULL) { + dmon__watch_state* state = (dmon__watch_state*)DMON_MALLOC(sizeof(dmon__watch_state)); + DMON_ASSERT(state); + if (state == NULL) { + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return _dmon_make_id(0); + } + memset(state, 0x0, sizeof(dmon__watch_state)); + _dmon.watches[index] = state; + } + + ++_dmon.num_watches; + + dmon__watch_state* watch = _dmon.watches[index]; + watch->id = _dmon_make_id(id); + watch->watch_flags = flags; + watch->watch_cb = watch_cb; + watch->user_data = user_data; + + _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + _dmon_unixpath(watch->rootdir, sizeof(watch->rootdir), rootdir); + size_t rootdir_len = strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + _DMON_WINAPI_STR(rootdir, DMON_MAX_PATH); + watch->dir_handle = + CreateFile(_rootdir, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL); + if (watch->dir_handle != INVALID_HANDLE_VALUE) { + watch->notify_filter = FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_SIZE; + watch->overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + DMON_ASSERT(watch->overlapped.hEvent != INVALID_HANDLE_VALUE); + + if (!_dmon_refresh_watch(watch)) { + _dmon_unwatch(watch); + DMON_LOG_ERROR("ReadDirectoryChanges failed"); + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return _dmon_make_id(0); + } + } else { + _DMON_LOG_ERRORF("Could not open: %s", rootdir); + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return _dmon_make_id(0); + } + + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return _dmon_make_id(id); +} + +DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) +{ + DMON_ASSERT(_dmon_init); + DMON_ASSERT(id.id > 0); + int index = id.id - 1; + DMON_ASSERT(index < DMON_MAX_WATCHES); + DMON_ASSERT(_dmon.watches[index]); + DMON_ASSERT(_dmon.num_watches > 0); + + if (_dmon.watches[index]) { + _InterlockedExchange(&_dmon.modify_watches, 1); + EnterCriticalSection(&_dmon.mutex); + + _dmon_unwatch(_dmon.watches[index]); + DMON_FREE(_dmon.watches[index]); + _dmon.watches[index] = NULL; + + --_dmon.num_watches; + int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches; + _dmon.freelist[num_freelist - 1] = index; + + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + } +} + +#elif DMON_OS_LINUX +// --------------------------------------------------------------------------------------------------------------------- +// @Linux +// inotify linux backend +#define _DMON_TEMP_BUFFSIZE ((sizeof(struct inotify_event) + PATH_MAX) * 1024) + +typedef struct dmon__watch_subdir { + char rootdir[DMON_MAX_PATH]; +} dmon__watch_subdir; + +typedef struct dmon__inotify_event { + char filepath[DMON_MAX_PATH]; + uint32_t mask; + uint32_t cookie; + dmon_watch_id watch_id; + bool skip; +} dmon__inotify_event; + +typedef struct dmon__watch_state { + dmon_watch_id id; + int fd; + uint32_t watch_flags; + _dmon_watch_cb* watch_cb; + void* user_data; + char rootdir[DMON_MAX_PATH]; + dmon__watch_subdir* subdirs; + int* wds; +} dmon__watch_state; + +typedef struct dmon__state { + dmon__watch_state* watches[DMON_MAX_WATCHES]; + int freelist[DMON_MAX_WATCHES]; + dmon__inotify_event* events; + int num_watches; + pthread_t thread_handle; + pthread_mutex_t mutex; + bool quit; +} dmon__state; + +static bool _dmon_init; +static dmon__state _dmon; + +_DMON_PRIVATE void _dmon_watch_recursive(const char* dirname, int fd, uint32_t mask, + bool followlinks, dmon__watch_state* watch) +{ + struct dirent* entry; + DIR* dir = opendir(dirname); + DMON_ASSERT(dir); + + char watchdir[DMON_MAX_PATH]; + + while ((entry = readdir(dir)) != NULL) { + bool entry_valid = false; + if (entry->d_type == DT_DIR) { + if (strcmp(entry->d_name, "..") != 0 && strcmp(entry->d_name, ".") != 0) { + _dmon_strcpy(watchdir, sizeof(watchdir), dirname); + _dmon_strcat(watchdir, sizeof(watchdir), entry->d_name); + entry_valid = true; + } + } else if (followlinks && entry->d_type == DT_LNK) { + char linkpath[PATH_MAX]; + _dmon_strcpy(watchdir, sizeof(watchdir), dirname); + _dmon_strcat(watchdir, sizeof(watchdir), entry->d_name); + char* r = realpath(watchdir, linkpath); + _DMON_UNUSED(r); + DMON_ASSERT(r); + _dmon_strcpy(watchdir, sizeof(watchdir), linkpath); + entry_valid = true; + } + + // add sub-directory to watch dirs + if (entry_valid) { + int watchdir_len = (int)strlen(watchdir); + if (watchdir[watchdir_len - 1] != '/') { + watchdir[watchdir_len] = '/'; + watchdir[watchdir_len + 1] = '\0'; + } + int wd = inotify_add_watch(fd, watchdir, mask); + _DMON_UNUSED(wd); + DMON_ASSERT(wd != -1); + + dmon__watch_subdir subdir; + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // recurse + _dmon_watch_recursive(watchdir, fd, mask, followlinks, watch); + } + } + closedir(dir); +} + +_DMON_PRIVATE const char* _dmon_find_subdir(const dmon__watch_state* watch, int wd) +{ + const int* wds = watch->wds; + int i, c; + for (i = 0, c = stb_sb_count(wds); i < c; i++) { + if (wd == wds[i]) { + return watch->subdirs[i].rootdir; + } + } + + return NULL; +} + +_DMON_PRIVATE void _dmon_gather_recursive(dmon__watch_state* watch, const char* dirname) +{ + struct dirent* entry; + DIR* dir = opendir(dirname); + DMON_ASSERT(dir); + + char newdir[DMON_MAX_PATH]; + while ((entry = readdir(dir)) != NULL) { + bool entry_valid = false; + bool is_dir = false; + if (strcmp(entry->d_name, "..") != 0 && strcmp(entry->d_name, ".") != 0) { + _dmon_strcpy(newdir, sizeof(newdir), dirname); + _dmon_strcat(newdir, sizeof(newdir), entry->d_name); + is_dir = (entry->d_type == DT_DIR); + entry_valid = true; + } + + // add sub-directory to watch dirs + if (entry_valid) { + dmon__watch_subdir subdir; + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), newdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), newdir + strlen(watch->rootdir)); + } + + dmon__inotify_event dev = { { 0 }, IN_CREATE|(is_dir ? IN_ISDIR : 0U), 0, watch->id, false }; + _dmon_strcpy(dev.filepath, sizeof(dev.filepath), subdir.rootdir); + stb_sb_push(_dmon.events, dev); + } + } + closedir(dir); +} + +_DMON_PRIVATE void _dmon_inotify_process_events(void) +{ + int i, c; + for (i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__inotify_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + + // remove redundant modify events on a single file + if (ev->mask & IN_MODIFY) { + int j; + for (j = i + 1; j < c; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { + ev->skip = true; + break; + } else if ((ev->mask & IN_ISDIR) && (check_ev->mask & (IN_ISDIR|IN_MODIFY))) { + // in some cases, particularly when created files under sub directories + // there can be two modify events for a single subdir one with trailing slash and one without + // remove trailing slash from both cases and test + int l1 = (int)strlen(ev->filepath); + int l2 = (int)strlen(check_ev->filepath); + if (ev->filepath[l1-1] == '/') ev->filepath[l1-1] = '\0'; + if (check_ev->filepath[l2-1] == '/') check_ev->filepath[l2-1] = '\0'; + if (strcmp(ev->filepath, check_ev->filepath) == 0) { + ev->skip = true; + break; + } + } + } + } else if (ev->mask & IN_CREATE) { + int j; + bool loop_break = false; + for (j = i + 1; j < c && !loop_break; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if ((check_ev->mask & IN_MOVED_FROM) && strcmp(ev->filepath, check_ev->filepath) == 0) { + // there is a case where some programs (like gedit): + // when we save, it creates a temp file, and moves it to the file being modified + // search for these cases and remove all of them + int k; + for (k = j + 1; k < c; k++) { + dmon__inotify_event* third_ev = &_dmon.events[k]; + if (third_ev->mask & IN_MOVED_TO && check_ev->cookie == third_ev->cookie) { + third_ev->mask = IN_MODIFY; // change to modified + ev->skip = check_ev->skip = true; + loop_break = true; + break; + } + } + } else if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { + // Another case is that file is copied. CREATE and MODIFY happens sequentially + // so we ignore MODIFY event + check_ev->skip = true; + } + } + } else if (ev->mask & IN_MOVED_FROM) { + bool move_valid = false; + int j; + for (j = i + 1; j < c; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) { + move_valid = true; + break; + } + } + + // in some environments like nautilus file explorer: + // when a file is deleted, it is moved to recycle bin + // so if the destination of the move is not valid, it's probably DELETE + if (!move_valid) { + ev->mask = IN_DELETE; + } + } else if (ev->mask & IN_MOVED_TO) { + bool move_valid = false; + int j; + for (j = 0; j < i; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if (check_ev->mask & IN_MOVED_FROM && ev->cookie == check_ev->cookie) { + move_valid = true; + break; + } + } + + // in some environments like nautilus file explorer: + // when a file is deleted, it is moved to recycle bin, on undo it is moved back it + // so if the destination of the move is not valid, it's probably CREATE + if (!move_valid) { + ev->mask = IN_CREATE; + } + } else if (ev->mask & IN_DELETE) { + int j; + for (j = i + 1; j < c; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + // if the file is DELETED and then MODIFIED after, just ignore the modify event + if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { + check_ev->skip = true; + break; + } + } + } + } + + // trigger user callbacks + for (i = 0; i < stb_sb_count(_dmon.events); i++) { + dmon__inotify_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + dmon__watch_state* watch = _dmon.watches[ev->watch_id.id - 1]; + + if(watch == NULL || watch->watch_cb == NULL) { + continue; + } + + if (ev->mask & IN_CREATE) { + if (ev->mask & IN_ISDIR) { + if (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) { + char watchdir[DMON_MAX_PATH]; + _dmon_strcpy(watchdir, sizeof(watchdir), watch->rootdir); + _dmon_strcat(watchdir, sizeof(watchdir), ev->filepath); + _dmon_strcat(watchdir, sizeof(watchdir), "/"); + uint32_t mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + int wd = inotify_add_watch(watch->fd, watchdir, mask); + _DMON_UNUSED(wd); + DMON_ASSERT(wd != -1); + + dmon__watch_subdir subdir; + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // some directories may be already created, for instance, with the command: mkdir -p + // so we will enumerate them manually and add them to the events + _dmon_gather_recursive(watch, watchdir); + ev = &_dmon.events[i]; // gotta refresh the pointer because it may be relocated + } + } + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data); + } + else if (ev->mask & IN_MODIFY) { + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, watch->user_data); + } + else if (ev->mask & IN_MOVED_FROM) { + int j; + for (j = i + 1; j < stb_sb_count(_dmon.events); j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } + else if (ev->mask & IN_DELETE) { + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, watch->user_data); + } + } + + stb_sb_reset(_dmon.events); +} + +static void* _dmon_thread(void* arg) +{ + _DMON_UNUSED(arg); + + static uint8_t buff[_DMON_TEMP_BUFFSIZE]; + struct timespec req = { (time_t)DMON_SLEEP_INTERVAL / 1000, (long)(DMON_SLEEP_INTERVAL * 1000000) }; + struct timespec rem = { 0, 0 }; + struct timeval timeout; + uint64_t usecs_elapsed = 0; + + struct timeval starttm; + gettimeofday(&starttm, 0); + + while (!_dmon.quit) { + nanosleep(&req, &rem); + if (_dmon.num_watches == 0 || pthread_mutex_trylock(&_dmon.mutex) != 0) { + continue; + } + + // Create read FD set + fd_set rfds; + FD_ZERO(&rfds); + { + int i; + for (i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = _dmon.watches[i]; + FD_SET(watch->fd, &rfds); + } + } + + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + if (select(FD_SETSIZE, &rfds, NULL, NULL, &timeout)) { + int i; + for (i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = _dmon.watches[i]; + if (FD_ISSET(watch->fd, &rfds)) { + ssize_t offset = 0; + ssize_t len = read(watch->fd, buff, _DMON_TEMP_BUFFSIZE); + if (len <= 0) { + continue; + } + + while (offset < len) { + struct inotify_event* iev = (struct inotify_event*)&buff[offset]; + + const char *subdir = _dmon_find_subdir(watch, iev->wd); + if (subdir) { + char filepath[DMON_MAX_PATH]; + _dmon_strcpy(filepath, sizeof(filepath), subdir); + _dmon_strcat(filepath, sizeof(filepath), iev->name); + + // TODO: ignore directories if flag is set + + if (stb_sb_count(_dmon.events) == 0) { + usecs_elapsed = 0; + } + dmon__inotify_event dev = { { 0 }, iev->mask, iev->cookie, watch->id, false }; + _dmon_strcpy(dev.filepath, sizeof(dev.filepath), filepath); + stb_sb_push(_dmon.events, dev); + } + + offset += sizeof(struct inotify_event) + iev->len; + } + } + } + } + + struct timeval tm; + gettimeofday(&tm, 0); + long dt = (tm.tv_sec - starttm.tv_sec) * 1000000 + tm.tv_usec - starttm.tv_usec; + starttm = tm; + usecs_elapsed += dt; + if (usecs_elapsed > 100000 && stb_sb_count(_dmon.events) > 0) { + _dmon_inotify_process_events(); + usecs_elapsed = 0; + } + + pthread_mutex_unlock(&_dmon.mutex); + } + return 0x0; +} + +_DMON_PRIVATE void _dmon_unwatch(dmon__watch_state* watch) +{ + close(watch->fd); + stb_sb_free(watch->subdirs); + stb_sb_free(watch->wds); +} + +DMON_API_IMPL void dmon_init(void) +{ + DMON_ASSERT(!_dmon_init); + pthread_mutex_init(&_dmon.mutex, NULL); + + int r = pthread_create(&_dmon.thread_handle, NULL, _dmon_thread, NULL); + _DMON_UNUSED(r); + DMON_ASSERT(r == 0 && "pthread_create failed"); + + for (int i = 0; i < DMON_MAX_WATCHES; i++) + _dmon.freelist[i] = DMON_MAX_WATCHES - i - 1; + + _dmon_init = true; +} + +DMON_API_IMPL void dmon_deinit(void) +{ + DMON_ASSERT(_dmon_init); + _dmon.quit = true; + pthread_join(_dmon.thread_handle, NULL); + + { + int i; + for (i = 0; i < _dmon.num_watches; i++) { + if (_dmon.watches[i]) { + _dmon_unwatch(_dmon.watches[i]); + DMON_FREE(_dmon.watches[i]); + } + } + } + + pthread_mutex_destroy(&_dmon.mutex); + stb_sb_free(_dmon.events); + memset(&_dmon, 0x0, sizeof(_dmon)); + _dmon_init = false; +} + +DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* dirname, const char* filename, + const char* oldname, void* user), + uint32_t flags, void* user_data) +{ + DMON_ASSERT(_dmon_init); + DMON_ASSERT(watch_cb); + DMON_ASSERT(rootdir && rootdir[0]); + + pthread_mutex_lock(&_dmon.mutex); + + DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); + if (_dmon.num_watches >= DMON_MAX_WATCHES) { + DMON_LOG_ERROR("Exceeding maximum number of watches"); + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + + int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches; + int index = _dmon.freelist[num_freelist - 1]; + uint32_t id = (uint32_t)(index + 1); + + if (_dmon.watches[index] == NULL) { + dmon__watch_state* state = (dmon__watch_state*)DMON_MALLOC(sizeof(dmon__watch_state)); + DMON_ASSERT(state); + if (state == NULL) { + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + memset(state, 0x0, sizeof(dmon__watch_state)); + _dmon.watches[index] = state; + } + + ++_dmon.num_watches; + + dmon__watch_state* watch = _dmon.watches[index]; + DMON_ASSERT(watch); + watch->id = _dmon_make_id(id); + watch->watch_flags = flags; + watch->watch_cb = watch_cb; + watch->user_data = user_data; + + struct stat root_st; + if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || (root_st.st_mode & S_IRUSR) != S_IRUSR) { + _DMON_LOG_ERRORF("Could not open/read directory: %s", rootdir); + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + + if (S_ISLNK(root_st.st_mode)) { + if (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) { + char linkpath[PATH_MAX]; + char* r = realpath(rootdir, linkpath); + _DMON_UNUSED(r); + DMON_ASSERT(r); + + _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath); + } else { + _DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", + rootdir); + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + } else { + _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + } + + // add trailing slash + int rootdir_len = (int)strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + watch->fd = inotify_init(); + if (watch->fd < -1) { + DMON_LOG_ERROR("could not create inotify instance"); + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + + uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + int wd = inotify_add_watch(watch->fd, watch->rootdir, inotify_mask); + if (wd < 0) { + _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watch->rootdir, errno); + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + dmon__watch_subdir subdir; + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), ""); // root dir is just a dummy entry + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // recursive mode: enumerate all child directories and add them to watch + if (flags & DMON_WATCHFLAGS_RECURSIVE) { + _dmon_watch_recursive(watch->rootdir, watch->fd, inotify_mask, + (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) ? true : false, watch); + } + + + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(id); +} + +DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) +{ + DMON_ASSERT(_dmon_init); + DMON_ASSERT(id.id > 0); + int index = id.id - 1; + DMON_ASSERT(index < DMON_MAX_WATCHES); + DMON_ASSERT(_dmon.watches[index]); + DMON_ASSERT(_dmon.num_watches > 0); + + if (_dmon.watches[index]) { + pthread_mutex_lock(&_dmon.mutex); + + _dmon_unwatch(_dmon.watches[index]); + DMON_FREE(_dmon.watches[index]); + _dmon.watches[index] = NULL; + + --_dmon.num_watches; + int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches; + _dmon.freelist[num_freelist - 1] = index; + + pthread_mutex_unlock(&_dmon.mutex); + } +} +#elif DMON_OS_MACOS +// --------------------------------------------------------------------------------------------------------------------- +// @MacOS +// FSEvents MacOS backend +typedef struct dmon__fsevent_event { + char filepath[DMON_MAX_PATH]; + uint64_t event_id; + long event_flags; + dmon_watch_id watch_id; + bool skip; + bool move_valid; +} dmon__fsevent_event; + +typedef struct dmon__watch_state { + dmon_watch_id id; + uint32_t watch_flags; + FSEventStreamRef fsev_stream_ref; + _dmon_watch_cb* watch_cb; + void* user_data; + char rootdir[DMON_MAX_PATH]; + char rootdir_unmod[DMON_MAX_PATH]; + bool init; +} dmon__watch_state; + +typedef struct dmon__state { + dmon__watch_state* watches[DMON_MAX_WATCHES]; + int freelist[DMON_MAX_WATCHES]; + dmon__fsevent_event* events; + int num_watches; + volatile int modify_watches; + pthread_t thread_handle; + dispatch_semaphore_t thread_sem; + pthread_mutex_t mutex; + CFRunLoopRef cf_loop_ref; + CFAllocatorRef cf_alloc_ref; + bool quit; +} dmon__state; + +union dmon__cast_userdata { + void* ptr; + uint32_t id; +}; + +static bool _dmon_init; +static dmon__state _dmon; + +_DMON_PRIVATE void* _dmon_cf_malloc(CFIndex size, CFOptionFlags hints, void* info) +{ + _DMON_UNUSED(hints); + _DMON_UNUSED(info); + return DMON_MALLOC(size); +} + +_DMON_PRIVATE void _dmon_cf_free(void* ptr, void* info) +{ + _DMON_UNUSED(info); + DMON_FREE(ptr); +} + +_DMON_PRIVATE void* _dmon_cf_realloc(void* ptr, CFIndex newsize, CFOptionFlags hints, void* info) +{ + _DMON_UNUSED(hints); + _DMON_UNUSED(info); + return DMON_REALLOC(ptr, (size_t)newsize); +} + +_DMON_PRIVATE void _dmon_fsevent_process_events(void) +{ + int i, c; + for (i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__fsevent_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + + // remove redundant modify events on a single file + if (ev->event_flags & kFSEventStreamEventFlagItemModified) { + int j; + for (j = i + 1; j < c; j++) { + dmon__fsevent_event* check_ev = &_dmon.events[j]; + if ((check_ev->event_flags & kFSEventStreamEventFlagItemModified) && + strcmp(ev->filepath, check_ev->filepath) == 0) { + ev->skip = true; + break; + } + } + } else if ((ev->event_flags & kFSEventStreamEventFlagItemRenamed) && !ev->move_valid) { + int j; + for (j = i + 1; j < c; j++) { + dmon__fsevent_event* check_ev = &_dmon.events[j]; + if ((check_ev->event_flags & kFSEventStreamEventFlagItemRenamed) && + check_ev->event_id == (ev->event_id + 1)) { + ev->move_valid = check_ev->move_valid = true; + break; + } + } + + // in some environments like finder file explorer: + // when a file is deleted, it is moved to recycle bin + // so if the destination of the move is not valid, it's probably DELETE or CREATE + // decide CREATE if file exists + if (!ev->move_valid) { + ev->event_flags &= ~kFSEventStreamEventFlagItemRenamed; + + char abs_filepath[DMON_MAX_PATH]; + dmon__watch_state* watch = _dmon.watches[ev->watch_id.id-1]; + _dmon_strcpy(abs_filepath, sizeof(abs_filepath), watch->rootdir); + _dmon_strcat(abs_filepath, sizeof(abs_filepath), ev->filepath); + + struct stat root_st; + if (stat(abs_filepath, &root_st) != 0) { + ev->event_flags |= kFSEventStreamEventFlagItemRemoved; + } else { + ev->event_flags |= kFSEventStreamEventFlagItemCreated; + } + } + } + } + + // trigger user callbacks + for (i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__fsevent_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + dmon__watch_state* watch = _dmon.watches[ev->watch_id.id - 1]; + + if(watch == NULL || watch->watch_cb == NULL) { + continue; + } + + if (ev->event_flags & kFSEventStreamEventFlagItemCreated) { + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir_unmod, ev->filepath, NULL, + watch->user_data); + } + + if (ev->event_flags & kFSEventStreamEventFlagItemModified) { + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir_unmod, ev->filepath, NULL, watch->user_data); + } else if (ev->event_flags & kFSEventStreamEventFlagItemRenamed) { + int j; + for (j = i + 1; j < c; j++) { + dmon__fsevent_event* check_ev = &_dmon.events[j]; + if (check_ev->event_flags & kFSEventStreamEventFlagItemRenamed) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir_unmod, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } else if (ev->event_flags & kFSEventStreamEventFlagItemRemoved) { + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir_unmod, ev->filepath, NULL, + watch->user_data); + } + } + + stb_sb_reset(_dmon.events); +} + +_DMON_PRIVATE void* _dmon_thread(void* arg) +{ + _DMON_UNUSED(arg); + + struct timespec req = { (time_t)DMON_SLEEP_INTERVAL / 1000, (long)(DMON_SLEEP_INTERVAL * 1000000) }; + struct timespec rem = { 0, 0 }; + + _dmon.cf_loop_ref = CFRunLoopGetCurrent(); + dispatch_semaphore_signal(_dmon.thread_sem); + + while (!_dmon.quit) { + int i; + if (_dmon.modify_watches || pthread_mutex_trylock(&_dmon.mutex) != 0) { + nanosleep(&req, &rem); + continue; + } + + if (_dmon.num_watches == 0) { + nanosleep(&req, &rem); + pthread_mutex_unlock(&_dmon.mutex); + continue; + } + + for (i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = _dmon.watches[i]; + if (!watch->init) { + DMON_ASSERT(watch->fsev_stream_ref); + FSEventStreamScheduleWithRunLoop(watch->fsev_stream_ref, _dmon.cf_loop_ref, kCFRunLoopDefaultMode); + FSEventStreamStart(watch->fsev_stream_ref); + + watch->init = true; + } + } + + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, kCFRunLoopRunTimedOut); + _dmon_fsevent_process_events(); + + pthread_mutex_unlock(&_dmon.mutex); + } + + CFRunLoopStop(_dmon.cf_loop_ref); + _dmon.cf_loop_ref = NULL; + return 0x0; +} + +_DMON_PRIVATE void _dmon_unwatch(dmon__watch_state* watch) +{ + if (watch->fsev_stream_ref) { + FSEventStreamStop(watch->fsev_stream_ref); + FSEventStreamInvalidate(watch->fsev_stream_ref); + FSEventStreamRelease(watch->fsev_stream_ref); + watch->fsev_stream_ref = NULL; + } +} + +DMON_API_IMPL void dmon_init(void) +{ + DMON_ASSERT(!_dmon_init); + pthread_mutex_init(&_dmon.mutex, NULL); + + CFAllocatorContext cf_alloc_ctx = { 0 }; + cf_alloc_ctx.allocate = _dmon_cf_malloc; + cf_alloc_ctx.deallocate = _dmon_cf_free; + cf_alloc_ctx.reallocate = _dmon_cf_realloc; + _dmon.cf_alloc_ref = CFAllocatorCreate(NULL, &cf_alloc_ctx); + + _dmon.thread_sem = dispatch_semaphore_create(0); + DMON_ASSERT(_dmon.thread_sem); + + int r = pthread_create(&_dmon.thread_handle, NULL, _dmon_thread, NULL); + _DMON_UNUSED(r); + DMON_ASSERT(r == 0 && "pthread_create failed"); + + // wait for thread to initialize loop object + dispatch_semaphore_wait(_dmon.thread_sem, DISPATCH_TIME_FOREVER); + + for (int i = 0; i < DMON_MAX_WATCHES; i++) + _dmon.freelist[i] = DMON_MAX_WATCHES - i - 1; + + _dmon_init = true; +} + +DMON_API_IMPL void dmon_deinit(void) +{ + DMON_ASSERT(_dmon_init); + _dmon.quit = true; + pthread_join(_dmon.thread_handle, NULL); + + dispatch_release(_dmon.thread_sem); + + { + int i; + for (i = 0; i < _dmon.num_watches; i++) { + if (_dmon.watches[i]) { + _dmon_unwatch(_dmon.watches[i]); + DMON_FREE(_dmon.watches[i]); + } + } + } + + pthread_mutex_destroy(&_dmon.mutex); + stb_sb_free(_dmon.events); + if (_dmon.cf_alloc_ref) + CFRelease(_dmon.cf_alloc_ref); + + memset(&_dmon, 0x0, sizeof(_dmon)); + _dmon_init = false; +} + +_DMON_PRIVATE void _dmon_fsevent_callback(ConstFSEventStreamRef stream_ref, void* user_data, + size_t num_events, void* event_paths, + const FSEventStreamEventFlags event_flags[], + const FSEventStreamEventId event_ids[]) +{ + _DMON_UNUSED(stream_ref); + + union dmon__cast_userdata _userdata; + _userdata.ptr = user_data; + dmon_watch_id watch_id = _dmon_make_id(_userdata.id); + DMON_ASSERT(watch_id.id > 0); + dmon__watch_state* watch = _dmon.watches[watch_id.id - 1]; + char abs_filepath[DMON_MAX_PATH]; + char abs_filepath_lower[DMON_MAX_PATH]; + + { + size_t i; + for (i = 0; i < num_events; i++) { + const char *filepath = ((const char **) event_paths)[i]; + long flags = (long) event_flags[i]; + uint64_t event_id = (uint64_t) event_ids[i]; + dmon__fsevent_event ev; + memset(&ev, 0x0, sizeof(ev)); + + _dmon_strcpy(abs_filepath, sizeof(abs_filepath), filepath); + _dmon_unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath); + + // normalize path, so it would be the same on both MacOS file-system types (case/nocase) + _dmon_tolower(abs_filepath_lower, sizeof(abs_filepath), abs_filepath); + DMON_ASSERT(strstr(abs_filepath_lower, watch->rootdir) == abs_filepath_lower); + + // strip the root dir from the beginning + _dmon_strcpy(ev.filepath, sizeof(ev.filepath), abs_filepath + strlen(watch->rootdir)); + + ev.event_flags = flags; + ev.event_id = event_id; + ev.watch_id = watch_id; + stb_sb_push(_dmon.events, ev); + } + } +} + +DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* dirname, const char* filename, + const char* oldname, void* user), + uint32_t flags, void* user_data) +{ + DMON_ASSERT(_dmon_init); + DMON_ASSERT(watch_cb); + DMON_ASSERT(rootdir && rootdir[0]); + + __sync_lock_test_and_set(&_dmon.modify_watches, 1); + pthread_mutex_lock(&_dmon.mutex); + + DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); + if (_dmon.num_watches >= DMON_MAX_WATCHES) { + DMON_LOG_ERROR("Exceeding maximum number of watches"); + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + + + int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches; + int index = _dmon.freelist[num_freelist - 1]; + uint32_t id = (uint32_t)(index + 1); + + if (_dmon.watches[index] == NULL) { + dmon__watch_state* state = (dmon__watch_state*)DMON_MALLOC(sizeof(dmon__watch_state)); + DMON_ASSERT(state); + if (state == NULL) { + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + memset(state, 0x0, sizeof(dmon__watch_state)); + _dmon.watches[index] = state; + } + + ++_dmon.num_watches; + + dmon__watch_state* watch = _dmon.watches[id - 1]; + DMON_ASSERT(watch); + watch->id = _dmon_make_id(id); + watch->watch_flags = flags; + watch->watch_cb = watch_cb; + watch->user_data = user_data; + + struct stat root_st; + if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || + (root_st.st_mode & S_IRUSR) != S_IRUSR) { + _DMON_LOG_ERRORF("Could not open/read directory: %s", rootdir); + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return _dmon_make_id(0); + } + + if (S_ISLNK(root_st.st_mode)) { + if (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) { + char linkpath[PATH_MAX]; + char* r = realpath(rootdir, linkpath); + _DMON_UNUSED(r); + DMON_ASSERT(r); + + _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath); + } else { + _DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", rootdir); + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return _dmon_make_id(0); + } + } else { + char rootdir_abspath[DMON_MAX_PATH]; + if (realpath(rootdir, rootdir_abspath) != NULL) { + _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir_abspath); + } else { + _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + } + } + + _dmon_unixpath(watch->rootdir, sizeof(watch->rootdir), watch->rootdir); + + // add trailing slash + int rootdir_len = (int)strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + _dmon_strcpy(watch->rootdir_unmod, sizeof(watch->rootdir_unmod), watch->rootdir); + _dmon_tolower(watch->rootdir, sizeof(watch->rootdir), watch->rootdir); + + // create FS objects + CFStringRef cf_dir = CFStringCreateWithCString(NULL, watch->rootdir_unmod, kCFStringEncodingUTF8); + CFArrayRef cf_dirarr = CFArrayCreate(NULL, (const void**)&cf_dir, 1, NULL); + + FSEventStreamContext ctx; + union dmon__cast_userdata userdata; + userdata.id = id; + ctx.version = 0; + ctx.info = userdata.ptr; + ctx.retain = NULL; + ctx.release = NULL; + ctx.copyDescription = NULL; + watch->fsev_stream_ref = FSEventStreamCreate(_dmon.cf_alloc_ref, _dmon_fsevent_callback, &ctx, + cf_dirarr, kFSEventStreamEventIdSinceNow, 0.25, + kFSEventStreamCreateFlagFileEvents); + + + CFRelease(cf_dirarr); + CFRelease(cf_dir); + + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return _dmon_make_id(id); +} + +DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) +{ + DMON_ASSERT(_dmon_init); + DMON_ASSERT(id.id > 0); + int index = id.id - 1; + DMON_ASSERT(index < DMON_MAX_WATCHES); + DMON_ASSERT(_dmon.watches[index]); + DMON_ASSERT(_dmon.num_watches > 0); + + if (_dmon.watches[index]) { + __sync_lock_test_and_set(&_dmon.modify_watches, 1); + pthread_mutex_lock(&_dmon.mutex); + + _dmon_unwatch(_dmon.watches[index]); + DMON_FREE(_dmon.watches[index]); + _dmon.watches[index] = NULL; + + --_dmon.num_watches; + int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches; + _dmon.freelist[num_freelist - 1] = index; + + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + } +} + +#endif + +#endif // DMON_IMPL +#endif // __DMON_H__ diff --git a/third-party/dmon/dmon_extra.h b/third-party/dmon/dmon_extra.h new file mode 100644 index 0000000..7c1211d --- /dev/null +++ b/third-party/dmon/dmon_extra.h @@ -0,0 +1,164 @@ +#ifndef __DMON_EXTRA_H__ +#define __DMON_EXTRA_H__ + +// +// Copyright 2023 Sepehr Taghdisian (septag@github). All rights reserved. +// License: https://github.com/septag/dmon#license-bsd-2-clause +// +// Extra header functionality for dmon.h for the backend based on inotify +// +// Add/Remove directory functions: +// dmon_watch_add: Adds a sub-directory to already valid watch_id. sub-directories are assumed to be relative to watch root_dir +// dmon_watch_add: Removes a sub-directory from already valid watch_id. sub-directories are assumed to be relative to watch root_dir +// Reason: The inotify backend does not work well when watching in recursive mode a root directory of a large tree, it may take +// quite a while until all inotify watches are established, and events will not be received in this time. Also, since one +// inotify watch will be established per subdirectory, it is possible that the maximum amount of inotify watches per user +// will be reached. The default maximum is 8192. +// When using inotify backend users may turn off the DMON_WATCHFLAGS_RECURSIVE flag and add/remove selectively the +// sub-directories to be watched based on application-specific logic about which sub-directory actually needs to be watched. +// The function dmon_watch_add and dmon_watch_rm are used to this purpose. +// + +#ifndef __DMON_H__ +#error "Include 'dmon.h' before including this file" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +DMON_API_DECL bool dmon_watch_add(dmon_watch_id id, const char* subdir); +DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir); + +#ifdef __cplusplus +} +#endif + +#ifdef DMON_IMPL +#if DMON_OS_LINUX +DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir) +{ + DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); + + bool skip_lock = pthread_self() == _dmon.thread_handle; + + if (!skip_lock) + pthread_mutex_lock(&_dmon.mutex); + + dmon__watch_state* watch = _dmon.watches[id.id - 1]; + + int dirlen, i, c; + + // check if the directory exists + // if watchdir contains absolute/root-included path, try to strip the rootdir from it + // else, we assume that watchdir is correct, so save it as it is + struct stat st; + dmon__watch_subdir subdir; + if (stat(watchdir, &st) == 0 && (st.st_mode & S_IFDIR)) { + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); + } + } else { + char fullpath[DMON_MAX_PATH]; + _dmon_strcpy(fullpath, sizeof(fullpath), watch->rootdir); + _dmon_strcat(fullpath, sizeof(fullpath), watchdir); + if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) { + _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + } + + dirlen = (int)strlen(subdir.rootdir); + if (subdir.rootdir[dirlen - 1] != '/') { + subdir.rootdir[dirlen] = '/'; + subdir.rootdir[dirlen + 1] = '\0'; + } + + // check that the directory is not already added + for (i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) { + if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) { + _DMON_LOG_ERRORF("Error watching directory '%s', because it is already added.", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + } + + const uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + char fullpath[DMON_MAX_PATH]; + _dmon_strcpy(fullpath, sizeof(fullpath), watch->rootdir); + _dmon_strcat(fullpath, sizeof(fullpath), subdir.rootdir); + int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask); + if (wd == -1) { + _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watchdir, errno); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + + return true; +} + +DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir) +{ + DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); + + bool skip_lock = pthread_self() == _dmon.thread_handle; + + if (!skip_lock) + pthread_mutex_lock(&_dmon.mutex); + + dmon__watch_state* watch = _dmon.watches[id.id - 1]; + + char subdir[DMON_MAX_PATH]; + _dmon_strcpy(subdir, sizeof(subdir), watchdir); + if (strstr(subdir, watch->rootdir) == subdir) { + _dmon_strcpy(subdir, sizeof(subdir), watchdir + strlen(watch->rootdir)); + } + + int dirlen = (int)strlen(subdir); + if (subdir[dirlen - 1] != '/') { + subdir[dirlen] = '/'; + subdir[dirlen + 1] = '\0'; + } + + int i, c = stb_sb_count(watch->subdirs); + for (i = 0; i < c; i++) { + if (strcmp(watch->subdirs[i].rootdir, subdir) == 0) { + break; + } + } + if (i >= c) { + _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + inotify_rm_watch(watch->fd, watch->wds[i]); + + /* Remove entry from subdirs and wds by swapping position with the last entry */ + watch->subdirs[i] = stb_sb_last(watch->subdirs); + stb_sb_pop(watch->subdirs); + + watch->wds[i] = stb_sb_last(watch->wds); + stb_sb_pop(watch->wds); + + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return true; +} +#endif // DMON_OS_LINUX +#endif // DMON_IMPL + +#endif // __DMON_EXTRA_H__ + diff --git a/third-party/x-watcher/array.h b/third-party/x-watcher/array.h deleted file mode 100644 index 50849f1..0000000 --- a/third-party/x-watcher/array.h +++ /dev/null @@ -1,77 +0,0 @@ -#ifndef ARRAY_H -#define ARRAY_H - -#include -#include - -struct _ArrayHeader { - size_t count, capacity; -}; - -#define ARRAY_INITIAL_SIZE 8 - -#define _arr_header(a) ((struct _ArrayHeader*)(a) - 1) - -#define arr_init(a) arr_init_n((a), ARRAY_INITIAL_SIZE) - -#define arr_init_n(a, n) do { \ - struct _ArrayHeader *header; \ - header = malloc(sizeof(*header) + (sizeof(*(a)) * (n))); \ - header->count = 0; \ - header->capacity = (n); \ - (a) = (void*)(header + 1); \ -} while(0) - -#define arr_count(a) (_arr_header(a)->count) -#define arr_capacity(a) (_arr_header(a)->capacity) - -#define arr_back(a) ((a)[arr_count(a) - 1]) -#define arr_pop(a) ((a)[_arr_header(a)->count--]) - -#define arr_reserve(a, n) do { \ - if(n <= arr_capacity(a)) break; \ - struct _ArrayHeader *header = _arr_header(a); \ - header->capacity = n; \ - (a) = (void*)((struct _ArrayHeader*)realloc( \ - header, sizeof(*header) + (sizeof(*(a)) * (n))) + 1); \ -} while(0) - -#define arr_resize(a, n) do { \ - arr_reserve((a), (n)); \ - _arr_header(a)->count = n; \ -} while(0) - -#define arr_resize_zero(a, n) do { \ - size_t initial_count = arr_count(a); \ - arr_resize((a), (n)); \ - if(arr_count(a) > initial_count) \ - memset( \ - &(a)[initial_count], 0, \ - (arr_count(a) - initial_count) * sizeof(*a)); \ -} while(0) - -// Take a vararg list to support compound literals -#define arr_add(a, ...) do { \ - struct _ArrayHeader *header = _arr_header(a); \ - if(header->count == header->capacity) \ - arr_reserve((a), header->capacity << 1); \ - (a)[_arr_header(a)->count++] = (__VA_ARGS__); \ -} while(0) - -#define arr_free(a) do { \ - free(_arr_header(a)); \ -} while(0) - -#define arr_find(a, val, idx) do { \ - *idx = -1; \ - for(size_t i = 0; i < arr_count((a)); i++) { \ - if((a)[i] == val) { \ - *idx = i; \ - break; \ - } \ - } \ -} while(0) - -#endif - - diff --git a/third-party/x-watcher/x-watcher.h b/third-party/x-watcher/x-watcher.h deleted file mode 100644 index d04f802..0000000 --- a/third-party/x-watcher/x-watcher.h +++ /dev/null @@ -1,838 +0,0 @@ -#ifndef __X_WATCHER_H -#define __X_WATCHER_H - -// necessary includes for the public stuff -#include -#include -#if defined(__linux__) - // noop -#elif defined(__WIN32__) - #include - #include -#else - #error "Unsupported" -#endif - -// PUBLIC STUFF -typedef enum event { - XWATCHER_FILE_UNSPECIFIED, - XWATCHER_FILE_REMOVED, - XWATCHER_FILE_CREATED, - XWATCHER_FILE_MODIFIED, - XWATCHER_FILE_OPENED, - XWATCHER_FILE_ATTRIBUTES_CHANGED, - XWATCHER_FILE_NONE, - XWATCHER_FILE_RENAMED, - // probs more but i couldn't care much -} XWATCHER_FILE_EVENT; - -typedef struct xWatcher_reference { - char *path; - void (*callback_func)( - XWATCHER_FILE_EVENT event, - const char *path, - int context, - void *additional_data); - int context; - void *additional_data; -} xWatcher_reference; - -typedef struct xWatcher_file { - // just the file name alone - char *name; - // used for adding (additional) context in the handler (if needed) - int context; - // in case you'd like to avoid global variables - void *additional_data; - - void (*callback_func)( - XWATCHER_FILE_EVENT event, - const char *path, - int context, - void *additional_data); -} xWatcher_file; - -typedef struct xWatcher_directory { - // list of files - struct xWatcher_file *files; - - char *path; - // used for adding (additional) context in the handler (if needed) - int context; - // in case you'd like to avoid global variables - void *additional_data; - - void (*callback_func)( - XWATCHER_FILE_EVENT event, - const char *path, - int context, - void *additional_data); - - #if defined(__linux__) - // we need additional file descriptors (per directory basis) - int inotify_watch_fd; - #elif defined(__WIN32__) - HANDLE handle; - OVERLAPPED overlapped; - uint8_t *event_buffer; - #else - #error "Unsupported" - #endif -} xWatcher_directory; - -typedef struct x_watcher { - struct xWatcher_directory *directories; - pthread_t thread; - int thread_id; - bool alive; - - #if defined(__linux__) - int inotify_fd; // fd == file descriptor (a common UNIX thing) - #elif defined(__WIN32__) - // literal noop - #else - #error "Unsupported" - #endif -} x_watcher; - -// PRIVATE STUFF -#include -#include -#include - -#include "array.h" - -#ifdef __linux__ - #include - #include - #include - #include - #include // for POLLIN - #include // for O_NONBLOCK - - #define EVENT_SIZE (sizeof(struct inotify_event)) - #define BUF_LEN (1024 * (EVENT_SIZE + 16)) - #define DIRBRK '/' - - static inline void *__internal_xWatcherProcess(void *argument) { - x_watcher *watcher = (x_watcher*) argument; - char buffer[BUF_LEN]; - ssize_t lenght; - struct xWatcher_directory *directories = watcher->directories; - - while(watcher->alive) { - // poll for events - struct pollfd pfd = { watcher->inotify_fd, POLLIN, 0 }; - int ret = poll(&pfd, 1, 50); // timeout of 50ms - - if (ret < 0) { - // oops - fprintf(stderr, "poll failed: %s\n", strerror(errno)); - break; - } else if (ret == 0) { - // Timeout with no events, move on. - continue; - } - - // wait for the kernel to do it's thing - lenght = read(watcher->inotify_fd, buffer, BUF_LEN); - if(lenght < 0) { - // something messed up clearly - perror("read"); - return NULL; - } - - // pointer to the event structure - ssize_t i = 0; - while(i < lenght) { - // the event list itself - struct inotify_event *event = (struct inotify_event *) - &buffer[i]; - - // find directory for which this even matches via the descriptor - struct xWatcher_directory *directory = NULL; - for(size_t j = 0; j < arr_count(directories); j++) { - if(directories[j].inotify_watch_fd == event->wd) { - directory = &directories[j]; - } - } - if(directory == NULL) { - fprintf(stderr, - "MATCHING FILE DESCRIPTOR NOT FOUND! ERROR!\n"); - // BAIL???? - } - - // find matching file (if any) - struct xWatcher_file *file = NULL; - for(size_t j = 0; j < arr_count(directory->files); j++) { - if(strcmp(directory->files[j].name, event->name) == 0) { - file = &directory->files[j]; - } - } - - XWATCHER_FILE_EVENT send_event = XWATCHER_FILE_NONE; - - if(event->mask & IN_CREATE) - send_event = XWATCHER_FILE_CREATED; - if(event->mask & IN_MODIFY) - send_event = XWATCHER_FILE_MODIFIED; - if(event->mask & IN_DELETE) - send_event = XWATCHER_FILE_REMOVED; - if(event->mask & IN_CLOSE_WRITE || - event->mask & IN_CLOSE_NOWRITE) - send_event = XWATCHER_FILE_REMOVED; - if(event->mask & IN_ATTRIB) - send_event = XWATCHER_FILE_ATTRIBUTES_CHANGED; - if(event->mask & IN_OPEN) - send_event = XWATCHER_FILE_OPENED; - - // file found(?) - if(file != NULL) { - if(send_event != XWATCHER_FILE_NONE) { - // figure out the file path size - size_t filepath_size = strlen(directory->path); - filepath_size += strlen(file->name); - filepath_size += 2; - - // create file path string - char *filepath = (char*)malloc(filepath_size); - snprintf(filepath, filepath_size, "%s/%s", - directory->path, file->name); - - // callback - file->callback_func(send_event, - filepath, - file->context, - file->additional_data); - - // free that garbage - free(filepath); - } - } else { - // Cannot find file, lets try directory - if(directory->callback_func != NULL && - send_event != XWATCHER_FILE_NONE) { - directory->callback_func(send_event, - directory->path, - directory->context, - directory->additional_data); - } - } - - i += EVENT_SIZE + event->len; - } - } - - // cleanup time - for(size_t i = 0; i < arr_count(watcher->directories); i++) { - struct xWatcher_directory *directory = &watcher->directories[i]; - for(size_t j = 0; j < arr_count(directory->files); j++) { - struct xWatcher_file *file = &directory->files[j]; - free(file->name); - } - arr_free(directory->files); - free(directory->path); - inotify_rm_watch(watcher->inotify_fd, directory->inotify_watch_fd); - } - close(watcher->inotify_fd); - arr_free(watcher->directories); - // should we signify that the thread is dead? - return NULL; - } -#elif defined(__WIN32__) - #include - - #define BUF_LEN 1024 - #define DIRBRK '\\' - - static inline void *__internal_xWatcherProcess(void *argument) { - x_watcher *watcher = (x_watcher*) argument; - struct xWatcher_directory *directories = watcher->directories; - - // create an event list so we can still make use of the Windows API - HANDLE events[arr_count(directories)]; - for(int i = 0; i < arr_count(directories); i++) { - events[i] = directories[i].overlapped.hEvent; - } - - // obv first check if we need to stay alive - while(watcher->alive) { - // wait for any of the objects to respond - DWORD result = WaitForMultipleObjects(arr_count(directories), - events, FALSE, 50 /** timeout of 50ms **/); - - // test which object was it - int object_index = -1; - for(int i = 0; i < arr_count(directories); i++) { - if(result == (WAIT_OBJECT_0 + i)) { - object_index = i; - break; - } - } - - if(object_index == -1) { - if(result == WAIT_TIMEOUT) { - // it just timed out, let's continue - continue; - } else { - // RUNTIME ERROR! Let's bail - ExitProcess(GetLastError()); - } - } - - // shorhand for convenience - struct xWatcher_directory *dir = &directories[object_index]; - - // retrieve event data - DWORD bytes_transferred; - GetOverlappedResult(dir->handle, - &dir->overlapped, - &bytes_transferred, FALSE); - - // assign the data's pointer to a proper format for convenience - FILE_NOTIFY_INFORMATION *event = (FILE_NOTIFY_INFORMATION*) - dir->event_buffer; - - // loop through the data - for (;;) { - // figure out the wchar string size and allocate as needed - DWORD name_len = event->FileNameLength / sizeof(wchar_t); - char *name_char = malloc(sizeof(char)*(name_len+1)); - size_t converted_chars; - - // convert wchar* filename to char* - wcstombs_s(&converted_chars, name_char, - name_len+1, event->FileName, name_len); - - // convert to proper event type - XWATCHER_FILE_EVENT send_event = XWATCHER_FILE_NONE; - switch (event->Action) { - case FILE_ACTION_ADDED: - send_event = XWATCHER_FILE_CREATED; - break; - case FILE_ACTION_REMOVED: - send_event = XWATCHER_FILE_REMOVED; - break; - case FILE_ACTION_MODIFIED: - send_event = XWATCHER_FILE_MODIFIED; - break; - case FILE_ACTION_RENAMED_OLD_NAME: - case FILE_ACTION_RENAMED_NEW_NAME: - send_event = XWATCHER_FILE_RENAMED; - break; - default: - send_event = XWATCHER_FILE_UNSPECIFIED; - break; - } - - // find matching file (if any) - struct xWatcher_file *file = NULL; - for(size_t j = 0; j < arr_count(dir->files); j++) { - if(strcmp(dir->files[j].name, name_char) == 0) { - file = &dir->files[j]; - } - } - - // file found(?) - if(file != NULL) { - if(send_event != XWATCHER_FILE_NONE) { - // figure out the file path size - size_t filepath_size = strlen(dir->path); - filepath_size += strlen(file->name); - filepath_size += 2; - - // create file path string - char *filepath = (char*)malloc(filepath_size); - snprintf(filepath, filepath_size, "%s%c%s", - dir->path, DIRBRK, file->name); - - // callback - file->callback_func(send_event, - filepath, - file->context, - file->additional_data); - - // free that garbage - free(filepath); - } - } else { - // Cannot find file, lets try directory - if(dir->callback_func != NULL && - send_event != XWATCHER_FILE_NONE) { - dir->callback_func(send_event, - dir->path, - dir->context, - dir->additional_data); - } - } - - // free up the converted string - free(name_char); - - // Are there more events to handle? - if (event->NextEntryOffset) { - *((uint8_t**)&event) += event->NextEntryOffset; - } else { - break; - } - } - - DWORD dwNotifyFilter = - FILE_NOTIFY_CHANGE_FILE_NAME | - FILE_NOTIFY_CHANGE_DIR_NAME | - FILE_NOTIFY_CHANGE_ATTRIBUTES | - FILE_NOTIFY_CHANGE_SIZE | - FILE_NOTIFY_CHANGE_LAST_WRITE | - FILE_NOTIFY_CHANGE_LAST_ACCESS | - FILE_NOTIFY_CHANGE_CREATION | - FILE_NOTIFY_CHANGE_SECURITY; - BOOL recursive = FALSE; - - // Queue the next event - BOOL success = ReadDirectoryChangesW( - dir->handle, - dir->event_buffer, - BUF_LEN, - recursive, - dwNotifyFilter, - NULL, - &dir->overlapped, - NULL); - - if(!success) { - // get error code - DWORD error = GetLastError(); - char *message; - - // get error message - FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, - error, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPTSTR) &message, - 0, NULL); - - fprintf(stderr, "ReadDirectoryChangesW failed: %s\n", - message); - - ExitProcess(error); - } - } - // cleanup time - for(size_t i = 0; i < arr_count(watcher->directories); i++) { - struct xWatcher_directory *directory = &watcher->directories[i]; - for(size_t j = 0; j < arr_count(directory->files); j++) { - struct xWatcher_file *file = &directory->files[j]; - free(file->name); - } - arr_free(directory->files); - free(directory->path); - free(directory->event_buffer); - CloseHandle(directory->handle); - } - arr_free(watcher->directories); - - return NULL; - } -#else - #error "Unsupported" -#endif - - -static inline x_watcher *xWatcher_create(void) { - x_watcher *watcher = (x_watcher*)malloc(sizeof(x_watcher)); - - arr_init(watcher->directories); - - #if defined(__WIN32__) - // literally noop - #elif defined(__linux__) - watcher->inotify_fd = inotify_init1(O_NONBLOCK); - if(watcher->inotify_fd < 0) { - perror("inotify_init"); - return NULL; - } - #else - #error "Unsupported" - #endif - - return watcher; -} - -static inline bool xWatcher_appendFile( - x_watcher *watcher, - xWatcher_reference *reference) { - char *path = strdup(reference->path); - - // the file MUST NOT contain slashed at the end - if(path[strlen(path)-1] == DIRBRK) - return false; - - char *filename = NULL; - - // we need to split the filename and path - for(size_t i = strlen(path)-1; i > 0; i--) { - if(path[i] == DIRBRK) { - path[i] = '\0'; // break the string, so it splits into two - filename = &path[i+1]; // set the rest of it as the filename - break; - } - } - - // If the directory is specifically local, treat it as such. - if(filename == NULL) { - filename = path; - } - - struct xWatcher_directory *dir = NULL; - - // check against the database of (pre-existing) directories - for(size_t i = 0; i < arr_count(watcher->directories); i++) { - // paths match - if(strcmp(watcher->directories[i].path, path) == 0) { - dir = &watcher->directories[i]; - } - } - - // directory exists, check if an callback has been already added - if(dir == NULL) { - struct xWatcher_directory new_dir; - - new_dir.callback_func = NULL; // DO NOT add callbacks if it's a file - new_dir.context = 0; // context should be invalid as well - new_dir.additional_data = NULL; // so should the data - new_dir.path = path; // add a path to the directory - #if defined(__linux__) - new_dir.inotify_watch_fd = -1; // invalidate inotify - #elif defined(__WIN32__) - new_dir.handle = NULL; - #else - #error "Unsupported" - #endif - - // initialize file arrays - arr_init(new_dir.files); - - // add the directory to the masses - arr_add(watcher->directories, new_dir); - - // move the pointer to the newly added element - dir = &watcher->directories[arr_count(watcher->directories)-1]; - } - - // search for the file - struct xWatcher_file *file = NULL; - for(size_t i = 0; i < arr_count(dir->files); i++) { - if(strcmp(dir->files[i].name, filename) == 0) { - file = &dir->files[i]; - } - } - - if(file != NULL) { - return false; // file already exists, that's an ERROR - } - - struct xWatcher_file new_file; - // avoid an invalid free because this shares the memory space - // of the full path string - new_file.name = strdup(filename); - new_file.context = reference->context; - new_file.additional_data = reference->additional_data; - new_file.callback_func = reference->callback_func; - - // the the element - arr_add(dir->files, new_file); - - // and move the pointer to the newly added element - file = &dir->files[arr_count(watcher->directories)-1]; - - // add the file watcher - #if defined(__linux__) - if(dir->inotify_watch_fd == -1) { - dir->inotify_watch_fd = inotify_add_watch( - watcher->inotify_fd, - path, - IN_ALL_EVENTS); - if(dir->inotify_watch_fd == -1) { - perror("inotify_watch_fd"); - return false; - } - } - #elif defined(__WIN32__) - // add directory path - dir->handle = CreateFile(dir->path, - FILE_LIST_DIRECTORY, - FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, - NULL, - OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, - NULL); - if(dir->handle == INVALID_HANDLE_VALUE) { - // get error code - DWORD error = GetLastError(); - char *message; - - // get error message - FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, - error, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPTSTR) &message, - 0, NULL); - - fprintf(stderr, "CreateFile failed: %s\n", - message); - - return false; - } - - // create event structure - dir->overlapped.hEvent = CreateEvent(NULL, FALSE, 0, NULL); - if(dir->overlapped.hEvent == NULL) { - // get error code - DWORD error = GetLastError(); - char *message; - - // get error message - FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, - error, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPTSTR) &message, - 0, NULL); - - fprintf(stderr, "CreateEvent failed: %s\n", - message); - - return false; - } - - // allocate the event buffer - dir->event_buffer = malloc(BUF_LEN); - if(dir->event_buffer == NULL) { - fprintf(stderr, "malloc failed at __FILE__:__LINE__!\n"); - return false; - } - - // set reading params - BOOL success = ReadDirectoryChangesW( - dir->handle, dir->event_buffer, BUF_LEN, TRUE, - FILE_NOTIFY_CHANGE_FILE_NAME | - FILE_NOTIFY_CHANGE_DIR_NAME | - FILE_NOTIFY_CHANGE_LAST_WRITE, - NULL, &dir->overlapped, NULL); - if(!success) { - // get error code - DWORD error = GetLastError(); - char *message; - - // get error message - FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, - error, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPTSTR) &message, - 0, NULL); - - fprintf(stderr, "ReadDirectoryChangesW failed: %s\n", - message); - - return false; - } - #else - #error "Unsupported" - #endif - return true; -} - -static inline bool xWatcher_appendDir( - x_watcher *watcher, - xWatcher_reference *reference) { - char *path = strdup(reference->path); - - // inotify only works with directories that do NOT have a front-slash - // at the end, so we have to make sure to cut that out - if(path[strlen(path)-1] == DIRBRK) - path[strlen(path)-1] = '\0'; - - struct xWatcher_directory *dir = NULL; - - // check against the database of (pre-existing) directories - for(size_t i=0; i < arr_count(watcher->directories); i++) { - // paths match - if(strcmp(watcher->directories[i].path, path) == 0) { - dir = &watcher->directories[i]; - } - } - - // directory exists, check if an callback has been already added - if(dir) { - // ERROR, CALLBACK EXISTS - if(dir->callback_func) { - return false; - } - - dir->callback_func = reference->callback_func; - dir->context = reference->context; - dir->additional_data = reference->additional_data; - } else { - // keep an eye for this one as it's on the stack - struct xWatcher_directory dir; - - dir.path = path; - dir.callback_func = reference->callback_func; - dir.context = reference->context; - dir.additional_data = reference->additional_data; - - // initialize file arrays - arr_init(dir.files); - - #if defined(__linux__) - dir.inotify_watch_fd = inotify_add_watch( - watcher->inotify_fd, - dir.path, - IN_ALL_EVENTS); - if(dir.inotify_watch_fd == -1) { - perror("inotify_watch_fd"); - return false; - } - #elif defined(__WIN32__) - // add directory path - dir.handle = CreateFile(dir.path, - FILE_LIST_DIRECTORY, - FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, - NULL, - OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, - NULL); - if(dir.handle == INVALID_HANDLE_VALUE) { - // get error code - DWORD error = GetLastError(); - char *message; - - // get error message - FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, - error, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPTSTR) &message, - 0, NULL); - - fprintf(stderr, "CreateFile failed: %s\n", - message); - - return false; - } - - // create event structure - dir.overlapped.hEvent = CreateEvent(NULL, FALSE, 0, NULL); - if(dir.overlapped.hEvent == NULL) { - // get error code - DWORD error = GetLastError(); - char *message; - - // get error message - FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, - error, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPTSTR) &message, - 0, NULL); - - fprintf(stderr, "CreateEvent failed: %s\n", - message); - - return false; - } - - // allocate the event buffer - dir.event_buffer = malloc(BUF_LEN); - if(dir.event_buffer == NULL) { - fprintf(stderr, "malloc failed at __FILE__:__LINE__!\n"); - return false; - } - - // set reading params - BOOL success = ReadDirectoryChangesW( - dir.handle, dir.event_buffer, BUF_LEN, TRUE, - FILE_NOTIFY_CHANGE_FILE_NAME | - FILE_NOTIFY_CHANGE_DIR_NAME | - FILE_NOTIFY_CHANGE_LAST_WRITE, - NULL, &dir.overlapped, NULL); - if(!success) { - // get error code - DWORD error = GetLastError(); - char *message; - - // get error message - FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, - error, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPTSTR) &message, - 0, NULL); - - fprintf(stderr, "ReadDirectoryChangesW failed: %s\n", - message); - - return false; - } - #else - #error "Unsupported" - #endif - - arr_add(watcher->directories, dir); - } - - return true; -} - -static inline bool xWatcher_start(x_watcher *watcher) { - watcher->alive = true; - - // create watcher thread - watcher->thread_id = pthread_create( - &watcher->thread, - NULL, - __internal_xWatcherProcess, - watcher); - - if(watcher->thread_id != 0) { - perror("pthread_create"); - watcher->alive = false; - return false; - } - - return true; -} - -static inline void xWatcher_destroy(x_watcher *watcher) { - void *ret; - watcher->alive = false; - pthread_join(watcher->thread, &ret); - free(watcher); -} - -#endif -