1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2008, guenther
6 * Copyright (c) 2017-2023, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// An inotify wrapper
19
20#include <C4Include.h>
21#include <C4FileMonitor.h>
22#include <C4Application.h>
23#include <C4Log.h>
24
25#include <StdFile.h>
26
27#ifdef _WIN32
28#include "StdStringEncodingConverter.h"
29#endif
30
31#ifdef __linux__
32#include "C4Awaiter.h"
33
34#include <cerrno>
35#include <utility>
36
37#include <sys/inotify.h>
38#include <sys/ioctl.h>
39
40static constexpr std::uint32_t NotificationMask{IN_CREATE | IN_MODIFY | IN_MOVED_TO | IN_MOVE_SELF};
41
42C4FileMonitor::C4FileMonitor(ChangeNotifyCallback &&callback)
43 : callback{std::move(callback)}
44{
45 fd = inotify_init();
46 if (fd == -1)
47 {
48 throw std::runtime_error{std::strerror(errno)};
49 }
50
51 task = Execute();
52}
53
54C4FileMonitor::~C4FileMonitor()
55{
56 if (task)
57 {
58 std::move(task).CancelAndWait();
59 Application.InteractiveThread.ClearCallback(eEvent: Ev_FileChange, pnNetworkCallback: this);
60 }
61
62 while (close(fd: fd) == -1 && errno == EINTR) {}
63}
64
65void C4FileMonitor::StartMonitoring()
66{
67 Application.InteractiveThread.SetCallback(eEvent: Ev_FileChange, pnNetworkCallback: this);
68 task.Start();
69}
70
71void C4FileMonitor::AddDirectory(const char *const path)
72{
73 const int wd{inotify_add_watch(fd: fd, name: path, mask: NotificationMask | IN_ONLYDIR)};
74 if (wd != -1)
75 {
76 watchDescriptors[wd] = path;
77 }
78}
79
80C4FileMonitor::TaskType C4FileMonitor::Execute()
81{
82 for (;;)
83 {
84 co_await C4Awaiter::ResumeOnSignal(fd: {.fd = fd, .events = POLLIN});
85
86 int bytesToRead;
87 if (ioctl(fd: fd, FIONREAD, &bytesToRead) < 0)
88 {
89 continue;
90 }
91
92 if (std::cmp_less(t: bytesToRead, u: sizeof(inotify_event)))
93 {
94 continue;
95 }
96
97 const auto size = static_cast<std::size_t>(bytesToRead);
98 const auto buf = std::make_unique<char[]>(num: size);
99
100 if (read(fd: fd, buf: buf.get(), nbytes: static_cast<std::size_t>(bytesToRead)) != bytesToRead)
101 {
102 continue;
103 }
104
105 std::size_t offset{0};
106 auto *event = reinterpret_cast<inotify_event *>(buf.get());
107
108 for (;;)
109 {
110 if (event->mask & NotificationMask)
111 {
112 Application.InteractiveThread.PushEvent(eEventType: Ev_FileChange, data: watchDescriptors[event->wd]);
113 }
114
115 const std::size_t eventSize{sizeof(inotify_event) + event->len};
116
117 offset += eventSize;
118 if (offset >= size)
119 {
120 break;
121 }
122
123 event = reinterpret_cast<inotify_event *>(reinterpret_cast<char *>(event) + offset);
124 }
125 }
126}
127
128#elif defined(_WIN32)
129
130C4FileMonitor::MonitoredDirectory::MonitoredDirectory(winrt::file_handle &&handle, std::string path)
131 : handle{std::move(handle)}, path{std::move(path)}
132{
133 task = Execute();
134}
135
136void C4FileMonitor::MonitoredDirectory::StartMonitoring()
137{
138 task.Start();
139}
140
141auto C4FileMonitor::MonitoredDirectory::Execute() -> TaskType
142{
143 C4ThreadPool::Io io{handle.get()};
144
145 std::chrono::time_point<std::chrono::steady_clock> lastNotification{};
146 std::wstring lastNotificationFilename;
147
148 // Avoid multiple notifications in a very short time span
149 static constexpr std::chrono::milliseconds NotificationDebounce{10};
150
151 for (;;)
152 {
153 const std::uint64_t numberOfBytesTransferred{co_await io.ExecuteAsync([this](const HANDLE handle, OVERLAPPED *const overlapped)
154 {
155 if (ReadDirectoryChangesW(handle, buffer.data(), buffer.size(), false, NotificationFilter, nullptr, overlapped, nullptr))
156 {
157 SetLastError(ERROR_IO_PENDING);
158 }
159
160 return false;
161 })};
162
163 DWORD offset{0};
164 auto *information = reinterpret_cast<FILE_NOTIFY_INFORMATION *>(buffer.data());
165
166 for (;;)
167 {
168 const std::wstring_view filename{static_cast<wchar_t *>(information->FileName), information->FileNameLength / sizeof(wchar_t)};
169 const auto now = std::chrono::steady_clock::now();
170
171 if (now - lastNotification >= NotificationDebounce || lastNotificationFilename != filename)
172 {
173 lastNotification = now;
174 lastNotificationFilename = filename;
175 Application.InteractiveThread.PushEvent(Ev_FileChange, path + DirectorySeparator + StdStringEncodingConverter::Utf16ToWinAcp(filename));
176 }
177
178 if (!information->NextEntryOffset || offset + information->NextEntryOffset > std::min<std::size_t>(buffer.size(), numberOfBytesTransferred))
179 {
180 break;
181 }
182
183 information = reinterpret_cast<FILE_NOTIFY_INFORMATION *>(reinterpret_cast<char *>(information) + information->NextEntryOffset);
184 }
185 }
186}
187
188C4FileMonitor::C4FileMonitor(ChangeNotifyCallback &&callback)
189 : callback{std::move(callback)}
190{
191}
192
193C4FileMonitor::~C4FileMonitor()
194{
195 if (started)
196 {
197 Application.InteractiveThread.ClearCallback(Ev_FileChange, this);
198 }
199}
200
201void C4FileMonitor::StartMonitoring()
202{
203 Application.InteractiveThread.SetCallback(Ev_FileChange, this);
204 std::ranges::for_each(directories, &MonitoredDirectory::StartMonitoring);
205 started = true;
206}
207
208void C4FileMonitor::AddDirectory(const char *const path)
209{
210 winrt::file_handle directory{CreateFileA(
211 path,
212 FILE_LIST_DIRECTORY,
213 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
214 nullptr,
215 OPEN_EXISTING,
216 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
217 nullptr
218 )};
219
220 if (!directory)
221 {
222 return;
223 }
224
225 try
226 {
227 directories.emplace_back(std::make_unique<MonitoredDirectory>(
228 std::move(directory),
229 path
230 ));
231 }
232 catch (const winrt::hresult_error &)
233 {
234 }
235}
236
237#elif defined(__APPLE__)
238
239C4FileMonitor::C4FileMonitor(ChangeNotifyCallback &&callback)
240 : callback{std::move(callback)}
241{
242}
243
244C4FileMonitor::~C4FileMonitor()
245{
246 if (started)
247 {
248 FSEventStreamStop(eventStream);
249 FSEventStreamSetDispatchQueue(eventStream, nullptr);
250 FSEventStreamRelease(eventStream);
251
252 Application.InteractiveThread.ClearCallback(Ev_FileChange, this);
253 }
254}
255
256static void EventStreamCallback(ConstFSEventStreamRef streamRef, void *const clientCallbackInfo, const std::size_t numEvents, void *const eventPaths, const FSEventStreamEventFlags *const eventFlags, const FSEventStreamEventId *const eventIds)
257{
258 for (std::size_t i{0}; i < numEvents; ++i)
259 {
260 if (eventFlags[i] & (kFSEventStreamEventFlagUserDropped | kFSEventStreamEventFlagKernelDropped))
261 {
262 continue;
263 }
264
265 std::string path{reinterpret_cast<const char **>(eventPaths)[i]};
266 if (path.ends_with(DirectorySeparator))
267 {
268 path.resize(path.size() - 1);
269 }
270
271 Application.InteractiveThread.PushEvent(Ev_FileChange, std::move(path));
272 }
273}
274
275void C4FileMonitor::StartMonitoring()
276{
277 const CFUniquePtr<CFArrayRef> array{CFArrayCreate(nullptr, reinterpret_cast<const void **>(paths.data()), paths.size(), &kCFTypeArrayCallBacks)};
278
279 FSEventStreamContext context{
280 0,
281 reinterpret_cast<void *>(this),
282 nullptr,
283 nullptr,
284 [](const void *) { return CFSTR("C4FileMonitor"); }
285 };
286
287 eventStream = FSEventStreamCreate(nullptr, &EventStreamCallback, &context, array.get(), kFSEventStreamEventIdSinceNow, 1, 0);
288
289 if (eventStream)
290 {
291 FSEventStreamSetDispatchQueue(eventStream, dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0));
292 FSEventStreamStart(eventStream);
293
294 Application.InteractiveThread.SetCallback(Ev_FileChange, this);
295 started = true;
296 }
297}
298
299void C4FileMonitor::AddDirectory(const char *const path)
300{
301 if (!started)
302 {
303 paths.emplace_back(CFStringCreateWithCString(nullptr, path, kCFStringEncodingUTF8));
304 }
305}
306
307#endif
308
309void C4FileMonitor::OnThreadEvent(const C4InteractiveEventType event, const std::any &eventData)
310{
311 if (event != Ev_FileChange) return;
312
313 callback(std::any_cast<const std::string &>(any: eventData).c_str());
314}
315