|
| 1 | +#include <future> |
| 2 | +#include <ranges> |
| 3 | + |
| 4 | +#include <Windows.h> |
| 5 | + |
| 6 | +#include <DynamicOutput/DynamicOutput.hpp> |
| 7 | +#include <Helpers/String.hpp> |
| 8 | + |
| 9 | +namespace RC |
| 10 | +{ |
| 11 | + struct Data |
| 12 | + { |
| 13 | + HANDLE handle{}; |
| 14 | + std::vector<uint8_t> buffer{}; |
| 15 | + std::filesystem::path path{}; |
| 16 | + Data() |
| 17 | + { |
| 18 | + buffer.resize(1000); |
| 19 | + } |
| 20 | + }; |
| 21 | + |
| 22 | + auto init_filesystem_watcher(FilesystemWatcher& watcher, const std::filesystem::path& path) -> void |
| 23 | + { |
| 24 | + auto data = new Data{}; |
| 25 | + watcher.m_handle = data; |
| 26 | + watcher.m_handles.emplace_back(static_cast<void*>(data)); |
| 27 | + data->handle = CreateFileW(path.native().c_str(), |
| 28 | + FILE_LIST_DIRECTORY, |
| 29 | + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, |
| 30 | + nullptr, |
| 31 | + OPEN_EXISTING, |
| 32 | + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, |
| 33 | + nullptr); |
| 34 | + data->path = path; |
| 35 | + } |
| 36 | + |
| 37 | + FilesystemWatcher::~FilesystemWatcher() |
| 38 | + { |
| 39 | + m_stop_source.request_stop(); |
| 40 | + m_polling_thread.join(); |
| 41 | + auto data = static_cast<Data*>(m_handle); |
| 42 | + if (data && data->handle) |
| 43 | + { |
| 44 | + FindCloseChangeNotification(data->handle); |
| 45 | + delete data; |
| 46 | + m_handle = nullptr; |
| 47 | + } |
| 48 | + } |
| 49 | + |
| 50 | + auto FilesystemWatcher::poll(std::stop_token stop_token) -> void |
| 51 | + { |
| 52 | + std::vector<HANDLE> handles{}; |
| 53 | + for (const auto& handle_data_raw : m_handles) |
| 54 | + { |
| 55 | + auto handle_data = static_cast<Data*>(handle_data_raw); |
| 56 | + handles.emplace_back(FindFirstChangeNotificationW(handle_data->path.wstring().c_str(), false, FILE_NOTIFY_CHANGE_LAST_WRITE)); |
| 57 | + if (handles.back() == INVALID_HANDLE_VALUE) |
| 58 | + { |
| 59 | + Output::send<LogLevel::Error>(STR("ERROR: FindFirstChangeNotification function failed.\n")); |
| 60 | + return; |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + std::vector<std::future<void>> futures{}; |
| 65 | + static constexpr int32_t s_max_objects_per_thread = MAXIMUM_WAIT_OBJECTS; |
| 66 | + const auto use_single_thread = handles.size() <= s_max_objects_per_thread; |
| 67 | + futures.resize(use_single_thread ? 1 : handles.size() / s_max_objects_per_thread); |
| 68 | + |
| 69 | + size_t range_start{}; |
| 70 | + for (const auto& [index, future] : std::ranges::enumerate_view(futures)) |
| 71 | + { |
| 72 | + if (stop_token.stop_requested()) |
| 73 | + { |
| 74 | + break; |
| 75 | + } |
| 76 | + const auto data = &handles[range_start]; |
| 77 | + const auto last_chunk = handles.size() <= s_max_objects_per_thread; |
| 78 | + const auto size = last_chunk ? handles.size() : s_max_objects_per_thread; |
| 79 | + Output::send<LogLevel::Verbose>(STR("Creating filesystem watcher {} with range {}-{}, range_start: {}, data: {}\n"), index, range_start, range_start + size, range_start, (void*)data); |
| 80 | + future = std::async(std::launch::async, [](FilesystemWatcher* watcher, std::vector<HANDLE>* in_handles, const HANDLE* data, const size_t size, const size_t range_start, std::stop_token* stop_token) { |
| 81 | + while (!stop_token->stop_requested()) |
| 82 | + { |
| 83 | + auto status = WaitForMultipleObjects(size, data, false, INFINITE); |
| 84 | + if (status == WAIT_TIMEOUT || status == WAIT_ABANDONED_0 || status == WAIT_FAILED) |
| 85 | + { |
| 86 | + continue; |
| 87 | + } |
| 88 | + auto index = status + range_start; |
| 89 | + auto handle_data = static_cast<Data*>(watcher->m_handles[index]); |
| 90 | + for (const auto& watch : watcher->m_watches) |
| 91 | + { |
| 92 | + // The intent here is to allow notifiers based on the file that was changed. |
| 93 | + // But because the FindFirst/NextChangeNotification APIs don't allow you to retrieve any information about what has changed, we can't do that. |
| 94 | + // Instead, we allow everything through via the "*" option. |
| 95 | + // When/if this code is revamped to use ReadDirectoryChangesW, we can retrieve the file name and stop bypassing with "*". |
| 96 | + // For now, if the name of the watcher notifier isn't "*", the watch will never get notified. |
| 97 | + bool match_all = watch.name == "*"; |
| 98 | + if (match_all/* || watch.name == name_no_extension*/) |
| 99 | + { |
| 100 | + auto now = std::chrono::high_resolution_clock::now(); |
| 101 | + if (now - watcher->m_last_notification < watcher->m_min_duration_between_notifications) |
| 102 | + { |
| 103 | + continue; |
| 104 | + } |
| 105 | + watcher->m_last_notification = now; |
| 106 | + // TODO: If 'match_all' is true, we don't have any file information besides what was registered for the watch. |
| 107 | + // It would be useful for the user to have this information. |
| 108 | + // We never actully have this information right now because the FindFirst/NextChangeNotification APIs don't allow you to know what changed. |
| 109 | + // The ReadDirectoryChangesW API does, but it's a more complicated API. |
| 110 | + watch.notify(handle_data->path, match_all); |
| 111 | + } |
| 112 | + } |
| 113 | + if (!FindNextChangeNotification((*in_handles)[index])) |
| 114 | + { |
| 115 | + Output::send<LogLevel::Error>(STR("ERROR: FindNextChangeNotification function failed. Code: {}\n"), GetLastError()); |
| 116 | + } |
| 117 | + } |
| 118 | + }, this, &handles, data, size, range_start, &stop_token); |
| 119 | + range_start += s_max_objects_per_thread; |
| 120 | + } |
| 121 | + |
| 122 | + // Note that we'll only ever execute this code if all the futures have returned. |
| 123 | + // Futures can be triggered to return via the stop_token passed to this function. |
| 124 | + // This stop_token is triggered when FilesystemWatcher goes out of scope. |
| 125 | + for (const auto& [index, future] : std::ranges::enumerate_view(futures)) |
| 126 | + { |
| 127 | + future.wait(); |
| 128 | + } |
| 129 | + } |
| 130 | +} // namespace RC |
0 commit comments