changeset 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 811697ad826a
children a0bc3ae5164a
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;