1//************************************ bs::framework - Copyright 2018 Marko Pintera **************************************//
2//*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********//
3#include "Platform/BsFolderMonitor.h"
4#include "FileSystem/BsFileSystem.h"
5#include "Error/BsException.h"
6#include <sys/inotify.h>
7
8namespace bs
9{
10 struct FolderMonitor::FolderWatchInfo
11 {
12 FolderWatchInfo(const Path& folderToMonitor, int inHandle, bool monitorSubdirectories, FolderChangeBits filter);
13 ~FolderWatchInfo();
14
15 void startMonitor();
16 void stopMonitor();
17
18 void addPath(const Path& path);
19 void removePath(const Path& path);
20 Path getPath(INT32 handle);
21
22 Path folderToMonitor;
23 int dirHandle;
24 bool monitorSubdirectories;
25 FolderChangeBits filter;
26
27 UnorderedMap<Path, INT32> pathToHandle;
28 UnorderedMap<INT32, Path> handleToPath;
29 };
30
31 FolderMonitor::FolderWatchInfo::FolderWatchInfo(const Path& folderToMonitor, int inHandle, bool monitorSubdirectories,
32 FolderChangeBits filter)
33 : folderToMonitor(folderToMonitor), dirHandle(inHandle), monitorSubdirectories(monitorSubdirectories)
34 , filter(filter)
35 { }
36
37 FolderMonitor::FolderWatchInfo::~FolderWatchInfo()
38 {
39 stopMonitor();
40 }
41
42 void FolderMonitor::FolderWatchInfo::startMonitor()
43 {
44 addPath(folderToMonitor);
45
46 if(monitorSubdirectories)
47 {
48 FileSystem::iterate(folderToMonitor, nullptr, [this](const Path& path)
49 {
50 addPath(path);
51 return true;
52 });
53 }
54 }
55
56 void FolderMonitor::FolderWatchInfo::stopMonitor()
57 {
58 for(auto& entry : pathToHandle)
59 inotify_rm_watch(dirHandle, entry.second);
60
61 pathToHandle.clear();
62 }
63
64 void FolderMonitor::FolderWatchInfo::addPath(const Path& path)
65 {
66 String pathString = path.toString();
67
68 INT32 watchHandle = inotify_add_watch(dirHandle, pathString.c_str(), IN_ALL_EVENTS);
69 if(watchHandle == -1)
70 {
71 String error = strerror(errno);
72 LOGERR("Unable to start folder monitor for path: \"" + pathString +"\". Error: " + error);
73 }
74
75 pathToHandle[path] = watchHandle;
76 handleToPath[watchHandle] = path;
77 }
78
79 void FolderMonitor::FolderWatchInfo::removePath(const Path& path)
80 {
81 auto iterFind = pathToHandle.find(path);
82 if(iterFind != pathToHandle.end())
83 {
84 INT32 watchHandle = iterFind->second;
85 pathToHandle.erase(iterFind);
86
87 handleToPath.erase(watchHandle);
88 }
89 }
90
91 Path FolderMonitor::FolderWatchInfo::getPath(INT32 handle)
92 {
93 auto iterFind = handleToPath.find(handle);
94 if(iterFind != handleToPath.end())
95 return iterFind->second;
96
97 return Path::BLANK;
98 }
99
100 class FolderMonitor::FileNotifyInfo
101 {
102 };
103
104 enum class FileActionType
105 {
106 Added,
107 Removed,
108 Modified,
109 Renamed
110 };
111
112 struct FileAction
113 {
114 static FileAction* createAdded(const String& fileName)
115 {
116 UINT8* bytes = (UINT8*)bs_alloc((UINT32)(sizeof(FileAction) + (fileName.size() + 1) * sizeof(String::value_type)));
117
118 FileAction* action = (FileAction*)bytes;
119 bytes += sizeof(FileAction);
120
121 action->oldName = nullptr;
122 action->newName = (String::value_type*)bytes;
123 action->type = FileActionType::Added;
124
125 memcpy(action->newName, fileName.data(), fileName.size() * sizeof(String::value_type));
126 action->newName[fileName.size()] = L'\0';
127 action->lastSize = 0;
128 action->checkForWriteStarted = false;
129
130 return action;
131 }
132
133 static FileAction* createRemoved(const String& fileName)
134 {
135 UINT8* bytes = (UINT8*)bs_alloc((UINT32)(sizeof(FileAction) + (fileName.size() + 1) * sizeof(String::value_type)));
136
137 FileAction* action = (FileAction*)bytes;
138 bytes += sizeof(FileAction);
139
140 action->oldName = nullptr;
141 action->newName = (String::value_type*)bytes;
142 action->type = FileActionType::Removed;
143
144 memcpy(action->newName, fileName.data(), fileName.size() * sizeof(String::value_type));
145 action->newName[fileName.size()] = L'\0';
146 action->lastSize = 0;
147 action->checkForWriteStarted = false;
148
149 return action;
150 }
151
152 static FileAction* createModified(const String& fileName)
153 {
154 UINT8* bytes = (UINT8*)bs_alloc((UINT32)(sizeof(FileAction) + (fileName.size() + 1) * sizeof(String::value_type)));
155
156 FileAction* action = (FileAction*)bytes;
157 bytes += sizeof(FileAction);
158
159 action->oldName = nullptr;
160 action->newName = (String::value_type*)bytes;
161 action->type = FileActionType::Modified;
162
163 memcpy(action->newName, fileName.data(), fileName.size() * sizeof(String::value_type));
164 action->newName[fileName.size()] = L'\0';
165 action->lastSize = 0;
166 action->checkForWriteStarted = false;
167
168 return action;
169 }
170
171 static FileAction* createRenamed(const String& oldFilename, const String& newfileName)
172 {
173 UINT8* bytes = (UINT8*)bs_alloc((UINT32)(sizeof(FileAction) +
174 (oldFilename.size() + newfileName.size() + 2) * sizeof(String::value_type)));
175
176 FileAction* action = (FileAction*)bytes;
177 bytes += sizeof(FileAction);
178
179 action->oldName = (String::value_type*)bytes;
180 bytes += (oldFilename.size() + 1) * sizeof(String::value_type);
181
182 action->newName = (String::value_type*)bytes;
183 action->type = FileActionType::Modified;
184
185 memcpy(action->oldName, oldFilename.data(), oldFilename.size() * sizeof(String::value_type));
186 action->oldName[oldFilename.size()] = L'\0';
187
188 memcpy(action->newName, newfileName.data(), newfileName.size() * sizeof(String::value_type));
189 action->newName[newfileName.size()] = L'\0';
190 action->lastSize = 0;
191 action->checkForWriteStarted = false;
192
193 return action;
194 }
195
196 static void destroy(FileAction* action)
197 {
198 bs_free(action);
199 }
200
201 String::value_type* oldName;
202 String::value_type* newName;
203 FileActionType type;
204
205 UINT64 lastSize;
206 bool checkForWriteStarted;
207 };
208
209 struct FolderMonitor::Pimpl
210 {
211 Vector<FolderWatchInfo*> monitors;
212
213 Vector<FileAction*> fileActions;
214 Vector<FileAction*> activeFileActions;
215
216 int inHandle;
217 bool started;
218 Mutex mainMutex;
219 Thread* workerThread;
220 };
221
222 FolderMonitor::FolderMonitor()
223 {
224 m = bs_new<Pimpl>();
225 m->workerThread = nullptr;
226 m->inHandle = 0;
227 m->started = false;
228 }
229
230 FolderMonitor::~FolderMonitor()
231 {
232 stopMonitorAll();
233
234 // No need for mutex since we know worker thread is shut down by now
235 for(auto& action : m->fileActions)
236 FileAction::destroy(action);
237
238 bs_delete(m);
239 }
240
241 void FolderMonitor::startMonitor(const Path& folderPath, bool subdirectories, FolderChangeBits changeFilter)
242 {
243 if(!FileSystem::isDirectory(folderPath))
244 {
245 LOGERR("Provided path \"" + folderPath.toString() + "\" is not a directory");
246 return;
247 }
248
249 // Check if there is overlap with existing monitors
250 for(auto& monitor : m->monitors)
251 {
252 // Identical monitor exists
253 if(monitor->folderToMonitor.equals(folderPath))
254 {
255 LOGWRN("Folder is already monitored, cannot monitor it again.");
256 return;
257 }
258
259 // This directory is part of a directory that's being monitored
260 if(monitor->monitorSubdirectories && folderPath.includes(monitor->folderToMonitor))
261 {
262 LOGWRN("Folder is already monitored, cannot monitor it again.");
263 return;
264 }
265
266 // This directory would include a directory of another monitor
267 if(subdirectories && monitor->folderToMonitor.includes(folderPath))
268 {
269 LOGWRN("Cannot add a recursive monitor as it conflicts with a previously monitored path");
270 return;
271 }
272 }
273
274 // Initialize inotify if required
275 if(!m->started)
276 {
277 Lock lock(m->mainMutex);
278
279 m->inHandle = inotify_init();
280 m->started = true;
281 }
282
283 FolderWatchInfo* watchInfo = bs_new<FolderWatchInfo>(folderPath, m->inHandle, subdirectories, changeFilter);
284
285 // Register and start the monitor
286 {
287 Lock lock(m->mainMutex);
288
289 m->monitors.push_back(watchInfo);
290 watchInfo->startMonitor();
291 }
292
293 // Start the worker thread if it isn't already
294 if(m->workerThread == nullptr)
295 {
296 m->workerThread = bs_new<Thread>(std::bind(&FolderMonitor::workerThreadMain, this));
297
298 if(m->workerThread == nullptr)
299 LOGERR("Failed to create a new worker thread for folder monitoring");
300 }
301 }
302
303 void FolderMonitor::stopMonitor(const Path& folderPath)
304 {
305 auto findIter = std::find_if(m->monitors.begin(), m->monitors.end(),
306 [&](const FolderWatchInfo* x) { return x->folderToMonitor == folderPath; });
307
308 if(findIter != m->monitors.end())
309 {
310 // Special case if this is the last monitor
311 if(m->monitors.size() == 1)
312 stopMonitorAll();
313 else
314 {
315 Lock lock(m->mainMutex);
316 FolderWatchInfo* watchInfo = *findIter;
317
318 watchInfo->stopMonitor();
319 bs_delete(watchInfo);
320
321 m->monitors.erase(findIter);
322 }
323 }
324 }
325
326 void FolderMonitor::stopMonitorAll()
327 {
328 if(m->started)
329 {
330 Lock lock(m->mainMutex);
331
332 // First tell the thread it's ready to be shutdown
333 m->started = false;
334
335 // Remove all watches (this will also wake up the thread). Note that at least one watch must be present otherwise
336 // the thread won't wake up (we ensure that elsewhere).
337 for (auto& watchInfo : m->monitors)
338 {
339 watchInfo->stopMonitor();
340 bs_delete(watchInfo);
341 }
342
343 m->monitors.clear();
344 }
345
346 // Wait for the thread to shutdown
347 if(m->workerThread != nullptr)
348 {
349 m->workerThread->join();
350 bs_delete(m->workerThread);
351 m->workerThread = nullptr;
352 }
353
354 // Close the inotify handle
355 {
356 Lock lock(m->mainMutex);
357 if (m->inHandle != 0)
358 {
359 close(m->inHandle);
360 m->inHandle = 0;
361 }
362 }
363 }
364
365 void FolderMonitor::workerThreadMain()
366 {
367 static const UINT32 BUFFER_SIZE = 16384;
368
369 bool shouldRun;
370 INT32 watchHandle;
371 {
372 Lock(m->mainMutex);
373 watchHandle = m->inHandle;
374 shouldRun = m->started;
375 }
376
377 UINT8 buffer[BUFFER_SIZE];
378
379 while(shouldRun)
380 {
381 INT32 length = (INT32)read(watchHandle, buffer, sizeof(buffer));
382
383 // Handle was closed, shutdown thread
384 if (length < 0)
385 return;
386
387 // Note: Must be after read, so shutdown can be started when we remove the watches (as then read() will return)
388 {
389 Lock(m->mainMutex);
390 shouldRun = m->started;
391 }
392
393 INT32 readPos = 0;
394 while(readPos < length)
395 {
396 inotify_event* event = (inotify_event*)&buffer[readPos];
397 if(event->len > 0)
398 {
399 {
400 Lock lock(m->mainMutex);
401
402 Path path;
403 FolderWatchInfo* monitor = nullptr;
404 for (auto& entry : m->monitors)
405 {
406 path = entry->getPath(event->wd);
407 if (!path.isEmpty())
408 {
409 path.append(event->name);
410 monitor = entry;
411 break;
412 }
413 }
414
415 // This can happen if the path got removed during some recent previous event
416 if(monitor == nullptr)
417 goto next;
418
419 // Need to add/remove sub-directories to/from watch list
420 bool isDirectory = (event->mask & IN_ISDIR) != 0;
421 if(isDirectory && monitor->monitorSubdirectories)
422 {
423 bool added = (event->mask & (IN_CREATE | IN_MOVED_TO)) != 0;
424 bool removed = (event->mask & (IN_DELETE | IN_MOVED_FROM)) != 0;
425
426 if(added)
427 monitor->addPath(path);
428 else if(removed)
429 monitor->removePath(path);
430 }
431
432 // Actually trigger the events
433
434 // File/folder was added
435 if(((event->mask & (IN_CREATE | IN_MOVED_TO)) != 0))
436 {
437 if (isDirectory)
438 {
439 if (monitor->filter.isSet(FolderChangeBit::DirName))
440 m->fileActions.push_back(FileAction::createAdded(path.toString()));
441 }
442 else
443 {
444 if (monitor->filter.isSet(FolderChangeBit::FileName))
445 m->fileActions.push_back(FileAction::createAdded(path.toString()));
446 }
447 }
448
449 // File/folder was removed
450 if(((event->mask & (IN_DELETE | IN_MOVED_FROM)) != 0))
451 {
452 if(isDirectory)
453 {
454 if(monitor->filter.isSet(FolderChangeBit::DirName))
455 m->fileActions.push_back(FileAction::createRemoved(path.toString()));
456 }
457 else
458 {
459 if(monitor->filter.isSet(FolderChangeBit::FileName))
460 m->fileActions.push_back(FileAction::createRemoved(path.toString()));
461 }
462 }
463
464 // File was modified
465 if(((event->mask & IN_CLOSE_WRITE) != 0) && monitor->filter.isSet(FolderChangeBit::FileWrite))
466 {
467 m->fileActions.push_back(FileAction::createModified(path.toString()));
468 }
469
470 // Note: Not reporting renames, instead a remove + add event is created. To support renames I'd need
471 // to defer all event triggering until I have processed move event pairs and determined if the
472 // move is a rename (i.e. parent folder didn't change). All events need to be deferred (not just
473 // move events) in order to preserve the event ordering. For now this is too much hassle considering
474 // no external code relies on the rename functionality.
475 }
476 }
477
478 next:
479 readPos += sizeof(inotify_event) + event->len;
480 }
481 }
482 }
483
484 void FolderMonitor::handleNotifications(FileNotifyInfo& notifyInfo, FolderWatchInfo& watchInfo)
485 {
486 // Do nothing
487 }
488
489 void FolderMonitor::_update()
490 {
491 {
492 Lock lock(m->mainMutex);
493
494 std::swap(m->fileActions, m->activeFileActions);
495 }
496
497 for(auto& action : m->activeFileActions)
498 {
499 switch (action->type)
500 {
501 case FileActionType::Added:
502 if (!onAdded.empty())
503 onAdded(Path(action->newName));
504 break;
505 case FileActionType::Removed:
506 if (!onRemoved.empty())
507 onRemoved(Path(action->newName));
508 break;
509 case FileActionType::Modified:
510 if (!onModified.empty())
511 onModified(Path(action->newName));
512 break;
513 case FileActionType::Renamed:
514 if (!onRenamed.empty())
515 onRenamed(Path(action->oldName), Path(action->newName));
516 break;
517 }
518
519 FileAction::destroy(action);
520 }
521
522 m->activeFileActions.clear();
523 }
524}
525