From 50ff15c35673523d7702c098a76c4a4f02c3c6c4 Mon Sep 17 00:00:00 2001 From: hjfod Date: Thu, 27 Apr 2023 09:22:56 +0300 Subject: [PATCH] add public file watching api --- loader/include/Geode/utils/file.hpp | 36 ++++++++++++++++++++ loader/src/internal/FileWatcher.hpp | 20 +++++++++-- loader/src/utils/file.cpp | 52 +++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/loader/include/Geode/utils/file.hpp b/loader/include/Geode/utils/file.hpp index 30168ed2..aa1a5f61 100644 --- a/loader/include/Geode/utils/file.hpp +++ b/loader/include/Geode/utils/file.hpp @@ -252,4 +252,40 @@ namespace geode::utils::file { * @param options Picker options */ GEODE_DLL Result> pickFiles(FilePickOptions const& options); + + class GEODE_DLL FileWatchEvent : public Event { + protected: + ghc::filesystem::path m_path; + + public: + FileWatchEvent(ghc::filesystem::path const& path); + ghc::filesystem::path getPath() const; + }; + + class GEODE_DLL FileWatchFilter : public EventFilter { + protected: + ghc::filesystem::path m_path; + + public: + using Callback = void(FileWatchEvent*); + + ListenerResult handle(utils::MiniFunction callback, FileWatchEvent* event); + FileWatchFilter(ghc::filesystem::path const& path); + }; + + /** + * Watch a file for changes. Whenever the file is modified on disk, a + * FileWatchEvent is emitted. Add an EventListener with FileWatchFilter + * to catch these events + * @param file The file to watch + * @note Watching uses file system equivalence instead of path equivalence, + * so different paths that point to the same file will be considered the + * same + */ + GEODE_DLL Result<> watchFile(ghc::filesystem::path const& file); + /** + * Stop watching a file for changes + * @param file The file to unwatch + */ + GEODE_DLL void unwatchFile(ghc::filesystem::path const& file); } diff --git a/loader/src/internal/FileWatcher.hpp b/loader/src/internal/FileWatcher.hpp index c42b62bf..6558e781 100644 --- a/loader/src/internal/FileWatcher.hpp +++ b/loader/src/internal/FileWatcher.hpp @@ -25,12 +25,28 @@ protected: public: bool watching() const; - ghc::filesystem::path path() { + ghc::filesystem::path path() const { return m_file; } FileWatcher( - ghc::filesystem::path const& file, FileWatchCallback callback, ErrorCallback error = nullptr + ghc::filesystem::path const& file, + FileWatchCallback callback, + ErrorCallback error = nullptr ); + FileWatcher(FileWatcher const&) = delete; + inline FileWatcher(FileWatcher&& other) + : m_file(std::move(other.m_file)), + m_callback(std::move(other.m_callback)), + m_error(std::move(other.m_error)), + m_filemode(other.m_filemode), + m_platformHandle(other.m_platformHandle), + m_exiting(other.m_exiting) + { + other.m_callback = nullptr; + other.m_error = nullptr; + other.m_platformHandle = nullptr; + other.m_exiting = true; + } ~FileWatcher(); }; diff --git a/loader/src/utils/file.cpp b/loader/src/utils/file.cpp index 2a889a21..c8a6d5e5 100644 --- a/loader/src/utils/file.cpp +++ b/loader/src/utils/file.cpp @@ -11,6 +11,7 @@ #include #include #include +#include using namespace geode::prelude; using namespace geode::utils::file; @@ -551,3 +552,54 @@ Result<> Zip::addAllFrom(Path const& dir) { Result<> Zip::addFolder(Path const& entry) { return m_impl->addFolder(entry); } + +FileWatchEvent::FileWatchEvent(ghc::filesystem::path const& path) + : m_path(path) {} + +ghc::filesystem::path FileWatchEvent::getPath() const { + return m_path; +} + +ListenerResult FileWatchFilter::handle( + MiniFunction callback, + FileWatchEvent* event +) { + std::error_code ec; + if (ghc::filesystem::equivalent(event->getPath(), m_path, ec)) { + callback(event); + } + return ListenerResult::Propagate; +} + +FileWatchFilter::FileWatchFilter(ghc::filesystem::path const& path) + : m_path(path) {} + +// This is a vector because need to use ghc::filesystem::equivalent for +// comparisons and removal is not exactly performance-critical here +// (who's going to add and remove 500 file watchers every frame) +static std::vector> FILE_WATCHERS {}; + +Result<> file::watchFile(ghc::filesystem::path const& file) { + if (!ghc::filesystem::exists(file)) { + return Err("File does not exist"); + } + auto watcher = std::make_unique( + file, + [](auto const& path) { + Loader::get()->queueInGDThread([=] { + FileWatchEvent(path).post(); + }); + } + ); + if (!watcher->watching()) { + return Err("Unknown error watching file"); + } + FILE_WATCHERS.emplace_back(std::move(watcher)); + return Ok(); +} + +void file::unwatchFile(ghc::filesystem::path const& file) { + ranges::remove(FILE_WATCHERS, [=](std::unique_ptr const& watcher) { + return ghc::filesystem::equivalent(file, watcher->path()); + }); +}