1 module eventcore.drivers.winapi.watchers; 2 3 version (Windows): 4 5 import eventcore.driver; 6 import eventcore.drivers.winapi.core; 7 import eventcore.drivers.winapi.driver : WinAPIEventDriver; // FIXME: this is an ugly dependency 8 import eventcore.internal.win32; 9 import std.experimental.allocator.mallocator : Mallocator; 10 import std.experimental.allocator : dispose, makeArray; 11 12 13 final class WinAPIEventDriverWatchers : EventDriverWatchers { 14 @safe: /*@nogc:*/ nothrow: 15 private { 16 WinAPIEventDriverCore m_core; 17 } 18 19 this(WinAPIEventDriverCore core) 20 @nogc { 21 m_core = core; 22 } 23 24 override WatcherID watchDirectory(string path, bool recursive, FileChangesCallback callback) 25 { 26 import std.utf : toUTF16z; 27 28 auto handle = () @trusted { 29 scope (failure) assert(false); 30 return CreateFileW(path.toUTF16z, FILE_LIST_DIRECTORY, 31 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 32 null, OPEN_EXISTING, 33 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, 34 null); 35 } (); 36 37 if (handle == INVALID_HANDLE_VALUE) 38 return WatcherID.invalid; 39 40 auto slot = m_core.setupSlot!WatcherSlot(handle); 41 slot.directory = path; 42 slot.recursive = recursive; 43 slot.callback = callback; 44 slot.overlapped.driver = m_core; 45 slot.buffer = () @trusted { 46 try return Mallocator.instance.makeArray!ubyte(16384); 47 catch (Exception e) assert(false, "Failed to allocate directory watcher buffer."); 48 } (); 49 50 auto id = WatcherID(cast(size_t)handle, m_core.m_handles[handle].validationCounter); 51 52 if (!triggerRead(handle, *slot)) { 53 releaseRef(id); 54 return WatcherID.invalid; 55 } 56 57 // keep alive as long as the overlapped I/O operation is pending 58 addRef(id); 59 60 m_core.addWaiter(); 61 62 return id; 63 } 64 65 override bool isValid(WatcherID handle) 66 const { 67 if (auto ph = idToHandle(handle) in m_core.m_handles) 68 return ph.validationCounter == handle.validationCounter; 69 return false; 70 } 71 72 override void addRef(WatcherID descriptor) 73 { 74 if (!isValid(descriptor)) return; 75 76 m_core.m_handles[idToHandle(descriptor)].addRef(); 77 } 78 79 override bool releaseRef(WatcherID descriptor) 80 { 81 if (!isValid(descriptor)) return true; 82 83 return doReleaseRef(idToHandle(descriptor)); 84 } 85 86 protected override void* rawUserData(WatcherID descriptor, size_t size, DataInitializer initialize, DataInitializer destroy) 87 @system { 88 if (!isValid(descriptor)) return null; 89 90 return m_core.rawUserDataImpl(idToHandle(descriptor), size, initialize, destroy); 91 } 92 93 private static bool doReleaseRef(HANDLE handle) 94 { 95 auto core = WinAPIEventDriver.threadInstance.core; 96 auto slot = () @trusted { return &core.m_handles[handle]; } (); 97 98 if (!slot.releaseRef(() nothrow { 99 CloseHandle(handle); 100 101 () @trusted { 102 try Mallocator.instance.dispose(slot.watcher.buffer); 103 catch (Exception e) assert(false, "Freeing directory watcher buffer failed."); 104 } (); 105 slot.watcher.buffer = null; 106 core.discardEvents(&slot.watcher.overlapped); 107 core.freeSlot(handle); 108 })) 109 { 110 return false; 111 } 112 113 // If only one reference left, then this is the reference created for 114 // the current wait operation. Simply cancel the I/O to let the 115 // completion callback 116 if (slot.refCount == 1) { 117 () @trusted { CancelIoEx(handle, &slot.watcher.overlapped.overlapped); } (); 118 slot.watcher.callback = null; 119 core.removeWaiter(); 120 } 121 122 return true; 123 } 124 125 private static nothrow 126 void onIOCompleted(DWORD dwError, DWORD cbTransferred, OVERLAPPED_CORE* overlapped) 127 { 128 import std.algorithm.iteration : map; 129 import std.conv : to; 130 import std.file : isDir; 131 import std.path : dirName, baseName, buildPath; 132 133 auto handle = overlapped.hEvent; // *file* handle 134 auto gslot = () @trusted { return &WinAPIEventDriver.threadInstance.core.m_handles[handle]; } (); 135 auto slot = () @trusted { return &gslot.watcher(); } (); 136 auto id = WatcherID(cast(size_t)handle, gslot.validationCounter); 137 138 if (dwError != 0 || gslot.refCount == 1) { 139 // FIXME: error must be propagated to the caller (except for ABORTED 140 // errors) 141 //logWarn("Failed to read directory changes: %s", dwError); 142 doReleaseRef(handle); 143 return; 144 } 145 146 if (!slot.callback) return; 147 148 // NOTE: cbTransferred can be 0 if the buffer overflowed 149 ubyte[] result = slot.buffer[0 .. cbTransferred]; 150 while (result.length) { 151 assert(result.length >= FILE_NOTIFY_INFORMATION._FileName.offsetof); 152 auto fni = () @trusted { return cast(FILE_NOTIFY_INFORMATION*)result.ptr; } (); 153 auto fulllen = () @trusted { try return cast(ubyte*)&fni.FileName[fni.FileNameLength/2] - result.ptr; catch (Exception e) return size_t.max; } (); 154 if (fni.NextEntryOffset > result.length || fulllen > (fni.NextEntryOffset ? fni.NextEntryOffset : result.length)) { 155 import std.stdio : stderr; 156 () @trusted { 157 try stderr.writefln("ERROR: Invalid directory watcher event received: %s", *fni); 158 catch (Exception e) {} 159 } (); 160 break; 161 } 162 result = result[fni.NextEntryOffset .. $]; 163 164 FileChange ch; 165 switch (fni.Action) { 166 default: ch.kind = FileChangeKind.modified; break; 167 case 0x1: ch.kind = FileChangeKind.added; break; 168 case 0x2: ch.kind = FileChangeKind.removed; break; 169 case 0x3: ch.kind = FileChangeKind.modified; break; 170 case 0x4: ch.kind = FileChangeKind.removed; break; 171 case 0x5: ch.kind = FileChangeKind.added; break; 172 } 173 174 ch.baseDirectory = slot.directory; 175 string path; 176 try { 177 () @trusted { 178 path = fni.FileName[0 .. fni.FileNameLength/2].map!(ch => dchar(ch)).to!string; 179 } (); 180 } catch (Exception e) { 181 import std.stdio : stderr; 182 // NOTE: sometimes corrupted strings and invalid UTF-16 183 // surrogate pairs occur here, until the cause of this is 184 // found, the best alternative is to ignore those changes 185 () @trusted { 186 try stderr.writefln("Invalid path in directory change: %(%02X %)", cast(ushort[])fni.FileName[0 .. fni.FileNameLength/2]); 187 catch (Exception e) assert(false, e.msg); 188 } (); 189 if (fni.NextEntryOffset > 0) continue; 190 else break; 191 } 192 auto fullpath = buildPath(slot.directory, path); 193 ch.directory = dirName(path); 194 if (ch.directory == ".") ch.directory = ""; 195 ch.name = baseName(path); 196 slot.callback(id, ch); 197 if (fni.NextEntryOffset == 0 || !slot.callback) break; 198 } 199 200 if (slot.callback) 201 triggerRead(handle, *slot); 202 else if (gslot.refCount == 1) 203 doReleaseRef(handle); 204 } 205 206 private static bool triggerRead(HANDLE handle, ref WatcherSlot slot) 207 { 208 enum UINT notifications = FILE_NOTIFY_CHANGE_FILE_NAME| 209 FILE_NOTIFY_CHANGE_DIR_NAME|FILE_NOTIFY_CHANGE_SIZE| 210 FILE_NOTIFY_CHANGE_LAST_WRITE; 211 212 slot.overlapped.Internal = 0; 213 slot.overlapped.InternalHigh = 0; 214 slot.overlapped.Offset = 0; 215 slot.overlapped.OffsetHigh = 0; 216 slot.overlapped.hEvent = handle; 217 218 BOOL ret; 219 auto handler = &overlappedIOHandler!onIOCompleted; 220 () @trusted { 221 ret = ReadDirectoryChangesW(handle, slot.buffer.ptr, cast(DWORD)slot.buffer.length, slot.recursive, 222 notifications, null, &slot.overlapped.overlapped, handler); 223 } (); 224 225 if (!ret) { 226 //logError("Failed to read directory changes in '%s'", m_path); 227 return false; 228 } 229 230 return true; 231 } 232 233 static private HANDLE idToHandle(WatcherID id) @trusted @nogc { return cast(HANDLE)cast(size_t)id.value; } 234 }