1 /* Copyright (C) 2010 Wildfire Games.
2  *
3  * Permission is hereby granted, free of charge, to any person obtaining
4  * a copy of this software and associated documentation files (the
5  * "Software"), to deal in the Software without restriction, including
6  * without limitation the rights to use, copy, modify, merge, publish,
7  * distribute, sublicense, and/or sell copies of the Software, and to
8  * permit persons to whom the Software is furnished to do so, subject to
9  * the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included
12  * in all copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17  * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18  * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19  * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21  */
22 
23 /*
24  * Win32 directory change notification
25  */
26 
27 #include "precompiled.h"
28 #include "lib/sysdep/dir_watch.h"
29 
30 #include "lib/allocators/shared_ptr.h"
31 #include "lib/path.h"	// path_is_subpath
32 #include "lib/sysdep/os/win/win.h"
33 #include "lib/sysdep/os/win/winit.h"
34 #include "lib/sysdep/os/win/wutil.h"
35 #include "lib/sysdep/os/win/wiocp.h"
36 
37 
38 WINIT_REGISTER_MAIN_INIT(wdir_watch_Init);
39 WINIT_REGISTER_MAIN_SHUTDOWN(wdir_watch_Shutdown);
40 
41 
42 //-----------------------------------------------------------------------------
43 // DirHandle
44 
45 class DirHandle
46 {
47 public:
DirHandle(const OsPath & path)48 	DirHandle(const OsPath& path)
49 	{
50 		WinScopedPreserveLastError s;	// CreateFile
51 		const DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
52 		const DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED;
53 		m_hDir = CreateFileW(OsString(path).c_str(), FILE_LIST_DIRECTORY, share, 0, OPEN_EXISTING, flags, 0);
54 	}
55 
~DirHandle()56 	~DirHandle()
57 	{
58 		// contrary to MSDN, the canceled IOs do not issue a completion notification.
59 		// (receiving packets after (unsuccessful) cancellation would be dangerous)
60 		BOOL ok = CancelIo(m_hDir);
61 		WARN_IF_FALSE(ok);
62 
63 		CloseHandle(m_hDir);
64 		m_hDir = INVALID_HANDLE_VALUE;
65 	}
66 
67 	// == INVALID_HANDLE_VALUE if path doesn't exist
operator HANDLE() const68 	operator HANDLE() const
69 	{
70 		return m_hDir;
71 	}
72 
73 private:
74 	HANDLE m_hDir;
75 };
76 
77 
78 //-----------------------------------------------------------------------------
79 // DirWatchRequest
80 
81 class DirWatchRequest
82 {
83 	NONCOPYABLE(DirWatchRequest);
84 public:
DirWatchRequest(const OsPath & path)85 	DirWatchRequest(const OsPath& path)
86 		: m_path(path), m_dirHandle(path), m_data(new u8[dataSize])
87 	{
88 		m_ovl = (OVERLAPPED*)calloc(1, sizeof(OVERLAPPED));	// rationale for dynamic alloc: see decl
89 		ENSURE(m_ovl);
90 
91 		// (hEvent is needed for the wait after CancelIo below)
92 		const BOOL manualReset = TRUE;
93 		const BOOL initialState = FALSE;
94 		m_ovl->hEvent = CreateEvent(0, manualReset, initialState, 0);
95 	}
96 
~DirWatchRequest()97 	~DirWatchRequest()
98 	{
99 		// we need to free m_data here, so the pending IO had better
100 		// not write to that memory in future. therefore:
101 		WARN_IF_FALSE(CancelIo(m_dirHandle));
102 		// however, this is not synchronized with the DPC (?) that apparently
103 		// delivers the data - m_data is filled anyway.
104 		// we need to ensure that either the IO has happened or that it
105 		// was successfully canceled before freeing m_data and m_ovl, so wait:
106 		{
107 			WinScopedPreserveLastError s;
108 			// (GetOverlappedResult without a valid hEvent hangs on Vista;
109 			// we'll abort after a timeout to be safe.)
110 			const DWORD ret = WaitForSingleObject(m_ovl->hEvent, 1000);
111 			WARN_IF_FALSE(CloseHandle(m_ovl->hEvent));
112 			if(ret == WAIT_OBJECT_0 || GetLastError() == ERROR_OPERATION_ABORTED)
113 			{
114 				SetLastError(0);
115 				delete[] m_data;
116 				free(m_ovl);
117 			}
118 			else
119 			{
120 				// (this could conceivably happen if a kernel debugger
121 				// hangs the system during the wait duration.)
122 				debug_printf("WARNING: IO may still be pending; to avoid memory corruption, we won't free the buffer.\n");
123 				DEBUG_WARN_ERR(ERR::TIMED_OUT);
124 				// intentionally leak m_data and m_ovl!
125 			}
126 		}
127 	}
128 
Path() const129 	const OsPath& Path() const
130 	{
131 		return m_path;
132 	}
133 
AttachTo(HANDLE & hIOCP) const134 	void AttachTo(HANDLE& hIOCP) const
135 	{
136 		AttachToCompletionPort(m_dirHandle, hIOCP, (uintptr_t)this);
137 	}
138 
139 	// (called again after each notification, so it mustn't AttachToCompletionPort)
Issue()140 	Status Issue()
141 	{
142 		if(m_dirHandle == INVALID_HANDLE_VALUE)
143 			WARN_RETURN(ERR::PATH_NOT_FOUND);
144 
145 		const BOOL watchSubtree = TRUE;	// (see IntrusiveLink comments)
146 		const DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME |
147 			FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE |
148 			FILE_NOTIFY_CHANGE_CREATION;
149 		// not set: FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_LAST_ACCESS, FILE_NOTIFY_CHANGE_SECURITY
150 		DWORD undefined = 0;	// (non-NULL pointer avoids BoundsChecker warning)
151 		m_ovl->Internal = 0;
152 		WARN_IF_FALSE(ReadDirectoryChangesW(m_dirHandle, m_data, dataSize, watchSubtree, filter, &undefined, m_ovl, 0));
153 		return INFO::OK;
154 	}
155 
156 	/**
157 	 * (call when completion port indicates data is available)
158 	 **/
RetrieveNotifications(DirWatchNotifications & notifications) const159 	void RetrieveNotifications(DirWatchNotifications& notifications) const
160 	{
161 		const FILE_NOTIFY_INFORMATION* fni = (const FILE_NOTIFY_INFORMATION*)m_data;
162 		for(;;)
163 		{
164 			// convert (non-zero-terminated) BSTR to Path::String
165 			cassert(sizeof(wchar_t) == sizeof(WCHAR));
166 			const size_t length = fni->FileNameLength / sizeof(WCHAR);
167 			Path::String name(fni->FileName, length);
168 
169 			// (NB: name is actually a relative path since we watch entire subtrees)
170 			const OsPath pathname = m_path / name;
171 			const DirWatchNotification::EType type = TypeFromAction(fni->Action);
172 			notifications.push_back(DirWatchNotification(pathname, type));
173 
174 			if(!fni->NextEntryOffset)	// this was the last entry.
175 				break;
176 			fni = (const FILE_NOTIFY_INFORMATION*)(uintptr_t(fni) + fni->NextEntryOffset);
177 		}
178 	}
179 
180 private:
TypeFromAction(const DWORD action)181 	static DirWatchNotification::EType TypeFromAction(const DWORD action)
182 	{
183 		switch(action)
184 		{
185 		case FILE_ACTION_ADDED:
186 		case FILE_ACTION_RENAMED_NEW_NAME:
187 			return DirWatchNotification::Created;
188 
189 		case FILE_ACTION_REMOVED:
190 		case FILE_ACTION_RENAMED_OLD_NAME:
191 			return DirWatchNotification::Deleted;
192 
193 		case FILE_ACTION_MODIFIED:
194 			return DirWatchNotification::Changed;
195 
196 		default:
197 			DEBUG_WARN_ERR(ERR::LOGIC);
198 			return DirWatchNotification::Changed;
199 		}
200 	}
201 
202 	OsPath m_path;
203 	DirHandle m_dirHandle;
204 
205 	// rationale:
206 	// - if too small, notifications may be lost! (the CSD-poll application
207 	//   may be confronted with hundreds of new files in a short time frame)
208 	// - requests larger than 64 KiB fail on SMB due to packet restrictions.
209 	static const size_t dataSize = 64*KiB;
210 
211 	// rationale:
212 	// - each instance needs their own buffer. (we can't share a central
213 	//   copy because the watches are independent and may be triggered
214 	//   'simultaneously' before the next poll.)
215 	// - lifetime must be managed manually (see dtor)
216 	u8* m_data;
217 
218 	// rationale:
219 	// - ReadDirectoryChangesW's asynchronous mode is triggered by passing
220 	//   a valid OVERLAPPED parameter; notification proceeds via
221 	//   completion ports, but we still need hEvent - see above.
222 	// - this must remain valid while the IO is pending. if the wait
223 	//   were to fail, we must not free this memory, either.
224 	OVERLAPPED* m_ovl;
225 };
226 
227 typedef shared_ptr<DirWatchRequest> PDirWatchRequest;
228 
229 
230 //-----------------------------------------------------------------------------
231 // IntrusiveLink
232 
233 // using watches of entire subtrees to satisfy single-directory requests
234 // requires a list of existing watches. an intrusive, doubly-linked list
235 // is convenient because removal must occur within the DirWatch destructor.
236 // since boost::intrusive doesn't automatically remove objects from their
237 // containers when they are destroyed, we implement a simple circular list
238 // via sentinel. note that DirWatchManager iterates over DirWatch, not their
239 // embedded links. we map from link to the parent object via offsetof
240 // (slightly less complex than storing back pointers to the parents, and
241 // avoids 'this-pointer used during initialization list' warnings).
242 
243 class IntrusiveLink
244 {
245 public:
IntrusiveLink()246 	IntrusiveLink()
247 	{
248 		m_prev = m_next = this;	// sentinel
249 	}
250 
IntrusiveLink(IntrusiveLink * sentinel)251 	IntrusiveLink(IntrusiveLink* sentinel)
252 	{
253 		// insert after sentinel
254 		m_prev = sentinel;
255 		m_next = sentinel->m_next;
256 		m_next->m_prev = this;
257 		sentinel->m_next = this;
258 	}
259 
~IntrusiveLink()260 	~IntrusiveLink()
261 	{
262 		// remove from list
263 		m_prev->m_next = m_next;
264 		m_next->m_prev = m_prev;
265 	}
266 
Next() const267 	IntrusiveLink* Next() const
268 	{
269 		return m_next;
270 	}
271 
272 private:
273 	IntrusiveLink* m_prev;
274 	IntrusiveLink* m_next;
275 };
276 
277 
278 //-----------------------------------------------------------------------------
279 // DirWatch
280 
281 struct DirWatch
282 {
DirWatchDirWatch283 	DirWatch(IntrusiveLink* sentinel, const PDirWatchRequest& request)
284 		: link(sentinel), request(request)
285 	{
286 	}
287 
288 	IntrusiveLink link;
289 	PDirWatchRequest request;
290 };
291 
292 
293 //-----------------------------------------------------------------------------
294 // DirWatchManager
295 
296 class DirWatchManager
297 {
298 public:
DirWatchManager()299 	DirWatchManager()
300 		: hIOCP(0)	// Win32 requires 0-init; created in the first call to AttachTo
301 	{
302 	}
303 
~DirWatchManager()304 	~DirWatchManager()
305 	{
306 		CloseHandle(hIOCP);
307 	}
308 
Add(const OsPath & path,PDirWatch & dirWatch)309 	Status Add(const OsPath& path, PDirWatch& dirWatch)
310 	{
311 		ENSURE(path.IsDirectory());
312 
313 		// check if this is a subdirectory of a tree that's already being
314 		// watched (this is much faster than issuing a new watch; it also
315 		// prevents accidentally watching the same directory twice).
316 		for(IntrusiveLink* link = m_sentinel.Next(); link != &m_sentinel; link = link->Next())
317 		{
318 			DirWatch* const existingDirWatch = (DirWatch*)(uintptr_t(link) - offsetof(DirWatch, link));
319 			if(path_is_subpath(OsString(path).c_str(), OsString(existingDirWatch->request->Path()).c_str()))
320 			{
321 				dirWatch.reset(new DirWatch(&m_sentinel, existingDirWatch->request));
322 				return INFO::OK;
323 			}
324 		}
325 
326 		PDirWatchRequest request(new DirWatchRequest(path));
327 		request->AttachTo(hIOCP);
328 		RETURN_STATUS_IF_ERR(request->Issue());
329 		dirWatch.reset(new DirWatch(&m_sentinel, request));
330 		return INFO::OK;
331 	}
332 
Poll(DirWatchNotifications & notifications)333 	Status Poll(DirWatchNotifications& notifications)
334 	{
335 POLL_AGAIN:
336 		DWORD bytesTransferred; ULONG_PTR key; OVERLAPPED* ovl;
337 		const Status ret = PollCompletionPort(hIOCP, 0, bytesTransferred, key, ovl);
338 		if(ret == ERR::ABORTED)	// watch was canceled
339 			goto POLL_AGAIN;
340 		RETURN_STATUS_IF_ERR(ret);
341 
342 		DirWatchRequest* request = (DirWatchRequest*)key;
343 		request->RetrieveNotifications(notifications);
344 		RETURN_STATUS_IF_ERR(request->Issue());	// re-issue
345 		return INFO::OK;
346 	}
347 
348 private:
349 	IntrusiveLink m_sentinel;
350 	HANDLE hIOCP;
351 };
352 
353 static DirWatchManager* s_dirWatchManager;
354 
355 
356 //-----------------------------------------------------------------------------
357 
dir_watch_Add(const OsPath & path,PDirWatch & dirWatch)358 Status dir_watch_Add(const OsPath& path, PDirWatch& dirWatch)
359 {
360 	WinScopedLock lock(WDIR_WATCH_CS);
361 	return s_dirWatchManager->Add(path, dirWatch);
362 }
363 
dir_watch_Poll(DirWatchNotifications & notifications)364 Status dir_watch_Poll(DirWatchNotifications& notifications)
365 {
366 	WinScopedLock lock(WDIR_WATCH_CS);
367 	return s_dirWatchManager->Poll(notifications);
368 }
369 
370 
371 //-----------------------------------------------------------------------------
372 
wdir_watch_Init()373 static Status wdir_watch_Init()
374 {
375 	s_dirWatchManager = new DirWatchManager;
376 	return INFO::OK;
377 }
378 
wdir_watch_Shutdown()379 static Status wdir_watch_Shutdown()
380 {
381 	SAFE_DELETE(s_dirWatchManager);
382 	return INFO::OK;
383 }
384