Scott Hanselman

BATCH FILE VOODOO: Determine if multiple (and which) versions of an MSI-installed Product are installed using UpgradeCode

June 23, '04 Comments [1] Posted in Tools | XML
Sponsored By

We have a product that supports side-by-side installs, and I wanted to enumerate all the versions of that product and display it's version/product name.  Sure, it could be done in yet-another-C# program, but why not use Batch?

Every MSI installer has an UpgradeCode GUID that can't change for the life of the product.  The ProductCode and PackageCode may change, but the UpgradeCode is what tells us if the product is already installed.  At that point the installer can decide if it wants to upgrade or just install along side.  Those UpgradeCodes (for all apps) are in HKCU\Software\Microsoft\Installer\UpgradeCodes\ with SubKeys for each Product while the Products are in HKCU\Software\Microsoft\Installer\Products\ with details for each like Version, ProductName, etc. In this example I'm getting the ProductName which includes the version for me, but of course you can get anything you want.

NOTE: We're using REG.EXE which is included in the PATH on 2003 and XP but is an add-on to Windows 2000.

Here's the voodoo (all one line).  Note - The delims= includes a TAB, then a space:

FOR /F "tokens=1 skip=4 delims=        " %%A IN ('REG QUERY HKCU\Software\Microsoft\Installer\UpgradeCodes\<yourUpgradeCodeGUIDHere> /s') DO FOR /F "tokens=5 delims=            " %%Z IN ('REG QUERY HKCU\Software\Microsoft\Installer\Products\%%A /v ProductName') DO ECHO %%Z

Here it is broken down:

FOR /F "tokens=1 skip=4 delims=           " %%A
  
IN (
       'REG QUERY HKCU\Software\Microsoft\Installer\UpgradeCodes\<yourUpgradeCodeGUIDHere> /s'
      )
   DO 
     FOR /F "tokens=5 delims=            " %%Z 
         IN (
             'REG QUERY HKCU\Software\Microsoft\Installer\Products\%%A /v ProductName'
             ) 
         DO 
            ECHO %%Z

Thanks to Rob van der Woude's great (and oft-updated) Batch File site.

UPDATE: Some interesting details on compressed GUIDs from John Walker. Thanks John!:

"Scott,

Thanks for the help with REG.exe and FOR /F. My requirement was to be
able to uninstall any installed version of a product in an automated
fashion via a batch file. Because we don't know the exact product codes
that might be installed, we start our search with the upgrade code.

Initially, I had some trouble determining the values to use for:
<yourUpgradeCodeGUIDHere>.

What I found is that MSIs record 'compressed' GUIDs in the registry for
upgrade and product codes. So if you want to search the registry for
UpgradeCodes, you will need to search for the compressed code.
If <yourUpgradeCodeGUIDHere> is:
       {abcdefgh-ijkl-mnop-qrst-uvwxyz123456}
Then the compressed version you should search for is:
       hgfedcbalkjiponmrqtsvuxwzy214365

http://www.appdeploy.com/messageboards/tm.asp?m=11996&mpage=1&#12037
explains the process for converting an upgrade code.


I also searched a different location to determine product codes at:
HKCR\Installer\UpgradeCodes\ based on information on Windows Installer
registry locations from:
http://msdn2.microsoft.com/en-US/library/aa367758.aspx because we
install per-machine.

The value(s) stored under an UpgradeCode path in the registry will be
compressed product codes. In order to run an msiexec uninstall you need
an uncompressed code in the {8-4-4-4-12} format. I ended up writing a
small console app to do the translations between compressed and
uncompressed codes. (Why does MSI compress the codes?) Here is the meat
of the translator (errorhandling and usage message code removed):

   class GuidCompressor
   {
       /// <example>GuidCompressor.exe {abcdefgh-ijkl-mnop-qrst-uvwxyz123456} N
       /// returns: hgfedcbalkjiponmrqtsvuxwzy214365</example>
       /// <example>GuidCompressor.exe hgfedcbalkjiponmrqtsvuxwzy214365 B
       /// returns: {abcdefgh-ijkl-mnop-qrst-uvwxyz123456}</example>
       static void Main(string[] args)
       {
           Guid origGuid = new Guid(args[0]);
           //outputFormat should be N, D, B, P
           string outputFormat = args[1];

           string raw = origGuid.ToString("N");
           char[] aRaw = raw.ToCharArray();
           //compressed format reverses 11 byte sequences of the original guid
           int[] revs
               = new int[]{8, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2};
           int pos = 0;
           for (int i = 0; i < revs.Length; i++)
           {
               Array.Reverse(aRaw, pos, revs[i]);
               pos += revs[i];
           }
           string n = new string(aRaw);
           Guid newGuid = new Guid(n);
           //GUID in registry are all caps.
           Console.WriteLine(newGuid.ToString(outputFormat).ToUpper());
       }
   }


