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             GetLongPathNameW(szPath, szName, _countof(szName));
166             lstrcpynW(szPath, szName, _countof(szPath));
167         }
168 
169         // convert action to event
170         fDir = PathIsDirectoryW(szPath);
171         dwEvent = ConvertActionToEvent(pInfo->Action, fDir);
172 
173         // convert SHCNE_DELETE to SHCNE_RMDIR if the path is a directory
174         if (!fDir && (dwEvent == SHCNE_DELETE) && m_dir_list.ContainsPath(szPath))
175         {
176             fDir = TRUE;
177             dwEvent = SHCNE_RMDIR;
178         }
179 
180         // update m_dir_list
181         switch (dwEvent)
182         {
183             case SHCNE_MKDIR:
184                 if (!PathIsDirectoryW(szPath) || !m_dir_list.AddPath(szPath))
185                     dwEvent = 0;
186                 break;
187             case SHCNE_CREATE:
188                 if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath))
189                     dwEvent = 0;
190                 break;
191             case SHCNE_RENAMEFOLDER:
192                 if (!PathIsDirectoryW(szPath) || !m_dir_list.RenamePath(szTempPath, szPath))
193                     dwEvent = 0;
194                 break;
195             case SHCNE_RENAMEITEM:
196                 if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath))
197                     dwEvent = 0;
198                 break;
199             case SHCNE_RMDIR:
200                 if (PathIsDirectoryW(szPath) || !m_dir_list.DeletePath(szPath))
201                     dwEvent = 0;
202                 break;
203             case SHCNE_DELETE:
204                 if (PathFileExistsW(szPath))
205                     dwEvent = 0;
206                 break;
207         }
208 
209         if (dwEvent != 0)
210         {
211             // notify
212             if (pInfo->Action == FILE_ACTION_RENAMED_NEW_NAME)
213                 NotifyFileSystemChange(dwEvent, szTempPath, szPath);
214             else
215                 NotifyFileSystemChange(dwEvent, szPath, NULL);
216         }
217         else if (pInfo->Action == FILE_ACTION_RENAMED_OLD_NAME)
218         {
219             // save path for next FILE_ACTION_RENAMED_NEW_NAME
220             lstrcpynW(szTempPath, szPath, MAX_PATH);
221         }
222 
223         if (pInfo->NextEntryOffset == 0)
224             break; // there is no next entry
225 
226         // go next entry
227         pInfo = (PFILE_NOTIFY_INFORMATION)((LPBYTE)pInfo + pInfo->NextEntryOffset);
228     }
229 
230     TRACE("CDirectoryWatcher::ProcessNotification: leave\n");
231 }
232 
233 void CDirectoryWatcher::ReadCompletion(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered)
234 {
235     // If the FSD doesn't support directory change notifications, there's no
236     // no need to retry and requeue notification
237     if (dwErrorCode == ERROR_INVALID_FUNCTION)
238     {
239         ERR("ERROR_INVALID_FUNCTION\n");
240         return;
241     }
242 
243     // Also, if the notify operation was canceled (like, user moved to another
244     // directory), then, don't requeue notification.
245     if (dwErrorCode == ERROR_OPERATION_ABORTED)
246     {
247         TRACE("ERROR_OPERATION_ABORTED\n");
248         if (IsDead())
249             delete this;
250         return;
251     }
252 
253     // is this watch dead?
254     if (IsDead())
255     {
256         TRACE("IsDead()\n");
257         delete this;
258         return;
259     }
260 
261     // This likely means overflow, so force whole directory refresh.
262     if (dwNumberOfBytesTransfered == 0)
263     {
264         // do notify a SHCNE_UPDATEDIR
265         NotifyFileSystemChange(SHCNE_UPDATEDIR, m_szDirectoryPath, NULL);
266     }
267     else
268     {
269         // do notify
270         ProcessNotification();
271     }
272 
273     // restart a watch
274     RestartWatching();
275 }
276 
277 // The completion routine of ReadDirectoryChangesW.
278 static void CALLBACK
279 _NotificationCompletion(DWORD dwErrorCode,
280                         DWORD dwNumberOfBytesTransfered,
281                         LPOVERLAPPED lpOverlapped)
282 {
283     // MSDN: The hEvent member of the OVERLAPPED structure is not used by the
284     // system in this case, so you can use it yourself. We do just this, storing
285     // a pointer to the working struct in the overlapped structure.
286     CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)lpOverlapped->hEvent;
287     assert(pDirectoryWatcher != NULL);
288 
289     pDirectoryWatcher->ReadCompletion(dwErrorCode, dwNumberOfBytesTransfered);
290 }
291 
292 // convert events to notification filter
293 static DWORD
294 GetFilterFromEvents(DWORD fEvents)
295 {
296     // FIXME
297     return (FILE_NOTIFY_CHANGE_FILE_NAME |
298             FILE_NOTIFY_CHANGE_DIR_NAME |
299             FILE_NOTIFY_CHANGE_CREATION |
300             FILE_NOTIFY_CHANGE_SIZE);
301 }
302 
303 // Restart a watch by using ReadDirectoryChangesW function
304 BOOL CDirectoryWatcher::RestartWatching()
305 {
306     assert(this != NULL);
307 
308     if (IsDead())
309     {
310         delete this;
311         return FALSE; // the watch is dead
312     }
313 
314     // initialize the buffer and the overlapped
315     ZeroMemory(s_buffer, sizeof(s_buffer));
316     ZeroMemory(&m_overlapped, sizeof(m_overlapped));
317     m_overlapped.hEvent = (HANDLE)this;
318 
319     // start the directory watch
320     DWORD dwFilter = GetFilterFromEvents(SHCNE_ALLEVENTS);
321     if (!ReadDirectoryChangesW(m_hDirectory, s_buffer, sizeof(s_buffer),
322                                m_fRecursive, dwFilter, NULL,
323                                &m_overlapped, _NotificationCompletion))
324     {
325         ERR("ReadDirectoryChangesW for '%S' failed (error: %ld)\n",
326             m_szDirectoryPath, GetLastError());
327         return FALSE; // failure
328     }
329 
330     return TRUE; // success
331 }
332 
333 BOOL CDirectoryWatcher::CreateAPCThread()
334 {
335     if (s_hThreadAPC != NULL)
336         return TRUE;
337 
338     unsigned tid;
339     s_fTerminateAllWatchers = FALSE;
340     s_hThreadAPC = (HANDLE)_beginthreadex(NULL, 0, DirectoryWatcherThreadFuncAPC,
341                                           NULL, 0, &tid);
342     return s_hThreadAPC != NULL;
343 }
344 
345 BOOL CDirectoryWatcher::RequestAddWatcher()
346 {
347     assert(this != NULL);
348 
349     // create an APC thread for directory watching
350     if (!CreateAPCThread())
351         return FALSE;
352 
353     // request adding the watch
354     QueueUserAPC(_AddDirectoryProcAPC, s_hThreadAPC, (ULONG_PTR)this);
355     return TRUE;
356 }
357 
358 BOOL CDirectoryWatcher::RequestTermination()
359 {
360     assert(this != NULL);
361 
362     if (s_hThreadAPC)
363     {
364         QueueUserAPC(_RequestTerminationAPC, s_hThreadAPC, (ULONG_PTR)this);
365         return TRUE;
366     }
367 
368     return FALSE;
369 }
370 
371 /*static*/ void CDirectoryWatcher::RequestAllWatchersTermination()
372 {
373     if (!s_hThreadAPC)
374         return;
375 
376     // request termination of all directory watches
377     QueueUserAPC(_RequestAllTerminationAPC, s_hThreadAPC, (ULONG_PTR)NULL);
378 }
379 
380 void CDirectoryWatcher::QuitWatching()
381 {
382     assert(this != NULL);
383 
384     m_fDead = TRUE;
385     CancelIo(m_hDirectory);
386 }
387 
388 BOOL CDirectoryWatcher::IsDead() const
389 {
390     return m_fDead;
391 }
392