Mercurial > minori
changeset 398:650a9159a0e7 default tip
filesystem: implement win32 watchers
These are really nice :)
| author | Paper <paper@tflc.us> |
|---|---|
| date | Fri, 07 Nov 2025 14:32:11 -0500 |
| parents | 811697ad826a |
| children | |
| files | src/core/filesystem.cc src/library/library.cc |
| diffstat | 2 files changed, 232 insertions(+), 28 deletions(-) [+] |
line wrap: on
line diff
--- a/src/core/filesystem.cc Fri Nov 07 08:39:24 2025 -0500 +++ b/src/core/filesystem.cc Fri Nov 07 14:32:11 2025 -0500 @@ -6,6 +6,10 @@ #include <filesystem> +#ifdef WIN32 +# include <windows.h> +#endif + namespace Filesystem { /* this runs fs::create_directories() on the @@ -79,11 +83,40 @@ * This is the portable version for non-Windows (of which the Windows * specific version hasn't been written yet... TODO) */ -template<typename T> +/* +We can't use std::recursive_directory_iterator! +Why? Well, to put it blunt, it sucks. The main reason +being is that if it finds a single directory it cannot +recurse into, instead of safely recovering, it just +completely stops. I am dumbfounded at this behavior, but +nevertheless it means we simply can't use it, and must +resort to old-fashioned recursion. --paper +*/ +static void IterateDirectory(const std::filesystem::path &path, bool recursive, + std::function<void(const std::filesystem::path &path)> func) +{ + std::error_code ec; + static const std::filesystem::directory_iterator end; + + for (std::filesystem::directory_iterator item(path, ec); item != end; item.increment(ec)) { + if (ec) + continue; + + std::filesystem::path p = item->path(); + + /* Hand the path off to the listener */ + func(p); + + /* If we're dealing with another directory, recurse into it. */ + if (recursive && item->is_directory()) + IterateDirectory(p, true, func); + } +} + class StdFilesystemWatcher : public Watcher { public: - StdFilesystemWatcher(void *opaque, const std::filesystem::path &path, EventHandler handler) - : Watcher(opaque, path, handler) + StdFilesystemWatcher(void *opaque, const std::filesystem::path &path, EventHandler handler, bool recursive) + : Watcher(opaque, path, handler), recursive_(recursive) { } @@ -95,16 +128,15 @@ * iterate over the directory ONCE. */ UntoggleAllPaths(); - for (const auto &item : T(path_)) { - std::filesystem::path p = item.path(); - + /* Start iterating directories. If we're recursive, this + * will go through the whole tree. */ + IterateDirectory(path_, recursive_, [this](const std::filesystem::path &p) { if (FindAndTogglePath(p)) - continue; + return; - /* Hand the path off to the listener */ handler_(opaque_, p, Event::Created); - paths_.push_back({true, p}); - } + paths_.insert({p, true}); + }); DeleteUntoggledPaths(); } @@ -112,20 +144,18 @@ protected: bool FindAndTogglePath(const std::filesystem::path &p) { - for (auto &pp : paths_) { - if (pp.path == p) { - pp.found = true; - return true; - } - } + auto it = paths_.find(p); + if (it == paths_.end()) + return false; - return false; + it->second = true; + return true; } void UntoggleAllPaths() { for (auto &pp : paths_) - pp.found = false; + pp.second = false; } void DeleteUntoggledPaths() @@ -133,8 +163,8 @@ auto it = paths_.begin(); while (it != paths_.end()) { - if (!it->found) { - handler_(opaque_, it->path, Event::Deleted); + if (!it->second) { + handler_(opaque_, it->first, Event::Deleted); it = paths_.erase(it); } else { it++; @@ -142,24 +172,195 @@ } } - struct PathStatus { - bool found; - std::filesystem::path path; - }; + /* unordered hashmap, path[found] */ + std::unordered_map<std::filesystem::path, bool> paths_; + bool recursive_; +}; + +#ifdef WIN32 +/* On Windows, we can ask the OS whether the folder has changed or not. + * This is great for us! */ +class Win32Watcher : public StdFilesystemWatcher { +public: + Win32Watcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler, bool recursive) + : StdFilesystemWatcher(opaque, path, handler, recursive), dirwatcher_(INVALID_HANDLE_VALUE), first_(true) + { + } + + virtual ~Win32Watcher() override + { + // delete handle + if (dirwatcher_ != INVALID_HANDLE_VALUE) + FindCloseChangeNotification(dirwatcher_); + } + + virtual void Process() override + { + if (first_ || dirwatcher_ == INVALID_HANDLE_VALUE) { + /* We want to create this right before iteration so + * we minimize possible race conditions while also + * reducing unnecessary processing */ + TryCreateDirWatcher(); + StdFilesystemWatcher::Process(); + first_ = false; + return; + } + + /* We have a valid handle */ + if (WaitForSingleObject(dirwatcher_, 0) != WAIT_OBJECT_0) + return; + + StdFilesystemWatcher::Process(); + FindNextChangeNotification(dirwatcher_); + } + +protected: + bool TryCreateDirWatcher() + { + dirwatcher_ = FindFirstChangeNotificationW(Watcher::path_.wstring().c_str(), recursive_, + FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME); + + return (dirwatcher_ != INVALID_HANDLE_VALUE); + } + + /* variables */ + HANDLE dirwatcher_; + bool first_; +}; + +class Win32WatcherVista : public Win32Watcher { +public: + Win32WatcherVista(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler, bool recursive) + : Win32Watcher(opaque, path, handler, recursive), dirhandle_(INVALID_HANDLE_VALUE) + { + overlapped_.hEvent = nullptr; + } + + virtual ~Win32WatcherVista() override + { + if (dirhandle_ != INVALID_HANDLE_VALUE) + CloseHandle(dirhandle_); + + if (overlapped_.hEvent != nullptr) + CloseHandle(overlapped_.hEvent); + } + + virtual void Process() override + { + if (first_) { + if (TryCreateHandles()) { + /* Queue a directory read asynchronously. On the next run, this will + * be waited on. */ + QueueDirectoryRead(); - /* TODO this is probably DAMN slow */ - std::vector<PathStatus> paths_; + /* Avoid running Win32Watcher::Process, as it will read the whole + * directory tree into memory. Instead, iterate through the directory + * ourselves. */ + IterateDirectory(path_, recursive_, + [this](const std::filesystem::path &p) { handler_(opaque_, p, Event::Created); }); + } else { + /* Uh oh; we might have to fall back to Win32Watcher. Call into it to + * load the tree into memory. */ + Win32Watcher::Process(); + } + + first_ = false; + return; + } + + if (!HandlesAreValid()) { + /* If we're here, we already fell back into Win32Watcher, so just + * call back into again. On the slim chance TryCreateHandles() might + * succeed, call it as well... */ + if (TryCreateHandles()) + QueueDirectoryRead(); + Win32Watcher::Process(); + return; + } + + if (WaitForSingleObject(overlapped_.hEvent, 0) != WAIT_OBJECT_0) + return; + + FILE_NOTIFY_INFORMATION *ev = reinterpret_cast<FILE_NOTIFY_INFORMATION *>(change_buf_); + + for (;;) { + /* Tack on the file name to the end of our path */ + std::filesystem::path p = Watcher::path_ / std::wstring(ev->FileName, ev->FileNameLength / sizeof(WCHAR)); + + switch (ev->Action) { + case FILE_ACTION_ADDED: + case FILE_ACTION_RENAMED_NEW_NAME: + /* File was added */ + handler_(opaque_, p, Event::Created); + break; + case FILE_ACTION_REMOVED: + case FILE_ACTION_RENAMED_OLD_NAME: + /* File was removed */ + handler_(opaque_, p, Event::Deleted); + break; + } + + if (ev->NextEntryOffset) { + /* ugh ugh ugh ugh */ + ev = reinterpret_cast<FILE_NOTIFY_INFORMATION *>(reinterpret_cast<char *>(ev) + ev->NextEntryOffset); + } else { + break; + } + } + + /* Queue a directory read for the next call */ + QueueDirectoryRead(); + } + +protected: + bool HandlesAreValid() { return (overlapped_.hEvent && dirhandle_ != INVALID_HANDLE_VALUE); } + + bool TryCreateHandles() + { + if (!overlapped_.hEvent) { + overlapped_.hEvent = CreateEventW(nullptr, FALSE, 0, nullptr); + if (!overlapped_.hEvent) + return false; + } + + if (dirhandle_ == INVALID_HANDLE_VALUE) { + dirhandle_ = + CreateFileW(Watcher::path_.wstring().c_str(), FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, nullptr); + if (dirhandle_ == INVALID_HANDLE_VALUE) + return false; + } + + /* We're done here */ + return true; + } + + bool QueueDirectoryRead() + { + return ReadDirectoryChangesW(dirhandle_, change_buf_, sizeof(change_buf_), TRUE, + FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME, nullptr, &overlapped_, + nullptr); + } + + HANDLE dirhandle_; + OVERLAPPED overlapped_; + alignas(FILE_NOTIFY_INFORMATION) char change_buf_[4096]; }; +using DefaultWatcher = Win32WatcherVista; +#else +using DefaultWatcher = StdFilesystemWatcher; +#endif + IWatcher *GetRecursiveFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler) { /* .... :) */ - return new StdFilesystemWatcher<std::filesystem::recursive_directory_iterator>(opaque, path, handler); + return new DefaultWatcher(opaque, path, handler, true); } IWatcher *GetFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler) { - return new StdFilesystemWatcher<std::filesystem::directory_iterator>(opaque, path, handler); + return new DefaultWatcher(opaque, path, handler, false); } } // namespace Filesystem
--- a/src/library/library.cc Fri Nov 07 08:39:24 2025 -0500 +++ b/src/library/library.cc Fri Nov 07 14:32:11 2025 -0500 @@ -16,12 +16,15 @@ Database::Database() { /* Do this immediately :) */ + /* Nevermind, this causes an immediate segfault on Windows UpdateWatchers(); + */ } void Database::UpdateWatchers() { /* TODO also need to remove unused watchers */ + for (const auto &p : session.config.library.paths) { if (watchers_.count(p)) continue;
