software development

Get-MSI

One of the problems you run into frequently as a Windows developer is cleaning up MSI packages. You’re constantly installing and uninstalling dev versions, and if you mislabel a version string you can end up with an orphaned package. And Microsoft Windows does not make it easy to figure out what is actually considered installed. Add/Remove Programs doesn’t always tell the whole story. With a little PowerShell mojo I managed to cook up a script that should help rectify this situation in a fashion similar to Clint at the end of Unforgiven. In other words, you best not be messin’ with Danny (or Donald for that matter) Glover! In other, other words, here is a PowerShell script called Get-MSI that lists all of the MSI products installed on your system including their:

  • Product Name
  • Product Code
  • Install Date
  • Local Package
  • Version

The script is relatively straight-forward:

##############################################################################
# Get-MSI
#
#   Lists all of the installed packages on the system along with their
#   install date, product name, product code, and file path.
#
# By: Schley Andrew Kutz <schley.kutz@emc.com>
# On: 2010/11/03
##############################################################################

##############################################################################
# MsiUtil Class
##############################################################################
$msiUtilSignature = @'
/// <summary>
/// The HRESULT error code for success.
/// </summary>
private const int ErrorSuccess = 0;

/// <summary>
/// The HRESULT error code that indicates there is more
/// data available to retrieve.
/// </summary>
private const int ErrorMoreData = 234;

/// <summary>
/// The HRESULT error code that indicates there is
/// no more data available.
/// </summary>
private const int ErrorNoMoreItems = 259;

/// <summary>
/// The expected length of a GUID.
/// </summary>
private const int GuidLength = 39;

/// <summary>
/// Gets an array of the installed MSI products.
/// </summary>
public Product[] Products
{
    get { return new ProductEnumeratorWrapper(default(Product)).ToArray(); }
}

/**
 * http://msdn.microsoft.com/en-us/library/aa370101(VS.85).aspx
 */

[DllImport(@"msi.dll", CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.U4)]
private static extern int MsiEnumProducts(
    [MarshalAs(UnmanagedType.U4)] int iProductIndex,
    [Out] StringBuilder lpProductBuf);

/**
 * http://msdn.microsoft.com/en-us/library/aa370130(VS.85).aspx
 */

[DllImport(@"msi.dll", CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.U4)]
private static extern int MsiGetProductInfo(
    string szProduct,
    string szProperty,
    [Out] StringBuilder lpValueBuf,
    [MarshalAs(UnmanagedType.U4)] [In] [Out] ref int pcchValueBuf);

#region Nested type: Product

/// <summary>
/// An MSI product.
/// </summary>
public struct Product
{
    /// <summary>
    /// Gets or sets the product's unique GUID.
    /// </summary>
    public string ProductCode { get; internal set; }

    /// <summary>
    /// Gets the product's name.
    /// </summary>
    public string ProductName { get; internal set; }

    /// <summary>
    /// Gets the path to the product's local package.
    /// </summary>
    public FileInfo LocalPackage { get; internal set; }

    /// <summary>
    /// Gets the product's version.
    /// </summary>
    public string ProductVersion { get; internal set; }

    /// <summary>
    /// Gets the product's install date.
    /// </summary>
    public DateTime InstallDate { get; internal set; }
}

#endregion

#region Nested type: ProductEnumeratorWrapper

private class ProductEnumeratorWrapper : IEnumerable<Product>
{
    private Product data;

    public ProductEnumeratorWrapper(Product data)
    {
        this.data = data;
    }

    #region IEnumerable<Product> Members

