1 ///
2 module fswatch;
3 
4 import std.file;
5 
6 import core.thread;
7 
8 debug (FSWTestRun2) version = FSWForcePoll;
9 
10 ///
11 enum FileChangeEventType : ubyte
12 {
13 	/// Occurs when a file or folder is created.
14 	create,
15 	/// Occurs when a file or folder is modified.
16 	modify,
17 	/// Occurs when a file or folder is removed.
18 	remove,
19 	/// Occurs when a file or folder inside a folder is renamed.
20 	rename,
21 	/// Occurs when the watched path gets created.
22 	createSelf,
23 	/// Occurs when the watched path gets deleted.
24 	removeSelf
25 }
26 
27 /// Structure containing information about filesystem events.
28 struct FileChangeEvent
29 {
30 	/// The type of this event.
31 	FileChangeEventType type;
32 	/// The path of the file of this event. Might not be set for createSelf and removeSelf.
33 	string path;
34 	/// The path the file got renamed to for a rename event.
35 	string newPath = null;
36 }
37 
38 private ulong getUniqueHash(DirEntry entry)
39 {
40 	version (Windows)
41 		return entry.timeCreated.stdTime ^ cast(ulong) entry.attributes;
42 	else version (Posix)
43 		return entry.statBuf.st_ino | (cast(ulong) entry.statBuf.st_dev << 32UL);
44 	else
45 		return (entry.timeLastModified.stdTime ^ (
46 				cast(ulong) entry.attributes << 32UL) ^ entry.linkAttributes) * entry.size;
47 }
48 
49 version (FSWForcePoll)
50 	version = FSWUsesPolling;
51 else
52 {
53 	version (Windows)
54 		version = FSWUsesWin32;
55 	else version (linux)
56 		version = FSWUsesINotify;
57 	else version = FSWUsesPolling;
58 }
59 
60 /// An instance of a FileWatcher
61 /// Contains different implementations (win32 api, inotify and polling using the std.file methods)
62 /// Specify `version = FSWForcePoll;` to force using std.file (is slower and more resource intensive than the other implementations)
63 struct FileWatch
64 {
65 	/// Path of the file set using the constructor
66 	const string path;
67 
68 	version (FSWUsesWin32)
69 	{
70 		import core.sys.windows.windows : HANDLE, OVERLAPPED, CloseHandle,
71 			GetOverlappedResult, CreateFile, GetLastError,
72 			ReadDirectoryChangesW, FILE_NOTIFY_INFORMATION, FILE_ACTION_ADDED,
73 			FILE_ACTION_REMOVED, FILE_ACTION_MODIFIED,
74 			FILE_ACTION_RENAMED_NEW_NAME, FILE_ACTION_RENAMED_OLD_NAME,
75 			FILE_LIST_DIRECTORY, FILE_SHARE_WRITE, FILE_SHARE_READ,
76 			FILE_SHARE_DELETE, OPEN_EXISTING, FILE_FLAG_OVERLAPPED,
77 			FILE_FLAG_BACKUP_SEMANTICS, FILE_NOTIFY_CHANGE_FILE_NAME,
78 			FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_LAST_WRITE,
79 			ERROR_IO_PENDING, ERROR_IO_INCOMPLETE, DWORD;
80 		import std.utf : toUTF8, toUTF16;
81 		import std.path : absolutePath;
82 		import std.conv : to;
83 		import std.datetime : SysTime;
84 
85 		private HANDLE pathHandle;
86 		private ubyte[1024 * 4] changeBuffer; // 4kb buffer for file changes
87 		private bool isDir, exists, recursive;
88 		private SysTime timeLastModified;
89 		private DWORD receivedBytes;
90 		private OVERLAPPED overlapObj;
91 		private bool queued;
92 
93 		/// Creates an instance using the Win32 API
94 		this(string path, bool recursive = false, bool treatDirAsFile = false)
95 		{
96 			this.path = absolutePath(path, getcwd);
97 			this.recursive = recursive;
98 			isDir = !treatDirAsFile;
99 			if (!isDir && recursive)
100 				throw new Exception("Can't recursively check on a file");
101 			getEvents();
102 		}
103 
104 		~this()
105 		{
106 			CloseHandle(pathHandle);
107 		}
108 
109 		/// Implementation using Win32 API or polling for files
110 		FileChangeEvent[] getEvents()
111 		{
112 			if (isDir && (!path.exists || path.isDir))
113 			{
114 				if (!path.exists)
115 				{
116 					if (pathHandle)
117 					{
118 						if (GetOverlappedResult(pathHandle, &overlapObj, &receivedBytes, false))
119 						{
120 						}
121 						queued = false;
122 						CloseHandle(pathHandle);
123 						pathHandle = null;
124 						return [FileChangeEvent(FileChangeEventType.removeSelf, ".")];
125 					}
126 					return [];
127 				}
128 				FileChangeEvent[] events;
129 				if (!pathHandle)
130 				{
131 					pathHandle = CreateFile((path.toUTF16 ~ cast(wchar) 0).ptr, FILE_LIST_DIRECTORY,
132 							FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
133 							null, OPEN_EXISTING,
134 							FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS, null);
135 					if (!pathHandle)
136 						throw new Exception("Error opening directory. Error code 0x" ~ GetLastError()
137 								.to!string(16));
138 					queued = false;
139 					events ~= FileChangeEvent(FileChangeEventType.createSelf, ".");
140 				}
141 				if (!queued)
142 				{
143 					if (!ReadDirectoryChangesW(pathHandle, changeBuffer.ptr, changeBuffer.length, recursive,
144 							FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
145 							&receivedBytes, &overlapObj, null))
146 						throw new Exception("Failed to queue read. Error 0x" ~ GetLastError()
147 								.to!string(16));
148 					queued = true;
149 				}
150 				else
151 				{
152 					if (GetOverlappedResult(pathHandle, &overlapObj, &receivedBytes, false))
153 					{
154 						int i = 0;
155 						string fromFilename;
156 						while (true)
157 						{
158 							auto info = cast(FILE_NOTIFY_INFORMATION*)(changeBuffer.ptr + i);
159 							string fileName = (cast(wchar[])(
160 									cast(ubyte*) info.FileName)[0 .. info.FileNameLength])
161 								.toUTF8.idup;
162 							switch (info.Action)
163 							{
164 							case FILE_ACTION_ADDED:
165 								events ~= FileChangeEvent(FileChangeEventType.create, fileName);
166 								break;
167 							case FILE_ACTION_REMOVED:
168 								events ~= FileChangeEvent(FileChangeEventType.remove, fileName);
169 								break;
170 							case FILE_ACTION_MODIFIED:
171 								events ~= FileChangeEvent(FileChangeEventType.modify, fileName);
172 								break;
173 							case FILE_ACTION_RENAMED_OLD_NAME:
174 								fromFilename = fileName;
175 								break;
176 							case FILE_ACTION_RENAMED_NEW_NAME:
177 								events ~= FileChangeEvent(FileChangeEventType.rename,
178 										fromFilename, fileName);
179 								break;
180 							default:
181 								throw new Exception(
182 										"Unknown file notify action 0x" ~ info.Action.to!string(
183 										16));
184 							}
185 							i += info.NextEntryOffset;
186 							if (info.NextEntryOffset == 0)
187 								break;
188 						}
189 						queued = false;
190 					}
191 					else if (GetLastError() != ERROR_IO_PENDING
192 							&& GetLastError() != ERROR_IO_INCOMPLETE)
193 						throw new Exception("Error receiving changes. Error code 0x" ~ GetLastError()
194 								.to!string(16));
195 				}
196 				return events;
197 			}
198 			else
199 			{
200 				const nowExists = path.exists;
201 				if (nowExists && !exists)
202 				{
203 					exists = true;
204 					timeLastModified = path.timeLastModified;
205 					return [FileChangeEvent(FileChangeEventType.createSelf, path)];
206 				}
207 				else if (!nowExists && exists)
208 				{
209 					exists = false;
210 					return [FileChangeEvent(FileChangeEventType.removeSelf, path)];
211 				}
212 				else if (nowExists)
213 				{
214 					const modTime = path.timeLastModified;
215 					if (modTime != timeLastModified)
216 					{
217 						timeLastModified = modTime;
218 						return [FileChangeEvent(FileChangeEventType.modify, path)];
219 					}
220 					else
221 						return [];
222 				}
223 				else
224 					return [];
225 			}
226 		}
227 	}
228 	else version (FSWUsesINotify)
229 	{
230 		import core.sys.linux.sys.inotify : inotify_rm_watch, inotify_init1,
231 			inotify_add_watch, inotify_event, IN_CREATE, IN_DELETE,
232 			IN_DELETE_SELF, IN_MODIFY, IN_MOVE_SELF, IN_MOVED_FROM, IN_MOVED_TO,
233 			IN_NONBLOCK, IN_ATTRIB, IN_EXCL_UNLINK;
234 		import core.sys.linux.unistd : close, read;
235 		import core.sys.linux.fcntl : fcntl, F_SETFD, FD_CLOEXEC;
236 		import core.sys.linux.errno : errno;
237 		import core.sys.posix.poll : pollfd, poll, POLLIN;
238 		import std.algorithm : countUntil;
239 		import std.string : toStringz, fromStringz;
240 		import std.conv : to;
241 		import std.path : relativePath, buildPath;
242 
243 		private int fd;
244 		private bool recursive;
245 		private ubyte[1024 * 4] eventBuffer; // 4kb buffer for events
246 		private pollfd pfd;
247 		private struct FDInfo { int wd; bool watched; string path; }
248 		private FDInfo[] directoryMap; // map every watch descriptor to a directory
249 
250 		/// Creates an instance using the linux inotify API
251 		this(string path, bool recursive = false, bool ignored = false)
252 		{
253 			this.path = path;
254 			this.recursive = recursive;
255 			getEvents();
256 		}
257 
258 		~this()
259 		{
260 			if (fd)
261 			{
262 				foreach (ref fdinfo; directoryMap)
263 					if (fdinfo.watched)
264 						inotify_rm_watch(fd, fdinfo.wd);
265 				close(fd);
266 			}
267 		}
268 
269 		private void addWatch(string path)
270 		{
271 			auto wd = inotify_add_watch(fd, path.toStringz,
272 					IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE_SELF
273 					| IN_MOVED_FROM | IN_MOVED_TO | IN_ATTRIB | IN_EXCL_UNLINK);
274 			assert(wd != -1,
275 					"inotify_add_watch returned invalid watch descriptor. Error code "
276 					~ errno.to!string);
277 			assert(fcntl(fd, F_SETFD, FD_CLOEXEC) != -1,
278 					"Could not set FD_CLOEXEC bit. Error code " ~ errno.to!string);
279 			directoryMap ~= FDInfo(wd, true, path);
280 		}
281 
282 		/// Implementation using inotify
283 		FileChangeEvent[] getEvents()
284 		{
285 			FileChangeEvent[] events;
286 			if (!fd && path.exists)
287 			{
288 				fd = inotify_init1(IN_NONBLOCK);
289 				assert(fd != -1,
290 						"inotify_init1 returned invalid file descriptor. Error code "
291 						~ errno.to!string);
292 				addWatch(path);
293 				events ~= FileChangeEvent(FileChangeEventType.createSelf, path);
294 
295 				if (recursive)
296 					foreach(string subPath; dirEntries(path, SpanMode.depth))
297 					{
298 						addWatch(subPath);
299 						events ~= FileChangeEvent(FileChangeEventType.createSelf, subPath);
300 					}
301 			}
302 			if (!fd)
303 				return events;
304 			pfd.fd = fd;
305 			pfd.events = POLLIN;
306 			const code = poll(&pfd, 1, 0);
307 			if (code < 0)
308 				throw new Exception("Failed to poll events. Error code " ~ errno.to!string);
309 			else if (code == 0)
310 				return events;
311 			else
312 			{
313 				const receivedBytes = read(fd, eventBuffer.ptr, eventBuffer.length);
314 				int i = 0;
315 				string fromFilename;
316 				uint cookie;
317 				while (true)
318 				{
319 					auto info = cast(inotify_event*)(eventBuffer.ptr + i);
320 					// contains \0 at the end otherwise
321 					string fileName = info.name.ptr.fromStringz().idup;
322 					auto mapIndex = directoryMap.countUntil!(a => a.wd == info.wd);
323 					string absoluteFileName = buildPath(directoryMap[mapIndex].path, fileName);
324 					string relativeFilename = relativePath("/" ~ absoluteFileName, "/" ~ path);
325 					if (cookie && (info.mask & IN_MOVED_TO) == 0)
326 					{
327 						events ~= FileChangeEvent(FileChangeEventType.remove, fromFilename);
328 						fromFilename.length = 0;
329 						cookie = 0;
330 					}
331 					if ((info.mask & IN_CREATE) != 0)
332 					{
333 						if (absoluteFileName.isDir && recursive)
334 						{
335 							addWatch(absoluteFileName);
336 						}
337 						events ~= FileChangeEvent(FileChangeEventType.create, relativeFilename);
338 					}
339 					if ((info.mask & IN_DELETE) != 0)
340 						events ~= FileChangeEvent(FileChangeEventType.remove, relativeFilename);
341 					if ((info.mask & IN_MODIFY) != 0 || (info.mask & IN_ATTRIB) != 0)
342 						events ~= FileChangeEvent(FileChangeEventType.modify, relativeFilename);
343 					if ((info.mask & IN_MOVED_FROM) != 0)
344 					{
345 						fromFilename = fileName;
346 						cookie = info.cookie;
347 					}
348 					if ((info.mask & IN_MOVED_TO) != 0)
349 					{
350 						if (info.cookie == cookie)
351 						{
352 							events ~= FileChangeEvent(FileChangeEventType.rename,
353 									fromFilename, relativeFilename);
354 						}
355 						else
356 							events ~= FileChangeEvent(FileChangeEventType.create, relativeFilename);
357 						cookie = 0;
358 					}
359 					if ((info.mask & IN_DELETE_SELF) != 0 || (info.mask & IN_MOVE_SELF) != 0)
360 					{
361 						if (fd)
362 						{
363 							inotify_rm_watch(fd, info.wd);
364 							directoryMap[mapIndex].watched = false;
365 						}
366 						if (directoryMap[mapIndex].path == path)
367 							events ~= FileChangeEvent(FileChangeEventType.removeSelf, ".");
368 					}
369 					i += inotify_event.sizeof + info.len;
370 					if (i >= receivedBytes)
371 						break;
372 				}
373 				if (cookie)
374 				{
375 					events ~= FileChangeEvent(FileChangeEventType.remove, fromFilename);
376 					fromFilename.length = 0;
377 					cookie = 0;
378 				}
379 			}
380 			return events;
381 		}
382 	}
383 	else version (FSWUsesPolling)
384 	{
385 		import std.datetime : SysTime;
386 		import std.algorithm : countUntil, remove;
387 		import std.path : relativePath, absolutePath, baseName;
388 
389 		private struct FileEntryCache
390 		{
391 			SysTime lastModification;
392 			const string name;
393 			bool isDirty;
394 			ulong uniqueHash;
395 		}
396 
397 		private FileEntryCache[] cache;
398 		private bool isDir, recursive, exists;
399 		private SysTime timeLastModified;
400 		private string cwd;
401 
402 		/// Generic fallback implementation using std.file.dirEntries
403 		this(string path, bool recursive = false, bool treatDirAsFile = false)
404 		{
405 			this.path = path;
406 			cwd = getcwd;
407 			this.recursive = recursive;
408 			isDir = !treatDirAsFile;
409 			if (!isDir && recursive)
410 				throw new Exception("Can't recursively check on a file");
411 			getEvents();
412 		}
413 
414 		/// Generic polling implementation
415 		FileChangeEvent[] getEvents()
416 		{
417 			const nowExists = path.exists;
418 			if (isDir && (!nowExists || path.isDir))
419 			{
420 				FileChangeEvent[] events;
421 				if (nowExists && !exists)
422 				{
423 					exists = true;
424 					events ~= FileChangeEvent(FileChangeEventType.createSelf, ".");
425 				}
426 				if (!nowExists && exists)
427 				{
428 					exists = false;
429 					return [FileChangeEvent(FileChangeEventType.removeSelf, ".")];
430 				}
431 				if (!nowExists)
432 					return [];
433 				foreach (ref e; cache)
434 					e.isDirty = true;
435 				DirEntry[] created;
436 				foreach (file; dirEntries(path, recursive ? SpanMode.breadth : SpanMode.shallow))
437 				{
438 					auto newCache = FileEntryCache(file.timeLastModified,
439 							file.name, false, file.getUniqueHash);
440 					bool found = false;
441 					foreach (ref cacheEntry; cache)
442 					{
443 						if (cacheEntry.name == newCache.name)
444 						{
445 							if (cacheEntry.lastModification != newCache.lastModification)
446 							{
447 								cacheEntry.lastModification = newCache.lastModification;
448 								events ~= FileChangeEvent(FileChangeEventType.modify,
449 										relativePath(file.name.absolutePath(cwd),
450 											path.absolutePath(cwd)));
451 							}
452 							cacheEntry.isDirty = false;
453 							found = true;
454 							break;
455 						}
456 					}
457 					if (!found)
458 					{
459 						cache ~= newCache;
460 						created ~= file;
461 					}
462 				}
463 				foreach_reverse (i, ref e; cache)
464 				{
465 					if (e.isDirty)
466 					{
467 						auto idx = created.countUntil!((a, b) => a.getUniqueHash == b.uniqueHash)(e);
468 						if (idx != -1)
469 						{
470 							events ~= FileChangeEvent(FileChangeEventType.rename,
471 									relativePath(e.name.absolutePath(cwd),
472 										path.absolutePath(cwd)), relativePath(created[idx].name.absolutePath(cwd),
473 										path.absolutePath(cwd)));
474 							created = created.remove(idx);
475 						}
476 						else
477 						{
478 							events ~= FileChangeEvent(FileChangeEventType.remove,
479 									relativePath(e.name.absolutePath(cwd), path.absolutePath(cwd)));
480 						}
481 						cache = cache.remove(i);
482 					}
483 				}
484 				foreach (ref e; created)
485 				{
486 					events ~= FileChangeEvent(FileChangeEventType.create,
487 							relativePath(e.name.absolutePath(cwd), path.absolutePath(cwd)));
488 				}
489 				if (events.length && events[0].type == FileChangeEventType.createSelf)
490 					return [events[0]];
491 				return events;
492 			}
493 			else
494 			{
495 				if (nowExists && !exists)
496 				{
497 					exists = true;
498 					timeLastModified = path.timeLastModified;
499 					return [FileChangeEvent(FileChangeEventType.createSelf, ".")];
500 				}
501 				else if (!nowExists && exists)
502 				{
503 					exists = false;
504 					return [FileChangeEvent(FileChangeEventType.removeSelf, ".")];
505 				}
506 				else if (nowExists)
507 				{
508 					const modTime = path.timeLastModified;
509 					if (modTime != timeLastModified)
510 					{
511 						timeLastModified = modTime;
512 						return [FileChangeEvent(FileChangeEventType.modify, path.baseName)];
513 					}
514 					else
515 						return [];
516 				}
517 				else
518 					return [];
519 			}
520 		}
521 	}
522 	else
523 		static assert(0, "No filesystem watching method?! Try setting version = FSWForcePoll;");
524 }
525 
526 ///
527 unittest
528 {
529 	import core.thread;
530 
531 	FileChangeEvent waitForEvent(ref FileWatch watcher)
532 	{
533 		FileChangeEvent[] ret;
534 		while ((ret = watcher.getEvents()).length == 0)
535 		{
536 			Thread.sleep(1.msecs);
537 		}
538 		return ret[0];
539 	}
540 
541 	if (exists("test"))
542 		rmdirRecurse("test");
543 	scope (exit)
544 	{
545 		if (exists("test"))
546 			rmdirRecurse("test");
547 	}
548 
549 	auto watcher = FileWatch("test", true);
550 	mkdir("test");
551 	auto ev = waitForEvent(watcher);
552 	assert(ev.type == FileChangeEventType.createSelf);
553 	write("test/a.txt", "abc");
554 	ev = waitForEvent(watcher);
555 	assert(ev.type == FileChangeEventType.create);
556 	assert(ev.path == "a.txt");
557 	Thread.sleep(2000.msecs); // for polling variant
558 	append("test/a.txt", "def");
559 	ev = waitForEvent(watcher);
560 	assert(ev.type == FileChangeEventType.modify);
561 	assert(ev.path == "a.txt");
562 	rename("test/a.txt", "test/b.txt");
563 	ev = waitForEvent(watcher);
564 	assert(ev.type == FileChangeEventType.rename);
565 	assert(ev.path == "a.txt");
566 	assert(ev.newPath == "b.txt");
567 	remove("test/b.txt");
568 	ev = waitForEvent(watcher);
569 	assert(ev.type == FileChangeEventType.remove);
570 	assert(ev.path == "b.txt");
571 	rmdirRecurse("test");
572 	ev = waitForEvent(watcher);
573 	assert(ev.type == FileChangeEventType.removeSelf);
574 }
575 
576 version (linux) unittest
577 {
578 	import core.thread;
579 
580 	FileChangeEvent waitForEvent(ref FileWatch watcher)
581 	{
582 		FileChangeEvent[] ret;
583 		while ((ret = watcher.getEvents()).length == 0)
584 		{
585 			Thread.sleep(1.msecs);
586 		}
587 		return ret[0];
588 	}
589 
590 	if (exists("test2"))
591 		rmdirRecurse("test2");
592 	if (exists("test3"))
593 		rmdirRecurse("test3");
594 	scope (exit)
595 	{
596 		if (exists("test2"))
597 			rmdirRecurse("test2");
598 		if (exists("test3"))
599 			rmdirRecurse("test3");
600 	}
601 
602 	auto watcher = FileWatch("test2", true);
603 	mkdir("test2");
604 	auto ev = waitForEvent(watcher);
605 	assert(ev.type == FileChangeEventType.createSelf);
606 	write("test2/a.txt", "abc");
607 	ev = waitForEvent(watcher);
608 	assert(ev.type == FileChangeEventType.create);
609 	assert(ev.path == "a.txt");
610 	rename("test2/a.txt", "./testfile-a.txt");
611 	ev = waitForEvent(watcher);
612 	assert(ev.type == FileChangeEventType.remove);
613 	assert(ev.path == "a.txt");
614 	rename("./testfile-a.txt", "test2/b.txt");
615 	ev = waitForEvent(watcher);
616 	assert(ev.type == FileChangeEventType.create);
617 	assert(ev.path == "b.txt");
618 	remove("test2/b.txt");
619 	ev = waitForEvent(watcher);
620 	assert(ev.type == FileChangeEventType.remove);
621 	assert(ev.path == "b.txt");
622 
623 	version (FSWUsesINotify)
624 	{
625 		// test for creation, modification, removal of subdirectory
626 		mkdir("test2/subdir");
627 		ev = waitForEvent(watcher);
628 		assert(ev.type == FileChangeEventType.create);
629 		assert(ev.path == "subdir");
630 		write("test2/subdir/c.txt", "abc");
631 		ev = waitForEvent(watcher);
632 		assert(ev.type == FileChangeEventType.create);
633 		assert(ev.path == "subdir/c.txt");
634 		write("test2/subdir/c.txt", "\nabc");
635 		ev = waitForEvent(watcher);
636 		assert(ev.type == FileChangeEventType.modify);
637 		assert(ev.path == "subdir/c.txt");
638 		rmdirRecurse("test2/subdir");
639 		auto events = watcher.getEvents();
640 		assert(events[0].type == FileChangeEventType.remove);
641 		assert(events[0].path == "subdir/c.txt");
642 		assert(events[1].type == FileChangeEventType.remove);
643 		assert(events[1].path == "subdir");
644 	}
645 	// removal of watched folder
646 	rmdirRecurse("test2");
647 	ev = waitForEvent(watcher);
648 	assert(ev.type == FileChangeEventType.removeSelf);
649 	assert(ev.path == ".");
650 
651 	version (FSWUsesINotify)
652 	{
653 		// test for a subdirectory already present
654 		// both when recursive = true and recursive = false
655 		foreach (recursive; [true, false])
656 		{
657 			mkdir("test3");
658 			mkdir("test3/a");
659 			mkdir("test3/a/b");
660 			watcher = FileWatch("test3", recursive);
661 			write("test3/a/b/c.txt", "abc");
662 			if (recursive)
663 			{
664 				ev = waitForEvent(watcher);
665 				assert(ev.type == FileChangeEventType.create);
666 				assert(ev.path == "a/b/c.txt");
667 			}
668 			if (!recursive)
669 			{
670 				// creation of subdirectory and file within
671 				// test that addWatch doesn't get called
672 				mkdir("test3/d");
673 				write("test3/d/e.txt", "abc");
674 				auto revents = watcher.getEvents();
675 				assert(revents.length == 1);
676 				assert(revents[0].type == FileChangeEventType.create);
677 				assert(revents[0].path == "d");
678 				rmdirRecurse("test3/d");
679 				revents = watcher.getEvents();
680 				assert(revents.length == 1);
681 				assert(revents[0].type == FileChangeEventType.remove);
682 				assert(revents[0].path == "d");
683 			}
684 			rmdirRecurse("test3");
685 			events = watcher.getEvents();
686 			if (recursive)
687 			{
688 				assert(events.length == 4);
689 				assert(events[0].type == FileChangeEventType.remove);
690 				assert(events[0].path == "a/b/c.txt");
691 				assert(events[1].type == FileChangeEventType.remove);
692 				assert(events[1].path == "a/b");
693 				assert(events[2].type == FileChangeEventType.remove);
694 				assert(events[2].path == "a");
695 				assert(events[3].type == FileChangeEventType.removeSelf);
696 				assert(events[3].path == ".");
697 			}
698 			else
699 			{
700 				assert(events.length == 2);
701 				assert(events[0].type == FileChangeEventType.remove);
702 				assert(events[0].path == "a");
703 				assert(events[1].type == FileChangeEventType.removeSelf);
704 				assert(events[1].path == ".");
705 			}
706 		}
707 	}
708 }