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 }