Add file watcher API and Linux implementation#15342
Add file watcher API and Linux implementation#15342meyraud705 wants to merge 7 commits intolibsdl-org:mainfrom
Conversation
|
A design consideration: would it be possible to use a callback instead of an event? If a given program listens to different paths at different places in the code, events can make it difficult to transfer the signal from the event loop down to the specific part of the code that needs that event. That was my reasoning when developing the file dialog subsystem; using events would've split the part of the code that invokes the dialog from the part that handles the subsequent file operations, which can make a repo quite messy if not carefully managed. |
I'd also like to see this be callback based rather than event based. You have more local control, and it's always possible to generate an event from a callback. @icculus, thoughts? |
|
Some counter-arguments here:
But, ultimately:
|
With event, you only need to make your callback public and have a dispatch function in the event loop. Doesn't look messy to me and you know when and from which thread you reload the file. SDL_Event e;
while (SDL_PollEvent(&e)) {
if (e.type == SDL_EVENT_FILE_CHANGED) {
if (SDL_strncmp(e->file_watch.path, "./shader/", 9) == 0) {
ReloadShader(&e->file_watch.path[9]);
} else if (...) {
...
}
}
}
It is also possible to generate a callback from an event with |
|
For simple enough use cases, I don't expect events to be too complicated to manage, but for more advanced uses, I see a number of problems arising:
IMO, it would be easier to emit an event from within a callback than to simulate a callback from the event loop, since it requires less refactoring. With callbacks, the code requires only to add one function in the same file where the watching is registered: void callback(const char* path)
{
SDL_Event event;
SDL_zero(event);
event.file_watch.type = SDL_EVENT_FILE_CHANGED;
event.file_watch.timestamp = SDL_GetTicksNS();
event.file_watch.path = path;
SDL_PushEvent(&event);
}
void fn()
{
// ...
SDL_WatchFileForChanges("/path/to/file", callback);
// ...
}Conversely, wiring callbacks from events will require keeping track of path-callback pairs, sending them to the main loop, and adding logic in the main loop to find which callback to invoke when a path notification arises. It's feasible, just more complicated. |
|
You convinced me, I'll make another PR with callback. |
There's no reason we can't do both, where the callback is optional and we always send a file changed event. |
e6e97db to
7b8d615
Compare
Co-authored-by: Sam Lantinga <slouken@libsdl.org>
| if (watch_entry->path[slen - 1] == '/') { | ||
| watch_entry->path[slen - 1] = '\0'; | ||
| } |
There was a problem hiding this comment.
Should this remove all trailing slashes?
| 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'; | |
| } |
There was a problem hiding this comment.
No, only last slash is removed because it is added back when concatenating directory path and file name.
The reason for not removing all trailing slashes is that the path your receive in event is the same as the one you pass to SDL_WatchPathForChanges(), so that you can strncmp them.
const char *path = "/path/.///";
SDL_WatchPathForChanges(path, NULL, NULL) ;
/* in event loop: */
if (SDL_strncmp(event.file_watch.path, path, SDL_strlen(path))) {
const char *file_name = &event.file_watch.path[SDL_strlen(path)];
...
}Maybe we should add SDL_CanonicalizePath() or SDL_ComparePath().
|
Can/should this feature have a test? |
|
|
||
| /* 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. */ |
There was a problem hiding this comment.
What about SDL_EVENT_FILE_WATCH_OVERFLOW?
Or do file watchers have multiple error conditions?
There was a problem hiding this comment.
inotify uses a queue which can overflow. I don't know error for other platform so I kept he name generic.
| * 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. |
There was a problem hiding this comment.
the callback will be called for every file modified in that directory
Does this include the directory? Or only its entries?
There was a problem hiding this comment.
What about file creation and deletion? I suppose we also get notified about those.
There was a problem hiding this comment.
Modify means a call to write() on the file. You cannot write to directory. If the documentation is not clear please propose another wording.
Directory watch are not recursive.
There is no notification for file and directory creation, deletion or rename. I could add these if you want them.
| 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. */ |
There was a problem hiding this comment.
If this event includes creation and deletion, I'm not sure the name (and doc string) covers them all.
Correctly launch and stop file watch thread.
|
|
||
| while (remain > 0) { | ||
| const WatchEntry *watch_entry; | ||
| if (SDL_FindInHashTable(watch_descriptor_table, (void *)(intptr_t)buf.event.wd, (const void **)&watch_entry)) { |
There was a problem hiding this comment.
I don't see a check that read had read a complete event. Can't this access uninitialized or stale memory?
There was a problem hiding this comment.
Read of inotify event is copied from https://github.com/libsdl-org/SDL/blob/main/src/joystick/linux/SDL_sysjoystick.c#L764
| static int SDL_FileWatchThread(void *userdata) | ||
| { | ||
| while (SDL_GetAtomicInt(&quit_watch_file) == 0) { | ||
| SDL_Delay(100); |
There was a problem hiding this comment.
Is polled reading used for a reason? Would blocked I/O work?
There was a problem hiding this comment.
We could use poll() or select() with a timeout if you think it is better.
There was a problem hiding this comment.
I don't know either. I'm just asking questions :)
The joystick inotify code runs on the main thread so cannot block, which is not the case here.
There was a problem hiding this comment.
Is select needed? We're just waiting on one file, not multiple. I'm not sure what to do about destruction and writing/reading to/from the descriptor in multiple threads.
There was a problem hiding this comment.
It worked like the joystick system at the beginning because there was only event. But now there is callback so it needs to work even if you don't use the event subsystem.
You need a timeout to check if we should quit the thread.
All operations on descriptor are protected by a mutex.
|
Can we also get an equivalent API for directories? Reasons:
// rough sketch
void watch_file(const char *path, Callback cb) {
const char *dir = GetFolder(path); // get the folder where this file is
SDL_Hashtable *dir_table = INTERNAL_WatchDirAndGetTable(dir); // keep a table of (file -> callback) internally, per folder?
// ^ This just returns the corresponding table if this directory was already registered
Table-Insert(dir_table, FileHandle(path), cb); // add callback to our table, to be called later
// ^ actually this can be an array too, how many files can you possibly register individually within the same directory?
} |
| * | ||
| * \sa SDL_FileWatchEvent | ||
| */ | ||
| extern SDL_DECLSPEC bool SDLCALL SDL_WatchPathForChanges(const char *path, SDL_FileWatchCallback callback, void *userdata); |
There was a problem hiding this comment.
Please run gendynapi.py under src/dynapi/ (after a rebase), so that your new api additions are exported properly.
Co-authored-by: Anonymous Maarten <madebr@users.noreply.github.com>
Rename SDL_WatchPathForChange() to SDL_AddPathWatch(). Improve documentation.
Description
Add 1 public function:
bool SDL_WatchFileForChanges(const char *path)and 2 event types:SDL_EVENT_FILE_WATCH_ERRORandSDL_EVENT_FILE_CHANGED.SDL_UpdateFileWatch()is called when pumping event to update file watch. Resources are cleared inSDL_QuitFilesystem(void).Implement this API for Linux using inotify. I put it in
SDL_sysfsops.c, I don't know how other platforms work so that may not be the correct place.Existing Issue(s)
Implement #15070 for Linux.