views:

46

answers:

3

Hi,

Whilst working with the MSI Interop API I have come across some unusual behaviour which is causing my application to crash. It is simple enough to 'handle' the problem but I would like to know more about 'why' this is happening.

My first call to MSIEnumRelatedProducts returns an value of 0 and correctly sets my string buffer to a productcode. My understanding is that this would only happen if the given upgradecode (passed as a parm to the method) has a 'related family product' currently installed, otherwise it would return 259 ERROR_NO_MORE_ITEMS.

However when I subsequently call MSIGetProductInfo using the same productcode I get the return value 1605, "This action is only valid for products that are currently installed.".

Does anyone have any ideas under what circumstances this might happen? It is 100% repeatable on 1 machine but I have not yet managed to get reproduction steps on another machine.

All our products are build with the Wix Property "AllUsers=1" so products should be installed for all users, not just one.

Any ideas/suggestions appreciated.

Thanks Ben

Update: I've noticed that when running the problem msi package with logging the following line is shown:

MSI (s) (88:68) [12:15:50:235]: FindRelatedProducts: could not read ASSIGNMENTTYPE info for product '{840C...etc.....96}'. Skipping...

Does anyone have any idea what this might mean?

Update: Code sample.

do
{
   result = _MSIApi.EnumRelatedProducts(upgradeCode.ToString("B"), 0, 
                                        productIndex, productCode);
   if (result == MSIApi.ERROR_BAD_CONFIGURATION ||
       result == MSIApi.ERROR_INVALID_PARAMETER ||
       result == MSIApi.ERROR_NOT_ENOUGH_MEMORY)
   {
      throw new MSIInteropException("Failed to check for related products", 
                                     new Win32Exception((Int32)result));
   }

   if(!String.IsNullOrEmpty(productCode.ToString()))
   {
      Int32 size = 255;
      StringBuilder buffer = new StringBuilder(size);
      Int32 result = (Int32)_MSIApi.GetProductInfo(productCode, 
                             MSIApi.INSTALLPROPERTY_VERSIONSTRING, 
                             buffer, 
                             ref size);

      if (result != MSIApi.ERROR_SUCCESS)
      {               
         throw new MSIInteropException("Failed to get installed version", 
                                        new Win32Exception(result));
      }

      version = new Version(buffer.ToString());
   }

   productCode = new StringBuilder(39);
   productIndex++;
}
while (result == MSIApi.ERROR_SUCCESS);
+2  A: 

I suppose that you try to use MsiGetProductInfo to get a property other as described in documentation. For example you can get in the way the value of the "PackageCode" property (INSTALLPROPERTY_PACKAGECODE) without any problem, but you can't get the value of the "UpgradeCode" property with respect of MsiGetProductInfo and receive the error 1605 (ERROR_UNKNOWN_PRODUCT).

UPDATED: OK, now I understand you problem. How you can find in the internet there are a bug in MsiGetProductInfo, so it work not always. Sometime it get back 1605 (ERROR_UNKNOWN_PRODUCT) or 1608 (ERROR_UNKNOWN_PROPERTY) back. In the case as the only workaround is to get the version property manually. I could reproduce the problem which you described on my computer with the Microsoft Office Outlook 2010 MUI (UpgradeCode = "{00140000-001A-0000-0000-0000000FF1CE}") and wrote a workaround where I get the product version from the registry. In the example I get information only from HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products. If you have an interest to products installed not only for all users you have to modify the program. Here is the code

using System;
using System.Text;
using System.Runtime.InteropServices;
using Microsoft.Win32;

namespace EnumInstalledMsiProducts {
    internal static class NativeMethods {
        internal const int MaxGuidChars = 38;
        internal const int NoError = 0;
        internal const int ErrorNoMoreItems = 259;
        internal const int ErrorUnknownProduct = 1605;
        internal const int ErrorUnknownProperty = 1608;
        internal const int ErrorMoreData = 234;

