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 }