Mercurial > minori
comparison src/core/filesystem.cc @ 398:650a9159a0e7
filesystem: implement win32 watchers
These are really nice :)
| author | Paper <paper@tflc.us> |
|---|---|
| date | Fri, 07 Nov 2025 14:32:11 -0500 |
| parents | 963047512d34 |
| children | a0bc3ae5164a |
comparison
equal
deleted
inserted
replaced
| 397:811697ad826a | 398:650a9159a0e7 |
|---|---|
| 3 #include "core/strings.h" | 3 #include "core/strings.h" |
| 4 | 4 |
| 5 #include <QStandardPaths> | 5 #include <QStandardPaths> |
| 6 | 6 |
| 7 #include <filesystem> | 7 #include <filesystem> |
| 8 | |
| 9 #ifdef WIN32 | |
| 10 # include <windows.h> | |
| 11 #endif | |
| 8 | 12 |
| 9 namespace Filesystem { | 13 namespace Filesystem { |
| 10 | 14 |
| 11 /* this runs fs::create_directories() on the | 15 /* this runs fs::create_directories() on the |
| 12 PARENT directory. */ | 16 PARENT directory. */ |
| 77 /* ------------------------------------------------------------------------ */ | 81 /* ------------------------------------------------------------------------ */ |
| 78 /* Filesystem watcher. | 82 /* Filesystem watcher. |
| 79 * This is the portable version for non-Windows (of which the Windows | 83 * This is the portable version for non-Windows (of which the Windows |
| 80 * specific version hasn't been written yet... TODO) */ | 84 * specific version hasn't been written yet... TODO) */ |
| 81 | 85 |
| 82 template<typename T> | 86 /* |
| 87 We can't use std::recursive_directory_iterator! | |
| 88 Why? Well, to put it blunt, it sucks. The main reason | |
| 89 being is that if it finds a single directory it cannot | |
| 90 recurse into, instead of safely recovering, it just | |
| 91 completely stops. I am dumbfounded at this behavior, but | |
| 92 nevertheless it means we simply can't use it, and must | |
| 93 resort to old-fashioned recursion. --paper | |
| 94 */ | |
| 95 static void IterateDirectory(const std::filesystem::path &path, bool recursive, | |
| 96 std::function<void(const std::filesystem::path &path)> func) | |
| 97 { | |
| 98 std::error_code ec; | |
| 99 static const std::filesystem::directory_iterator end; | |
| 100 | |
| 101 for (std::filesystem::directory_iterator item(path, ec); item != end; item.increment(ec)) { | |
| 102 if (ec) | |
| 103 continue; | |
| 104 | |
| 105 std::filesystem::path p = item->path(); | |
| 106 | |
| 107 /* Hand the path off to the listener */ | |
| 108 func(p); | |
| 109 | |
| 110 /* If we're dealing with another directory, recurse into it. */ | |
| 111 if (recursive && item->is_directory()) | |
| 112 IterateDirectory(p, true, func); | |
| 113 } | |
| 114 } | |
| 115 | |
| 83 class StdFilesystemWatcher : public Watcher { | 116 class StdFilesystemWatcher : public Watcher { |
| 84 public: | 117 public: |
| 85 StdFilesystemWatcher(void *opaque, const std::filesystem::path &path, EventHandler handler) | 118 StdFilesystemWatcher(void *opaque, const std::filesystem::path &path, EventHandler handler, bool recursive) |
| 86 : Watcher(opaque, path, handler) | 119 : Watcher(opaque, path, handler), recursive_(recursive) |
| 87 { | 120 { |
| 88 } | 121 } |
| 89 | 122 |
| 90 virtual ~StdFilesystemWatcher() override {} | 123 virtual ~StdFilesystemWatcher() override {} |
| 91 | 124 |
| 93 { | 126 { |
| 94 /* Untoggle all paths. This allows us to only ever | 127 /* Untoggle all paths. This allows us to only ever |
| 95 * iterate over the directory ONCE. */ | 128 * iterate over the directory ONCE. */ |
| 96 UntoggleAllPaths(); | 129 UntoggleAllPaths(); |
| 97 | 130 |
| 98 for (const auto &item : T(path_)) { | 131 /* Start iterating directories. If we're recursive, this |
| 99 std::filesystem::path p = item.path(); | 132 * will go through the whole tree. */ |
| 100 | 133 IterateDirectory(path_, recursive_, [this](const std::filesystem::path &p) { |
| 101 if (FindAndTogglePath(p)) | 134 if (FindAndTogglePath(p)) |
| 102 continue; | 135 return; |
| 103 | 136 |
| 104 /* Hand the path off to the listener */ | |
| 105 handler_(opaque_, p, Event::Created); | 137 handler_(opaque_, p, Event::Created); |
| 106 paths_.push_back({true, p}); | 138 paths_.insert({p, true}); |
| 107 } | 139 }); |
| 108 | 140 |
| 109 DeleteUntoggledPaths(); | 141 DeleteUntoggledPaths(); |
| 110 } | 142 } |
| 111 | 143 |
| 112 protected: | 144 protected: |
| 113 bool FindAndTogglePath(const std::filesystem::path &p) | 145 bool FindAndTogglePath(const std::filesystem::path &p) |
| 114 { | 146 { |
| 115 for (auto &pp : paths_) { | 147 auto it = paths_.find(p); |
| 116 if (pp.path == p) { | 148 if (it == paths_.end()) |
| 117 pp.found = true; | 149 return false; |
| 118 return true; | 150 |
| 119 } | 151 it->second = true; |
| 120 } | 152 return true; |
| 121 | |
| 122 return false; | |
| 123 } | 153 } |
| 124 | 154 |
| 125 void UntoggleAllPaths() | 155 void UntoggleAllPaths() |
| 126 { | 156 { |
| 127 for (auto &pp : paths_) | 157 for (auto &pp : paths_) |
| 128 pp.found = false; | 158 pp.second = false; |
| 129 } | 159 } |
| 130 | 160 |
| 131 void DeleteUntoggledPaths() | 161 void DeleteUntoggledPaths() |
| 132 { | 162 { |
| 133 auto it = paths_.begin(); | 163 auto it = paths_.begin(); |
| 134 | 164 |
| 135 while (it != paths_.end()) { | 165 while (it != paths_.end()) { |
| 136 if (!it->found) { | 166 if (!it->second) { |
| 137 handler_(opaque_, it->path, Event::Deleted); | 167 handler_(opaque_, it->first, Event::Deleted); |
| 138 it = paths_.erase(it); | 168 it = paths_.erase(it); |
| 139 } else { | 169 } else { |
| 140 it++; | 170 it++; |
| 141 } | 171 } |
| 142 } | 172 } |
| 143 } | 173 } |
| 144 | 174 |
| 145 struct PathStatus { | 175 /* unordered hashmap, path[found] */ |
| 146 bool found; | 176 std::unordered_map<std::filesystem::path, bool> paths_; |
| 147 std::filesystem::path path; | 177 bool recursive_; |
| 148 }; | |
| 149 | |
| 150 /* TODO this is probably DAMN slow */ | |
| 151 std::vector<PathStatus> paths_; | |
| 152 }; | 178 }; |
| 153 | 179 |
| 180 #ifdef WIN32 | |
| 181 /* On Windows, we can ask the OS whether the folder has changed or not. | |
| 182 * This is great for us! */ | |
| 183 class Win32Watcher : public StdFilesystemWatcher { | |
| 184 public: | |
| 185 Win32Watcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler, bool recursive) | |
| 186 : StdFilesystemWatcher(opaque, path, handler, recursive), dirwatcher_(INVALID_HANDLE_VALUE), first_(true) | |
| 187 { | |
| 188 } | |
| 189 | |
| 190 virtual ~Win32Watcher() override | |
| 191 { | |
| 192 // delete handle | |
| 193 if (dirwatcher_ != INVALID_HANDLE_VALUE) | |
| 194 FindCloseChangeNotification(dirwatcher_); | |
| 195 } | |
| 196 | |
| 197 virtual void Process() override | |
| 198 { | |
| 199 if (first_ || dirwatcher_ == INVALID_HANDLE_VALUE) { | |
| 200 /* We want to create this right before iteration so | |
| 201 * we minimize possible race conditions while also | |
| 202 * reducing unnecessary processing */ | |
| 203 TryCreateDirWatcher(); | |
| 204 StdFilesystemWatcher::Process(); | |
| 205 first_ = false; | |
| 206 return; | |
| 207 } | |
| 208 | |
| 209 /* We have a valid handle */ | |
| 210 if (WaitForSingleObject(dirwatcher_, 0) != WAIT_OBJECT_0) | |
| 211 return; | |
| 212 | |
| 213 StdFilesystemWatcher::Process(); | |
| 214 FindNextChangeNotification(dirwatcher_); | |
| 215 } | |
| 216 | |
| 217 protected: | |
| 218 bool TryCreateDirWatcher() | |
| 219 { | |
| 220 dirwatcher_ = FindFirstChangeNotificationW(Watcher::path_.wstring().c_str(), recursive_, | |
| 221 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME); | |
| 222 | |
| 223 return (dirwatcher_ != INVALID_HANDLE_VALUE); | |
| 224 } | |
| 225 | |
| 226 /* variables */ | |
| 227 HANDLE dirwatcher_; | |
| 228 bool first_; | |
| 229 }; | |
| 230 | |
| 231 class Win32WatcherVista : public Win32Watcher { | |
| 232 public: | |
| 233 Win32WatcherVista(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler, bool recursive) | |
| 234 : Win32Watcher(opaque, path, handler, recursive), dirhandle_(INVALID_HANDLE_VALUE) | |
| 235 { | |
| 236 overlapped_.hEvent = nullptr; | |
| 237 } | |
| 238 | |
| 239 virtual ~Win32WatcherVista() override | |
| 240 { | |
| 241 if (dirhandle_ != INVALID_HANDLE_VALUE) | |
| 242 CloseHandle(dirhandle_); | |
| 243 | |
| 244 if (overlapped_.hEvent != nullptr) | |
| 245 CloseHandle(overlapped_.hEvent); | |
| 246 } | |
| 247 | |
| 248 virtual void Process() override | |
| 249 { | |
| 250 if (first_) { | |
| 251 if (TryCreateHandles()) { | |
| 252 /* Queue a directory read asynchronously. On the next run, this will | |
| 253 * be waited on. */ | |
| 254 QueueDirectoryRead(); | |
| 255 | |
| 256 /* Avoid running Win32Watcher::Process, as it will read the whole | |
| 257 * directory tree into memory. Instead, iterate through the directory | |
| 258 * ourselves. */ | |
| 259 IterateDirectory(path_, recursive_, | |
| 260 [this](const std::filesystem::path &p) { handler_(opaque_, p, Event::Created); }); | |
| 261 } else { | |
| 262 /* Uh oh; we might have to fall back to Win32Watcher. Call into it to | |
| 263 * load the tree into memory. */ | |
| 264 Win32Watcher::Process(); | |
| 265 } | |
| 266 | |
| 267 first_ = false; | |
| 268 return; | |
| 269 } | |
| 270 | |
| 271 if (!HandlesAreValid()) { | |
| 272 /* If we're here, we already fell back into Win32Watcher, so just | |
| 273 * call back into again. On the slim chance TryCreateHandles() might | |
| 274 * succeed, call it as well... */ | |
| 275 if (TryCreateHandles()) | |
| 276 QueueDirectoryRead(); | |
| 277 Win32Watcher::Process(); | |
| 278 return; | |
| 279 } | |
| 280 | |
| 281 if (WaitForSingleObject(overlapped_.hEvent, 0) != WAIT_OBJECT_0) | |
| 282 return; | |
| 283 | |
| 284 FILE_NOTIFY_INFORMATION *ev = reinterpret_cast<FILE_NOTIFY_INFORMATION *>(change_buf_); | |
| 285 | |
| 286 for (;;) { | |
| 287 /* Tack on the file name to the end of our path */ | |
| 288 std::filesystem::path p = Watcher::path_ / std::wstring(ev->FileName, ev->FileNameLength / sizeof(WCHAR)); | |
| 289 | |
| 290 switch (ev->Action) { | |
| 291 case FILE_ACTION_ADDED: | |
| 292 case FILE_ACTION_RENAMED_NEW_NAME: | |
| 293 /* File was added */ | |
| 294 handler_(opaque_, p, Event::Created); | |
| 295 break; | |
| 296 case FILE_ACTION_REMOVED: | |
| 297 case FILE_ACTION_RENAMED_OLD_NAME: | |
| 298 /* File was removed */ | |
| 299 handler_(opaque_, p, Event::Deleted); | |
| 300 break; | |
| 301 } | |
| 302 | |
| 303 if (ev->NextEntryOffset) { | |
| 304 /* ugh ugh ugh ugh */ | |
| 305 ev = reinterpret_cast<FILE_NOTIFY_INFORMATION *>(reinterpret_cast<char *>(ev) + ev->NextEntryOffset); | |
| 306 } else { | |
| 307 break; | |
| 308 } | |
| 309 } | |
| 310 | |
| 311 /* Queue a directory read for the next call */ | |
| 312 QueueDirectoryRead(); | |
| 313 } | |
| 314 | |
| 315 protected: | |
| 316 bool HandlesAreValid() { return (overlapped_.hEvent && dirhandle_ != INVALID_HANDLE_VALUE); } | |
| 317 | |
| 318 bool TryCreateHandles() | |
| 319 { | |
| 320 if (!overlapped_.hEvent) { | |
| 321 overlapped_.hEvent = CreateEventW(nullptr, FALSE, 0, nullptr); | |
| 322 if (!overlapped_.hEvent) | |
| 323 return false; | |
| 324 } | |
| 325 | |
| 326 if (dirhandle_ == INVALID_HANDLE_VALUE) { | |
| 327 dirhandle_ = | |
| 328 CreateFileW(Watcher::path_.wstring().c_str(), FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE, | |
| 329 nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, nullptr); | |
| 330 if (dirhandle_ == INVALID_HANDLE_VALUE) | |
| 331 return false; | |
| 332 } | |
| 333 | |
| 334 /* We're done here */ | |
| 335 return true; | |
| 336 } | |
| 337 | |
| 338 bool QueueDirectoryRead() | |
| 339 { | |
| 340 return ReadDirectoryChangesW(dirhandle_, change_buf_, sizeof(change_buf_), TRUE, | |
| 341 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME, nullptr, &overlapped_, | |
| 342 nullptr); | |
| 343 } | |
| 344 | |
| 345 HANDLE dirhandle_; | |
| 346 OVERLAPPED overlapped_; | |
| 347 alignas(FILE_NOTIFY_INFORMATION) char change_buf_[4096]; | |
| 348 }; | |
| 349 | |
| 350 using DefaultWatcher = Win32WatcherVista; | |
| 351 #else | |
| 352 using DefaultWatcher = StdFilesystemWatcher; | |
| 353 #endif | |
| 354 | |
| 154 IWatcher *GetRecursiveFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler) | 355 IWatcher *GetRecursiveFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler) |
| 155 { | 356 { |
| 156 /* .... :) */ | 357 /* .... :) */ |
| 157 return new StdFilesystemWatcher<std::filesystem::recursive_directory_iterator>(opaque, path, handler); | 358 return new DefaultWatcher(opaque, path, handler, true); |
| 158 } | 359 } |
| 159 | 360 |
| 160 IWatcher *GetFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler) | 361 IWatcher *GetFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler) |
| 161 { | 362 { |
| 162 return new StdFilesystemWatcher<std::filesystem::directory_iterator>(opaque, path, handler); | 363 return new DefaultWatcher(opaque, path, handler, false); |
| 163 } | 364 } |
| 164 | 365 |
| 165 } // namespace Filesystem | 366 } // namespace Filesystem |
