Is MsiOpenProduct the correct way to read properties from an installed product?
Asked Answered
L

3

4

Given an MSI product code I want to get the upgrade code (among other properties) from an already installed product. I have tried this by calling the MsiOpenProduct method, followed by MsiGetProductProperty(). An (abbreviated) example looks like this:

MSIHANDLE handle = NULL;
MsiOpenProduct(strProductCode,&handle);
CString strUpgradeCode;
MsiGetProductProperty(handle,_T("UpgradeCode"), strUpgradeCode.GetBuffer(GUID_LENGTH), &dwSize);
strUpgradeCode.ReleaseBuffer();
MsiCloseHandle(handle);

This gets me the desired value, and judging from the MSDN documentation this seems like a valid way to do this:

The MsiOpenProduct function opens a product for use with the functions that access the product database. The MsiCloseHandle function must be called with the handle when the handle is no longer needed.

However the call to MsiOpenProduct() pops up the "Windows installer is preparing the installation..." dialog. The call to MsiCloseHandle() makes it disappear again.

This left me wondering:

  • What does the call to MsiOpenProduct() do under the hood? I do not want to trigger any actions, I just want to read properties.
  • I don't mind the dialog popping up, as this is only for unit test code as long as this has no side effects. And as there are many unit tests that do this, it must still work when opening and closing handles in rapid succession.
  • Although I stumbled over the MsiGetProductInfo method, there seems to be no way to get the upgrade code. Am I right?
  • Is MsiOpenProduct the correct way to read properties like the upgrade code?
Liberati answered 23/6, 2010 at 13:32 Comment(1)
I have come to the same conclusion - I use OpenProduct from VBScript to get the UpgradeCode (COM call, different from your C++ call). I suppress the GUI you speak of by setting the UILevel property for the top level installer object. The only other way I found to get the upgrade code is via WMI: Select * from Win32_Property Where Property = 'UpgradeCode'. And for a specific product code: SELECT * FROM Win32_Property WHERE Property='UpgradeCode' AND ProductCode='{B862B671-59FD-7457-AFA0-C738FB7ABD60}'. Maybe I should add an answer below with this information - despite it being VBScript.Botswana
M
3

MsiOpenProduct should be fine So long as you don't run any sequences or actions, it won't do anything. If you want to silence the dialog, you can with careful use of either MsiSetInternalUI() or MsiSetExternalUI().

Another approach you can take, as long as the ProductCode and UpgradeCode are safely static (i.e. as long as they aren't changed by transforms), is to locate the database using MsiGetProductInfo() and call MsiOpenDatabase() on that. The difference is that MsiOpenProduct() (or similarly MsiOpenPackage) applies the transforms that were used at installation time and prepares a session, whereas MsiOpenDatabase() does neither.

Minardi answered 24/6, 2010 at 13:42 Comment(1)
Well that's working perfectly for me. MsiGetProductInfo is exactly what I needed.Jijib
E
3

There is a comprehensive answer with information on how to get the UpgradeCode using Powershell or VBScript and WMI here: How can I find the Upgrade Code for an installed MSI file?

Below is a quick, basic example using VBScript / COM automation (MSI API, not WMI) and the approach discussed by OP (using the OpenProduct method - the COM equivalent to the Win32 installer function).


As discussed in my comment above, I will just add this little VBScript to do the same as OP does in C++. Note that Windows Installer can be accessed via WMI (Win32_Product object), COM automation and Win32 C++ installer functions.

For some reason the UpgradeCode for a package appears to not be available directly from the COM API or the Win32 API. Very strange indeed, especially since it is an input parameter to functions like Installer.RelatedProducts - it is not clear in the documentation that the actual call should be RelatedProducts(UpgradeCode), but looking in the msi.IDL you see: StringList* RelatedProducts([in] BSTR UpgradeCode);

The WMI option works, but so does this OpenProduct call demonstrated below (which is significantly faster and appears safe - WMI is completely read-only as far as I know though - but heaven knows what they are doing "under the hood". Are they spinning up a session object? Or are they reading from a WMI database? WMI does "feels" safer somehow).

The beauty of the below method is that it will apply all transforms that were applied to the product in question at installation time. If you want to write to disk instead of showing message boxes and can't be bothered looking up the docs, here is a similar VBScript that writes package info to a desktop text file: How can I find the product GUID of an installed MSI setup? - quite a bit down the page, just copy a couple of lines and you are message box free).

Note! The script below will create one log file per opened MSI if automatic logging is enabled on the system. As it stands the script will only open one MSI before it exists though (the Exit For construct).

On Error Resume Next ' This "tersified" script has no error handling

Const msiUILevelNone = 2
Set installer = CreateObject("WindowsInstaller.Installer")
Set products = installer.ProductsEx("", "", 7)
installer.UILevel = msiUILevelNone ' Suppress GUI (MSI progress dialog)

'Iterate over all MSI packages on the box
For Each product In products
   ' productcode = product.ProductCode
   ' name = product.InstallProperty("ProductName")
   ' version = product.InstallProperty("VersionString")
   ' pkgcode = product.InstallProperty("PackageCode")

   Set session = installer.OpenProduct(product.ProductCode)
   upgradecode = session.ProductProperty("UpgradeCode")
   MsgBox upgradecode
   Set session = Nothing ' Important, close the session (doesn't work in Javascript btw)

   Exit For   ' End after one iteration (so you don't get a million message boxes)
              ' Alternatively something like: If i > 4 Then Exit For
