Clear ClickOnce Cache
Click-Once is a great technology that was shipped with Visual Studio 2005. It allows isolated, low impact, smart deployments of managed clients. There are two types of basic applications, Installed and Online. Installed applications behave like typical MSI installed applications in the sense, user gets start menu entry that can be used to launch the application and Add Remove Program entry that can be used to manage life time of an application. Online applications are intended to be used like a web page but richer. There is one problem though, for online application end user does not have any way of managing lifetime. Online applications get cached locally and scavenged based on disk usage.
Mage is the tool in .net SDK that allows user to clear online cache but the problem is it is shipped with SDK that means typical end user who does not have anything to do with code wont have that tool on their machine. Interestingly Mage uses an API from DFSHIM.dll that is used with runtime and hence can be used in better way than a command line tool so that end users can clean online cache.
So in this post I talk about two way I chose to use this API so that there can be a better experience around clearing online cache. The problem is broken down in two simple steps.
- Code that actually calls into DFSHIM to clear the cache.
- Code that exposes this functionality in two different way
- IE Tools custom menu (Exe, IOleCommandTarget Implementation) I did not want to go into details of IE Extensibility so more details are here
- Disk Cleanup Wizard in Windows
Code that calls into DFSHIM.dll
This is pretty straight forward code where you can load DFSHIM dll and call an entrypoint from that that dll. For now this is a generic function but for other examples we will convert this into COM friendly API.
int CleanCache(void)
{
typedef void* (*CREATEFUNCPTR)() ;
//Loads DFSHIM.dll
HINSTANCE hComponent = ::LoadLibraryW(L”dfshim.dll”);
if(hComponent != NULL)
{
//gets handle to CleanOnlineAppCache
CREATEFUNCPTR cleanOnlineCache = (CREATEFUNCPTR)::GetProcAddress(hComponent,
“CleanOnlineAppCache”);
if(cleanOnlineCache !=NULL)
{
//Calls clean and then unle
cleanOnlineCache();
}
else
goto Cleanup;
}
else
return 1;
Cleanup:
::FreeLibrary(hComponent);
return 0;
}
Exposing Through end user UI
Option1: Use IE Tools extensibility & Win32 EXE
This is by far the easiest method. You could create a simple Win32 EXE that basically shows some confirmation to user and calls into function similar to above.
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
TCHAR szTmpText[BUFFER_SIZE], szTmpTitle[BUFFER_SIZE];
//Get handle to calling EXE
HINSTANCE hComponent = ::GetModuleHandleW(NULL);
//get name and description from resources
int iNameLen = 1 + LoadString( hComponent,
IDS_MESSAGEBOXTEXT,
szTmpText,
ARRAYSIZE(szTmpText));
int iDescLen = 1 + LoadString( hComponent,
IDS_MESSAGEBOXTITLE,
szTmpTitle,
ARRAYSIZE(szTmpTitle));
// meaning LoadString returned 0 (error)
if ( (iNameLen == 1) || (iDescLen == 1) )
return 1;
if( ::MessageBoxW(NULL, szTmpText, szTmpTitle, MB_ICONQUESTION
|MB_OKCANCEL) == IDOK)
{
return CleanCache();
}
return 0;
}
Option2 : Use IE Tools extensibility & IOleCommandTarget
For this to work your component need to implement IOleCommandTarget and do the necessary COM registration. This offcourse gets loaded IE process. IE does not support loading of out of proc com servers through this mechanism. Here is the sample implementation of IOleCommandTarget
//////////////////////////////////////////////////////////////////////////////////////
// IOleCommandTarget Implementation
//////////////////////////////////////////////////////////////////////////////////////
HRESULT __stdcall ClearCache::QueryStatus(
const GUID *pguidCmdGroup,// Pointer to command group
ULONG cCmds, // Number of commands in prgCmds array
OLECMD *prgCmds, // Array of commands
OLECMDTEXT *pCmdText // Pointer to name or status of command
)
{
return S_OK;
}
HRESULT __stdcall ClearCache::Exec(
const GUID *pguidCmdGroup, // Pointer to command group
DWORD nCmdID, // Identifier of command to execute
DWORD nCmdExecOpt, // Options for executing the command
VARIANTARG *pvaIn, // Pointer to input arguments
VARIANTARG *pvaOut // Pointer to command output
)
{
HRESULT hr = S_FALSE;
if( ::MessageBoxW(NULL, L”Do you want to clean?”, L”Clean Cache”, MB_ICONQUESTION
|MB_OKCANCEL) == IDOK)
{
hr = CleanClickOnceCache();
}
return hr;
}
Here is the same function where it returns HRESULT instead of int
/*
This function uses CleanOnlineCache API that is available
from dfshim.dll to clean online cache
*/
HRESULT __stdcall ClearCache::CleanClickOnceCache()
{
HRESULT hr = S_FALSE;
//Loads DFShim.dll
HINSTANCE hComponent = ::LoadLibraryW(DFSHIM_DLL);
if(hComponent != NULL)
{
//gets handle to CleanOnlineAppCache
CREATEFUNCPTR cleanOnlineCache = (CREATEFUNCPTR)::GetProcAddress(hComponent,
“CleanOnlineAppCache”);
if(cleanOnlineCache !=NULL)
{
//Calls clean and then unle
cleanOnlineCache();
hr = S_OK;
}
else
goto Cleanup;
}
else
return hr;
Cleanup:
::FreeLibrary(hComponent);
return hr;
}
Option2: Use Shell Disk Cleanup Wizard extensibility & IEmptyVolumeCache2
This implements IEmptyVolumeCache2 interface that makes cache cleaner appear in the disk cleaner wizard. Here is the sample implementation of IEmptyVolumeCache2. Though this is a difficult to discover compared to other options, this one also works if user is not using IE as the browser.
Useful links:
///////////////////////////////////////////////////////////
//
// IEmptyVolumeCache2 Implementation
//
///////////////////////////////////////////////////////////
/*
This function is called during initialization and is used to display
name and description in the disk utility UI
*/
HRESULT __stdcall CacheCleaner::InitializeEx(HKEY hkRegKey,
LPCWSTR pcwszVolume,
LPCWSTR pcwszKeyName,
LPWSTR *ppwszDisplayName,
LPWSTR *ppwszDescription,
LPWSTR *ppwszBtnText,
DWORD *pdwFlags)
{
return (this->Initialize(hkRegKey, pcwszVolume, ppwszDisplayName, ppwszDescription, pdwFlags));
}
/*
This function is called during initialization for downlevel 9X os.
Since this will only run on XP/2003 or Windows Vista, this function
is not implemented.
*/
HRESULT __stdcall CacheCleaner::Initialize(HKEY hkRegKey,
LPCWSTR pcwszVolume,
LPWSTR *ppwszDisplayName,
LPWSTR *ppwszDescription,
DWORD *pdwFlags)
{
HRESULT hr = E_FAIL;
if(!pdwFlags)
return E_INVALIDARG;
//To get the name and description string, load the component dll and get resources from it.
//Declare required variables
TCHAR szTmpName[BUFFER_SIZE], szTmpDesc[BUFFER_SIZE];
HKEY cleanerPathKey;
WCHAR cleanerPath[BUFFER_SIZE];
DWORD dwcleanerPathLen=BUFFER_SIZE -1;
//open regkey where path to DLL is stored
hr = ::RegOpenKeyExW(HKEY_CLASSES_ROOT,
CACHECLEANER_REGISTRY_PATH,
0,
KEY_QUERY_VALUE,
&cleanerPathKey);
if(FAILED(hr))
goto Cleanup;
//get the location of DLL
hr = RegQueryValueEx( cleanerPathKey,
NULL,
NULL,
NULL,
(LPBYTE) cleanerPath,
&dwcleanerPathLen);
//close the reg key
RegCloseKey(cleanerPathKey);
if(FAILED(hr))
goto Cleanup;
if (SUCCEEDED(hr))
{
//Get handle to dll module
HINSTANCE hComponent = ::GetModuleHandleW(cleanerPath);
//get name and description from resources
int iNameLen = 1 + LoadString( hComponent,
IDS_CACHECLEANER_NAME,
szTmpName,
ARRAYSIZE(szTmpName));
int iDescLen = 1 + LoadString( hComponent,
IDS_CACHECLEANER_DESC,
szTmpDesc,
ARRAYSIZE(szTmpDesc));
// meaning LoadString returned 0 (error)
if ( (iNameLen == 1) || (iDescLen == 1) )
return E_FAIL;
//Allocate memory to store display name
*ppwszDisplayName = (LPWSTR)CoTaskMemAlloc( iNameLen*sizeof(WCHAR) );
if ( !*ppwszDisplayName )
return E_OUTOFMEMORY;
//Allocate memory to store description
*ppwszDescription = (LPWSTR)CoTaskMemAlloc( iDescLen*sizeof(WCHAR) );
if ( !*ppwszDescription )
{
CoTaskMemFree(ppwszDisplayName);
*ppwszDisplayName = NULL;
return E_OUTOFMEMORY;
}
//copy name and description from buffer
hr = StringCchCopy(*ppwszDisplayName, iNameLen, szTmpName);
if(FAILED(hr))
goto Cleanup;
hr = StringCchCopy(*ppwszDescription, iDescLen, szTmpDesc);
if(FAILED(hr))
goto Cleanup;
}
/*
//This flag is set if the disk cleanup manager is being run on a schedule.
//You must assign values to the ppwszDisplayName and ppwszDescription parameters.
//If this flag is set, the disk cleanup manager will not call IEmptyVolumeCache::GetSpaceUsed,
//IEmptyVolumeCache::Purge, or IEmptyVolumeCache::ShowProperties.
//Because IEmptyVolumeCache::Purge will not be called,
//cleanup must be handled by IEmptyVolumeCache::Initialize.
//The handler should ignore the pcwszVolume parameter and clean up any unneeded files
//regardless of what drive they are on.
*/
if(*pdwFlags == EVCF_SETTINGSMODE)
return(CleanClickOnceCache());
//
//ClickOnce writes the cache under %HOMEDRIVE%\Documents and Settings.
//When not in scheduler mode this utility will only show up in the list
//if the drive user has selected is %HOMEDRIVE%
WCHAR homeDrive[5];
hr = ::ExpandEnvironmentStrings(ENVIRONMENTVARIABLE_HOMEDRIVE,
homeDrive,
ARRAYSIZE(homeDrive));
if(FAILED(hr))
goto Cleanup;
//Drive selected by user does not have clickonce cache
//hence return without initialization
if (pcwszVolume[0] ==homeDrive[0])
return S_OK;
else
return S_FALSE;
Cleanup:
return hr;
}
/*
This function is used to get the size of memory that will be free after you run this
utility.
*/
HRESULT __stdcall CacheCleaner::GetSpaceUsed(DWORDLONG *pdwlSpaceUsed,
IEmptyVolumeCacheCallBack *picb)
{
//Calls internal GetClickOnceCacheSize to get the size of online cache used
return(GetClickOnceCacheSize(pdwlSpaceUsed));
}
/*
This function actually cleans the cache and is called when user selects this
utility from list box.
*/
HRESULT __stdcall CacheCleaner::Purge(DWORDLONG dwlSpaceToFree,
IEmptyVolumeCacheCallBack *picb)
{
//Calls internal CleanClickOnceCache to clean the cache
return(CleanClickOnceCache());
}
/*
Since there are no properties to show for this utility. This function is not
implemented
*/
HRESULT __stdcall CacheCleaner::ShowProperties(HWND hwnd)
{
return S_FALSE;
}
/*
No Clean up necessary, hence function is not implemented
*/
HRESULT __stdcall CacheCleaner::Deactivate(DWORD *pdwFlags)
{
return S_OK;
}