Skip to content

Commit c69f745

Browse files
committed
Add file watcher API and Linux implementation
1 parent 1442c5a commit c69f745

8 files changed

Lines changed: 225 additions & 0 deletions

File tree

include/SDL3/SDL_events.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ typedef enum SDL_EventType
259259
SDL_EVENT_CAMERA_DEVICE_APPROVED, /**< A camera device has been approved for use by the user. */
260260
SDL_EVENT_CAMERA_DEVICE_DENIED, /**< A camera device has been denied for use by the user. */
261261

262+
/* File watch events */
263+
SDL_EVENT_FILE_WATCH_ERROR = 0x1500, /**< Watched files may have been modified, but the events are lost. */
264+
SDL_EVENT_FILE_CHANGED, /**< A watched file was written. */
265+
262266
/* Render events */
263267
SDL_EVENT_RENDER_TARGETS_RESET = 0x2000, /**< The render targets have been reset and their contents need to be updated */
264268
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
970974
Uint64 sensor_timestamp; /**< The timestamp of the sensor reading in nanoseconds, not necessarily synchronized with the system clock */
971975
} SDL_SensorEvent;
972976

977+
/**
978+
* File watch event structure (event.file_watch.*)
979+
*/
980+
typedef struct SDL_FileWatchEvent
981+
{
982+
SDL_EventType type; /**< SDL_EVENT_FILE_WATCH_ERROR or SDL_EVENT_FILE_CHANGED */
983+
Uint32 reserved;
984+
Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */
985+
const char* path; /**< Path of the modified file for SDL_EVENT_FILE_CHANGED, NULL for SDL_EVENT_FILE_WATCH_ERROR */
986+
} SDL_FileWatchEvent;
987+
973988
/**
974989
* The "quit requested" event
975990
*
@@ -1054,6 +1069,7 @@ typedef union SDL_Event
10541069
SDL_RenderEvent render; /**< Render event data */
10551070
SDL_DropEvent drop; /**< Drag and drop event data */
10561071
SDL_ClipboardEvent clipboard; /**< Clipboard event data */
1072+
SDL_FileWatchEvent file_watch; /**< File watch event data */
10571073

