About a year ago I was working on building our DotTwain ActiveX control and wrote two articles on some useful tips that I discovered.  Since that time, I’ve seen a problem on a few customer computers where, after upgrading once, the control would never run.  Instead, it would just try to reinstall after every page load.

After much frustration, it turned out that the problem is related to a little-known Internet Explorer registry entry called VerCache. 

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Ext\Settings\{GUID}] "VerCache"

I figured this out after noticing that every machine exhibiting this problem had the same value for VerCache.  Once the VerCache was set to this value, uninstalling did nothing.  The repeated-reinstallation behavior happened with any new version installed which shared the control GUID.

 

 

What’s a VerCache?

Strangely enough, searching for “VerCache” ActiveX gives you little more than an IE Team blog post and one MSDN forums entry.

At first, I was very excited when I found this IE team blog post.  It appeared that I had found my solution.

Here in the above mentioned screenshots they both are having same file date time stamps and that Causes the VerCache registry key to not get updated.

To resolve this , ensure that at least one of these parameters - “Created” date time stamp, “Modified” date time stamp or the file size, on the updated control is different from the old version of the control and you should be GTG!

Unfortunately, this information seems to be incorrect, or at least overly vague.  I observed the same bad VerCache value on different machines with different version of the control.  Each of these had different “created” and “modified” time stamps.  In fact, the CAB, the installer in the CAB, and the control assembly itself each had different “created” and “modified” time stamps for every single version.  I even tried updating the INF and setup launcher timestamps to no avail.

The only thing which did not vary consistently was file size as we rebuild and repackage our control for each DotImage version we release.

 

 

Some of the things I tried or considered.

Early on, I discovered simply deleting the registry key after uninstalling fixed the issue temporarily and was under the impression that it was a single bad version of the control causing this.  So, I wrote a small cleaner utility to fix the issue.  However, as time wore on it was apparent that this issue occurred in a somewhat random fashion with all versions of the control.

Another simple fix I thought about was updating the control GUID with each version.  However, this would require updating both our documentation and demos with every build.  Additionally, our customers would need to update their javascript with every upgrade.  This was unacceptable.

The next thing I tried was to have the installer launch a custom console program on install which cleaned the registry value.  This worked great in XP.  However, in Windows 7 the console program, even as admin, did not have access to the registry values I wanted to delete.

 

 

Finally, a solution.

In the end the only solution I found worked consistently and met our needs was to put custom installer actions into the control itself.  When both installing and uninstalling the installer calls the control’s custom actions which removes any existing VerCache value for the control.

 

First, I snagged a class I had made previously for cleaning guid-based registry entries:

Code Snippet
  1. public delegate bool RegActionBreakOnTrue(RegistryKey basekey, string key);
  2.  
  3. public class EventArgs<T> : EventArgs { private T eventData; public EventArgs(T eventData) { this.eventData = eventData; } public T EventData { get { return eventData; } } }
  4.  
  5. public class RegGuidCleaner
  6. {
  7.     string _guid;
  8.     public RegGuidCleaner(string guid)
  9.     {
  10.         _guid = guid;
  11.         Locations = new string[] {};
  12.     }
  13.  
  14.     public string[] Locations { get; set; }
  15.  
  16.     public RegistryKey GetPathBase(string path)
  17.     {
  18.         RegistryKey baseKey;
  19.         if (path.StartsWith(@"HKEY_CURRENT_USER\"))
  20.             baseKey = Registry.CurrentUser;
  21.         else if (path.StartsWith(@"HKEY_LOCAL_MACHINE\"))
  22.             baseKey = Registry.LocalMachine;
  23.         else
  24.             throw new ApplicationException("Unexpected location type: " + path);
  25.         return baseKey;
  26.     }
  27.  
  28.     private string GetBaselessPath(string location)
  29.     {
  30.         string truncatedLoc = location.Substring(location.IndexOf('\\') + 1);
  31.         return truncatedLoc;
  32.     }
  33.  
  34.     public void PerformRegActionForEach(RegActionBreakOnTrue action)
  35.     {
  36.         foreach (var location in Locations)
  37.         {
  38.             string fullPath = location + _guid;
  39.             RegistryKey baseKey = GetPathBase(fullPath);
  40.             string baselessPath = GetBaselessPath(fullPath);
  41.  
  42.             if (action(baseKey, baselessPath))
  43.                 break;
  44.         }
  45.     }
  46.  
  47.     public bool EnteriesExist
  48.     {
  49.         get
  50.         {
  51.             bool doesKeyExist = false;
  52.             PerformRegActionForEach((baseKey, subkey) =>
  53.             {
  54.                 var key = baseKey.OpenSubKey(subkey);
  55.                 if (key != null)
  56.                 {
  57.                     doesKeyExist = true;
  58.                     return true;
  59.                 }
  60.                 else
  61.                     return false;
  62.             });
  63.             return doesKeyExist;
  64.         }
  65.     }
  66.  
  67.     public void DeleteEntries()
  68.     {
  69.         PerformRegActionForEach((baseKey, subkey) =>
  70.         {
  71.             try
  72.             {
  73.                 Messages(this, new EventArgs<String>("Deleting Registry Key: " + baseKey.ToString() + @"\" + subkey));
  74.                 baseKey.DeleteSubKeyTree(subkey);
  75.             }
  76.             catch (Exception ex)
  77.             {
  78.                 Messages(this, new EventArgs<String>(ex.ToString()));
  79.             }
  80.  
  81.             return false;
  82.         });
  83.     }
  84.  
  85.     public event EventHandler<EventArgs<String>> Messages = delegate { };
  86. }

Please forgive the catch (Exception ex).  So far it has been unnecessary to handle failure cases for anything other than logging.

 

Next, I added a custom installer class to remove the entries:

Code Snippet
  1. [RunInstaller(true)]
  2. public partial class RegistryCleanerInstallerClass : Installer
  3. {
  4.     public RegistryCleanerInstallerClass()
  5.     {
  6.         InitializeComponent();
  7.     }
  8.  
  9.     private void CleanVerCache()
  10.     {
  11.         try
  12.         {
  13.             RegGuidCleaner cleaner = new RegGuidCleaner("{" + typeof(AcquisitionControl).GUID + "}");
  14.             cleaner.Locations = new string[] {
  15.                 @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Ext\Settings\",
  16.              };
  17.  
  18.             cleaner.DeleteEntries();
  19.         }
  20.         catch { }
  21.     }
  22.  
  23.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  24.     public override void Install(IDictionary stateSaver)
  25.     {
  26.         base.Install(stateSaver);
  27.         CleanVerCache();
  28.     }
  29.  
  30.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  31.     public override void Commit(IDictionary savedState)
  32.     {
  33.         base.Commit(savedState);
  34.     }
  35.  
  36.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  37.     public override void Rollback(IDictionary savedState)
  38.     {
  39.         base.Rollback(savedState);
  40.         CleanVerCache();
  41.     }
  42.  
  43.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  44.     public override void Uninstall(IDictionary savedState)
  45.     {
  46.         base.Uninstall(savedState);
  47.         CleanVerCache();
  48.     }
  49. }

 

I also decided to move the com registration into a custom action as well.  The installer-based registration had been intermittently not working which caused us to hand test every release version of the control before sending it out.

Code Snippet
  1. [RunInstaller(true)]
  2. public partial class ComRegisterInstallerClass : Installer
  3. {
  4.     public ComRegisterInstallerClass()
  5.     {
  6.         InitializeComponent();
  7.     }
  8.  
  9.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  10.     public override void Install(IDictionary stateSaver)
  11.     {
  12.         base.Install(stateSaver);
  13.  
  14.         try
  15.         {
  16.             RegistrationServices regSrv = new System.Runtime.InteropServices.RegistrationServices();
  17.  
  18.             if (!regSrv.RegisterAssembly(
  19.                 this.GetType().Assembly, AssemblyRegistrationFlags.SetCodeBase))
  20.             {
  21.                 throw new InstallException("Failed to register componenet for COM interop");
  22.             }
  23.         }
  24.         catch
  25.         {
  26.         }
  27.     }
  28.  
  29.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  30.     public override void Commit(IDictionary savedState)
  31.     {
  32.         base.Commit(savedState);
  33.     }
  34.  
  35.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  36.     public override void Rollback(IDictionary savedState)
  37.     {
  38.         base.Rollback(savedState);
  39.  
  40.         try
  41.         {
  42.             RegistrationServices regSrv = new System.Runtime.InteropServices.RegistrationServices();
  43.  
  44.             if (!regSrv.UnregisterAssembly(this.GetType().Assembly))
  45.             {
  46.                 throw new InstallException("Failed to unregister componenet for COM interop");
  47.             }
  48.         }
  49.         catch
  50.         {
  51.         }
  52.     }
  53.  
  54.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  55.     public override void Uninstall(IDictionary savedState)
  56.     {
  57.         base.Uninstall(savedState);
  58.  
  59.         try
  60.         {
  61.             RegistrationServices regSrv = new System.Runtime.InteropServices.RegistrationServices();
  62.  
  63.             if (!regSrv.UnregisterAssembly(this.GetType().Assembly))
  64.             {
  65.                 throw new InstallException("Failed to unregister componenet for COM interop");
  66.             }
  67.         }
  68.         catch
  69.         {
  70.         }
  71.     }
  72. }

 

Finally, I added the the custom action calls to the installer.

customAction1

customAction2

 

This still won’t allow an existing broken control to upgrade.  However, after manually uninstalling the old control, the new one will install and work fine.  At least that’s what I’ve found in IE 8.0.7600.16385 Update 0 on Windows 7 Enterprise 64-bit.  ActiveX tends to be a bit of a moving target these days.

Now, let’s hope I never need speak of this again.