1 /*
2 * PROJECT: shell32
3 * LICENSE: LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later)
4 * PURPOSE: Shell change notification
5 * COPYRIGHT: Copyright 2020 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
6 */
7 #include "shelldesktop.h"
8 #include "CDirectoryWatcher.h"
9 #include <process.h> // for _beginthreadex
10 #include <assert.h> // for assert
11
12 WINE_DEFAULT_DEBUG_CHANNEL(shcn);
13
14 // Notify filesystem change
15 static inline void
NotifyFileSystemChange(LONG wEventId,LPCWSTR path1,LPCWSTR path2)16 NotifyFileSystemChange(LONG wEventId, LPCWSTR path1, LPCWSTR path2)
17 {
18 SHChangeNotify(wEventId | SHCNE_INTERRUPT, SHCNF_PATHW | SHCNF_FLUSH, path1, path2);
19 }
20
21 // The handle of the APC thread
22 static HANDLE s_hThreadAPC = NULL;
23
24 // Terminate now?
25 static BOOL s_fTerminateAllWatchers = FALSE;
26
27 // the buffer for ReadDirectoryChangesW
28 #define BUFFER_SIZE 0x1000
29 static BYTE s_buffer[BUFFER_SIZE];
30
31 // The APC thread function for directory watch
DirectoryWatcherThreadFuncAPC(void *)32 static unsigned __stdcall DirectoryWatcherThreadFuncAPC(void *)
33 {
34 while (!s_fTerminateAllWatchers)
35 {
36 #if 1 // FIXME: This is a HACK
37 WaitForSingleObjectEx(GetCurrentThread(), INFINITE, TRUE);
38 #else
39 SleepEx(INFINITE, TRUE);
40 #endif
41 }
42 return 0;
43 }
44
45 // The APC procedure to add a CDirectoryWatcher and start the directory watch
_AddDirectoryProcAPC(ULONG_PTR Parameter)46 static void NTAPI _AddDirectoryProcAPC(ULONG_PTR Parameter)
47 {
48 CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)Parameter;
49 assert(pDirectoryWatcher != NULL);
50
51 pDirectoryWatcher->RestartWatching();
52 }
53
54 // The APC procedure to request termination of a CDirectoryWatcher
_RequestTerminationAPC(ULONG_PTR Parameter)55 static void NTAPI _RequestTerminationAPC(ULONG_PTR Parameter)
56 {
57 CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)Parameter;
58 assert(pDirectoryWatcher != NULL);
59
60 pDirectoryWatcher->QuitWatching();
61 }
62
63 // The APC procedure to request termination of all the directory watches
_RequestAllTerminationAPC(ULONG_PTR Parameter)64 static void NTAPI _RequestAllTerminationAPC(ULONG_PTR Parameter)
65 {
66 s_fTerminateAllWatchers = TRUE;
67 CloseHandle(s_hThreadAPC);
68 s_hThreadAPC = NULL;
69 }
70
CDirectoryWatcher(HWND hNotifyWnd,LPCWSTR pszDirectoryPath,BOOL fSubTree)71 CDirectoryWatcher::CDirectoryWatcher(HWND hNotifyWnd, LPCWSTR pszDirectoryPath, BOOL fSubTree)
72 : m_hNotifyWnd(hNotifyWnd)
73 , m_fDead(FALSE)
74 , m_fRecursive(fSubTree)
75 , m_dir_list(pszDirectoryPath, fSubTree)
76 {
77 TRACE("%p, '%S'\n", this, pszDirectoryPath);
78
79 GetFullPathNameW(pszDirectoryPath, _countof(m_szDirectoryPath), m_szDirectoryPath, NULL);
80
81 // open the directory to watch changes (for ReadDirectoryChangesW)
82 m_hDirectory = CreateFileW(m_szDirectoryPath, FILE_LIST_DIRECTORY,
83 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
84 NULL, OPEN_EXISTING,
85 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
86 NULL);
87 }
88
89 /*static*/ CDirectoryWatcher *
Create(HWND hNotifyWnd,LPCWSTR pszDirectoryPath,BOOL fSubTree)90 CDirectoryWatcher::Create(HWND hNotifyWnd, LPCWSTR pszDirectoryPath, BOOL fSubTree)
91 {
92 CDirectoryWatcher *pDirectoryWatcher =
93 new CDirectoryWatcher(hNotifyWnd, pszDirectoryPath, fSubTree);
94 if (pDirectoryWatcher->m_hDirectory == INVALID_HANDLE_VALUE)
95 {
96 ERR("CreateFileW failed\n");
97 delete pDirectoryWatcher;
98 pDirectoryWatcher = NULL;
99 }
100 return pDirectoryWatcher;
101 }
102
~CDirectoryWatcher()103 CDirectoryWatcher::~CDirectoryWatcher()
104 {
105 TRACE("%p, '%S'\n", this, m_szDirectoryPath);
106
107 if (m_hDirectory != INVALID_HANDLE_VALUE)
108 CloseHandle(m_hDirectory);
109 }
110
111 // convert the file action to an event
112 static DWORD
ConvertActionToEvent(DWORD Action,BOOL fDir)113 ConvertActionToEvent(DWORD Action, BOOL fDir)
114 {
115 switch (Action)
116 {
117 case FILE_ACTION_ADDED:
118 return (fDir ? SHCNE_MKDIR : SHCNE_CREATE);
119 case FILE_ACTION_REMOVED:
120 return (fDir ? SHCNE_RMDIR : SHCNE_DELETE);
121 case FILE_ACTION_MODIFIED:
122 return (fDir ? SHCNE_UPDATEDIR : SHCNE_UPDATEITEM);
123 case FILE_ACTION_RENAMED_OLD_NAME:
124 break;
125 case FILE_ACTION_RENAMED_NEW_NAME:
126 return (fDir ? SHCNE_RENAMEFOLDER : SHCNE_RENAMEITEM);
127 default:
128 break;
129 }
130 return 0;
131 }
132
133 // Notify a filesystem notification using pDirectoryWatcher.
ProcessNotification()134 void CDirectoryWatcher::ProcessNotification()
135 {
136 PFILE_NOTIFY_INFORMATION pInfo = (PFILE_NOTIFY_INFORMATION)s_buffer;
137 WCHAR szName[MAX_PATH], szPath[MAX_PATH], szTempPath[MAX_PATH];
138 DWORD dwEvent, cbName;
139 BOOL fDir;
140 TRACE("CDirectoryWatcher::ProcessNotification: enter\n");
141
142 // for each entry in s_buffer
143 szPath[0] = szTempPath[0] = 0;
144 for (;;)
145 {
146 // get name (relative from m_szDirectoryPath)
147 cbName = pInfo->FileNameLength;
148 if (sizeof(szName) - sizeof(UNICODE_NULL) < cbName)
149 {
150 ERR("pInfo->FileName is longer than szName\n");
151 break;
152 }
153 // NOTE: FILE_NOTIFY_INFORMATION.FileName is not null-terminated.
154 ZeroMemory(szName, sizeof(szName));
155 CopyMemory(szName, pInfo->FileName, cbName);
156
157 // get full path
158 lstrcpynW(szPath, m_szDirectoryPath, _countof(szPath));
159 PathAppendW(szPath, szName);
160
161 // convert to long pathname if it contains '~'
162 if (StrChrW(szPath, L'~') != NULL)
163 {
164 if (GetLongPathNameW(szPath, szName, _countof(szName)) &&
165 !PathIsRelativeW(szName))
166 {
167 lstrcpynW(szPath, szName, _countof(szPath));
168 }
169 }
170
171 // convert action to event
172 fDir = PathIsDirectoryW(szPath);
173 dwEvent = ConvertActionToEvent(pInfo->Action, fDir);
174
175 // convert SHCNE_DELETE to SHCNE_RMDIR if the path is a directory
176 if (!fDir && (dwEvent == SHCNE_DELETE) && m_dir_list.ContainsPath(szPath))
177 {
178 fDir = TRUE;
179 dwEvent = SHCNE_RMDIR;
180 }
181
182 // update m_dir_list
183 switch (dwEvent)
184 {
185 case SHCNE_MKDIR:
186 if (!PathIsDirectoryW(szPath) || !m_dir_list.AddPath(szPath))
187 dwEvent = 0;
188 break;
189 case SHCNE_CREATE:
190 if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath))
191 dwEvent = 0;
192 break;
193 case SHCNE_RENAMEFOLDER:
194 if (!PathIsDirectoryW(szPath) || !m_dir_list.RenamePath(szTempPath, szPath))
195 dwEvent = 0;
196 break;
197 case SHCNE_RENAMEITEM:
198 if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath))
199 dwEvent = 0;
200 break;
201 case SHCNE_RMDIR:
202 if (PathIsDirectoryW(szPath) || !m_dir_list.DeletePath(szPath))
203 dwEvent = 0;
204 break;
205 case SHCNE_DELETE:
206 if (PathFileExistsW(szPath))
207 dwEvent = 0;
208 break;
209 }
210
211 if (dwEvent != 0)
212 {
213 // notify
214 if (pInfo->Action == FILE_ACTION_RENAMED_NEW_NAME)
215 NotifyFileSystemChange(dwEvent, szTempPath, szPath);
216 else
217 NotifyFileSystemChange(dwEvent, szPath, NULL);
218 }
219 else if (pInfo->Action == FILE_ACTION_RENAMED_OLD_NAME)
220 {
221 // save path for next FILE_ACTION_RENAMED_NEW_NAME
222 lstrcpynW(szTempPath, szPath, MAX_PATH);
223 }
224
225 if (pInfo->NextEntryOffset == 0)
226 break; // there is no next entry
227
228 // go next entry
229 pInfo = (PFILE_NOTIFY_INFORMATION)((LPBYTE)pInfo + pInfo->NextEntryOffset);
230 }
231
232 TRACE("CDirectoryWatcher::ProcessNotification: leave\n");
233 }
234
ReadCompletion(DWORD dwErrorCode,DWORD dwNumberOfBytesTransfered)235 void CDirectoryWatcher::ReadCompletion(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered)
236 {
237 // If the FSD doesn't support directory change notifications, there's no
238 // no need to retry and requeue notification
239 if (dwErrorCode == ERROR_INVALID_FUNCTION)
240 {
241 ERR("ERROR_INVALID_FUNCTION\n");
242 return;
243 }
244
245 // Also, if the notify operation was canceled (like, user moved to another
246 // directory), then, don't requeue notification.
247 if (dwErrorCode == ERROR_OPERATION_ABORTED)
248 {
249 TRACE("ERROR_OPERATION_ABORTED\n");
250 if (IsDead())
251 delete this;
252 return;
253 }
254
255 // is this watch dead?
256 if (IsDead())
257 {
258 TRACE("IsDead()\n");
259 delete this;
260 return;
261 }
262
263 // This likely means overflow, so force whole directory refresh.
264 if (dwNumberOfBytesTransfered == 0)
265 {
266 // do notify a SHCNE_UPDATEDIR
267 NotifyFileSystemChange(SHCNE_UPDATEDIR, m_szDirectoryPath, NULL);
268 }
269 else
270 {
271 // do notify
272 ProcessNotification();
273 }
274
275 // restart a watch
276 RestartWatching();
277 }
278
279 // The completion routine of ReadDirectoryChangesW.
280 static void CALLBACK
_NotificationCompletion(DWORD dwErrorCode,DWORD dwNumberOfBytesTransfered,LPOVERLAPPED lpOverlapped)281 _NotificationCompletion(DWORD dwErrorCode,
282 DWORD dwNumberOfBytesTransfered,
283 LPOVERLAPPED lpOverlapped)
284 {
285 // MSDN: The hEvent member of the OVERLAPPED structure is not used by the
286 // system in this case, so you can use it yourself. We do just this, storing
287 // a pointer to the working struct in the overlapped structure.
288 CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)lpOverlapped->hEvent;
289 assert(pDirectoryWatcher != NULL);
290
291 pDirectoryWatcher->ReadCompletion(dwErrorCode, dwNumberOfBytesTransfered);
292 }
293
294 // convert events to notification filter
295 static DWORD
GetFilterFromEvents(DWORD fEvents)296 GetFilterFromEvents(DWORD fEvents)
297 {
298 // FIXME
299 return (FILE_NOTIFY_CHANGE_FILE_NAME |
300 FILE_NOTIFY_CHANGE_DIR_NAME |
301 FILE_NOTIFY_CHANGE_CREATION |
302 FILE_NOTIFY_CHANGE_SIZE);
303 }
304
305 // Restart a watch by using ReadDirectoryChangesW function
RestartWatching()306 BOOL CDirectoryWatcher::RestartWatching()
307 {
308 assert(this != NULL);
309
310 if (IsDead())
311 {
312 delete this;
313 return FALSE; // the watch is dead
314 }
315
316 // initialize the buffer and the overlapped
317 ZeroMemory(s_buffer, sizeof(s_buffer));
318 ZeroMemory(&m_overlapped, sizeof(m_overlapped));
319 m_overlapped.hEvent = (HANDLE)this;
320
321 // start the directory watch
322 DWORD dwFilter = GetFilterFromEvents(SHCNE_ALLEVENTS);
323 if (!ReadDirectoryChangesW(m_hDirectory, s_buffer, sizeof(s_buffer),
324 m_fRecursive, dwFilter, NULL,
325 &m_overlapped, _NotificationCompletion))
326 {
327 ERR("ReadDirectoryChangesW for '%S' failed (error: %ld)\n",
328 m_szDirectoryPath, GetLastError());
329 return FALSE; // failure
330 }
331
332 return TRUE; // success
333 }
334
CreateAPCThread()335 BOOL CDirectoryWatcher::CreateAPCThread()
336 {
337 if (s_hThreadAPC != NULL)
338 return TRUE;
339
340 unsigned tid;
341 s_fTerminateAllWatchers = FALSE;
342 s_hThreadAPC = (HANDLE)_beginthreadex(NULL, 0, DirectoryWatcherThreadFuncAPC,
343 NULL, 0, &tid);
344 return s_hThreadAPC != NULL;
345 }
346
RequestAddWatcher()347 BOOL CDirectoryWatcher::RequestAddWatcher()
348 {
349 assert(this != NULL);
350
351 // create an APC thread for directory watching
352 if (!CreateAPCThread())
353 return FALSE;
354
355 // request adding the watch
356 QueueUserAPC(_AddDirectoryProcAPC, s_hThreadAPC, (ULONG_PTR)this);
357 return TRUE;
358 }
359
RequestTermination()360 BOOL CDirectoryWatcher::RequestTermination()
361 {
362 assert(this != NULL);
363
364 if (s_hThreadAPC)
365 {
366 QueueUserAPC(_RequestTerminationAPC, s_hThreadAPC, (ULONG_PTR)this);
367 return TRUE;
368 }
369
370 return FALSE;
371 }
372
RequestAllWatchersTermination()373 /*static*/ void CDirectoryWatcher::RequestAllWatchersTermination()
374 {
375 if (!s_hThreadAPC)
376 return;
377
378 // request termination of all directory watches
379 QueueUserAPC(_RequestAllTerminationAPC, s_hThreadAPC, (ULONG_PTR)NULL);
380 }
381
QuitWatching()382 void CDirectoryWatcher::QuitWatching()
383 {
384 assert(this != NULL);
385
386 m_fDead = TRUE;
387 m_hNotifyWnd = NULL;
388 CancelIo(m_hDirectory);
389 }
390
IsDead()391 BOOL CDirectoryWatcher::IsDead()
392 {
393 if (m_hNotifyWnd && !::IsWindow(m_hNotifyWnd))
394 {
395 m_hNotifyWnd = NULL;
396 m_fDead = TRUE;
397 CancelIo(m_hDirectory);
398 }
399 return m_fDead;
400 }
401