aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Engelbrecht <[email protected]>2024-10-10 13:13:11 +0200
committerDan Engelbrecht <[email protected]>2024-10-10 13:13:11 +0200
commit4f6966db61715c445203ca71c786417f0d650548 (patch)
treeeda3089df31777cbce9d5a2c354ded01ac078a15
parentoplog mirror and vfs utf8 paths (#189) (diff)
downloadzen-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.cpp383
-rw-r--r--src/zencore/include/zencore/filesystem.h25
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