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);
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.
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.