Professional, Software

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:

Creating Disk Cleanup Handler

///////////////////////////////////////////////////////////
//
// 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;
}

 

 

Advertisements
Standard

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s