1 | // |
2 | // DirectoryWatcher.cpp |
3 | // |
4 | // Library: Foundation |
5 | // Package: Filesystem |
6 | // Module: DirectoryWatcher |
7 | // |
8 | // Copyright (c) 2012, Applied Informatics Software Engineering GmbH. |
9 | // and Contributors. |
10 | // |
11 | // SPDX-License-Identifier: BSL-1.0 |
12 | // |
13 | |
14 | |
15 | #include "Poco/DirectoryWatcher.h" |
16 | #ifdef POCO_OS_FAMILY_WINDOWS |
17 | #include "Poco/UnWindows.h" |
18 | #endif |
19 | |
20 | |
21 | #ifndef POCO_NO_INOTIFY |
22 | |
23 | |
24 | #include "Poco/Path.h" |
25 | #include "Poco/Glob.h" |
26 | #include "Poco/DirectoryIterator.h" |
27 | #include "Poco/Event.h" |
28 | #include "Poco/Exception.h" |
29 | #include "Poco/Buffer.h" |
30 | #if POCO_OS == POCO_OS_LINUX || POCO_OS == POCO_OS_ANDROID |
31 | #include <sys/inotify.h> |
32 | #include <sys/select.h> |
33 | #include <unistd.h> |
34 | #elif POCO_OS == POCO_OS_MAC_OS_X || POCO_OS == POCO_OS_FREE_BSD |
35 | #include <fcntl.h> |
36 | #include <sys/types.h> |
37 | #include <sys/event.h> |
38 | #include <sys/time.h> |
39 | #include <unistd.h> |
40 | #if (POCO_OS == POCO_OS_FREE_BSD) && !defined(O_EVTONLY) |
41 | #define O_EVTONLY 0x8000 |
42 | #endif |
43 | #endif |
44 | #include <algorithm> |
45 | #include <map> |
46 | |
47 | |
48 | namespace Poco { |
49 | |
50 | |
51 | class DirectoryWatcherStrategy |
52 | { |
53 | public: |
54 | DirectoryWatcherStrategy(DirectoryWatcher& ownerWatcher): |
55 | _owner(ownerWatcher) |
56 | { |
57 | } |
58 | |
59 | virtual ~DirectoryWatcherStrategy() |
60 | { |
61 | } |
62 | |
63 | DirectoryWatcher& owner() |
64 | { |
65 | return _owner; |
66 | } |
67 | |
68 | virtual void run() = 0; |
69 | virtual void stop() = 0; |
70 | virtual bool supportsMoveEvents() const = 0; |
71 | |
72 | protected: |
73 | struct ItemInfo |
74 | { |
75 | ItemInfo(): |
76 | size(0) |
77 | { |
78 | } |
79 | |
80 | ItemInfo(const ItemInfo& other): |
81 | path(other.path), |
82 | size(other.size), |
83 | lastModified(other.lastModified) |
84 | { |
85 | } |
86 | |
87 | explicit ItemInfo(const File& f): |
88 | path(f.path()), |
89 | size(f.isFile() ? f.getSize() : 0), |
90 | lastModified(f.getLastModified()) |
91 | { |
92 | } |
93 | |
94 | std::string path; |
95 | File::FileSize size; |
96 | Timestamp lastModified; |
97 | }; |
98 | typedef std::map<std::string, ItemInfo> ItemInfoMap; |
99 | |
100 | void scan(ItemInfoMap& entries) |
101 | { |
102 | DirectoryIterator it(owner().directory()); |
103 | DirectoryIterator end; |
104 | while (it != end) |
105 | { |
106 | // DirectoryWatcher should not stop watching if it fails to get the info |
107 | // of a file, it should just ignore it. |
108 | try |
109 | { |
110 | entries[it.path().getFileName()] = ItemInfo(*it); |
111 | } |
112 | catch (Poco::FileNotFoundException&) |
113 | { |
114 | // The file is missing, remove it from the entries so it is treated as |
115 | // deleted. |
116 | entries.erase(it.path().getFileName()); |
117 | } |
118 | ++it; |
119 | } |
120 | } |
121 | |
122 | void compare(ItemInfoMap& oldEntries, ItemInfoMap& newEntries) |
123 | { |
124 | for (ItemInfoMap::iterator itn = newEntries.begin(); itn != newEntries.end(); ++itn) |
125 | { |
126 | ItemInfoMap::iterator ito = oldEntries.find(itn->first); |
127 | if (ito != oldEntries.end()) |
128 | { |
129 | if ((owner().eventMask() & DirectoryWatcher::DW_ITEM_MODIFIED) && !owner().eventsSuspended()) |
130 | { |
131 | if (itn->second.size != ito->second.size || itn->second.lastModified != ito->second.lastModified) |
132 | { |
133 | Poco::File f(itn->second.path); |
134 | DirectoryWatcher::DirectoryEvent ev(f, DirectoryWatcher::DW_ITEM_MODIFIED); |
135 | owner().itemModified(&owner(), ev); |
136 | } |
137 | } |
138 | oldEntries.erase(ito); |
139 | } |
140 | else if ((owner().eventMask() & DirectoryWatcher::DW_ITEM_ADDED) && !owner().eventsSuspended()) |
141 | { |
142 | Poco::File f(itn->second.path); |
143 | DirectoryWatcher::DirectoryEvent ev(f, DirectoryWatcher::DW_ITEM_ADDED); |
144 | owner().itemAdded(&owner(), ev); |
145 | } |
146 | } |
147 | if ((owner().eventMask() & DirectoryWatcher::DW_ITEM_REMOVED) && !owner().eventsSuspended()) |
148 | { |
149 | for (ItemInfoMap::iterator it = oldEntries.begin(); it != oldEntries.end(); ++it) |
150 | { |
151 | Poco::File f(it->second.path); |
152 | DirectoryWatcher::DirectoryEvent ev(f, DirectoryWatcher::DW_ITEM_REMOVED); |
153 | owner().itemRemoved(&owner(), ev); |
154 | } |
155 | } |
156 | } |
157 | |
158 | private: |
159 | DirectoryWatcherStrategy(); |
160 | DirectoryWatcherStrategy(const DirectoryWatcherStrategy&); |
161 | DirectoryWatcherStrategy& operator = (const DirectoryWatcherStrategy&); |
162 | |
163 | DirectoryWatcher& _owner; |
164 | }; |
165 | |
166 | |
167 | #if POCO_OS == POCO_OS_WINDOWS_NT |
168 | |
169 | |
170 | class WindowsDirectoryWatcherStrategy: public DirectoryWatcherStrategy |
171 | { |
172 | public: |
173 | WindowsDirectoryWatcherStrategy(DirectoryWatcher& owner): |
174 | DirectoryWatcherStrategy(owner) |
175 | { |
176 | _hStopped = CreateEventW(NULL, FALSE, FALSE, NULL); |
177 | if (!_hStopped) |
178 | throw SystemException("cannot create event" ); |
179 | } |
180 | |
181 | ~WindowsDirectoryWatcherStrategy() |
182 | { |
183 | CloseHandle(_hStopped); |
184 | } |
185 | |
186 | void run() |
187 | { |
188 | ItemInfoMap entries; |
189 | scan(entries); |
190 | |
191 | DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME; |
192 | if (owner().eventMask() & DirectoryWatcher::DW_ITEM_MODIFIED) |
193 | filter |= FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE; |
194 | |
195 | std::string path(owner().directory().path()); |
196 | std::wstring upath; |
197 | FileImpl::convertPath(path.c_str(), upath); |
198 | HANDLE hChange = FindFirstChangeNotificationW(upath.c_str(), FALSE, filter); |
199 | |
200 | if (hChange == INVALID_HANDLE_VALUE) |
201 | { |
202 | try |
203 | { |
204 | FileImpl::handleLastErrorImpl(path); |
205 | } |
206 | catch (Poco::Exception& exc) |
207 | { |
208 | owner().scanError(&owner(), exc); |
209 | } |
210 | return; |
211 | } |
212 | |
213 | bool stopped = false; |
214 | while (!stopped) |
215 | { |
216 | try |
217 | { |
218 | HANDLE h[2]; |
219 | h[0] = _hStopped; |
220 | h[1] = hChange; |
221 | switch (WaitForMultipleObjects(2, h, FALSE, INFINITE)) |
222 | { |
223 | case WAIT_OBJECT_0: |
224 | stopped = true; |
225 | break; |
226 | case WAIT_OBJECT_0 + 1: |
227 | { |
228 | ItemInfoMap newEntries; |
229 | scan(newEntries); |
230 | compare(entries, newEntries); |
231 | std::swap(entries, newEntries); |
232 | if (FindNextChangeNotification(hChange) == FALSE) |
233 | { |
234 | FileImpl::handleLastErrorImpl(path); |
235 | } |
236 | } |
237 | break; |
238 | default: |
239 | throw SystemException("failed to wait for directory changes" ); |
240 | } |
241 | } |
242 | catch (Poco::Exception& exc) |
243 | { |
244 | owner().scanError(&owner(), exc); |
245 | } |
246 | } |
247 | FindCloseChangeNotification(hChange); |
248 | } |
249 | |
250 | void stop() |
251 | { |
252 | SetEvent(_hStopped); |
253 | } |
254 | |
255 | bool supportsMoveEvents() const |
256 | { |
257 | return false; |
258 | } |
259 | |
260 | private: |
261 | HANDLE _hStopped; |
262 | }; |
263 | |
264 | |
265 | #elif POCO_OS == POCO_OS_LINUX || POCO_OS == POCO_OS_ANDROID |
266 | |
267 | |
268 | class LinuxDirectoryWatcherStrategy: public DirectoryWatcherStrategy |
269 | { |
270 | public: |
271 | LinuxDirectoryWatcherStrategy(DirectoryWatcher& ownerWatcher): |
272 | DirectoryWatcherStrategy(ownerWatcher), |
273 | _fd(-1), |
274 | _stopped(false) |
275 | { |
276 | _fd = inotify_init(); |
277 | if (_fd == -1) throw Poco::IOException("cannot initialize inotify" , errno); |
278 | } |
279 | |
280 | ~LinuxDirectoryWatcherStrategy() |
281 | { |
282 | close(_fd); |
283 | } |
284 | |
285 | void run() |
286 | { |
287 | int mask = 0; |
288 | if (owner().eventMask() & DirectoryWatcher::DW_ITEM_ADDED) |
289 | mask |= IN_CREATE; |
290 | if (owner().eventMask() & DirectoryWatcher::DW_ITEM_REMOVED) |
291 | mask |= IN_DELETE; |
292 | if (owner().eventMask() & DirectoryWatcher::DW_ITEM_MODIFIED) |
293 | mask |= IN_MODIFY; |
294 | if (owner().eventMask() & DirectoryWatcher::DW_ITEM_MOVED_FROM) |
295 | mask |= IN_MOVED_FROM; |
296 | if (owner().eventMask() & DirectoryWatcher::DW_ITEM_MOVED_TO) |
297 | mask |= IN_MOVED_TO; |
298 | int wd = inotify_add_watch(_fd, owner().directory().path().c_str(), mask); |
299 | if (wd == -1) |
300 | { |
301 | try |
302 | { |
303 | FileImpl::handleLastErrorImpl(owner().directory().path()); |
304 | } |
305 | catch (Poco::Exception& exc) |
306 | { |
307 | owner().scanError(&owner(), exc); |
308 | } |
309 | } |
310 | |
311 | Poco::Buffer<char> buffer(4096); |
312 | while (!_stopped) |
313 | { |
314 | fd_set fds; |
315 | FD_ZERO(&fds); |
316 | FD_SET(_fd, &fds); |
317 | |
318 | struct timeval tv; |
319 | tv.tv_sec = 0; |
320 | tv.tv_usec = 200000; |
321 | |
322 | if (select(_fd + 1, &fds, NULL, NULL, &tv) == 1) |
323 | { |
324 | int n = read(_fd, buffer.begin(), buffer.size()); |
325 | int i = 0; |
326 | if (n > 0) |
327 | { |
328 | while (n > 0) |
329 | { |
330 | struct inotify_event* pEvent = reinterpret_cast<struct inotify_event*>(buffer.begin() + i); |
331 | |
332 | if (pEvent->len > 0) |
333 | { |
334 | if (!owner().eventsSuspended()) |
335 | { |
336 | Poco::Path p(owner().directory().path()); |
337 | p.makeDirectory(); |
338 | p.setFileName(pEvent->name); |
339 | Poco::File f(p.toString()); |
340 | |
341 | if ((pEvent->mask & IN_CREATE) && (owner().eventMask() & DirectoryWatcher::DW_ITEM_ADDED)) |
342 | { |
343 | DirectoryWatcher::DirectoryEvent ev(f, DirectoryWatcher::DW_ITEM_ADDED); |
344 | owner().itemAdded(&owner(), ev); |
345 | } |
346 | if ((pEvent->mask & IN_DELETE) && (owner().eventMask() & DirectoryWatcher::DW_ITEM_REMOVED)) |
347 | { |
348 | DirectoryWatcher::DirectoryEvent ev(f, DirectoryWatcher::DW_ITEM_REMOVED); |
349 | owner().itemRemoved(&owner(), ev); |
350 | } |
351 | if ((pEvent->mask & IN_MODIFY) && (owner().eventMask() & DirectoryWatcher::DW_ITEM_MODIFIED)) |
352 | { |
353 | DirectoryWatcher::DirectoryEvent ev(f, DirectoryWatcher::DW_ITEM_MODIFIED); |
354 | owner().itemModified(&owner(), ev); |
355 | } |
356 | if ((pEvent->mask & IN_MOVED_FROM) && (owner().eventMask() & DirectoryWatcher::DW_ITEM_MOVED_FROM)) |
357 | { |
358 | DirectoryWatcher::DirectoryEvent ev(f, DirectoryWatcher::DW_ITEM_MOVED_FROM); |
359 | owner().itemMovedFrom(&owner(), ev); |
360 | } |
361 | if ((pEvent->mask & IN_MOVED_TO) && (owner().eventMask() & DirectoryWatcher::DW_ITEM_MOVED_TO)) |
362 | { |
363 | DirectoryWatcher::DirectoryEvent ev(f, DirectoryWatcher::DW_ITEM_MOVED_TO); |
364 | owner().itemMovedTo(&owner(), ev); |
365 | } |
366 | } |
367 | } |
368 | |
369 | i += sizeof(inotify_event) + pEvent->len; |
370 | n -= sizeof(inotify_event) + pEvent->len; |
371 | } |
372 | } |
373 | } |
374 | } |
375 | } |
376 | |
377 | void stop() |
378 | { |
379 | _stopped = true; |
380 | } |
381 | |
382 | bool supportsMoveEvents() const |
383 | { |
384 | return true; |
385 | } |
386 | |
387 | private: |
388 | int _fd; |
389 | bool _stopped; |
390 | }; |
391 | |
392 | |
393 | #elif POCO_OS == POCO_OS_MAC_OS_X || POCO_OS == POCO_OS_FREE_BSD |
394 | |
395 | |
396 | class BSDDirectoryWatcherStrategy: public DirectoryWatcherStrategy |
397 | { |
398 | public: |
399 | BSDDirectoryWatcherStrategy(DirectoryWatcher& owner): |
400 | DirectoryWatcherStrategy(owner), |
401 | _queueFD(-1), |
402 | _dirFD(-1), |
403 | _stopped(false) |
404 | { |
405 | _dirFD = open(owner.directory().path().c_str(), O_EVTONLY); |
406 | if (_dirFD < 0) throw Poco::FileNotFoundException(owner.directory().path()); |
407 | _queueFD = kqueue(); |
408 | if (_queueFD < 0) |
409 | { |
410 | close(_dirFD); |
411 | throw Poco::SystemException("Cannot create kqueue" , errno); |
412 | } |
413 | } |
414 | |
415 | ~BSDDirectoryWatcherStrategy() |
416 | { |
417 | close(_dirFD); |
418 | close(_queueFD); |
419 | } |
420 | |
421 | void run() |
422 | { |
423 | Poco::Timestamp lastScan; |
424 | ItemInfoMap entries; |
425 | scan(entries); |
426 | |
427 | while (!_stopped) |
428 | { |
429 | struct timespec timeout; |
430 | timeout.tv_sec = 0; |
431 | timeout.tv_nsec = 200000000; |
432 | unsigned eventFilter = NOTE_WRITE; |
433 | struct kevent event; |
434 | struct kevent eventData; |
435 | EV_SET(&event, _dirFD, EVFILT_VNODE, EV_ADD | EV_CLEAR, eventFilter, 0, 0); |
436 | int nEvents = kevent(_queueFD, &event, 1, &eventData, 1, &timeout); |
437 | if (nEvents < 0 || eventData.flags == EV_ERROR) |
438 | { |
439 | try |
440 | { |
441 | FileImpl::handleLastErrorImpl(owner().directory().path()); |
442 | } |
443 | catch (Poco::Exception& exc) |
444 | { |
445 | owner().scanError(&owner(), exc); |
446 | } |
447 | } |
448 | else if (nEvents > 0 || ((owner().eventMask() & DirectoryWatcher::DW_ITEM_MODIFIED) && lastScan.isElapsed(owner().scanInterval()*1000000))) |
449 | { |
450 | ItemInfoMap newEntries; |
451 | scan(newEntries); |
452 | compare(entries, newEntries); |
453 | std::swap(entries, newEntries); |
454 | lastScan.update(); |
455 | } |
456 | } |
457 | } |
458 | |
459 | void stop() |
460 | { |
461 | _stopped = true; |
462 | } |
463 | |
464 | bool supportsMoveEvents() const |
465 | { |
466 | return false; |
467 | } |
468 | |
469 | private: |
470 | int _queueFD; |
471 | int _dirFD; |
472 | bool _stopped; |
473 | }; |
474 | |
475 | |
476 | #endif |
477 | |
478 | |
479 | class PollingDirectoryWatcherStrategy: public DirectoryWatcherStrategy |
480 | { |
481 | public: |
482 | PollingDirectoryWatcherStrategy(DirectoryWatcher& ownerWatcher): |
483 | DirectoryWatcherStrategy(ownerWatcher) |
484 | { |
485 | } |
486 | |
487 | ~PollingDirectoryWatcherStrategy() |
488 | { |
489 | } |
490 | |
491 | void run() |
492 | { |
493 | ItemInfoMap entries; |
494 | scan(entries); |
495 | while (!_stopped.tryWait(1000*owner().scanInterval())) |
496 | { |
497 | try |
498 | { |
499 | ItemInfoMap newEntries; |
500 | scan(newEntries); |
501 | compare(entries, newEntries); |
502 | std::swap(entries, newEntries); |
503 | } |
504 | catch (Poco::Exception& exc) |
505 | { |
506 | owner().scanError(&owner(), exc); |
507 | } |
508 | } |
509 | } |
510 | |
511 | void stop() |
512 | { |
513 | _stopped.set(); |
514 | } |
515 | |
516 | bool supportsMoveEvents() const |
517 | { |
518 | return false; |
519 | } |
520 | |
521 | private: |
522 | Poco::Event _stopped; |
523 | }; |
524 | |
525 | |
526 | |
527 | DirectoryWatcher::DirectoryWatcher(const std::string& path, int otherEventMask, int otherScanInterval, |
528 | bool forceScan) : |
529 | _directory(path), |
530 | _eventMask(otherEventMask), |
531 | _eventsSuspended(0), |
532 | _scanInterval(otherScanInterval), |
533 | _forceScan(forceScan) |
534 | { |
535 | init(); |
536 | } |
537 | |
538 | |
539 | DirectoryWatcher::DirectoryWatcher(const Poco::File& otherDirectory, int otherEventMask, int otherScanInterval, |
540 | bool forceScan) : |
541 | _directory(otherDirectory), |
542 | _eventMask(otherEventMask), |
543 | _eventsSuspended(0), |
544 | _scanInterval(otherScanInterval), |
545 | _forceScan(forceScan) |
546 | { |
547 | init(); |
548 | } |
549 | |
550 | |
551 | DirectoryWatcher::~DirectoryWatcher() |
552 | { |
553 | try |
554 | { |
555 | stop(); |
556 | delete _pStrategy; |
557 | } |
558 | catch (...) |
559 | { |
560 | poco_unexpected(); |
561 | } |
562 | } |
563 | |
564 | |
565 | void DirectoryWatcher::suspendEvents() |
566 | { |
567 | poco_assert (_eventsSuspended > 0); |
568 | |
569 | _eventsSuspended--; |
570 | } |
571 | |
572 | |
573 | void DirectoryWatcher::resumeEvents() |
574 | { |
575 | _eventsSuspended++; |
576 | } |
577 | |
578 | |
579 | void DirectoryWatcher::init() |
580 | { |
581 | if (!_directory.exists()) |
582 | throw Poco::FileNotFoundException(_directory.path()); |
583 | |
584 | if (!_directory.isDirectory()) |
585 | throw Poco::InvalidArgumentException("not a directory" , _directory.path()); |
586 | |
587 | if (!_forceScan) |
588 | { |
589 | #if POCO_OS == POCO_OS_WINDOWS_NT |
590 | _pStrategy = new WindowsDirectoryWatcherStrategy(*this); |
591 | #elif POCO_OS == POCO_OS_LINUX || POCO_OS == POCO_OS_ANDROID |
592 | _pStrategy = new LinuxDirectoryWatcherStrategy(*this); |
593 | #elif POCO_OS == POCO_OS_MAC_OS_X || POCO_OS == POCO_OS_FREE_BSD |
594 | _pStrategy = new BSDDirectoryWatcherStrategy(*this); |
595 | #else |
596 | _pStrategy = new PollingDirectoryWatcherStrategy(*this); |
597 | #endif |
598 | } |
599 | else |
600 | { |
601 | _pStrategy = new PollingDirectoryWatcherStrategy(*this); |
602 | } |
603 | |
604 | _thread.start(*this); |
605 | } |
606 | |
607 | |
608 | void DirectoryWatcher::run() |
609 | { |
610 | _pStrategy->run(); |
611 | } |
612 | |
613 | |
614 | void DirectoryWatcher::stop() |
615 | { |
616 | _pStrategy->stop(); |
617 | _thread.join(); |
618 | } |
619 | |
620 | |
621 | bool DirectoryWatcher::supportsMoveEvents() const |
622 | { |
623 | return _pStrategy->supportsMoveEvents(); |
624 | } |
625 | |
626 | |
627 | } // namespace Poco |
628 | |
629 | |
630 | #endif // POCO_NO_INOTIFY |
631 | |