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 }