Cogs.Core
FileSystemWatcher.Windows.cpp
1#include "Logging/Logger.h"
2#include "FileSystemWatcher.h"
3
4#include "IO.h"
5
6#define DR_FSW_IMPLEMENTATION
7#include <dr_libs/dr_fsw.h>
8
9#include <atomic>
10#include <mutex>
11#include <thread>
12#include <unordered_map>
13
14namespace Cogs {
15 constexpr Cogs::Logging::Log logger = Cogs::Logging::getLogger("FileSystemWatcher");
16
17 struct FileEntry {
18 std::vector<FileSystemWatcher::CallbackFn> callbacks;
19 fs::file_time_type fileTime;
20 };
21
23 std::atomic<int> usage = {};
24 bool initial = true;
25 };
26
27 struct Storage {
28 drfsw_context * fswContext = nullptr;
29
30 std::mutex directoriesMutex;
31 std::unordered_map<std::string, DirectoryEntry> directories;
32
33 std::mutex filesMutex;
34 std::unordered_map<std::string, FileEntry> files;
35
36 std::thread watcherThread;
37 std::atomic<bool> watcherDone = false;
38 };
39}
40
41Cogs::FileSystemWatcher::FileSystemWatcher()
42{
43 storage = new Storage();
44 storage->fswContext = drfsw_create_context();
45
46 if (!storage->fswContext) {
47 // ...
48 return;
49 }
50
51 storage->watcherThread = std::thread([this]{
52 drfsw_event e;
53 while (!storage->watcherDone && drfsw_next_event(storage->fswContext, &e)) {
54 switch (e.type) {
55 case drfsw_event_type_updated:
56 {
57 const std::string canonicalFilePath = IO::canonical(e.absolutePath);
58
59 if (std::strcmp(e.absoluteBasePath, e.absolutePath) == 0) {
60
61 // If base path and path is equal, this is an extra event
62 // when a directory watch is just become operational.
63
64 std::unique_lock dirsLock(storage->directoriesMutex);
65 if (auto dirIt = storage->directories.find(canonicalFilePath); dirIt != storage->directories.end()) {
66 DirectoryEntry& dirEntry = dirIt->second;
67
68 // First check if this is the first time we get such an event, if not, ignore it.
69 if (!dirEntry.initial) {
70 break; // Found directory and it was not first visit, done with event.
71 }
72 else {
73 dirEntry.initial = false; // First visit
74 dirsLock.unlock(); // Done with directories and dirEntry
75
76 // Now, check all files with this prefix if the timestamp has changed.
77 const size_t N = canonicalFilePath.length();
78
79 std::lock_guard filesLock(storage->filesMutex);
80 for (auto& fileIt : storage->files) {
81 const std::string& filePath = fileIt.first;
82
83 // Check if directory is a prefix of path
84 if (N <= canonicalFilePath.size() && (std::strncmp(canonicalFilePath.c_str(), filePath.c_str(), N) == 0)) {
85
86 // Yes, check if filetime has changed
87 FileEntry& fileEntry = fileIt.second;
88 IO::FileTimeType fileTime = IO::getFileTime(filePath);
89 if (fileEntry.fileTime != fileTime) {
90 fileEntry.fileTime = fileTime;
91
92 // Yes, we have a missing file modification
93 LOG_DEBUG(logger, "Detected modification between watcher issued and operational: %s", filePath.c_str());
94 FileSystemWatcher::Event args = { filePath, FileSystemWatcher::EventType::FileUpdated };
95 for (auto& callback : fileEntry.callbacks) {
96 callback(args);
97 }
98 }
99 }
100 }
101 }
102 }
103 }
104
105 else {
106
107 // Run through callbacks and see if we have a match
108 std::lock_guard filesLock(storage->filesMutex);
109 if (auto cbIt = storage->files.find(canonicalFilePath); cbIt != storage->files.end()) {
110 FileEntry& fileEntry = cbIt->second;
111
112 IO::FileTimeType fileTime = IO::getFileTime(canonicalFilePath);
113 if (fileEntry.fileTime != fileTime) {
114 fileEntry.fileTime = fileTime;
115
116 LOG_DEBUG(logger, "Detected modification: %s", canonicalFilePath.c_str());
117
118 FileSystemWatcher::Event args = { cbIt->first, FileSystemWatcher::EventType::FileUpdated };
119 for (auto& callback : fileEntry.callbacks) {
120 callback(args);
121 }
122 }
123 break; // Found entry and checked timestamp, done with event
124 }
125 }
126 }
127 break;
128 default:
129 break;
130 }
131 }
132 });
133}
134
135Cogs::FileSystemWatcher::~FileSystemWatcher()
136{
137 if (storage->fswContext) {
138 // See dr_fsw.h for shutdown sequence documentation.
139 auto fswContext = storage->fswContext;
140 storage->fswContext = nullptr;
141 drfsw_delete_context(fswContext);
142 }
143
144 if (storage->watcherThread.joinable()) {
145 storage->watcherDone = true;
146 storage->watcherThread.join();
147 }
148
149 delete storage;
150}
151
152bool Cogs::FileSystemWatcher::watchFile(const StringView& path, const CallbackFn& callback)
153{
154 const std::string absoluteFilePath = IO::absolute(path);
155
156 if (!IO::exists(absoluteFilePath)) return false;
157
158 const std::string canonicalPath = IO::canonical(absoluteFilePath);
159 const std::string directoryPath = IO::parentPath(canonicalPath);
160
161 {
162 std::lock_guard lock(storage->filesMutex);
163
164 // If this is the first watch for a file, we set the timestamp to the current
165 // timestamp as this is the version that the caller will see when we return.
166 auto it = storage->files.insert({ canonicalPath, {{}, IO::getFileTime(canonicalPath) } });
167 it.first->second.callbacks.emplace_back(callback);
168 }
169
170 {
171 // Set up a watcher for this directory if we don't already have one.
172 std::lock_guard lock(storage->directoriesMutex);
173 if (auto found = storage->directories.find(directoryPath); found == storage->directories.end()) {
174 storage->directories[directoryPath].usage++;
175 drfsw_add_directory(storage->fswContext, directoryPath.c_str());
176 }
177 }
178
179
180 return true;
181}
182
183bool Cogs::FileSystemWatcher::unwatchFile(const StringView & path)
184{
185 const std::string absoluteFilePath = IO::absolute(path);
186
187 if (!IO::exists(absoluteFilePath)) return false;
188
189 const std::string canonicalPath = IO::canonical(path);
190 const std::string directoryPath = IO::parentPath(canonicalPath);
191
192 {
193 std::lock_guard lock(storage->directoriesMutex);
194
195 auto found = storage->directories.find(directoryPath);
196
197 if (found == storage->directories.end()) {
198 return false;
199 }
200
201 if (found->second.usage.fetch_sub(1) == 1) {
202 drfsw_remove_directory(storage->fswContext, directoryPath.c_str());
203
204 storage->directories.erase(found);
205 }
206 }
207
208 std::lock_guard lock(storage->filesMutex);
209 if (auto fileIt = storage->files.find(absoluteFilePath); fileIt != storage->files.end()) {
210 storage->files.erase(fileIt);
211 }
212
213 return true;
214}
Log implementation class.
Definition: LogManager.h:139
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:180
Contains all Cogs related functionality.
Definition: FieldSetter.h:23