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 /// Path of the file set using the constructor 66 const string path; 67 68 version (FSWUsesWin32) 69 { 70 import core.sys.windows.windows : HANDLE, OVERLAPPED, CloseHandle, 71 GetOverlappedResult, CreateFile, GetLastError, 72 ReadDirectoryChangesW, FILE_NOTIFY_INFORMATION, FILE_ACTION_ADDED, 73 FILE_ACTION_REMOVED, FILE_ACTION_MODIFIED, 74 FILE_ACTION_RENAMED_NEW_NAME, FILE_ACTION_RENAMED_OLD_NAME, 75 FILE_LIST_DIRECTORY, FILE_SHARE_WRITE, FILE_SHARE_READ, 76 FILE_SHARE_DELETE, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 77 FILE_FLAG_BACKUP_SEMANTICS, FILE_NOTIFY_CHANGE_FILE_NAME, 78 FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_LAST_WRITE, 79 ERROR_IO_PENDING, ERROR_IO_INCOMPLETE; 80 import std.utf : toUTF8, toUTF16; 81 import std.path : absolutePath; 82 import std.conv : to; 83 import std.datetime : SysTime; 84 85 private HANDLE pathHandle; 86 private ubyte[1024 * 4] changeBuffer; // 4kb buffer for file changes 87 private bool isDir, exists, recursive; 88 private SysTime timeLastModified; 89 private size_t receivedBytes; 90 private OVERLAPPED overlapObj; 91 private bool queued; 92 93 /// Creates an instance using the Win32 API 94 this(string path, bool recursive = false, bool treatDirAsFile = false) 95 { 96 this.path = absolutePath(path, getcwd); 97 this.recursive = recursive; 98 isDir = !treatDirAsFile; 99 if (!isDir && recursive) 100 throw new Exception("Can't recursively check on a file"); 101 getEvents(); 102 } 103 104 ~this() 105 { 106 CloseHandle(pathHandle); 107 } 108 109 /// Implementation using Win32 API or polling for files 110 FileChangeEvent[] getEvents() 111 { 112 if (isDir && (!path.exists || path.isDir)) 113 { 114 if (!path.exists) 115 { 116 if (pathHandle) 117 { 118 if (GetOverlappedResult(pathHandle, &overlapObj, &receivedBytes, false)) 119 { 120 } 121 queued = false; 122 CloseHandle(pathHandle); 123 pathHandle = null; 124 return [FileChangeEvent(FileChangeEventType.removeSelf, ".")]; 125 } 126 return []; 127 } 128 FileChangeEvent[] events; 129 if (!pathHandle) 130 { 131 pathHandle = CreateFile((path.toUTF16 ~ cast(wchar) 0).ptr, FILE_LIST_DIRECTORY, 132 FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, 133 null, OPEN_EXISTING, 134 FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS, null); 135 if (!pathHandle) 136 throw new Exception("Error opening directory. Error code 0x" ~ GetLastError() 137 .to!string(16)); 138 queued = false; 139 events ~= FileChangeEvent(FileChangeEventType.createSelf, "."); 140 } 141 if (!queued) 142 { 143 if (!ReadDirectoryChangesW(pathHandle, changeBuffer.ptr, changeBuffer.length, recursive, 144 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE, 145 &receivedBytes, &overlapObj, null)) 146 throw new Exception("Failed to queue read. Error 0x" ~ GetLastError() 147 .to!string(16)); 148 queued = true; 149 } 150 else 151 { 152 if (GetOverlappedResult(pathHandle, &overlapObj, &receivedBytes, false)) 153 { 154 int i = 0; 155 string fromFilename; 156 while (true) 157 { 158 auto info = cast(FILE_NOTIFY_INFORMATION*)(changeBuffer.ptr + i); 159 string fileName = (cast(wchar[])( 160 cast(ubyte*) info.FileName)[0 .. info.FileNameLength]) 161 .toUTF8.idup; 162 switch (info.Action) 163 { 164 case FILE_ACTION_ADDED: 165 events ~= FileChangeEvent(FileChangeEventType.create, fileName); 166 break; 167 case FILE_ACTION_REMOVED: 168 events ~= FileChangeEvent(FileChangeEventType.remove, fileName); 169 break; 170 case FILE_ACTION_MODIFIED: 171 events ~= FileChangeEvent(FileChangeEventType.modify, fileName); 172 break; 173 case FILE_ACTION_RENAMED_OLD_NAME: 174 fromFilename = fileName; 175 break; 176 case FILE_ACTION_RENAMED_NEW_NAME: 177 events ~= FileChangeEvent(FileChangeEventType.rename, 178 fromFilename, fileName); 179 break; 180 default: 181 throw new Exception( 182 "Unknown file notify action 0x" ~ info.Action.to!string( 183 16)); 184 } 185 i += info.NextEntryOffset; 186 if (info.NextEntryOffset == 0) 187 break; 188 } 189 queued = false; 190 } 191 else if (GetLastError() != ERROR_IO_PENDING 192 && GetLastError() != ERROR_IO_INCOMPLETE) 193 throw new Exception("Error receiving changes. Error code 0x" ~ GetLastError() 194 .to!string(16)); 195 } 196 return events; 197 } 198 else 199 { 200 const nowExists = path.exists; 201 if (nowExists && !exists) 202 { 203 exists = true; 204 timeLastModified = path.timeLastModified; 205 return [FileChangeEvent(FileChangeEventType.createSelf, path)]; 206 } 207 else if (!nowExists && exists) 208 { 209 exists = false; 210 return [FileChangeEvent(FileChangeEventType.removeSelf, path)]; 211 } 212 else if (nowExists) 213 { 214 const modTime = path.timeLastModified; 215 if (modTime != timeLastModified) 216 { 217 timeLastModified = modTime; 218 return [FileChangeEvent(FileChangeEventType.modify, path)]; 219 } 220 else 221 return []; 222 } 223 else 224 return []; 225 } 226 } 227 } 228 else version (FSWUsesINotify) 229 { 230 import core.sys.linux.sys.inotify : inotify_rm_watch, inotify_init1, 231 inotify_add_watch, inotify_event, IN_CREATE, IN_DELETE, 232 IN_DELETE_SELF, IN_MODIFY, IN_MOVE_SELF, IN_MOVED_FROM, IN_MOVED_TO, 233 IN_NONBLOCK; 234 import core.sys.linux.unistd : close, read; 235 import core.sys.linux.errno : errno; 236 import core.sys.posix.poll : pollfd, poll, POLLIN; 237 import std.string : toStringz, fromStringz; 238 import std.conv : to; 239 240 private int fd, wd; 241 private ubyte[1024 * 4] eventBuffer; // 4kb buffer for events 242 private pollfd pfd; 243 244 /// Creates an instance using the linux inotify API 245 this(string path, bool ignored1 = false, bool ignored2 = false) 246 { 247 this.path = path; 248 getEvents(); 249 } 250 251 ~this() 252 { 253 if (fd) 254 { 255 inotify_rm_watch(fd, wd); 256 close(fd); 257 } 258 } 259 260 /// Implementation using inotify 261 FileChangeEvent[] getEvents() 262 { 263 FileChangeEvent[] events; 264 if (!fd && path.exists) 265 { 266 fd = inotify_init1(IN_NONBLOCK); 267 wd = inotify_add_watch(fd, path.toStringz, 268 IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY 269 | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO); 270 events ~= FileChangeEvent(FileChangeEventType.createSelf, "."); 271 } 272 if (!fd) 273 return events; 274 pfd.fd = fd; 275 pfd.events = POLLIN; 276 const code = poll(&pfd, 1, 0); 277 if (code < 0) 278 throw new Exception("Failed to poll events. Error code " ~ errno.to!string); 279 else if (code == 0) 280 return events; 281 else 282 { 283 const receivedBytes = read(fd, eventBuffer.ptr, eventBuffer.length); 284 int i = 0; 285 string fromFilename; 286 uint cookie; 287 while (true) 288 { 289 auto info = cast(inotify_event*)(eventBuffer.ptr + i); 290 assert(info.wd == wd); 291 // contains \0 at the end otherwise 292 string fileName = info.name.ptr.fromStringz().idup; 293 if ((info.mask & IN_CREATE) != 0) 294 events ~= FileChangeEvent(FileChangeEventType.create, fileName); 295 if ((info.mask & IN_DELETE) != 0) 296 events ~= FileChangeEvent(FileChangeEventType.remove, fileName); 297 if ((info.mask & IN_MODIFY) != 0) 298 events ~= FileChangeEvent(FileChangeEventType.modify, fileName); 299 if ((info.mask & IN_MOVED_FROM) != 0) 300 { 301 fromFilename = fileName; 302 cookie = info.cookie; 303 } 304 if ((info.mask & IN_MOVED_TO) != 0) 305 { 306 assert(info.cookie == cookie, "Cookies don't match"); 307 events ~= FileChangeEvent(FileChangeEventType.rename, 308 fromFilename, fileName); 309 cookie = 0; 310 } 311 if ((info.mask & IN_DELETE_SELF) != 0 || (info.mask & IN_MOVE_SELF) != 0) 312 { 313 if (fd) 314 { 315 inotify_rm_watch(fd, wd); 316 close(fd); 317 fd = wd = 0; 318 } 319 events ~= FileChangeEvent(FileChangeEventType.removeSelf, "."); 320 } 321 i += inotify_event.sizeof + info.len; 322 if (i >= receivedBytes || (cast(inotify_event*)(eventBuffer.ptr + i)).wd != wd) 323 break; 324 } 325 } 326 return events; 327 } 328 } 329 else version (FSWUsesPolling) 330 { 331 import std.datetime : SysTime; 332 import std.algorithm : countUntil, remove; 333 import std.path : relativePath, absolutePath, baseName; 334 335 private struct FileEntryCache 336 { 337 SysTime lastModification; 338 const string name; 339 bool isDirty; 340 ulong uniqueHash; 341 } 342 343 private FileEntryCache[] cache; 344 private bool isDir, recursive, exists; 345 private SysTime timeLastModified; 346 private string cwd; 347 348 /// Generic fallback implementation using std.file.dirEntries 349 this(string path, bool recursive = false, bool treatDirAsFile = false) 350 { 351 this.path = path; 352 cwd = getcwd; 353 this.recursive = recursive; 354 isDir = !treatDirAsFile; 355 if (!isDir && recursive) 356 throw new Exception("Can't recursively check on a file"); 357 getEvents(); 358 } 359 360 /// Generic polling implementation 361 FileChangeEvent[] getEvents() 362 { 363 const nowExists = path.exists; 364 if (isDir && (!nowExists || path.isDir)) 365 { 366 FileChangeEvent[] events; 367 if (nowExists && !exists) 368 { 369 exists = true; 370 events ~= FileChangeEvent(FileChangeEventType.createSelf, "."); 371 } 372 if (!nowExists && exists) 373 { 374 exists = false; 375 return [FileChangeEvent(FileChangeEventType.removeSelf, ".")]; 376 } 377 if (!nowExists) 378 return []; 379 foreach (ref e; cache) 380 e.isDirty = true; 381 DirEntry[] created; 382 foreach (file; dirEntries(path, recursive ? SpanMode.breadth : SpanMode.shallow)) 383 { 384 auto newCache = FileEntryCache(file.timeLastModified, 385 file.name, false, file.getUniqueHash); 386 bool found = false; 387 foreach (ref cacheEntry; cache) 388 { 389 if (cacheEntry.name == newCache.name) 390 { 391 if (cacheEntry.lastModification != newCache.lastModification) 392 { 393 cacheEntry.lastModification = newCache.lastModification; 394 events ~= FileChangeEvent(FileChangeEventType.modify, 395 relativePath(file.name.absolutePath(cwd), 396 path.absolutePath(cwd))); 397 } 398 cacheEntry.isDirty = false; 399 found = true; 400 break; 401 } 402 } 403 if (!found) 404 { 405 cache ~= newCache; 406 created ~= file; 407 } 408 } 409 foreach_reverse (i, ref e; cache) 410 { 411 if (e.isDirty) 412 { 413 auto idx = created.countUntil!((a, b) => a.getUniqueHash == b.uniqueHash)(e); 414 if (idx != -1) 415 { 416 events ~= FileChangeEvent(FileChangeEventType.rename, 417 relativePath(e.name.absolutePath(cwd), 418 path.absolutePath(cwd)), relativePath(created[idx].name.absolutePath(cwd), 419 path.absolutePath(cwd))); 420 created = created.remove(idx); 421 } 422 else 423 { 424 events ~= FileChangeEvent(FileChangeEventType.remove, 425 relativePath(e.name.absolutePath(cwd), path.absolutePath(cwd))); 426 } 427 cache = cache.remove(i); 428 } 429 } 430 foreach (ref e; created) 431 { 432 events ~= FileChangeEvent(FileChangeEventType.create, 433 relativePath(e.name.absolutePath(cwd), path.absolutePath(cwd))); 434 } 435 if (events.length && events[0].type == FileChangeEventType.createSelf) 436 return [events[0]]; 437 return events; 438 } 439 else 440 { 441 if (nowExists && !exists) 442 { 443 exists = true; 444 timeLastModified = path.timeLastModified; 445 return [FileChangeEvent(FileChangeEventType.createSelf, ".")]; 446 } 447 else if (!nowExists && exists) 448 { 449 exists = false; 450 return [FileChangeEvent(FileChangeEventType.removeSelf, ".")]; 451 } 452 else if (nowExists) 453 { 454 const modTime = path.timeLastModified; 455 if (modTime != timeLastModified) 456 { 457 timeLastModified = modTime; 458 return [FileChangeEvent(FileChangeEventType.modify, path.baseName)]; 459 } 460 else 461 return []; 462 } 463 else 464 return []; 465 } 466 } 467 } 468 else 469 static assert(0, "No filesystem watching method?! Try setting version = FSWForcePoll;"); 470 } 471 472 /// 473 unittest 474 { 475 import core.thread; 476 477 FileChangeEvent waitForEvent(ref FileWatch watcher) 478 { 479 FileChangeEvent[] ret; 480 while ((ret = watcher.getEvents()).length == 0) 481 { 482 Thread.sleep(1.msecs); 483 } 484 return ret[0]; 485 } 486 487 if (exists("test")) 488 rmdirRecurse("test"); 489 scope (exit) 490 { 491 if (exists("test")) 492 rmdirRecurse("test"); 493 } 494 495 auto watcher = FileWatch("test", true); 496 mkdir("test"); 497 auto ev = waitForEvent(watcher); 498 assert(ev.type == FileChangeEventType.createSelf); 499 write("test/a.txt", "abc"); 500 ev = waitForEvent(watcher); 501 assert(ev.type == FileChangeEventType.create); 502 assert(ev.path == "a.txt"); 503 Thread.sleep(2000.msecs); // for polling variant 504 append("test/a.txt", "def"); 505 ev = waitForEvent(watcher); 506 assert(ev.type == FileChangeEventType.modify); 507 assert(ev.path == "a.txt"); 508 rename("test/a.txt", "test/b.txt"); 509 ev = waitForEvent(watcher); 510 assert(ev.type == FileChangeEventType.rename); 511 assert(ev.path == "a.txt"); 512 assert(ev.newPath == "b.txt"); 513 remove("test/b.txt"); 514 ev = waitForEvent(watcher); 515 assert(ev.type == FileChangeEventType.remove); 516 assert(ev.path == "b.txt"); 517 rmdirRecurse("test"); 518 ev = waitForEvent(watcher); 519 assert(ev.type == FileChangeEventType.removeSelf); 520 }