From 2b7faf60a31c6f8ea865465e4032d8f93c1c3b59 Mon Sep 17 00:00:00 2001 From: GPUCode Date: Sat, 11 Nov 2023 15:35:11 +0200 Subject: [PATCH] common: Add FileWatcher --- src/common/CMakeLists.txt | 2 + src/common/file_watcher.cpp | 140 ++++++++++++++++++++++++++++++++++++ src/common/file_watcher.h | 32 +++++++++ 3 files changed, 174 insertions(+) create mode 100644 src/common/file_watcher.cpp create mode 100644 src/common/file_watcher.h diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 6b97b6837..e3701223e 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -89,6 +89,8 @@ add_library(citra_common STATIC expected.h file_util.cpp file_util.h + file_watcher.cpp + file_watcher.h hash.h linear_disk_cache.h literals.h diff --git a/src/common/file_watcher.cpp b/src/common/file_watcher.cpp new file mode 100644 index 000000000..6bdac82f9 --- /dev/null +++ b/src/common/file_watcher.cpp @@ -0,0 +1,140 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include + +#include "common/assert.h" +#include "common/file_watcher.h" + +namespace Common { + +static FileAction Win32ActionToFileAction(DWORD action) { + switch (action) { + case FILE_ACTION_ADDED: + return FileAction::Added; + case FILE_ACTION_REMOVED: + return FileAction::Removed; + case FILE_ACTION_MODIFIED: + return FileAction::Modified; + case FILE_ACTION_RENAMED_OLD_NAME: + case FILE_ACTION_RENAMED_NEW_NAME: + return FileAction::Renamed; + default: + UNREACHABLE_MSG("Unknown action {}", action); + return FileAction::Invalid; + } +} + +struct FileWatcher::Impl { + explicit Impl(const std::wstring& path, FileWatcher::Callback&& callback_) + : callback{callback_} { + // Create file handle for the directory we are watching. + dir_handle = + CreateFileW(path.c_str(), FILE_LIST_DIRECTORY | GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL); + ASSERT_MSG(dir_handle != INVALID_HANDLE_VALUE, "Unable to create watch file"); + + // Create an event that will terminate the thread when fired. + termination_event = CreateEvent(NULL, TRUE, FALSE, NULL); + ASSERT_MSG(termination_event != INVALID_HANDLE_VALUE, "Unable to create watch event"); + + // Create an event that will wake up the watcher thread on filesystem changes. + overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + ASSERT_MSG(overlapped.hEvent != INVALID_HANDLE_VALUE, "Unable to create watch event"); + + // Create the watcher thread. + watch_thread = std::thread([this] { WatcherThread(); }); + } + + ~Impl() { + // Signal watcher thread to terminate. + SetEvent(termination_event); + + // Wait for said termination. + if (watch_thread.joinable()) { + watch_thread.join(); + } + + // Close used handles. + CancelIo(dir_handle); + GetOverlappedResult(dir_handle, &overlapped, &num_bytes_read, TRUE); + CloseHandle(termination_event); + CloseHandle(overlapped.hEvent); + } + + void WatcherThread() { + const std::array wait_handles{overlapped.hEvent, termination_event}; + while (is_running) { + bool result = + ReadDirectoryChangesW(dir_handle, buffer.data(), buffer.size(), TRUE, + FILE_NOTIFY_CHANGE_LAST_WRITE, NULL, &overlapped, NULL); + ASSERT_MSG(result, "Unable to read directory changes: {}", GetLastErrorMsg()); + + // Sleep until we receive a file changed notification or a termination event. + switch ( + WaitForMultipleObjects(wait_handles.size(), wait_handles.data(), FALSE, INFINITE)) { + case WAIT_OBJECT_0: { + // Retrieve asynchronously the data from ReadDirectoryChangesW. + result = GetOverlappedResult(dir_handle, &overlapped, &num_bytes_read, TRUE); + ASSERT_MSG(result, "Unable to retrieve overlapped result: {}", GetLastErrorMsg()); + + // Notify about file changes. + NotifyFileChanges(); + break; + } + case WAIT_OBJECT_0 + 1: + is_running = false; + break; + case WAIT_FAILED: + UNREACHABLE_MSG("Failed waiting for file watcher events: {}", GetLastErrorMsg()); + break; + } + } + } + + void NotifyFileChanges() { + // If no data was read we have nothing to do. + if (num_bytes_read == 0) [[unlikely]] { + return; + } + + size_t next_entry_offset{}; + FILE_NOTIFY_INFORMATION fni; + do { + // Retrieve file notify information. + std::memcpy(&fni, buffer.data() + next_entry_offset, sizeof(fni)); + next_entry_offset += fni.NextEntryOffset; + + // Call the callback function informing about the change. + if (fni.Action != 0) { + const std::wstring file_name{fni.FileName, fni.FileNameLength}; + const FileAction action = Win32ActionToFileAction(fni.Action); + callback(file_name, action); + } + + // If this was the last action, break. + if (fni.NextEntryOffset == 0) { + break; + } + } while (true); + } + +private: + static constexpr size_t DirectoryWatcherBufferSize = 4096; + FileWatcher::Callback callback; + HANDLE dir_handle{}; + HANDLE termination_event{}; + OVERLAPPED overlapped{}; + std::array buffer{}; + std::atomic_bool is_running{true}; + DWORD num_bytes_read{}; + std::thread watch_thread; +}; + +FileWatcher::FileWatcher(const std::wstring& log_dir, Callback&& callback) + : impl{std::make_unique(log_dir, std::move(callback))} {} + +} // namespace Common diff --git a/src/common/file_watcher.h b/src/common/file_watcher.h new file mode 100644 index 000000000..7acb57888 --- /dev/null +++ b/src/common/file_watcher.h @@ -0,0 +1,32 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +namespace Common { + +enum class FileAction : u8 { + Added, + Removed, + Modified, + Renamed, + Invalid = std::numeric_limits::max(), +}; + +class FileWatcher { + using Callback = std::function; + +public: + explicit FileWatcher(const std::wstring& log_dir, Callback&& callback); + ~FileWatcher(); + +private: + struct Impl; + std::unique_ptr impl; +}; + +} // namespace Common