common: Add FileWatcher
This commit is contained in:
@ -89,6 +89,8 @@ add_library(citra_common STATIC
|
|||||||
expected.h
|
expected.h
|
||||||
file_util.cpp
|
file_util.cpp
|
||||||
file_util.h
|
file_util.h
|
||||||
|
file_watcher.cpp
|
||||||
|
file_watcher.h
|
||||||
hash.h
|
hash.h
|
||||||
linear_disk_cache.h
|
linear_disk_cache.h
|
||||||
literals.h
|
literals.h
|
||||||
|
140
src/common/file_watcher.cpp
Normal file
140
src/common/file_watcher.cpp
Normal 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
32
src/common/file_watcher.h
Normal 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
|
Reference in New Issue
Block a user