Mercurial > minori
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
