common: Add FileWatcher

This commit is contained in:
GPUCode
2023-11-11 15:35:11 +02:00
parent fd32a82b4e
commit 2b7faf60a3
3 changed files with 174 additions and 0 deletions

View File

@ -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

140
src/common/file_watcher.cpp Normal file
View File

@ -0,0 +1,140 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <windows.h>
#include <thread>
#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<u8, DirectoryWatcherBufferSize> 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<Impl>(log_dir, std::move(callback))} {}
} // namespace Common

32
src/common/file_watcher.h Normal file
View File

@ -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 <functional>
#include <string>
namespace Common {
enum class FileAction : u8 {
Added,
Removed,
Modified,
Renamed,
Invalid = std::numeric_limits<u8>::max(),
};
class FileWatcher {
using Callback = std::function<void(const std::wstring&, FileAction)>;
public:
explicit FileWatcher(const std::wstring& log_dir, Callback&& callback);
~FileWatcher();
private:
struct Impl;
std::unique_ptr<Impl> impl;
};
} // namespace Common