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