diff options
| author | Dan Engelbrecht <[email protected]> | 2024-10-10 13:13:11 +0200 |
|---|---|---|
| committer | Dan Engelbrecht <[email protected]> | 2024-10-10 13:13:11 +0200 |
| commit | 4f6966db61715c445203ca71c786417f0d650548 (patch) | |
| tree | eda3089df31777cbce9d5a2c354ded01ac078a15 | |
| parent | oplog mirror and vfs utf8 paths (#189) (diff) | |
| download | zen-de/file-change-monitor.tar.xz zen-de/file-change-monitor.zip | |
windows implementation first draftde/file-change-monitor
| -rw-r--r-- | src/zencore/filesystem.cpp | 383 | ||||
| -rw-r--r-- | src/zencore/include/zencore/filesystem.h | 25 |
2 files changed, 403 insertions, 5 deletions
diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp index 7ca076daa..6291e3039 100644 --- a/src/zencore/filesystem.cpp +++ b/src/zencore/filesystem.cpp @@ -40,6 +40,9 @@ ZEN_THIRD_PARTY_INCLUDES_END # include <sys/syslimits.h> # include <unistd.h> #endif +#if ZEN_WITH_TESTS +# include <zencore/testutils.h> +#endif #include <fmt/format.h> #include <filesystem> @@ -1709,6 +1712,222 @@ SearchPathForExecutable(std::string_view ExecutableName) #endif } +#if ZEN_PLATFORM_WINDOWS + +class FileSystemMonitor::FileMonitorImpl +{ +public: + FileMonitorImpl() {} + + ~FileMonitorImpl() + { + try + { + m_ExitSignal.store(true); + m_WakeupEvent.Set(); + + std::unique_ptr<std::thread> MonitorThread; + m_MonitorLock.WithExclusiveLock([&]() { MonitorThread = std::move(m_MonitorThread); }); + + if (MonitorThread) + { + if (MonitorThread->joinable()) + { + MonitorThread->join(); + } + MonitorThread = {}; + } + for (HANDLE Handle : m_MonitorHandles) + { + FindCloseChangeNotification(Handle); + } + } + catch (const std::exception& Ex) + { + ZEN_ERROR("FileMonitor destructor threw exception: ", Ex.what()); + } + } + + FileSystemMonitor::MonitorHandle AddPath(const std::filesystem::path& Directory, FileSystemMonitor::NotificationCallback&& Callback) + { + ZEN_ASSERT(m_ExitSignal.load() == false); + + void* Result = nullptr; + m_MonitorLock.WithExclusiveLock([&]() { + HANDLE NotifyHandle = FindFirstChangeNotification(Directory.wstring().c_str(), + FALSE, + FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME); + if (NotifyHandle == INVALID_HANDLE_VALUE) + { + ThrowLastError(fmt::format("Failed starting change notification monitoring for path '{}'", Directory)); + } + m_MonitorPaths.push_back(Directory); + m_MonitorCallbacks.push_back(std::move(Callback)); + m_MonitorHandles.push_back(NotifyHandle); + m_WakeupEvent.Set(); + Result = NotifyHandle; + if (!m_MonitorThread) + { + m_MonitorThread = std::make_unique<std::thread>([&]() { MonitorThread(); }); + } + }); + return FileSystemMonitor::MonitorHandle{.Handle = Result}; + } + + void RemovePath(FileSystemMonitor::MonitorHandle Monitor) + { + ZEN_ASSERT(m_ExitSignal.load() == false); + + HANDLE HandleToClose = RemoveMonitorHandle((HANDLE)Monitor.Handle); + if (HandleToClose != INVALID_HANDLE_VALUE) + { + FindCloseChangeNotification(HandleToClose); + } + } + + HANDLE RemoveMonitorHandle(HANDLE MonitorHandle) + { + ZEN_ASSERT(MonitorHandle != INVALID_HANDLE_VALUE); + HANDLE HandleToClose = INVALID_HANDLE_VALUE; + m_MonitorLock.WithExclusiveLock([&]() { + for (size_t MonitorIndex = 0; MonitorIndex < m_MonitorHandles.size(); MonitorIndex++) + { + if (MonitorHandle == m_MonitorHandles[MonitorIndex]) + { + m_MonitorPaths.erase(m_MonitorPaths.begin() + MonitorIndex); + m_MonitorCallbacks.erase(m_MonitorCallbacks.begin() + MonitorIndex); + m_MonitorHandles.erase(m_MonitorHandles.begin() + MonitorIndex); + HandleToClose = MonitorHandle; + break; + } + } + }); + return HandleToClose; + } + + void MonitorThread() + { + try + { + while (m_ExitSignal.load() == false) + { + std::vector<HANDLE> WaitHandles; + WaitHandles.push_back((HANDLE)m_WakeupEvent.GetWindowsHandle()); + m_MonitorLock.WithSharedLock( + [&]() { WaitHandles.insert(WaitHandles.end(), m_MonitorHandles.begin(), m_MonitorHandles.end()); }); + DWORD WaitResult = WaitForMultipleObjects((DWORD)WaitHandles.size(), WaitHandles.data(), FALSE, INFINITE); + if (WaitResult == WAIT_FAILED) + { + std::error_code Error = std::error_code(zen::GetLastError(), std::system_category()); + ZEN_ERROR("Wait failed in MonitorThread. Reason: '{}'", Error.message()); + } + else if (WaitResult == WAIT_OBJECT_0) + { + m_WakeupEvent.Reset(); + } + else if (WaitResult > WAIT_OBJECT_0 && WaitResult < (WAIT_OBJECT_0 + WaitHandles.size())) + { + size_t HandleIndex = size_t(WaitResult - WAIT_OBJECT_0); + ZEN_ASSERT(HandleIndex != 0); + ZEN_ASSERT(HandleIndex < WaitHandles.size()); + HANDLE MonitorHandle = WaitHandles[HandleIndex]; + + if (FindNextChangeNotification(MonitorHandle)) + { + bool ContinueMonitor = false; + + m_MonitorLock.WithSharedLock([&]() { + for (size_t MonitorIndex = 0; MonitorIndex < m_MonitorHandles.size(); MonitorIndex++) + { + if (MonitorHandle == m_MonitorHandles[MonitorIndex]) + { + try + { + if (m_MonitorCallbacks[MonitorIndex](FileSystemMonitor::MonitorHandle{.Handle = MonitorHandle}, + m_MonitorPaths[MonitorIndex])) + { + ContinueMonitor = true; + } + } + catch (const std::exception& Ex) + { + ZEN_ERROR( + "File monitor callback for path '{}' threw exception, monitor will be terminated. Exception: " + "'{}'", + m_MonitorPaths[MonitorIndex], + Ex.what()); + } + break; + } + } + }); + + if (!ContinueMonitor) + { + if (HANDLE HandleToClose = RemoveMonitorHandle(MonitorHandle); HandleToClose != INVALID_HANDLE_VALUE) + { + FindCloseChangeNotification(HandleToClose); + } + } + } + } + else if (WaitResult > WAIT_ABANDONED_0 && WaitResult < (WAIT_ABANDONED_0 + WaitHandles.size())) + { + size_t HandleIndex = size_t(WaitResult - WAIT_OBJECT_0); + ZEN_ASSERT(HandleIndex != 0); + ZEN_ASSERT(HandleIndex < WaitHandles.size()); + HANDLE WaitHandle = WaitHandles[HandleIndex]; + + if (HANDLE HandleToClose = RemoveMonitorHandle(WaitHandle); HandleToClose != INVALID_HANDLE_VALUE) + { + FindCloseChangeNotification(HandleToClose); + } + } + else + { + ZEN_ERROR("Unexpected result from WaitForMultipleObjects: {}", WaitResult); + } + } + } + catch (const std::exception& Ex) + { + ZEN_ERROR("File monitor thread threw exception: '{}'", Ex.what()); + } + } + +private: + std::unique_ptr<std::thread> m_MonitorThread; + RwLock m_MonitorLock; + + Event m_WakeupEvent; + std::atomic_bool m_ExitSignal = false; + + std::vector<std::filesystem::path> m_MonitorPaths; + std::vector<FileSystemMonitor::NotificationCallback> m_MonitorCallbacks; + std::vector<HANDLE> m_MonitorHandles; +}; + +#endif // ZEN_PLATFORM_WINDOWS + +FileSystemMonitor::FileSystemMonitor() : Impl(std::make_unique<FileMonitorImpl>()) +{ +} + +FileSystemMonitor::~FileSystemMonitor() +{ +} + +FileSystemMonitor::MonitorHandle +FileSystemMonitor::AddPath(const std::filesystem::path& Directory, NotificationCallback&& Callback) +{ + return Impl->AddPath(Directory, std::move(Callback)); +} +void +FileSystemMonitor::RemovePath(MonitorHandle Monitor) +{ + Impl->RemovePath(Monitor); +} + ////////////////////////////////////////////////////////////////////////// // // Testing related code follows... @@ -1721,7 +1940,7 @@ filesystem_forcelink() { } -TEST_CASE("filesystem") +TEST_CASE("filesystem.basics") { using namespace std::filesystem; @@ -1780,7 +1999,7 @@ TEST_CASE("filesystem") CHECK_EQ(BinScan.size(), BinRead.Data[0].GetSize()); } -TEST_CASE("WriteFile") +TEST_CASE("filesystem.WriteFile") { std::filesystem::path TempFile = GetRunningExecutablePath().parent_path(); TempFile /= "write_file_test"; @@ -1817,7 +2036,7 @@ TEST_CASE("WriteFile") std::filesystem::remove(TempFile); } -TEST_CASE("DiskSpaceInfo") +TEST_CASE("filesystem.DiskSpaceInfo") { std::filesystem::path BinPath = GetRunningExecutablePath(); @@ -1834,7 +2053,7 @@ TEST_CASE("DiskSpaceInfo") CHECK(int64_t(Space.Free) > 0); // Hopefully there's at least one byte free } -TEST_CASE("PathBuilder") +TEST_CASE("filesystem.PathBuilder") { # if ZEN_PLATFORM_WINDOWS const char* foo_bar = "/foo\\bar"; @@ -1868,7 +2087,7 @@ TEST_CASE("PathBuilder") # endif } -TEST_CASE("RotateDirectories") +TEST_CASE("filesystem.RotateDirectories") { std::filesystem::path TestBaseDir = GetRunningExecutablePath().parent_path() / ".test"; CleanDirectory(TestBaseDir); @@ -1917,6 +2136,160 @@ TEST_CASE("RotateDirectories") } } +TEST_CASE("filesystem.monitor") +{ + using namespace std::literals; + ScopedTemporaryDirectory TempDir; + std::filesystem::path RootPath = TempDir.Path(); + + SUBCASE("add/remove") + { + FileSystemMonitor Monitor; + FileSystemMonitor::MonitorHandle RootHandle = + Monitor.AddPath(RootPath, [](const FileSystemMonitor::MonitorHandle&, const std::filesystem::path&) { + CHECK(false); + return true; + }); + Monitor.RemovePath(RootHandle); + } + + SUBCASE("single") + { + Event MonitoringDone; + FileSystemMonitor Monitor; + const std::string NewFileContent("This is the new file content"); + const IoBuffer Content(IoBuffer::Wrap, NewFileContent.data(), NewFileContent.length()); + const std::string NewFileName("newfile"); + + FileSystemMonitor::MonitorHandle RootHandle = + Monitor.AddPath(RootPath, [&](const FileSystemMonitor::MonitorHandle&, const std::filesystem::path& Root) { + if (std::filesystem::exists(Root / NewFileName)) + { + IoBuffer ReadContent = ReadFile(Root / NewFileName).Flatten(); + if (ReadContent.Size() == Content.Size()) + { + if (ReadContent.GetView().EqualBytes(Content.GetView())) + { + // All done, no need to monitor anymore + MonitoringDone.Set(); + return false; + } + } + } + return true; + }); + + WriteFile(RootPath / NewFileName, Content); + CHECK(MonitoringDone.Wait(1000)); + + MonitoringDone.Reset(); + + RootHandle = Monitor.AddPath(RootPath, [&](const FileSystemMonitor::MonitorHandle&, const std::filesystem::path& Root) { + if (!std::filesystem::exists(Root / NewFileName)) + { + MonitoringDone.Set(); + return false; + } + return true; + }); + + WriteFile(RootPath / "hej1", IoBuffer(IoBuffer::Wrap, "hej1", 3)); + CHECK(!MonitoringDone.Wait(10)); + WriteFile(RootPath / "hej2", IoBuffer(IoBuffer::Wrap, "hej2", 3)); + CHECK(!MonitoringDone.Wait(10)); + std::filesystem::remove(RootPath / NewFileName); + CHECK(MonitoringDone.Wait(1000)); + } + + SUBCASE("multi") + { + Event MonitoringDone[3]; + FileSystemMonitor Monitor; + const std::string NewFileContent("This is the new file content"); + const IoBuffer Content(IoBuffer::Wrap, NewFileContent.data(), NewFileContent.length()); + const std::string NewFileName("newfile"); + std::filesystem::path RootPaths[3]{RootPath / "0", RootPath / "1", RootPath / "2"}; + CreateDirectories(RootPaths[0]); + CreateDirectories(RootPaths[1]); + CreateDirectories(RootPaths[2]); + + FileSystemMonitor::MonitorHandle RootHandles[3]; + + RootHandles[0] = Monitor.AddPath(RootPaths[0], [&](const FileSystemMonitor::MonitorHandle&, const std::filesystem::path& Root) { + if (std::filesystem::exists(Root / NewFileName)) + { + IoBuffer ReadContent = ReadFile(Root / NewFileName).Flatten(); + if (ReadContent.Size() == Content.Size()) + { + if (ReadContent.GetView().EqualBytes(Content.GetView())) + { + // All done, no need to monitor anymore + MonitoringDone[0].Set(); + return true; + } + } + } + return true; + }); + RootHandles[1] = Monitor.AddPath(RootPaths[1], [&](const FileSystemMonitor::MonitorHandle&, const std::filesystem::path& Root) { + if (std::filesystem::exists(Root / NewFileName)) + { + IoBuffer ReadContent = ReadFile(Root / NewFileName).Flatten(); + if (ReadContent.Size() == Content.Size()) + { + if (ReadContent.GetView().EqualBytes(Content.GetView())) + { + // All done, no need to monitor anymore + MonitoringDone[1].Set(); + return true; + } + } + } + return true; + }); + RootHandles[2] = Monitor.AddPath(RootPaths[2], [&](const FileSystemMonitor::MonitorHandle&, const std::filesystem::path& Root) { + if (std::filesystem::exists(Root / NewFileName)) + { + IoBuffer ReadContent = ReadFile(Root / NewFileName).Flatten(); + if (ReadContent.Size() == Content.Size()) + { + if (ReadContent.GetView().EqualBytes(Content.GetView())) + { + // All done, no need to monitor anymore + MonitoringDone[2].Set(); + return true; + } + } + } + return true; + }); + + CHECK(!MonitoringDone[0].Wait(10)); + WriteFile(RootPaths[0] / NewFileName, Content); + CHECK(!MonitoringDone[2].Wait(10)); + WriteFile(RootPaths[2] / NewFileName, Content); + + CHECK(!MonitoringDone[1].Wait(10)); + CHECK(MonitoringDone[0].Wait(1000)); + MonitoringDone[0].Reset(); + CHECK(MonitoringDone[2].Wait(1000)); + MonitoringDone[2].Reset(); + + CHECK(!MonitoringDone[1].Wait(10)); + WriteFile(RootPaths[1] / NewFileName, Content); + CHECK(MonitoringDone[1].Wait(1000)); + MonitoringDone[1].Reset(); + + CHECK(!MonitoringDone[0].Wait(10)); + CHECK(!MonitoringDone[1].Wait(10)); + CHECK(!MonitoringDone[1].Wait(10)); + + Monitor.RemovePath(RootHandles[0]); + Monitor.RemovePath(RootHandles[1]); + Monitor.RemovePath(RootHandles[2]); + } +} + #endif } // namespace zen diff --git a/src/zencore/include/zencore/filesystem.h b/src/zencore/include/zencore/filesystem.h index 0aab6a4ae..a71a9fea1 100644 --- a/src/zencore/include/zencore/filesystem.h +++ b/src/zencore/include/zencore/filesystem.h @@ -225,6 +225,31 @@ std::filesystem::path SearchPathForExecutable(std::string_view ExecutableName); std::error_code RotateFiles(const std::filesystem::path& Filename, std::size_t MaxFiles); std::error_code RotateDirectories(const std::filesystem::path& DirectoryName, std::size_t MaxDirectories); +class FileSystemMonitor +{ +public: + FileSystemMonitor(); + ~FileSystemMonitor(); + + struct MonitorHandle + { + void* Handle = nullptr; + }; + + // The notification handle is called each time a change in the directory is detected. + // A shared lock in the FileSystemMonitor is held while the callback is executing so calling + // AddPath or RemovePath during this is forbidden and causes deadlocks. + // While a callback is executing no other monitor callbacks will be called for the FileSystemMonitor instance. + typedef std::function<bool(const MonitorHandle&, const std::filesystem::path&)> NotificationCallback; + + MonitorHandle AddPath(const std::filesystem::path& Directory, NotificationCallback&& Callback); + void RemovePath(MonitorHandle Monitor); + +private: + class FileMonitorImpl; + std::unique_ptr<FileMonitorImpl> Impl; +}; + ////////////////////////////////////////////////////////////////////////// void filesystem_forcelink(); // internal |