10581074
/* This is necessary for ABI compatibility between Visual C++ and GCC.
10591075
Visual C++ will respect the push pack pragma and use 52 bytes (size of

src/events/SDL_events.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "../SDL_hints_c.h"
2929
#include "../audio/SDL_audio_c.h"
3030
#include "../camera/SDL_camera_c.h"
31+
#include "../filesystem/SDL_filesystem_c.h"
3132
#include "../timer/SDL_timer_c.h"
3233
#include "../core/linux/SDL_udev.h"
3334
#ifndef SDL_JOYSTICK_DISABLED
@@ -1458,6 +1459,8 @@ bool SDL_RunOnMainThread(SDL_MainThreadCallback callback, void *userdata, bool w
14581459

14591460
void SDL_PumpEventMaintenance(void)
14601461
{
1462+
SDL_UpdateFileWatch();
1463+
14611464
#ifdef SDL_USE_LIBUDEV
14621465
SDL_UDEV_Poll();
14631466
#endif

src/filesystem/SDL_filesystem.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,19 @@ bool SDL_GetPathInfo(const char *path, SDL_PathInfo *info)
142142
return SDL_SYS_GetPathInfo(path, info);
143143
}
144144

145+
bool SDL_WatchFileForChanges(const char *path)
146+
{
147+
CHECK_PARAM(!path) {
148+
return SDL_InvalidParamError("path");
149+
}
150+
return SDL_SYS_WatchFileForChanges(path);
151+
}
152+
153+
void SDL_UpdateFileWatch(void)
154+
{
155+
SDL_SYS_UpdateFileWatch();
156+
}
157+
145158
static bool EverythingMatch(const char *pattern, const char *str, bool *matched_to_dir)
146159
{
147160
SDL_assert(pattern == NULL);
@@ -547,5 +560,6 @@ void SDL_QuitFilesystem(void)
547560
CachedUserFolders[i] = NULL;
548561
}
549562
}
563+
SDL_SYS_QuitFileWatch();
550564
}
551565

src/filesystem/SDL_filesystem_c.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@
2525
extern void SDL_InitFilesystem(void);
2626
extern void SDL_QuitFilesystem(void);
2727

28+
extern void SDL_UpdateFileWatch(void);
2829
#endif
2930

src/filesystem/SDL_sysfilesystem.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ extern bool SDL_SYS_CopyFile(const char *oldpath, const char *newpath);
3535
extern bool SDL_SYS_CreateDirectory(const char *path);
3636
extern bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info);
3737

38+
extern bool SDL_SYS_WatchFileForChanges(const char *path);
39+
extern void SDL_SYS_UpdateFileWatch(void);
40+
extern void SDL_SYS_QuitFileWatch(void);
41+
3842
typedef bool (*SDL_GlobEnumeratorFunc)(const char *path, SDL_EnumerateDirectoryCallback cb, void *cbuserdata, void *userdata);
3943
typedef bool (*SDL_GlobGetPathInfoFunc)(const char *path, SDL_PathInfo *info, void *userdata);
4044
extern char **SDL_InternalGlobDirectory(const char *path, const char *pattern, SDL_GlobFlags flags, int *count, SDL_GlobEnumeratorFunc enumerator, SDL_GlobGetPathInfoFunc getpathinfo, void *userdata);

src/filesystem/dummy/SDL_sysfsops.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,18 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info)
5858
return SDL_Unsupported();
5959
}
6060

61+
bool SDL_SYS_WatchFileForChanges(const char *path)
62+
{
63+
return SDL_Unsupported();
64+
}
65+
66+
void SDL_SYS_UpdateFileWatch(void)
67+
{
68+
}
69+
70+
void SDL_SYS_QuitFileWatch(void)
71+
{
72+
}
73+
6174
#endif // SDL_FSOPS_DUMMY
6275

src/filesystem/posix/SDL_sysfsops.c

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
// System dependent filesystem routines
2828

2929
#include "../SDL_sysfilesystem.h"
30+
#include "../../SDL_hashtable.h"
31+
#include "../../events/SDL_events_c.h"
3032

3133
#include <stdio.h>
3234
#include <string.h>
@@ -39,6 +41,10 @@
3941
#include "../../core/android/SDL_android.h"
4042
#endif
4143

44+
#ifdef HAVE_INOTIFY
45+
#include <sys/inotify.h>
46+
#include <linux/limits.h>
47+
#endif
4248

4349
bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata)
4450
{
@@ -410,6 +416,161 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info)
410416
return true;
411417
}
412418

419+
#ifdef HAVE_INOTIFY
420+
static int inotify_fd = -1;
421+
static SDL_HashTable *watch_descriptor_table = NULL; // stores directory or file path for a watch descriptor
422+
static SDL_Mutex *file_watch_lock = NULL;
423+
424+
#ifdef HAVE_INOTIFY_INIT1
425+
static int SDL_inotify_init1(void)
426+
{
427+
return inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
428+
}
429+
#else
430+
static int SDL_inotify_init1(void)
431+
{
432+
int fd = inotify_init();
433+
if (fd < 0) {
434+
return -1;
435+
}
436+
fcntl(fd, F_SETFL, O_NONBLOCK);
437+
fcntl(fd, F_SETFD, FD_CLOEXEC);
438+
return fd;
439+
}
440+
#endif // HAVE_INOTIFY_INIT1
441+
#endif // HAVE_INOTIFY
442+
443+
bool SDL_SYS_WatchFileForChanges(const char *path)
444+
{
445+
#ifdef HAVE_INOTIFY
446+
if (!watch_descriptor_table) {
447+
watch_descriptor_table = SDL_CreateHashTable(0, false, SDL_HashID, SDL_KeyMatchID, SDL_DestroyHashValue, NULL);
448+
if (!watch_descriptor_table) {
449+
return false;
450+
}
451+
inotify_fd = SDL_inotify_init1();
452+
if (inotify_fd == -1) {
453+
SDL_DestroyHashTable(watch_descriptor_table);
454+
watch_descriptor_table = NULL;
455+
return SDL_SetError("Could not initialize inotify: %s", strerror(errno));
456+
}
457+
file_watch_lock = SDL_CreateMutex();
458+
if (!file_watch_lock) {
459+
SDL_DestroyHashTable(watch_descriptor_table);
460+
watch_descriptor_table = NULL;
461+
close(inotify_fd);
462+
inotify_fd = -1;
463+
return false;
464+
}
465+
}
466+
467+
char *p = SDL_strdup(path);
468+
if (!p) {
469+
return false;
470+
}
471+
// remove separator at the end of the path
472+
const size_t slen = SDL_strlen(p);
473+
if (p[slen - 1] == '/') {
474+
p[slen - 1] = '\0';
475+
}
476+
477+
SDL_LockMutex(file_watch_lock);
478+
int wd = inotify_add_watch(inotify_fd, p, IN_MODIFY);
479+
if (wd == -1) {
480+
SDL_UnlockMutex(file_watch_lock);
481+
SDL_free(p);
482+
return SDL_SetError("inotify_add_watch failed: %s", strerror(errno));
483+
}
484+
if (!SDL_InsertIntoHashTable(watch_descriptor_table, (void *)(intptr_t)wd, p, false)) {
485+
inotify_rm_watch(inotify_fd, wd);
486+
SDL_UnlockMutex(file_watch_lock);
487+
SDL_free(p);
488+
return false;
489+
}
490+
SDL_UnlockMutex(file_watch_lock);
491+
return true;
492+
#else
493+
return SDL_Unsupported();
494+
#endif // HAVE_INOTIFY
495+
}
496+
497+
void SDL_SYS_UpdateFileWatch(void)
498+
{
499+
#ifdef HAVE_INOTIFY
500+
if (inotify_fd >= 0) {
501+
SDL_LockMutex(file_watch_lock);
502+
union
503+
{
504+
struct inotify_event event;
505+
char storage[4096];
506+
char enough_for_inotify[sizeof(struct inotify_event) + NAME_MAX + 1];
507+
} buf;
508+
ssize_t bytes;
509+
size_t remain = 0;
510+
size_t len;
511+
char path[PATH_MAX];
512+
513+
bytes = read(inotify_fd, &buf, sizeof(buf));
514+
515+
if (bytes > 0) {
516+
remain = (size_t)bytes;
517+
}
518+
519+
while (remain > 0) {
520+
const char *watched_path;
521+
if (SDL_FindInHashTable(watch_descriptor_table, (void *)(intptr_t)buf.event.wd, (const void **)&watched_path)) {
522+
const char *path_tmp;
523+
SDL_EventType event_type;
524+
bool post_event = true;
525+
if (buf.event.mask & IN_Q_OVERFLOW) {
526+
event_type = SDL_EVENT_FILE_WATCH_ERROR;
527+
path_tmp = NULL;
528+
} else if (buf.event.len != 0) {
529+
(void)SDL_snprintf(path, SDL_arraysize(path), "%s/%s", watched_path, buf.event.name);
530+
event_type = SDL_EVENT_FILE_CHANGED;
531+
path_tmp = SDL_CreateTemporaryString(path);
532+
post_event = (path_tmp != NULL);
533+
} else {
534+
event_type = SDL_EVENT_FILE_CHANGED;
535+
path_tmp = SDL_CreateTemporaryString(watched_path);
536+
post_event = (path_tmp != NULL);
537+
}
538+
if (post_event && SDL_EventEnabled(event_type)) {
539+
SDL_Event event;
540+
SDL_zero(event);
541+
event.type = event_type;
542+
event.common.timestamp = 0;
543+
event.file_watch.path = path_tmp;
544+
SDL_PushEvent(&event);
545+
}
546+
}
547+
548+
len = sizeof(struct inotify_event) + buf.event.len;
549+
remain -= len;
550+
551+
if (remain != 0) {
552+
SDL_memmove(&buf.storage[0], &buf.storage[len], remain);
553+
}
554+
}
555+
SDL_UnlockMutex(file_watch_lock);
556+
}
557+
#endif // HAVE_INOTIFY
558+
}
559+
560+
void SDL_SYS_QuitFileWatch(void)
561+
{
562+
#ifdef HAVE_INOTIFY
563+
if (inotify_fd >= 0) {
564+
close(inotify_fd);
565+
inotify_fd = -1;
566+
SDL_DestroyMutex(file_watch_lock);
567+
file_watch_lock = NULL;
568+
SDL_DestroyHashTable(watch_descriptor_table);
569+
watch_descriptor_table = NULL;
570+
}
571+
#endif // HAVE_INOTIFY
572+
}
573+
413574
// Note that this is actually part of filesystem, not fsops, but everything that uses posix fsops uses this implementation, even with separate filesystem code.
414575
char *SDL_SYS_GetCurrentDirectory(void)
415576
{

src/filesystem/windows/SDL_sysfsops.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,5 +231,18 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info)
231231
return true;
232232
}
233233

234+
bool SDL_SYS_WatchFileForChanges(const char *path)
235+
{
236+
return SDL_Unsupported();
237+
}
238+
239+
void SDL_SYS_UpdateFileWatch(void)
240+
{
241+
}
242+
243+
void SDL_SYS_QuitFileWatch(void)
244+
{
245+
}
246+
234247
#endif // SDL_FSOPS_WINDOWS
235248

0 commit comments

Comments
 (0)