#include "shellprv.h" #include "util.h" #include "ids.h" #include "bitbuck.h" #include "mtpt.h" // states for state machine, values are relavant as we compare them // durring transitions to see if UI should be triggered or not typedef enum { STATE_1MB = 0, // the "disk is completely filled" case STATE_50MB = 1, // < 50MB case STATE_80MB = 2, // < 80MB case STATE_200MB = 3, // < 200MB case, only do this one on > 2.25GB drives STATE_ALLGOOD = 4, // > 200MB, everything is fine STATE_UNKNOWN = 5, } LOWDISK_STATE; #define BYTES_PER_MB ((ULONGLONG)0x100000) typedef struct { LOWDISK_STATE lds; ULONG dwMinMB; // range (min) that defines this state ULONG dwMaxMB; // range (max) that defines this state DWORD dwCleanupFlags; // DISKCLEANUP_ DWORD dwShowTime; // in sec DWORD dwIntervalTime; // in sec UINT cRetry; DWORD niif; } STATE_DATA; #define HOURS (60 * 60) #define MINS (60) const STATE_DATA c_state_data[] = { {STATE_1MB, 0, 1, DISKCLEANUP_VERYLOWDISK, 30, 5 * MINS, -1, NIIF_ERROR}, {STATE_50MB, 1, 50, DISKCLEANUP_VERYLOWDISK, 30, 5 * MINS, -1, NIIF_WARNING}, {STATE_80MB, 50, 80, DISKCLEANUP_LOWDISK, 30, 4 * HOURS, 1, NIIF_WARNING}, {STATE_200MB, 80, 200, DISKCLEANUP_LOWDISK, 30, 0 * HOURS, 0, NIIF_INFO}, }; void SRNotify(LPCWSTR pszDrive, DWORD dwFreeSpaceInMB, BOOL bImproving) { typedef void (* PFNSRNOTIFYFREESPACE)(LPCWSTR, DWORD, BOOL); static HMODULE s_hmodSR = NULL; if (NULL == s_hmodSR) s_hmodSR = LoadLibrary(TEXT("srclient.dll")); if (s_hmodSR) { PFNSRNOTIFYFREESPACE pfnNotifyFreeSpace = (PFNSRNOTIFYFREESPACE)GetProcAddress(s_hmodSR, "SRNotify"); if (pfnNotifyFreeSpace) pfnNotifyFreeSpace(pszDrive, dwFreeSpaceInMB, bImproving); } } class CLowDiskSpaceUI : IQueryContinue { public: CLowDiskSpaceUI(int iDrive); void CheckDiskSpace(); // IUnknown STDMETHODIMP QueryInterface(REFIID riid, void **ppv); STDMETHODIMP_(ULONG) AddRef(); STDMETHODIMP_(ULONG) Release(); // IQueryContinue STDMETHODIMP QueryContinue(); // S_OK -> Continue, other private: ~CLowDiskSpaceUI(); BOOL _EnterExclusive(); void _LeaveExclusive(); void _DoNotificationUI(); void _DoStateMachine(); const STATE_DATA *_StateData(LOWDISK_STATE lds); LOWDISK_STATE _StateFromFreeSpace(ULARGE_INTEGER ulTotal, ULARGE_INTEGER ulFree); LOWDISK_STATE _GetCurrentState(BOOL bInStateMachine); static DWORD CALLBACK s_ThreadProc(void *pv); LONG _cRef; TCHAR _szRoot[5]; HANDLE _hMutex; LOWDISK_STATE _ldsCurrent; BOOL _bShowUI; BOOL _bSysVolume; DWORD _dwLastFreeMB; }; CLowDiskSpaceUI::CLowDiskSpaceUI(int iDrive) : _cRef(1), _ldsCurrent(STATE_UNKNOWN) { ASSERT(_bShowUI == FALSE); ASSERT(_bSysVolume == FALSE); PathBuildRoot(_szRoot, iDrive); TCHAR szWinDir[MAX_PATH]; if (GetWindowsDirectory(szWinDir, ARRAYSIZE(szWinDir))) { _bSysVolume = szWinDir[0] == _szRoot[0]; } } CLowDiskSpaceUI::~CLowDiskSpaceUI() { if (_hMutex) CloseHandle(_hMutex); } HRESULT CLowDiskSpaceUI::QueryInterface(REFIID riid, void **ppv) { static const QITAB qit[] = { QITABENT(CLowDiskSpaceUI, IQueryContinue), { 0 }, }; return QISearch(this, qit, riid, ppv); } STDMETHODIMP_(ULONG) CLowDiskSpaceUI::AddRef() { return InterlockedIncrement(&_cRef); } STDMETHODIMP_(ULONG) CLowDiskSpaceUI::Release() { ASSERT(0 != _cRef); ULONG cRef = InterlockedDecrement(&_cRef); if (0 == cRef) { delete this; } return cRef; } HRESULT CLowDiskSpaceUI::QueryContinue() { LOWDISK_STATE ldsOld = _ldsCurrent; return ldsOld == _GetCurrentState(TRUE) ? S_OK : S_FALSE; } const STATE_DATA *CLowDiskSpaceUI::_StateData(LOWDISK_STATE lds) { for (int i = 0; i < ARRAYSIZE(c_state_data); i++) { if (c_state_data[i].lds == lds) return &c_state_data[i]; } return NULL; } LOWDISK_STATE CLowDiskSpaceUI::_StateFromFreeSpace(ULARGE_INTEGER ulTotal, ULARGE_INTEGER ulFree) { ULONGLONG ulTotalMB = (ulTotal.QuadPart / BYTES_PER_MB); ULONGLONG ulFreeMB = (ulFree.QuadPart / BYTES_PER_MB); _dwLastFreeMB = (DWORD)ulFreeMB; for (int i = 0; i < ARRAYSIZE(c_state_data); i++) { // total needs to be 8 times the max of this range for us to consider it if ((ulTotalMB / 8) > c_state_data[i].dwMaxMB) { if ((c_state_data[i].lds == _ldsCurrent) ? ((ulFreeMB >= c_state_data[i].dwMinMB) && (ulFreeMB <= (c_state_data[i].dwMaxMB + 3))) : ((ulFreeMB >= c_state_data[i].dwMinMB) && (ulFreeMB <= c_state_data[i].dwMaxMB))) { // only report 200MB state on drives >= 2.25GB if ((c_state_data[i].lds != STATE_200MB) || (ulTotal.QuadPart >= (2250 * BYTES_PER_MB))) return c_state_data[i].lds; } } } return STATE_ALLGOOD; } LOWDISK_STATE CLowDiskSpaceUI::_GetCurrentState(BOOL bInStateMachine) { LOWDISK_STATE ldsNew = STATE_ALLGOOD; // assume this in case of failure ULARGE_INTEGER ulTotal, ulFree; if (SHGetDiskFreeSpaceEx(_szRoot, NULL, &ulTotal, &ulFree)) { ldsNew = _StateFromFreeSpace(ulTotal, ulFree); } if (bInStateMachine) { if (_ldsCurrent != ldsNew) { // state change // if things are getting worse need to show UI (if we are in the state machine) _bShowUI = (ldsNew < _ldsCurrent); SRNotify(_szRoot, _dwLastFreeMB, ldsNew > _ldsCurrent); // call system restore } _ldsCurrent = ldsNew; } return ldsNew; } // creates the notification icon in the tray and shows a balloon with it // this is a modal call, when it returns either the UI has timed out or the // user has clicked on the notification UI. void CLowDiskSpaceUI::_DoNotificationUI() { // assume this will be a one shot UI, but this can get reset via our callback _bShowUI = FALSE; const STATE_DATA *psd = _StateData(_ldsCurrent); if (psd && (_bSysVolume || (psd->lds <= STATE_80MB))) { IUserNotification *pun; HRESULT hr = CoCreateInstance(CLSID_UserNotification, NULL, CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER, IID_PPV_ARG(IUserNotification, &pun)); if (SUCCEEDED(hr)) { SHFILEINFO sfi = {0}; SHGetFileInfo(_szRoot, FILE_ATTRIBUTE_DIRECTORY, &sfi, sizeof(sfi), SHGFI_USEFILEATTRIBUTES | SHGFI_DISPLAYNAME | SHGFI_ICON | SHGFI_SMALLICON | SHGFI_ADDOVERLAYS); TCHAR szTitle[64], szMsg[256], szTemplate[256]; UINT niif = _bSysVolume ? psd->niif : NIIF_INFO; LoadString(HINST_THISDLL, IDS_DISK_FULL_TITLE, szTitle, ARRAYSIZE(szTitle)); LoadString(HINST_THISDLL, NIIF_INFO == niif ? IDS_DISK_FULL_TEXT : IDS_DISK_FULL_TEXT_SERIOUS, szTemplate, ARRAYSIZE(szTemplate)); StringCchPrintf(szMsg, ARRAYSIZE(szMsg), szTemplate, sfi.szDisplayName); pun->SetIconInfo(sfi.hIcon, szTitle); pun->SetBalloonRetry(psd->dwShowTime * 1000, psd->dwIntervalTime * 1000, psd->cRetry); // pun->SetBalloonInfo(szTitle, L" Click here for notepad", niif); pun->SetBalloonInfo(szTitle, szMsg, niif); hr = pun->Show(SAFECAST(this, IQueryContinue *), 1 * 1000); // 1 sec poll for callback if (sfi.hIcon) DestroyIcon(sfi.hIcon); if (S_OK == hr) { // S_OK -> user click on icon or balloon LaunchDiskCleanup(NULL, DRIVEID(_szRoot), (_bSysVolume ? psd->dwCleanupFlags : 0) | DISKCLEANUP_MODAL); } pun->Release(); } } } void CLowDiskSpaceUI::_DoStateMachine() { do { if (_bShowUI) { _DoNotificationUI(); } else { SHProcessMessagesUntilEvent(NULL, NULL, 5 * 1000); // 5 seconds } } while (STATE_ALLGOOD != _GetCurrentState(TRUE)); } BOOL CLowDiskSpaceUI::_EnterExclusive() { if (NULL == _hMutex) { TCHAR szEvent[32]; StringCchPrintf(szEvent, ARRAYSIZE(szEvent), TEXT("LowDiskOn%C"), _szRoot[0]); // ok to truncate _hMutex = CreateMutex(SHGetAllAccessSA(), FALSE, szEvent); } return _hMutex && WAIT_OBJECT_0 == WaitForSingleObject(_hMutex, 0); // zero timeout } void CLowDiskSpaceUI::_LeaveExclusive() { ASSERT(_hMutex); ReleaseMutex(_hMutex); } DWORD CALLBACK CLowDiskSpaceUI::s_ThreadProc(void *pv) { CLowDiskSpaceUI *pldsui = (CLowDiskSpaceUI *)pv; if (pldsui->_EnterExclusive()) { pldsui->_DoStateMachine(); pldsui->_LeaveExclusive(); } pldsui->Release(); return 0; } void CLowDiskSpaceUI::CheckDiskSpace() { if (STATE_ALLGOOD != _GetCurrentState(FALSE)) { AddRef(); if (!SHCreateThread(s_ThreadProc, this, CTF_COINIT, NULL)) { Release(); } } } STDAPI CheckDiskSpace() { // the only caller of this is in explorer\tray.cpp // it checks against SHRestricted(REST_NOLOWDISKSPACECHECKS) on that side. for (int i = 0; i < 26; i++) { CMountPoint* pmp = CMountPoint::GetMountPoint(i); if (pmp) { if (pmp->IsFixedDisk() && !pmp->IsRemovableDevice()) { CLowDiskSpaceUI *pldsui = new CLowDiskSpaceUI(i); if (pldsui) { pldsui->CheckDiskSpace(); pldsui->Release(); } } pmp->Release(); } } return S_OK; } STDAPI_(BOOL) GetDiskCleanupPath(LPTSTR pszBuf, UINT cchSize) { if (pszBuf) *pszBuf = 0; DWORD cbLen = CbFromCch(cchSize); return SUCCEEDED(SKGetValue(SHELLKEY_HKLM_EXPLORER, TEXT("MyComputer\\cleanuppath"), NULL, NULL, pszBuf, &cbLen)); } STDAPI_(void) LaunchDiskCleanup(HWND hwnd, int iDrive, UINT uFlags) { TCHAR szPathTemplate[MAX_PATH]; if (GetDiskCleanupPath(szPathTemplate, ARRAYSIZE(szPathTemplate))) { TCHAR szPath[MAX_PATH], szArgs[MAX_PATH]; HRESULT hr; BOOL fExec = FALSE; hr = StringCchPrintf(szPath, ARRAYSIZE(szPath), szPathTemplate, TEXT('A') + iDrive); if (SUCCEEDED(hr)) { if (uFlags & DISKCLEANUP_LOWDISK) { hr = StringCchCat(szPath, ARRAYSIZE(szPath), TEXT(" /LOWDISK")); } else if (uFlags & DISKCLEANUP_VERYLOWDISK) { hr = StringCchCat(szPath, ARRAYSIZE(szPath), TEXT(" /VERYLOWDISK")); } if (SUCCEEDED(hr)) { hr = PathSeperateArgs(szPath, szArgs, ARRAYSIZE(szArgs), NULL); if (SUCCEEDED(hr)) { SHELLEXECUTEINFO ei = { sizeof(ei), SEE_MASK_FLAG_NO_UI | SEE_MASK_NOCLOSEPROCESS, NULL, NULL, szPath, szArgs, NULL, SW_SHOWNORMAL, NULL }; if (ShellExecuteEx(&ei)) { fExec = TRUE; if (ei.hProcess) { if (DISKCLEANUP_MODAL & uFlags) SHProcessMessagesUntilEvent(NULL, ei.hProcess, INFINITE); CloseHandle(ei.hProcess); } } } } } if (!fExec) { ShellMessageBox(HINST_THISDLL, NULL, MAKEINTRESOURCE(IDS_NO_CLEANMGR_APP), NULL, MB_OK | MB_ICONEXCLAMATION | MB_SETFOREGROUND); } } } // public export STDAPI_(void) SHHandleDiskFull(HWND hwnd, int idDrive) { // legacy, no one calls this }