#include "private.h" #include "throttle.h" #include "subsmgrp.h" #include #define TF_THISMODULE TF_THROTTLER const int MAX_AUTOCACHESIZE_ASK = 2; const int MIN_CACHE_INCREASE = 1024; // in KB // Strings for cache restrictions const TCHAR c_szKeyRestrict[] = TEXT("Software\\Policies\\Microsoft\\Internet Explorer\\Control Panel"); const TCHAR c_szCache[] = TEXT("Cache"); CThrottler *CThrottler::s_pThrottler = NULL; const CFactoryData CThrottler::s_ThrottlerFactoryData = { &CLSID_SubscriptionThrottler, CreateInstance, 0 }; #ifdef DEBUG void DUMPITEM(CHAR *pszMsg, const SUBSCRIPTIONCOOKIE *pCookie) { ISubscriptionItem *psi; if (SUCCEEDED(SubscriptionItemFromCookie(FALSE, pCookie, &psi))) { BSTR bstrName; if (SUCCEEDED(ReadBSTR(psi, c_szPropName, &bstrName))) { TraceMsgA(TF_THISMODULE, "%s: %S", pszMsg, bstrName); SysFreeString(bstrName); } psi->Release(); } } #else #define DUMPITEM(pszMsg, pCookie) #endif // dwSyncFlags has 8 bits of enum (EVENTMASK) and the rest is flags inline BOOL IsSyncEvent(DWORD dwSyncFlags, DWORD dwSyncEvent) { return (dwSyncFlags & SYNCMGRFLAG_EVENTMASK) == dwSyncEvent; } inline BOOL IsSyncEventFlag(DWORD dwSyncFlags, DWORD dwSyncEvent) { return (dwSyncFlags & dwSyncEvent) != 0; } inline BOOL IsIgnoreIdleSyncEvent(DWORD dwSyncFlags) { return !IsSyncEvent(dwSyncFlags, SYNCMGRFLAG_IDLE); /* return IsSyncEvent(dwSyncFlags, SYNCMGRFLAG_CONNECT) || IsSyncEvent(dwSyncFlags, SYNCMGRFLAG_PENDINGDISCONNECT) || IsSyncEvent(dwSyncFlags, SYNCMGRFLAG_MANUAL) || IsSyncEvent(dwSyncFlags, SYNCMGRFLAG_INVOKE); */ } inline BOOL IsScheduleSyncEvent(DWORD dwSyncFlags) { return IsSyncEvent(dwSyncFlags, SYNCMGRFLAG_SCHEDULED) || IsSyncEvent(dwSyncFlags, SYNCMGRFLAG_IDLE); } class CThrottlerProxy : public ISubscriptionThrottler { public: CThrottlerProxy(CThrottler *pThrottler) { m_cRef = 1; m_pThrottler = pThrottler; m_pThrottler->ExternalAddRef(); } STDMETHODIMP QueryInterface(REFIID riid, void **punk) { if ((riid == IID_IUnknown) || (riid == IID_ISubscriptionThrottler)) { *punk = (ISubscriptionThrottler *)this; AddRef(); return S_OK; } else { *punk = NULL; return E_NOINTERFACE; } } STDMETHODIMP_(ULONG) AddRef() { return ++m_cRef; } STDMETHODIMP_(ULONG) Release() { if (--m_cRef == 0) { delete this; return 0; } return m_cRef; } STDMETHODIMP GetSubscriptionRunState( /* [in] */ DWORD dwNumCookies, /* [size_is][in] */ const SUBSCRIPTIONCOOKIE *pCookies, /* [size_is][out] */ DWORD *pdwRunState) { return m_pThrottler->GetSubscriptionRunState(dwNumCookies, pCookies, pdwRunState); } STDMETHODIMP AbortItems( /* [in] */ DWORD dwNumCookies, /* [size_is][in] */ const SUBSCRIPTIONCOOKIE *pCookies) { return m_pThrottler->AbortItems(dwNumCookies, pCookies); } STDMETHODIMP AbortAll() { return m_pThrottler->AbortAll(); } private: ULONG m_cRef; CThrottler *m_pThrottler; ~CThrottlerProxy() { m_pThrottler->ExternalRelease(); } }; HRESULT CThrottler::CreateInstance(IUnknown *punkOuter, IUnknown **ppunk) { HRESULT hr; ASSERT(NULL == punkOuter); ASSERT(NULL != ppunk); if (NULL != CThrottler::s_pThrottler) { *ppunk = new CThrottlerProxy(CThrottler::s_pThrottler); if (NULL != *ppunk) { hr = S_OK; } else { hr = E_OUTOFMEMORY; } } else { TraceMsg(TF_ALWAYS, "UNEXPECTED ERROR: Failed to attached to throttler in CreateInstance"); hr = E_UNEXPECTED; } return hr; } CThrottler::CThrottler() { ASSERT(NULL == s_pThrottler); ASSERT(NULL == m_pItemsHead); ASSERT(NULL == m_pItemsTail); ASSERT(NULL == m_updateQueue[0]); // APPCOMPAT - this is only until msidle is multi-client aware. IdleEnd(); //m_fUserIsIdle = TRUE; // TODO: need to determine this better IdleBegin(NULL); m_cRef = 1; } CThrottler::~CThrottler() { DBG("Destroying Throttler"); ASSERT(GetCurrentThreadId() == m_dwThreadId); IdleEnd(); // Destroy window if (m_hwndThrottler) { SetWindowLongPtr(m_hwndThrottler, GWLP_USERDATA, 0); DestroyWindow(m_hwndThrottler); m_hwndThrottler = NULL; } s_pThrottler = NULL; RevokeClassObject(); } HRESULT CThrottler::RevokeClassObject() { HRESULT hr; if (m_dwRegister) { hr = CoRevokeClassObject(m_dwRegister); m_dwRegister = 0; } else { hr = S_FALSE; } return hr; } HRESULT /* static */ CThrottler::GetThrottler(CThrottler **ppThrottler) { HRESULT hr = S_OK; ASSERT(NULL != ppThrottler); if (NULL != ppThrottler) { *ppThrottler = NULL; // If there is no throttler create a new one if (NULL == s_pThrottler) { DBG("Creating new throttler in GetThrottler"); s_pThrottler = new CThrottler; if (NULL != s_pThrottler) { IClassFactory *pcf = new CClassFactory(&s_ThrottlerFactoryData); if (NULL != pcf) { HRESULT hrRegister = CoRegisterClassObject(CLSID_SubscriptionThrottler, pcf, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &s_pThrottler->m_dwRegister); if (FAILED(hrRegister)) { TraceMsg(TF_ALWAYS, "CoRegisterClassObject failed - other processes can't talk to us!"); } pcf->Release(); } *ppThrottler = s_pThrottler; #ifdef DEBUG s_pThrottler->m_dwThreadId = GetCurrentThreadId(); #endif } else { DBG("Failed to create Throttler class factory"); hr = E_OUTOFMEMORY; } } else { // Attach to existing throttler ASSERT(GetCurrentThreadId() == s_pThrottler->m_dwThreadId); s_pThrottler->AddRef(); *ppThrottler = s_pThrottler; } } else { hr = E_INVALIDARG; } return hr; } void CThrottler::OnIdleStateChange(DWORD dwState) { if (NULL != s_pThrottler) { switch(dwState) { case STATE_USER_IDLE_BEGIN: DBG("OnIdleStateChange: Idle Begin"); #ifdef DEBUG LogEvent(TEXT("Idle state begins")); #endif s_pThrottler->OnIdleBegin(); break; case STATE_USER_IDLE_END: DBG("OnIdleStateChange: Idle End"); #ifdef DEBUG LogEvent(TEXT("Idle state ends")); #endif s_pThrottler->OnIdleEnd(); break; } } } void CThrottler::OnIdleBegin() { ASSERT(GetCurrentThreadId() == m_dwThreadId); m_fUserIsIdle = TRUE; FillTheQueue(); } void CThrottler::OnIdleEnd() { ASSERT(GetCurrentThreadId() == m_dwThreadId); m_fUserIsIdle = FALSE; for (int i = 0; i < ARRAYSIZE(m_updateQueue); i++) { if ((NULL != m_updateQueue[i]) && (m_updateQueue[i]->m_dwRunState & RS_SUSPENDONIDLE)) { DUMPITEM("Suspending in CThrottler::OnIdleEnd", &m_updateQueue[i]->m_cookie); ISubscriptionAgentControl *pSubsAgentCtl; CUpdateItem *pUpdateItem = m_updateQueue[i]; pSubsAgentCtl = m_updateQueue[i]->m_pSubsAgentCtl; m_updateQueue[i]->m_dwRunState &= ~RS_UPDATING; m_updateQueue[i]->m_dwRunState |= RS_SUSPENDED; m_updateQueue[i] = NULL; ASSERT(NULL != pSubsAgentCtl); if (SUCCEEDED(pSubsAgentCtl->PauseUpdate(0))) { WCHAR wszMsg[256]; MLLoadStringW(IDS_UPDATE_PAUSED, wszMsg, ARRAYSIZE(wszMsg)); NotifyHandlers(NH_UPDATEPROGRESS, &pUpdateItem->m_cookie, -1, -1, -1, WC_INTERNAL_S_PAUSED, wszMsg); } } } FillTheQueue(); } STDMETHODIMP CThrottler::QueryInterface(REFIID riid, void **ppv) { ASSERT(GetCurrentThreadId() == m_dwThreadId); if (NULL == ppv) { return E_INVALIDARG; } if ((IID_IUnknown == riid) || (IID_ISubscriptionAgentEvents == riid)) { *ppv = (ISubscriptionAgentEvents *)this; } else if (IID_ISubscriptionThrottler == riid) { *ppv = (ISubscriptionThrottler *)this; } else { *ppv = NULL; return E_NOINTERFACE; } AddRef(); return S_OK; } ULONG CThrottler::ExternalAddRef() { AddRef(); return ++m_cExternalRef; } ULONG CThrottler::ExternalRelease() { ULONG cRef = --m_cExternalRef; Release(); return cRef; } STDMETHODIMP_(ULONG) CThrottler::AddRef() { ASSERT(GetCurrentThreadId() == m_dwThreadId); return ++m_cRef; } STDMETHODIMP_(ULONG) CThrottler::Release() { ASSERT(GetCurrentThreadId() == m_dwThreadId); if (--m_cRef == 0) { delete this; return 0; } return m_cRef; } HRESULT CThrottler::NotifyHandlers(int idCmd, const SUBSCRIPTIONCOOKIE *pSubscriptionCookie, ...) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr = S_OK; va_list va; long lSizeDownloaded = -1; long lProgressCurrent = -1; long lProgressMax = -1; HRESULT hrParam = E_UNEXPECTED; LPCWSTR wszParam = NULL; va_start(va, pSubscriptionCookie); // First extract args switch (idCmd) { case NH_UPDATEBEGIN: // Nothing to do for now break; case NH_UPDATEPROGRESS: lSizeDownloaded = va_arg(va, long); lProgressCurrent = va_arg(va, long); lProgressMax = va_arg(va, long); hrParam = va_arg(va, HRESULT); wszParam = va_arg(va, LPCWSTR); break; case NH_UPDATEEND: lSizeDownloaded = va_arg(va, long); hrParam = va_arg(va, HRESULT); wszParam = va_arg(va, LPCWSTR); break; case NH_REPORTERROR: hrParam = va_arg(va, HRESULT); wszParam = va_arg(va, LPCWSTR); break; default: ASSERT(0); // Don't know what to do hr = E_UNEXPECTED; break; } // Now loop HRESULT hrTemp = S_OK; CSyncMgrNode *pSyncMgrNode = m_pSyncMgrs; while (pSyncMgrNode) { COfflineSync *pOfflineSync = pSyncMgrNode->m_pOfflineSync; pSyncMgrNode = pSyncMgrNode->m_pNext; switch (idCmd) { case NH_UPDATEBEGIN: hrTemp = pOfflineSync->UpdateBegin(pSubscriptionCookie); break; case NH_UPDATEPROGRESS: hrTemp = pOfflineSync->UpdateProgress(pSubscriptionCookie, lSizeDownloaded, lProgressCurrent, lProgressMax, hrParam, wszParam); break; case NH_UPDATEEND: hrTemp = pOfflineSync->UpdateEnd(pSubscriptionCookie, lSizeDownloaded, hrParam, wszParam); break; case NH_REPORTERROR: hrTemp = pOfflineSync->ReportError(pSubscriptionCookie, hrParam, wszParam); break; } if (FAILED(hrTemp)) { hr = hrTemp; } } va_end(va); return hr; } // ISubscriptionAgentEvents members STDMETHODIMP CThrottler::UpdateBegin(const SUBSCRIPTIONCOOKIE *pSubscriptionCookie) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr; CUpdateItem *pUpdateItem; hr = FindCookie(pSubscriptionCookie, &pUpdateItem); ASSERT(SUCCEEDED(hr)); if (SUCCEEDED(hr)) { DUMPITEM("CThrottler::UpdateBegin", pSubscriptionCookie); pUpdateItem->m_dwRunState &= ~(RS_READY | RS_SUSPENDED); pUpdateItem->m_dwRunState |= RS_UPDATING; hr = NotifyHandlers(NH_UPDATEBEGIN, pSubscriptionCookie); } return hr; } STDMETHODIMP CThrottler::UpdateProgress( const SUBSCRIPTIONCOOKIE *pSubscriptionCookie, long lSizeDownloaded, long lProgressCurrent, long lProgressMax, HRESULT hrStatus, LPCWSTR wszStatus) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr; CUpdateItem *pUpdateItem; // TODO: // Adjust the max to fool syncmgr if (SUCCEEDED(FindCookie(pSubscriptionCookie, &pUpdateItem))) { if ((lProgressMax < 0) || (lProgressMax <= lProgressCurrent)) { if (pUpdateItem->m_nMax <= lProgressCurrent) { pUpdateItem->m_nMax = (lProgressCurrent * 3) / 2; } lProgressMax = pUpdateItem->m_nMax; } } hr = NotifyHandlers(NH_UPDATEPROGRESS, pSubscriptionCookie, lSizeDownloaded, lProgressCurrent, lProgressMax, hrStatus, wszStatus); return hr; } STDMETHODIMP CThrottler::UpdateEnd( const SUBSCRIPTIONCOOKIE *pSubscriptionCookie, long lSizeDownloaded, HRESULT hrResult, LPCWSTR wszResult) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr; CUpdateItem *pUpdateItem; SUBSCRIPTIONCOOKIE cookie = *pSubscriptionCookie; hr = FindCookie(pSubscriptionCookie, &pUpdateItem); if (SUCCEEDED(hr)) { DUMPITEM("CThrottler::UpdateEnd", pSubscriptionCookie); pUpdateItem->m_dwRunState &= ~(RS_READY | RS_SUSPENDED | RS_UPDATING | RS_SUSPENDONIDLE); pUpdateItem->m_dwRunState |= RS_COMPLETED; RemoveItemFromList(pUpdateItem, TRUE); // ************************************************************************ // Don't use anything that could have come from pUpdateItem after this // including the pSubscriptionCookie above which came from an agent which // probably no longer exists! // (actually, the agent keeps itself alive until this call returns) // ************************************************************************ } hr = NotifyHandlers(NH_UPDATEEND, &cookie, lSizeDownloaded, hrResult, wszResult); FireSubscriptionEvent(SUBSNOTF_SYNC_STOP, &cookie); FillTheQueue(); return hr; } STDMETHODIMP CThrottler::ReportError( const SUBSCRIPTIONCOOKIE *pSubscriptionCookie, HRESULT hrError, LPCWSTR wszError) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr; if (INET_E_AGENT_EXCEEDING_CACHE_SIZE == hrError) { // Agent is notifying us that they're about to exceed the cache size. hr = AutoCacheSizeRequest(pSubscriptionCookie); } else hr = NotifyHandlers(NH_REPORTERROR, pSubscriptionCookie, hrError, wszError); return hr; } STDMETHODIMP CThrottler::GetSubscriptionRunState( /* [in] */ DWORD dwNumCookies, /* [size_is][in] */ const SUBSCRIPTIONCOOKIE *pCookies, /* [size_is][out] */ DWORD *pdwRunState) { ASSERT(GetCurrentThreadId() == m_dwThreadId); if ((0 == dwNumCookies) || (NULL == pCookies) || (NULL == pdwRunState)) { return E_INVALIDARG; } for (DWORD i = 0; i < dwNumCookies; i++, pCookies++, pdwRunState++) { CUpdateItem *pUpdateItem; if (SUCCEEDED(FindCookie(pCookies, &pUpdateItem))) { *pdwRunState = pUpdateItem->m_dwRunState; } else { *pdwRunState = 0; } } return S_OK; } // DoAbortItem will cause the CThrottler to get released if the last running // agent is aborted (Agent notifies it's done, SyncMgr releases throttler, // then agent releases throttler) HRESULT CThrottler::DoAbortItem(CUpdateItem *pUpdateItem) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr; ASSERT(((pUpdateItem->m_dwRunState & (RS_UPDATING | RS_SUSPENDED)) && (NULL != pUpdateItem->m_pSubsAgentCtl)) || (NULL == pUpdateItem->m_pSubsAgentCtl)); if ((pUpdateItem->m_dwRunState & (RS_UPDATING | RS_SUSPENDED)) && (NULL != pUpdateItem->m_pSubsAgentCtl)) { DUMPITEM("CThrottler::DoAbortItem with existing Agent", &pUpdateItem->m_cookie); hr = pUpdateItem->m_pSubsAgentCtl->AbortUpdate(0); } else { WCHAR wszMsg[256]; MLLoadStringW(IDS_STATUS_ABORTED, wszMsg, ARRAYSIZE(wszMsg)); DUMPITEM("CThrottler::DoAbortItem with no Agent", &pUpdateItem->m_cookie); ReportThrottlerError(&pUpdateItem->m_cookie, E_ABORT, wszMsg); hr = UpdateEnd(&pUpdateItem->m_cookie, 0, E_ABORT, wszMsg); } return hr; } STDMETHODIMP CThrottler::AbortItems( /* [in] */ DWORD dwNumCookies, /* [size_is][in] */ const SUBSCRIPTIONCOOKIE *pCookies) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr; if ((0 == dwNumCookies) || (NULL == pCookies)) { return E_INVALIDARG; } if (FAILED(CreateThrottlerWnd())) return E_FAIL; hr = S_OK; void *pItem = MemAlloc(LMEM_FIXED, dwNumCookies * sizeof(SUBSCRIPTIONCOOKIE)); if (pItem) { #ifdef DEBUG for (DWORD i = 0; i < dwNumCookies; i++) { DUMPITEM("Aborting in CThrottler::AbortItems", &pCookies[i]); } #endif memcpy(pItem, pCookies, dwNumCookies * sizeof(SUBSCRIPTIONCOOKIE)); PostMessage(m_hwndThrottler, WM_THROTTLER_ABORTITEM, (WPARAM)dwNumCookies, (LPARAM)pItem); } else { DBG_WARN("Memory alloc failed in CThrottler::AbortItems"); hr = S_FALSE; } return hr; } STDMETHODIMP CThrottler::ActuallyAbortItems( /* [in] */ DWORD dwNumCookies, /* [size_is][in] */ const SUBSCRIPTIONCOOKIE *pCookies) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr; if ((0 == dwNumCookies) || (NULL == pCookies)) { return E_INVALIDARG; } hr = S_OK; // DoAbortItem will cause the CThrottler to get released if the last // running agent is aborted. Protect against that. AddRef(); for (DWORD i = 0; i < dwNumCookies; i++, pCookies++) { HRESULT hrItem; CUpdateItem *pUpdateItem; hrItem = FindCookie(pCookies, &pUpdateItem); if (SUCCEEDED(hrItem)) { hrItem = DoAbortItem(pUpdateItem); // ************************************************************************ // pUpdateItem is no longer valid! // ************************************************************************ } if (FAILED(hrItem)) { hr = S_FALSE; } } Release(); return hr; } HRESULT CThrottler::CreateThrottlerWnd() { ASSERT(GetCurrentThreadId() == m_dwThreadId); if (!m_hwndThrottler) { WNDCLASS wc; wc.style = 0; wc.lpfnWndProc = CThrottler::ThrottlerWndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = g_hInst; wc.hIcon = NULL; wc.hCursor = NULL; wc.hbrBackground = (HBRUSH)NULL; wc.lpszMenuName = NULL; wc.lpszClassName = THROTTLER_WNDCLASS; RegisterClass(&wc); m_hwndThrottler = CreateWindow(THROTTLER_WNDCLASS, TEXT("YO"), WS_OVERLAPPED, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, g_hInst, (LPVOID)this); if (NULL == m_hwndThrottler) { DBG_WARN("CThrottler CreateWindow failed"); return E_FAIL; } } return S_OK; } LRESULT CThrottler::ThrottlerWndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) { CThrottler *pThis = (CThrottler*) GetWindowLongPtr(hWnd, GWLP_USERDATA); // Validate pThis #ifdef DEBUG if (pThis && IsBadWritePtr(pThis, sizeof(*pThis))) { TraceMsg(TF_THISMODULE|TF_WARNING, "Invalid 'this' in ThrottlerWndProc (0x%08x) - already destroyed?", pThis); } if (pThis) { ASSERT(GetCurrentThreadId() == pThis->m_dwThreadId); ASSERT(GetCurrentThreadId() == GetWindowThreadProcessId(hWnd, NULL)); } #endif switch (Msg) { case WM_CREATE : { LPCREATESTRUCT pcs = (LPCREATESTRUCT)lParam; if (!pcs || !(pcs->lpCreateParams)) { DBG_WARN("Invalid param ThrottlerWndProc Create"); return -1; } SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR) pcs->lpCreateParams); return 0; } case WM_THROTTLER_ABORTALL: if (pThis) pThis->ActuallyAbortAll(); break; case WM_THROTTLER_ABORTITEM: if (pThis) pThis->ActuallyAbortItems((ULONG) wParam, (SUBSCRIPTIONCOOKIE*) lParam); MemFree((HLOCAL)lParam); break; case WM_THROTTLER_AUTOCACHESIZE_ASK: if (pThis) pThis->AutoCacheSizeAskUser((DWORD)lParam); break; default: return DefWindowProc(hWnd, Msg, wParam, lParam); } return 0; } STDMETHODIMP CThrottler::AbortAll() { ASSERT(GetCurrentThreadId() == m_dwThreadId); if (FAILED(CreateThrottlerWnd())) return E_FAIL; DBG("Aborting all items"); PostMessage(m_hwndThrottler, WM_THROTTLER_ABORTALL, 0, 0); return S_OK; } STDMETHODIMP CThrottler::ActuallyAbortAll() { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr = S_OK; CUpdateItem *pItem = m_pItemsHead; if (FALSE == m_fAbortingAll) { m_fAbortingAll = TRUE; while (pItem) { CUpdateItem *pUpdateItem = pItem; // Move forward now since this item should get yanked! pItem = pItem->m_pNext; if (FAILED(DoAbortItem(pUpdateItem))) { hr = S_FALSE; } } m_fAbortingAll = FALSE; } else { hr = S_FALSE; } return hr; } HRESULT CThrottler::Advise(COfflineSync *pOfflineSync) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr; CSyncMgrNode *pSyncMgrNode; ASSERT(NULL != pOfflineSync); #ifdef DEBUG pSyncMgrNode = m_pSyncMgrs; while (pSyncMgrNode) { if (pSyncMgrNode->m_pOfflineSync == pOfflineSync) { ASSERT(0); // Shouldn't advise more than once! } pSyncMgrNode = pSyncMgrNode->m_pNext; } #endif ASSERT(!m_hwndParent || (m_hwndParent == pOfflineSync->GetParentWindow())); m_hwndParent = pOfflineSync->GetParentWindow(); pSyncMgrNode = new CSyncMgrNode(pOfflineSync, m_pSyncMgrs); if (NULL != pSyncMgrNode) { pOfflineSync->AddRef(); m_pSyncMgrs = pSyncMgrNode; hr = S_OK; } else { hr = E_OUTOFMEMORY; } return hr; } HRESULT CThrottler::Unadvise(COfflineSync *pOfflineSync) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr = E_FAIL; CSyncMgrNode *pSyncMgrNode; CSyncMgrNode **ppSyncMgrPrev; ASSERT(NULL != pOfflineSync); pSyncMgrNode = m_pSyncMgrs; ppSyncMgrPrev = &m_pSyncMgrs; while (pSyncMgrNode) { if (pSyncMgrNode->m_pOfflineSync == pOfflineSync) { *ppSyncMgrPrev = pSyncMgrNode->m_pNext; delete pSyncMgrNode; hr = S_OK; break; } ppSyncMgrPrev = &pSyncMgrNode->m_pNext; pSyncMgrNode = pSyncMgrNode->m_pNext; } ASSERT(SUCCEEDED(hr)); // This is internal goo so should not fail! if (NULL == m_pSyncMgrs) { // Everyone has lost interest in us... RevokeClassObject(); s_pThrottler = NULL; while (m_cExternalRef > 0) { TraceMsg(TF_WARNING, "CThrottle::Unadvise m_cExternalRef = %d", m_cExternalRef); MSG msg; if (PeekMessage(&msg, NULL, 0, 0, TRUE)) { DispatchMessage(&msg); } } } return hr; } int CThrottler::GetCookieIndexInQueue(const SUBSCRIPTIONCOOKIE *pCookie) { ASSERT(GetCurrentThreadId() == m_dwThreadId); int index = -1; for (int i = 0; i < ARRAYSIZE(m_updateQueue); i++) { if ((NULL != m_updateQueue[i]) && (m_updateQueue[i]->m_cookie == *pCookie)) { index = i; break; } } return index; } void CThrottler::FailedUpdate(HRESULT hr, const SUBSCRIPTIONCOOKIE *pCookie) { ASSERT(GetCurrentThreadId() == m_dwThreadId); WCHAR wszMsg[256]; int resID; switch (hr) { case INET_E_SCHEDULED_UPDATES_DISABLED: resID = IDS_SCHEDULED_UPDATES_DISABLED; break; case INET_E_SCHEDULED_UPDATES_RESTRICTED: resID = IDS_SCHEDULED_UPDATES_RESTRICTED; break; case INET_E_SCHEDULED_UPDATE_INTERVAL: resID = IDS_SCHEDULED_UPDATE_INTERVAL; break; case INET_E_SCHEDULED_EXCLUDE_RANGE: resID = IDS_SCHEDULED_EXCLUDE_RANGE; break; default: resID = IDS_CRAWL_STATUS_NOT_OK; break; } MLLoadStringW(resID, wszMsg, ARRAYSIZE(wszMsg)); ReportThrottlerError(pCookie, hr, wszMsg); UpdateEnd(pCookie, 0, hr, wszMsg); } void CThrottler::RunItem(int queueSlot, CUpdateItem *pUpdateItem) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr; ASSERT(NULL == m_updateQueue[queueSlot]); m_updateQueue[queueSlot] = pUpdateItem; if (pUpdateItem->m_dwRunState & RS_SUSPENDED) { DUMPITEM("Resuming suspended item in CThrottler::RunItem", &pUpdateItem->m_cookie); ASSERT(NULL != pUpdateItem->m_pSubsAgentCtl); pUpdateItem->m_dwRunState |= RS_UPDATING; pUpdateItem->m_dwRunState &= ~RS_SUSPENDED; hr = pUpdateItem->m_pSubsAgentCtl->ResumeUpdate(0); if (SUCCEEDED(hr)) { WCHAR wszMsg[256]; MLLoadStringW(IDS_UPDATE_RESUMING, wszMsg, ARRAYSIZE(wszMsg)); NotifyHandlers(NH_UPDATEPROGRESS, &pUpdateItem->m_cookie, -1, -1, -1, WC_INTERNAL_S_RESUMING, wszMsg); } } else { ISubscriptionItem *psi; hr = SubscriptionItemFromCookie(FALSE, &pUpdateItem->m_cookie, &psi); if (SUCCEEDED(hr)) { SUBSCRIPTIONITEMINFO sii; sii.cbSize = sizeof(SUBSCRIPTIONITEMINFO); hr = psi->GetSubscriptionItemInfo(&sii); if (SUCCEEDED(hr)) { hr = CoCreateInstance(sii.clsidAgent, NULL, CLSCTX_INPROC_SERVER, IID_ISubscriptionAgentControl, (void**)&pUpdateItem->m_pSubsAgentCtl); if (SUCCEEDED(hr)) { DUMPITEM("Running item in CThrottler::RunItem", &pUpdateItem->m_cookie); hr = pUpdateItem->m_pSubsAgentCtl->StartUpdate(psi, (ISubscriptionAgentEvents *)this); FireSubscriptionEvent(SUBSNOTF_SYNC_START, &pUpdateItem->m_cookie); } else { DBG_WARN("CoCreate Agent FAILED in CThrottler::RunItem"); } } psi->Release(); } else { DBG_WARN("SubscriptionItemFromCookie FAILED in CThrottler::RunItem"); } } if (FAILED(hr)) { FailedUpdate(hr, &pUpdateItem->m_cookie); } } int CThrottler::GetFreeQueueSlot() { ASSERT(GetCurrentThreadId() == m_dwThreadId); int index = -1; for (int i = 0; i < ARRAYSIZE(m_updateQueue); i++) { if (NULL == m_updateQueue[i]) { index = i; break; } } return index; } void CThrottler::FillTheQueue() { ASSERT(GetCurrentThreadId() == m_dwThreadId); if ((FALSE == m_fFillingTheQueue) && // avoid re-entrancy (FALSE == m_fAbortingAll) && // avoid re-entrancy (FALSE == m_fAutoCacheSizePending)) // we have a dialog up for the user { m_fFillingTheQueue = TRUE; CUpdateItem *pNextItem = m_pItemsHead; CUpdateItem *pItem; while (NULL != pNextItem) { pItem = pNextItem; // Move ahead since this item may not be here // if we run it and it fails pNextItem = pNextItem->m_pNext; if (!(pItem->m_dwRunState & (RS_COMPLETED | RS_UPDATING))) { int freeSlot = GetFreeQueueSlot(); if ((freeSlot >= 0) && (m_fUserIsIdle || (!(pItem->m_dwRunState & RS_SUSPENDONIDLE)))) { RunItem(freeSlot, pItem); } else { // If we didn't run it then let's make sure the UI reflects the current // state properly HRESULT hrStatus; WCHAR wszMsg[256]; if ((pItem->m_dwRunState & RS_SUSPENDONIDLE) && (!m_fUserIsIdle)) { MLLoadStringW(IDS_UPDATE_PAUSED, wszMsg, ARRAYSIZE(wszMsg)); hrStatus = WC_INTERNAL_S_PAUSED; } else { StrCpyW(wszMsg, L" "); // Don't say it, I know what you're thinking... // ...if we don't do this, then the status // text won't change. hrStatus = WC_INTERNAL_S_PENDING; } NotifyHandlers(NH_UPDATEPROGRESS, &pItem->m_cookie, -1, -1, -1, hrStatus, wszMsg); } } } m_fFillingTheQueue = FALSE; } } HRESULT CThrottler::AddItemToListTail(CUpdateItem *pAddItem) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr = S_OK; ASSERT(NULL != pAddItem); if (NULL != pAddItem) { if (NULL == m_pItemsTail) { // Nothing in the list ASSERT(NULL == m_pItemsHead); m_pItemsHead = pAddItem; } else { m_pItemsTail->m_pNext = pAddItem; } m_pItemsTail = pAddItem; } else { hr = E_UNEXPECTED; } ASSERT(NULL != m_pItemsHead); ASSERT(NULL != m_pItemsTail); ASSERT(NULL == m_pItemsTail->m_pNext); ASSERT(SUCCEEDED(hr)); return hr; } HRESULT CThrottler::RemoveItemFromList(CUpdateItem *pRemoveItem, BOOL fDelete) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr = E_UNEXPECTED; CUpdateItem *pItem = m_pItemsHead; CUpdateItem *pPrevItem = NULL; ASSERT(NULL != pRemoveItem); ASSERT(NULL != m_pItemsHead); ASSERT(NULL != m_pItemsTail); if (NULL != pRemoveItem) { int queueIndex = GetCookieIndexInQueue(&pRemoveItem->m_cookie); if (queueIndex >= 0) { m_updateQueue[queueIndex] = NULL; } while (pItem) { if (pItem == pRemoveItem) { if (NULL != pPrevItem) { // Removing beyond the head pPrevItem->m_pNext = pItem->m_pNext; } else { // Removing the head m_pItemsHead = pItem->m_pNext; } // Now fix the tail if (m_pItemsTail == pRemoveItem) { m_pItemsTail = pPrevItem; } hr = S_OK; break; } pPrevItem = pItem; pItem = pItem->m_pNext; } if (fDelete) { delete pRemoveItem; } } ASSERT(((NULL != m_pItemsHead) && (NULL != m_pItemsTail) && (NULL == m_pItemsTail->m_pNext)) || ((NULL == m_pItemsHead) && (NULL == m_pItemsTail))); ASSERT(SUCCEEDED(hr)); // If we have just removed our last item from the list, check to see if we forced // global online mode or autodialed and fix up if so. if ((NULL == m_pItemsHead) && (m_fForcedGlobalOnline || m_fAutoDialed)) { if (m_fForcedGlobalOnline) { SetGlobalOffline(TRUE); m_fForcedGlobalOnline = FALSE; m_fAutoDialed = FALSE; } else { ASSERT(m_fAutoDialed); InternetAutodialHangup(0); m_fAutoDialed=FALSE; } } return hr; } HRESULT CThrottler::CanScheduledItemRun(ISubscriptionItem *pSubsItem) { ASSERT(GetCurrentThreadId() == m_dwThreadId); // If this item is running as a result of a schedule invocation, then // we need to check time/range restrictions. HRESULT hr = S_OK; const TCHAR c_szNoScheduledUpdates[] = TEXT("NoScheduledUpdates"); DWORD dwData; DWORD cbData = sizeof(dwData); // First check if the user has disabled scheduled updates in inetcpl. if ((ERROR_SUCCESS == SHGetValue(HKEY_CURRENT_USER, c_szRegKey, c_szNoScheduledUpdates, NULL, &dwData, &cbData)) && dwData) { hr = INET_E_SCHEDULED_UPDATES_DISABLED; } if (SUCCEEDED(hr)) { // Check if admin has disabled scheduled updates altogether if (SHRestricted2W(REST_NoScheduledUpdates, NULL, 0)) { hr = INET_E_SCHEDULED_UPDATES_RESTRICTED; } } if (SUCCEEDED(hr)) { // Check if admin has set a minimum update interval. DWORD dwMinUpdateInterval = SHRestricted2W(REST_MinUpdateInterval, NULL, 0); if (dwMinUpdateInterval > 0) { DATE dt; if (SUCCEEDED(ReadDATE(pSubsItem, c_szPropCompletionTime, &dt))) { SYSTEMTIME st; GetLocalTime(&st); CFileTime lastUpdate; CFileTime currentTime; VariantTimeToFileTime(dt, lastUpdate); SystemTimeToFileTime(&st, ¤tTime); if ((currentTime - lastUpdate) < ((__int64)dwMinUpdateInterval * ONE_MINUTE_IN_FILETIME)) { hr = INET_E_SCHEDULED_UPDATE_INTERVAL; } } } } if (SUCCEEDED(hr)) { DWORD dwBegin = SHRestricted2W(REST_UpdateExcludeBegin, NULL, 0); DWORD dwEnd = SHRestricted2W(REST_UpdateExcludeEnd, NULL, 0); // Check if admin has specified a blackout time for scheduled updates. if (dwBegin && dwEnd) { SYSTEMTIME st; CFileTime ftNow, ftBegin, ftEnd; GetLocalTime(&st); SystemTimeToFileTime(&st, &ftNow); st.wSecond = 0; st.wMilliseconds = 0; st.wHour = (WORD)dwBegin / 60; st.wMinute = (WORD)dwBegin % 60; SystemTimeToFileTime(&st, &ftBegin); st.wHour = (WORD)dwEnd / 60; st.wMinute = (WORD)dwEnd % 60; SystemTimeToFileTime(&st, &ftEnd); // if these values are normalized (ie. begin comes before end) if (ftBegin <= ftEnd) { // Then just check to see if time now is between begin // and end. (ie. ftEnd >= ftNow >= ftBegin) if ((ftNow >= ftBegin) && (ftNow <= ftEnd)) { hr = INET_E_SCHEDULED_EXCLUDE_RANGE; } } else { // Begin and end are not normalized. So we check to see if // now is before end or now is after begin. // For example: // Assuming begin is 6pm and end is 6am. If now is 5 pm, the // item should run. If now is 10pm or 4am, it should not run. if ((ftNow <= ftEnd) || (ftNow >= ftBegin)) { hr = INET_E_SCHEDULED_EXCLUDE_RANGE; } } } } return hr; } HRESULT CThrottler::RunCookies(DWORD dwNumCookies, const SUBSCRIPTIONCOOKIE *pSubscriptionCookies, DWORD dwSyncFlags) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr = S_OK; DWORD i; CUpdateItem *pUpdateItem; DWORD nValidCookies; ASSERT(NULL != m_pSyncMgrs); ASSERT(0 != dwNumCookies); ASSERT(NULL != pSubscriptionCookies); if ((0 == dwNumCookies) || (NULL == pSubscriptionCookies)) { return E_INVALIDARG; } // Check for global offline mode. if (!m_fForcedGlobalOnline && IsGlobalOffline()) { // Force global online mode so that our update will succeed. DBG("CThrottler::RunCookies; forcing global online mode"); SetGlobalOffline(FALSE); m_fForcedGlobalOnline = TRUE; } if (IsSyncEvent(dwSyncFlags, SYNCMGRFLAG_MANUAL) || IsSyncEvent(dwSyncFlags, SYNCMGRFLAG_INVOKE)) { if (!InternetGetConnectedStateEx(NULL, NULL, 0, 0)) { if (!InternetAutodial(INTERNET_AUTODIAL_FORCE_ONLINE, 0)) { // REARCHITECT clean up this extra addref/release/return stuff AddRef(); DBG("CThrottler::RunCookies autodial failed"); // Uh-oh. The user cancelled the dial after starting a // manual update. Clean up and return. if (m_fForcedGlobalOnline) { SetGlobalOffline(TRUE); m_fForcedGlobalOnline=FALSE; } WCHAR wszMsg[256]; MLLoadStringW(IDS_STATUS_ABORTED, wszMsg, ARRAYSIZE(wszMsg)); for (i=0; im_dwRunState &= ~RS_SUSPENDONIDLE; } if (IsSyncEventFlag(dwSyncFlags, SYNCMGRFLAG_MAYBOTHERUSER)) { // We may now bother user for this item pUpdateItem->m_dwRunState |= RS_MAYBOTHERUSER; } } else { ISubscriptionItem *psi; HRESULT hrItem = SubscriptionItemFromCookie(FALSE, pCookie, &psi); if (SUCCEEDED(hrItem)) { SUBSCRIPTIONITEMINFO sii; sii.cbSize = sizeof(SUBSCRIPTIONITEMINFO); hrItem = psi->GetSubscriptionItemInfo(&sii); if (SUCCEEDED(hrItem)) { DWORD dwRunState = RS_READY; if (IsSyncEvent(dwSyncFlags, SYNCMGRFLAG_IDLE)) { dwRunState |= RS_SUSPENDONIDLE; } if (IsSyncEventFlag(dwSyncFlags, SYNCMGRFLAG_MAYBOTHERUSER)) { dwRunState |= RS_MAYBOTHERUSER; } if (IsScheduleSyncEvent(dwSyncFlags)) { hrItem = CanScheduledItemRun(psi); } if (SUCCEEDED(hrItem)) { pUpdateItem = new CUpdateItem(*pCookie, dwRunState); if (NULL != pUpdateItem) { AddItemToListTail(pUpdateItem); } else { hrItem = E_OUTOFMEMORY; } } } psi->Release(); } if (FAILED(hrItem)) { // If we fail on an item, we will continue to try others, but // we need to indicate failure for this one. FailedUpdate(hrItem, pCookie); hr = S_FALSE; } } if (NULL == m_pSyncMgrs) { // We have been unadvised! break; } } // No point in trying to update if nobody wants to listen if (NULL != m_pSyncMgrs) { FillTheQueue(); } Release(); delete [] pCookies; if (0 == nValidCookies) { hr = E_FAIL; } // ************************************************************************ // No member variable access after this since we could be dead!!!! // ************************************************************************ } else { hr = E_OUTOFMEMORY; } return hr; } HRESULT CThrottler::FindCookie( const SUBSCRIPTIONCOOKIE *pSubscriptionCookie, CUpdateItem **ppUpdateItem) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr = E_FAIL; CUpdateItem *pItem = m_pItemsHead; ASSERT(NULL != ppUpdateItem); *ppUpdateItem = NULL; while (pItem) { if (pItem->m_cookie == *pSubscriptionCookie) { *ppUpdateItem = pItem; hr = S_OK; break; } pItem = pItem->m_pNext; } return hr; } //============================================================================== // // Auto cache size increase // //============================================================================== // We can return: // E_PENDING - agent will pause and wait to be resumed or aborted // INET_S_AGENT_INCREASED_CACHE_SIZE - agent will try making stuff sticky again // anything else - agent will abort with INET_E_AGENT_CACHE_SIZE_EXCEEDED HRESULT CThrottler::AutoCacheSizeRequest( const SUBSCRIPTIONCOOKIE *pSubscriptionCookie) { ASSERT(GetCurrentThreadId() == m_dwThreadId); HRESULT hr = S_OK; DWORD dwCacheSizeKB; int queueIndex; DWORD dwValue, dwSize = sizeof(dwValue); if (ERROR_SUCCESS == SHGetValue(HKEY_CURRENT_USER, c_szKeyRestrict, c_szCache, NULL, &dwValue, &dwSize) && (dwValue != 0)) { // Not allowed to change the cache size. hr = E_FAIL; } queueIndex = GetCookieIndexInQueue(pSubscriptionCookie); if (queueIndex >= 0) { if (!(m_updateQueue[queueIndex]->m_dwRunState & RS_MAYBOTHERUSER)) { // We're not allowed to bother user. Fail. hr = E_FAIL; } } else { DBG_WARN("CThrottler::AutoCacheSizeRequest couldn't find cookie in run queue."); hr = E_FAIL; // Couldn't find this cookie in our queue?! } if (SUCCEEDED(hr) && m_fAutoCacheSizePending) { // We're already asking the user to increase the cache size. hr = E_PENDING; } if (SUCCEEDED(hr)) { // Let's try to increase the cache. if (SUCCEEDED(IncreaseCacheSize(&dwCacheSizeKB))) { hr = INET_S_AGENT_INCREASED_CACHE_SIZE; } else { // We need to ask the user. if ((++ m_nAutoCacheSizeTimesAsked) > MAX_AUTOCACHESIZE_ASK) { hr = E_ABORT; // Already bothered them enough. } else { // Let's ask the user. We need unwind our call stack now, however. // Tell the throttler to ask the user if (SUCCEEDED(CreateThrottlerWnd())) { PostMessage(m_hwndThrottler, WM_THROTTLER_AUTOCACHESIZE_ASK, 0, dwCacheSizeKB); m_fAutoCacheSizePending = TRUE; hr = E_PENDING; } else { hr = E_FAIL; } } } } // !m_fAutoCacheSizePending if (hr == E_PENDING) { // Mark this agent as paused. int queueIndex = GetCookieIndexInQueue(pSubscriptionCookie); ASSERT(queueIndex >= 0); if (queueIndex >= 0) { m_updateQueue[queueIndex]->m_dwRunState &= ~RS_UPDATING; m_updateQueue[queueIndex]->m_dwRunState |= RS_SUSPENDED; m_updateQueue[queueIndex] = NULL; } } return hr; } HRESULT CThrottler::AutoCacheSizeAskUser(DWORD dwCacheSizeKB) { ASSERT(GetCurrentThreadId() == m_dwThreadId); ASSERT(m_fAutoCacheSizePending); ASSERT(dwCacheSizeKB); ASSERT(m_hwndParent); HRESULT hr = E_FAIL; // Keep-Alive AddRef(); if (IDOK == ShellMessageBox(MLGetHinst(), m_hwndParent, MAKEINTRESOURCE(IDS_CACHELIMIT_MESSAGE), MAKEINTRESOURCE(IDS_CACHELIMIT_TITLE), MB_OKCANCEL | MB_SETFOREGROUND | MB_ICONQUESTION)) { // Come up with a good cache size increase and resume agents m_dwAutoCacheSizeIncrease = dwCacheSizeKB / 4; if (m_dwAutoCacheSizeIncrease < MIN_CACHE_INCREASE) { m_dwAutoCacheSizeIncrease = MIN_CACHE_INCREASE; } m_dwMaxAutoCacheSize = dwCacheSizeKB + (2 * m_dwAutoCacheSizeIncrease); if (SUCCEEDED(IncreaseCacheSize(NULL))) { hr = S_OK; } } else { // Abort agents } m_fAutoCacheSizePending = FALSE; if (FAILED(hr)) { // User said no (or we couldn't increase the cache). ActuallyAbortAll(); } else { FillTheQueue(); } Release(); return hr; } // Auto-increase cache size if user previously ok'd it HRESULT CThrottler::IncreaseCacheSize(DWORD *pdwNewCacheSizeKB) { ASSERT(GetCurrentThreadId() == m_dwThreadId); LPINTERNET_CACHE_CONFIG_INFOA pCCI=NULL; DWORD dwSizeInKB=0, dwPercent; DWORD dwNewSizeInKB=0; HRESULT hr = E_FAIL; if (SUCCEEDED(GetCacheInfo(&pCCI, &dwSizeInKB, &dwPercent))) { if (dwSizeInKB < m_dwMaxAutoCacheSize) { ASSERT(m_dwAutoCacheSizeIncrease > 1023); // At least 1 meg if (m_dwAutoCacheSizeIncrease) { // We still have room to increase cache without asking the user. Use it. dwNewSizeInKB = dwSizeInKB + m_dwAutoCacheSizeIncrease; if (dwNewSizeInKB > m_dwMaxAutoCacheSize) { dwNewSizeInKB = m_dwMaxAutoCacheSize; } if (SUCCEEDED(SetCacheSize(pCCI, dwNewSizeInKB))) { hr = S_OK; dwSizeInKB = dwNewSizeInKB; DBG("Throttler just increased TIF cache size"); } } } MemFree(pCCI); } if (pdwNewCacheSizeKB) { *pdwNewCacheSizeKB = dwSizeInKB; } return hr; }