Combining the GuidCompressor, UpgradeCodeGuid, and HKCR registry
location here is the batch file that I use to uninstall:
Delims are <tab><space>

SET UncompressedUpgradeCodeGUID={abcdefgh-ijkl-mnop-qrstuvwxyz123456}
SET logfile=C:\temp\mylogfile.txt
SET binpath=C:\tools
FOR /F %%A IN ('%binpath%\GuidCompressor.exe
%UncompressedUpgradeCodeGUID%   N') DO (
       FOR /F "tokens=1 skip=4 delims=  " %%B IN ('REG QUERY
HKCR\Installer\UpgradeCodes\%%A /s') DO (
               FOR /F %%C IN ('%binpath%\GuidCompressor.exe %%B   B')
DO (
                       %windir%\System32\msiexec.exe /x %%C /lvx*
%logfile% /qb
               )
       )
)


This research was tested on 32bit versions of WinXP and Win2k3."

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

A multi-request-safe ViewState Persister

June 23, '04 Comments [7] Posted in ASP.NET | ViewState
Sponsored By

Mark Miller has posted his code for a ViewStatePersister using the "common sense but not obvious" GUID technique that was outlined previously by Scott Mitchell and myself.

He stores a GUID in the ViewState hidden field, and sticks the bloated ViewState in a temp file on the server.  It doesn't solve the problem when running multiple web servers while using stateless balancing (meaning: NOT using sticky sessions/node affinity) but it's the most elegant and complete solution I've seen yet and should work great on a single web server. 

A few questions I have though:

  • When do the files get cleaned up and how often? Do you clean up old ones in a background thread within ASP.NET or a separate Windows Service?  Thought: I wonder if you could delete them after immediately after the Load?  You wouldn't be able to RE-post data, but it'd be cleaner, no?
  • GUID generation is very expensive, and can really slow you down under load.  I wonder if it would be faster/easier to have a single long and use InterlockedIncrement or  InterlockedIncrement64 to safely increase the value on each call until it overflows and you start again at 0.

Many thanks to Mark for sharing!

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

How to transfer/move all your Email from Hotmail (or Yahoo!) to Gmail

June 23, '04 Comments [29] Posted in Bugs
Sponsored By

I was lucky today and a friend offered me a Gmail invite (Gmail being Google's 1 gig Free Email BETA if you've been living in a cave without wireless).  I don't need a Gmail account but the wife digs web-based email and suffers with a 1 meg Hotmail account.  She's constantly deleting emails trying to stay under the limit.  (and she's too cheap to pay for more storage :) )

But, how to move all her Hotmail stuff (several hundred emails) over to Gmail?  If I can't move them, she'll hardly find the service useful. 

So...

  • Download Eudora, and install the Sponsored version. 
    • Why Eudora and not Outlook or Thunderbird?  Because you'll want to REDIRECT the emails that come in from Hotmail, NOT FORWARD THEM.  If you just Forward them, they'll all look like they came from you!  You want to hang on to the FROM and the TO.  Eudora is the easiest and most available email client to do this.  (Yes, I was surprised Thunderbird doesn't - it's been a bug for a while)
  • Download Hotmail Popper.
    UPDATEIf you have Yahoo! then you can use YPOPs which does the same thing!   
    • This little gem sits in your tray and creates a localhost POP server that bridges to your Hotmail account. 
      Note: Make sure you check Retrieve messages from folders other than Inbox or you won't get all your Hotmail!
  • Create a REDIRECT Filter that grabs all incoming mail.
    • Make a filter like this.  Mine says match all mail the DOESN'T contain this nonsense word (which is all mail) and REDIRECTS it to the wife's gmail.com account. 
      Note: Turn off IMMEDIATE SEND in Eudora, we'll want to QUEUE the mails, as we need to change the outgoing SMTP (can't be Hotmail!)
  • Get the Mail.
    • Now, in Eudora, make and account like this:
    • Important: Make sure you check LEAVE MAIL ON SERVER under "Incoming Mail" so you don't delete all the messages in your Hotmail Account.
    • At this point, you should have downloaded all the email from Hotmail, and all the same messages should be in the Eudora Outbox ready to send.
    • Remember, we have to Redirect the emails, so we'll need access to an SMTP server that supports REDIRECTS.  Make sure you have one, or know of one you can use.  I have my hanselman.com one, but it's SMTP (outgoing mail) requires Authentication as all good SMTP servers should.
  • Send (Redirect) the Mail to Gmail.
    • Important: First, change login name in the Checking Mail option (see above) to the name required for your Outgoing SMTP server.  That's the name that is used when sending!  It WILL be different than your Hotmail name.
    • TIP: Have any Gmail Filters that you might wanted applied to incoming messages setup BEFORE you do the big "export/import."  Otherwise you'll have to Search and Apply Label later.
    • Now, in the Sending Mail option set your return mail address to your new Gmail.com account.  (That way your Redirects will list you as the "On Behalf Of" name)
    • Enter your SMTP Server, and select Immediate Send.
    • Now, from the Eudora File Menu, select "Send Queued Messages" - this will send them with the new SMTP authentication info as REDIRECTS.
  • Enjoy your new, fully populated - from Hotmail - Gmail account! Spread the word!
    • Optionally: Uninstall Eudora, Hotmail Popper, and instruct Eudora to delete the local email store.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Be aware of your DLLs and who's loading them - know about assembly binding redirects