    public IEnumerator<Product> GetEnumerator()
    {
        return new ProductEnumerator(this);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion

    #region Nested type: ProductEnumerator

    private class ProductEnumerator : IEnumerator<Product>
    {
        /// <summary>
        /// A format provider used to format DateTime objects.
        /// </summary>
        private static readonly IFormatProvider DateTimeFormatProvider =
            CultureInfo.CreateSpecificCulture("en-US");

        /// <summary>
        /// The enumerator's wrapper.
        /// </summary>
        private readonly ProductEnumeratorWrapper wrapper;

        /// <summary>
        /// The index.
        /// </summary>
        private int i;

        public ProductEnumerator(ProductEnumeratorWrapper wrapper)
        {
            this.wrapper = wrapper;
        }

        #region IEnumerator<Product> Members

        public Product Current
        {
            get { return this.wrapper.data; }
        }

        object IEnumerator.Current
        {
            get { return this.wrapper.data; }
        }

        public bool MoveNext()
        {
            var buffer = new StringBuilder(GuidLength);
            var hresult = MsiEnumProducts(this.i++, buffer);
            this.wrapper.data.ProductCode = buffer.ToString();

            switch (hresult)
            {
                case ErrorSuccess:
                {
                    try
                    {
                        this.wrapper.data.InstallDate =
                            DateTime.ParseExact(
                                GetProperty(@"InstallDate"),
                                "yyyyMMdd",
                                DateTimeFormatProvider);
                    }
                    catch 
                    {
                        this.wrapper.data.InstallDate = DateTime.MinValue;
                    }
                    

                    try
                    {
                        this.wrapper.data.LocalPackage =
                            new FileInfo(GetProperty(@"LocalPackage"));
                    }
                    catch 
                    {
                        this.wrapper.data.LocalPackage = null;
                    }
                    
                    try
                    {
                        this.wrapper.data.ProductName =
                            GetProperty(@"InstalledProductName");
                    }
                    catch 
                    {
                        this.wrapper.data.ProductName = null;
                    }
                    
                    try
                    {
                        this.wrapper.data.ProductVersion =
                            GetProperty(@"VersionString");
                    }
                    catch
                    {
                        this.wrapper.data.ProductVersion = null;
                    }

                    return true;
                }
                case ErrorNoMoreItems:
                {
                    return false;
                }
                default:
                {
                    // throw new Win32Exception(hresult);
                    return true;
                }
            }
        }

        public void Reset()
        {
            this.i = 0;
        }

        public void Dispose()
        {
            // Do nothing
        }

        #endregion

        /// <summary>
        /// Gets an MSI property.
        /// </summary>
        /// <param name="name">The name of the property to get.</param>
        /// <returns>The property's value.</returns>
        /// <remarks>
        /// For more information on available properties please see:
        /// http://msdn.microsoft.com/en-us/library/aa370130(VS.85).aspx
        /// </remarks>
        private string GetProperty(string name)
        {
            var size = 0;
            var hresult =
                MsiGetProductInfo(
                    this.wrapper.data.ProductCode, name, null, ref size);

            if (hresult == ErrorSuccess || hresult == ErrorMoreData)
            {
                var buffer = new StringBuilder(++size);
                hresult =
                    MsiGetProductInfo(
                        this.wrapper.data.ProductCode,
                        name,
                        buffer,
                        ref size);

                if (hresult == ErrorSuccess)
                {
                    return buffer.ToString();
                }
            }

            throw new Win32Exception(hresult);
        }
    }

    #endregion
}

#endregion
'@;

$msiUtilType = Add-Type `
    -MemberDefinition $msiUtilSignature `
    -Name "MsiUtil" `
    -Namespace "Win32Native" `
    -Language CSharpVersion3 `
    -UsingNamespace System.Linq, `
                    System.IO, `
                    System.Collections, `
                    System.Collections.Generic, `
                    System.ComponentModel, `
                    System.Globalization, `
                    System.Text `
    -PassThru;

# Initialize a new Win32Native.MsiUtil object.
$msiUtil = New-Object -TypeName "Win32Native.MsiUtil";

# Print the pubished or installed products.
$msiUtil.Products

The script requires PowerShell 2.0 and .NET 3.5. If you don’t have these I’ll wait here until you install them.

Ready? Good. Let’s proceed. Invoking the script is very simple. Save the above script as a file called Get-MSI.ps1 on your desktop. Next, open a PowerShell command prompt, navigate to your desktop folder, and type Get-MSI.ps1. If PowerShell complains about script execution policy and you don’t know what to do, go read up on the subject and come back when you’re ready. I’ll be here waiting.

Here is an abbreviated output of Get-MSI.ps1:

...

ProductCode    : {4A03706F-666A-4037-7777-5F2748764D10}
ProductName    : Java Auto Updater
LocalPackage   : C:\WINDOWS\Installer\2836d3.msi
ProductVersion : 2.0.2.4
InstallDate    : 10/11/2010 12:00:00 AM

ProductCode    : {6956856F-B6B3-4BE0-BA0B-8F495BE32033}
ProductName    : Apple Software Update
LocalPackage   : C:\WINDOWS\Installer\4163b.msi
ProductVersion : 2.1.1.116
InstallDate    : 5/1/2010 12:00:00 AM

ProductCode    : {CAA376AF-0DE8-4FCA-942E-C6AC579B94B3}
ProductName    : Microsoft Windows SDK for Visual Studio 2008 SP1 Tools
LocalPackage   : C:\WINDOWS\Installer\84579fc.msi
ProductVersion : 6.1.5294.17011
InstallDate    : 9/23/2010 12:00:00 AM

ProductCode    : {0A0CADCF-78DA-33C4-A350-CD51849B9702}
ProductName    : Microsoft .NET Framework 4 Extended
LocalPackage   : C:\WINDOWS\Installer\c2dc0.msi
ProductVersion : 4.0.30319
InstallDate    : 10/2/2010 12:00:00 AM

...

There are all sorts of fun things we can do with this script. For example, what if I want to know about all of the MSI products installed that have a product name that begins with the text “EMC”? All I have to do is use PowerShell’s Where-Object syntax:

.\Get-MSI.ps1 | ? {$_.ProductName -match "EMC.*"}

ProductCode    : {05E0C740-CB6A-4E54-864C-7F846EE4DCED}
ProductName    : EMC VSI for VMware vSphere Feature - Storage Viewer
LocalPackage   : C:\WINDOWS\Installer\187a0e41.msi
ProductVersion : 4.0.44.0
InstallDate    : 10/20/2010 12:00:00 AM

ProductCode    : {D1FBB12B-13F2-4603-AD2D-69E768BE941C}
ProductName    : EMC Unified Storage Plug-in for VMware
LocalPackage   : C:\WINDOWS\Installer\130ed1be.msi
ProductVersion : 2.0.3.4
InstallDate    : 8/26/2010 12:00:00 AM

ProductCode    : {BF4B1AB4-E546-4C1F-BEDD-8F5AEE08E5C8}
ProductName    : EMC Solutions Enabler
LocalPackage   : C:\WINDOWS\Installer\321cfd69.msi
ProductVersion : 7.12.1059
InstallDate    : 10/26/2010 12:00:00 AM

ProductCode    : {5B3B8A55-9762-4061-B6CD-6198557532A0}
ProductName    : EMC Virtual Storage Integrator (VSI) for VMware vSphere
LocalPackage   : C:\WINDOWS\Installer\48eaeb63.msi
ProductVersion : 4.0.0.0
InstallDate    : 10/31/2010 12:00:00 AM

ProductCode    : {C5F91C56-367E-4804-ABC0-1C9C6EEE5088}
ProductName    : EMC VSI for VMware vSphere Feature - HelloWorld
LocalPackage   : C:\WINDOWS\Installer\48eaeb4b.msi
ProductVersion : 4.0.0.1
InstallDate    : 10/31/2010 12:00:00 AM

Or what if I wanted to uninstall the EMC VSI for VMware vSphere Feature, HelloWorld?

.\Get-MSI.ps1 | ? {$_.ProductName -match "EMC.*HelloWorld"} | msiexec /uninstall $_.LocalPackage

You could even remove all of a company’s (we’ll call the company “Fubar”) MSI products silently:

.\Get-MSI.ps1 | ? {$_.ProductName -match "Fubar.*"} | msiexec /uninstall $_.LocalPackage /qn

Hope this helps!

Advertisements

9 thoughts on “Get-MSI

  1. Great script, Andrew! I found the results similar to the much simpler “gwmi win32_product”, but at a 20x performance improvement. Your script took about 5 seconds to query and display results. Using WMI took me 102 seconds. The WMI class has more properties in it, as well as a few methods, but I’m not sure that they are all that useful. It’s always bothered me that win32_product was so slow.

    1. Thanks! WMI in my opinion is generally slow and useless. Also, it doesn’t let you do anything you can’t get to via the win32api. The properties and methods you’re referring to are probably available via the win32api, and I could probably expose them given the need.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s