/* * PROJECT: shell32 * LICENSE: LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later) * PURPOSE: Shell change notification * COPYRIGHT: Copyright 2020 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com) */ #include "shelldesktop.h" #include "shlwapi_undoc.h" #include "CDirectoryWatcher.h" #include // for assert WINE_DEFAULT_DEBUG_CHANNEL(shcn); ////////////////////////////////////////////////////////////////////////////// // notification target item struct ITEM { UINT nRegID; // The registration ID. DWORD dwUserPID; // The user PID; that is the process ID of the target window. HANDLE hRegEntry; // The registration entry. HWND hwndBroker; // Client broker window (if any). CDirectoryWatcher *pDirWatch; // for filesystem notification }; typedef CWinTraits < WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, WS_EX_TOOLWINDOW > CChangeNotifyServerTraits; ////////////////////////////////////////////////////////////////////////////// // CChangeNotifyServer // // CChangeNotifyServer implements a window that handles all shell change notifications. // It runs in the context of explorer and specifically in the thread of the shell desktop. // Shell change notification api exported from shell32 forwards all their calls // to this window where all processing takes place. class CChangeNotifyServer : public CWindowImpl, public CComObjectRootEx, public IOleWindow { public: CChangeNotifyServer(); virtual ~CChangeNotifyServer(); HRESULT Initialize(); // *** IOleWindow methods *** virtual HRESULT STDMETHODCALLTYPE GetWindow(HWND *lphwnd); virtual HRESULT STDMETHODCALLTYPE ContextSensitiveHelp(BOOL fEnterMode); // Message handlers LRESULT OnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); LRESULT OnUnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); LRESULT OnDeliverNotification(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); LRESULT OnSuspendResume(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); LRESULT OnRemoveByPID(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); DECLARE_NOT_AGGREGATABLE(CChangeNotifyServer) DECLARE_PROTECT_FINAL_CONSTRUCT() BEGIN_COM_MAP(CChangeNotifyServer) COM_INTERFACE_ENTRY_IID(IID_IOleWindow, IOleWindow) END_COM_MAP() DECLARE_WND_CLASS_EX(L"WorkerW", 0, 0) BEGIN_MSG_MAP(CChangeNotifyServer) MESSAGE_HANDLER(CN_REGISTER, OnRegister) MESSAGE_HANDLER(CN_UNREGISTER, OnUnRegister) MESSAGE_HANDLER(CN_DELIVER_NOTIFICATION, OnDeliverNotification) MESSAGE_HANDLER(CN_SUSPEND_RESUME, OnSuspendResume) MESSAGE_HANDLER(CN_UNREGISTER_PROCESS, OnRemoveByPID); MESSAGE_HANDLER(WM_DESTROY, OnDestroy); END_MSG_MAP() private: UINT m_nNextRegID; CSimpleArray m_items; BOOL AddItem(UINT nRegID, DWORD dwUserPID, HANDLE hRegEntry, HWND hwndBroker, CDirectoryWatcher *pDirWatch); BOOL RemoveItemsByRegID(UINT nRegID, DWORD dwOwnerPID); void RemoveItemsByProcess(DWORD dwOwnerPID, DWORD dwUserPID); void DestroyItem(ITEM& item, DWORD dwOwnerPID, HWND *phwndBroker); UINT GetNextRegID(); BOOL DeliverNotification(HANDLE hTicket, DWORD dwOwnerPID); BOOL ShouldNotify(LPDELITICKET pTicket, LPREGENTRY pRegEntry); }; CChangeNotifyServer::CChangeNotifyServer() : m_nNextRegID(INVALID_REG_ID) { } CChangeNotifyServer::~CChangeNotifyServer() { } BOOL CChangeNotifyServer::AddItem(UINT nRegID, DWORD dwUserPID, HANDLE hRegEntry, HWND hwndBroker, CDirectoryWatcher *pDirWatch) { // find the empty room for (INT i = 0; i < m_items.GetSize(); ++i) { if (m_items[i].nRegID == INVALID_REG_ID) { // found the room, populate it m_items[i].nRegID = nRegID; m_items[i].dwUserPID = dwUserPID; m_items[i].hRegEntry = hRegEntry; m_items[i].hwndBroker = hwndBroker; m_items[i].pDirWatch = pDirWatch; return TRUE; } } // no empty room found ITEM item = { nRegID, dwUserPID, hRegEntry, hwndBroker, pDirWatch }; m_items.Add(item); return TRUE; } void CChangeNotifyServer::DestroyItem(ITEM& item, DWORD dwOwnerPID, HWND *phwndBroker) { // destroy broker if any and if first time HWND hwndBroker = item.hwndBroker; item.hwndBroker = NULL; if (hwndBroker && hwndBroker != *phwndBroker) { ::DestroyWindow(hwndBroker); *phwndBroker = hwndBroker; } // request termination of pDirWatch if any CDirectoryWatcher *pDirWatch = item.pDirWatch; item.pDirWatch = NULL; if (pDirWatch) pDirWatch->RequestTermination(); // free SHFreeShared(item.hRegEntry, dwOwnerPID); item.nRegID = INVALID_REG_ID; item.dwUserPID = 0; item.hRegEntry = NULL; item.hwndBroker = NULL; item.pDirWatch = NULL; } BOOL CChangeNotifyServer::RemoveItemsByRegID(UINT nRegID, DWORD dwOwnerPID) { BOOL bFound = FALSE; HWND hwndBroker = NULL; assert(nRegID != INVALID_REG_ID); for (INT i = 0; i < m_items.GetSize(); ++i) { if (m_items[i].nRegID == nRegID) { bFound = TRUE; DestroyItem(m_items[i], dwOwnerPID, &hwndBroker); } } return bFound; } void CChangeNotifyServer::RemoveItemsByProcess(DWORD dwOwnerPID, DWORD dwUserPID) { HWND hwndBroker = NULL; assert(dwUserPID != 0); for (INT i = 0; i < m_items.GetSize(); ++i) { if (m_items[i].dwUserPID == dwUserPID) { DestroyItem(m_items[i], dwOwnerPID, &hwndBroker); } } } // create a CDirectoryWatcher from a REGENTRY static CDirectoryWatcher * CreateDirectoryWatcherFromRegEntry(LPREGENTRY pRegEntry) { if (pRegEntry->ibPidl == 0) return NULL; // get the path WCHAR szPath[MAX_PATH]; LPITEMIDLIST pidl = (LPITEMIDLIST)((LPBYTE)pRegEntry + pRegEntry->ibPidl); if (!SHGetPathFromIDListW(pidl, szPath) || !PathIsDirectoryW(szPath)) return NULL; // create a CDirectoryWatcher CDirectoryWatcher *pDirectoryWatcher = CDirectoryWatcher::Create(szPath, pRegEntry->fRecursive); if (pDirectoryWatcher == NULL) return NULL; return pDirectoryWatcher; } // Message CN_REGISTER: Register the registration entry. // wParam: The handle of registration entry. // lParam: The owner PID of registration entry. // return: TRUE if successful. LRESULT CChangeNotifyServer::OnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { TRACE("OnRegister(%p, %u, %p, %p)\n", m_hWnd, uMsg, wParam, lParam); // lock the registration entry HANDLE hRegEntry = (HANDLE)wParam; DWORD dwOwnerPID = (DWORD)lParam; LPREGENTRY pRegEntry = (LPREGENTRY)SHLockSharedEx(hRegEntry, dwOwnerPID, TRUE); if (pRegEntry == NULL || pRegEntry->dwMagic != REGENTRY_MAGIC) { ERR("pRegEntry is invalid\n"); SHUnlockShared(pRegEntry); return FALSE; } // update registration ID if necessary if (pRegEntry->nRegID == INVALID_REG_ID) pRegEntry->nRegID = GetNextRegID(); TRACE("pRegEntry->nRegID: %u\n", pRegEntry->nRegID); // get the user PID; that is the process ID of the target window DWORD dwUserPID; GetWindowThreadProcessId(pRegEntry->hwnd, &dwUserPID); // get broker if any HWND hwndBroker = pRegEntry->hwndBroker; // clone the registration entry HANDLE hNewEntry = SHAllocShared(pRegEntry, pRegEntry->cbSize, dwOwnerPID); if (hNewEntry == NULL) { ERR("Out of memory\n"); pRegEntry->nRegID = INVALID_REG_ID; SHUnlockShared(pRegEntry); return FALSE; } // create a directory watch if necessary CDirectoryWatcher *pDirWatch = NULL; if (pRegEntry->ibPidl && (pRegEntry->fSources & SHCNRF_InterruptLevel)) { pDirWatch = CreateDirectoryWatcherFromRegEntry(pRegEntry); if (pDirWatch && !pDirWatch->RequestAddWatcher()) { ERR("RequestAddWatcher failed: %u\n", pRegEntry->nRegID); pRegEntry->nRegID = INVALID_REG_ID; SHUnlockShared(pRegEntry); delete pDirWatch; return FALSE; } } // unlock the registry entry SHUnlockShared(pRegEntry); // add an ITEM return AddItem(m_nNextRegID, dwUserPID, hNewEntry, hwndBroker, pDirWatch); } // Message CN_UNREGISTER: Unregister registration entries. // wParam: The registration ID. // lParam: Ignored. // return: TRUE if successful. LRESULT CChangeNotifyServer::OnUnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { TRACE("OnUnRegister(%p, %u, %p, %p)\n", m_hWnd, uMsg, wParam, lParam); // validate registration ID UINT nRegID = (UINT)wParam; if (nRegID == INVALID_REG_ID) { ERR("INVALID_REG_ID\n"); return FALSE; } // remove it DWORD dwOwnerPID; GetWindowThreadProcessId(m_hWnd, &dwOwnerPID); return RemoveItemsByRegID(nRegID, dwOwnerPID); } // Message CN_DELIVER_NOTIFICATION: Perform a delivery. // wParam: The handle of delivery ticket. // lParam: The owner PID of delivery ticket. // return: TRUE if necessary. LRESULT CChangeNotifyServer::OnDeliverNotification(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { TRACE("OnDeliverNotification(%p, %u, %p, %p)\n", m_hWnd, uMsg, wParam, lParam); HANDLE hTicket = (HANDLE)wParam; DWORD dwOwnerPID = (DWORD)lParam; // do delivery BOOL ret = DeliverNotification(hTicket, dwOwnerPID); // free the ticket SHFreeShared(hTicket, dwOwnerPID); return ret; } // Message CN_SUSPEND_RESUME: Suspend or resume the change notification. // (specification is unknown) LRESULT CChangeNotifyServer::OnSuspendResume(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { TRACE("OnSuspendResume\n"); // FIXME return FALSE; } // Message CN_UNREGISTER_PROCESS: Remove registration entries by PID. // wParam: The user PID. // lParam: Ignored. // return: Zero. LRESULT CChangeNotifyServer::OnRemoveByPID(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { DWORD dwOwnerPID, dwUserPID = (DWORD)wParam; GetWindowThreadProcessId(m_hWnd, &dwOwnerPID); RemoveItemsByProcess(dwOwnerPID, dwUserPID); return 0; } LRESULT CChangeNotifyServer::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { CDirectoryWatcher::RequestAllWatchersTermination(); return 0; } // get next valid registration ID UINT CChangeNotifyServer::GetNextRegID() { m_nNextRegID++; if (m_nNextRegID == INVALID_REG_ID) m_nNextRegID++; return m_nNextRegID; } // This function is called from CChangeNotifyServer::OnDeliverNotification. // The function notifies to the registration entries that should be notified. BOOL CChangeNotifyServer::DeliverNotification(HANDLE hTicket, DWORD dwOwnerPID) { TRACE("DeliverNotification(%p, %p, 0x%lx)\n", m_hWnd, hTicket, dwOwnerPID); // lock the delivery ticket LPDELITICKET pTicket = (LPDELITICKET)SHLockSharedEx(hTicket, dwOwnerPID, FALSE); if (pTicket == NULL || pTicket->dwMagic != DELITICKET_MAGIC) { ERR("pTicket is invalid\n"); SHUnlockShared(pTicket); return FALSE; } // for all items for (INT i = 0; i < m_items.GetSize(); ++i) { // validate the item if (m_items[i].nRegID == INVALID_REG_ID) continue; HANDLE hRegEntry = m_items[i].hRegEntry; if (hRegEntry == NULL) continue; // lock the registration entry LPREGENTRY pRegEntry = (LPREGENTRY)SHLockSharedEx(hRegEntry, dwOwnerPID, FALSE); if (pRegEntry == NULL || pRegEntry->dwMagic != REGENTRY_MAGIC) { ERR("pRegEntry is invalid\n"); SHUnlockShared(pRegEntry); continue; } // should we notify for it? BOOL bNotify = ShouldNotify(pTicket, pRegEntry); if (bNotify) { // do notify TRACE("Notifying: %p, 0x%x, %p, %lu\n", pRegEntry->hwnd, pRegEntry->uMsg, hTicket, dwOwnerPID); SendMessageW(pRegEntry->hwnd, pRegEntry->uMsg, (WPARAM)hTicket, dwOwnerPID); TRACE("GetLastError(): %ld\n", ::GetLastError()); } // unlock the registration entry SHUnlockShared(pRegEntry); } // unlock the ticket SHUnlockShared(pTicket); return TRUE; } BOOL CChangeNotifyServer::ShouldNotify(LPDELITICKET pTicket, LPREGENTRY pRegEntry) { #define RETURN(x) do { \ TRACE("ShouldNotify return %d\n", (x)); \ return (x); \ } while (0) if (pTicket->wEventId & SHCNE_INTERRUPT) { if (!(pRegEntry->fSources & SHCNRF_InterruptLevel)) RETURN(FALSE); if (!pRegEntry->ibPidl) RETURN(FALSE); } else { if (!(pRegEntry->fSources & SHCNRF_ShellLevel)) RETURN(FALSE); } if (!(pTicket->wEventId & pRegEntry->fEvents)) RETURN(FALSE); LPITEMIDLIST pidl = NULL, pidl1 = NULL, pidl2 = NULL; if (pRegEntry->ibPidl) pidl = (LPITEMIDLIST)((LPBYTE)pRegEntry + pRegEntry->ibPidl); if (pTicket->ibOffset1) pidl1 = (LPITEMIDLIST)((LPBYTE)pTicket + pTicket->ibOffset1); if (pTicket->ibOffset2) pidl2 = (LPITEMIDLIST)((LPBYTE)pTicket + pTicket->ibOffset2); if (pidl == NULL || (pTicket->wEventId & SHCNE_GLOBALEVENTS)) RETURN(TRUE); if (pRegEntry->fRecursive) { if (ILIsParent(pidl, pidl1, FALSE) || (pidl2 && ILIsParent(pidl, pidl2, FALSE))) { RETURN(TRUE); } } else { if (ILIsEqual(pidl, pidl1) || ILIsParent(pidl, pidl1, TRUE) || (pidl2 && ILIsParent(pidl, pidl2, TRUE))) { RETURN(TRUE); } } RETURN(FALSE); #undef RETURN } HRESULT WINAPI CChangeNotifyServer::GetWindow(HWND* phwnd) { if (!phwnd) return E_INVALIDARG; *phwnd = m_hWnd; return S_OK; } HRESULT WINAPI CChangeNotifyServer::ContextSensitiveHelp(BOOL fEnterMode) { return E_NOTIMPL; } HRESULT CChangeNotifyServer::Initialize() { // This is called by CChangeNotifyServer_CreateInstance right after instantiation. // Create the window of the server here. Create(0); if (!m_hWnd) return E_FAIL; return S_OK; } HRESULT CChangeNotifyServer_CreateInstance(REFIID riid, void **ppv) { return ShellObjectCreatorInit(riid, ppv); }