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, 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 }