From 5ed86c7580fb0166166ed4e17041f39bb6eab30a Mon Sep 17 00:00:00 2001 From: meyraud705 Date: Wed, 8 Apr 2026 12:43:28 +0200 Subject: [PATCH 1/7] Add file watcher API and Linux implementation --- include/SDL3/SDL_events.h | 16 +++ src/events/SDL_events.c | 3 + src/filesystem/SDL_filesystem.c | 14 +++ src/filesystem/SDL_filesystem_c.h | 1 + src/filesystem/SDL_sysfilesystem.h | 4 + src/filesystem/dummy/SDL_sysfsops.c | 13 +++ src/filesystem/posix/SDL_sysfsops.c | 162 ++++++++++++++++++++++++++ src/filesystem/windows/SDL_sysfsops.c | 13 +++ 8 files changed, 226 insertions(+) diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h index fa923b0d1e..71b0eabf15 100644 --- a/include/SDL3/SDL_events.h +++ b/include/SDL3/SDL_events.h @@ -259,6 +259,10 @@ typedef enum SDL_EventType SDL_EVENT_CAMERA_DEVICE_APPROVED, /**< A camera device has been approved for use by the user. */ SDL_EVENT_CAMERA_DEVICE_DENIED, /**< A camera device has been denied for use by the user. */ + /* File watch events */ + SDL_EVENT_FILE_WATCH_ERROR = 0x1500, /**< Watched files may have been modified, but the events are lost. */ + SDL_EVENT_FILE_CHANGED, /**< A watched file was written. */ + /* Render events */ SDL_EVENT_RENDER_TARGETS_RESET = 0x2000, /**< The render targets have been reset and their contents need to be updated */ SDL_EVENT_RENDER_DEVICE_RESET, /**< The device has been reset and all textures need to be recreated */ @@ -970,6 +974,17 @@ typedef struct SDL_SensorEvent Uint64 sensor_timestamp; /**< The timestamp of the sensor reading in nanoseconds, not necessarily synchronized with the system clock */ } SDL_SensorEvent; +/** + * File watch event structure (event.file_watch.*) + */ +typedef struct SDL_FileWatchEvent +{ + SDL_EventType type; /**< SDL_EVENT_FILE_WATCH_ERROR or SDL_EVENT_FILE_CHANGED */ + Uint32 reserved; + Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */ + const char *path; /**< Path of the modified file for SDL_EVENT_FILE_CHANGED, NULL for SDL_EVENT_FILE_WATCH_ERROR */ +} SDL_FileWatchEvent; + /** * The "quit requested" event * @@ -1054,6 +1069,7 @@ typedef union SDL_Event SDL_RenderEvent render; /**< Render event data */ SDL_DropEvent drop; /**< Drag and drop event data */ SDL_ClipboardEvent clipboard; /**< Clipboard event data */ + SDL_FileWatchEvent file_watch; /**< File watch event data */ /* This is necessary for ABI compatibility between Visual C++ and GCC. Visual C++ will respect the push pack pragma and use 52 bytes (size of diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c index c909a2a583..162d26e134 100644 --- a/src/events/SDL_events.c +++ b/src/events/SDL_events.c @@ -28,6 +28,7 @@ #include "../SDL_hints_c.h" #include "../audio/SDL_audio_c.h" #include "../camera/SDL_camera_c.h" +#include "../filesystem/SDL_filesystem_c.h" #include "../timer/SDL_timer_c.h" #include "../core/linux/SDL_udev.h" #ifndef SDL_JOYSTICK_DISABLED @@ -1458,6 +1459,8 @@ bool SDL_RunOnMainThread(SDL_MainThreadCallback callback, void *userdata, bool w void SDL_PumpEventMaintenance(void) { + SDL_UpdateFileWatch(); + #ifdef SDL_USE_LIBUDEV SDL_UDEV_Poll(); #endif diff --git a/src/filesystem/SDL_filesystem.c b/src/filesystem/SDL_filesystem.c index dca0da5c99..c89be22f1a 100644 --- a/src/filesystem/SDL_filesystem.c +++ b/src/filesystem/SDL_filesystem.c @@ -142,6 +142,19 @@ bool SDL_GetPathInfo(const char *path, SDL_PathInfo *info) return SDL_SYS_GetPathInfo(path, info); } +bool SDL_WatchFileForChanges(const char *path) +{ + CHECK_PARAM(!path) { + return SDL_InvalidParamError("path"); + } + return SDL_SYS_WatchFileForChanges(path); +} + +void SDL_UpdateFileWatch(void) +{ + SDL_SYS_UpdateFileWatch(); +} + static bool EverythingMatch(const char *pattern, const char *str, bool *matched_to_dir) { SDL_assert(pattern == NULL); @@ -547,5 +560,6 @@ void SDL_QuitFilesystem(void) CachedUserFolders[i] = NULL; } } + SDL_SYS_QuitFileWatch(); } diff --git a/src/filesystem/SDL_filesystem_c.h b/src/filesystem/SDL_filesystem_c.h index 685cda2822..5f52280963 100644 --- a/src/filesystem/SDL_filesystem_c.h +++ b/src/filesystem/SDL_filesystem_c.h @@ -25,5 +25,6 @@ extern void SDL_InitFilesystem(void); extern void SDL_QuitFilesystem(void); +extern void SDL_UpdateFileWatch(void); #endif diff --git a/src/filesystem/SDL_sysfilesystem.h b/src/filesystem/SDL_sysfilesystem.h index 660c712480..4584f789d4 100644 --- a/src/filesystem/SDL_sysfilesystem.h +++ b/src/filesystem/SDL_sysfilesystem.h @@ -35,6 +35,10 @@ extern bool SDL_SYS_CopyFile(const char *oldpath, const char *newpath); extern bool SDL_SYS_CreateDirectory(const char *path); extern bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info); +extern bool SDL_SYS_WatchFileForChanges(const char *path); +extern void SDL_SYS_UpdateFileWatch(void); +extern void SDL_SYS_QuitFileWatch(void); + typedef bool (*SDL_GlobEnumeratorFunc)(const char *path, SDL_EnumerateDirectoryCallback cb, void *cbuserdata, void *userdata); typedef bool (*SDL_GlobGetPathInfoFunc)(const char *path, SDL_PathInfo *info, void *userdata); extern char **SDL_InternalGlobDirectory(const char *path, const char *pattern, SDL_GlobFlags flags, int *count, SDL_GlobEnumeratorFunc enumerator, SDL_GlobGetPathInfoFunc getpathinfo, void *userdata); diff --git a/src/filesystem/dummy/SDL_sysfsops.c b/src/filesystem/dummy/SDL_sysfsops.c index ce8ca29e65..f3aac0281b 100644 --- a/src/filesystem/dummy/SDL_sysfsops.c +++ b/src/filesystem/dummy/SDL_sysfsops.c @@ -58,5 +58,18 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) return SDL_Unsupported(); } +bool SDL_SYS_WatchFileForChanges(const char *path) +{ + return SDL_Unsupported(); +} + +void SDL_SYS_UpdateFileWatch(void) +{ +} + +void SDL_SYS_QuitFileWatch(void) +{ +} + #endif // SDL_FSOPS_DUMMY diff --git a/src/filesystem/posix/SDL_sysfsops.c b/src/filesystem/posix/SDL_sysfsops.c index 1ab9f80301..3dde451ec2 100644 --- a/src/filesystem/posix/SDL_sysfsops.c +++ b/src/filesystem/posix/SDL_sysfsops.c @@ -27,6 +27,8 @@ // System dependent filesystem routines #include "../SDL_sysfilesystem.h" +#include "../../SDL_hashtable.h" +#include "../../events/SDL_events_c.h" #include #include @@ -39,6 +41,11 @@ #include "../../core/android/SDL_android.h" #endif +#ifdef HAVE_INOTIFY +#include +#include +#include +#endif bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata) { @@ -410,6 +417,161 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) return true; } +#ifdef HAVE_INOTIFY +static int inotify_fd = -1; +static SDL_HashTable *watch_descriptor_table = NULL; // stores directory or file path for a watch descriptor +static SDL_Mutex *file_watch_lock = NULL; + +#ifdef HAVE_INOTIFY_INIT1 +static int SDL_inotify_init1(void) +{ + return inotify_init1(IN_NONBLOCK | IN_CLOEXEC); +} +#else +static int SDL_inotify_init1(void) +{ + int fd = inotify_init(); + if (fd < 0) { + return -1; + } + fcntl(fd, F_SETFL, O_NONBLOCK); + fcntl(fd, F_SETFD, FD_CLOEXEC); + return fd; +} +#endif // HAVE_INOTIFY_INIT1 +#endif // HAVE_INOTIFY + +bool SDL_SYS_WatchFileForChanges(const char *path) +{ +#ifdef HAVE_INOTIFY + if (!watch_descriptor_table) { + watch_descriptor_table = SDL_CreateHashTable(0, false, SDL_HashID, SDL_KeyMatchID, SDL_DestroyHashValue, NULL); + if (!watch_descriptor_table) { + return false; + } + inotify_fd = SDL_inotify_init1(); + if (inotify_fd == -1) { + SDL_DestroyHashTable(watch_descriptor_table); + watch_descriptor_table = NULL; + return SDL_SetError("Could not initialize inotify: %s", strerror(errno)); + } + file_watch_lock = SDL_CreateMutex(); + if (!file_watch_lock) { + SDL_DestroyHashTable(watch_descriptor_table); + watch_descriptor_table = NULL; + close(inotify_fd); + inotify_fd = -1; + return false; + } + } + + char *p = SDL_strdup(path); + if (!p) { + return false; + } + // remove separator at the end of the path + const size_t slen = SDL_strlen(p); + if (p[slen - 1] == '/') { + p[slen - 1] = '\0'; + } + + SDL_LockMutex(file_watch_lock); + int wd = inotify_add_watch(inotify_fd, p, IN_MODIFY); + if (wd == -1) { + SDL_UnlockMutex(file_watch_lock); + SDL_free(p); + return SDL_SetError("inotify_add_watch failed: %s", strerror(errno)); + } + if (!SDL_InsertIntoHashTable(watch_descriptor_table, (void *)(intptr_t)wd, p, false)) { + inotify_rm_watch(inotify_fd, wd); + SDL_UnlockMutex(file_watch_lock); + SDL_free(p); + return false; + } + SDL_UnlockMutex(file_watch_lock); + return true; +#else + return SDL_Unsupported(); +#endif // HAVE_INOTIFY +} + +void SDL_SYS_UpdateFileWatch(void) +{ +#ifdef HAVE_INOTIFY + if (inotify_fd >= 0) { + SDL_LockMutex(file_watch_lock); + union + { + struct inotify_event event; + char storage[4096]; + char enough_for_inotify[sizeof(struct inotify_event) + NAME_MAX + 1]; + } buf; + ssize_t bytes; + size_t remain = 0; + size_t len; + char path[PATH_MAX]; + + bytes = read(inotify_fd, &buf, sizeof(buf)); + + if (bytes > 0) { + remain = (size_t)bytes; + } + + while (remain > 0) { + const char *watched_path; + if (SDL_FindInHashTable(watch_descriptor_table, (void *)(intptr_t)buf.event.wd, (const void **)&watched_path)) { + const char *path_tmp; + SDL_EventType event_type; + bool post_event = true; + if (buf.event.mask & IN_Q_OVERFLOW) { + event_type = SDL_EVENT_FILE_WATCH_ERROR; + path_tmp = NULL; + } else if (buf.event.len != 0) { + (void)SDL_snprintf(path, SDL_arraysize(path), "%s/%s", watched_path, buf.event.name); + event_type = SDL_EVENT_FILE_CHANGED; + path_tmp = SDL_CreateTemporaryString(path); + post_event = (path_tmp != NULL); + } else { + event_type = SDL_EVENT_FILE_CHANGED; + path_tmp = SDL_CreateTemporaryString(watched_path); + post_event = (path_tmp != NULL); + } + if (post_event && SDL_EventEnabled(event_type)) { + SDL_Event event; + SDL_zero(event); + event.type = event_type; + event.common.timestamp = 0; + event.file_watch.path = path_tmp; + SDL_PushEvent(&event); + } + } + + len = sizeof(struct inotify_event) + buf.event.len; + remain -= len; + + if (remain != 0) { + SDL_memmove(&buf.storage[0], &buf.storage[len], remain); + } + } + SDL_UnlockMutex(file_watch_lock); + } +#endif // HAVE_INOTIFY +} + +void SDL_SYS_QuitFileWatch(void) +{ +#ifdef HAVE_INOTIFY + if (inotify_fd >= 0) { + close(inotify_fd); + inotify_fd = -1; + SDL_DestroyMutex(file_watch_lock); + file_watch_lock = NULL; + SDL_DestroyHashTable(watch_descriptor_table); + watch_descriptor_table = NULL; + } +#endif // HAVE_INOTIFY +} + // Note that this is actually part of filesystem, not fsops, but everything that uses posix fsops uses this implementation, even with separate filesystem code. char *SDL_SYS_GetCurrentDirectory(void) { diff --git a/src/filesystem/windows/SDL_sysfsops.c b/src/filesystem/windows/SDL_sysfsops.c index f0fc0fd959..f8b4f1e3ea 100644 --- a/src/filesystem/windows/SDL_sysfsops.c +++ b/src/filesystem/windows/SDL_sysfsops.c @@ -231,5 +231,18 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) return true; } +bool SDL_SYS_WatchFileForChanges(const char *path) +{ + return SDL_Unsupported(); +} + +void SDL_SYS_UpdateFileWatch(void) +{ +} + +void SDL_SYS_QuitFileWatch(void) +{ +} + #endif // SDL_FSOPS_WINDOWS From 7b8d6155cc7fc0e9b62c98d27d93fafceea710bb Mon Sep 17 00:00:00 2001 From: meyraud705 Date: Tue, 21 Apr 2026 23:02:17 +0200 Subject: [PATCH 2/7] Add callback to file watch. --- include/SDL3/SDL_events.h | 8 +- include/SDL3/SDL_filesystem.h | 32 ++++++++ src/events/SDL_events.c | 3 - src/filesystem/SDL_filesystem.c | 9 +-- src/filesystem/SDL_filesystem_c.h | 1 - src/filesystem/SDL_sysfilesystem.h | 3 +- src/filesystem/dummy/SDL_sysfsops.c | 6 +- src/filesystem/posix/SDL_sysfsops.c | 112 +++++++++++++++++--------- src/filesystem/windows/SDL_sysfsops.c | 6 +- 9 files changed, 116 insertions(+), 64 deletions(-) diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h index 71b0eabf15..9d84574c83 100644 --- a/include/SDL3/SDL_events.h +++ b/include/SDL3/SDL_events.h @@ -260,8 +260,8 @@ typedef enum SDL_EventType SDL_EVENT_CAMERA_DEVICE_DENIED, /**< A camera device has been denied for use by the user. */ /* File watch events */ - SDL_EVENT_FILE_WATCH_ERROR = 0x1500, /**< Watched files may have been modified, but the events are lost. */ - SDL_EVENT_FILE_CHANGED, /**< A watched file was written. */ + SDL_EVENT_FILE_CHANGED = 0x1500, /**< A watched file was written. */ + SDL_EVENT_FILE_WATCH_ERROR, /**< Watched files may have been modified, but the events are lost. */ /* Render events */ SDL_EVENT_RENDER_TARGETS_RESET = 0x2000, /**< The render targets have been reset and their contents need to be updated */ @@ -976,6 +976,10 @@ typedef struct SDL_SensorEvent /** * File watch event structure (event.file_watch.*) + * + * You can add file to the watch list with SDL_WatchFileForChanges(). + * + * \sa SDL_WatchFileForChanges */ typedef struct SDL_FileWatchEvent { diff --git a/include/SDL3/SDL_filesystem.h b/include/SDL3/SDL_filesystem.h index 53e7ac9b43..94cb272030 100644 --- a/include/SDL3/SDL_filesystem.h +++ b/include/SDL3/SDL_filesystem.h @@ -529,6 +529,38 @@ extern SDL_DECLSPEC char ** SDLCALL SDL_GlobDirectory(const char *path, const ch */ extern SDL_DECLSPEC char * SDLCALL SDL_GetCurrentDirectory(void); +/** + * A function pointer used for callbacks that watch for files change. + * + * \param userdata what was passed as `userdata` to SDL_WatchFileForChanges(). + * \param path path of file that was modified. + * + * \threadsafety SDL may call this callback at any time from any thread; the + * application is responsible for locking resources the callback + * touches that need to be protected. + */ +typedef void (SDLCALL *SDL_FileWatchCallback)(void *userdata, const char *path); + +/** + * This function adds a file watcher that will fires an app-provided callback + * and send an SDL_EVENT_FILE_CHANGED event every time the file is modified. If + * path is a directory, the callback will be called for every file modified in + * that directory. + * + * \param path file or directory path to watch. + * \param callback a function that is called when the watched file is modified. + * Can be NULL if you only want to receive event. + * \param userdata a pointer that is passed to `callback`. + * + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety It is safe to call this function from any thread. + * + * \sa SDL_FileWatchEvent + */ +extern SDL_DECLSPEC bool SDLCALL SDL_WatchFileForChanges(const char *path, SDL_FileWatchCallback callback, void *userdata); + /* Ends C function definitions when using C++ */ #ifdef __cplusplus } diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c index 162d26e134..c909a2a583 100644 --- a/src/events/SDL_events.c +++ b/src/events/SDL_events.c @@ -28,7 +28,6 @@ #include "../SDL_hints_c.h" #include "../audio/SDL_audio_c.h" #include "../camera/SDL_camera_c.h" -#include "../filesystem/SDL_filesystem_c.h" #include "../timer/SDL_timer_c.h" #include "../core/linux/SDL_udev.h" #ifndef SDL_JOYSTICK_DISABLED @@ -1459,8 +1458,6 @@ bool SDL_RunOnMainThread(SDL_MainThreadCallback callback, void *userdata, bool w void SDL_PumpEventMaintenance(void) { - SDL_UpdateFileWatch(); - #ifdef SDL_USE_LIBUDEV SDL_UDEV_Poll(); #endif diff --git a/src/filesystem/SDL_filesystem.c b/src/filesystem/SDL_filesystem.c index c89be22f1a..646c78b606 100644 --- a/src/filesystem/SDL_filesystem.c +++ b/src/filesystem/SDL_filesystem.c @@ -142,17 +142,12 @@ bool SDL_GetPathInfo(const char *path, SDL_PathInfo *info) return SDL_SYS_GetPathInfo(path, info); } -bool SDL_WatchFileForChanges(const char *path) +bool SDL_WatchFileForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata) { CHECK_PARAM(!path) { return SDL_InvalidParamError("path"); } - return SDL_SYS_WatchFileForChanges(path); -} - -void SDL_UpdateFileWatch(void) -{ - SDL_SYS_UpdateFileWatch(); + return SDL_SYS_WatchFileForChanges(path, cb, userdata); } static bool EverythingMatch(const char *pattern, const char *str, bool *matched_to_dir) diff --git a/src/filesystem/SDL_filesystem_c.h b/src/filesystem/SDL_filesystem_c.h index 5f52280963..685cda2822 100644 --- a/src/filesystem/SDL_filesystem_c.h +++ b/src/filesystem/SDL_filesystem_c.h @@ -25,6 +25,5 @@ extern void SDL_InitFilesystem(void); extern void SDL_QuitFilesystem(void); -extern void SDL_UpdateFileWatch(void); #endif diff --git a/src/filesystem/SDL_sysfilesystem.h b/src/filesystem/SDL_sysfilesystem.h index 4584f789d4..8b9d1baa13 100644 --- a/src/filesystem/SDL_sysfilesystem.h +++ b/src/filesystem/SDL_sysfilesystem.h @@ -35,8 +35,7 @@ extern bool SDL_SYS_CopyFile(const char *oldpath, const char *newpath); extern bool SDL_SYS_CreateDirectory(const char *path); extern bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info); -extern bool SDL_SYS_WatchFileForChanges(const char *path); -extern void SDL_SYS_UpdateFileWatch(void); +extern bool SDL_SYS_WatchFileForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata); extern void SDL_SYS_QuitFileWatch(void); typedef bool (*SDL_GlobEnumeratorFunc)(const char *path, SDL_EnumerateDirectoryCallback cb, void *cbuserdata, void *userdata); diff --git a/src/filesystem/dummy/SDL_sysfsops.c b/src/filesystem/dummy/SDL_sysfsops.c index f3aac0281b..d52bf6ef55 100644 --- a/src/filesystem/dummy/SDL_sysfsops.c +++ b/src/filesystem/dummy/SDL_sysfsops.c @@ -58,15 +58,11 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) return SDL_Unsupported(); } -bool SDL_SYS_WatchFileForChanges(const char *path) +bool SDL_SYS_WatchFileForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata) { return SDL_Unsupported(); } -void SDL_SYS_UpdateFileWatch(void) -{ -} - void SDL_SYS_QuitFileWatch(void) { } diff --git a/src/filesystem/posix/SDL_sysfsops.c b/src/filesystem/posix/SDL_sysfsops.c index 3dde451ec2..72a566e4c8 100644 --- a/src/filesystem/posix/SDL_sysfsops.c +++ b/src/filesystem/posix/SDL_sysfsops.c @@ -26,6 +26,8 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ // System dependent filesystem routines +#include +#include #include "../SDL_sysfilesystem.h" #include "../../SDL_hashtable.h" #include "../../events/SDL_events_c.h" @@ -419,8 +421,18 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) #ifdef HAVE_INOTIFY static int inotify_fd = -1; -static SDL_HashTable *watch_descriptor_table = NULL; // stores directory or file path for a watch descriptor +typedef struct WatchEntry +{ + SDL_FileWatchCallback callback; + void *user_data; + char path[]; // directory or file path +} WatchEntry; +static SDL_HashTable *watch_descriptor_table = NULL; // stores WatchEntry for a watch descriptor + +static int SDL_FileWatchThread(void *user_data); +static SDL_Thread *file_watch_thread = NULL; static SDL_Mutex *file_watch_lock = NULL; +static SDL_AtomicInt quit_watch_file; #ifdef HAVE_INOTIFY_INIT1 static int SDL_inotify_init1(void) @@ -441,7 +453,7 @@ static int SDL_inotify_init1(void) #endif // HAVE_INOTIFY_INIT1 #endif // HAVE_INOTIFY -bool SDL_SYS_WatchFileForChanges(const char *path) +bool SDL_SYS_WatchFileForChanges(const char *path, SDL_FileWatchCallback cb, void *user_data) { #ifdef HAVE_INOTIFY if (!watch_descriptor_table) { @@ -463,29 +475,41 @@ bool SDL_SYS_WatchFileForChanges(const char *path) inotify_fd = -1; return false; } + file_watch_thread = SDL_CreateThread(SDL_FileWatchThread, "SDL_FileWatch", NULL); + SDL_SetAtomicInt(&quit_watch_file, 0); + if (!file_watch_thread) { + SDL_DestroyHashTable(watch_descriptor_table); + watch_descriptor_table = NULL; + close(inotify_fd); + inotify_fd = -1; + SDL_DestroyMutex(file_watch_lock); + file_watch_lock = NULL; + return false; + } } - - char *p = SDL_strdup(path); - if (!p) { + const size_t slen = SDL_strlen(path); + WatchEntry *watch_entry = SDL_malloc(sizeof(*watch_entry) + slen + 1); + if (!watch_entry) { return false; } + watch_entry->callback = cb; + watch_entry->user_data = user_data; + SDL_memcpy(watch_entry->path, path, slen + 1); // remove separator at the end of the path - const size_t slen = SDL_strlen(p); - if (p[slen - 1] == '/') { - p[slen - 1] = '\0'; + if (watch_entry->path[slen - 1] == '/') { + watch_entry->path[slen - 1] = '\0'; } - SDL_LockMutex(file_watch_lock); - int wd = inotify_add_watch(inotify_fd, p, IN_MODIFY); + int wd = inotify_add_watch(inotify_fd, path, IN_MODIFY); if (wd == -1) { SDL_UnlockMutex(file_watch_lock); - SDL_free(p); + SDL_free(watch_entry); return SDL_SetError("inotify_add_watch failed: %s", strerror(errno)); } - if (!SDL_InsertIntoHashTable(watch_descriptor_table, (void *)(intptr_t)wd, p, false)) { + if (!SDL_InsertIntoHashTable(watch_descriptor_table, (void *)(intptr_t)wd, watch_entry, false)) { inotify_rm_watch(inotify_fd, wd); SDL_UnlockMutex(file_watch_lock); - SDL_free(p); + SDL_free(watch_entry); return false; } SDL_UnlockMutex(file_watch_lock); @@ -495,10 +519,27 @@ bool SDL_SYS_WatchFileForChanges(const char *path) #endif // HAVE_INOTIFY } -void SDL_SYS_UpdateFileWatch(void) -{ #ifdef HAVE_INOTIFY - if (inotify_fd >= 0) { +static void SendFileWatchEvent(SDL_EventType event_type, const char *path) { + if (SDL_EventEnabled(event_type)) { + SDL_Event event; + SDL_zero(event); + event.type = event_type; + event.common.timestamp = 0; + if (path) { + event.file_watch.path = SDL_CreateTemporaryString(path); + if (!event.file_watch.path) { + return; + } + } + SDL_PushEvent(&event); + } +} + +static int SDL_FileWatchThread(void *userdata) +{ + while (SDL_GetAtomicInt(&quit_watch_file) == 0) { + SDL_Delay(100); SDL_LockMutex(file_watch_lock); union { @@ -518,31 +559,21 @@ void SDL_SYS_UpdateFileWatch(void) } while (remain > 0) { - const char *watched_path; - if (SDL_FindInHashTable(watch_descriptor_table, (void *)(intptr_t)buf.event.wd, (const void **)&watched_path)) { - const char *path_tmp; - SDL_EventType event_type; - bool post_event = true; + const WatchEntry *watch_entry; + if (SDL_FindInHashTable(watch_descriptor_table, (void *)(intptr_t)buf.event.wd, (const void **)&watch_entry)) { if (buf.event.mask & IN_Q_OVERFLOW) { - event_type = SDL_EVENT_FILE_WATCH_ERROR; - path_tmp = NULL; + SendFileWatchEvent(SDL_EVENT_FILE_WATCH_ERROR, NULL); } else if (buf.event.len != 0) { - (void)SDL_snprintf(path, SDL_arraysize(path), "%s/%s", watched_path, buf.event.name); - event_type = SDL_EVENT_FILE_CHANGED; - path_tmp = SDL_CreateTemporaryString(path); - post_event = (path_tmp != NULL); + (void)SDL_snprintf(path, SDL_arraysize(path), "%s/%s", watch_entry->path, buf.event.name); + if (watch_entry->callback) { + watch_entry->callback(watch_entry->user_data, path); + } + SendFileWatchEvent(SDL_EVENT_FILE_CHANGED, path); } else { - event_type = SDL_EVENT_FILE_CHANGED; - path_tmp = SDL_CreateTemporaryString(watched_path); - post_event = (path_tmp != NULL); - } - if (post_event && SDL_EventEnabled(event_type)) { - SDL_Event event; - SDL_zero(event); - event.type = event_type; - event.common.timestamp = 0; - event.file_watch.path = path_tmp; - SDL_PushEvent(&event); + if (watch_entry->callback) { + watch_entry->callback(watch_entry->user_data, watch_entry->path); + } + SendFileWatchEvent(SDL_EVENT_FILE_CHANGED, watch_entry->path); } } @@ -555,13 +586,16 @@ void SDL_SYS_UpdateFileWatch(void) } SDL_UnlockMutex(file_watch_lock); } -#endif // HAVE_INOTIFY + return 0; } +#endif // HAVE_INOTIFY void SDL_SYS_QuitFileWatch(void) { #ifdef HAVE_INOTIFY if (inotify_fd >= 0) { + SDL_SetAtomicInt(&quit_watch_file, 0); + SDL_WaitThread(file_watch_thread, NULL); close(inotify_fd); inotify_fd = -1; SDL_DestroyMutex(file_watch_lock); diff --git a/src/filesystem/windows/SDL_sysfsops.c b/src/filesystem/windows/SDL_sysfsops.c index f8b4f1e3ea..a8a115c8b5 100644 --- a/src/filesystem/windows/SDL_sysfsops.c +++ b/src/filesystem/windows/SDL_sysfsops.c @@ -231,15 +231,11 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) return true; } -bool SDL_SYS_WatchFileForChanges(const char *path) +bool SDL_SYS_WatchFileForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata) { return SDL_Unsupported(); } -void SDL_SYS_UpdateFileWatch(void) -{ -} - void SDL_SYS_QuitFileWatch(void) { } From ba99fdcf629eb9d0bbed88c19969fbe1e9929dcc Mon Sep 17 00:00:00 2001 From: Mathieu Eyraud <70028899+meyraud705@users.noreply.github.com> Date: Tue, 21 Apr 2026 23:59:49 +0200 Subject: [PATCH 3/7] Apply suggestions from code review Co-authored-by: Sam Lantinga --- include/SDL3/SDL_events.h | 2 +- include/SDL3/SDL_filesystem.h | 2 +- src/filesystem/SDL_sysfilesystem.h | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h index 9d84574c83..d4bad77202 100644 --- a/include/SDL3/SDL_events.h +++ b/include/SDL3/SDL_events.h @@ -983,7 +983,7 @@ typedef struct SDL_SensorEvent */ typedef struct SDL_FileWatchEvent { - SDL_EventType type; /**< SDL_EVENT_FILE_WATCH_ERROR or SDL_EVENT_FILE_CHANGED */ + SDL_EventType type; /**< SDL_EVENT_FILE_CHANGED or SDL_EVENT_FILE_WATCH_ERROR */ Uint32 reserved; Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */ const char *path; /**< Path of the modified file for SDL_EVENT_FILE_CHANGED, NULL for SDL_EVENT_FILE_WATCH_ERROR */ diff --git a/include/SDL3/SDL_filesystem.h b/include/SDL3/SDL_filesystem.h index 94cb272030..fd5b39180e 100644 --- a/include/SDL3/SDL_filesystem.h +++ b/include/SDL3/SDL_filesystem.h @@ -559,7 +559,7 @@ typedef void (SDLCALL *SDL_FileWatchCallback)(void *userdata, const char *path); * * \sa SDL_FileWatchEvent */ -extern SDL_DECLSPEC bool SDLCALL SDL_WatchFileForChanges(const char *path, SDL_FileWatchCallback callback, void *userdata); +extern SDL_DECLSPEC bool SDLCALL SDL_WatchPathForChanges(const char *path, SDL_FileWatchCallback callback, void *userdata); /* Ends C function definitions when using C++ */ #ifdef __cplusplus diff --git a/src/filesystem/SDL_sysfilesystem.h b/src/filesystem/SDL_sysfilesystem.h index 8b9d1baa13..31eb4fcffb 100644 --- a/src/filesystem/SDL_sysfilesystem.h +++ b/src/filesystem/SDL_sysfilesystem.h @@ -35,8 +35,8 @@ extern bool SDL_SYS_CopyFile(const char *oldpath, const char *newpath); extern bool SDL_SYS_CreateDirectory(const char *path); extern bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info); -extern bool SDL_SYS_WatchFileForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata); -extern void SDL_SYS_QuitFileWatch(void); +extern bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata); +extern void SDL_SYS_QuitPathWatch(void); typedef bool (*SDL_GlobEnumeratorFunc)(const char *path, SDL_EnumerateDirectoryCallback cb, void *cbuserdata, void *userdata); typedef bool (*SDL_GlobGetPathInfoFunc)(const char *path, SDL_PathInfo *info, void *userdata); From ca00492d6164f0ab423716e1ebec0d41391dc1b3 Mon Sep 17 00:00:00 2001 From: meyraud705 Date: Wed, 22 Apr 2026 00:14:09 +0200 Subject: [PATCH 4/7] Rename WatchFile -> WatchPath --- include/SDL3/SDL_events.h | 4 ++-- include/SDL3/SDL_filesystem.h | 2 +- src/filesystem/SDL_filesystem.c | 6 +++--- src/filesystem/dummy/SDL_sysfsops.c | 4 ++-- src/filesystem/posix/SDL_sysfsops.c | 4 ++-- src/filesystem/windows/SDL_sysfsops.c | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h index d4bad77202..7a8d4dd774 100644 --- a/include/SDL3/SDL_events.h +++ b/include/SDL3/SDL_events.h @@ -977,9 +977,9 @@ typedef struct SDL_SensorEvent /** * File watch event structure (event.file_watch.*) * - * You can add file to the watch list with SDL_WatchFileForChanges(). + * You can add file to the watch list with SDL_WatchPathForChanges(). * - * \sa SDL_WatchFileForChanges + * \sa SDL_WatchPathForChanges */ typedef struct SDL_FileWatchEvent { diff --git a/include/SDL3/SDL_filesystem.h b/include/SDL3/SDL_filesystem.h index fd5b39180e..cf61e294cd 100644 --- a/include/SDL3/SDL_filesystem.h +++ b/include/SDL3/SDL_filesystem.h @@ -532,7 +532,7 @@ extern SDL_DECLSPEC char * SDLCALL SDL_GetCurrentDirectory(void); /** * A function pointer used for callbacks that watch for files change. * - * \param userdata what was passed as `userdata` to SDL_WatchFileForChanges(). + * \param userdata what was passed as `userdata` to SDL_WatchPathForChanges(). * \param path path of file that was modified. * * \threadsafety SDL may call this callback at any time from any thread; the diff --git a/src/filesystem/SDL_filesystem.c b/src/filesystem/SDL_filesystem.c index 646c78b606..e29e8ae46c 100644 --- a/src/filesystem/SDL_filesystem.c +++ b/src/filesystem/SDL_filesystem.c @@ -142,12 +142,12 @@ bool SDL_GetPathInfo(const char *path, SDL_PathInfo *info) return SDL_SYS_GetPathInfo(path, info); } -bool SDL_WatchFileForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata) +bool SDL_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata) { CHECK_PARAM(!path) { return SDL_InvalidParamError("path"); } - return SDL_SYS_WatchFileForChanges(path, cb, userdata); + return SDL_SYS_WatchPathForChanges(path, cb, userdata); } static bool EverythingMatch(const char *pattern, const char *str, bool *matched_to_dir) @@ -555,6 +555,6 @@ void SDL_QuitFilesystem(void) CachedUserFolders[i] = NULL; } } - SDL_SYS_QuitFileWatch(); + SDL_SYS_QuitPathWatch(); } diff --git a/src/filesystem/dummy/SDL_sysfsops.c b/src/filesystem/dummy/SDL_sysfsops.c index d52bf6ef55..30ed7779ae 100644 --- a/src/filesystem/dummy/SDL_sysfsops.c +++ b/src/filesystem/dummy/SDL_sysfsops.c @@ -58,12 +58,12 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) return SDL_Unsupported(); } -bool SDL_SYS_WatchFileForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata) +bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata) { return SDL_Unsupported(); } -void SDL_SYS_QuitFileWatch(void) +void SDL_SYS_QuitPathWatch(void) { } diff --git a/src/filesystem/posix/SDL_sysfsops.c b/src/filesystem/posix/SDL_sysfsops.c index 72a566e4c8..4eaf9ada1b 100644 --- a/src/filesystem/posix/SDL_sysfsops.c +++ b/src/filesystem/posix/SDL_sysfsops.c @@ -453,7 +453,7 @@ static int SDL_inotify_init1(void) #endif // HAVE_INOTIFY_INIT1 #endif // HAVE_INOTIFY -bool SDL_SYS_WatchFileForChanges(const char *path, SDL_FileWatchCallback cb, void *user_data) +bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *user_data) { #ifdef HAVE_INOTIFY if (!watch_descriptor_table) { @@ -590,7 +590,7 @@ static int SDL_FileWatchThread(void *userdata) } #endif // HAVE_INOTIFY -void SDL_SYS_QuitFileWatch(void) +void SDL_SYS_QuitPathWatch(void) { #ifdef HAVE_INOTIFY if (inotify_fd >= 0) { diff --git a/src/filesystem/windows/SDL_sysfsops.c b/src/filesystem/windows/SDL_sysfsops.c index a8a115c8b5..7a6e9dd95f 100644 --- a/src/filesystem/windows/SDL_sysfsops.c +++ b/src/filesystem/windows/SDL_sysfsops.c @@ -231,12 +231,12 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) return true; } -bool SDL_SYS_WatchFileForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata) +bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata) { return SDL_Unsupported(); } -void SDL_SYS_QuitFileWatch(void) +void SDL_SYS_QuitPathWatch(void) { } From e74f9df537b7d808b50f85ee1306116cc9bba3cd Mon Sep 17 00:00:00 2001 From: meyraud705 Date: Wed, 22 Apr 2026 13:11:41 +0200 Subject: [PATCH 5/7] Reject empty path for path watch. Correctly launch and stop file watch thread. --- src/filesystem/SDL_filesystem.c | 3 +++ src/filesystem/posix/SDL_sysfsops.c | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/filesystem/SDL_filesystem.c b/src/filesystem/SDL_filesystem.c index e29e8ae46c..a0568ff95d 100644 --- a/src/filesystem/SDL_filesystem.c +++ b/src/filesystem/SDL_filesystem.c @@ -147,6 +147,9 @@ bool SDL_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *u CHECK_PARAM(!path) { return SDL_InvalidParamError("path"); } + CHECK_PARAM(path[0] == '\0') { + return SDL_InvalidParamError("path"); + } return SDL_SYS_WatchPathForChanges(path, cb, userdata); } diff --git a/src/filesystem/posix/SDL_sysfsops.c b/src/filesystem/posix/SDL_sysfsops.c index 4eaf9ada1b..4b9fdff624 100644 --- a/src/filesystem/posix/SDL_sysfsops.c +++ b/src/filesystem/posix/SDL_sysfsops.c @@ -475,8 +475,8 @@ bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, voi inotify_fd = -1; return false; } - file_watch_thread = SDL_CreateThread(SDL_FileWatchThread, "SDL_FileWatch", NULL); SDL_SetAtomicInt(&quit_watch_file, 0); + file_watch_thread = SDL_CreateThread(SDL_FileWatchThread, "SDL_FileWatch", NULL); if (!file_watch_thread) { SDL_DestroyHashTable(watch_descriptor_table); watch_descriptor_table = NULL; @@ -495,7 +495,8 @@ bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, voi watch_entry->callback = cb; watch_entry->user_data = user_data; SDL_memcpy(watch_entry->path, path, slen + 1); - // remove separator at the end of the path + // remove separator at the end of the path, it is added back when + // concatenating directory path and file name if (watch_entry->path[slen - 1] == '/') { watch_entry->path[slen - 1] = '\0'; } @@ -594,7 +595,7 @@ void SDL_SYS_QuitPathWatch(void) { #ifdef HAVE_INOTIFY if (inotify_fd >= 0) { - SDL_SetAtomicInt(&quit_watch_file, 0); + SDL_SetAtomicInt(&quit_watch_file, 1); SDL_WaitThread(file_watch_thread, NULL); close(inotify_fd); inotify_fd = -1; From 3d9e80029487496a58ca7dd43b2e4cdea44400fd Mon Sep 17 00:00:00 2001 From: Mathieu Eyraud <70028899+meyraud705@users.noreply.github.com> Date: Thu, 7 May 2026 21:33:58 +0200 Subject: [PATCH 6/7] Fix FileWatchThread call convention. Co-authored-by: Anonymous Maarten --- src/filesystem/posix/SDL_sysfsops.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filesystem/posix/SDL_sysfsops.c b/src/filesystem/posix/SDL_sysfsops.c index 4b9fdff624..a440b93447 100644 --- a/src/filesystem/posix/SDL_sysfsops.c +++ b/src/filesystem/posix/SDL_sysfsops.c @@ -429,7 +429,7 @@ typedef struct WatchEntry } WatchEntry; static SDL_HashTable *watch_descriptor_table = NULL; // stores WatchEntry for a watch descriptor -static int SDL_FileWatchThread(void *user_data); +static int SDLCALL SDL_FileWatchThread(void *user_data); static SDL_Thread *file_watch_thread = NULL; static SDL_Mutex *file_watch_lock = NULL; static SDL_AtomicInt quit_watch_file; From 0ea3074fa14fa43a6ddd6eaeb9f09c78eefbc114 Mon Sep 17 00:00:00 2001 From: meyraud705 Date: Thu, 7 May 2026 21:30:39 +0200 Subject: [PATCH 7/7] Add SDL_RemovePathWatch(). Rename SDL_WatchPathForChange() to SDL_AddPathWatch(). Improve documentation. --- include/SDL3/SDL_events.h | 12 ++--- include/SDL3/SDL_filesystem.h | 27 ++++++++-- src/filesystem/SDL_filesystem.c | 15 +++++- src/filesystem/SDL_sysfilesystem.h | 3 +- src/filesystem/dummy/SDL_sysfsops.c | 6 ++- src/filesystem/posix/SDL_sysfsops.c | 75 +++++++++++++++++++++++++-- src/filesystem/windows/SDL_sysfsops.c | 6 ++- 7 files changed, 123 insertions(+), 21 deletions(-) diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h index 7a8d4dd774..baa5b5a35a 100644 --- a/include/SDL3/SDL_events.h +++ b/include/SDL3/SDL_events.h @@ -260,8 +260,8 @@ typedef enum SDL_EventType SDL_EVENT_CAMERA_DEVICE_DENIED, /**< A camera device has been denied for use by the user. */ /* File watch events */ - SDL_EVENT_FILE_CHANGED = 0x1500, /**< A watched file was written. */ - SDL_EVENT_FILE_WATCH_ERROR, /**< Watched files may have been modified, but the events are lost. */ + SDL_EVENT_FILE_DATA_WRITTEN = 0x1500, /**< Data was written in a watched file. */ + SDL_EVENT_FILE_WATCH_ERROR, /**< Watched files may have been modified, but the events are lost. */ /* Render events */ SDL_EVENT_RENDER_TARGETS_RESET = 0x2000, /**< The render targets have been reset and their contents need to be updated */ @@ -977,16 +977,16 @@ typedef struct SDL_SensorEvent /** * File watch event structure (event.file_watch.*) * - * You can add file to the watch list with SDL_WatchPathForChanges(). + * You can add file to the watch list with SDL_AddPathWatch(). * - * \sa SDL_WatchPathForChanges + * \sa SDL_AddPathWatch */ typedef struct SDL_FileWatchEvent { - SDL_EventType type; /**< SDL_EVENT_FILE_CHANGED or SDL_EVENT_FILE_WATCH_ERROR */ + SDL_EventType type; /**< SDL_EVENT_FILE_DATA_WRITTEN or SDL_EVENT_FILE_WATCH_ERROR */ Uint32 reserved; Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */ - const char *path; /**< Path of the modified file for SDL_EVENT_FILE_CHANGED, NULL for SDL_EVENT_FILE_WATCH_ERROR */ + const char *path; /**< Path of the modified file for SDL_EVENT_FILE_DATA_WRITTEN, NULL for SDL_EVENT_FILE_WATCH_ERROR */ } SDL_FileWatchEvent; /** diff --git a/include/SDL3/SDL_filesystem.h b/include/SDL3/SDL_filesystem.h index cf61e294cd..ec127c1857 100644 --- a/include/SDL3/SDL_filesystem.h +++ b/include/SDL3/SDL_filesystem.h @@ -532,7 +532,7 @@ extern SDL_DECLSPEC char * SDLCALL SDL_GetCurrentDirectory(void); /** * A function pointer used for callbacks that watch for files change. * - * \param userdata what was passed as `userdata` to SDL_WatchPathForChanges(). + * \param userdata what was passed as `userdata` to SDL_AddPathWatch(). * \param path path of file that was modified. * * \threadsafety SDL may call this callback at any time from any thread; the @@ -543,9 +543,9 @@ typedef void (SDLCALL *SDL_FileWatchCallback)(void *userdata, const char *path); /** * This function adds a file watcher that will fires an app-provided callback - * and send an SDL_EVENT_FILE_CHANGED event every time the file is modified. If - * path is a directory, the callback will be called for every file modified in - * that directory. + * and send an SDL_EVENT_FILE_CHANGED event every time data is written in the + * file. If path is a directory, the callback will be called for every file + * in that directory that has data written into. * * \param path file or directory path to watch. * \param callback a function that is called when the watched file is modified. @@ -558,8 +558,25 @@ typedef void (SDLCALL *SDL_FileWatchCallback)(void *userdata, const char *path); * \threadsafety It is safe to call this function from any thread. * * \sa SDL_FileWatchEvent + * \sa SDL_RemovePathWatch */ -extern SDL_DECLSPEC bool SDLCALL SDL_WatchPathForChanges(const char *path, SDL_FileWatchCallback callback, void *userdata); +extern SDL_DECLSPEC bool SDLCALL SDL_AddPathWatch(const char *path, SDL_FileWatchCallback callback, void *userdata); + +/** + * Remove an file watch callback added with SDL_AddPathWatch(). + * + * This function takes the same input as SDL_AddPathWatch() to identify and + * delete the corresponding callback. + * + * \param path the path originally passed to SDL_AddPathWatch(). + * \param callback the callback originally passed to SDL_AddPathWatch(). + * \param userdata the pointer originally passed to SDL_AddPathWatch(). + * + * \threadsafety It is safe to call this function from any thread. + * + * \sa SDL_AddPathWatch + */ +extern SDL_DECLSPEC void SDLCALL SDL_RemovePathWatch(const char *path, SDL_FileWatchCallback callback, void *userdata); /* Ends C function definitions when using C++ */ #ifdef __cplusplus diff --git a/src/filesystem/SDL_filesystem.c b/src/filesystem/SDL_filesystem.c index a0568ff95d..9dce77b4a4 100644 --- a/src/filesystem/SDL_filesystem.c +++ b/src/filesystem/SDL_filesystem.c @@ -142,7 +142,7 @@ bool SDL_GetPathInfo(const char *path, SDL_PathInfo *info) return SDL_SYS_GetPathInfo(path, info); } -bool SDL_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata) +bool SDL_AddPathWatch(const char *path, SDL_FileWatchCallback cb, void *userdata) { CHECK_PARAM(!path) { return SDL_InvalidParamError("path"); @@ -150,7 +150,18 @@ bool SDL_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *u CHECK_PARAM(path[0] == '\0') { return SDL_InvalidParamError("path"); } - return SDL_SYS_WatchPathForChanges(path, cb, userdata); + return SDL_SYS_AddPathWatch(path, cb, userdata); +} + +void SDL_RemovePathWatch(const char *path, SDL_FileWatchCallback cb, void *userdata) +{ + CHECK_PARAM(!path) { + return; + } + CHECK_PARAM(path[0] == '\0') { + return; + } + SDL_SYS_RemovePathWatch(path, cb, userdata); } static bool EverythingMatch(const char *pattern, const char *str, bool *matched_to_dir) diff --git a/src/filesystem/SDL_sysfilesystem.h b/src/filesystem/SDL_sysfilesystem.h index 31eb4fcffb..3af35e891d 100644 --- a/src/filesystem/SDL_sysfilesystem.h +++ b/src/filesystem/SDL_sysfilesystem.h @@ -35,7 +35,8 @@ extern bool SDL_SYS_CopyFile(const char *oldpath, const char *newpath); extern bool SDL_SYS_CreateDirectory(const char *path); extern bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info); -extern bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata); +extern bool SDL_SYS_AddPathWatch(const char *path, SDL_FileWatchCallback cb, void *userdata); +extern void SDL_SYS_RemovePathWatch(const char *path, SDL_FileWatchCallback cb, void *userdata); extern void SDL_SYS_QuitPathWatch(void); typedef bool (*SDL_GlobEnumeratorFunc)(const char *path, SDL_EnumerateDirectoryCallback cb, void *cbuserdata, void *userdata); diff --git a/src/filesystem/dummy/SDL_sysfsops.c b/src/filesystem/dummy/SDL_sysfsops.c index 30ed7779ae..bf5b1af120 100644 --- a/src/filesystem/dummy/SDL_sysfsops.c +++ b/src/filesystem/dummy/SDL_sysfsops.c @@ -58,11 +58,15 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) return SDL_Unsupported(); } -bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata) +bool SDL_SYS_AddPathWatch(const char *path, SDL_FileWatchCallback cb, void *userdata) { return SDL_Unsupported(); } +void SDL_SYS_RemovePathWatch(const char *path, SDL_FileWatchCallback cb, void *userdata) +{ +} + void SDL_SYS_QuitPathWatch(void) { } diff --git a/src/filesystem/posix/SDL_sysfsops.c b/src/filesystem/posix/SDL_sysfsops.c index a440b93447..a2a1fe7974 100644 --- a/src/filesystem/posix/SDL_sysfsops.c +++ b/src/filesystem/posix/SDL_sysfsops.c @@ -451,9 +451,10 @@ static int SDL_inotify_init1(void) return fd; } #endif // HAVE_INOTIFY_INIT1 + #endif // HAVE_INOTIFY -bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *user_data) +bool SDL_SYS_AddPathWatch(const char *path, SDL_FileWatchCallback cb, void *user_data) { #ifdef HAVE_INOTIFY if (!watch_descriptor_table) { @@ -487,7 +488,11 @@ bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, voi return false; } } + const size_t slen = SDL_strlen(path); + if (slen >= PATH_MAX) { + return SDL_SetError("path too long"); + } WatchEntry *watch_entry = SDL_malloc(sizeof(*watch_entry) + slen + 1); if (!watch_entry) { return false; @@ -507,7 +512,7 @@ bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, voi SDL_free(watch_entry); return SDL_SetError("inotify_add_watch failed: %s", strerror(errno)); } - if (!SDL_InsertIntoHashTable(watch_descriptor_table, (void *)(intptr_t)wd, watch_entry, false)) { + if (!SDL_InsertIntoHashTable(watch_descriptor_table, (void *) (intptr_t) wd, watch_entry, false)) { inotify_rm_watch(inotify_fd, wd); SDL_UnlockMutex(file_watch_lock); SDL_free(watch_entry); @@ -561,20 +566,24 @@ static int SDL_FileWatchThread(void *userdata) while (remain > 0) { const WatchEntry *watch_entry; - if (SDL_FindInHashTable(watch_descriptor_table, (void *)(intptr_t)buf.event.wd, (const void **)&watch_entry)) { + if (SDL_FindInHashTable(watch_descriptor_table, (void *) (intptr_t) buf.event.wd, (const void **) &watch_entry)) { if (buf.event.mask & IN_Q_OVERFLOW) { SendFileWatchEvent(SDL_EVENT_FILE_WATCH_ERROR, NULL); + } else if (buf.event.mask & IN_IGNORED) { + // removing a watch generate an IN_IGNORED event + } else if (buf.event.mask & IN_UNMOUNT) { + // file system containing watched path was unmounted } else if (buf.event.len != 0) { (void)SDL_snprintf(path, SDL_arraysize(path), "%s/%s", watch_entry->path, buf.event.name); if (watch_entry->callback) { watch_entry->callback(watch_entry->user_data, path); } - SendFileWatchEvent(SDL_EVENT_FILE_CHANGED, path); + SendFileWatchEvent(SDL_EVENT_FILE_DATA_WRITTEN, path); } else { if (watch_entry->callback) { watch_entry->callback(watch_entry->user_data, watch_entry->path); } - SendFileWatchEvent(SDL_EVENT_FILE_CHANGED, watch_entry->path); + SendFileWatchEvent(SDL_EVENT_FILE_DATA_WRITTEN, watch_entry->path); } } @@ -589,8 +598,64 @@ static int SDL_FileWatchThread(void *userdata) } return 0; } + +typedef struct FindWatchEntryByValueData { + WatchEntry *entry_to_find; + const void *key_found; +} FindWatchEntryByValueData; + +static bool SDLCALL FindWatchEntryByValue(void *userdata, const SDL_HashTable *table, const void *key, const void *value) +{ + FindWatchEntryByValueData *d = (FindWatchEntryByValueData *) userdata; + const WatchEntry *iterator = (const WatchEntry *) value; + if (SDL_strcmp(iterator->path, d->entry_to_find->path) == 0 + && iterator->callback == d->entry_to_find->user_data + && iterator->user_data == d->entry_to_find->user_data) { + d->key_found = key; + return false; + } + return true; +} #endif // HAVE_INOTIFY +void SDL_SYS_RemovePathWatch(const char *path, SDL_FileWatchCallback cb, void *user_data) +{ +#ifdef HAVE_INOTIFY + if (!watch_descriptor_table) { + return; + } + const size_t slen = SDL_strlen(path); + if (slen >= PATH_MAX) { + return; + } + + union LongestWatchEntry{ + WatchEntry entry; + char enougn_for_longest_path[sizeof(WatchEntry) + PATH_MAX]; + } watch_entry; + watch_entry.entry.callback = cb; + watch_entry.entry.user_data = user_data; + SDL_memcpy(watch_entry.entry.path, path, slen + 1); + // remove separator at the end of the path, it is added back when + // concatenating directory path and file name + if (watch_entry.entry.path[slen - 1] == '/') { + watch_entry.entry.path[slen - 1] = '\0'; + } + + FindWatchEntryByValueData data = {&watch_entry.entry, (void *) (intptr_t) -1}; + SDL_LockMutex(file_watch_lock); + if (!SDL_IterateHashTable(watch_descriptor_table, FindWatchEntryByValue, &data)) { + SDL_UnlockMutex(file_watch_lock); + return; + } + if (data.key_found != (void *) (intptr_t) -1) { + SDL_RemoveFromHashTable(watch_descriptor_table, data.key_found); + inotify_rm_watch(inotify_fd, (int) (intptr_t) data.key_found); + } + SDL_UnlockMutex(file_watch_lock); +#endif // HAVE_INOTIFY +} + void SDL_SYS_QuitPathWatch(void) { #ifdef HAVE_INOTIFY diff --git a/src/filesystem/windows/SDL_sysfsops.c b/src/filesystem/windows/SDL_sysfsops.c index 7a6e9dd95f..b5dde464b6 100644 --- a/src/filesystem/windows/SDL_sysfsops.c +++ b/src/filesystem/windows/SDL_sysfsops.c @@ -231,11 +231,15 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) return true; } -bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata) +bool SDL_SYS_AddPathWatch(const char *path, SDL_FileWatchCallback cb, void *userdata) { return SDL_Unsupported(); } +void SDL_SYS_RemovePathWatch(const char *path, SDL_FileWatchCallback cb, void *userdata) +{ +} + void SDL_SYS_QuitPathWatch(void) { }