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