Today I ran into an interesting continuous integration-type scenario. One of the skunkworks projects that I’m looking at internally yields frequent builds of MSI files for the same product. These MSI files are automatically generated (sometimes daily) by our source control system.
So I have a machine where I’ve got this MSI installed. Let’s call it “Installer.MSI” and when I look in Add/Remove Programs it shows up as “Installer v1.9.0.” Very nice. Now I have a new build today. What if I wanted to upgrade to the “today” build?
I came up with this little case statement:
Has ____ build installed | DO: |
---|---|
no | Install MSI using msiexec.exe |
old | Uninstall Old MSI using msiexec.exe, then Install MSI using msiexec.exe |
same | No installs, just watch videos online |
This is the problem that I found though – although I can get version information about the MSI file…
…and I can get version information about the current installed version…
… the two don’t correlate together. There’s no “ProductVersion” information available via the metadata on the MSI.
Turns out that’s because the good details are stored in WITHIN the MSI database – like files are in a computer.
So, I poured over the Bing and Google references and ended up on an MSDN page and on a page on Nickolaj Andersen’s (@NickolajA) website.
Now I read thought the entire script and it seemed fairly straightforward, but I wasn’t familiar with the intricacies of the ComObject. Nickolaj set it up as a script with parameters. I took his information and refactored it as a PowerShell function.
<pre class="wp-block-syntaxhighlighter-code"><#
.Synopsis
Get product- & version-specific information from MSI file
.DESCRIPTION
Use the MsiInstaller.Installer ComObject to enumerate MSI database specific information
There are only 5 properties for MSI's that are mandatory. (According to https://msdn.microsoft.com/en-us/library/windows/desktop/aa370905(v=vs.85).aspx )
These are:
ProductCode - A unique identifier for a specific product release.
Manufacturer - Name of the application manufacturer.
ProductName - Human readable name of an application.
ProductVersion - String format of the product version as a numeric value.
ProductLanguage - Numeric language identifier (LANGID) for the database.
By default all of these are returned. This can be modified by using the [-Property] Parameter.
.EXAMPLE
PS C:\> Get-MsiInformation -Path "$env:Temp\Installer.msi"
Path : C:\Users\username\AppData\Local\Temp\Installer.msi
ProductCode : {75BDEFC7-6E84-55FF-C326-CE14E3C889EC}
ProductVersion : 1.9.492.0
ProductName : Installer v1.9.0
Manufacturer : My Company, Inc.
ProductLanguage : 1033
This example takes the path as a parameter and returns all fields
.EXAMPLE
Get-ChildItem -Path "$env:Temp\1.0.0" -Recurse -File -Include "*.msi" | Get-MsiInformation -Property ProductVersion
Path ProductVersion
---- --------------
C:\Users\username\AppData\Local\Temp\Build456\Installer.msi 1.0.456.0
C:\Users\username\AppData\Local\Temp\Build457\Installer.msi 1.0.451.0
C:\Users\username\AppData\Local\Temp\Build458\Installer.msi 1.0.451.0
C:\Users\username\AppData\Local\Temp\Build459\Installer.msi 1.0.451.0
C:\Users\username\AppData\Local\Temp\Build460\Installer.msi 1.0.451.0
C:\Users\username\AppData\Local\Temp\Build461\Installer.msi 1.0.451.0
C:\Users\username\AppData\Local\Temp\Build462\Installer.msi 1.0.462.0
C:\Users\username\AppData\Local\Temp\Build463\Installer.msi 1.0.463.0
C:\Users\username\AppData\Local\Temp\Build464\Installer.msi 1.0.451.0
C:\Users\username\AppData\Local\Temp\Build465\Installer.msi 1.0.465.0
C:\Users\username\AppData\Local\Temp\Build466\Installer.msi 1.0.466.0
C:\Users\username\AppData\Local\Temp\Build467\Installer.msi 1.0.467.0
C:\Users\username\AppData\Local\Temp\Build468\Installer.msi 1.0.468.0
C:\Users\username\AppData\Local\Temp\Build469\Installer.msi 1.0.467.0
C:\Users\username\AppData\Local\Temp\Build471\Installer.msi 1.0.471.0
C:\Users\username\AppData\Local\Temp\Build472\Installer.msi 1.0.472.0
C:\Users\username\AppData\Local\Temp\Build473\Installer.msi 1.0.473.0
C:\Users\username\AppData\Local\Temp\Build474\Installer.msi 1.0.474.0
C:\Users\username\AppData\Local\Temp\Build475\Installer.msi 1.0.475.0
C:\Users\username\AppData\Local\Temp\Build476\Installer.msi 1.0.476.0
C:\Users\username\AppData\Local\Temp\Build477\Installer.msi 1.0.477.0
C:\Users\username\AppData\Local\Temp\Build478\Installer.msi 1.0.473.0
C:\Users\username\AppData\Local\Temp\Build479\Installer.msi 1.0.479.0
C:\Users\username\AppData\Local\Temp\Build480\Installer.msi 1.0.479.0
C:\Users\username\AppData\Local\Temp\Build481\Installer.msi 1.0.481.0
C:\Users\username\AppData\Local\Temp\Build482\Installer.msi 1.0.479.0
C:\Users\username\AppData\Local\Temp\Build483\Installer.msi 1.0.479.0
C:\Users\username\AppData\Local\Temp\Build484\Installer.msi 1.0.484.0
C:\Users\username\AppData\Local\Temp\Build485\Installer.msi 1.0.485.0
C:\Users\username\AppData\Local\Temp\Build486\Installer.msi 1.0.486.0
C:\Users\username\AppData\Local\Temp\Build487\Installer.msi 1.0.487.0
C:\Users\username\AppData\Local\Temp\Build488\Installer.msi 1.0.488.0
C:\Users\username\AppData\Local\Temp\Build489\Installer.msi 1.0.488.0
C:\Users\username\AppData\Local\Temp\Build490\Installer.msi 1.0.488.0
C:\Users\username\AppData\Local\Temp\Build491\Installer.msi 1.0.491.0
C:\Users\username\AppData\Local\Temp\Build492\Installer.msi 1.0.492.0
This example takes multiple paths from a Get-ChildItem query and extracts the information
.INPUTS
[System.IO.File[]] - Single or Array of Paths to interrogate
.OUTPUTS
[System.Management.Automation.PSCustomObject[]] - Contains the Msi File Object and Associated Properties
.LINK
http://blog.kmsigma.com/
.LINK
https://msdn.microsoft.com/en-us/library/windows/desktop/aa370905(v=vs.85).aspx
.LINK
<blockquote class="wp-embedded-content" data-secret="04FLTT0eH3"><a href="http://www.scconfigmgr.com/2014/08/22/how-to-get-msi-file-information-with-powershell/">How to get MSI file information with PowerShell</a></blockquote><iframe class="wp-embedded-content" sandbox="allow-scripts" security="restricted" style="position: absolute; clip: rect(1px, 1px, 1px, 1px);" src="http://www.scconfigmgr.com/2014/08/22/how-to-get-msi-file-information-with-powershell/embed/#?secret=04FLTT0eH3" data-secret="04FLTT0eH3" width="600" height="338" title="“How to get MSI file information with PowerShell” — System Center ConfigMgr" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe>
.NOTES
Heavily Infuenced by http://www.scconfigmgr.com/2014/08/22/how-to-get-msi-file-information-with-powershell/
.FUNCTIONALITY
Uses ComObjects to Enumerate specific fields in the MSI database
#>
function Get-MsiInformation
{
[CmdletBinding(SupportsShouldProcess=$true,
PositionalBinding=$false,
ConfirmImpact='Medium')]
[Alias("gmsi")]
#[OutputType([System.Management.Automation.PSCustomObject[]])]
Param(
[parameter(Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true,
HelpMessage = "Provide the path to an MSI")]
[ValidateNotNullOrEmpty()]
[System.IO.FileInfo[]]$Path,
[parameter(Mandatory=$false)]
[ValidateSet( "ProductCode", "Manufacturer", "ProductName", "ProductVersion", "ProductLanguage" )]
[string[]]$Property = ( "ProductCode", "Manufacturer", "ProductName", "ProductVersion", "ProductLanguage" )
)
Begin
{
# Do nothing for prep
}
Process
{
ForEach ( $P in $Path )
{
if ($pscmdlet.ShouldProcess($P, "Get MSI Properties"))
{
try
{
Write-Verbose -Message "Resolving file information for $P"
$MsiFile = Get-Item -Path $P
Write-Verbose -Message "Executing on $P"
# Read property from MSI database
$WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer
$MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($MsiFile.FullName, 0))
# Build hashtable for retruned objects properties
$PSObjectPropHash = [ordered]@{File = $MsiFile.FullName}
ForEach ( $Prop in $Property )
{
Write-Verbose -Message "Enumerating Property: $Prop"
$Query = "SELECT Value FROM Property WHERE Property = '$( $Prop )'"
$View = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, ($Query))
$View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null)
$Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null)
$Value = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1)
# Return the value to the Property Hash
$PSObjectPropHash.Add($Prop, $Value)
}
# Build the Object to Return
$Object = @( New-Object -TypeName PSObject -Property $PSObjectPropHash )
# Commit database and close view
$MSIDatabase.GetType().InvokeMember("Commit", "InvokeMethod", $null, $MSIDatabase, $null)
$View.GetType().InvokeMember("Close", "InvokeMethod", $null, $View, $null)
$MSIDatabase = $null
$View = $null
}
catch
{
Write-Error -Message $_.Exception.Message
}
finally
{
Write-Output -InputObject @( $Object )
}
} # End of ShouldProcess If
} # End For $P in $Path Loop
}
End
{
# Run garbage collection and release ComObject
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($WindowsInstaller) | Out-Null
[System.GC]::Collect()
}
}
<#
End of Function
#></pre>
I’m going to place the entire script here for download and consumption.
If you find this script useful, please let me know.
If you find a problem with this script, please let me know.
Ultimately, if there’s something with this script in some way, shape, or form, feel free to let me know.
Until next time crew!