1 module eventcore.drivers.posix.watchers; 2 @safe: 3 4 import eventcore.driver; 5 import eventcore.drivers.posix.driver; 6 import eventcore.internal.utils : mallocT, freeT, nogc_assert; 7 8 9 final class InotifyEventDriverWatchers(Events : EventDriverEvents) : EventDriverWatchers 10 { 11 import core.stdc.errno : errno, EAGAIN, EINPROGRESS; 12 import core.sys.posix.fcntl, core.sys.posix.unistd, core.sys.linux.sys.inotify; 13 import std.algorithm.comparison : among; 14 import std.file; 15 16 private { 17 alias Loop = typeof(Events.init.loop); 18 Loop m_loop; 19 20 struct WatchState { 21 string[int] watcherPaths; 22 string basePath; 23 bool recursive; 24 } 25 26 WatchState[WatcherID] m_watches; // TODO: use a @nogc (allocator based) map 27 } 28 29 this(Events events) { m_loop = events.loop; } 30 31 final override WatcherID watchDirectory(string path, bool recursive, FileChangesCallback callback) 32 { 33 import std.path : buildPath, pathSplitter; 34 import std.range : drop; 35 import std.range.primitives : walkLength; 36 37 enum IN_NONBLOCK = 0x800; // value in core.sys.linux.sys.inotify is incorrect 38 auto handle = () @trusted { return inotify_init1(IN_NONBLOCK | IN_CLOEXEC); } (); 39 if (handle == -1) return WatcherID.invalid; 40 41 auto ret = m_loop.initFD!WatcherID(handle, FDFlags.none, WatcherSlot(callback)); 42 m_loop.registerFD(cast(FD)ret, EventMask.read); 43 m_loop.setNotifyCallback!(EventType.read)(cast(FD)ret, &onChanges); 44 45 m_watches[ret] = WatchState(null, path, recursive); 46 47 addWatch(ret, path, ""); 48 if (recursive) 49 addSubWatches(ret, path, ""); 50 51 processEvents(ret); 52 53 return ret; 54 } 55 56 final override bool isValid(WatcherID handle) 57 const { 58 if (handle.value >= m_loop.m_fds.length) return false; 59 return m_loop.m_fds[handle.value].common.validationCounter == handle.validationCounter; 60 } 61 62 final override void addRef(WatcherID descriptor) 63 { 64 if (!isValid(descriptor)) return; 65 assert(m_loop.m_fds[descriptor].common.refCount > 0, "Adding reference to unreferenced event FD."); 66 m_loop.m_fds[descriptor].common.refCount++; 67 } 68 69 final override bool releaseRef(WatcherID descriptor) 70 { 71 if (!isValid(descriptor)) return true; 72 73 FD fd = cast(FD)descriptor; 74 auto slot = () @trusted { return &m_loop.m_fds[fd]; } (); 75 nogc_assert(slot.common.refCount > 0, "Releasing reference to unreferenced event FD."); 76 if (--slot.common.refCount == 1) { // NOTE: 1 because setNotifyCallback increments the reference count 77 m_loop.setNotifyCallback!(EventType.read)(fd, null); 78 m_loop.unregisterFD(fd, EventMask.read); 79 m_loop.clearFD!WatcherSlot(fd); 80 m_watches.remove(descriptor); 81 /*errnoEnforce(*/close(cast(int)fd)/* == 0)*/; 82 return false; 83 } 84 85 return true; 86 } 87 88 final protected override void* rawUserData(WatcherID descriptor, size_t size, DataInitializer initialize, DataInitializer destroy) 89 @system { 90 if (!isValid(descriptor)) return null; 91 return m_loop.rawUserDataImpl(descriptor, size, initialize, destroy); 92 } 93 94 private void onChanges(FD fd) 95 { 96 processEvents(cast(WatcherID)fd); 97 } 98 99 private void processEvents(WatcherID id) 100 { 101 import std.path : buildPath, dirName; 102 import core.stdc.stdio : FILENAME_MAX; 103 import core.stdc.string : strlen; 104 105 ubyte[inotify_event.sizeof + FILENAME_MAX + 1] buf = void; 106 while (true) { 107 auto ret = () @trusted { return read(cast(int)id, &buf[0], buf.length); } (); 108 109 if (ret == -1 && errno.among!(EAGAIN, EINPROGRESS)) 110 break; 111 assert(ret <= buf.length); 112 113 auto w = m_watches[id]; 114 115 auto rem = buf[0 .. ret]; 116 while (rem.length > 0) { 117 auto ev = () @trusted { return cast(inotify_event*)rem.ptr; } (); 118 rem = rem[inotify_event.sizeof + ev.len .. $]; 119 120 // is the watch already deleted? 121 if (ev.mask & IN_IGNORED) continue; 122 123 FileChange ch; 124 if (ev.mask & (IN_CREATE|IN_MOVED_TO)) 125 ch.kind = FileChangeKind.added; 126 else if (ev.mask & (IN_DELETE|IN_DELETE_SELF|IN_MOVE_SELF|IN_MOVED_FROM)) 127 ch.kind = FileChangeKind.removed; 128 else if (ev.mask & IN_MODIFY) 129 ch.kind = FileChangeKind.modified; 130 131 if (ev.mask & IN_DELETE_SELF) { 132 () @trusted { inotify_rm_watch(cast(int)id, ev.wd); } (); 133 w.watcherPaths.remove(ev.wd); 134 continue; 135 } else if (ev.mask & IN_MOVE_SELF) { 136 // NOTE: the should have been updated by a previous IN_MOVED_TO 137 continue; 138 } 139 140 auto name = () @trusted { return ev.name.ptr[0 .. strlen(ev.name.ptr)]; } (); 141 142 auto subdir = w.watcherPaths[ev.wd]; 143 144 // IN_MODIFY for directories reports the added/removed file instead of the directory itself 145 if (ev.mask == (IN_MODIFY|IN_ISDIR)) 146 name = null; 147 148 if (w.recursive && ev.mask & (IN_CREATE|IN_MOVED_TO) && ev.mask & IN_ISDIR) { 149 auto subpath = subdir == "" ? name.idup : buildPath(subdir, name); 150 addWatch(id, w.basePath, subpath); 151 if (w.recursive) 152 addSubWatches(id, w.basePath, subpath); 153 } 154 155 ch.baseDirectory = m_watches[id].basePath; 156 ch.directory = subdir; 157 ch.name = name; 158 addRef(id); // assure that the id doesn't get invalidated until after the callback 159 auto cb = m_loop.m_fds[id].watcher.callback; 160 cb(id, ch); 161 if (!releaseRef(id)) return; 162 } 163 } 164 } 165 166 private bool addSubWatches(WatcherID handle, string base_path, string subpath) 167 { 168 import std.path : buildPath, pathSplitter; 169 import std.range : drop; 170 import std.range.primitives : walkLength; 171 172 try { 173 auto path = buildPath(base_path, subpath); 174 auto base_segements = base_path.pathSplitter.walkLength; 175 if (path.isDir) () @trusted { 176 foreach (de; path.dirEntries(SpanMode.depth)) 177 if (de.isDir) { 178 auto subdir = de.name.pathSplitter.drop(base_segements).buildPath; 179 addWatch(handle, base_path, subdir); 180 } 181 } (); 182 return true; 183 } catch (Exception e) { 184 // TODO: decide if this should be ignored or if the error should be forwarded 185 return false; 186 } 187 } 188 189 private bool addWatch(WatcherID handle, string base_path, string path) 190 { 191 import std.path : buildPath; 192 import std.string : toStringz; 193 194 enum EVENTS = IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | 195 IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO; 196 immutable wd = () @trusted { return inotify_add_watch(cast(int)handle, buildPath(base_path, path).toStringz, EVENTS); } (); 197 if (wd == -1) return false; 198 m_watches[handle].watcherPaths[wd] = path; 199 return true; 200 } 201 } 202 203 version (darwin) 204 final class FSEventsEventDriverWatchers(Events : EventDriverEvents) : EventDriverWatchers { 205 @safe: /*@nogc:*/ nothrow: 206 import eventcore.internal.corefoundation; 207 import eventcore.internal.coreservices; 208 import std.string : toStringz; 209 210 private { 211 static struct WatcherSlot { 212 FSEventStreamRef stream; 213 string path; 214 string fullPath; 215 FileChangesCallback callback; 216 WatcherID id; 217 int refCount = 1; 218 bool recursive; 219 FSEventStreamEventId lastEvent; 220 ubyte[16 * size_t.sizeof] userData; 221 DataInitializer userDataDestructor; 222 } 223 //HashMap!(void*, WatcherSlot) m_watches; 224 WatcherSlot[WatcherID] m_watches; 225 WatcherID[void*] m_streamMap; 226 Events m_events; 227 size_t m_handleCounter = 1; 228 uint m_validationCounter; 229 } 230 231 this(Events events) { m_events = events; } 232 233 final override WatcherID watchDirectory(string path, bool recursive, FileChangesCallback on_change) 234 @trusted { 235 import std.file : isSymlink, readLink; 236 import std.path : absolutePath, buildPath, buildNormalizedPath, dirName, pathSplitter; 237 238 FSEventStreamContext ctx; 239 ctx.info = () @trusted { return cast(void*)this; } (); 240 241 static string resolveSymlinks(string path) 242 { 243 string res; 244 foreach (ps; path.pathSplitter) { 245 if (!res.length) res = ps; 246 else res = buildPath(res, ps); 247 if (isSymlink(res)) { 248 res = readLink(res).absolutePath(dirName(res)); 249 } 250 } 251 return res.buildNormalizedPath; 252 } 253 254 string abspath; 255 try abspath = resolveSymlinks(absolutePath(path)); 256 catch (Exception e) { 257 return WatcherID.invalid; 258 } 259 260 if (m_handleCounter == 0) { 261 m_handleCounter++; 262 m_validationCounter++; 263 } 264 auto id = WatcherID(cast(size_t)m_handleCounter++, m_validationCounter); 265 266 WatcherSlot slot = { 267 path: path, 268 fullPath: abspath, 269 callback: on_change, 270 id: id, 271 recursive: recursive, 272 lastEvent: kFSEventStreamEventIdSinceNow 273 }; 274 275 startStream(slot, kFSEventStreamEventIdSinceNow); 276 277 m_events.loop.m_waiterCount++; 278 m_watches[id] = slot; 279 return id; 280 } 281 282 final override bool isValid(WatcherID handle) 283 const { 284 return !!(handle in m_watches); 285 } 286 287 final override void addRef(WatcherID descriptor) 288 { 289 if (!isValid(descriptor)) return; 290 291 auto slot = descriptor in m_watches; 292 slot.refCount++; 293 } 294 295 final override bool releaseRef(WatcherID descriptor) 296 { 297 if (!isValid(descriptor)) return true; 298 299 auto slot = descriptor in m_watches; 300 if (!--slot.refCount) { 301 destroyStream(slot.stream); 302 m_watches.remove(descriptor); 303 m_events.loop.m_waiterCount--; 304 return false; 305 } 306 307 return true; 308 } 309 310 final protected override void* rawUserData(WatcherID descriptor, size_t size, DataInitializer initialize, DataInitializer destroy) 311 @system { 312 if (!isValid(descriptor)) return null; 313 314 auto slot = descriptor in m_watches; 315 316 if (size > WatcherSlot.userData.length) assert(false); 317 if (!slot.userDataDestructor) { 318 initialize(slot.userData.ptr); 319 slot.userDataDestructor = destroy; 320 } 321 return slot.userData.ptr; 322 } 323 324 private static extern(C) void onFSEvent(ConstFSEventStreamRef streamRef, 325 void* clientCallBackInfo, size_t numEvents, void* eventPaths, 326 const(FSEventStreamEventFlags)* eventFlags, 327 const(FSEventStreamEventId)* eventIds) 328 { 329 import std.conv : to; 330 import std.path : asRelativePath, baseName, dirName; 331 332 if (!numEvents) return; 333 334 auto this_ = () @trusted { return cast(FSEventsEventDriverWatchers)clientCallBackInfo; } (); 335 auto h = () @trusted { return cast(void*)streamRef; } (); 336 auto ps = h in this_.m_streamMap; 337 if (!ps) return; 338 auto id = *ps; 339 auto slot = id in this_.m_watches; 340 341 auto patharr = () @trusted { return (cast(const(char)**)eventPaths)[0 .. numEvents]; } (); 342 auto flagsarr = () @trusted { return eventFlags[0 .. numEvents]; } (); 343 auto idarr = () @trusted { return eventIds[0 .. numEvents]; } (); 344 345 if (flagsarr[0] & kFSEventStreamEventFlagHistoryDone) { 346 if (!--numEvents) return; 347 patharr = patharr[1 .. $]; 348 flagsarr = flagsarr[1 .. $]; 349 idarr = idarr[1 .. $]; 350 } 351 352 // A new stream needs to be created after every change, because events 353 // get coalesced per file (event flags get or'ed together) and it becomes 354 // impossible to determine the actual event 355 this_.startStream(*slot, idarr[$-1]); 356 357 foreach (i; 0 .. numEvents) { 358 auto pathstr = () @trusted { return to!string(patharr[i]); } (); 359 auto f = flagsarr[i]; 360 361 string rp; 362 try rp = pathstr.asRelativePath(slot.fullPath).to!string; 363 catch (Exception e) assert(false, e.msg); 364 365 if (rp == "." || rp == "") continue; 366 367 FileChange ch; 368 ch.baseDirectory = slot.path; 369 ch.directory = dirName(rp); 370 ch.name = baseName(rp); 371 372 if (ch.directory == ".") ch.directory = ""; 373 374 if (!slot.recursive && ch.directory != "") continue; 375 376 void emit(FileChangeKind k) 377 { 378 ch.kind = k; 379 slot.callback(id, ch); 380 } 381 382 import std.file : exists; 383 bool does_exist = exists(pathstr); 384 385 // The order of tests is important to properly lower the more 386 // complex flags system to the three event types provided by 387 // eventcore 388 if (f & kFSEventStreamEventFlagItemRenamed) { 389 if (!does_exist) emit(FileChangeKind.removed); 390 else emit(FileChangeKind.added); 391 } else if (f & kFSEventStreamEventFlagItemRemoved && !does_exist) { 392 emit(FileChangeKind.removed); 393 } else if (f & kFSEventStreamEventFlagItemModified && does_exist) { 394 emit(FileChangeKind.modified); 395 } else if (f & kFSEventStreamEventFlagItemCreated && does_exist) { 396 emit(FileChangeKind.added); 397 } 398 } 399 } 400 401 private void startStream(ref WatcherSlot slot, FSEventStreamEventId since_when) 402 @trusted { 403 if (slot.stream) { 404 destroyStream(slot.stream); 405 slot.stream = null; 406 } 407 408 FSEventStreamContext ctx; 409 ctx.info = () @trusted { return cast(void*)this; } (); 410 411 auto pstr = CFStringCreateWithBytes(null, 412 cast(const(ubyte)*)slot.path.ptr, slot.path.length, 413 kCFStringEncodingUTF8, false); 414 scope (exit) CFRelease(pstr); 415 auto paths = CFArrayCreate(null, cast(const(void)**)&pstr, 1, null); 416 scope (exit) CFRelease(paths); 417 418 slot.stream = FSEventStreamCreate(null, &onFSEvent, () @trusted { return &ctx; } (), 419 paths, since_when, 0.1, kFSEventStreamCreateFlagFileEvents|kFSEventStreamCreateFlagNoDefer); 420 FSEventStreamScheduleWithRunLoop(slot.stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); 421 FSEventStreamStart(slot.stream); 422 423 m_streamMap[cast(void*)slot.stream] = slot.id; 424 } 425 426 private void destroyStream(FSEventStreamRef stream) 427 @trusted { 428 FSEventStreamStop(stream); 429 FSEventStreamInvalidate(stream); 430 FSEventStreamRelease(stream); 431 m_streamMap.remove(cast(void*)stream); 432 } 433 } 434 435 436 /** Generic directory watcher implementation based on periodic directory 437 scanning. 438 439 Note that this implementation, although it works on all operating systems, 440 is not efficient for directories with many files, since it has to keep a 441 representation of the whole directory in memory and needs to list all files 442 for each polling period, which can result in excessive hard disk activity. 443 */ 444 final class PollEventDriverWatchers(Events : EventDriverEvents) : EventDriverWatchers { 445 @safe: /*@nogc:*/ nothrow: 446 import core.thread : Thread; 447 import core.sync.mutex : Mutex; 448 449 private { 450 Events m_events; 451 PollingThread[EventID] m_pollers; 452 } 453 454 this(Events events) 455 @nogc { 456 m_events = events; 457 } 458 459 void dispose() 460 @trusted { 461 foreach (pt; m_pollers.byValue) { 462 pt.dispose(); 463 try pt.join(); 464 catch (Exception e) { 465 // not bringing down the application here, because not being 466 // able to join the thread here likely isn't a problem 467 } 468 } 469 } 470 471 final override WatcherID watchDirectory(string path, bool recursive, FileChangesCallback on_change) 472 { 473 import std.file : exists, isDir; 474 475 // validate base directory 476 try if (!isDir(path)) return WatcherID.invalid; 477 catch (Exception e) return WatcherID.invalid; 478 479 // create event to wait on for new changes 480 auto evt = m_events.create(); 481 assert(evt !is EventID.invalid, "Failed to create event."); 482 auto pt = new PollingThread(() @trusted { return cast(shared)m_events; } (), evt, path, recursive, on_change); 483 m_pollers[evt] = pt; 484 try () @trusted { pt.isDaemon = true; } (); 485 catch (Exception e) assert(false, e.msg); 486 () @trusted { pt.start(); } (); 487 488 m_events.wait(evt, &onEvent); 489 490 return cast(WatcherID)evt; 491 } 492 493 final override bool isValid(WatcherID handle) 494 const { 495 return m_events.isValid(cast(EventID)handle); 496 } 497 498 final override void addRef(WatcherID descriptor) 499 { 500 if (!isValid(descriptor)) return; 501 502 auto evt = cast(EventID)descriptor; 503 auto pt = evt in m_pollers; 504 assert(pt !is null); 505 m_events.addRef(evt); 506 } 507 508 final override bool releaseRef(WatcherID descriptor) 509 { 510 if (!isValid(descriptor)) return true; 511 512 auto evt = cast(EventID)descriptor; 513 auto pt = evt in m_pollers; 514 nogc_assert(pt !is null, "Directory watcher polling thread does not exist"); 515 if (!m_events.releaseRef(evt)) { 516 pt.dispose(); 517 m_pollers.remove(evt); 518 return false; 519 } 520 return true; 521 } 522 523 final protected override void* rawUserData(WatcherID descriptor, size_t size, DataInitializer initialize, DataInitializer destroy) 524 @system { 525 return m_events.loop.rawUserDataImpl(cast(EventID)descriptor, size, initialize, destroy); 526 } 527 528 private void onEvent(EventID evt) 529 { 530 auto pt = evt in m_pollers; 531 if (!pt) return; 532 533 m_events.wait(evt, &onEvent); 534 535 foreach (ref ch; pt.readChanges()) 536 pt.m_callback(cast(WatcherID)evt, ch); 537 } 538 539 540 private final class PollingThread : Thread { 541 private { 542 shared(Events) m_eventsDriver; 543 Mutex m_changesMutex; 544 /*shared*/ FileChange[] m_changes; // protected by m_changesMutex 545 EventID m_changesEvent; // protected by m_changesMutex 546 immutable string m_basePath; 547 immutable bool m_recursive; 548 immutable FileChangesCallback m_callback; 549 } 550 551 this(shared(Events) event_driver, EventID event, string path, bool recursive, FileChangesCallback callback) 552 @trusted nothrow { 553 import core.time : seconds; 554 555 m_changesMutex = new Mutex; 556 m_eventsDriver = event_driver; 557 m_changesEvent = event; 558 m_basePath = path; 559 m_recursive = recursive; 560 m_callback = callback; 561 562 try super(&run); 563 catch (Exception e) assert(false, e.msg); 564 } 565 566 void dispose() 567 nothrow { 568 try synchronized (m_changesMutex) { 569 m_changesEvent = EventID.invalid; 570 } catch (Exception e) assert(false, e.msg); 571 } 572 573 FileChange[] readChanges() 574 nothrow { 575 import std.algorithm.mutation : swap; 576 577 FileChange[] changes; 578 try synchronized (m_changesMutex) 579 swap(changes, m_changes); 580 catch (Exception e) assert(false, "Failed to acquire mutex: "~e.msg); 581 return changes; 582 } 583 584 private void run() 585 nothrow @trusted { 586 import core.time : MonoTime, msecs; 587 import std.algorithm.comparison : min; 588 589 auto poller = new DirectoryPoller(m_basePath, m_recursive, (ch) { 590 try synchronized (m_changesMutex) { 591 m_changes ~= ch; 592 } catch (Exception e) assert(false, "Mutex lock failed: "~e.msg); 593 }); 594 595 poller.scan(false); 596 597 try while (true) { 598 auto timeout = MonoTime.currTime() + min(poller.entryCount, 60000).msecs + 1000.msecs; 599 while (true) { 600 try synchronized (m_changesMutex) { 601 if (m_changesEvent == EventID.invalid) return; 602 } catch (Exception e) assert(false, "Mutex lock failed: "~e.msg); 603 auto remaining = timeout - MonoTime.currTime(); 604 if (remaining <= 0.msecs) break; 605 sleep(min(1000.msecs, remaining)); 606 } 607 608 poller.scan(true); 609 610 try synchronized (m_changesMutex) { 611 if (m_changesEvent == EventID.invalid) return; 612 if (m_changes.length) 613 m_eventsDriver.trigger(m_changesEvent, false); 614 } catch (Exception e) assert(false, "Mutex lock failed: "~e.msg); 615 } catch (Throwable th) { 616 import core.stdc.stdio : fprintf, stderr; 617 import core.stdc.stdlib : abort; 618 619 fprintf(stderr, "Fatal error: %.*s\n", 620 cast(int) th.msg.length, th.msg.ptr); 621 abort(); 622 } 623 } 624 625 } 626 627 private final class DirectoryPoller { 628 private final static class Entry { 629 Entry parent; 630 string name; 631 ulong size; 632 long lastChange; 633 634 this(Entry parent, string name, ulong size, long lastChange) 635 { 636 this.parent = parent; 637 this.name = name; 638 this.size = size; 639 this.lastChange = lastChange; 640 } 641 642 string path() 643 { 644 import std.path : buildPath; 645 if (parent) 646 return buildPath(parent.path, name); 647 else return name; 648 } 649 650 bool isDir() const { return size == ulong.max; } 651 } 652 653 private struct Key { 654 Entry parent; 655 string name; 656 } 657 658 alias ChangeCallback = void delegate(FileChange) @safe nothrow; 659 660 private { 661 immutable string m_basePath; 662 immutable bool m_recursive; 663 664 Entry[Key] m_entries; 665 size_t m_entryCount; 666 ChangeCallback m_onChange; 667 } 668 669 this(string path, bool recursive, ChangeCallback on_change) 670 { 671 m_basePath = path; 672 m_recursive = recursive; 673 m_onChange = on_change; 674 } 675 676 @property size_t entryCount() const { return m_entryCount; } 677 678 private void addChange(FileChangeKind kind, Key key) 679 { 680 m_onChange(FileChange(kind, m_basePath, key.parent ? key.parent.path : "", key.name)); 681 } 682 683 private void scan(bool generate_changes) 684 @trusted nothrow { 685 import std.algorithm.mutation : swap; 686 687 Entry[Key] new_entries; 688 Entry[] added; 689 size_t ec = 0; 690 691 scan(null, generate_changes, new_entries, added, ec); 692 693 // detect all roots of removed sub trees 694 foreach (e; m_entries.byKeyValue) { 695 if (!e.key.parent || Key(e.key.parent.parent, e.key.parent.name) !in m_entries) { 696 if (generate_changes) 697 addChange(FileChangeKind.removed, e.key); 698 } 699 } 700 701 foreach (e; added) 702 addChange(FileChangeKind.added, Key(e.parent, e.name)); 703 704 swap(m_entries, new_entries); 705 m_entryCount = ec; 706 707 // clear all left-over entries (delted directly or indirectly) 708 foreach (e; new_entries.byValue) { 709 try freeT(e); 710 catch (Exception e) assert(false, e.msg); 711 } 712 } 713 714 private void scan(Entry parent, bool generate_changes, ref Entry[Key] new_entries, ref Entry[] added, ref size_t ec) 715 @trusted nothrow { 716 import std.file : SpanMode, dirEntries; 717 import std.path : buildPath, baseName; 718 719 auto ppath = parent ? buildPath(m_basePath, parent.path) : m_basePath; 720 try foreach (de; dirEntries(ppath, SpanMode.shallow)) { 721 auto key = Key(parent, de.name.baseName); 722 auto modified_time = de.timeLastModified.stdTime; 723 if (auto pe = key in m_entries) { 724 if ((*pe).isDir) { 725 if (m_recursive) 726 scan(*pe, generate_changes, new_entries, added, ec); 727 } else { 728 if ((*pe).size != de.size || (*pe).lastChange != modified_time) { 729 if (generate_changes) 730 addChange(FileChangeKind.modified, key); 731 (*pe).size = de.size; 732 (*pe).lastChange = modified_time; 733 } 734 } 735 736 new_entries[key] = *pe; 737 ec++; 738 m_entries.remove(key); 739 } else { 740 auto e = mallocT!Entry(parent, key.name, de.isDir ? ulong.max : de.size, modified_time); 741 new_entries[key] = e; 742 ec++; 743 if (generate_changes) added ~= e; 744 745 if (de.isDir && m_recursive) scan(e, false, new_entries, added, ec); 746 } 747 } catch (Exception e) {} // will result in all children being flagged as removed 748 } 749 } 750 } 751 752 package struct WatcherSlot { 753 alias Handle = WatcherID; 754 FileChangesCallback callback; 755 }