cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
tcom36
Level 7

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?
Labels (1)
0 Kudos
(28) Replies
ITI_Randy
Level 6

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.
0 Kudos
Christopher_Pai
Level 16

Would you be willing to share that code? I started writing such a script last night but I'm interested to see how others are doing it to see if there are any aspects that I'm missing.
0 Kudos
tcom36
Level 7

ITI_Randy wrote:
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?
0 Kudos
ITI_Randy
Level 6

For those who have requested this, I am more than happy to share this code to clean the msi cache on target machines. The example below is for basic msi setups, but can easily be adapted for other project types.

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.
0 Kudos
MichaelU
Level 12 Flexeran
Level 12 Flexeran

If it helps any, I'm pretty sure the GUID used in the subfolder name is the package code of the cached setup...
0 Kudos
Christopher_Pai
Level 16

Here is what I wrote last night, I'd appreciate feedback. Basically I have a set of mutually exclusive CustomActionData strings that pass in the cache location, current package code and installation mode ( uninstall or everything else ). Inside the script I get a list of cached packages and during uninstall I remove them all and during everything else I remove all of them except for the one currently being installed/upgraded/repaired.


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 % szPackageCode )) then
DeleteDir( szDir, ALLCONTENTS );
endif;
nResult = ListGetNextString (listDirs, szDir);

endwhile;

return ERROR_SUCCESS;

end;
0 Kudos
ITI_Randy
Level 6

Michael,

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.
0 Kudos
ITI_Randy
Level 6

Just as a side note, when we started doing cache cleanup, we were warned by InstallShield support early on that there are some issues that could cause us real problems if we are not careful to leave the last cached msi. This is why we looped through the folders deleting all but the latest. In testing, we found that we could really lock up a machine when removing all and then accidently removing a protected component, triggering repair. We would then get caught in a loop with the target machine asking us to locate the msi so it could repair the damage. Even clicking on a desktop icon or attempting to open another app would start the endless loop and message boxes asking to locate the deleted msi cache.
0 Kudos
Christopher_Pai
Level 16

if ( szInstallMode = "Uninstall" || !( szDir % szPackageCode )) then
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?
0 Kudos
ITI_Randy
Level 6

Chris,

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.
0 Kudos
Christopher_Pai
Level 16

I think they are wrong. 🙂 Maybe I am?

I don't like the location because if User A performs a Per-Machine install and an administrator later deletes User A's profile ( he was a slacker that spent too much time on blogs 🙂 ) then User B is going to be screwed when he needs to do a repair.

