view src/core/filesystem.cc @ 402:d859306e2db4 default tip

filesystem: actually check for inotify instead of blindly assuming it exists
author Paper <paper@tflc.us>
date Fri, 07 Nov 2025 18:36:18 -0500
parents 2f89797b6a44
children
line wrap: on
line source

#include "core/filesystem.h"
#include "core/config.h"
#include "core/strings.h"

#include <QStandardPaths>

#include <filesystem>

#ifdef WIN32
# include <windows.h>
#elif defined(HAVE_INOTIFY)
/* ehhhh */
# include <fcntl.h>
# include <unistd.h>
# include <sys/inotify.h>
#endif

#include <iostream>

namespace Filesystem {

/* this runs fs::create_directories() on the
   PARENT directory. */
void CreateDirectories(const std::filesystem::path &path)
{
	if (path.empty())
		return;

	const auto &parent = path.parent_path();
	if (!std::filesystem::exists(parent))
		std::filesystem::create_directories(parent);
}

std::filesystem::path GetDotPath()
{
	/*
	 * Windows: ~/AppData/Roaming/Minori
	 * macOS: ~/Library/Application Support/Minori
	 * ...: ~/.config/minori
	 *
	 * FIXME: are windows and mac properly cased?
	 */
#ifdef WIN32
	return Strings::ToUtf8String(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
#else
	return Strings::ToUtf8String(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
#endif
}

std::filesystem::path GetConfigPath()
{
	return GetDotPath() / CONFIG_NAME;
}

std::filesystem::path GetAnimeDBPath()
{
	return GetDotPath() / "anime" / "db.json";
}

std::filesystem::path GetTorrentsPath()
{
	return GetDotPath() / "torrents";
}

std::filesystem::path GetAnimePostersPath()
{
	return GetDotPath() / "anime" / "posters";
}

/* ------------------------------------------------------------------------ */
/* This is the "base class" for basically every watcher. */

class Watcher : public IWatcher {
public:
	Watcher(void *opaque, const std::filesystem::path &path, EventHandler handler)
	    : path_(path), handler_(handler), opaque_(opaque)
	{
	}

	virtual ~Watcher() override {}

protected:
	std::filesystem::path path_;
	EventHandler handler_;
	void *opaque_;
};

/* ------------------------------------------------------------------------ */
/* Filesystem watcher.
 * This is the portable version for non-Windows; however, pretty much
 * every implementation should be based on this in some shape or form. */

/*
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, bool recursive)
	    : Watcher(opaque, path, handler), recursive_(recursive)
	{
	}

	virtual ~StdFilesystemWatcher() override {}

	virtual void Process() override
	{
		/* Untoggle all paths. This allows us to only ever
		 * iterate over the directory ONCE. */
		UntoggleAllPaths();

		/* 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))
				return;

			handler_(opaque_, p, Event::Created);
			paths_.insert({p, true});
		});

		DeleteUntoggledPaths();
	}

protected:
	bool FindAndTogglePath(const std::filesystem::path &p)
	{
		auto it = paths_.find(p);
		if (it == paths_.end())
			return false;

		it->second = true;
		return true;
	}

	void UntoggleAllPaths()
	{
		for (auto &pp : paths_)
			pp.second = false;
	}

	void DeleteUntoggledPaths()
	{
		auto it = paths_.begin();

		while (it != paths_.end()) {
			if (!it->second) {
				handler_(opaque_, it->first, Event::Deleted);
				it = paths_.erase(it);
			} else {
				it++;
			}
		}
	}

	/* unordered hashmap, path[found] */
	std::unordered_map<std::filesystem::path, bool> paths_;
	bool recursive_;
};

/* There does actually exist an API in Qt to do file system watching.
 * However, it is of little use for us, since it
 *  a) isn't recursive
 *  b) requires constant conversion to and from QString, which takes
 *     up valuable system resources
 *  c) uses signals etc. that are a pain in the ass to use in an
 *     event-processing logic like we do here. */

#ifdef WIN32
/* On Windows, we can ask the OS whether the directory tree 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_;
};

/* I think "Vista" is actually a misnomer, MSDN says that ReadDirectoryChangesW
 * has existed since Windows XP.
 *
 * Anyway, this is a version that doesn't need to keep the whole directory
 * tree in-memory if it is available. */
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();

				/* 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;
#elif defined(HAVE_INOTIFY)
/* 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

IWatcher *GetRecursiveFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler)
{
	/* .... :) */
	return new DefaultWatcher(opaque, path, handler, true);
}

IWatcher *GetFilesystemWatcher(void *opaque, const std::filesystem::path &path, IWatcher::EventHandler handler)
{
	return new DefaultWatcher(opaque, path, handler, false);
}

} // namespace Filesystem