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 }