Next

Set installer = Nothing

MsgBox "Finished"

I have tried to look in the C++ Win32 installer functions for any other way to retrieve the UpgradeCode, but I can't see anything obvious. The session approach should work in C++ as well, but I am a little apprehensive about the release of handles and resources. I am not properly potty-trained with C++, but know more than enough to be dangerous. Fire In The Hole. Etc...

I wonder if the OP retrieved all packages on the box, or just a single one. I wonder if the timing issues and concurrent session object problems seen with Javascript would strike in C++ as well? I will give it a go I think - someday.

Enzyme answered 5/2, 2018 at 2:35 Comment(5)
Closing the session that way also doesn't work in C#.Mccrory
I suppose that is because of garbage collection? Maybe you would need to force it. Very bad. What data are you looking for?Botswana
I'm looking for the installation directory. Since the installer is created with Wix#, the ProductCode is randomly generated each build. Therefore I need to find the installed application by its UpgradeCode.Mccrory
A strange Linq variant here. The call "RelatedProducts" gives you all products matching an upgrade code (more). There are many ways, you could get a component directly and its installation directory for example.Botswana
Throwing in this somewhat unrelated answer on how to uninstall by product name, by upgrade code, by product code, etc..., and getting properties (very basic, just a link I included so it is easily found). C++ "get property" approach and "common C++ CA problemsBotswana
U
1

For the information you want, it sounds like you can just call ::MsiGetProductInfo(). ::MsiOpenDatabase() is a very slow operation while ::MsiGetProductInfo() is (IIRC) more on par with registry look ups.

Unfolded answered 11/7, 2010 at 14:55 Comment(2)
That may be true, but like I wrote above, I found no way to get the upgrade code via MsiGetProductInfo. It seems that you can only query a limited set of properties. See msdn.microsoft.com/en-us/library/aa370130%28VS.85%29.aspx for a list of properties.Liberati
Never saw this question back when I was looking for info on this issue. I used WMI to retrieve the upgrade code, but tried again with MSI API and it worked (shown in the VBScript I added as a separate answer here). MSI API seems to work about 2-3 times faster than WMI, but it is still slow. Not great to say the least, but it does report what I needed. Any thoughts on spinning up so many Session objects? Javascript totally failed here, stumbling on concurrently open sessions (could be my poor Javascript skills). I have seen no side effects so far from all the session objects with VBScript.Botswana

© 2022 - 2024 — McMap. All rights reserved.