Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions include/SDL3/SDL_events.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_CHANGED = 0x1500, /**< A watched file was written. */
SDL_EVENT_FILE_WATCH_ERROR, /**< Watched files may have been modified, but the events are lost. */
Copy link
Copy Markdown
Contributor

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?


/* 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 */
Expand Down Expand Up @@ -970,6 +974,21 @@ 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.*)
*
* You can add file to the watch list with SDL_WatchPathForChanges().
*
* \sa SDL_WatchPathForChanges
*/
typedef struct SDL_FileWatchEvent
{
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 */
} SDL_FileWatchEvent;

/**
* The "quit requested" event
*
Expand Down Expand Up @@ -1054,6 +1073,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
Expand Down
32 changes: 32 additions & 0 deletions include/SDL3/SDL_filesystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the callback will be called for every file modified in that directory

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to remove a file watcher?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
}
Expand Down
9 changes: 9 additions & 0 deletions src/filesystem/SDL_filesystem.c
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ 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)
{
CHECK_PARAM(!path) {
return SDL_InvalidParamError("path");
}
return SDL_SYS_WatchPathForChanges(path, cb, userdata);
}

static bool EverythingMatch(const char *pattern, const char *str, bool *matched_to_dir)
{
SDL_assert(pattern == NULL);
Expand Down Expand Up @@ -547,5 +555,6 @@ void SDL_QuitFilesystem(void)
CachedUserFolders[i] = NULL;
}
}
SDL_SYS_QuitPathWatch();
}

3 changes: 3 additions & 0 deletions src/filesystem/SDL_sysfilesystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ 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 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);
extern char **SDL_InternalGlobDirectory(const char *path, const char *pattern, SDL_GlobFlags flags, int *count, SDL_GlobEnumeratorFunc enumerator, SDL_GlobGetPathInfoFunc getpathinfo, void *userdata);
Expand Down
9 changes: 9 additions & 0 deletions src/filesystem/dummy/SDL_sysfsops.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,14 @@ 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)
{
return SDL_Unsupported();
}

void SDL_SYS_QuitPathWatch(void)
{
}

#endif // SDL_FSOPS_DUMMY

196 changes: 196 additions & 0 deletions src/filesystem/posix/SDL_sysfsops.c
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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)
{
Expand Down Expand Up @@ -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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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;

#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] == '/') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also needs a test for slen > 0.
This also begs the question: is adding a file watcher for the path "" valid?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Contributor

@madebr madebr Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this remove all trailing slashes?

Suggested change
if (watch_entry->path[slen - 1] == '/') {
watch_entry->path[slen - 1] = '\0';
}
while (slen > 0 && watch_entry->path[slen - 1] == '/') {
watch_entry->path[--slen] = '\0';
}

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)
{
Expand Down
9 changes: 9 additions & 0 deletions src/filesystem/windows/SDL_sysfsops.c
Original file line number Diff line number Diff line change
Expand Up @@ -231,5 +231,14 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info)
return true;
}

bool SDL_SYS_WatchPathForChanges(const char *path, SDL_FileWatchCallback cb, void *userdata)
{
return SDL_Unsupported();
}

void SDL_SYS_QuitPathWatch(void)
{
}

#endif // SDL_FSOPS_WINDOWS

Loading