Skip to content

Instantly share code, notes, and snippets.

@jstangroome
Created April 11, 2011 04:43
Show Gist options
  • Save jstangroome/913062 to your computer and use it in GitHub Desktop.
Save jstangroome/913062 to your computer and use it in GitHub Desktop.
Read the ProductVersion from a Windows Installer MSI file via PowerShell
function Get-MsiProductVersion {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateScript({$_ | Test-Path -PathType Leaf})]
[string]
$Path
)
function Get-Property ($Object, $PropertyName, [object[]]$ArgumentList) {
return $Object.GetType().InvokeMember($PropertyName, 'Public, Instance, GetProperty', $null, $Object, $ArgumentList)
}
function Invoke-Method ($Object, $MethodName, $ArgumentList) {
return $Object.GetType().InvokeMember($MethodName, 'Public, Instance, InvokeMethod', $null, $Object, $ArgumentList)
}
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
#http://msdn.microsoft.com/en-us/library/aa369432(v=vs.85).aspx
$msiOpenDatabaseModeReadOnly = 0
$Installer = New-Object -ComObject WindowsInstaller.Installer
$Database = Invoke-Method $Installer OpenDatabase @($Path, $msiOpenDatabaseModeReadOnly)
$View = Invoke-Method $Database OpenView @("SELECT Value FROM Property WHERE Property='ProductVersion'")
Invoke-Method $View Execute
$Record = Invoke-Method $View Fetch
if ($Record) {
Write-Output (Get-Property $Record StringData 1)
}
Invoke-Method $View Close @()
Remove-Variable -Name Record, View, Database, Installer
}
@chicinho
Copy link

Is it also possible to set a property?

@NicklausBrain
Copy link

Great!

@pauby
Copy link

pauby commented Jul 24, 2018

Line 30 and Line 37 $need to have $null = Invoke-Method ... as what they return is going back into the output which creates a 3 string array with only the second item having the version string. Making those changes results in a simple string which is what we are looking for.

@DeveloperAbelCastillo
Copy link

Excelente aporte

@Mobe1969
Copy link

Great function.

One problem I have (and haven't been able to fire a fix) it the script leaves the file locked for the remainder of the session. So if I'm checking the installer version, and getting a newer one if required, it fails.

@jstangroome
Copy link
Author

One problem I have (and haven't been able to fire a fix) it the script leaves the file locked for the remainder of the session. So if I'm checking the installer version, and getting a newer one if required, it fails.

I haven't used this script in some time but I would try adding [System.Runtime.Interopservices.Marshal]::ReleaseComObject($Database) between lines 37 and 38.
MicrosoftDocs/PowerShell-Docs#4617

@Mobe1969
Copy link

One problem I have (and haven't been able to fire a fix) it the script leaves the file locked for the remainder of the session. So if I'm checking the installer version, and getting a newer one if required, it fails.

I haven't used this script in some time but I would try adding [System.Runtime.Interopservices.Marshal]::ReleaseComObject($Database) between lines 37 and 38.
MicrosoftDocs/PowerShell-Docs#4617

Yes, I found some more ideas on that as well, and garbage collection. Still no luck. It is driving me nuts!
I was also trying
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Installer) | Out-Null
[System.GC]::Collect()

I even tried doing it in a Process{} End {} construct.

I don't understand why microsoft made it so ridiculous to check a file version in such a bizarre way.

I might need to try figuring how to spawn it in a thread

@jstangroome
Copy link
Author

I might need to try figuring how to spawn it in a thread

Not a thread, perform the check(s) in a child powershell.exe process, that will separate the scope of the objects and their corresponding file locks. If I remember correctly powershell.exe -EncodedCommand ... is a very convenient way to run large chunks of script in another shell, maybe also ... -OutputFormat XML | Import-Clixml to pass back rich objects.

@Mobe1969
Copy link

Mobe1969 commented Feb 13, 2020

OK I got a working version. Dealing with returns is also a pain. I moved to a reference variable...

Also, it returns 0.0.0.0 if it doesn't exist so I can assume need to retrieve it...

`function Get-MsiProductVersion {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[string]
$Path,
[ref]
$Result
)
if (!(Test-Path $Path)) {
Write-Output "0.0.0.0"
} else {
#http://msdn.microsoft.com/en-us/library/aa369432(v=vs.85).aspx
$job = start-job -scriptblock {
param($Path)

        function Get-Property ($Object, $PropertyName, [object[]]$ArgumentList) {
            return $Object.GetType().InvokeMember($PropertyName, 'Public, Instance, GetProperty', $null, $Object, $ArgumentList)
        }
        function Invoke-Method ($Object, $MethodName, $ArgumentList) {
            return $Object.GetType().InvokeMember($MethodName, 'Public, Instance, InvokeMethod', $null, $Object, $ArgumentList)
        }
        $ErrorActionPreference = 'Stop'
        Set-StrictMode -Version Latest
        $msiOpenDatabaseModeReadOnly = 0
        $Installer = New-Object -ComObject WindowsInstaller.Installer
        $Database = Invoke-Method $Installer OpenDatabase  @($Path, $msiOpenDatabaseModeReadOnly) -ErrorAction SilentlyContinue
        if ($null -ne $Database) {
            $View = Invoke-Method $Database OpenView  @("SELECT Value FROM Property WHERE Property='ProductVersion'")
            Invoke-Method $View Execute
            $Record = Invoke-Method $View Fetch
            if ($Record) {
                Write-Output (Get-Property $Record StringData 1)
            }
            Invoke-Method $View Close @()
            [System.Runtime.Interopservices.Marshal]::ReleaseComObject($Database) | Out-Null
        }
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($Installer) | Out-Null
        Remove-Variable -Name Record, View, Database, Installer
        [System.GC]::Collect()
    } -argumentlist @($Path)
    $job | wait-job
    $Output = Receive-Job $job -ErrorAction Stop
    $Result.Value = $Output[1]
    Remove-job $job
    Write-Output $Result.Value
}

}
$CurrentVersion = $null
Get-MsiProductVersion -Path $InstallerPath -Result ([ref]$CurrentVersion)
Write-Output "Current Version: $CurrentVersion"
`

Copy link

ghost commented Dec 2, 2021

@Mobe1969 - Thank you for sharing this. I have been stuck with the file-lock issue when retrieving MSI data. Your bit of code did the trick!

@Mobe1969
Copy link

Mobe1969 commented Dec 2, 2021

No worries @DoublNAT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment