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 	// internal path variable which shouldn't be changed because it will not update inotify/poll/win32 structures uniformly.
66 	string _path;
67 
68 	/// Path of the file set using the constructor
69 	const ref const(string) path() return @property @safe @nogc nothrow pure
70 	{
71 		return _path;
72 	}
73 
74 	version (FSWUsesWin32)
75 	{
76 		/*
77 		 * The Windows version works by first creating an asynchronous path handle using CreateFile.
78 		 * The name may suggest this creates a new file on disk, but it actually gives
79 		 * a handle to basically anything I/O related. By using the flags FILE_FLAG_OVERLAPPED
80 		 * and FILE_FLAG_BACKUP_SEMANTICS it can be used in ReadDirectoryChangesW.
81 		 * 'Overlapped' here means asynchronous, it can also be done synchronously but that would
82 		 * mean getEvents() would wait until a directory change is registered.
83 		 * The asynchronous results can be received in a callback, but since FSWatch is polling
84 		 * based it polls the results using GetOverlappedResult. If messages are received,
85 		 * ReadDirectoryChangesW is called again.
86 		 * The function will not notify when the watched directory itself is removed, so
87 		 * if it doesn't exist anymore the handle is closed and set to null until it exists again.
88 		 */
89 		import core.sys.windows.windows : HANDLE, OVERLAPPED, CloseHandle,
90 			GetOverlappedResult, CreateFile, GetLastError,
91 			ReadDirectoryChangesW, FILE_NOTIFY_INFORMATION, FILE_ACTION_ADDED,
92 			FILE_ACTION_REMOVED, FILE_ACTION_MODIFIED,
93 			FILE_ACTION_RENAMED_NEW_NAME, FILE_ACTION_RENAMED_OLD_NAME,
94 			FILE_LIST_DIRECTORY, FILE_SHARE_WRITE, FILE_SHARE_READ,
95 			FILE_SHARE_DELETE, OPEN_EXISTING, FILE_FLAG_OVERLAPPED,
96 			FILE_FLAG_BACKUP_SEMANTICS, FILE_NOTIFY_CHANGE_FILE_NAME,
97 			FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_LAST_WRITE,
98 			ERROR_IO_PENDING, ERROR_IO_INCOMPLETE, DWORD;
99 		import std.utf : toUTF8, toUTF16;
100 		import std.path : absolutePath;
101 		import std.conv : to;
102 		import std.datetime : SysTime;
103 
104 		private HANDLE pathHandle; // Windows 'file' handle for ReadDirectoryChangesW
105 		private ubyte[1024 * 4] changeBuffer; // 4kb buffer for file changes
106 		private bool isDir, exists, recursive;
107 		private SysTime timeLastModified;
108 		private DWORD receivedBytes;
109 		private OVERLAPPED overlapObj;
110 		private bool queued; // Whether a directory changes watch is issued to Windows
111 
112 		/// Creates an instance using the Win32 API
113 		this(string path, bool recursive = false, bool treatDirAsFile = false)
114 		{
115 			_path = absolutePath(path, getcwd);
116 			this.recursive = recursive;
117 			isDir = !treatDirAsFile;
118 			if (!isDir && recursive)
119 				throw new Exception("Can't recursively check on a file");
120 			getEvents(); // To create a path handle and start the watch queue
121 			// The result, likely containing just 'createSelf' or 'removeSelf', is discarded
122 			// This way, the first actual call to getEvents() returns actual events
123 		}
124 
125 		~this()
126 		{
127 			CloseHandle(pathHandle);
128 		}
129 
130 		private void startWatchQueue()
131 		{
132 			if (!ReadDirectoryChangesW(pathHandle, changeBuffer.ptr, changeBuffer.length, recursive,
133 					FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
134 					&receivedBytes, &overlapObj, null))
135 				throw new Exception("Failed to start directory watch queue. Error 0x" ~ GetLastError()
136 					.to!string(16));
137 			queued = true;
138 		}
139 
140 		/// Implementation using Win32 API or polling for files
141 		FileChangeEvent[] getEvents()
142 		{
143 			const pathExists = path.exists; // cached so it is not called twice
144 			if (isDir && (!pathExists || path.isDir))
145 			{
146 				// ReadDirectoryChangesW does not report changes to the specified directory
147 				// itself, so 'removeself' is checked manually
148 				if (!pathExists)
149 				{
150 					if (pathHandle)
151 					{
152 						if (GetOverlappedResult(pathHandle, &overlapObj, &receivedBytes, false))
153 						{
154 						}
155 						queued = false;
156 						CloseHandle(pathHandle);
157 						pathHandle = null;
158 						return [FileChangeEvent(FileChangeEventType.removeSelf, ".")];
159 					}
160 					return [];
161 				}
162 				FileChangeEvent[] events;
163 				if (!pathHandle)
164 				{
165 					pathHandle = CreateFile((path.toUTF16 ~ cast(wchar) 0).ptr, FILE_LIST_DIRECTORY,
166 							FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
167 							null, OPEN_EXISTING,
168 							FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS, null);
169 					if (!pathHandle)
170 						throw new Exception("Error opening directory. Error code 0x" ~ GetLastError()
171 								.to!string(16));
172 					queued = false;
173 					events ~= FileChangeEvent(FileChangeEventType.createSelf, ".");
174 				}
175 				if (!queued)
176 				{
177 					startWatchQueue();
178 				}
179 				else
180 				{
181 					// ReadDirectoryW can give double modify messages, making the queue one event behind
182 					// This sequence is repeated as a fix for now, until the intricacy of WinAPI is figured out
183 					foreach(_; 0..2)
184 					{
185 						if (GetOverlappedResult(pathHandle, &overlapObj, &receivedBytes, false))
186 						{
187 							int i = 0;
188 							string fromFilename;
189 							while (true)
190 							{
191 								auto info = cast(FILE_NOTIFY_INFORMATION*)(changeBuffer.ptr + i);
192 								string fileName = (cast(wchar[])(
193 										cast(ubyte*) info.FileName)[0 .. info.FileNameLength])
194 									.toUTF8.idup;
195 								switch (info.Action)
196 								{
197 								case FILE_ACTION_ADDED:
198 									events ~= FileChangeEvent(FileChangeEventType.create, fileName);
199 									break;
200 								case FILE_ACTION_REMOVED:
201 									events ~= FileChangeEvent(FileChangeEventType.remove, fileName);
202 									break;
203 								case FILE_ACTION_MODIFIED:
204 									events ~= FileChangeEvent(FileChangeEventType.modify, fileName);
205 									break;
206 								case FILE_ACTION_RENAMED_OLD_NAME:
207 									fromFilename = fileName;
208 									break;
209 								case FILE_ACTION_RENAMED_NEW_NAME:
210 									events ~= FileChangeEvent(FileChangeEventType.rename,
211 											fromFilename, fileName);
212 									break;
213 								default:
214 									throw new Exception(
215 											"Unknown file notify action 0x" ~ info.Action.to!string(
216 											16));
217 								}
218 								i += info.NextEntryOffset;
219 								if (info.NextEntryOffset == 0)
220 									break;
221 							}
222 							queued = false;
223 							startWatchQueue();
224 						}
225 						else
226 						{
227 							if (GetLastError() != ERROR_IO_PENDING
228 								&& GetLastError() != ERROR_IO_INCOMPLETE)
229 								throw new Exception("Error receiving changes. Error code 0x"
230 									~ GetLastError().to!string(16));
231 							break;
232 						}
233 					}
234 				}
235 				return events;
236 			}
237 			else
238 			{
239 				const nowExists = path.exists;
240 				if (nowExists && !exists)
241 				{
242 					exists = true;
243 					timeLastModified = path.timeLastModified;
244 					return [FileChangeEvent(FileChangeEventType.createSelf, path)];
245 				}
246 				else if (!nowExists && exists)
247 				{
248 					exists = false;
249 					return [FileChangeEvent(FileChangeEventType.removeSelf, path)];
250 				}
251 				else if (nowExists)
252 				{
253 					const modTime = path.timeLastModified;
254 					if (modTime != timeLastModified)
255 					{
256 						timeLastModified = modTime;
257 						return [FileChangeEvent(FileChangeEventType.modify, path)];
258 					}
259 					else
260 						return [];
261 				}
262 				else
263 					return [];
264 			}
265 		}
266 	}
267 	else version (FSWUsesINotify)
268 	{
269 		import core.sys.linux.sys.inotify : inotify_rm_watch, inotify_init1,
270 			inotify_add_watch, inotify_event, IN_CREATE, IN_DELETE,
271 			IN_DELETE_SELF, IN_MODIFY, IN_MOVE_SELF, IN_MOVED_FROM, IN_MOVED_TO,
272 			IN_NONBLOCK, IN_ATTRIB, IN_EXCL_UNLINK;
273 		import core.sys.linux.unistd : close, read;
274 		import core.sys.linux.fcntl : fcntl, F_SETFD, FD_CLOEXEC, stat, stat_t, S_ISDIR;
275 		import core.sys.linux.errno : errno;
276 		import core.sys.posix.poll : pollfd, poll, POLLIN;
277 		import core.stdc.errno : ENOENT;
278 		import std.algorithm : countUntil;
279 		import std..string : toStringz, stripRight;
280 		import std.conv : to;
281 		import std.path : relativePath, buildPath;
282 
283 		private int fd;
284 		private bool recursive;
285 		private ubyte[1024 * 4] eventBuffer; // 4kb buffer for events
286 		private pollfd pfd;
287 		private struct FDInfo { int wd; bool watched; string path; }
288 		private FDInfo[] directoryMap; // map every watch descriptor to a directory
289 
290 		/// Creates an instance using the linux inotify API
291 		this(string path, bool recursive = false, bool ignored = false)
292 		{
293 			_path = path;
294 			this.recursive = recursive;
295 			getEvents();
296 		}
297 
298 		~this()
299 		{
300 			if (fd)
301 			{
302 				foreach (ref fdinfo; directoryMap)
303 					if (fdinfo.watched)
304 						inotify_rm_watch(fd, fdinfo.wd);
305 				close(fd);
306 			}
307 		}
308 
309 		private void addWatch(string path)
310 		{
311 			auto wd = inotify_add_watch(fd, path.toStringz,
312 					IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE_SELF
313 					| IN_MOVED_FROM | IN_MOVED_TO | IN_ATTRIB | IN_EXCL_UNLINK);
314 			assert(wd != -1,
315 					"inotify_add_watch returned invalid watch descriptor. Error code "
316 					~ errno.to!string);
317 			assert(fcntl(fd, F_SETFD, FD_CLOEXEC) != -1,
318 					"Could not set FD_CLOEXEC bit. Error code " ~ errno.to!string);
319 			directoryMap ~= FDInfo(wd, true, path);
320 		}
321 
322 		/// Implementation using inotify
323 		FileChangeEvent[] getEvents()
324 		{
325 			FileChangeEvent[] events;
326 			if (!fd && path.exists)
327 			{
328 				fd = inotify_init1(IN_NONBLOCK);
329 				assert(fd != -1,
330 						"inotify_init1 returned invalid file descriptor. Error code "
331 						~ errno.to!string);
332 				addWatch(path);
333 				events ~= FileChangeEvent(FileChangeEventType.createSelf, path);
334 
335 				if (recursive)
336 					foreach(string subPath; dirEntries(path, SpanMode.depth))
337 					{
338 						addWatch(subPath);
339 						events ~= FileChangeEvent(FileChangeEventType.createSelf, subPath);
340 					}
341 			}
342 			if (!fd)
343 				return events;
344 			pfd.fd = fd;
345 			pfd.events = POLLIN;
346 			const code = poll(&pfd, 1, 0);
347 			if (code < 0)
348 				throw new Exception("Failed to poll events. Error code " ~ errno.to!string);
349 			else if (code == 0)
350 				return events;
351 			else
352 			{
353 				const receivedBytes = read(fd, eventBuffer.ptr, eventBuffer.length);
354 				int i = 0;
355 				string fromFilename;
356 				uint cookie;
357 				while (true)
358 				{
359 					auto info = cast(inotify_event*)(eventBuffer.ptr + i);
360 					string fileName = info.name.ptr[0..info.len].stripRight("\0").idup;
361 					auto mapIndex = directoryMap.countUntil!(a => a.wd == info.wd);
362 					string absoluteFileName = buildPath(directoryMap[mapIndex].path, fileName);
363 					string relativeFilename = relativePath("/" ~ absoluteFileName, "/" ~ path);
364 					if (cookie && (info.mask & IN_MOVED_TO) == 0)
365 					{
366 						events ~= FileChangeEvent(FileChangeEventType.remove, fromFilename);
367 						fromFilename.length = 0;
368 						cookie = 0;
369 					}
370 					if ((info.mask & IN_CREATE) != 0)
371 					{
372 						// If a dir/file is created and deleted immediately then
373 						// isDir will throw FileException(ENOENT)
374 						if (recursive)
375 						{
376 							stat_t dirCheck;
377 							if (stat(absoluteFileName.toStringz, &dirCheck) == 0)
378 							{
379 								if (S_ISDIR(dirCheck.st_mode))
380 									addWatch(absoluteFileName);
381 							}
382 							else
383 							{
384 								const err = errno;
385 								if (err != ENOENT)
386 									throw new FileException(absoluteFileName, err);
387 							}
388 						}
389 
390 						events ~= FileChangeEvent(FileChangeEventType.create, relativeFilename);
391 					}
392 					if ((info.mask & IN_DELETE) != 0)
393 						events ~= FileChangeEvent(FileChangeEventType.remove, relativeFilename);
394 					if ((info.mask & IN_MODIFY) != 0 || (info.mask & IN_ATTRIB) != 0)
395 						events ~= FileChangeEvent(FileChangeEventType.modify, relativeFilename);
396 					if ((info.mask & IN_MOVED_FROM) != 0)
397 					{
398 						fromFilename = fileName;
399 						cookie = info.cookie;
400 					}
401 					if ((info.mask & IN_MOVED_TO) != 0)
402 					{
403 						if (info.cookie == cookie)
404 						{
405 							events ~= FileChangeEvent(FileChangeEventType.rename,
406 									fromFilename, relativeFilename);
407 						}
408 						else
409 							events ~= FileChangeEvent(FileChangeEventType.create, relativeFilename);
410 						cookie = 0;
411 					}
412 					if ((info.mask & IN_DELETE_SELF) != 0 || (info.mask & IN_MOVE_SELF) != 0)
413 					{
414 						if (fd)
415 						{
416 							inotify_rm_watch(fd, info.wd);
417 							directoryMap[mapIndex].watched = false;
418 						}
419 						if (directoryMap[mapIndex].path == path)
420 							events ~= FileChangeEvent(FileChangeEventType.removeSelf, ".");
421 					}
422 					i += inotify_event.sizeof + info.len;
423 					if (i >= receivedBytes)
424 						break;
425 				}
426 				if (cookie)
427 				{
428 					events ~= FileChangeEvent(FileChangeEventType.remove, fromFilename);
429 					fromFilename.length = 0;
430 					cookie = 0;
431 				}
432 			}
433 			return events;
434 		}
435 	}
436 	else version (FSWUsesPolling)
437 	{
438 		import std.datetime : SysTime;
439 		import std.algorithm : countUntil, remove;
440 		import std.path : relativePath, absolutePath, baseName;
441 
442 		private struct FileEntryCache
443 		{
444 			SysTime lastModification;
445 			const string name;
446 			bool isDirty;
447 			ulong uniqueHash;
448 		}
449 
450 		private FileEntryCache[] cache;
451 		private bool isDir, recursive, exists;
452 		private SysTime timeLastModified;
453 		private string cwd;
454 
455 		/// Generic fallback implementation using std.file.dirEntries
456 		this(string path, bool recursive = false, bool treatDirAsFile = false)
457 		{
458 			_path = path;
459 			cwd = getcwd;
460 			this.recursive = recursive;
461 			isDir = !treatDirAsFile;
462 			if (!isDir && recursive)
463 				throw new Exception("Can't recursively check on a file");
464 			getEvents();
465 		}
466 
467 		/// Generic polling implementation
468 		FileChangeEvent[] getEvents()
469 		{
470 			const nowExists = path.exists;
471 			if (isDir && (!nowExists || path.isDir))
472 			{
473 				FileChangeEvent[] events;
474 				if (nowExists && !exists)
475 				{
476 					exists = true;
477 					events ~= FileChangeEvent(FileChangeEventType.createSelf, ".");
478 				}
479 				if (!nowExists && exists)
480 				{
481 					exists = false;
482 					return [FileChangeEvent(FileChangeEventType.removeSelf, ".")];
483 				}
484 				if (!nowExists)
485 					return [];
486 				foreach (ref e; cache)
487 					e.isDirty = true;
488 				DirEntry[] created;
489 				foreach (file; dirEntries(path, recursive ? SpanMode.breadth : SpanMode.shallow))
490 				{
491 					auto newCache = FileEntryCache(file.timeLastModified,
492 							file.name, false, file.getUniqueHash);
493 					bool found = false;
494 					foreach (ref cacheEntry; cache)
495 					{
496 						if (cacheEntry.name == newCache.name)
497 						{
498 							if (cacheEntry.lastModification != newCache.lastModification)
499 							{
500 								cacheEntry.lastModification = newCache.lastModification;
501 								events ~= FileChangeEvent(FileChangeEventType.modify,
502 										relativePath(file.name.absolutePath(cwd),
503 											path.absolutePath(cwd)));
504 							}
505 							cacheEntry.isDirty = false;
506 							found = true;
507 							break;
508 						}
509 					}
510 					if (!found)
511 					{
512 						cache ~= newCache;
513 						created ~= file;
514 					}
515 				}
516 				foreach_reverse (i, ref e; cache)
517 				{
518 					if (e.isDirty)
519 					{
520 						auto idx = created.countUntil!((a, b) => a.getUniqueHash == b.uniqueHash)(e);
521 						if (idx != -1)
522 						{
523 							events ~= FileChangeEvent(FileChangeEventType.rename,
524 									relativePath(e.name.absolutePath(cwd),
525 										path.absolutePath(cwd)), relativePath(created[idx].name.absolutePath(cwd),
526 										path.absolutePath(cwd)));
527 							created = created.remove(idx);
528 						}
529 						else
530 						{
531 							events ~= FileChangeEvent(FileChangeEventType.remove,
532 									relativePath(e.name.absolutePath(cwd), path.absolutePath(cwd)));
533 						}
534 						cache = cache.remove(i);
535 					}
536 				}
537 				foreach (ref e; created)
538 				{
539 					events ~= FileChangeEvent(FileChangeEventType.create,
540 							relativePath(e.name.absolutePath(cwd), path.absolutePath(cwd)));
541 				}
542 				if (events.length && events[0].type == FileChangeEventType.createSelf)
543 					return [events[0]];
544 				return events;
545 			}
546 			else
547 			{
548 				if (nowExists && !exists)
549 				{
550 					exists = true;
551 					timeLastModified = path.timeLastModified;
552 					return [FileChangeEvent(FileChangeEventType.createSelf, ".")];
553 				}
554 				else if (!nowExists && exists)
555 				{
556 					exists = false;
557 					return [FileChangeEvent(FileChangeEventType.removeSelf, ".")];
558 				}
559 				else if (nowExists)
560 				{
561 					const modTime = path.timeLastModified;
562 					if (modTime != timeLastModified)
563 					{
564 						timeLastModified = modTime;
565 						return [FileChangeEvent(FileChangeEventType.modify, path.baseName)];
566 					}
567 					else
568 						return [];
569 				}
570 				else
571 					return [];
572 			}
573 		}
574 	}
575 	else
576 		static assert(0, "No filesystem watching method?! Try setting version = FSWForcePoll;");
577 }
578 
579 ///
580 unittest
581 {
582 	import core.thread;
583 
584 	FileChangeEvent waitForEvent(ref FileWatch watcher)
585 	{
586 		FileChangeEvent[] ret;
587 		while ((ret = watcher.getEvents()).length == 0)
588 		{
589 			Thread.sleep(1.msecs);
590 		}
591 		return ret[0];
592 	}
593 
594 	if (exists("test"))
595 		rmdirRecurse("test");
596 	scope (exit)
597 	{
598 		if (exists("test"))
599 			rmdirRecurse("test");
600 	}
601 
602 	auto watcher = FileWatch("test", true);
603 	assert(watcher.path == "test");
604 	mkdir("test");
605 	auto ev = waitForEvent(watcher);
606 	assert(ev.type == FileChangeEventType.createSelf);
607 	write("test/a.txt", "abc");
608 	ev = waitForEvent(watcher);
609 	assert(ev.type == FileChangeEventType.create);
610 	assert(ev.path == "a.txt");
611 	Thread.sleep(2000.msecs); // for polling variant
612 	append("test/a.txt", "def");
613 	ev = waitForEvent(watcher);
614 	assert(ev.type == FileChangeEventType.modify);
615 	assert(ev.path == "a.txt");
616 	rename("test/a.txt", "test/b.txt");
617 	ev = waitForEvent(watcher);
618 	assert(ev.type == FileChangeEventType.rename);
619 	assert(ev.path == "a.txt");
620 	assert(ev.newPath == "b.txt");
621 	remove("test/b.txt");
622 	ev = waitForEvent(watcher);
623 	assert(ev.type == FileChangeEventType.remove);
624 	assert(ev.path == "b.txt");
625 	rmdirRecurse("test");
626 	ev = waitForEvent(watcher);
627 	assert(ev.type == FileChangeEventType.removeSelf);
628 }
629 
630 version (linux) unittest
631 {
632 	import core.thread;
633 
634 	FileChangeEvent waitForEvent(ref FileWatch watcher, Duration timeout = 2.seconds)
635 	{
636 		FileChangeEvent[] ret;
637 		Duration elapsed;
638 		while ((ret = watcher.getEvents()).length == 0)
639 		{
640 			Thread.sleep(1.msecs);
641 			elapsed += 1.msecs;
642 			if (elapsed >= timeout)
643 				throw new Exception("timeout");
644 		}
645 		return ret[0];
646 	}
647 
648 	if (exists("test2"))
649 		rmdirRecurse("test2");
650 	if (exists("test3"))
651 		rmdirRecurse("test3");
652 	scope (exit)
653 	{
654 		if (exists("test2"))
655 			rmdirRecurse("test2");
656 		if (exists("test3"))
657 			rmdirRecurse("test3");
658 	}
659 
660 	auto watcher = FileWatch("test2", true);
661 	mkdir("test2");
662 	auto ev = waitForEvent(watcher);
663 	assert(ev.type == FileChangeEventType.createSelf);
664 	write("test2/a.txt", "abc");
665 	ev = waitForEvent(watcher);
666 	assert(ev.type == FileChangeEventType.create);
667 	assert(ev.path == "a.txt");
668 	rename("test2/a.txt", "./testfile-a.txt");
669 	ev = waitForEvent(watcher);
670 	assert(ev.type == FileChangeEventType.remove);
671 	assert(ev.path == "a.txt");
672 	rename("./testfile-a.txt", "test2/b.txt");
673 	ev = waitForEvent(watcher);
674 	assert(ev.type == FileChangeEventType.create);
675 	assert(ev.path == "b.txt");
676 	remove("test2/b.txt");
677 	ev = waitForEvent(watcher);
678 	assert(ev.type == FileChangeEventType.remove);
679 	assert(ev.path == "b.txt");
680 
681 	mkdir("test2/mydir");
682 	rmdir("test2/mydir");
683 	try
684 	{
685 		ev = waitForEvent(watcher);
686 		// waitForEvent only returns first event (just a test method anyway) because on windows or unprecise platforms events can be spawned multiple times
687 		// or could be never fired in case of slow polling mechanism
688 		assert(ev.type == FileChangeEventType.create);
689 		assert(ev.path == "mydir");
690 	}
691 	catch (Exception e)
692 	{
693 		if (e.msg != "timeout")
694 			throw e;
695 	}
696 
697 	version (FSWUsesINotify)
698 	{
699 		// test for creation, modification, removal of subdirectory
700 		mkdir("test2/subdir");
701 		ev = waitForEvent(watcher);
702 		assert(ev.type == FileChangeEventType.create);
703 		assert(ev.path == "subdir");
704 		write("test2/subdir/c.txt", "abc");
705 		ev = waitForEvent(watcher);
706 		assert(ev.type == FileChangeEventType.create);
707 		assert(ev.path == "subdir/c.txt");
708 		write("test2/subdir/c.txt", "\nabc");
709 		ev = waitForEvent(watcher);
710 		assert(ev.type == FileChangeEventType.modify);
711 		assert(ev.path == "subdir/c.txt");
712 		rmdirRecurse("test2/subdir");
713 		auto events = watcher.getEvents();
714 		assert(events[0].type == FileChangeEventType.remove);
715 		assert(events[0].path == "subdir/c.txt");
716 		assert(events[1].type == FileChangeEventType.remove);
717 		assert(events[1].path == "subdir");
718 	}
719 	// removal of watched folder
720 	rmdirRecurse("test2");
721 	ev = waitForEvent(watcher);
722 	assert(ev.type == FileChangeEventType.removeSelf);
723 	assert(ev.path == ".");
724 
725 	version (FSWUsesINotify)
726 	{
727 		// test for a subdirectory already present
728 		// both when recursive = true and recursive = false
729 		foreach (recursive; [true, false])
730 		{
731 			mkdir("test3");
732 			mkdir("test3/a");
733 			mkdir("test3/a/b");
734 			watcher = FileWatch("test3", recursive);
735 			write("test3/a/b/c.txt", "abc");
736 			if (recursive)
737 			{
738 				ev = waitForEvent(watcher);
739 				assert(ev.type == FileChangeEventType.create);
740 				assert(ev.path == "a/b/c.txt");
741 			}
742 			if (!recursive)
743 			{
744 				// creation of subdirectory and file within
745 				// test that addWatch doesn't get called
746 				mkdir("test3/d");
747 				write("test3/d/e.txt", "abc");
748 				auto revents = watcher.getEvents();
749 				assert(revents.length == 1);
750 				assert(revents[0].type == FileChangeEventType.create);
751 				assert(revents[0].path == "d");
752 				rmdirRecurse("test3/d");
753 				revents = watcher.getEvents();
754 				assert(revents.length == 1);
755 				assert(revents[0].type == FileChangeEventType.remove);
756 				assert(revents[0].path == "d");
757 			}
758 			rmdirRecurse("test3");
759 			events = watcher.getEvents();
760 			if (recursive)
761 			{
762 				assert(events.length == 4);
763 				assert(events[0].type == FileChangeEventType.remove);
764 				assert(events[0].path == "a/b/c.txt");
765 				assert(events[1].type == FileChangeEventType.remove);
766 				assert(events[1].path == "a/b");
767 				assert(events[2].type == FileChangeEventType.remove);
768 				assert(events[2].path == "a");
769 				assert(events[3].type == FileChangeEventType.removeSelf);
770 				assert(events[3].path == ".");
771 			}
772 			else
773 			{
774 				assert(events.length == 2);
775 				assert(events[0].type == FileChangeEventType.remove);
776 				assert(events[0].path == "a");
777 				assert(events[1].type == FileChangeEventType.removeSelf);
778 				assert(events[1].path == ".");
779 			}
780 		}
781 	}
782 }