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() @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, fromStringz;
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 					// contains \0 at the end otherwise
361 					string fileName = info.name.ptr.fromStringz().idup;
362 					auto mapIndex = directoryMap.countUntil!(a => a.wd == info.wd);
363 					string absoluteFileName = buildPath(directoryMap[mapIndex].path, fileName);
364 					string relativeFilename = relativePath("/" ~ absoluteFileName, "/" ~ path);
365 					if (cookie && (info.mask & IN_MOVED_TO) == 0)
366 					{
367 						events ~= FileChangeEvent(FileChangeEventType.remove, fromFilename);
368 						fromFilename.length = 0;
369 						cookie = 0;
370 					}
371 					if ((info.mask & IN_CREATE) != 0)
372 					{
373 						// If a dir/file is created and deleted immediately then
374 						// isDir will throw FileException(ENOENT)
375 						if (recursive)
376 						{
377 							stat_t dirCheck;
378 							if (stat(absoluteFileName.toStringz, &dirCheck) == 0)
379 							{
380 								if (S_ISDIR(dirCheck.st_mode))
381 									addWatch(absoluteFileName);
382 							}
383 							else
384 							{
385 								const err = errno;
386 								if (err != ENOENT)
387 									throw new FileException(absoluteFileName, err);
388 							}
389 						}
390 
391 						events ~= FileChangeEvent(FileChangeEventType.create, relativeFilename);
392 					}
393 					if ((info.mask & IN_DELETE) != 0)
394 						events ~= FileChangeEvent(FileChangeEventType.remove, relativeFilename);
395 					if ((info.mask & IN_MODIFY) != 0 || (info.mask & IN_ATTRIB) != 0)
396 						events ~= FileChangeEvent(FileChangeEventType.modify, relativeFilename);
397 					if ((info.mask & IN_MOVED_FROM) != 0)
398 					{
399 						fromFilename = fileName;
400 						cookie = info.cookie;
401 					}
402 					if ((info.mask & IN_MOVED_TO) != 0)
403 					{
404 						if (info.cookie == cookie)
405 						{
406 							events ~= FileChangeEvent(FileChangeEventType.rename,
407 									fromFilename, relativeFilename);
408 						}
409 						else
410 							events ~= FileChangeEvent(FileChangeEventType.create, relativeFilename);
411 						cookie = 0;
412 					}
413 					if ((info.mask & IN_DELETE_SELF) != 0 || (info.mask & IN_MOVE_SELF) != 0)
414 					{
415 						if (fd)
416 						{
417 							inotify_rm_watch(fd, info.wd);
418 							directoryMap[mapIndex].watched = false;
419 						}
420 						if (directoryMap[mapIndex].path == path)
421 							events ~= FileChangeEvent(FileChangeEventType.removeSelf, ".");
422 					}
423 					i += inotify_event.sizeof + info.len;
424 					if (i >= receivedBytes)
425 						break;
426 				}
427 				if (cookie)
428 				{
429 					events ~= FileChangeEvent(FileChangeEventType.remove, fromFilename);
430 					fromFilename.length = 0;
431 					cookie = 0;
432 				}
433 			}
434 			return events;
435 		}
436 	}
437 	else version (FSWUsesPolling)
438 	{
439 		import std.datetime : SysTime;
440 		import std.algorithm : countUntil, remove;
441 		import std.path : relativePath, absolutePath, baseName;
442 
443 		private struct FileEntryCache
444 		{
445 			SysTime lastModification;
446 			const string name;
447 			bool isDirty;
448 			ulong uniqueHash;
449 		}
450 
451 		private FileEntryCache[] cache;
452 		private bool isDir, recursive, exists;
453 		private SysTime timeLastModified;
454 		private string cwd;
455 
456 		/// Generic fallback implementation using std.file.dirEntries
457 		this(string path, bool recursive = false, bool treatDirAsFile = false)
458 		{
459 			_path = path;
460 			cwd = getcwd;
461 			this.recursive = recursive;
462 			isDir = !treatDirAsFile;
463 			if (!isDir && recursive)
464 				throw new Exception("Can't recursively check on a file");
465 			getEvents();
466 		}
467 
468 		/// Generic polling implementation
469 		FileChangeEvent[] getEvents()
470 		{
471 			const nowExists = path.exists;
472 			if (isDir && (!nowExists || path.isDir))
473 			{
474 				FileChangeEvent[] events;
475 				if (nowExists && !exists)
476 				{
477 					exists = true;
478 					events ~= FileChangeEvent(FileChangeEventType.createSelf, ".");
479 				}
480 				if (!nowExists && exists)
481 				{
482 					exists = false;
483 					return [FileChangeEvent(FileChangeEventType.removeSelf, ".")];
484 				}
485 				if (!nowExists)
486 					return [];
487 				foreach (ref e; cache)
488 					e.isDirty = true;
489 				DirEntry[] created;
490 				foreach (file; dirEntries(path, recursive ? SpanMode.breadth : SpanMode.shallow))
491 				{
492 					auto newCache = FileEntryCache(file.timeLastModified,
493 							file.name, false, file.getUniqueHash);
494 					bool found = false;
495 					foreach (ref cacheEntry; cache)
496 					{
497 						if (cacheEntry.name == newCache.name)
498 						{
499 							if (cacheEntry.lastModification != newCache.lastModification)
500 							{
501 								cacheEntry.lastModification = newCache.lastModification;
502 								events ~= FileChangeEvent(FileChangeEventType.modify,
503 										relativePath(file.name.absolutePath(cwd),
504 											path.absolutePath(cwd)));
505 							}
506 							cacheEntry.isDirty = false;
507 							found = true;
508 							break;
509 						}
510 					}
511 					if (!found)
512 					{
513 						cache ~= newCache;
514 						created ~= file;
515 					}
516 				}
517 				foreach_reverse (i, ref e; cache)
518 				{
519 					if (e.isDirty)
520 					{
521 						auto idx = created.countUntil!((a, b) => a.getUniqueHash == b.uniqueHash)(e);
522 						if (idx != -1)
523 						{
524 							events ~= FileChangeEvent(FileChangeEventType.rename,
525 									relativePath(e.name.absolutePath(cwd),
526 										path.absolutePath(cwd)), relativePath(created[idx].name.absolutePath(cwd),
527 										path.absolutePath(cwd)));
528 							created = created.remove(idx);
529 						}
530 						else
531 						{
532 							events ~= FileChangeEvent(FileChangeEventType.remove,
533 									relativePath(e.name.absolutePath(cwd), path.absolutePath(cwd)));
534 						}
535 						cache = cache.remove(i);
536 					}
537 				}
538 				foreach (ref e; created)
539 				{
540 					events ~= FileChangeEvent(FileChangeEventType.create,
541 							relativePath(e.name.absolutePath(cwd), path.absolutePath(cwd)));
542 				}
543 				if (events.length && events[0].type == FileChangeEventType.createSelf)
544 					return [events[0]];
545 				return events;
546 			}
547 			else
548 			{
549 				if (nowExists && !exists)
550 				{
551 					exists = true;
552 					timeLastModified = path.timeLastModified;
553 					return [FileChangeEvent(FileChangeEventType.createSelf, ".")];
554 				}
555 				else if (!nowExists && exists)
556 				{
557 					exists = false;
558 					return [FileChangeEvent(FileChangeEventType.removeSelf, ".")];
559 				}
560 				else if (nowExists)
561 				{
562 					const modTime = path.timeLastModified;
563 					if (modTime != timeLastModified)
564 					{
565 						timeLastModified = modTime;
566 						return [FileChangeEvent(FileChangeEventType.modify, path.baseName)];
567 					}
568 					else
569 						return [];
570 				}
571 				else
572 					return [];
573 			}
574 		}
575 	}
576 	else
577 		static assert(0, "No filesystem watching method?! Try setting version = FSWForcePoll;");
578 }
579 
580 ///
581 unittest
582 {
583 	import core.thread;
584 
585 	FileChangeEvent waitForEvent(ref FileWatch watcher)
586 	{
587 		FileChangeEvent[] ret;
588 		while ((ret = watcher.getEvents()).length == 0)
589 		{
590 			Thread.sleep(1.msecs);
591 		}
592 		return ret[0];
593 	}
594 
595 	if (exists("test"))
596 		rmdirRecurse("test");
597 	scope (exit)
598 	{
599 		if (exists("test"))
600 			rmdirRecurse("test");
601 	}
602 
603 	auto watcher = FileWatch("test", true);
604 	assert(watcher.path == "test");
605 	mkdir("test");
606 	auto ev = waitForEvent(watcher);
607 	assert(ev.type == FileChangeEventType.createSelf);
608 	write("test/a.txt", "abc");
609 	ev = waitForEvent(watcher);
610 	assert(ev.type == FileChangeEventType.create);
611 	assert(ev.path == "a.txt");
612 	Thread.sleep(2000.msecs); // for polling variant
613 	append("test/a.txt", "def");
614 	ev = waitForEvent(watcher);
615 	assert(ev.type == FileChangeEventType.modify);
616 	assert(ev.path == "a.txt");
617 	rename("test/a.txt", "test/b.txt");
618 	ev = waitForEvent(watcher);
619 	assert(ev.type == FileChangeEventType.rename);
620 	assert(ev.path == "a.txt");
621 	assert(ev.newPath == "b.txt");
622 	remove("test/b.txt");
623 	ev = waitForEvent(watcher);
624 	assert(ev.type == FileChangeEventType.remove);
625 	assert(ev.path == "b.txt");
626 	rmdirRecurse("test");
627 	ev = waitForEvent(watcher);
628 	assert(ev.type == FileChangeEventType.removeSelf);
629 }
630 
631 version (linux) unittest
632 {
633 	import core.thread;
634 
635 	FileChangeEvent waitForEvent(ref FileWatch watcher, Duration timeout = 2.seconds)
636 	{
637 		FileChangeEvent[] ret;
638 		Duration elapsed;
639 		while ((ret = watcher.getEvents()).length == 0)
640 		{
641 			Thread.sleep(1.msecs);
642 			elapsed += 1.msecs;
643 			if (elapsed >= timeout)
644 				throw new Exception("timeout");
645 		}
646 		return ret[0];
647 	}
648 
649 	if (exists("test2"))
650 		rmdirRecurse("test2");
651 	if (exists("test3"))
652 		rmdirRecurse("test3");
653 	scope (exit)
654 	{
655 		if (exists("test2"))
656 			rmdirRecurse("test2");
657 		if (exists("test3"))
658 			rmdirRecurse("test3");
659 	}
660 
661 	auto watcher = FileWatch("test2", true);
662 	mkdir("test2");
663 	auto ev = waitForEvent(watcher);
664 	assert(ev.type == FileChangeEventType.createSelf);
665 	write("test2/a.txt", "abc");
666 	ev = waitForEvent(watcher);
667 	assert(ev.type == FileChangeEventType.create);
668 	assert(ev.path == "a.txt");
669 	rename("test2/a.txt", "./testfile-a.txt");
670 	ev = waitForEvent(watcher);
671 	assert(ev.type == FileChangeEventType.remove);
672 	assert(ev.path == "a.txt");
673 	rename("./testfile-a.txt", "test2/b.txt");
674 	ev = waitForEvent(watcher);
675 	assert(ev.type == FileChangeEventType.create);
676 	assert(ev.path == "b.txt");
677 	remove("test2/b.txt");
678 	ev = waitForEvent(watcher);
679 	assert(ev.type == FileChangeEventType.remove);
680 	assert(ev.path == "b.txt");
681 
682 	mkdir("test2/mydir");
683 	rmdir("test2/mydir");
684 	try
685 	{
686 		ev = waitForEvent(watcher);
687 		// waitForEvent only returns first event (just a test method anyway) because on windows or unprecise platforms events can be spawned multiple times
688 		// or could be never fired in case of slow polling mechanism
689 		assert(ev.type == FileChangeEventType.create);
690 		assert(ev.path == "mydir");
691 	}
692 	catch (Exception e)
693 	{
694 		if (e.msg != "timeout")
695 			throw e;
696 	}
697 
698 	version (FSWUsesINotify)
699 	{
700 		// test for creation, modification, removal of subdirectory
701 		mkdir("test2/subdir");
702 		ev = waitForEvent(watcher);
703 		assert(ev.type == FileChangeEventType.create);
704 		assert(ev.path == "subdir");
705 		write("test2/subdir/c.txt", "abc");
706 		ev = waitForEvent(watcher);
707 		assert(ev.type == FileChangeEventType.create);
708 		assert(ev.path == "subdir/c.txt");
709 		write("test2/subdir/c.txt", "\nabc");
710 		ev = waitForEvent(watcher);
711 		assert(ev.type == FileChangeEventType.modify);
712 		assert(ev.path == "subdir/c.txt");
713 		rmdirRecurse("test2/subdir");
714 		auto events = watcher.getEvents();
715 		assert(events[0].type == FileChangeEventType.remove);
716 		assert(events[0].path == "subdir/c.txt");
717 		assert(events[1].type == FileChangeEventType.remove);
718 		assert(events[1].path == "subdir");
719 	}
720 	// removal of watched folder
721 	rmdirRecurse("test2");
722 	ev = waitForEvent(watcher);
723 	assert(ev.type == FileChangeEventType.removeSelf);
724 	assert(ev.path == ".");
725 
726 	version (FSWUsesINotify)
727 	{
728 		// test for a subdirectory already present
729 		// both when recursive = true and recursive = false
730 		foreach (recursive; [true, false])
731 		{
732 			mkdir("test3");
733 			mkdir("test3/a");
734 			mkdir("test3/a/b");
735 			watcher = FileWatch("test3", recursive);
736 			write("test3/a/b/c.txt", "abc");
737 			if (recursive)
738 			{
739 				ev = waitForEvent(watcher);
740 				assert(ev.type == FileChangeEventType.create);
741 				assert(ev.path == "a/b/c.txt");
742 			}
743 			if (!recursive)
744 			{
745 				// creation of subdirectory and file within
746 				// test that addWatch doesn't get called
747 				mkdir("test3/d");
748 				write("test3/d/e.txt", "abc");
749 				auto revents = watcher.getEvents();
750 				assert(revents.length == 1);
751 				assert(revents[0].type == FileChangeEventType.create);
752 				assert(revents[0].path == "d");
753 				rmdirRecurse("test3/d");
754 				revents = watcher.getEvents();
755 				assert(revents.length == 1);
756 				assert(revents[0].type == FileChangeEventType.remove);
757 				assert(revents[0].path == "d");
758 			}
759 			rmdirRecurse("test3");
760 			events = watcher.getEvents();
761 			if (recursive)
762 			{
763 				assert(events.length == 4);
764 				assert(events[0].type == FileChangeEventType.remove);
765 				assert(events[0].path == "a/b/c.txt");
766 				assert(events[1].type == FileChangeEventType.remove);
767 				assert(events[1].path == "a/b");
768 				assert(events[2].type == FileChangeEventType.remove);
769 				assert(events[2].path == "a");
770 				assert(events[3].type == FileChangeEventType.removeSelf);
771 				assert(events[3].path == ".");
772 			}
773 			else
774 			{
775 				assert(events.length == 2);
776 				assert(events[0].type == FileChangeEventType.remove);
777 				assert(events[0].path == "a");
778 				assert(events[1].type == FileChangeEventType.removeSelf);
779 				assert(events[1].path == ".");
780 			}
781 		}
782 	}
783 }