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 "shlwapi_undoc.h"
9 #include "CDirectoryWatcher.h"
10 #include <assert.h>      // for assert
11 
12 WINE_DEFAULT_DEBUG_CHANNEL(shcn);
13 
14 //////////////////////////////////////////////////////////////////////////////
15 
16 // notification target item
17 struct CWatchItem
18 {
19     UINT nRegID;        // The registration ID.
20     DWORD dwUserPID;    // The user PID; that is the process ID of the target window.
21     LPREGENTRY pRegEntry;   // The registration entry.
22     HWND hwndBroker;    // Client broker window (if any).
23     CDirectoryWatcher *pDirWatch; // for filesystem notification
24 };
25 
26 //////////////////////////////////////////////////////////////////////////////
27 // CChangeNotifyServer
28 //
29 // CChangeNotifyServer implements a window that handles all shell change notifications.
30 // It runs in the context of explorer and specifically in the thread of the shell desktop.
31 // Shell change notification api exported from shell32 forwards all their calls
32 // to this window where all processing takes place.
33 
34 class CChangeNotifyServer :
35     public CWindowImpl<CChangeNotifyServer, CWindow, CWorkerTraits>,
36     public CComObjectRootEx<CComMultiThreadModelNoCS>,
37     public IOleWindow
38 {
39 public:
40     CChangeNotifyServer();
41     virtual ~CChangeNotifyServer();
42     HRESULT Initialize();
43 
44     // *** IOleWindow methods ***
45     STDMETHOD(GetWindow)(HWND *lphwnd) override;
46     STDMETHOD(ContextSensitiveHelp)(BOOL fEnterMode) override;
47 
48     // Message handlers
49     LRESULT OnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
50     LRESULT OnUnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
51     LRESULT OnDeliverNotification(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
52     LRESULT OnSuspendResume(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
53     LRESULT OnRemoveByPID(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
54     LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
55 
56     DECLARE_NOT_AGGREGATABLE(CChangeNotifyServer)
57 
58     DECLARE_PROTECT_FINAL_CONSTRUCT()
59     BEGIN_COM_MAP(CChangeNotifyServer)
60         COM_INTERFACE_ENTRY_IID(IID_IOleWindow, IOleWindow)
61     END_COM_MAP()
62 
63     DECLARE_WND_CLASS_EX(L"WorkerW", 0, 0)
64 
65     BEGIN_MSG_MAP(CChangeNotifyServer)
66         MESSAGE_HANDLER(CN_REGISTER, OnRegister)
67         MESSAGE_HANDLER(CN_UNREGISTER, OnUnRegister)
68         MESSAGE_HANDLER(CN_DELIVER_NOTIFICATION, OnDeliverNotification)
69         MESSAGE_HANDLER(CN_SUSPEND_RESUME, OnSuspendResume)
70         MESSAGE_HANDLER(CN_UNREGISTER_PROCESS, OnRemoveByPID);
71         MESSAGE_HANDLER(WM_DESTROY, OnDestroy);
72     END_MSG_MAP()
73 
74 private:
75     UINT m_nNextRegID;
76     CSimpleArray<CWatchItem*> m_items;
77 
78     BOOL AddItem(CWatchItem *pItem);
79     BOOL RemoveItemsByRegID(UINT nRegID);
80     BOOL RemoveItemsByProcess(DWORD dwUserPID);
81     void DestroyItem(CWatchItem *pItem, HWND *phwndBroker);
82     void DestroyAllItems();
83 
84     UINT GetNextRegID();
85     BOOL DeliverNotification(HANDLE hTicket, DWORD dwOwnerPID);
86     BOOL ShouldNotify(LPDELITICKET pTicket, LPREGENTRY pRegEntry);
87 };
88 
CChangeNotifyServer()89 CChangeNotifyServer::CChangeNotifyServer()
90     : m_nNextRegID(INVALID_REG_ID)
91 {
92 }
93 
~CChangeNotifyServer()94 CChangeNotifyServer::~CChangeNotifyServer()
95 {
96 }
97 
AddItem(CWatchItem * pItem)98 BOOL CChangeNotifyServer::AddItem(CWatchItem *pItem)
99 {
100     // find the empty room
101     for (INT i = 0; i < m_items.GetSize(); ++i)
102     {
103         if (m_items[i] == NULL)
104         {
105             // found the room, populate it
106             m_items[i] = pItem;
107             return TRUE;
108         }
109     }
110 
111     // no empty room found
112     m_items.Add(pItem);
113     return TRUE;
114 }
115 
DestroyItem(CWatchItem * pItem,HWND * phwndBroker)116 void CChangeNotifyServer::DestroyItem(CWatchItem *pItem, HWND *phwndBroker)
117 {
118     assert(pItem);
119 
120     // destroy broker if any and if first time
121     HWND hwndBroker = pItem->hwndBroker;
122     pItem->hwndBroker = NULL;
123     if (hwndBroker && hwndBroker != *phwndBroker)
124     {
125         ::DestroyWindow(hwndBroker);
126         *phwndBroker = hwndBroker;
127     }
128 
129     // request termination of pDirWatch if any
130     CDirectoryWatcher *pDirWatch = pItem->pDirWatch;
131     pItem->pDirWatch = NULL;
132     if (pDirWatch)
133         pDirWatch->RequestTermination();
134 
135     // free
136     SHFree(pItem->pRegEntry);
137     delete pItem;
138 }
139 
DestroyAllItems()140 void CChangeNotifyServer::DestroyAllItems()
141 {
142     for (INT i = 0; i < m_items.GetSize(); ++i)
143     {
144         if (m_items[i])
145         {
146             HWND hwndBroker = NULL;
147             DestroyItem(m_items[i], &hwndBroker);
148             m_items[i] = NULL;
149         }
150     }
151     m_items.RemoveAll();
152 }
153 
RemoveItemsByRegID(UINT nRegID)154 BOOL CChangeNotifyServer::RemoveItemsByRegID(UINT nRegID)
155 {
156     BOOL bFound = FALSE;
157     HWND hwndBroker = NULL;
158     assert(nRegID != INVALID_REG_ID);
159     for (INT i = 0; i < m_items.GetSize(); ++i)
160     {
161         if (m_items[i] && m_items[i]->nRegID == nRegID)
162         {
163             bFound = TRUE;
164             DestroyItem(m_items[i], &hwndBroker);
165             m_items[i] = NULL;
166         }
167     }
168     return bFound;
169 }
170 
RemoveItemsByProcess(DWORD dwUserPID)171 BOOL CChangeNotifyServer::RemoveItemsByProcess(DWORD dwUserPID)
172 {
173     BOOL bFound = FALSE;
174     HWND hwndBroker = NULL;
175     assert(dwUserPID != 0);
176     for (INT i = 0; i < m_items.GetSize(); ++i)
177     {
178         if (m_items[i] && m_items[i]->dwUserPID == dwUserPID)
179         {
180             bFound = TRUE;
181             DestroyItem(m_items[i], &hwndBroker);
182             m_items[i] = NULL;
183         }
184     }
185     return bFound;
186 }
187 
188 // create a CDirectoryWatcher from a REGENTRY
189 static CDirectoryWatcher *
CreateDirectoryWatcherFromRegEntry(LPREGENTRY pRegEntry)190 CreateDirectoryWatcherFromRegEntry(LPREGENTRY pRegEntry)
191 {
192     if (pRegEntry->ibPidl == 0)
193         return NULL;
194 
195     // get the path
196     WCHAR szPath[MAX_PATH];
197     LPITEMIDLIST pidl = (LPITEMIDLIST)((LPBYTE)pRegEntry + pRegEntry->ibPidl);
198     if (!SHGetPathFromIDListW(pidl, szPath) || !PathIsDirectoryW(szPath))
199         return NULL;
200 
201     // create a CDirectoryWatcher
202     CDirectoryWatcher *pDirectoryWatcher =
203         CDirectoryWatcher::Create(pRegEntry->hwnd, szPath, pRegEntry->fRecursive);
204     if (pDirectoryWatcher == NULL)
205         return NULL;
206 
207     return pDirectoryWatcher;
208 }
209 
210 // Message CN_REGISTER: Register the registration entry.
211 //   wParam: The handle of registration entry.
212 //   lParam: The owner PID of registration entry.
213 //   return: TRUE if successful.
OnRegister(UINT uMsg,WPARAM wParam,LPARAM lParam,BOOL & bHandled)214 LRESULT CChangeNotifyServer::OnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
215 {
216     TRACE("OnRegister(%p, %u, %p, %p)\n", m_hWnd, uMsg, wParam, lParam);
217 
218     // lock the registration entry
219     HANDLE hRegEntry = (HANDLE)wParam;
220     DWORD dwOwnerPID = (DWORD)lParam;
221     LPREGENTRY pRegEntry = (LPREGENTRY)SHLockSharedEx(hRegEntry, dwOwnerPID, TRUE);
222     if (pRegEntry == NULL || pRegEntry->dwMagic != REGENTRY_MAGIC)
223     {
224         ERR("pRegEntry is invalid\n");
225         SHUnlockShared(pRegEntry);
226         return FALSE;
227     }
228 
229     // update registration ID if necessary
230     if (pRegEntry->nRegID == INVALID_REG_ID)
231         pRegEntry->nRegID = GetNextRegID();
232 
233     TRACE("pRegEntry->nRegID: %u\n", pRegEntry->nRegID);
234 
235     // get the user PID; that is the process ID of the target window
236     DWORD dwUserPID;
237     GetWindowThreadProcessId(pRegEntry->hwnd, &dwUserPID);
238 
239     // get broker if any
240     HWND hwndBroker = pRegEntry->hwndBroker;
241 
242     // clone the registration entry
243     LPREGENTRY pNewEntry = (LPREGENTRY)SHAlloc(pRegEntry->cbSize);
244     if (pNewEntry == NULL)
245     {
246         ERR("Out of memory\n");
247         pRegEntry->nRegID = INVALID_REG_ID;
248         SHUnlockShared(pRegEntry);
249         return FALSE;
250     }
251     CopyMemory(pNewEntry, pRegEntry, pRegEntry->cbSize);
252 
253     // create a directory watch if necessary
254     CDirectoryWatcher *pDirWatch = NULL;
255     if (pRegEntry->ibPidl && (pRegEntry->fSources & SHCNRF_InterruptLevel))
256     {
257         pDirWatch = CreateDirectoryWatcherFromRegEntry(pRegEntry);
258         if (pDirWatch && !pDirWatch->RequestAddWatcher())
259         {
260             ERR("RequestAddWatcher failed: %u\n", pRegEntry->nRegID);
261             pRegEntry->nRegID = INVALID_REG_ID;
262             SHUnlockShared(pRegEntry);
263             delete pDirWatch;
264             return FALSE;
265         }
266     }
267 
268     // unlock the registry entry
269     SHUnlockShared(pRegEntry);
270 
271     // add an item
272     CWatchItem *pItem = new CWatchItem { m_nNextRegID, dwUserPID, pNewEntry, hwndBroker, pDirWatch };
273     return AddItem(pItem);
274 }
275 
276 // Message CN_UNREGISTER: Unregister registration entries.
277 //   wParam: The registration ID.
278 //   lParam: Ignored.
279 //   return: TRUE if successful.
OnUnRegister(UINT uMsg,WPARAM wParam,LPARAM lParam,BOOL & bHandled)280 LRESULT CChangeNotifyServer::OnUnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
281 {
282     TRACE("OnUnRegister(%p, %u, %p, %p)\n", m_hWnd, uMsg, wParam, lParam);
283 
284     // validate registration ID
285     UINT nRegID = (UINT)wParam;
286     if (nRegID == INVALID_REG_ID)
287     {
288         ERR("INVALID_REG_ID\n");
289         return FALSE;
290     }
291 
292     // remove it
293     return RemoveItemsByRegID(nRegID);
294 }
295 
296 // Message CN_DELIVER_NOTIFICATION: Perform a delivery.
297 //   wParam: The handle of delivery ticket.
298 //   lParam: The owner PID of delivery ticket.
299 //   return: TRUE if necessary.
OnDeliverNotification(UINT uMsg,WPARAM wParam,LPARAM lParam,BOOL & bHandled)300 LRESULT CChangeNotifyServer::OnDeliverNotification(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
301 {
302     TRACE("OnDeliverNotification(%p, %u, %p, %p)\n", m_hWnd, uMsg, wParam, lParam);
303 
304     HANDLE hTicket = (HANDLE)wParam;
305     DWORD dwOwnerPID = (DWORD)lParam;
306 
307     // do delivery
308     BOOL ret = DeliverNotification(hTicket, dwOwnerPID);
309 
310     // free the ticket
311     SHFreeShared(hTicket, dwOwnerPID);
312     return ret;
313 }
314 
315 // Message CN_SUSPEND_RESUME: Suspend or resume the change notification.
316 //   (specification is unknown)
OnSuspendResume(UINT uMsg,WPARAM wParam,LPARAM lParam,BOOL & bHandled)317 LRESULT CChangeNotifyServer::OnSuspendResume(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
318 {
319     TRACE("OnSuspendResume\n");
320 
321     // FIXME
322     return FALSE;
323 }
324 
325 // Message CN_UNREGISTER_PROCESS: Remove registration entries by PID.
326 //   wParam: The user PID.
327 //   lParam: Ignored.
328 //   return: Zero.
OnRemoveByPID(UINT uMsg,WPARAM wParam,LPARAM lParam,BOOL & bHandled)329 LRESULT CChangeNotifyServer::OnRemoveByPID(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
330 {
331     DWORD dwUserPID = (DWORD)wParam;
332     RemoveItemsByProcess(dwUserPID);
333     return 0;
334 }
335 
OnDestroy(UINT uMsg,WPARAM wParam,LPARAM lParam,BOOL & bHandled)336 LRESULT CChangeNotifyServer::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
337 {
338     DestroyAllItems();
339     CDirectoryWatcher::RequestAllWatchersTermination();
340     return 0;
341 }
342 
343 // get next valid registration ID
GetNextRegID()344 UINT CChangeNotifyServer::GetNextRegID()
345 {
346     m_nNextRegID++;
347     if (m_nNextRegID == INVALID_REG_ID)
348         m_nNextRegID++;
349     return m_nNextRegID;
350 }
351 
352 // This function is called from CChangeNotifyServer::OnDeliverNotification.
353 // The function notifies to the registration entries that should be notified.
DeliverNotification(HANDLE hTicket,DWORD dwOwnerPID)354 BOOL CChangeNotifyServer::DeliverNotification(HANDLE hTicket, DWORD dwOwnerPID)
355 {
356     TRACE("DeliverNotification(%p, %p, 0x%lx)\n", m_hWnd, hTicket, dwOwnerPID);
357 
358     // lock the delivery ticket
359     LPDELITICKET pTicket = (LPDELITICKET)SHLockSharedEx(hTicket, dwOwnerPID, FALSE);
360     if (pTicket == NULL || pTicket->dwMagic != DELITICKET_MAGIC)
361     {
362         ERR("pTicket is invalid\n");
363         SHUnlockShared(pTicket);
364         return FALSE;
365     }
366 
367     // for all items
368     for (INT i = 0; i < m_items.GetSize(); ++i)
369     {
370         if (m_items[i] == NULL)
371             continue;
372 
373         LPREGENTRY pRegEntry = m_items[i]->pRegEntry;
374         if (pRegEntry == NULL || pRegEntry->dwMagic != REGENTRY_MAGIC)
375         {
376             ERR("pRegEntry is invalid\n");
377             continue;
378         }
379 
380         // should we notify for it?
381         BOOL bNotify = ShouldNotify(pTicket, pRegEntry);
382         if (bNotify)
383         {
384             // do notify
385             TRACE("Notifying: %p, 0x%x, %p, %lu\n",
386                   pRegEntry->hwnd, pRegEntry->uMsg, hTicket, dwOwnerPID);
387             SendMessageW(pRegEntry->hwnd, pRegEntry->uMsg, (WPARAM)hTicket, dwOwnerPID);
388             TRACE("GetLastError(): %ld\n", ::GetLastError());
389         }
390     }
391 
392     // unlock the ticket
393     SHUnlockShared(pTicket);
394 
395     return TRUE;
396 }
397 
ShouldNotify(LPDELITICKET pTicket,LPREGENTRY pRegEntry)398 BOOL CChangeNotifyServer::ShouldNotify(LPDELITICKET pTicket, LPREGENTRY pRegEntry)
399 {
400 #define RETURN(x) do { \
401     TRACE("ShouldNotify return %d\n", (x)); \
402     return (x); \
403 } while (0)
404 
405     if (pTicket->wEventId & SHCNE_INTERRUPT)
406     {
407         if (!(pRegEntry->fSources & SHCNRF_InterruptLevel))
408             RETURN(FALSE);
409         if (!pRegEntry->ibPidl)
410             RETURN(FALSE);
411     }
412     else
413     {
414         if (!(pRegEntry->fSources & SHCNRF_ShellLevel))
415             RETURN(FALSE);
416     }
417 
418     if (!(pTicket->wEventId & pRegEntry->fEvents))
419         RETURN(FALSE);
420 
421     LPITEMIDLIST pidl = NULL, pidl1 = NULL, pidl2 = NULL;
422     if (pRegEntry->ibPidl)
423         pidl = (LPITEMIDLIST)((LPBYTE)pRegEntry + pRegEntry->ibPidl);
424     if (pTicket->ibOffset1)
425         pidl1 = (LPITEMIDLIST)((LPBYTE)pTicket + pTicket->ibOffset1);
426     if (pTicket->ibOffset2)
427         pidl2 = (LPITEMIDLIST)((LPBYTE)pTicket + pTicket->ibOffset2);
428 
429     if (pidl == NULL || (pTicket->wEventId & SHCNE_GLOBALEVENTS))
430         RETURN(TRUE);
431 
432     if (pRegEntry->fRecursive)
433     {
434         if (ILIsParent(pidl, pidl1, FALSE) ||
435             (pidl2 && ILIsParent(pidl, pidl2, FALSE)))
436         {
437             RETURN(TRUE);
438         }
439     }
440     else
441     {
442         if (ILIsEqual(pidl, pidl1) ||
443             ILIsParent(pidl, pidl1, TRUE) ||
444             (pidl2 && ILIsParent(pidl, pidl2, TRUE)))
445         {
446             RETURN(TRUE);
447         }
448     }
449 
450     RETURN(FALSE);
451 #undef RETURN
452 }
453 
GetWindow(HWND * phwnd)454 HRESULT WINAPI CChangeNotifyServer::GetWindow(HWND* phwnd)
455 {
456     if (!phwnd)
457         return E_INVALIDARG;
458     *phwnd = m_hWnd;
459     return S_OK;
460 }
461 
ContextSensitiveHelp(BOOL fEnterMode)462 HRESULT WINAPI CChangeNotifyServer::ContextSensitiveHelp(BOOL fEnterMode)
463 {
464     return E_NOTIMPL;
465 }
466 
Initialize()467 HRESULT CChangeNotifyServer::Initialize()
468 {
469     // This is called by CChangeNotifyServer_CreateInstance right after instantiation.
470     HWND hwnd = SHCreateDefaultWorkerWindow();
471     if (!hwnd)
472         return E_FAIL;
473     SubclassWindow(hwnd);
474     return S_OK;
475 }
476 
CChangeNotifyServer_CreateInstance(REFIID riid,void ** ppv)477 HRESULT CChangeNotifyServer_CreateInstance(REFIID riid, void **ppv)
478 {
479     return ShellObjectCreatorInit<CChangeNotifyServer>(riid, ppv);
480 }
481