changeset 401:2f89797b6a44

filesystem: add linux inotify watcher I don't really like this, but eh
author Paper <paper@tflc.us>
date Fri, 07 Nov 2025 18:28:36 -0500
parents 2f4dc1580b84
children d859306e2db4
files src/core/filesystem.cc
diffstat 1 files changed, 223 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/src/core/filesystem.cc	Fri Nov 07 15:40:56 2025 -0500
+++ b/src/core/filesystem.cc	Fri Nov 07 18:28:36 2025 -0500
@@ -8,8 +8,15 @@
 
 #ifdef WIN32
 # include <windows.h>
+#elif defined(linux)
+/* ehhhh */
+# include <fcntl.h>
+# include <unistd.h>
+# include <sys/inotify.h>
 #endif
 
+#include <iostream>
+
 namespace Filesystem {
 
 /* this runs fs::create_directories() on the
@@ -361,6 +368,222 @@
 };
 
 using DefaultWatcher = Win32WatcherVista;
+#elif defined(__linux__)
+/* Inotify watcher */
+class InotifyWatcher : public StdFilesystemWatcher {
+public:
+	InotifyWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler, bool recursive)
+	    : StdFilesystemWatcher(opaque, path, handler, recursive)
+	    , first_(true)
+	    , giveup_(false)
+	    , ev_(nullptr)
+	    , ev_size_(0)
+	{
+	}
+
+	virtual ~InotifyWatcher() override
+	{
+		/* We don't need to free our watch descriptors;
+		 * they are automatically free'd up by the kernel */
+		std::free(ev_);
+	}
+
+	virtual void Process() override
+	{
+		if (giveup_) {
+			StdFilesystemWatcher::Process();
+			return;
+		}
+
+		if (first_) {
+			/* Try creating the file descriptor */
+			if (TryCreateFd()) {
+				/* Add toplevel directory */
+				AddWatchDescriptor(Watcher::path_);
+
+				/* Yay, we don't need to keep the dir structure in memory */
+				IterateDirectory(Watcher::path_, recursive_, [this](const std::filesystem::path &p) {
+					/* hmm, we're stat'ing the file twice now... */
+					if (std::filesystem::is_directory(p))
+						AddWatchDescriptor(p);
+					handler_(opaque_, p, Event::Created);
+				});
+			} else {
+				/* Uh oh */
+				giveup_ = true;
+			}
+
+			if (giveup_) {
+				/* ;p */
+				StdFilesystemWatcher::Process();
+				return;
+			}
+
+			first_ = false;
+			return;
+		}
+
+		if (!fd_) {
+			/* oh what the hell */
+			giveup_ = true;
+			StdFilesystemWatcher::Process();
+			return;
+		}
+
+		/* Read in everything */
+		for (;;) {
+			int r;
+			do {
+				ev_size_ = (ev_size_) ? (ev_size_ * 2) : (sizeof(struct inotify_event) + 4096);
+				ev_ = reinterpret_cast<struct inotify_event *>(std::realloc(ev_, ev_size_));
+				if (!ev_) {
+					/* uh oh */
+					ev_size_ = 0;
+					return;
+				}
+
+				r = read(fd_.get(), ev_, ev_size_);
+			} while (r == 0 || (r < 0 && errno == EINVAL));
+
+			if (r < 0 && errno == EAGAIN) {
+				/* No more events to process */
+				break;
+			}
+
+			for (int i = 0; i < r; /* ok */) {
+				struct inotify_event *ev = reinterpret_cast<struct inotify_event *>(reinterpret_cast<char *>(ev_) + i);
+
+				if (ev->mask & (IN_DELETE_SELF)) {
+					/* Watched directory has been deleted */
+					RemoveWatchDescriptor(ev->wd);
+					continue;
+				}
+
+				if (ev->mask & (IN_MOVE|IN_CREATE|IN_DELETE)) {
+					std::filesystem::path p = wds_[ev->wd] / ev->name;
+
+					if (ev->mask & (IN_MOVED_TO|IN_CREATE)) {
+						if (std::filesystem::is_directory(p))
+							AddWatchDescriptor(p);
+						handler_(opaque_, p, Event::Created);
+					} else if (ev->mask & (IN_MOVED_FROM|IN_DELETE)) {
+
+						handler_(opaque_, wds_[ev->wd] / ev->name, Event::Deleted);
+					}
+				}
+
+				i += sizeof(inotify_event) + ev->len;
+			}
+		}
+	}
+
+protected:
+	/* File descriptor helper. Mostly follows std::unique_ptr,
+	 * but has a function for toggling non-blocking */
+	struct FileDescriptor {
+	public:
+		FileDescriptor() : fd_(-1) { }
+		~FileDescriptor() { reset(); }
+
+		int get() { return fd_; }
+		operator bool() { return (fd_ != -1); }
+		void reset(int fd = -1)
+		{
+			/* Close anything we already have */
+			if (fd_ != -1)
+				::close(fd_);
+			/* Put the new one in */
+			fd_ = fd;
+		}
+
+		bool SetNonBlocking(bool on)
+		{
+			if (fd_ < 0)
+				return false;
+
+			int x = fcntl(fd_, F_GETFL);
+			if (x < 0)
+				return false;
+
+			if (on) {
+				x |= O_NONBLOCK;
+			} else {
+				x &= ~O_NONBLOCK;
+			}
+
+			int r = fcntl(fd_, F_SETFL, x);
+			if (r < 0)
+				return false;
+
+			return true;
+		}
+
+	private:
+		int fd_;
+	};
+
+	bool TryCreateFd()
+	{
+		if (giveup_)
+			return false;
+
+		if (!fd_) {
+#ifdef HAVE_INOTIFY_INIT1
+			fd_.reset(inotify_init1(IN_NONBLOCK));
+#else
+			fd_.reset(inotify_init());
+#endif
+			if (!fd_)
+				return false;
+
+#ifndef HAVE_INOTIFY_INIT1
+			/* Very old linux */
+			if (!fd_.SetNonBlocking(true))
+				return false;
+#endif
+		}
+
+		return !!fd_;
+	}
+
+	bool AddWatchDescriptor(const std::filesystem::path &p)
+	{
+		if (!fd_ || giveup_)
+			return false;
+
+		int wd = inotify_add_watch(fd_.get(), p.string().c_str(), IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE_SELF);
+		if (wd < 0) {
+			/* Don't even try to watch any more */
+			giveup_ = true;
+			return false;
+		}
+
+		/* Add to our list; these IDs (should be) unique */
+		wds_[wd] = p;
+		return true;
+	}
+
+	void RemoveWatchDescriptor(int wd)
+	{
+		inotify_rm_watch(fd_.get(), wd);
+		wds_.erase(wd);
+	}
+
+	/* variables */
+	FileDescriptor fd_;
+	std::unordered_map<int, std::filesystem::path> wds_; /* watch descriptors */
+	bool first_;
+
+	/* set this variable if we've completely run out of
+	 * resources (watch descriptors) and need to fall
+	 * back to std::filesystem or fear ultimate damage */
+	bool giveup_;
+
+	struct inotify_event *ev_;
+	std::size_t ev_size_;
+};
+
+using DefaultWatcher = InotifyWatcher;
 #else
 using DefaultWatcher = StdFilesystemWatcher;
 #endif