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(LPCWSTR pszDirectoryPath, BOOL fSubTree)
72     : m_fDead(FALSE)
73     , m_fRecursive(fSubTree)
74     , m_dir_list(pszDirectoryPath, fSubTree)
75 {
76     TRACE("CDirectoryWatcher::CDirectoryWatcher: %p, '%S'\n", this, pszDirectoryPath);
77 
78     lstrcpynW(m_szDirectoryPath, pszDirectoryPath, MAX_PATH);
79 
80     // open the directory to watch changes (for ReadDirectoryChangesW)
81     m_hDirectory = CreateFileW(pszDirectoryPath, FILE_LIST_DIRECTORY,
82                                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
83                                NULL, OPEN_EXISTING,
84                                FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
85                                NULL);
86 }
87 
88 /*static*/ CDirectoryWatcher *
89 CDirectoryWatcher::Create(LPCWSTR pszDirectoryPath, BOOL fSubTree)
90 {
91     WCHAR szFullPath[MAX_PATH];
92     GetFullPathNameW(pszDirectoryPath, _countof(szFullPath), szFullPath, NULL);
93 
94     CDirectoryWatcher *pDirectoryWatcher = new CDirectoryWatcher(szFullPath, fSubTree);
95     if (pDirectoryWatcher->m_hDirectory == INVALID_HANDLE_VALUE)
96     {
97         ERR("CreateFileW failed\n");
98         delete pDirectoryWatcher;
99         pDirectoryWatcher = NULL;
100     }
101     return pDirectoryWatcher;
102 }
103 
104 CDirectoryWatcher::~CDirectoryWatcher()
105 {
106     TRACE("CDirectoryWatcher::~CDirectoryWatcher: %p, '%S'\n", this, m_szDirectoryPath);
107 
108     if (m_hDirectory != INVALID_HANDLE_VALUE)
109         CloseHandle(m_hDirectory);
110 }
111 
112 // convert the file action to an event
113 static DWORD
114 ConvertActionToEvent(DWORD Action, BOOL fDir)
115 {
116     switch (Action)
117     {
118         case FILE_ACTION_ADDED:
119             return (fDir ? SHCNE_MKDIR : SHCNE_CREATE);
120         case FILE_ACTION_REMOVED:
121             return (fDir ? SHCNE_RMDIR : SHCNE_DELETE);
122         case FILE_ACTION_MODIFIED:
123             return (fDir ? SHCNE_UPDATEDIR : SHCNE_UPDATEITEM);
124         case FILE_ACTION_RENAMED_OLD_NAME:
125             break;
126         case FILE_ACTION_RENAMED_NEW_NAME:
127             return (fDir ? SHCNE_RENAMEFOLDER : SHCNE_RENAMEITEM);
128         default:
129             break;
130     }
131     return 0;
132 }
133 
134 // Notify a filesystem notification using pDirectoryWatcher.
135 void CDirectoryWatcher::ProcessNotification()
136 {
137     PFILE_NOTIFY_INFORMATION pInfo = (PFILE_NOTIFY_INFORMATION)s_buffer;
138     WCHAR szName[MAX_PATH], szPath[MAX_PATH], szTempPath[MAX_PATH];
139     DWORD dwEvent, cbName;
140     BOOL fDir;
141     TRACE("CDirectoryWatcher::ProcessNotification: enter\n");
142 
143     // for each entry in s_buffer
144     szPath[0] = szTempPath[0] = 0;
145     for (;;)
146     {
147         // get name (relative from m_szDirectoryPath)
148         cbName = pInfo->FileNameLength;
149         if (sizeof(szName) - sizeof(UNICODE_NULL) < cbName)
150         {
151             ERR("pInfo->FileName is longer than szName\n");
152             break;
153         }
154         // NOTE: FILE_NOTIFY_INFORMATION.FileName is not null-terminated.
155         ZeroMemory(szName, sizeof(szName));
156         CopyMemory(szName, pInfo->FileName, cbName);
157 
158         // get full path
159         lstrcpynW(szPath, m_szDirectoryPath, _countof(szPath));
160         PathAppendW(szPath, szName);
161 
162         // convert to long pathname if it contains '~'
163         if (StrChrW(szPath, L'~') != NULL)
164         {
165             if (GetLongPathNameW(szPath, szName, _countof(szName)) &&
166                 !PathIsRelativeW(szName))
167             {
168                 lstrcpynW(szPath, szName, _countof(szPath));
169             }
170         }
171 
172         // convert action to event
173         fDir = PathIsDirectoryW(szPath);
174         dwEvent = ConvertActionToEvent(pInfo->Action, fDir);
175 
176         // convert SHCNE_DELETE to SHCNE_RMDIR if the path is a directory
177         if (!fDir && (dwEvent == SHCNE_DELETE) && m_dir_list.ContainsPath(szPath))
178         {
179             fDir = TRUE;
180             dwEvent = SHCNE_RMDIR;
181         }
182 
183         // update m_dir_list
184         switch (dwEvent)
185         {
186             case SHCNE_MKDIR:
187                 if (!PathIsDirectoryW(szPath) || !m_dir_list.AddPath(szPath))
188                     dwEvent = 0;
189                 break;
190             case SHCNE_CREATE:
191                 if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath))
192                     dwEvent = 0;
193                 break;
194             case SHCNE_RENAMEFOLDER:
195                 if (!PathIsDirectoryW(szPath) || !m_dir_list.RenamePath(szTempPath, szPath))
196                     dwEvent = 0;
197                 break;
198             case SHCNE_RENAMEITEM:
199                 if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath))
200                     dwEvent = 0;
201                 break;
202             case SHCNE_RMDIR:
203                 if (PathIsDirectoryW(szPath) || !m_dir_list.DeletePath(szPath))
204                     dwEvent = 0;
205                 break;
206             case SHCNE_DELETE:
207                 if (PathFileExistsW(szPath))
208                     dwEvent = 0;
209                 break;
210         }
211 
212         if (dwEvent != 0)
213         {
214             // notify
215             if (pInfo->Action == FILE_ACTION_RENAMED_NEW_NAME)
216                 NotifyFileSystemChange(dwEvent, szTempPath, szPath);
217             else
218                 NotifyFileSystemChange(dwEvent, szPath, NULL);
219         }
220         else if (pInfo->Action == FILE_ACTION_RENAMED_OLD_NAME)
221         {
222             // save path for next FILE_ACTION_RENAMED_NEW_NAME
223             lstrcpynW(szTempPath, szPath, MAX_PATH);
224         }
225 
226         if (pInfo->NextEntryOffset == 0)
227             break; // there is no next entry
228 
229         // go next entry
230         pInfo = (PFILE_NOTIFY_INFORMATION)((LPBYTE)pInfo + pInfo->NextEntryOffset);
231     }
232 
233     TRACE("CDirectoryWatcher::ProcessNotification: leave\n");
234 }
235 
236 void CDirectoryWatcher::ReadCompletion(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered)
237 {
238     // If the FSD doesn't support directory change notifications, there's no
239     // no need to retry and requeue notification
240     if (dwErrorCode == ERROR_INVALID_FUNCTION)
241     {
242         ERR("ERROR_INVALID_FUNCTION\n");
243         return;
244     }
245 
246     // Also, if the notify operation was canceled (like, user moved to another
247     // directory), then, don't requeue notification.
248     if (dwErrorCode == ERROR_OPERATION_ABORTED)
249     {
250         TRACE("ERROR_OPERATION_ABORTED\n");
251         if (IsDead())
252             delete this;
253         return;
254     }
255 
256     // is this watch dead?
257     if (IsDead())
258     {
259         TRACE("IsDead()\n");
260         delete this;
261         return;
262     }
263 
264     // This likely means overflow, so force whole directory refresh.
265     if (dwNumberOfBytesTransfered == 0)
266     {
267         // do notify a SHCNE_UPDATEDIR
268         NotifyFileSystemChange(SHCNE_UPDATEDIR, m_szDirectoryPath, NULL);
269     }
270     else
271     {
272         // do notify
273         ProcessNotification();
274     }
275 
276     // restart a watch
277     RestartWatching();
278 }
279 
280 // The completion routine of ReadDirectoryChangesW.
281 static void CALLBACK
282 _NotificationCompletion(DWORD dwErrorCode,
283                         DWORD dwNumberOfBytesTransfered,
284                         LPOVERLAPPED lpOverlapped)
285 {
286     // MSDN: The hEvent member of the OVERLAPPED structure is not used by the
287     // system in this case, so you can use it yourself. We do just this, storing
288     // a pointer to the working struct in the overlapped structure.
289     CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)lpOverlapped->hEvent;
290     assert(pDirectoryWatcher != NULL);
291 
292     pDirectoryWatcher->ReadCompletion(dwErrorCode, dwNumberOfBytesTransfered);
293 }
294 
295 // convert events to notification filter
296 static DWORD
297 GetFilterFromEvents(DWORD fEvents)
298 {
299     // FIXME
300     return (FILE_NOTIFY_CHANGE_FILE_NAME |
301             FILE_NOTIFY_CHANGE_DIR_NAME |
302             FILE_NOTIFY_CHANGE_CREATION |
303             FILE_NOTIFY_CHANGE_SIZE);
304 }
305 
306 // Restart a watch by using ReadDirectoryChangesW function
307 BOOL 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 
336 BOOL 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 
348 BOOL 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 
361 BOOL 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 
383 void CDirectoryWatcher::QuitWatching()
384 {
385     assert(this != NULL);
386 
387     m_fDead = TRUE;
388     CancelIo(m_hDirectory);
389 }
390 
391 BOOL CDirectoryWatcher::IsDead() const
392 {
393     return m_fDead;
394 }
395