Reactos
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
12WINE_DEFAULT_DEBUG_CHANNEL(shcn);
13
14// Notify filesystem change
15static inline void
16NotifyFileSystemChange(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
22static HANDLE s_hThreadAPC = NULL;
23
24// Terminate now?
25static BOOL s_fTerminateAllWatchers = FALSE;
26
27// the buffer for ReadDirectoryChangesW
28#define BUFFER_SIZE 0x1000
29static BYTE s_buffer[BUFFER_SIZE];
30
31// The APC thread function for directory watch
32static 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
46static 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
55static 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
64static void NTAPI _RequestAllTerminationAPC(ULONG_PTR Parameter)
65{
66 s_fTerminateAllWatchers = TRUE;
67 CloseHandle(s_hThreadAPC);
68 s_hThreadAPC = NULL;
69}
70
71CDirectoryWatcher::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 *
90CDirectoryWatcher::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
103CDirectoryWatcher::~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
112static DWORD
113ConvertActionToEvent(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.
134void 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
235void 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.
280static void CALLBACK
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
295static DWORD
296GetFilterFromEvents(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 FILE_NOTIFY_CHANGE_ATTRIBUTES);
304}
305
306// Restart a watch by using ReadDirectoryChangesW function
307BOOL CDirectoryWatcher::RestartWatching()
308{
309 assert(this != NULL);
310
311 if (IsDead())
312 {
313 delete this;
314 return FALSE; // the watch is dead
315 }
316
317 // initialize the buffer and the overlapped
318 ZeroMemory(s_buffer, sizeof(s_buffer));
319 ZeroMemory(&m_overlapped, sizeof(m_overlapped));
320 m_overlapped.hEvent = (HANDLE)this;
321
322 // start the directory watch
323 DWORD dwFilter = GetFilterFromEvents(SHCNE_ALLEVENTS);
324 if (!ReadDirectoryChangesW(m_hDirectory, s_buffer, sizeof(s_buffer),
325 m_fRecursive, dwFilter, NULL,
326 &m_overlapped, _NotificationCompletion))
327 {
328 ERR("ReadDirectoryChangesW for '%S' failed (error: %ld)\n",
329 m_szDirectoryPath, GetLastError());
330 return FALSE; // failure
331 }
332
333 return TRUE; // success
334}
335
336BOOL CDirectoryWatcher::CreateAPCThread()
337{
338 if (s_hThreadAPC != NULL)
339 return TRUE;
340
341 unsigned tid;
342 s_fTerminateAllWatchers = FALSE;
343 s_hThreadAPC = (HANDLE)_beginthreadex(NULL, 0, DirectoryWatcherThreadFuncAPC,
344 NULL, 0, &tid);
345 return s_hThreadAPC != NULL;
346}
347
348BOOL CDirectoryWatcher::RequestAddWatcher()
349{
350 assert(this != NULL);
351
352 // create an APC thread for directory watching
353 if (!CreateAPCThread())
354 return FALSE;
355
356 // request adding the watch
357 QueueUserAPC(_AddDirectoryProcAPC, s_hThreadAPC, (ULONG_PTR)this);
358 return TRUE;
359}
360
361BOOL CDirectoryWatcher::RequestTermination()
362{
363 assert(this != NULL);
364
365 if (s_hThreadAPC)
366 {
367 QueueUserAPC(_RequestTerminationAPC, s_hThreadAPC, (ULONG_PTR)this);
368 return TRUE;
369 }
370
371 return FALSE;
372}
373
374/*static*/ void CDirectoryWatcher::RequestAllWatchersTermination()
375{
376 if (!s_hThreadAPC)
377 return;
378
379 // request termination of all directory watches
380 QueueUserAPC(_RequestAllTerminationAPC, s_hThreadAPC, (ULONG_PTR)NULL);
381}
382
383void CDirectoryWatcher::QuitWatching()
384{
385 assert(this != NULL);
386
387 m_fDead = TRUE;
388 m_hNotifyWnd = NULL;
389 CancelIo(m_hDirectory);
390}
391
392BOOL CDirectoryWatcher::IsDead()
393{
394 if (m_hNotifyWnd && !::IsWindow(m_hNotifyWnd))
395 {
396 m_hNotifyWnd = NULL;
397 m_fDead = TRUE;
398 CancelIo(m_hDirectory);
399 }
400 return m_fDead;
401}