Files
deb-lite-xl/lib/dmon/dmon.h
T
Francesco Abbate 9be22f0b8d Attempt to fix dmon critical section for windows
Should fix commit bb12f085f3.

When taking the critical section we should always send the
event to wakeup the events thread. In addition use
TryEnterCriticalSection to send the event only if needed
reducing the number of spurious events sent.
2022-01-09 23:26:11 +01:00

1659 lines
56 KiB
C

#ifndef __DMON_H__
#define __DMON_H__
//
// 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_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
// 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
//
#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;
typedef enum dmon_error_enum {
DMON_SUCCESS = 0,
DMON_ERROR_WATCH_DIR,
DMON_ERROR_OPEN_DIR,
DMON_ERROR_MONITOR_FAIL,
DMON_ERROR_UNSUPPORTED_SYMLINK,
DMON_ERROR_SUBDIR_LOCATION,
DMON_ERROR_END
} dmon_error;
#ifdef __cplusplus
extern "C" {
#endif
DMON_API_DECL const char *dmon_error_str(dmon_error err);
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_error *error_code);
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 <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>
/* Recursive removed for Lite XL when using inotify. */
# define LITE_XL_DISABLE_INOTIFY_RECURSIVE
# define DMON_LOG_DEBUG(s)
#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_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_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_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 decleration)
typedef void (dmon__watch_cb)(dmon_watch_id, dmon_action, const char*, const char*, const char*, void*);
static const char *dmon__errors[] = {
"Success",
"Error watching directory",
"Error opening directory",
"Error enabling monitoring",
"Error support for symlink disabled",
"Error not a subdirectory",
};
DMON_API_IMPL const char *dmon_error_str(dmon_error err) {
DMON_ASSERT(err >= 0 && err < DMON_ERROR_END);
return dmon__errors[(int) err];
}
#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;
HANDLE wake_event;
} 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 + 1];
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;
}
const int n = _dmon.num_watches;
wait_handles[n] = _dmon.wake_event;
DWORD wait_result = WaitForMultipleObjects(n + 1, wait_handles, FALSE, INFINITE);
DMON_ASSERT(wait_result != WAIT_TIMEOUT);
// NOTE: maybe we should check for WAIT_ABANDONED_<n> values if that can happen.
if (wait_result != WAIT_FAILED && wait_result != WAIT_OBJECT_0 + n) {
dmon__watch_state* watch = &_dmon.watches[wait_result - WAIT_OBJECT_0];
DWORD bytes;
if (HasOverlappedIoCompleted(&watch->overlapped) &&
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.wake_event = CreateEvent(NULL, FALSE, FALSE, NULL);
DMON_ASSERT(_dmon.thread_handle);
_dmon_init = true;
}
static void dmon__enter_critical_wakeup() {
_InterlockedExchange(&_dmon.modify_watches, 1);
if (TryEnterCriticalSection(&_dmon.mutex) == 0) {
SetEvent(_dmon.wake_event);
EnterCriticalSection(&_dmon.mutex);
}
}
DMON_API_IMPL void dmon_deinit(void)
{
DMON_ASSERT(_dmon_init);
_dmon.quit = true;
dmon__enter_critical_wakeup();
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]);
}
LeaveCriticalSection(&_dmon.mutex);
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_error *error_code)
{
DMON_ASSERT(watch_cb);
DMON_ASSERT(rootdir && rootdir[0]);
dmon__enter_critical_wakeup();
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);
if (watch->overlapped.hEvent == INVALID_HANDLE_VALUE ||
!dmon__refresh_watch(watch)) {
dmon__unwatch(watch);
*error_code = DMON_ERROR_WATCH_DIR;
LeaveCriticalSection(&_dmon.mutex);
_InterlockedExchange(&_dmon.modify_watches, 0);
return dmon__make_id(0);
}
} else {
*error_code = DMON_ERROR_OPEN_DIR;
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);
dmon__enter_critical_wakeup();
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;
int wait_flag;
int wake_event_pipe[2];
bool quit;
} dmon__state;
static bool _dmon_init;
static dmon__state _dmon;
/* Implementation of recursive monitoring was removed on Linux for the Lite XL
* application. It is never used with recent version of Lite XL starting from 2.0.5
* and recursive monitoring with inotify was always problematic and half-broken.
* Do not cover the new calling signature with error_code because not used by
* Lite XL. */
#ifndef LITE_XL_DISABLE_INOTIFY_RECURSIVE
_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);
}
#endif
_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;
}
#ifndef LITE_XL_DISABLE_INOTIFY_RECURSIVE
_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);
}
#endif
_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) {
# ifndef LITE_XL_DISABLE_INOTIFY_RECURSIVE
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);
// Removing the assertion below because it was giving errors for some reason
// when building a new package.
// _DMON_UNUSED(wd);
// DMON_ASSERT(wd != -1);
if (wd == -1) continue;
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
}
}
# endif
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 };
uint64_t usecs_elapsed = 0;
struct timeval starttm;
gettimeofday(&starttm, 0);
int debug_count = 0;
while (!_dmon.quit) {
nanosleep(&req, &rem);
if (_dmon.num_watches == 0 || _dmon.wait_flag == 1 || pthread_mutex_trylock(&_dmon.mutex) != 0) {
continue;
}
// Create read FD set
fd_set rfds;
FD_ZERO(&rfds);
const int n = _dmon.num_watches;
int nfds = 0;
for (int i = 0; i < n; i++) {
dmon__watch_state* watch = &_dmon.watches[i];
FD_SET(watch->fd, &rfds);
if (watch->fd > nfds)
nfds = watch->fd;
}
int wake_fd = _dmon.wake_event_pipe[0];
FD_SET(wake_fd, &rfds);
if (wake_fd > nfds)
nfds = wake_fd;
if (select(nfds + 1, &rfds, NULL, NULL, NULL)) {
if (FD_ISSET(wake_fd, &rfds)) {
char read_char;
read(wake_fd, &read_char, 1);
}
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__mutex_wakeup_lock() {
_dmon.wait_flag = 1;
if (pthread_mutex_trylock(&_dmon.mutex) != 0) {
char send_char = 1;
write(_dmon.wake_event_pipe[1], &send_char, 1);
pthread_mutex_lock(&_dmon.mutex);
}
_dmon.wait_flag = 0;
}
_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);
_dmon.wait_flag = 0;
int ret_pipe = pipe(_dmon.wake_event_pipe);
DMON_ASSERT(ret_pipe == 0);
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;
dmon__mutex_wakeup_lock();
pthread_join(_dmon.thread_handle, NULL);
for (int i = 0; i < _dmon.num_watches; i++) {
dmon__unwatch(&_dmon.watches[i]);
}
pthread_mutex_unlock(&_dmon.mutex);
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_error *error_code)
{
DMON_ASSERT(watch_cb);
DMON_ASSERT(rootdir && rootdir[0]);
dmon__mutex_wakeup_lock();
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) {
*error_code = DMON_ERROR_OPEN_DIR;
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 {
*error_code = DMON_ERROR_UNSUPPORTED_SYMLINK;
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) {
*error_code = DMON_ERROR_MONITOR_FAIL;
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) {
*error_code = DMON_ERROR_WATCH_DIR;
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
#ifndef LITE_XL_DISABLE_INOTIFY_RECURSIVE
if (flags & DMON_WATCHFLAGS_RECURSIVE) {
dmon__watch_recursive(watch->rootdir, watch->fd, inotify_mask,
(flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) ? true : false, watch);
}
#endif
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);
dmon__mutex_wakeup_lock();
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_error *error_code)
{
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) {
*error_code = DMON_ERROR_OPEN_DIR;
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 {
*error_code = DMON_ERROR_UNSUPPORTED_SYMLINK;
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__