-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Add file watcher API and Linux implementation #15342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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_WatchPathForChanges(). | ||
| * \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. | ||
|
Comment on lines
+545
to
+548
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Does this include the directory? Or only its entries? |
||
| * | ||
| * \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_WatchPathForChanges(const char *path, SDL_FileWatchCallback callback, void *userdata); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to remove a file watcher?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yes, this should be AddPathWatch() with a corresponding RemovePathWatch() |
||
|
|
||
| /* Ends C function definitions when using C++ */ | ||
| #ifdef __cplusplus | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,7 +26,11 @@ | |||||||||||||
| /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ | ||||||||||||||
| // System dependent filesystem routines | ||||||||||||||
|
|
||||||||||||||
| #include <SDL3/SDL_atomic.h> | ||||||||||||||
| #include <SDL3/SDL_thread.h> | ||||||||||||||
| #include "../SDL_sysfilesystem.h" | ||||||||||||||
| #include "../../SDL_hashtable.h" | ||||||||||||||
| #include "../../events/SDL_events_c.h" | ||||||||||||||
|
|
||||||||||||||
| #include <stdio.h> | ||||||||||||||
| #include <string.h> | ||||||||||||||
|
|
@@ -39,6 +43,11 @@ | |||||||||||||
| #include "../../core/android/SDL_android.h" | ||||||||||||||
| #endif | ||||||||||||||
|
|
||||||||||||||
| #ifdef HAVE_INOTIFY | ||||||||||||||
| #include <sys/inotify.h> | ||||||||||||||
| #include <fcntl.h> | ||||||||||||||
| #include <limits.h> | ||||||||||||||
| #endif | ||||||||||||||
|
|
||||||||||||||
| bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata) | ||||||||||||||
| { | ||||||||||||||
|
|
@@ -410,6 +419,193 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info) | |||||||||||||
| return true; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #ifdef HAVE_INOTIFY | ||||||||||||||
| static int inotify_fd = -1; | ||||||||||||||
| 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); | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
| 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) | ||||||||||||||
| { | ||||||||||||||
| 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_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *user_data) | ||||||||||||||
| { | ||||||||||||||
| #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; | ||||||||||||||
| } | ||||||||||||||
| 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; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| 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 | ||||||||||||||
| if (watch_entry->path[slen - 1] == '/') { | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also needs a test for
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, we should check and reject "" as a path. |
||||||||||||||
| watch_entry->path[slen - 1] = '\0'; | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+499
to
+501
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this remove all trailing slashes?
Suggested change
|
||||||||||||||
| SDL_LockMutex(file_watch_lock); | ||||||||||||||
| int wd = inotify_add_watch(inotify_fd, path, IN_MODIFY); | ||||||||||||||
| if (wd == -1) { | ||||||||||||||
| SDL_UnlockMutex(file_watch_lock); | ||||||||||||||
| 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)) { | ||||||||||||||
| inotify_rm_watch(inotify_fd, wd); | ||||||||||||||
| SDL_UnlockMutex(file_watch_lock); | ||||||||||||||
| SDL_free(watch_entry); | ||||||||||||||
| return false; | ||||||||||||||
| } | ||||||||||||||
| SDL_UnlockMutex(file_watch_lock); | ||||||||||||||
| return true; | ||||||||||||||
| #else | ||||||||||||||
| return SDL_Unsupported(); | ||||||||||||||
| #endif // HAVE_INOTIFY | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #ifdef HAVE_INOTIFY | ||||||||||||||
| 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 | ||||||||||||||
| { | ||||||||||||||
| 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 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) { | ||||||||||||||
| SendFileWatchEvent(SDL_EVENT_FILE_WATCH_ERROR, NULL); | ||||||||||||||
| } 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); | ||||||||||||||
| } else { | ||||||||||||||
| if (watch_entry->callback) { | ||||||||||||||
| watch_entry->callback(watch_entry->user_data, watch_entry->path); | ||||||||||||||
| } | ||||||||||||||
| SendFileWatchEvent(SDL_EVENT_FILE_CHANGED, watch_entry->path); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| 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); | ||||||||||||||
| } | ||||||||||||||
| return 0; | ||||||||||||||
| } | ||||||||||||||
| #endif // HAVE_INOTIFY | ||||||||||||||
|
|
||||||||||||||
| void SDL_SYS_QuitPathWatch(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); | ||||||||||||||
| 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) | ||||||||||||||
| { | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about
SDL_EVENT_FILE_WATCH_OVERFLOW?Or do file watchers have multiple error conditions?