Or, if User A later isn't fired but instead just has his admin privs taken away. He'll have write access to the MSI stored in his profile and he could use that to inject untrusted code into the package ( I know, it's a stretch, but he was being disciplined for being a slacker spending to much time on MSI blogs hehehe ) and use that code to gain admin privs during a maintenance or repair operation.

Eitherway, the CacheRoot property can be set to whatever you choose in releases. It's not hard coded into the script file. The real key is I simplfy the question of `which packages` by dropping them into Company/Product folders.

Yes, the legacy location wasn't a worry since this is a newly ( to be ) deployed product. Calling the MSI API would make it easier to find packaged cached on a local drive where the upgrade code is in common but some of these calls aren't supported in MSI 2.0 and I really don't want to have to add 3.1/4.5 to my package since I otherwise have no need for it.
0 Kudos
ITI_Randy
Level 6

Chris,

Thanks for your ideas. I especially like your logic which optomizes what we had previously done in that it gets the cache location directly from the cache property (wherever that is), instead of the more indirect way of constructing the path in the script; and also in looking at the current Product Code rather than searching for the newest folder in the cache, eliminating the need to loop twice.

I noticed that you do a "DeleteDir( szDir, ALLCONTENTS );" At one point we did this the same way and found that for some reason it didn't work consistently. Sometimes the folder contents would go away but the empty folder would remain. I remembered this in looking at the old code where we tried to hack around this by first doing a "DeleteDir(sValue, ALLCONTENTS);" and then adding an extra line "DeleteDir(sValue, ROOT);" trying to make sure that the directory goes away. As I recall, we still got inconsistent results. Have you noticed any problems with this in your testing?
0 Kudos
Christopher_Pai
Level 16

I've not observed a problem with the DeleteDir() function yet. If you notice, I'm not deleting the CompanyName\ProductName folder on uninstall. It would only take a few lines of code to do that but I wasn't too worried since I'm not really a `purist`. It's the space consumed by the MSI's that are my real worry. So I suppose if DeleteDir was to wack an MSI and leave a directory, I probably wouldn't be too worried.
0 Kudos
Stefan_Krueger
Level 9

Did you think about these situations:

How does this work with minor update patches? Do they get cached as well (by update.exe)? In this case the previous version (i.e. the full .msi) must not be removed from the cache.

What would happen if a complete uninstall was initiated from double clicking setup.exe from the cache location?

What will happen if the uninstall is canceled and rolled back, maybe as part of a failed major upgrade?

Maybe it would be better to postpone the removal of the current version until the next reboot (doesn't Windows Installer do the same for the cached msi file)?
Stefan Krueger
InstallSite.org
0 Kudos
Christopher_Pai
Level 16

Hi Stefan-

Sorry, I never noticed your comments until today. I turned subscriptions off due to spam. I'll run through those test cases and see if I can break it.

Thanks,
Chris
0 Kudos
Marc75
Level 4

Hi,

why doesn't InstallShield support this feature out-of-the-box?
They are the ones that give an option to locally cache the package, why don't they remove or at least give an option to remove these cached packages automatically?

I've been using InstallShield for years now and I just stumbled across the cache location and found many locally cached packages for the same products!

In my opinion the InstallShield product misses a big issue here! I'm guessing lots of setup administrators do not even know about this ....feature!It is even not mentioned anywhere in the InstallShield documentation.

Hopefully InstallShield will add this 'delete cache'-feature in a future version! Where can I vote for it? :rolleyes:

Marc
0 Kudos
ITI_Randy
Level 6

In the release properties of your project, you can optionally choose to not cache at all. We prefer to allow the cache, for various reasons, while we don't want to allow a lot of this to pile up, as we typically have numerous programs on a given server which get updated several times a year, causing the cache to grow quite large. Not to mention that it is ever-growing and never being cleaned. We have used our own simple cache clean script now for years without issues. We just place it in every package and run it at the end of the install after InstallFinalize in the Execute sequence.

You might ask InstallShield support why they have not offered this option in the basic msi type of install package. My suspicion is that it can be dangerous if not done properly. We noticed that if we dumped ALL of the cached msi's for a product on a machine, then broke the install by deleting a needed component, the Windows Installer tries to repair it each time the user tries to access anything on the machine (even when it's not the damaged program). If it can't find the msi, it prompts the user to locate the msi so it can do the repair. If the msi is gone, the user is helpless and the machine is stuck in a nasty loop trying to repair. For this reason, we are careful in our script to delete all but the latest cached msi, always leaving the one from the latest install.
0 Kudos
Marc75
Level 4

Hi Randy,

you are right. It is dangerous if not done properly. Therefore, InstallShield should provide support for this feature in the first place.

The simple cache clean script you are using, is it still the same as listed in the beginning of th thread? If not, would you please be so kind to share it with us?

I really hope InstallShield will implement this in a future release. It will save a lot of work and a lot of disk space for anyone! 😛

Thanks!
Marc
0 Kudos
ITI_Randy
Level 6

The code here is a little more involved as we set the cache location property on the release to [LocalAppDataFolder]Downloaded Installations\[ProductName]. The recommended location was LocalAppDataFolder, but we have since realized that it may be better to sue the CommonAppDataFolder. We were originally told to use LocalAppDataFolder, which is the default in the InstallShield IDE. You can use either.

Also, we used to cache in other locations and wanted to clean up all places. The script allows for this, as it checks the last cache path each time and cleans up that one also. This helps if it changed, which we have seen it do.

My script is in 3 parts, only one is exported. The other 2 are private helper methods...

/*****************CLEAN MSI CACHE*****************/
export prototype CleanMsiCache(HWND);
prototype STRING FindLastCacheLocation(HWND);
prototype RemoveOldCache(HWND, STRING);

function CleanMsiCache(hMSI)
NUMBER nResult, nSize;
STRING sSourcePath, sPackageCode, sDir, sErrMsg;
LIST listDirs;
begin
try
MsiGetProperty(hMSI, "SourceDir", sSourcePath, nSize);
MsiGetProperty(hMSI, "PackageCode", sPackageCode, nSize);
StrReplace(sSourcePath, sPackageCode + "\\", "", 0);

//Remove legacy cache under other logins
RemoveOldCache(hMSI, sSourcePath);

listDirs = ListCreate (STRINGLIST);
FindAllDirs(sSourcePath, EXCLUDE_SUBDIR, listDirs);
nResult = ListGetFirstString (listDirs, sDir);

//Delete all but the current PackageCode in cache
while (nResult != END_OF_LIST);
if (!(sDir % sPackageCode)) then
DeleteDir(sDir, ALLCONTENTS);
endif;
nResult = ListGetNextString (listDirs, sDir);
endwhile;
return ERROR_SUCCESS;
catch
endcatch;
end;

function RemoveOldCache(hMSI, sCurrentCache)
STRING sCheck, sCache, sParent, sTitle, sLastCache, sErrMsg;
NUMBER nDataType, nData, nValueBuf, nLocation, nSize;
begin
try
sTitle = PackageTitle(hMSI);

if (StrLength(sTitle) > 0) then
//Anything in "[WindowsFolder]\Download Installations\" is not valid
sCheck = WindowsFolder ^ "Downloaded Installations" ^ sTitle;
if (Is(PATH_EXISTS, sCheck) = 1) then
DeleteDir(sCheck, ROOT);
endif;
endif;

sLastCache = FindLastCacheLocation(hMSI);

if (Is(PATH_EXISTS, sLastCache) = 1) then
/*Don't just always remove the last cached because the
user could be running in Maintenance Mode, in which case
the current cache is the same as the last cache, so it can't
be deleted.*/

nLocation = StrFindEx (sLastCache, sTitle, 0);
if (nLocation >= 0) then
StrSub(sParent, sLastCache, 0, nLocation + StrLength(sTitle));
StrRemoveLastSlash(sParent);

if (Is(PATH_EXISTS, sParent) = 1) then
if (! (sLastCache % sCurrentCache)) then
/*The last cache location is different from the current one and is therefore
invalid, so delete the entire parent directory and all of its subfolders.
***For safety, only delete the folder if its path name ends with the package title*/
StrSub(sCheck, sParent, StrLength(sParent) - StrLength(sTitle), StrLength(sTitle));
if (StrCompare(sCheck, sTitle) = 0) then
DeleteDir(sParent, ROOT);
endif;
endif;
endif;
endif;
endif;
catch
endcatch;
end;

function STRING FindLastCacheLocation(hMSI)
STRING sValue, sPathValue, sSearchValue, sProductName, sLastCache, sKey;
STRING sNumName, sSearchPath, sErrMsg;
NUMBER nType, nReturn, nResult, nSubResult, nSize;
LIST listSubKeys, listValues;

#define REGKEYVAL "Software\\Classes\\Installer\\Products"
begin
try
//get this install's 'ProductName'
MsiGetProperty(hMSI, "ProductName", sProductName, nSize);

//search for last installed location in registry
RegDBSetDefaultRoot (HKEY_LOCAL_MACHINE);
listSubKeys = ListCreate(STRINGLIST);

nReturn = RegDBQueryKey(REGKEYVAL, REGDB_KEYS, listSubKeys);
if (nReturn = 0) && (sProductName != "") then //successful
sNumName = "ProductName";
nType = REGDB_STRING;
nResult = ListGetFirstString (listSubKeys, sKey);

while (nResult != END_OF_LIST)
RegDBGetKeyValueEx (REGKEYVAL ^ sKey, sNumName, nType, sSearchValue, nSize);
if (StrCompare (sSearchValue, sProductName) = 0) then
//item is found, so get the path
sNumName = "LastUsedSource";
nType = REGDB_STRING_EXPAND;
RegDBGetKeyValueEx (REGKEYVAL ^ sKey ^ "SourceList", sNumName, nType, sPathValue, nSize);
if (sPathValue != "") then
listValues = ListCreate(STRINGLIST);
if (StrGetTokens (listValues, sPathValue, ";") = 0) then
nSubResult = ListGetFirstString (listValues, sValue);
while (nSubResult != END_OF_LIST)
if (Is(PATH_EXISTS, sValue) = 1) then
StrRemoveLastSlash (sValue);
sLastCache = sValue;
nSubResult = END_OF_LIST;
else
nSubResult = ListGetNextString (listValues, sValue);
endif;
endwhile;
endif;
endif;
endif;
if (nSubResult != END_OF_LIST) then
nResult = ListGetNextString (listSubKeys, sKey);
else
nResult = END_OF_LIST;
endif;
endwhile;
endif;

//cleanup
ListDestroy (listSubKeys);
ListDestroy (listValues);
return sLastCache;
catch
endcatch;
end;

Others have taken a more direct approach, setting the cache path to CommonAppDataFolder and not worrying about the last cache location that may have existed. This will allow you to shorten this process down to a single simple procedure. Chris Painter took this approach, with a good example he sent which appears earlier in this thread. His was as follows...

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 % szPackageCode )) then
DeleteDir( szDir, ALLCONTENTS );
endif;
nResult = ListGetNextString (listDirs, szDir);

endwhile;

return ERROR_SUCCESS;

end;

This is an excellent example which is much simpler. I would say you need to decide what the cache location will be, LocalAppDataFolder or CommonAppDataFolder. Decide if you care about cleaning up old cache and other locations (as we did). Remember you need to specify exactly what this location is down to the product name. Call the procedure once only in the execute sequence at the end of the install after InstallFinalize. Test, Test, Test before you release to verify that it is doing what you want it to do.

Good luck.
0 Kudos
Marc75
Level 4

Hi Randy,

thank you for the good explanation and for sharing the code with us! 🙂
Marc
0 Kudos