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