        [DllImport ("msi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern int MsiEnumRelatedProducts (string lpUpgradeCode, int dwReserved,
            int iProductIndex, //The zero-based index into the registered products.
            StringBuilder lpProductBuf); // A buffer to receive the product code GUID.
                                         // This buffer must be 39 characters long.
        // The first 38 characters are for the GUID, and the last character is for
        // the terminating null character.

        [DllImport ("msi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern Int32 MsiGetProductInfo (string product, string property,
            StringBuilder valueBuf, ref Int32 cchValueBuf);
    }
    class Program {
        static int GetProperty(string productCode, string propertyName, StringBuilder sbBuffer) {
            int len = sbBuffer.Capacity;
            sbBuffer.Length = 0;
            int status = NativeMethods.MsiGetProductInfo (productCode,
                                                          propertyName,
                                                          sbBuffer, ref len);
            if (status == NativeMethods.ErrorMoreData) {
                len++;
                sbBuffer.EnsureCapacity (len);
                status = NativeMethods.MsiGetProductInfo (productCode, propertyName, sbBuffer, ref len);
            }
            if ((status == NativeMethods.ErrorUnknownProduct ||
                 status == NativeMethods.ErrorUnknownProperty)
                && (String.Compare (propertyName, "ProductVersion", StringComparison.Ordinal) == 0 ||
                    String.Compare (propertyName, "ProductName", StringComparison.Ordinal) == 0)) {
                // try to get vesrion manually
                StringBuilder sbKeyName = new StringBuilder ();
                sbKeyName.Append ("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Installer\\UserData\\S-1-5-18\\Products\\");
                Guid guid = new Guid (productCode);
                byte[] buidAsBytes = guid.ToByteArray ();
                foreach (byte b in buidAsBytes) {
                    int by = ((b & 0xf) << 4) + ((b & 0xf0) >> 4);  // swap hex digits in the byte
                    sbKeyName.AppendFormat ("{0:X2}", by);
                }
                sbKeyName.Append ("\\InstallProperties");
                RegistryKey key = Registry.LocalMachine.OpenSubKey (sbKeyName.ToString ());
                if (key != null) {
                    string valueName = "DisplayName";
                    if (String.Compare (propertyName, "ProductVersion", StringComparison.Ordinal) == 0)
                        valueName = "DisplayVersion";
                    string val = key.GetValue (valueName) as string;
                    if (!String.IsNullOrEmpty (val)) {
                        sbBuffer.Length = 0;
                        sbBuffer.Append (val);
                        status = NativeMethods.NoError;
                    }
                }
            }

            return status;
        }

        static void Main () {
            string upgradeCode = "{00140000-001A-0000-0000-0000000FF1CE}";
            StringBuilder sbProductCode = new StringBuilder (39);
            StringBuilder sbProductName = new StringBuilder ();
            StringBuilder sbProductVersion = new StringBuilder (1024);
            for (int iProductIndex = 0; ; iProductIndex++) {
                int iRes = NativeMethods.MsiEnumRelatedProducts (upgradeCode, 0, iProductIndex, sbProductCode);
                if (iRes != NativeMethods.NoError) {
                    //  NativeMethods.ErrorNoMoreItems=259
                    break;
                }
                string productCode = sbProductCode.ToString();
                int status = GetProperty (productCode, "ProductVersion", sbProductVersion);
                if (status != NativeMethods.NoError) {
                    Console.WriteLine ("Can't get 'ProductVersion' for {0}", productCode);
                }
                status = GetProperty (productCode, "ProductName", sbProductName);
                if (status != NativeMethods.NoError) {
                    Console.WriteLine ("Can't get 'ProductName' for {0}", productCode);
                }

                Console.WriteLine ("ProductCode: {0}{3}ProductName:'{1}'{3}ProductVersion:'{2}'{3}",
                                   productCode, sbProductName, sbProductVersion, Environment.NewLine);
            }
        }
    }
}

which produce on my computer the correct output

ProductCode: {90140000-001A-0407-0000-0000000FF1CE}
ProductName:'Microsoft Office Outlook MUI (German) 2010'
ProductVersion:'14.0.4763.1000'

ProductCode: {90140000-001A-0419-0000-0000000FF1CE}
ProductName:'Microsoft Office Outlook MUI (Russian) 2010'
ProductVersion:'14.0.4763.1000'

instead of errors in the ProductVersion before.

Oleg
Attempting to get PackageCode returns the same 1605. That doesn't seem to make a different and if the API thinks the product is not installed, then I don't suppose changing the property would make a difference. Thanks for the suggestion though.
Ben Cawley
@Ben Cawley: I could post you some working code example. I want be sure exactly what you want. Do you know the UpgradeCode and try to get information about the related products or you want to get information about UpgradeCode having only ProductCode?
Oleg
Hi Oleg, I do have working code for most cases. Unfortunately it seems to be this 'one' msi that is causing problems either due to the msi package or some funny state of the machine I am trying to install it on. I am using the known 'upgradecode' to obtain the product code. I'll update my question with code examples.
Ben Cawley
Hi Oleg, Thanks for the information. I did try to google the problem first but I didn't find anything. Could you post the links you refer to regarding the known bug in MSIGetProductInfo? Do you happen to know if MSIGetProductInfoEx has the same problem? I might try that first. Thanks. In the mean time I'll try to look at the registry option!
Ben Cawley
@Ben Cawley: I searched for `MSIGetProductInfo` AND 1605 or `MSIGetProductInfo` AND 1608. You can find for example http://social.msdn.microsoft.com/Forums/en-US/sqlsetupandupgrade/thread/89eb8038-68a6-4201-a784-aa0063f4d8c0 and http://social.msdn.microsoft.com/Forums/en-US/sqlsetupandupgrade/thread/89eb8038-68a6-4201-a784-aa0063f4d8c0. But for me the most important was that I could reproduce the problem with **the first UpgradeCode which I picked up from the registry** of my computer. So for me it is clear a bug in the implementation of `MSIGetProductInfo`.
Oleg
+1  A: 

You should look at Windows Installer XML's Deployment Tools Foundation. It has a very mature MSI Interop ( Microsoft.Deployment.WindowsInstaller ) which will make writing and testing this code a lot easier.

I see you already have WiX ( hopefully v3+ ) so look for it in the C:\Program Files\Windows Installer XML v3\SDK folder.

Christopher Painter
Hi Chris,Thanks for the suggestion. I always thought the Deployment namespace was a .Net 3 addition but I can see now that it would also work for .Net 2. I'll look into this for future revisions.
Ben Cawley
.NET 2.0 is fine unless you add references to the LINQ classes.
Christopher Painter