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