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
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
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
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
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
64 static void NTAPI _RequestAllTerminationAPC(ULONG_PTR Parameter)
65 {
66     s_fTerminateAllWatchers = TRUE;
67     CloseHandle(s_hThreadAPC);
68     s_hThreadAPC = NULL;
69 }
70 
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 *
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 
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
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.
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 
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
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
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
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 
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 
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 
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 
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 
382 void CDirectoryWatcher::QuitWatching()
383 {
384     assert(this != NULL);
385 
386     m_fDead = TRUE;
387     m_hNotifyWnd = NULL;
388     CancelIo(m_hDirectory);
389 }
390 
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