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