Add file watcher API and Linux implementation#15342
Add file watcher API and Linux implementation#15342meyraud705 wants to merge 5 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. |
| * \param path path of file that was modified. | ||
| * \param userdata what was passed as `userdata` to SDL_WatchFileForChanges(). | ||
| */ | ||
| typedef void (SDLCALL *SDL_FileWatchCallback)(const char *path, void *userdata); |
There was a problem hiding this comment.
Typically userdata will be the first argument to the callback.
There was a problem hiding this comment.
It would also be helpful to know the thread safety situation for this callback. (Is it guaranteed to be called on a specific thread, etc.)
There was a problem hiding this comment.
It run on its own thread, but I don't know how other platforms will handle this so I documented it can run on any thread.
| 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. */ |
There was a problem hiding this comment.
Suggest flipping this, so the normal change event comes first.
| * | ||
| * \sa SDL_FileWatchEvent | ||
| */ | ||
| extern SDL_DECLSPEC bool SDLCALL SDL_WatchFileForChanges(const char *path, SDL_FileWatchCallback callback, void *userdata); |
There was a problem hiding this comment.
Does it make sense to add a separate API for watching an entire directory?
There was a problem hiding this comment.
Or if this function handles both, would it make sense to be called WatchPathForChanges?
There was a problem hiding this comment.
inotify handles both so does this function. You only get event when a file is modified though.
e6e97db to
7b8d615
Compare
Co-authored-by: Sam Lantinga <slouken@libsdl.org>
| * | ||
| * \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.
Is it possible to remove a file watcher?
There was a problem hiding this comment.
Oh yes, this should be AddPathWatch() with a corresponding RemovePathWatch()
| 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().
| } WatchEntry; | ||
| static SDL_HashTable *watch_descriptor_table = NULL; // stores WatchEntry for a watch descriptor | ||
|
|
||
| static int SDL_FileWatchThread(void *user_data); |
There was a problem hiding this comment.
| static int SDL_FileWatchThread(void *user_data); | |
| static int SDLCALL SDL_FileWatchThread(void *user_data); |
|
Can/should this feature have a test? |
| 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] == '/') { |
There was a problem hiding this comment.
This also needs a test for slen > 0.
This also begs the question: is adding a file watcher for the path "" valid?
There was a problem hiding this comment.
No, we should check and reject "" as a path.
|
|
||
| /* 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?
} |
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.