common: Add FileWatcher
This commit is contained in:
@ -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
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