June 23, '04 Comments [0] Posted in ASP.NET | XML | Bugs | Tools
Sponsored By

When it rains it pours.  Twice today folks came looking when unexpected behaviors occured after an SDK upgrade.

If you're using a versioning scheme, always ask yourself, what version of my assembly has been loaded into this AppDomain?  It's even more important to be aware (and not to Program By Coincidence) when using the GAC.

Digging into the problem showed that the wrong version was being loaded - an older version from the GAC due to an incorrect bindingRedirect.

A few tools to be aware of and WHAT they tell you about Assembly Binding:

  • Binding before it happens: ILDASM or Reflector will tell you what your assembly wants (what it was compiled against)
  • Binding as it happens: Fusion (the Assembly Binding Log Viewer) will show you all assembly binds if you set the HKLM\Software\Microsoft\Fusion\ForceLog registry value to 1
  • Binding after it happens: Process Explorer will tell you what DLL (assembly) is loaded in memory and from where it came.

In this case, a little command line showed me:

C:\>gacutil /l | find /i "Corillian" | more
   <snip>
Corillian.Thingie.Whatzit, Version=3.1.0.39, Culture=neutral, PublicKeyToken=xx
Corillian.Thingie.Whatzit, Version=3.1.1.31, Culture=neutral, PublicKeyToken=xx

Ah! There's an older version in the GAC, probably supporting another Web on this box.  Our ASP.NET site was compiled against this (says Reflector) and we confirm the wrong one was loaded with Process Explorer. But, we need a bug fix from the new version and can't recompile, so, in our Web.config we added:

<runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
   <dependentAssembly>
    <assemblyIdentity name="Corillian.Thingie.Whatzit"
     publicKeyToken="xx"
     culture="neutral" />
    <bindingRedirect oldVersion="3.1.0.39" newVersion="3.1.1.31" />
   </dependentAssembly>
  </assemblyBinding>
</runtime>

Now when our ASP.NET apps asks for 3.1.0.39, instead it gets 3.1.1.31.  Note that our assembly must be strongly-named for this to work.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Using NUnit and a better way to Unit Test with External File Dependancies

June 23, '04 Comments [2] Posted in NUnit
Sponsored By

Great stuff on using NUnit in conjunction with external files from Patrick Cauldwell.  It's one of those "Doh!" things that I've always MEANT to do, but somehow ended up using Pre- and Post-Build events instead.  I MUCH prefer Patrick's method.  It's much cleaner and it allows the once-external file to LIVE with the test.

        [SetUp] public void SetUp()
        {
            Assembly a = Assembly.GetExecutingAssembly();
            using (Stream s = a.GetManifestResourceStream("MyNameSpace.something.txt"))
            {
                using (StreamReader sr = new StreamReader(s))
                {
                    using (StreamWriter sw = File.CreateText(webConfigPath))
                    {
                        sw.Write(sr.ReadToEnd());
                        sw.Flush();
                    }
                }
            }
        }

        [TearDown] public void TearDown()
        {
            if(File.Exists(webConfigPath))
            {
                File.Delete(webConfigPath);
            }
        }

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.