They both have their pros and cons. Querying registry is straightforward on its own, but requires awkward manipulations and accessing the data which is actually backing the Add/Remove Applet, not necessarily the Windows Installer API which uses its own complex registration. Additionally it may not work correctly with different installation context (user/machine) and you may have to query two places to get both x86 and x64 installations on a x64 system.

So to have a solution which is both fast, reliable and without any side-effects, you may go for a third solution which is more complex, but once setup can be reused not only for querying but for a whole management of MSI-based installations. And so this blog today will be about P/invoking native msi.dll to get results returned by the true Windows Installer API.

This post may be too technical if you have never programmed in C/C++ or C#. If you just want the results without understanding how to implement them on your own, scroll to the bottom, the full content of the PowerShell script is there.

Required methods

We will certainly need the following functions:

MsiEnumProducts
This method enumerates all installed or advertised products. However, it does not return anything else but ProductCode (GUID). For properties like name, version etc. we need a separate method

MsiGetProductInfo
This method takes a ProductCode and returns a required property, for example name or version.

Marshalling

To P/invoke both methods in PowerShell, we need a small piece of code which creates a new static helper class that exposes managed methods calling unmanaged (P/invoked) msi.dll functions:

Has been translated to managed types using the following marshalling logic:

DWORD becomes int (a short alias for Int32)

LPTSTR becomes a out reference to StringBuilder

Returned type is UINT, which becomes uint (a short alias for UInt32)

Similar is done for the second method. For both I am specifying that I want the Unicode version of the method (which internally will be MsiEnumProductsW).

Enumerating products

To get the list of products, we pass a zero-based index to our wrapper, and if we get a success result (0) then the buffer should be filled with the actual ProductCode. The index i corresponds to the i-th entry available in the list, and therefore should be increased by one before requerying. The method returns 0 if everything went fine, and then and only then we may check the buffer. The documentation says that the buffer should have a lenght of 39 characters (which is the length of GUID 38 + 1 character for the sequence termination mark), and that the calls should be done from the same thread (which is not a problem for us).

We need to call the method in a loop, each time checking if its result is non-zero. Before the loop, a StringBuilder has to be created (with desired capacity) that will store the current ProductCode. After each call, we increase an index by one and use that index in the next call. All put together, the code looks like:

Getting product properties

Now let’s get back to the second method. This one has additional challenge, that we ask for properties whose length are not statically known (as opposed to a constant 38+1 length for GUID buffer). According to the documentation

When the MsiGetProductInfo function returns, the pcchValueBuf parameter contains the length of the string stored in the buffer. The count returned does not include the terminating null character. If the buffer is not large enough, MsiGetProductInfo returns ERROR_MORE_DATA and pcchValueBuf contains the size of the string, in characters, without counting the null character.

This means that we should actually perform two concesutive calls to the same method, once passing a deliberately small buffer, reading out the required buffer length, and then making a proper call with a buffer of that size. The backing StringBuilder instance itself can be reused, given the fact that we may need to call the same code several hundreds of thousands time, we only have to take care of managing its capacity. A small function which does all of these is:

We should make sure the capacity of the buffer is set to that value we have just read from first call, increased by 1 (the capacity may be changed only if it is lower than required, we do not have to make downsizing). Then in the second call, a buffer of a proper length is used. The method accepts three parameters: ProductCode, name of the property and the StringBuilder which will be used as the buffer. Note that we call ToString() with a parameter determining the required length. Since we do not downsize the capacity, is is important to only get relevant part of the buffer at this point.

By the way, available property names are shown on the msdn website. According to header files msi.h, the following are available:

PackageName

Transforms

Language

ProductName

AssignmentType

InstanceType

AuthorizedLUAApp

PackageCode

Version

ProductIcon

InstalledProductName

VersionString

HelpLink

HelpTelephone

InstallLocation

InstallSource

InstallDate

Publisher

LocalPackage

URLInfoAbout

URLUpdateInfo

VersionMinor

VersionMajor

ProductID

RegCompany

RegOwner

Uninstallable

State

PatchType

LUAEnabled

DisplayName

MoreInfoURL

LastUsedSource

LastUsedType

MediaPackagePath

DiskPrompt

Bootstrapping

For a better readibility and easier extensibility (including chaining and other methods which may work on pipelined results), we will create a small container class to hold our MSI information:

Let’s try to run it to display first 10 products starting with Microsoft, and format the results in a table. Save the content of the script in a file (for example test1.ps1) and run it, adding a chain of piped results:

2 comments

Is there a way to leverage msi.dll to retrieve a products installed features/sub components?
This is seen by going through Control Panel / Add Remove Programs / Modify / Vieweing sub components and features

Sure, use a similar approach and wrap MsiEnumFeatures (having a ProductCode, this returns all belonging features). Additionally, use MsiQueryFeatureState to determine whether the feature is installed or advertised.