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 | |
8 | namespace 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 | |