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