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