aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFrancesco Abbate <francesco.bbt@gmail.com>2021-07-12 18:21:27 +0200
committerFrancesco Abbate <francesco.bbt@gmail.com>2021-10-08 21:31:22 +0200
commit9c43727ebc269cb0695f6d418e5bf88615677aaf (patch)
treee35ca1181c11a4f09c1d82e4d4dfe79cf96e15e4 /src
parent92362586df7a2aa4451ef05e3812eef83468e985 (diff)
downloadlite-xl-9c43727ebc269cb0695f6d418e5bf88615677aaf.tar.gz
lite-xl-9c43727ebc269cb0695f6d418e5bf88615677aaf.zip
Implement directory monitoring using septag/dmon
Use a notification based directory monitoring based on the septag/dmon lirbary instead of periodically rescan the whole project's tree.
Diffstat (limited to 'src')
-rw-r--r--src/api/system.c100
-rw-r--r--src/dirmonitor.c60
-rw-r--r--src/dirmonitor.h14
-rw-r--r--src/dmon.h1706
-rw-r--r--src/main.c5
-rw-r--r--src/meson.build1
6 files changed, 1886 insertions, 0 deletions
diff --git a/src/api/system.c b/src/api/system.c
index d84f86dd..5b72b4d8 100644
--- a/src/api/system.c
+++ b/src/api/system.c
@@ -6,6 +6,7 @@
#include <errno.h>
#include <sys/stat.h>
#include "api.h"
+#include "dirmonitor.h"
#include "rencache.h"
#ifdef _WIN32
#include <direct.h>
@@ -236,6 +237,14 @@ top:
lua_pushnumber(L, e.wheel.y);
return 2;
+ case SDL_USEREVENT:
+ lua_pushstring(L, "dirchange");
+ lua_pushnumber(L, e.user.code >> 16);
+ lua_pushstring(L, (e.user.code & 0xffff) == DMON_ACTION_DELETE ? "delete" : "create");
+ lua_pushstring(L, e.user.data1);
+ free(e.user.data1);
+ return 4;
+
default:
goto top;
}
@@ -651,6 +660,91 @@ static int f_set_window_opacity(lua_State *L) {
return 1;
}
+static int f_watch_dir(lua_State *L) {
+ const char *path = luaL_checkstring(L, 1);
+ const int recursive = lua_toboolean(L, 2);
+ uint32_t dmon_flags = (recursive ? DMON_WATCHFLAGS_RECURSIVE : 0);
+ dmon_watch_id watch_id = dmon_watch(path, dirmonitor_watch_callback, dmon_flags, NULL);
+ if (watch_id.id == 0) { luaL_error(L, "directory monitoring watch failed"); }
+ lua_pushnumber(L, watch_id.id);
+ return 1;
+}
+
+#if __linux__
+static int f_watch_dir_add(lua_State *L) {
+ dmon_watch_id watch_id;
+ watch_id.id = luaL_checkinteger(L, 1);
+ const char *subdir = luaL_checkstring(L, 2);
+ lua_pushboolean(L, dmon_watch_add(watch_id, subdir));
+ return 1;
+}
+
+static int f_watch_dir_rm(lua_State *L) {
+ dmon_watch_id watch_id;
+ watch_id.id = luaL_checkinteger(L, 1);
+ const char *subdir = luaL_checkstring(L, 2);
+ lua_pushboolean(L, dmon_watch_rm(watch_id, subdir));
+ return 1;
+}
+#endif
+
+#ifdef _WIN32
+#define PATHSEP '\\'
+#else
+#define PATHSEP '/'
+#endif
+
+/* Special purpose filepath compare function. Corresponds to the
+ order used in the TreeView view of the project's files. Returns true iff
+ path1 < path2 in the TreeView order. */
+static int f_path_compare(lua_State *L) {
+ const char *path1 = luaL_checkstring(L, 1);
+ const char *type1_s = luaL_checkstring(L, 2);
+ const char *path2 = luaL_checkstring(L, 3);
+ const char *type2_s = luaL_checkstring(L, 4);
+ const int len1 = strlen(path1), len2 = strlen(path2);
+ int type1 = strcmp(type1_s, "dir") != 0;
+ int type2 = strcmp(type2_s, "dir") != 0;
+ /* Find the index of the common part of the path. */
+ int offset = 0, i;
+ for (i = 0; i < len1 && i < len2; i++) {
+ if (path1[i] != path2[i]) break;
+ if (path1[i] == PATHSEP) {
+ offset = i + 1;
+ }
+ }
+ /* If a path separator is present in the name after the common part we consider
+ the entry like a directory. */
+ if (strchr(path1 + offset, PATHSEP)) {
+ type1 = 0;
+ }
+ if (strchr(path2 + offset, PATHSEP)) {
+ type2 = 0;
+ }
+ /* If types are different "dir" types comes before "file" types. */
+ if (type1 != type2) {
+ lua_pushboolean(L, type1 < type2);
+ return 1;
+ }
+ /* If types are the same compare the files' path alphabetically. */
+ int cfr = 0;
+ int len_min = (len1 < len2 ? len1 : len2);
+ for (int j = offset; j <= len_min; j++) {
+ if (path1[j] == path2[j]) continue;
+ if (path1[j] == 0 || path2[j] == 0) {
+ cfr = (path1[j] == 0);
+ } else if (path1[j] == PATHSEP || path2[j] == PATHSEP) {
+ /* For comparison we treat PATHSEP as if it was the string terminator. */
+ cfr = (path1[j] == PATHSEP);
+ } else {
+ cfr = (path1[j] < path2[j]);
+ }
+ break;
+ }
+ lua_pushboolean(L, cfr);
+ return 1;
+}
+
static const luaL_Reg lib[] = {
{ "poll_event", f_poll_event },
@@ -678,6 +772,12 @@ static const luaL_Reg lib[] = {
{ "exec", f_exec },
{ "fuzzy_match", f_fuzzy_match },
{ "set_window_opacity", f_set_window_opacity },
+ { "watch_dir", f_watch_dir },
+ { "path_compare", f_path_compare },
+#if __linux__
+ { "watch_dir_add", f_watch_dir_add },
+ { "watch_dir_rm", f_watch_dir_rm },
+#endif
{ NULL, NULL }
};
diff --git a/src/dirmonitor.c b/src/dirmonitor.c
new file mode 100644
index 00000000..eb3b185f
--- /dev/null
+++ b/src/dirmonitor.c
@@ -0,0 +1,60 @@
+#include <stdio.h>
+#include <string.h>
+
+#include <SDL.h>
+
+#define DMON_IMPL
+#include "dmon.h"
+
+#include "dirmonitor.h"
+
+static void send_sdl_event(dmon_watch_id watch_id, dmon_action action, const char *filepath) {
+ SDL_Event ev;
+ const int size = strlen(filepath) + 1;
+ /* The string allocated below should be deallocated as soon as the event is
+ treated in the SDL main loop. */
+ char *new_filepath = malloc(size);
+ if (!new_filepath) return;
+ memcpy(new_filepath, filepath, size);
+#ifdef _WIN32
+ for (int i = 0; i < size; i++) {
+ if (new_filepath[i] == '/') {
+ new_filepath[i] = '\\';
+ }
+ }
+#endif
+ SDL_zero(ev);
+ ev.type = SDL_USEREVENT;
+ ev.user.code = ((watch_id.id & 0xffff) << 16) | (action & 0xffff);
+ ev.user.data1 = new_filepath;
+ SDL_PushEvent(&ev);
+}
+
+void dirmonitor_init() {
+ dmon_init();
+ /* In theory we should register our user event but since we
+ have just one type of user event this is not really needed. */
+ /* sdl_dmon_event_type = SDL_RegisterEvents(1); */
+}
+
+void dirmonitor_deinit() {
+ dmon_deinit();
+}
+
+void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir,
+ const char *filepath, const char *oldfilepath, void *user)
+{
+ (void) rootdir;
+ (void) user;
+ switch (action) {
+ case DMON_ACTION_MOVE:
+ send_sdl_event(watch_id, DMON_ACTION_DELETE, oldfilepath);
+ send_sdl_event(watch_id, DMON_ACTION_CREATE, filepath);
+ break;
+ case DMON_ACTION_MODIFY:
+ break;
+ default:
+ send_sdl_event(watch_id, action, filepath);
+ }
+}
+
diff --git a/src/dirmonitor.h b/src/dirmonitor.h
new file mode 100644
index 00000000..ab9376c0
--- /dev/null
+++ b/src/dirmonitor.h
@@ -0,0 +1,14 @@
+#ifndef DIRMONITOR_H
+#define DIRMONITOR_H
+
+#include <stdint.h>
+
+#include "dmon.h"
+
+void dirmonitor_init();
+void dirmonitor_deinit();
+void dirmonitor_watch_callback(dmon_watch_id watch_id, dmon_action action, const char *rootdir,
+ const char *filepath, const char *oldfilepath, void *user);
+
+#endif
+
diff --git a/src/dmon.h b/src/dmon.h
new file mode 100644
index 00000000..1ccae446
--- /dev/null
+++ b/src/dmon.h
@@ -0,0 +1,1706 @@
+//
+// Copyright 2021 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 declerations. (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
+//
+// TODO:
+// - 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
+//
+#ifndef __DMON_H__
+#define __DMON_H__
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#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);
+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
+
+#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 <windows.h>
+# include <intrin.h>
+# ifdef _MSC_VER
+# pragma intrinsic(_InterlockedExchange)
+# endif
+#elif DMON_OS_LINUX
+# ifndef __USE_MISC
+# define __USE_MISC
+# endif
+# include <dirent.h>
+# include <errno.h>
+# include <fcntl.h>
+# include <linux/limits.h>
+# include <pthread.h>
+# include <sys/inotify.h>
+# include <sys/stat.h>
+# include <sys/time.h>
+# include <time.h>
+# include <unistd.h>
+# include <stdlib.h>
+#elif DMON_OS_MACOS
+# include <pthread.h>
+# include <CoreServices/CoreServices.h>
+# include <sys/time.h>
+# include <sys/stat.h>
+# include <dispatch/dispatch.h>
+#endif
+
+#ifndef DMON_MALLOC
+# include <stdlib.h>
+# 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 <assert.h>
+# define DMON_ASSERT(e) assert(e)
+#endif
+
+#ifndef DMON_LOG_ERROR
+# include <stdio.h>
+# define DMON_LOG_ERROR(s) do { puts(s); DMON_ASSERT(0); } while(0)
+#endif
+
+#ifndef DMON_LOG_DEBUG
+# ifndef NDEBUG
+# include <stdio.h>
+# 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
+
+#include <string.h>
+
+#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);
+ len = dmon__min(len, (size_t)size - 1);
+
+ for (size_t 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_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 decleration)
+typedef void (dmon__watch_cb)(dmon_watch_id, dmon_action, const char*, const char*, const char*, void*);
+
+#if DMON_OS_WINDOWS
+// IOCP (windows)
+#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];
+ 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);
+ memset(watch, 0x0, sizeof(dmon__watch_state));
+}
+
+_DMON_PRIVATE void dmon__win32_process_events(void)
+{
+ for (int 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
+ for (int 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 (int 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 occurance of the NEW_NAME
+ // this is somewhat API flaw that we have no reference for relating old and new files
+ for (int 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];
+
+ SYSTEMTIME starttm;
+ GetSystemTime(&starttm);
+ uint64_t msecs_elapsed = 0;
+
+ while (!_dmon.quit) {
+ if (_dmon.modify_watches || !TryEnterCriticalSection(&_dmon.mutex)) {
+ Sleep(10);
+ continue;
+ }
+
+ if (_dmon.num_watches == 0) {
+ Sleep(10);
+ LeaveCriticalSection(&_dmon.mutex);
+ continue;
+ }
+
+ for (int i = 0; i < _dmon.num_watches; i++) {
+ dmon__watch_state* watch = &_dmon.watches[i];
+ 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 = &_dmon.watches[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);
+ _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);
+ }
+
+ for (int i = 0; i < _dmon.num_watches; i++) {
+ dmon__unwatch(&_dmon.watches[i]);
+ }
+
+ DeleteCriticalSection(&_dmon.mutex);
+ stb_sb_free(_dmon.events);
+ _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(watch_cb);
+ DMON_ASSERT(rootdir && rootdir[0]);
+
+ _InterlockedExchange(&_dmon.modify_watches, 1);
+ EnterCriticalSection(&_dmon.mutex);
+
+ DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES);
+
+ uint32_t id = ++_dmon.num_watches;
+ dmon__watch_state* watch = &_dmon.watches[id - 1];
+ 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(id.id > 0);
+
+ _InterlockedExchange(&_dmon.modify_watches, 1);
+ EnterCriticalSection(&_dmon.mutex);
+
+ int index = id.id - 1;
+ DMON_ASSERT(index < _dmon.num_watches);
+
+ dmon__unwatch(&_dmon.watches[index]);
+ if (index != _dmon.num_watches - 1) {
+ dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state);
+ }
+ --_dmon.num_watches;
+
+ LeaveCriticalSection(&_dmon.mutex);
+ _InterlockedExchange(&_dmon.modify_watches, 0);
+}
+
+#elif DMON_OS_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];
+ 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_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];
+
+ // 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);
+ }
+
+ int 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 (int 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]);
+
+ for (int j = i; j < c - 1; j++) {
+ memcpy(watch->subdirs + j, watch->subdirs + j + 1, sizeof(dmon__watch_subdir));
+ memcpy(watch->wds + j, watch->wds + j + 1, sizeof(int));
+ }
+ stb__sbraw(watch->subdirs)[1] = c - 1;
+ stb__sbraw(watch->wds)[1] = c - 1;
+
+ if (!skip_lock)
+ pthread_mutex_unlock(&_dmon.mutex);
+ return true;
+}
+
+_DMON_PRIVATE const char* dmon__find_subdir(const dmon__watch_state* watch, int wd)
+{
+ const int* wds = watch->wds;
+ for (int 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 : 0), 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)
+{
+ for (int 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) {
+ for (int 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 traling 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) {
+ bool loop_break = false;
+ for (int 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
+ for (int 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;
+ for (int 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;
+ for (int 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) {
+ for (int 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 (int 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) {
+ for (int 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)10 / 1000, (long)(10 * 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);
+ for (int 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)) {
+ for (int 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);
+ memset(watch, 0x0, sizeof(dmon__watch_state));
+}
+
+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");
+ _dmon_init = true;
+}
+
+DMON_API_IMPL void dmon_deinit(void)
+{
+ DMON_ASSERT(_dmon_init);
+ _dmon.quit = true;
+ pthread_join(_dmon.thread_handle, NULL);
+
+ for (int i = 0; i < _dmon.num_watches; i++) {
+ dmon__unwatch(&_dmon.watches[i]);
+ }
+
+ pthread_mutex_destroy(&_dmon.mutex);
+ stb_sb_free(_dmon.events);
+ _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(watch_cb);
+ DMON_ASSERT(rootdir && rootdir[0]);
+
+ pthread_mutex_lock(&_dmon.mutex);
+
+ DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES);
+
+ uint32_t id = ++_dmon.num_watches;
+ dmon__watch_state* watch = &_dmon.watches[id - 1];
+ 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: enumarate 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(id.id > 0);
+
+ pthread_mutex_lock(&_dmon.mutex);
+
+ int index = id.id - 1;
+ DMON_ASSERT(index < _dmon.num_watches);
+
+ dmon__unwatch(&_dmon.watches[index]);
+ if (index != _dmon.num_watches - 1) {
+ dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state);
+ }
+ --_dmon.num_watches;
+
+ pthread_mutex_unlock(&_dmon.mutex);
+}
+#elif DMON_OS_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];
+ 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)
+{
+ for (int 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) {
+ for (int 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) {
+ for (int 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 (int 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);
+ } else 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) {
+ for (int 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);
+}
+
+static void* dmon__thread(void* arg)
+{
+ _DMON_UNUSED(arg);
+
+ struct timespec req = { (time_t)10 / 1000, (long)(10 * 1000000) };
+ struct timespec rem = { 0, 0 };
+
+ _dmon.cf_loop_ref = CFRunLoopGetCurrent();
+ dispatch_semaphore_signal(_dmon.thread_sem);
+
+ while (!_dmon.quit) {
+ 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 (int 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;
+ }
+
+ memset(watch, 0x0, sizeof(dmon__watch_state));
+}
+
+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);
+
+ _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);
+
+ for (int i = 0; i < _dmon.num_watches; i++) {
+ dmon__unwatch(&_dmon.watches[i]);
+ }
+
+ pthread_mutex_destroy(&_dmon.mutex);
+ stb_sb_free(_dmon.events);
+ if (_dmon.cf_alloc_ref) {
+ CFRelease(_dmon.cf_alloc_ref);
+ }
+
+ _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];
+
+ for (size_t 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 begining
+ 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(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);
+
+ uint32_t id = ++_dmon.num_watches;
+ dmon__watch_state* watch = &_dmon.watches[id - 1];
+ 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(id.id > 0);
+
+ __sync_lock_test_and_set(&_dmon.modify_watches, 1);
+ pthread_mutex_lock(&_dmon.mutex);
+
+ int index = id.id - 1;
+ DMON_ASSERT(index < _dmon.num_watches);
+
+ dmon__unwatch(&_dmon.watches[index]);
+ if (index != _dmon.num_watches - 1) {
+ dmon__swap(_dmon.watches[index], _dmon.watches[_dmon.num_watches - 1], dmon__watch_state);
+ }
+ --_dmon.num_watches;
+
+ pthread_mutex_unlock(&_dmon.mutex);
+ __sync_lock_test_and_set(&_dmon.modify_watches, 0);
+}
+
+#endif
+
+#endif // DMON_IMPL
+#endif // __DMON_H__
diff --git a/src/main.c b/src/main.c
index 182511d0..e0fcb107 100644
--- a/src/main.c
+++ b/src/main.c
@@ -14,6 +14,8 @@
#include <mach-o/dyld.h>
#endif
+#include "dirmonitor.h"
+
SDL_Window *window;
@@ -107,6 +109,8 @@ int main(int argc, char **argv) {
SDL_DisplayMode dm;
SDL_GetCurrentDisplayMode(0, &dm);
+ dirmonitor_init();
+
window = SDL_CreateWindow(
"", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, dm.w * 0.8, dm.h * 0.8,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN);
@@ -189,6 +193,7 @@ init_lua:
lua_close(L);
ren_free_window_resources();
+ dirmonitor_deinit();
return EXIT_SUCCESS;
}
diff --git a/src/meson.build b/src/meson.build
index 707e04e9..2da04fda 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -6,6 +6,7 @@ lite_sources = [
'api/regex.c',
'api/system.c',
'api/process.c',
+ 'dirmonitor.c',
'renderer.c',
'renwindow.c',
'fontdesc.c',