
dominique.schrecklin1.552494444672522E12 asked a question.
Local cached MSI does not get deleted when uninstalling
I have an InstallScript MSI Project. I activated the "Cache MSI locally" and indicated the "Cache Path" to "[LocalAppDataFolder]Downloaded Installations".
I had a look today into this cache path folder and was really surprised to find a list of folder, not only from the actual installed versions, but also uninstalled versions.
Does IS2008 not delete the cached MSI files when uninstalling a version?
How do I get IS2008 to delete the cached MSI files during uninstall?
It has always been true that cached msi's are left on the target machine. Years ago, we added script to all of our product deployments to clean up msi cache. The script looks at the cache and removes all but the latest one. Recently, when the path for cached msi's changed to [LocalAppDataPath], we adjusted our scirpt to compensate. If you have questions about how to do this, let me know and I will be glad to help. "
Yes, I would be interested in that script. Are you willing to share this code with us?
The process involves the following basic steps:
1. Rename the Release cache location:
Since msi's are cached under a GUID-named folder, typically in "Downloaded Installations", it is necessary to rename the release cache folder so that it can be easily located. So for instance, my setup by default may cache to a folder named "C:\Documents and Settings\MyLoginID\Local Settings\Application Data\Downloaded Installations\{2BCC2647-316E-4230-89DF-187395E8EEC5}". My problem will be that the cache for all installations will fall under "Downloaded Installations" in an ambiguous GUID-named folder.
To remedy this, go to Media > Releases in the InstallShield IDE and reset the "Cache path" under setup.exe properties to be more specific. Instead of just "[LocalAppDataFolder]Downloaded Installations", set the path to be "[LocalAppDataFolder]Downloaded Installations\APS1724\MyProgramName". "MyProgramName" is typically the title of your setup that you have in the General Information section of your InstallShield IDE.
Now the GUID-named folders for each particular installation will all fall under the folder for "MyProgramName". This simplifies the process much as you now only have to worry about deleting all but the newest one found in the folder.
2. Write the script to clean up the msi cache. Here is a simple example that pulls the title of the package, assuming that the cache folder name was the package title as mentioned above, and loops through the folder to remove all but the newest GUID-named cache folder found.
export prototype CleanMsiCache(HWND);
function CleanMsiCache(hMSI)
HWND hStream;
STRING sParent, sTitle[39];
NUMBER nDataType, nData, nValueBuf;
POINTER ptrFile;
begin
try
//get the title of this package
MsiGetSummaryInformation (hMSI, "", 0, hStream);
nValueBuf = SizeOf(sTitle);
MsiSummaryInfoGetProperty (hStream, 2, nDataType, nData, ptrFile, sTitle, nValueBuf);
MsiCloseHandle(hStream);
//cleaning can only be done if setup name is found
if (StrLength(sTitle) > 0) then
//purge all but the newest folder in the current cache
sParent = LocalAppDataFolder ^ "Downloaded Installations" ^ sTitle;
if (Is(PATH_EXISTS, sParent) = 1) then
CleanExtraReleaseFolders(hMSI, sParent);
endif;
endif;
catch
endcatch;
end;
prototype CleanExtraReleaseFolders(HWND, STRING);
function CleanExtraReleaseFolders(hMSI, sTargetFolder)
LIST lstDirs;
STRING sValue, sFileName, sCheck, sNewest, sNewestFolder;
STRING sSearchPath, sErrMsg;
NUMBER nResult, nResult2, nDataType, nData, nValueBuf;
VARIANT vErrNum;
begin
try
//check for any residual cache and clean up all but the newest one
lstDirs = ListCreate (STRINGLIST);
//Get all sub folders in the target folder (these will have a GUID name)
nResult = FindAllDirs (sTargetFolder, EXCLUDE_SUBDIR, lstDirs);
if (nResult = 0) then
nResult = ListGetFirstString (lstDirs, sValue);
while (nResult != END_OF_LIST)
//get the file name in the folder
nResult2 = FindAllFiles (sValue, "*.*", sFileName, RESET);
// Get the time the file was last updated
if (nResult2 = 0) then
//file was found in the folder for comparrison
if (sNewest = "") then
sNewest = sFileName; //save the filename
sNewestFolder = sValue; //save the folder name
else
nResult = FileCompare (sFileName, sNewest, COMPARE_DATE);
if nResult = GREATER_THAN then
sNewest = sFileName; //save the filename
sNewestFolder = sValue; //save the folder name
endif;
endif;
else
//no files in the folder, so delete it
DeleteDir(sValue, ROOT);
endif;
nResult = ListGetNextString (lstDirs, sValue);
endwhile;
endif;
/*Loop back through and deleate all but the Newest file.
If a newer file was not located, then cleanup is invalid
because there is nothing to base a comparrision on for cleaning.*/
if (StrLength(sNewestFolder) > 0) then
nResult = ListGetFirstString (lstDirs, sValue);
while (nResult != END_OF_LIST)
if (StrCompare(sValue, sNewestFolder) != 0) then
StrRemoveLastSlash(sValue);
//verify it is a msi cache GUID directory
StrSub(sCheck, sValue, StrLength(sValue) - 1, 1);
if (StrCompare(sCheck, "}") = 0) then
DeleteDir(sValue, ALLCONTENTS);
DeleteDir(sValue, ROOT);
endif;
endif;
nResult = ListGetNextString (lstDirs, sValue);
endwhile;
endif;
catch
endcatch;
end;
3. Schedule the custom action to do the cleaning.
Place the custom action in the Execute Sequence after Install Finalize. This will allow the current installation to complete and cache before cleaning is done. Set the condition on the CA to be "REMOVE <> "ALL"".
--------------------------------
You can generically add this script to all install packages and it will always clean all but the latest msi cache. Remember, it is important to leave the latest one cached on the target machine.
An important consideration here is that the location of the msi cache changed with the introduction of Vista/Server 2008. It used to be in the [WindowsFolder]\Downloaded Installations. It is now, as I mentioned above, by default, in the [LocalAppDataPath]Downloaded Installations folder. This was done to follow the new security protocol by Microsoft, where messing with the WindowsFolder is becoming considered a "no-no". This affected our considerations for cache cleanup, as we used to have a script that cleaned the [WindowsFolder] location. I therefore wrote a somewhat more involved script which check to see where the msi was last cached (as it could have been at [WindowsFolder]) and also check the current cache, still removing all but the newest one. For those who are concerned about cleaning the past cache location that may be out there one the target machine, I would be happy to give you that extended script.
Assuming an InstallScript Deffered/System CA and Setup.exe Cache Setting Of:
[CommonAppDataFolder]Downloaded Installations\MyCompany\MyProduct
[CODE]
Not REMOVE="ALL"/>
REMOVE="ALL"/>
[/CODE]
export prototype PurgeCache(HWND);
function PurgeCache(hMSI)
number nResult;
string szInstallMode;
string szCacheRoot;
string szDir;
string szPackageCode;
LIST listDirs;
begin
szInstallMode = MsiGetCustomActionDataAttribute( hMSI, "/InstallMode=" );
szCacheRoot = MsiGetCustomActionDataAttribute( hMSI, "/CacheRoot=" );
szPackageCode = MsiGetCustomActionDataAttribute( hMSI, "/PackageCode=" );
listDirs = ListCreate (STRINGLIST);
FindAllDirs( szCacheRoot, EXCLUDE_SUBDIR, listDirs );
nResult = ListGetFirstString (listDirs, szDir);
while (nResult != END_OF_LIST);
if ( szInstallMode = "Uninstall" || !( szDir &37; szPackageCode )) then
DeleteDir( szDir, ALLCONTENTS );
endif;
nResult = ListGetNextString (listDirs, szDir);
endwhile;
return ERROR_SUCCESS;
end;
You are correct in that the GUID is the Package Code. The reason that we did not search directly for this is that we do mostly major version upgrades, so the package code changes for each new version of the setup. The only code that remains the same is the Upgrade Code. In this scenario, the same setup will continue to multiply new GUID-named folders.
DeleteDir( szDir, ALLCONTENTS );
endif;
Right. I leave the current package in all situations except full uninstall. I schedule this at the very end of the installation transaction and the MSI cached DB should be being used. I don't think it should be a problem, do you?
I like what you wrote and I believe it will work fine. I would throw in a couple of points that may be of assistance.
1. The default location for the cache will be [LocalAppDataPath] and not [CommonAppDataPath]. Early on I pressed InstallShield support staff and developers hard on this issue, as I wanted to get this right since it goes in every package we write. They assured me that the correct path was [LocalAppDataPath]. I was concerned that the cache was being placed under each login rather than in a common location. They told me that this was expected behavior and not a problem and that the location Microsoft recommends is [LocalAppDataPath]. Here is the exact quote given to me from support:
"As we discussed on the phone, the LocalAppDataFolder is the recommended location to cache the MSI file. This is also recommended by Microsoft. This location should still be accessible if the Maintenance is being performed by another user in a Per-machine installation."
Of course, we do all per-machine installations. It is also worth noting that the LocalAppDataPath is the location that will be defaulted in in the newer versions of InstallShield.
2.) It may be of some concern to you as to the left-over cache from older installations of your products which were cached out in the "[WindowsFolder]Downloaded Installations" area. We wanted to get these cleaned up as well, so we added a bit to our script in all newer packages to look there for any legacy cache and purge them. Obviously, this will not be of concern to some as it was to us.