#PSScriptInfo
.VERSION 2026.10.30.0
.GUID 3a7b9c4d-2e8f-4a1b-9d6c-5e3f7a8b9c2d
.AUTHOR Michael Escamilla
.COMPANYNAME
.COPYRIGHT
.TAGS
.LICENSEURI
.PROJECTURI https://github.com/MichaelEscamilla/GetMSIInformation
.ICONURI
.EXTERNALMODULEDEPENDENCIES
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
1.0.0.0 - Initial release
2.0.0.0 - 10-4-2024 - Added file hash information, and context menu items for the installation and uninstallation of a Right-Click Option in Windows Explorer. And some other UI improvements.
2024.10.4.1 - Updated the version numbering, and a sepearator in the context menu.
2024-10.13.0 - Added an error message when the file is locked
2024-12.8.0 - Formatted Script for Publishing to PowerShell Gallery
2026-1.5.0 - Added Icon extraction and export functionality. Added context menu items to open the icon temp folder and right-click menu folder.
2026.1.5.1 - Fixed a bug when launching the script from the internet
2026.1.6.0 - Fixed a bug where the 'No Icon' label would not hide
2026.1.6.1 - Added the michaeltheadmin.com icon to the Form and Right-Click Menu
2026.1.7.0 - Simplified console output messages during script download from GitHub
2026.8.11.0 - Updated UI for less 'default' look.
Added the Compressed Product Code (Compressed GUID) to the GUI.
Right-Click Install will directly call pwsh.exe if available.
2026.8.12.0 - Added a check for updates feature.
Manual check available under the 'About' menu.
A non-blocking background check runs at startup and notifies via the status bar when a newer release is available.
2026.8.12.1 - Added Logic to Update the script depending on the way it was launched.
2026.8.12.2 - Added status bar feedback when installing or removing the right-click menu.
The 'latest version' confirmation now shows in the status bar instead of a pop-up.
2026.8.13.0 - Added screenshot options under the File menu to copy the window to the clipboard or save it as a PNG.
Fixed the drag-and-drop box staying highlighted gray after being clicked.
Property fields now show the accent outline on hover instead of staying outlined after clicking.
2026.8.14.0 - The right-click menu now works on any file type to get hash information, not just MSI files.
Fixed repeating errors when the status bar message tried to reset.
The window now comes to the foreground faster on the first launch in a session.
2026.10.5.0 - Added a reload button for the current file and improved the compressed GUID labels.
2026.10.27.0 - The Upgrade Code label is now clickable and opens a window showing the MSI's full Upgrade table.
2026.10.28.0 - Added a Package Code row to the MSI properties, read from the Summary Information stream.
2026.10.30.0 - Widened the Upgrade table columns so the full Upgrade Code GUID is no longer truncated.
.PRIVATEDATA
#>
<#
.SYNOPSIS
This script provides a graphical user interface (GUI) for viewing and copying properties of MSI files.
.DESCRIPTION
The script creates a WPF-based GUI that allows users to drag and drop MSI files to view their properties such as Product Name, Manufacturer, Product Version, Product Code, and Upgrade Code.
It also provides functionality to copy these properties to the clipboard and to clear the displayed information.
Additionally, the script includes options to install and uninstall a context menu item for MSI files to retrieve their properties.
.PARAMETER FilePath
Optional parameter to specify the path of the MSI file to automatically load the information for.
.NOTES
#>
param (
[Parameter(Mandatory = $false)]
[string]$FilePath
)
#############################################
################# Variables #################
#############################################
# Script Name
$Script:ScriptName = "GetMSIInformation.ps1"
# Script Version
[System.Version]$Script:ScriptVersion = "2026.10.30.0"
$Script:RightClickMenuName = "Get MSI Information"
$Script:RightClickMenuFolderPath = "$env:LOCALAPPDATA\GetMSIInformation"
# Icon Temp Folder Path
$Script:IconTempFolderPath = "$env:TEMP\GetMSIInformation\Icons"
# GitHub Repository (used for the update check)
$Script:GitHubRepo = "MichaelEscamilla/GetMSIInformation"
$Script:ReleasesApiUrl = "https://api.github.com/repos/$Script:GitHubRepo/releases/latest"
$Script:ReleasesPageUrl = "https://github.com/$Script:GitHubRepo/releases"
# Headers required by the GitHub REST API (a User-Agent is mandatory)
$Script:UpdateCheckHeaders = @{
'User-Agent' = 'GetMSIInformation-UpdateCheck'
'Accept' = 'application/vnd.github+json'
}
# How this script was launched; drives which update action the 'Update Available' click takes.
$Script:UpdateChannel = $null
# File path to retry after a file-lock error.
$Script:RetryFilePath = $null
# Get the Security Principal
$Script:currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
# Get PowerShell Version
$Script:ScriptPSVersion = $PSVersionTable.PSVersion
# Get Pwsh Path
$Script:PowerShellPath = (Get-Command pwsh.exe -ErrorAction SilentlyContinue)
# michaeltheadmin.com Icon
$Script:WindowIconBase64 = "AAABAAEAIBwAAAEAIACYDgAAFgAAACgAAAAgAAAAOAAAAAEAIAAAAAAAcA4AAMQOAADEDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABaSFoNRkZGEUtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEEtLSxBLS0sQS0tLEFRGRhFaSEgNAAAAAAAAAABUVFQLUkxPrFNOUOlRTE7tUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7FFMTuxRTE7sUUxO7VROUOhSTU+qTExMCU5KTH5lYWP/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bm1t/25tbf9ubW3/bmxt/2RfYf9OSkx6TkhLtWtoaf94eHj/dnZ2/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3V1df91dXX/dXV1/3Z2dv94eHj/aWZn/05IS69OSUuxamdo/3h4eP9mZGX/Uk1P/05JS/9OSUv/VVFT/1VRUv9OSUv/T0pM/09KTP9PSkz/T0pM/09KTP9PSkz/T0pM/09KTP9PSkz/T0pM/09KTP9PSkz/TklL/1VRU/9VUVP/TklL/05JS/9STlD/ZmRl/3h4eP9oZWb/T0lMqk9JTLBqZ2j/eHl4/1dUVf90cHH/rqys/62rrP9rZ2j/b2tt/66srP+npaX/p6Wl/6elpf+npaX/p6Wl/6elpf+npaX/p6Wl/6elpf+npaX/p6Wl/6elpf+tq6z/a2do/29rbP+urKz/rays/3Bsbv9YVVb/eHl5/2hlZv9PSUyqT0lMsGpnaP94eXj/U1BR/5GPkP/09PT/8/Pz/399ff+Hg4X/9PT0/+jo6P/o6Oj/6Ojo/+jo6P/o6Oj/6Ojo/+jo6P/o6Oj/6Ojo/+jo6P/o6Oj/6Ojo//Pz8/9/fX3/hoOE//T09P/z9PP/i4iJ/1VRUv94eXn/aGVm/09JTKpPSUywamdo/3h5eP9VUlP/hIGC/9PS0v/S0dH/dXJz/3t3ef/T0tL/ycjI/8rJyv/Kycr/ycjI/8nIyP/Kycr/ysnK/8nIyP/JyMj/ysnK/8vKyf/Lycj/1NLR/3dzc/98eHj/1dPS/9TT0f9/fHz/V1NU/3h5ef9oZWb/T0lMqk9JTLBqZ2j/eHl4/19cXv9TTU//V1JU/1dSVP9TTlD/VE9R/1dSVP9XUlT/V1NV/1dTVf9XUlT/V1JU/1dTVf9XU1X/V1JU/1dSVP9XU1X/WFNU/1hSVP9ZU1T/VU9Q/1VPUP9ZU1T/WVNU/1JNTv9gXV7/eHl5/2hlZv9PSUyqT0lMsGpnaP94eXj/V1NV/3x4ef/Hx8j/v76//8HAwf/BwMD/v76//8jHx/9ybnD/dnN0/8jIyP/Ix8f/cm9w/3Vxc//Ix8j/yMfI/4B2cP85VYL/LXrf/y933P8xed7/MXne/y933P8tet7/Q1p//2NaU/94eXn/aGVm/09JTKpPSUywamdo/3h5eP9UUFL/jImK//L09P/n6Oj/5+jo/+fo6P/n6Oj/8/Pz/4B9f/+Gg4T/9PT0//L08/+Afn//hICC//L09P/y9PT/k4h//zRblv8jjf//J4n//yeJ//8nif//J4n//yON//9AYJL/Y1lQ/3h5ef9oZWb/T0lMqk9JTLBqZ2j/eHl4/1hUVv95dHP/v7u3/7izr/+3tLP/tLO0/7GwsP+5t7j/cGxu/3Rwcf+5uLj/ubi4/3Bsbv9ybnD/ubi4/7m4uP98c27/PVV9/y5zzf8xcMr/NHTO/zR0zv8xcMr/LnPN/0RXef9jWlX/eHl5/2hlZv9PSUyqT0lMsGpnaP94eXj/YFxd/01LUv9ITVz/SE5d/0pJUP9TTU7/XFdZ/1tXWf9VUVL/VlFT/1tXWf9bV1n/VVFT/1ZRU/9cV1n/XFdZ/1VQU/9ZU1L/ZFtY/2RbV/9ZUEz/WlFN/2RbWP9kW1j/VlBQ/19cXv94eXn/aGVm/09JTKpPSUywamdo/3h5eP9jWFH/QF+Q/ymE+f8phPn/OFiJ/4yCe//e3t7/3d3d/3p2eP9/fH3/3t7e/93e3f96d3j/fXl7/93e3v/d3t7/fXp7/4B9fv/f3t7/3t3d/3p2d/9/fH3/397e/97e3f+DgIH/VlJT/3h5ef9oZWb/T0lMqk9JTLBqZ2j/eHl4/2NYT/8+Ypr/JI3//ySN//81WpL/lYuC//Pz8//y8vL/gHx+/4aDhP/z8/P/8vPy/4B9fv+DgIL/8vPz//Lz8/+DgYL/h4SF//Pz8//y8vL/f3x9/4aDhP/z8/P/8vPy/4qIiP9VUVL/eHl5/2hlZv9PSUyqT0lMsGpnaP94eXj/YFhV/0ZTbv87aaj/O2mo/0NRbP9waGX/m5iZ/5uYmf9mYWP/aGRm/5uYmf+bmJn/ZmJj/2djZf+bmJn/m5iZ/2djZf9pZWb/m5iZ/5uYmf9lYWL/amZo/6Cdnv+fnZ7/a2Zo/1lVV/94eXn/aGVm/09JTKpOSUuxamdo/3d4eP9nZmj/XFVT/2BUTP9gVEz/X1hV/1dUVv9QTE7/UExO/1lVV/9ZVVb/UExO/1BMTv9ZVVf/WVVW/1BMTv9QTE7/WVVW/1hVVv9QTE7/UExO/1lVV/9ZVVb/UE1O/1FNTv9WUlT/aWdo/3d4eP9oZWb/TkhLq09JTLNraGn/eHl5/3d3d/93eHj/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4d/93eHf/d3h3/3d4eP93d3f/eHl4/2hmZ/9OSEuuUUpMbWRfYf9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/amhp/2poaf9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/bWts/21rbP9ta2z/bWts/21rbP9samv/Yl1f/1BLS2hRUVECUUtNjFNNT89QS0zTUEpM01BKTNNQSkzTUEpM01BKTNNQSkzTUEpM01BKTNNQSkzTUEpM01BLTdFQS03yUEtN8FBLTdFQSkzTUEpM01BKTNNQSkzTUEpM01BKTNNQSkzTUEpM01BKTNNQSkzTUEtM01NNT89RTE6IfHx8AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFNMT7lTTVCmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU01Qs1JNT7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABTTU1RWFNV/1NOUIhUTE9pVFBSaExMTBYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABRTE9mVlFS11NOUOJWUVLdUEtQNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//////////wAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//n////5////+B////wf///////////8="
#############################################
################# Functions #################
#############################################
#region Functions
function Test-FileLock {
param (
[Parameter(Mandatory = $true)]
[string]$Path
)
try {
$FileStream = [System.IO.File]::Open("$($Path)", 'Open', 'Write')
$FileStream.Close()
$FileStream.Dispose()
return $false
}
catch {
return $true
}
}
function Show-FileLockedMessage {
param (
[Parameter(Mandatory = $true)]
[string]$Path
)
$Script:RetryFilePath = $Path
$lsbox_FilePath.Items.Clear()
$lsbox_FilePath.Items.Add("ERROR: The file is locked:`n[$Path]")
$lsbox_FilePath.Background = [System.Windows.Media.Brushes]::Red
$lsbox_FilePath.Foreground = [System.Windows.Media.Brushes]::Yellow
$lsbox_FilePath.FontWeight = 'Bold'
$lsbox_FilePath.FontSize = 16
$btn_FilePath_Retry.Visibility = [System.Windows.Visibility]::Visible
$btn_FilePath_Retry.IsEnabled = $true
}
function Get-MsiProperties {
param (
[Parameter(Mandatory = $true)]
[IO.FileInfo[]]$Path
)
Write-Host "Getting MSI Properties for: [$Path]"
# Check if the MSI file path exists
if (-not (Test-Path $Path)) {
throw "The file $Path does not exist."
}
# Create a new Windows Installer COM object
$WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer
# Open the MSI database in read-only mode
$MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($Path.FullName, 0))
# Open a view on the Property table
$MSIPropertyView = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT * FROM Property"))
# Execute the view query
$MSIPropertyView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $MSIPropertyView, $null) | Out-Null
# Fetch the first record from the result set
$MSIRecord = $MSIPropertyView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $MSIPropertyView, $null)
# Initialize an empty System Object to store properties
[System.Object]$Properties = @{}
# Loop through all records in the result set
while ($null -ne $MSIRecord) {
# Get the property name from the first column
$property = $MSIRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $MSIRecord, @(1))
# Get the property value from the second column
$Value = $MSIRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $MSIRecord, @(2))
# Add the property name and value to the hashtable
$Properties[$Property] = $Value
# Fetch the next record from the result set
$MSIRecord = $MSIPropertyView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $MSIPropertyView, $null)
}
# Close the Property view
$MSIPropertyView.GetType().InvokeMember("Close", "InvokeMethod", $null, $MSIPropertyView, $null) | Out-Null
# The Package Code lives in the Summary Information stream (Revision Number, PID 9), not the Property table
try {
$SummaryInfo = $WindowsInstaller.GetType().InvokeMember("SummaryInformation", "GetProperty", $null, $WindowsInstaller, @($Path.FullName, 0))
$Properties["PackageCode"] = $SummaryInfo.GetType().InvokeMember("Property", "GetProperty", $null, $SummaryInfo, @(9))
}
catch {
Write-Verbose "Error retrieving Package Code: $_"
}
# Return the System Object of properties
$Properties
}
function Get-MsiUpgradeTable {
param (
[Parameter(Mandatory = $true)]
[IO.FileInfo[]]$Path
)
Write-Host "Getting MSI Upgrade Table for: [$Path]"
# Check if the MSI file path exists
if (-not (Test-Path $Path)) {
throw "The file $Path does not exist."
}
# Create a new Windows Installer COM object
$WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer
# Open the MSI database in read-only mode
$MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($Path.FullName, 0))
# Start building the list of Upgrade entries
[Collections.Generic.List[PSCustomObject]]$UpgradeEntries = @()
# Confirm the Upgrade table exists before querying it (not every MSI has one)
$TableView = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT Name FROM _Tables WHERE Name='Upgrade'"))
$TableView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $TableView, $null) | Out-Null
$TableExists = $TableView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $TableView, $null)
$TableView.GetType().InvokeMember("Close", "InvokeMethod", $null, $TableView, $null) | Out-Null
if ($TableExists) {
# Open a view on the Upgrade table
$UpgradeView = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT UpgradeCode,VersionMin,VersionMax,Language,Attributes,Remove,ActionProperty FROM Upgrade"))
# Execute the view query
$UpgradeView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $UpgradeView, $null) | Out-Null
# Fetch the first record from the result set
$UpgradeRecord = $UpgradeView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $UpgradeView, $null)
# Loop through all records in the result set
while ($null -ne $UpgradeRecord) {
$UpgradeEntries.Add([PSCustomObject]@{
UpgradeCode = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(1))
VersionMin = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(2))
VersionMax = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(3))
Language = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(4))
Attributes = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(5))
Remove = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(6))
ActionProperty = $UpgradeRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $UpgradeRecord, @(7))
})
# Fetch the next record from the result set
$UpgradeRecord = $UpgradeView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $UpgradeView, $null)
}
# Close the Upgrade view
$UpgradeView.GetType().InvokeMember("Close", "InvokeMethod", $null, $UpgradeView, $null) | Out-Null
}
# Return the list of Upgrade entries
$UpgradeEntries
}
# Stolen from: https://github.com/codaamok
# https://gist.github.com/codaamok/7ed30d01280ce28bb451621966707c1b
function Convert-GuidToCompressedGuid {
param(
[Parameter(Mandatory = $true)]
[string]$Guid
)
function Get-ReversedString ([array]$a) {
[String]::Join('', $a[-1.. - ($a.Count)])
}
function Get-ReversedBytes ([String]$a) {
[String]::Join('', ($a -split '(..)' -ne '' -replace '(\w)(\w)', '$2$1'))
}
# Strip braces and dashes from the GUID
$Guid = $Guid -replace '\{|\}|\-'
$data1 = Get-ReversedString $Guid[0..7]
$data2 = Get-ReversedString $Guid[8..11]
$data3 = Get-ReversedString $Guid[12..15]
$data4 = Get-ReversedBytes ($Guid[16..19] -join '')
$data5 = Get-ReversedBytes ($Guid[20..31] -join '')
return '{0}{1}{2}{3}{4}' -f $data1, $data2, $data3, $data4, $data5
}
function Enable-AllButtons {
param (
[Parameter(Mandatory = $false)]
[string]$Exclude
)
# Get all button variables
$Buttons = Get-Variable -Name "btn_*" -ValueOnly -ErrorAction SilentlyContinue
foreach ($Button in $Buttons) {
# Check that the button name does not contain the exclude variable
if ($Button.Name -notlike "*$Exclude*") {
# Enable Button
$Button.IsEnabled = $true
}
}
}
function Disable-AllButtons {
# Get all button variables
$Buttons = Get-Variable -Name "btn_*" -ValueOnly -ErrorAction SilentlyContinue
foreach ($Button in $Buttons) {
# Disable Button
$Button.IsEnabled = $false
}
}
function Clear-Textboxes {
# Get all textbox variables
$Textboxes = Get-Variable -Name "txt_*" -ValueOnly -ErrorAction SilentlyContinue
foreach ($Textbox in $Textboxes) {
# Clear textbox
$Textbox.Clear()
}
# Reset the Upgrade Code label back to its non-clickable look
Set-UpgradeCodeLabelClickable -Enabled $false
}
function Set-UpgradeCodeLabelClickable {
param (
[Parameter(Mandatory = $true)]
[bool]$Enabled
)
if ($Enabled) {
$lbl_UpgradeCode.Foreground = $formMSIProperties.FindResource("Accent")
$lbl_UpgradeCode.Cursor = [System.Windows.Input.Cursors]::Hand
$lbl_UpgradeCode.ToolTip = "Click to view the MSI Upgrade table"
}
else {
$lbl_UpgradeCode.Foreground = $formMSIProperties.FindResource("TextMuted")
$lbl_UpgradeCode.Cursor = [System.Windows.Input.Cursors]::Arrow
$lbl_UpgradeCode.ToolTip = $null
}
}
# Stolen from: https://github.com/PatchMyPCTeam/CustomerTroubleshooting/blob/Release/PowerShell/Get-LocalContentHashes.ps1
function Get-EncodedHash {
[CmdletBinding()]
Param(
[Parameter(Position = 0)]
[System.Object]$HashValue
)
$hashBytes = $hashValue.Hash -split '(?<=\G..)(?=.)' | ForEach-Object { [byte]::Parse($_, 'HexNumber') }
Return [Convert]::ToBase64String($hashBytes)
}
function Get-FileHashInformation {
param (
[Parameter(Mandatory = $true)]
[IO.FileInfo[]]$Path
)
Write-Host "Getting File Hash Information for: [$Path]"
# Initialize the hash object
$Hashes = @{}
# Get File Hash - MD5
$FileHashMD5 = Get-FileHash -Path $Path -Algorithm MD5
$Hashes["MD5"] = $FileHashMD5
# Get File Hash - SHA1
$FileHashSHA1 = Get-FileHash -Path $Path -Algorithm SHA1
$Hashes["SHA1"] = $FileHashSHA1
# Get File Hash - SHA256
$FileHashSHA256 = Get-FileHash -Path $Path -Algorithm SHA256
$Hashes["SHA256"] = $FileHashSHA256
# Get File Hash - SHA1 - Encoded
$FileHashEncoded = Get-EncodedHash -HashValue $FileHashSHA1
$Hashes["Digest"] = $FileHashEncoded
# Get File Hash - SHA256 - Encoded
$FileHashSHA256Encoded = Get-EncodedHash -HashValue $FileHashSHA256
$Hashes["D256"] = $FileHashSHA256Encoded
# Return the hash object
$Hashes
}
function Get-MsiIcon {
param (
[Parameter(Mandatory = $true)]
[IO.FileInfo[]]$Path,
[Parameter(Mandatory = $false)]
[string]$ExportFolder = "$($Script:IconTempFolderPath)"
)
Write-Host "Getting MSI Icon for: [$Path]"
# Ensure the export folder exists
if (-not (Test-Path -Path $ExportFolder)) {
New-Item -ItemType Directory -Path $ExportFolder -Force | Out-Null
}
# Create a new Windows Installer COM object
$WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer
# Open the MSI database in read-only mode
$MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($Path.FullName, 0))
# Get all Icons in the Icon table
try {
# Open a view on the Icon table to get the icon binary data
$IconView = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT Name FROM _Tables WHERE Name='Icon'"))
# Execute the view query
$IconView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $IconView, $null) | Out-Null
# Fetch Record
$IconTable = $IconView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $IconView, $null)
# Close the Icon view
$IconView.GetType().InvokeMember("Close", "InvokeMethod", $null, $IconView, $null) | Out-Null
if ($IconTable) {
# Open a view on the Icon table
$IconData = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, @("SELECT Name,Data FROM Icon"))
# Execute the view query
$IconData.GetType().InvokeMember("Execute", "InvokeMethod", $null, $IconData, $null) | Out-Null
# Start Building PSCustomObject
[Collections.Generic.List[PSCustomObject]]$IconInformation = @()
Do {
# Fetch the next record
$IconRecord = $IconData.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $IconData, $null)
if ($IconRecord) {
# Get the 'Name' Field
$IconDataName = $IconData.GetType().InvokeMember("StringData", 'Public, Instance, GetProperty', $null, $IconRecord, 1)
# Get the DataSize of the Binary Data
$IconDataSize = $IconData.GetType().InvokeMember("DataSize", "GetProperty", $null, $IconRecord, 2)
Write-Verbose "Found Icon: [$IconDataName] with Size: [$IconDataSize] bytes"
# Read the Binary Data
$IconBinaryData = $IconData.GetType().InvokeMember("ReadStream", "InvokeMethod", $null, $IconRecord, @(2, $IconDataSize, 2))
# Get Binary data as ANSI string - use Windows-1252 encoding
$ByteArray = [System.Text.Encoding]::GetEncoding(1252).GetBytes($IconBinaryData)
# Construct the path to save the ICO file
$IconPathTemp = Join-Path -Path $ExportFolder -ChildPath "$($IconDataName)"
# Check if the file already exists and delete it if necessary
if (Test-Path -Path $IconPathTemp) {
Remove-Item -Path $IconPathTemp -Force
}
# Write the byte array to an ICO file
[System.IO.File]::WriteAllBytes($IconPathTemp, $ByteArray)
# Create PSCustomObject for Icon Information
$IconInfoObject = [PSCustomObject]@{
Name = $IconDataName
Size = $IconDataSize
Path = $IconPathTemp
}
# Add to Icon Information List
$IconInformation.Add($IconInfoObject)
}
} While ($IconRecord)
# Close the IconData view
$IconData.GetType().InvokeMember("Close", "InvokeMethod", $null, $IconData, $null) | Out-Null
}
}
catch {
Write-Verbose "Error retrieving Icons: $_"
}
Return $IconInformation
}
function Set-TextboxInformation {
param (
[Parameter(Mandatory = $false)]
[System.Object]$MSIPropertiesInfo,
[Parameter(Mandatory = $true)]
[hashtable]$FileHashInfo
)
# Set the MSI file properties textboxes (only when MSI properties are provided)
if ($MSIPropertiesInfo) {
$txt_ProductName.Text = $MSIPropertiesInfo.ProductName
$txt_Manufacture.Text = $MSIPropertiesInfo.Manufacturer
$txt_ProductVersion.Text = $MSIPropertiesInfo.ProductVersion
$txt_ProductCode.Text = $MSIPropertiesInfo.ProductCode
$txt_UpgradeCode.Text = $MSIPropertiesInfo.UpgradeCode
$txt_PackageCode.Text = $MSIPropertiesInfo.PackageCode
# Set the compressed GUIDs from the Product and Upgrade Codes
if ($MSIPropertiesInfo.ProductCode) {
$txt_CompressedGUID.Text = Convert-GuidToCompressedGuid -Guid $MSIPropertiesInfo.ProductCode
}
if ($MSIPropertiesInfo.UpgradeCode) {
$txt_CompressedUpgradeGUID.Text = Convert-GuidToCompressedGuid -Guid $MSIPropertiesInfo.UpgradeCode
}
}
# Toggle the clickable look of the Upgrade Code label based on whether a code is present
Set-UpgradeCodeLabelClickable -Enabled ([bool]$MSIPropertiesInfo.UpgradeCode)
# Set the File Hash Information textboxes
$txt_MD5.Text = $FileHashInfo.MD5.Hash
$txt_SHA1.Text = $FileHashInfo.SHA1.Hash
$txt_SHA256.Text = $FileHashInfo.SHA256.Hash
$txt_Digest.Text = $FileHashInfo.Digest
$txt_D256.Text = $FileHashInfo.D256
}
function Show-UpgradeCodeDetails {
# Nothing to show without a loaded MSI
if (-not $Script:LoadedMSIPath) {
return
}
# Read the Upgrade table from the currently loaded MSI
try {
$UpgradeEntries = Get-MsiUpgradeTable -Path $Script:LoadedMSIPath
}
catch {
Write-Host "Error reading Upgrade table: $_"
$UpgradeEntries = @()
}
# Themed popup window (self-contained palette so it matches the main window)
[xml]$XAMLUpgrade = @"
"@
# Load the popup window
$ReaderUpgrade = New-Object System.Xml.XmlNodeReader $XAMLUpgrade
[System.Windows.Window]$UpgradeWindow = [Windows.Markup.XamlReader]::Load($ReaderUpgrade)
# Grab named controls
$dg_Upgrade = $UpgradeWindow.FindName("dg_Upgrade")
$txtblk_NoUpgrade = $UpgradeWindow.FindName("txtblk_NoUpgrade")
$txtblk_UpgradeStatus = $UpgradeWindow.FindName("txtblk_UpgradeStatus")
$titlebar = $UpgradeWindow.FindName("titlebar")
$titlebar_Close = $UpgradeWindow.FindName("titlebar_Close")
# Populate the grid, or show the empty-state message
if ($UpgradeEntries -and $UpgradeEntries.Count -gt 0) {
$dg_Upgrade.ItemsSource = @($UpgradeEntries)
$rowText = if ($UpgradeEntries.Count -eq 1) { "entry" } else { "entries" }
$txtblk_UpgradeStatus.Text = "$($UpgradeEntries.Count) Upgrade $rowText"
}
else {
$dg_Upgrade.Visibility = [System.Windows.Visibility]::Collapsed
$txtblk_NoUpgrade.Visibility = [System.Windows.Visibility]::Visible
$txtblk_UpgradeStatus.Text = "No Upgrade table found"
}
# Window chrome behavior
$titlebar.Add_MouseLeftButtonDown({ try { $UpgradeWindow.DragMove() } catch { } })
$titlebar_Close.Add_Click({ $UpgradeWindow.Close() })
# Center on the owner window and show modally
$UpgradeWindow.Owner = $formMSIProperties
$UpgradeWindow.WindowStartupLocation = "CenterOwner"
$UpgradeWindow.Icon = $formMSIProperties.Icon
$UpgradeWindow.ShowDialog() | Out-Null
}
function Build-IconImageControls {
param (
[Parameter(Mandatory = $true)]
[PSCustomObject[]]$IconObjects
)
# Start Building PSCustomObject
[Collections.Generic.List[PSCustomObject]]$Script:IconImageControlsList = @()
# Loop through each Icon
$IconIndex = 0
foreach ($IconObject in $IconObjects) {
if ((Test-Path ($IconObject.Path))) {
# IconBitmap Export File Path
$IconBitmapPath = Join-Path -Path "$($IconObject.Path | Split-Path -Parent)" -ChildPath "$([System.IO.Path]::GetFileNameWithoutExtension($IconObject.Name))_Export.ico"
# Extract the Binary file to a Bitmap File at 256px
if ($Script:ScriptPSVersion -lt [Version]"7.4") {
# This is the only method available in .NET 4.x.x
$IconExtract = [System.Drawing.Icon]::ExtractAssociatedIcon($IconObject.Path)
}
else {
# This uses newer .NET methods from 8+ and results in higher quality images
$IconExtract = [System.Drawing.Icon]::ExtractIcon($IconObject.Path, 0, 256)
}
# Delete any existing ICO file
#Remove-Item -Path "$($IconBitmapPath)" -Force -ErrorAction SilentlyContinue | Out-Null
# Save the Extracted icon to a bitmap file
$IconExtract.ToBitmap().Save("$($IconBitmapPath)")
# Create BitmapImage from the Bitmap file
$Bitmap = New-Object System.Windows.Media.Imaging.BitmapImage
$Bitmap.BeginInit()
$Bitmap.StreamSource = [System.IO.MemoryStream]::new([System.IO.File]::ReadAllBytes("$($IconBitmapPath)"))
$Bitmap.CacheOption = [System.Windows.Media.Imaging.BitmapCacheOption]::OnLoad
$Bitmap.EndInit()
$Bitmap.Freeze()
# Create a New Image Control in the Grid 'grid_Icon'
$ImageControlIcon = New-Object System.Windows.Controls.Image
$ImageControlIcon.SetValue([System.Windows.Controls.Control]::NameProperty, "img_Icon_$(([System.IO.Path]::GetFileNameWithoutExtension($IconObject.Name)) -replace '[^a-zA-Z0-9]', '_')")
$ImageControlIcon.Source = $Bitmap
$ImageControlIcon.SetValue([System.Windows.Controls.Grid]::RowProperty, 0)
$ToolTip = New-Object System.Windows.Controls.ToolTip
if ($Script:ScriptPSVersion -lt [Version]"7.4") {
$ToolTip.Content = "Click to Export.`nUse PowerShell 7.4 or higher for better quality."
$ToolTip.Background = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.Colors]::LightGoldenrodYellow)
$ToolTip.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.Colors]::Red)
}
else { $ToolTip.Content = "Click to Export" }
$ImageControlIcon.ToolTip = $ToolTip
$ImageControlIcon.SetValue([System.Windows.Controls.ToolTipService]::InitialShowDelayProperty, 100)
$ImageControlIcon.HorizontalAlignment = "Center"
$ImageControlIcon.VerticalAlignment = "Center"
$ImageControlIcon.Visibility = "Collapsed"
$ImageControlIcon.add_mouseleftbuttonup($Button_ExportToPNG_Handler)
# Add the Image Control to the Grid 'grid_Icon'
$grid_Icon.Children.Add($ImageControlIcon)
# Create PSCustomObject for the Image Control Information
$ImageControlIconInfo = [PSCustomObject]@{
Index = $IconIndex
Name = $ImageControlIcon.Name
IconName = "$([System.IO.Path]::GetFileNameWithoutExtension($IconObject.Name))"
IconBinaryPath = "$($IconObject.Path)"
IconBitmapPath = "$($IconBitmapPath)"
Control = $ImageControlIcon
}
# Add to IconImageControlsList
$Script:IconImageControlsList.Add($ImageControlIconInfo)
}
# Increment the Icon Index
$IconIndex++
}
}
function Set-IconImageNavigation {
[CmdletBinding(DefaultParameterSetName = 'New')]
param (
[Parameter(Mandatory = $true, ParameterSetName = 'New')]
[Parameter(Mandatory = $true, ParameterSetName = 'Next')]
[Parameter(Mandatory = $true, ParameterSetName = 'Previous')]
[PSCustomObject[]]$IconObjects,
[Parameter(Mandatory = $false, ParameterSetName = 'Next')]
[switch]$Next,
[Parameter(Mandatory = $false, ParameterSetName = 'Previous')]
[switch]$Previous
)
# IconImageControlsList Count
$IconCount = $Script:IconImageControlsList.Count
# Get the Current Icon Index
if ($null -eq $Script:CurrentIconIndex) {
$Script:CurrentIconIndex = 0
}
$CurrentIndex = $Script:CurrentIconIndex
if ($Next) {
# Increment the Index
$NewIndex = $CurrentIndex + 1
}
elseif ($Previous) {
# Decrement the Index
$NewIndex = $CurrentIndex - 1
}
else {
# If neither Next nor Previous is specified, keep the current index
$NewIndex = $CurrentIndex
}
# Set GlobalIndex
$Script:CurrentIconIndex = $NewIndex
# Hide the 'No Icon' Label
$lbl_NoIcon.Visibility = "Collapsed"
# Hide the current Icon
($Script:IconImageControlsList | Where-Object { $_.Index -eq $CurrentIndex }).Control.Visibility = "Hidden"
# Show the new Icon
($Script:IconImageControlsList | Where-Object { $_.Index -eq $NewIndex }).Control.Visibility = "Visible"
# Set the Navigation button states
if ($IconCount -gt 1) {
if ($NewIndex -eq 0) {
$btn_IconPrevious.IsEnabled = $false
$btn_IconNext.IsEnabled = $true
}
elseif ($NewIndex -eq ($IconCount - 1)) {
$btn_IconPrevious.IsEnabled = $true
$btn_IconNext.IsEnabled = $false
}
else {
$btn_IconPrevious.IsEnabled = $true
$btn_IconNext.IsEnabled = $true
}
}
}
function Set-IconImage {
param (
[Parameter(Mandatory = $true)]
[PSCustomObject[]]$IconObjects
)
# Build the Icon Image Controls
Build-IconImageControls -IconObjects $IconObjects
# Set the Icon Image Navigation
Set-IconImageNavigation -IconObjects $Script:IconImageControlsList
}
function Clear-IconImageControls {
# Clear the Script Scoped Variables
Remove-Variable -Name IconImageControlsList -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name CurrentIconIndex -Scope Script -ErrorAction SilentlyContinue
# Reload all Icon Image Controls on the form into variables
$formMSIProperties.FindName("grid_Icon").Children | Where-Object { $_.Name -like "img_Icon*" } | ForEach-Object { Set-Variable -Name $_.Name -Value $_ -Scope Script }
# Get all Icon Image Controls that start with the name 'img_Icon' and Hide them
$IconImageControls = Get-Variable -Name "img_Icon*" -Scope Script -ErrorAction SilentlyContinue
if ($null -ne $IconImageControls) {
foreach ($IconImageControl in $IconImageControls) {
$IconImageControl.Value.Visibility = "Collapsed"
}
}
# Show the No Icon Label Visibility
$lbl_NoIcon.Visibility = "Visible"
# Disable the Icon Navigation buttons
$btn_IconPrevious.IsEnabled = $false
$btn_IconNext.IsEnabled = $false
}
function Invoke-FormReset {
# Clear the Icon Image Controls
Clear-IconImageControls
# Clear the Textboxes
Clear-Textboxes
# Clear and Reset the listbox font style
$lsbox_FilePath.Items.Clear()
$lsbox_FilePath.ClearValue([System.Windows.Controls.Control]::BackgroundProperty)
$lsbox_FilePath.ClearValue([System.Windows.Controls.Control]::ForegroundProperty)
$lsbox_FilePath.ClearValue([System.Windows.Controls.Control]::FontWeightProperty)
$lsbox_FilePath.ClearValue([System.Windows.Controls.Control]::FontSizeProperty)
# Forget the loaded MSI so a relaunch (update) doesn't pass a stale path
$Script:LoadedMSIPath = $null
$Script:RetryFilePath = $null
$btn_FilePath_Retry.Visibility = [System.Windows.Visibility]::Collapsed
# Disable all buttons
Disable-AllButtons
}
function Invoke-GetMSIInformation {
param (
[Parameter(Mandatory = $true)]
[IO.FileInfo[]]$MSIPath
)
# Reset the form
Invoke-FormReset
# Get the File Hash Information for any file type
$HashInfo = Get-FileHashInformation -Path $MSIPath
# Check if the file is an MSI file
$IsMSI = ([System.IO.Path]::GetExtension($MSIPath[0])) -eq ".msi"
if ($IsMSI) {
# Get the MSI file properties
$FileMSIInfo = Get-MsiProperties -Path $MSIPath
# Populate the textboxes with the MSI properties and hash information
Set-TextboxInformation -MSIPropertiesInfo $FileMSIInfo -FileHashInfo $HashInfo
# Extract and display the Icons
$IconObjects = Get-MsiIcon -Path $MSIPath
if ($null -ne $IconObjects) {
Set-IconImage -IconObjects $IconObjects
}
# Enable the Copy buttons
Enable-AllButtons -Exclude "Icon"
}
else {
# Populate only the hash textboxes for non-MSI files
Set-TextboxInformation -FileHashInfo $HashInfo
# Enable only the Hash and FilePath Copy buttons
foreach ($ButtonName in @("btn_MD5_Copy", "btn_SHA1_Copy", "btn_SHA256_Copy", "btn_Digest_Copy", "btn_D256_Copy", "btn_FilePath_Copy")) {
(Get-Variable -Name $ButtonName -ValueOnly -ErrorAction SilentlyContinue).IsEnabled = $true
}
}
# Clear the listbox and add the filename
$lsbox_FilePath.Items.Clear()
$lsbox_FilePath.Items.Add($MSIPath[0])
# Remember the loaded file so an update relaunch can reopen it
$Script:LoadedMSIPath = $MSIPath[0].FullName
$Script:RetryFilePath = $MSIPath[0].FullName
$btn_FilePath_Retry.Visibility = [System.Windows.Visibility]::Visible
# Remove lock on current file
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}
function Invoke-LaunchAsPwsh {
# Are we running as PowerShell 7.4 or higher
if ($Script:ScriptPSVersion -ge [Version]"7.4") {
Write-Host "Running PowerShell 7.4 or higher"
}
else {
# Check that PowerShell is installed
if ($Script:PowerShellPath) {
# Check that it is version 7.4 or higher
if ($Script:PowerShellPath.Version -lt [Version]"7.4") {
Write-Host "Installed PowerShell (pwsh.exe) version [$($Script:PowerShellPath.Version)] is lower than 7.4. Continuing with existing PowerShell version [$($Script:ScriptPSVersion)]..."
}
else {
# Start PowerShell and run the script
Write-Host "Relaunching script in PowerShell (pwsh.exe)..."
if ($PSCommandPath -ne "") {
Start-Process -FilePath $Script:PowerShellPath -ArgumentList "-NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -File `"$PSCommandPath`" -FilePath `"$FilePath`""
}
else {
Start-Process -FilePath $Script:PowerShellPath -ArgumentList "-NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -Command `"Invoke-Expression (Invoke-RestMethod 'https://raw.githubusercontent.com/MichaelEscamilla/GetMSIInformation/main/GetMSIInformation.ps1')`""
}
Exit
}
}
else {
Write-Host "PowerShell (pwsh.exe) is not installed on this system."
}
}
}
# Authored as a scriptblock because it's passed to a runspace).
$Script:TestForUpdate = {
param(
[string]$ApiUrl,
[hashtable]$Headers,
[System.Version]$CurrentVersion
)
$release = Invoke-RestMethod -Uri $ApiUrl -Headers $Headers -TimeoutSec 10 -ErrorAction Stop
$latestVersion = [System.Version]($release.tag_name -replace '^v', '')
[PSCustomObject]@{
UpdateAvailable = ($latestVersion -gt $CurrentVersion)
LatestVersion = $latestVersion
HtmlUrl = $release.html_url
Tag = $release.tag_name
}
}
function Start-BackgroundUpdateCheck {
[CmdletBinding()]
param([switch]$Manual)
# Don't start a second check while one is still running (e.g. manual click during the startup check).
if ($Script:UpdateHandle -and -not $Script:UpdateHandle.IsCompleted) { return }
$Script:UpdateCheckManual = $Manual.IsPresent
# Detect the distribution channel once so the update click is a cheap lookup.
if (-not $Script:UpdateChannel) { $Script:UpdateChannel = Get-UpdateChannel }
try {
$Script:UpdatePowerShell = [powershell]::Create()
$Script:UpdatePowerShell.AddScript($Script:TestForUpdate).
AddArgument($Script:ReleasesApiUrl).
AddArgument($Script:UpdateCheckHeaders).
AddArgument($Script:ScriptVersion) | Out-Null
$Script:UpdateHandle = $Script:UpdatePowerShell.BeginInvoke()
$Script:UpdateTimer = New-Object System.Windows.Threading.DispatcherTimer
$Script:UpdateTimer.Interval = [TimeSpan]::FromMilliseconds(500)
$Script:UpdateTimer.Add_Tick({
if (-not $Script:UpdateHandle.IsCompleted) { return }
$Script:UpdateTimer.Stop()
try {
$result = $Script:UpdatePowerShell.EndInvoke($Script:UpdateHandle) | Select-Object -First 1
Write-Host "Background update check: UpdateAvailable=$($result.UpdateAvailable) | Installed [$($Script:ScriptVersion)] | Latest [$($result.LatestVersion)]"
Show-UpdateAvailable -Result $result
# Manual check only: confirm when already current (an available update is surfaced by the menu item).
if ($Script:UpdateCheckManual -and -not $result.UpdateAvailable) {
Set-StatusMessage -Message "You're running the latest version ($($Script:ScriptVersion))." -Type Success
}
}
catch {
Write-Host "Background update check failed: $($_.Exception.Message)"
if ($Script:UpdateCheckManual) {
[System.Windows.MessageBox]::Show("Update check failed:`n$($_.Exception.Message)", "Check for Updates", 'OK', 'Warning') | Out-Null
}
}
finally {
$Script:UpdatePowerShell.Dispose()
}
})
$Script:UpdateTimer.Start()
}
catch {
Write-Host "Unable to start background update check: $($_.Exception.Message)"
}
}
function Show-UpdateAvailable {
# Reveals the 'Update Available' menu item when a newer release exists, so both the background and
# manual checks surface the result the same way. Remembers the download link for the click handler.
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
$Result
)
if (-not $Result.UpdateAvailable) { return }
$Script:LatestReleaseUrl = if ($Result.HtmlUrl) { $Result.HtmlUrl } else { $Script:ReleasesPageUrl }
$Script:LatestReleaseTag = $Result.Tag
$MenuItem_UpdateAvailable.Header = "Update Available: $($Result.LatestVersion)"
$MenuItem_UpdateAvailable.Visibility = [System.Windows.Visibility]::Visible
}
function Set-StatusMessage {
# Flashes a temporary message in the status bar, then restores the previous text after a few seconds.
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Message,
[ValidateSet('Success', 'Danger', 'Accent', 'Muted')]
[string]$Type = 'Success'
)
if (-not $txtblk_StatusBar) { return }
$brushKey = switch ($Type) {
'Success' { 'Success' }
'Danger' { 'Danger' }
'Accent' { 'Accent' }
default { 'TextMuted' }
}
# Snapshot the resting state so the revert restores whatever was there before, not a hardcoded string.
# Only capture when the bar is at rest; otherwise a rapid second flash would capture the first transient.
if (-not ($Script:StatusTimer -and $Script:StatusTimer.IsEnabled)) {
$Script:StatusRestoreText = $txtblk_StatusBar.Text
$Script:StatusRestoreBrush = $txtblk_StatusBar.Foreground
$Script:StatusRestoreWeight = $txtblk_StatusBar.FontWeight
}
$txtblk_StatusBar.Text = $Message
$txtblk_StatusBar.Foreground = $formMSIProperties.FindResource($brushKey)
$txtblk_StatusBar.FontWeight = [System.Windows.FontWeights]::SemiBold
# Reuse a single timer so rapid clicks don't stack revert callbacks.
if (-not $Script:StatusTimer) {
$Script:StatusTimer = New-Object System.Windows.Threading.DispatcherTimer
$Script:StatusTimer.Interval = [TimeSpan]::FromSeconds(4)
# Stop via $sender and read the restore state off the timer's Tag: script-scoped
# variables aren't reliably visible inside the Tick callback, so keep it self-contained.
$Script:StatusTimer.Add_Tick({
param($timer, $e)
$timer.Stop()
$restore = $timer.Tag
if ($restore) {
$restore.Bar.Text = $restore.Text
$restore.Bar.Foreground = $restore.Brush
$restore.Bar.FontWeight = $restore.Weight
}
})
}
$Script:StatusTimer.Stop()
$Script:StatusTimer.Tag = [pscustomobject]@{
Bar = $txtblk_StatusBar
Text = $Script:StatusRestoreText
Brush = $Script:StatusRestoreBrush
Weight = $Script:StatusRestoreWeight
}
$Script:StatusTimer.Start()
}
function Get-WindowBitmap {
# Renders the window's visual tree to a bitmap at the current DPI so the capture is crisp on scaled displays.
[CmdletBinding()]
param()
$source = [System.Windows.PresentationSource]::FromVisual($formMSIProperties)
$scaleX = if ($source) { $source.CompositionTarget.TransformToDevice.M11 } else { 1 }
$scaleY = if ($source) { $source.CompositionTarget.TransformToDevice.M22 } else { 1 }
$pixelWidth = [int][Math]::Ceiling($formMSIProperties.ActualWidth * $scaleX)
$pixelHeight = [int][Math]::Ceiling($formMSIProperties.ActualHeight * $scaleY)
# Suppress hit-testing during the render so a control under the cursor (e.g. a textbox left
# hovered after the menu closes) doesn't capture its IsMouseOver accent state.
$formMSIProperties.IsHitTestVisible = $false
[System.Windows.Input.Mouse]::Synchronize()
$formMSIProperties.UpdateLayout()
try {
$rtb = New-Object System.Windows.Media.Imaging.RenderTargetBitmap($pixelWidth, $pixelHeight, (96 * $scaleX), (96 * $scaleY), [System.Windows.Media.PixelFormats]::Pbgra32)
$rtb.Render($formMSIProperties)
}
finally {
$formMSIProperties.IsHitTestVisible = $true
[System.Windows.Input.Mouse]::Synchronize()
}
return $rtb
}
function Get-UpdateChannel {
# Detects how the script was launched so the update action can match the channel.
# Cached once in $Script:UpdateChannel; ordering matters because RightClick and LooseFile
# both have a non-empty $PSCommandPath.
[CmdletBinding()]
param()
# Web: launched via iex (irm ...); nothing on disk to update, next launch is always latest.
if ([string]::IsNullOrEmpty($PSCommandPath)) {
$channel = 'Web'
}
else {
$scriptFolder = Split-Path -Path $PSCommandPath -Parent
# Right-click install: running from the LOCALAPPDATA copy.
if ($scriptFolder -eq $Script:RightClickMenuFolderPath) {
$channel = 'RightClick'
}
else {
# PowerShell Gallery install: Install-Script location matches where we're running from.
$installed = Get-InstalledScript -Name 'GetMSIInformation' -ErrorAction SilentlyContinue
if ($installed -and $installed.InstalledLocation -eq $scriptFolder) {
$channel = 'PSGallery'
}
else {
# Anything else: a loose .ps1 on disk.
$channel = 'LooseFile'
}
}
}
Write-Host "Update channel: [$channel]"
return $channel
}
function Open-ReleasePage {
# Universal fallback for the update action: open the newer release (or the releases list).
if ($Script:LatestReleaseUrl) { Start-Process $Script:LatestReleaseUrl }
else { Start-Process $Script:ReleasesPageUrl }
}
function Update-ScriptFile {
# Overwrites the target .ps1 with the latest release. Returns $false so the caller can fall
# back to the releases page when the file can't be replaced in place (locked, read-only, offline).
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ScriptPath,
[Parameter(Mandatory = $true)]
[string]$Tag
)
# Can't overwrite a file that's locked (e.g. open in an editor).
if (Test-FileLock -Path $ScriptPath) {
Write-Host "Update aborted: script file is locked [$ScriptPath]"
return $false
}
$downloadUrl = "https://raw.githubusercontent.com/$($Script:GitHubRepo)/$Tag/$($Script:ScriptName)"
$tempFile = Join-Path -Path $env:TEMP -ChildPath 'GetMSIInformation.update.ps1'
try {
Write-Host "Downloading update: [$downloadUrl]"
Invoke-WebRequest -Uri $downloadUrl -OutFile $tempFile -UseBasicParsing -ErrorAction Stop
if (-not (Test-Path $tempFile) -or (Get-Item $tempFile).Length -eq 0) {
throw 'Downloaded file is empty.'
}
Write-Host "Replacing script: [$ScriptPath]"
Copy-Item -Path $tempFile -Destination $ScriptPath -Force -ErrorAction Stop
return $true
}
catch {
Write-Host "Update failed: $($_.Exception.Message)"
return $false
}
finally {
Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue
}
}
function Restart-Script {
# Relaunches the on-disk script so an applied update takes effect, then closes this instance.
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ScriptPath,
[string]$FilePath
)
# Prefer pwsh 7.4+ (matches the startup relaunch); fall back to Windows PowerShell.
if ($Script:PowerShellPath -and $Script:PowerShellPath.Version -ge [Version]"7.4") {
$CommandExe = $Script:PowerShellPath.Path
}
else {
$CommandExe = "C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe"
}
$argList = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -File `"$ScriptPath`""
if (-not [string]::IsNullOrEmpty($FilePath)) {
$argList += " -FilePath `"$FilePath`""
}
Write-Host "Relaunching updated script: [$ScriptPath]"
Start-Process -FilePath $CommandExe -ArgumentList $argList
$formMSIProperties.Close()
}
function Install-RightClickMenu {
# Installs or refreshes the LOCALAPPDATA copy, icon, and .msi right-click registry entry.
# Shared by the Install menu item and the RightClick update path.
[CmdletBinding()]
param()
# Set Script Name
$SaveAsScriptName = $ScriptName
# Create a new directory in the LOCALAPPDATA folder
Write-Host "Creating Folder: [$($Script:RightClickMenuFolderPath)]"
$DestinationFolderPath = "$($Script:RightClickMenuFolderPath)"
if (-not (Test-Path $DestinationFolderPath)) {
$DestinationFolder = New-Item -ItemType Directory -Path $DestinationFolderPath -ErrorAction SilentlyContinue
}
else {
$DestinationFolder = Get-Item -Path $DestinationFolderPath
}
# Create an ico file from $Script:WindowIconBase64
$IconFilePath = "$($DestinationFolder.FullName)\GetMSIInformation.ico"
# Delete existing Icon file if it exists
Remove-Item $IconFilePath -Force -ErrorAction SilentlyContinue | Out-Null
Write-Host "Creating Icon file: [$IconFilePath]"
$IconByteArray = [System.Convert]::FromBase64String($Script:WindowIconBase64)
[System.IO.File]::WriteAllBytes($IconFilePath, $IconByteArray)
# Check if the script is being Invoked from the Internet
if ($PSCommandPath -ne "") {
# Copy the script to the new directory
Write-Host "Creating Script: [$($DestinationFolder.FullName)\$($SaveAsScriptName)]"
Copy-Item "$PSScriptRoot\$([System.IO.Path]::GetFileName($PSCommandPath))" -Destination "$($DestinationFolder.FullName)\$($SaveAsScriptName)" -ErrorAction SilentlyContinue
}
else {
Write-Host "PSCommandPath is not available."
# Script URL
$ScriptURL = "https://raw.githubusercontent.com/MichaelEscamilla/GetMSIInformation/main/GetMSIInformation.ps1"
Write-Host "Downloading script: [$ScriptURL]"
try {
Invoke-WebRequest -Uri $ScriptURL -OutFile "$($DestinationFolder.FullName)\$($SaveAsScriptName)" -ErrorAction Stop
Write-Host "Script saved: [$($DestinationFolder.FullName)\$($SaveAsScriptName)]"
}
catch {
Write-Host "Failed to download the script: $_"
}
}
# Remove the legacy .msi-only entry so upgraders don't keep a stale duplicate on MSI files.
if ((Test-Path -LiteralPath "HKCU:\Software\Classes\SystemFileAssociations\.msi\shell\$RightClickMenuName") -eq $true) {
Remove-Item "HKCU:\Software\Classes\SystemFileAssociations\.msi\shell\$RightClickMenuName" -Force -Recurse -ErrorAction SilentlyContinue
}
# Prefer pwsh 7.4+ so the menu launches directly and skips the slow relaunch.
# Fall back to Windows PowerShell (always present) when pwsh isn't installed.
if ($Script:PowerShellPath -and $Script:PowerShellPath.Version -ge [Version]"7.4") {
$CommandExe = $Script:PowerShellPath.Path
}
else {
$CommandExe = "C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe"
}
# Pick the icon: prefer the extracted .ico, then pwsh, then Windows PowerShell.
if (Test-Path -LiteralPath $IconFilePath) {
$IconValue = $IconFilePath
}
elseif ($Script:PowerShellPath) {
$IconValue = $Script:PowerShellPath.Path
}
else {
$IconValue = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
}
# Register under the '*' key so the entry appears on the right-click menu of every file type.
# '*' is a literal registry key but a wildcard to the PowerShell registry provider, so use the
# .NET registry API to create/set it reliably.
# TODO: Revisit the menu label - "Get MSI Information" now shows on all file types, not just .msi.
$MenuKey = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey("Software\Classes\*\shell\$RightClickMenuName")
$MenuKey.SetValue('', $RightClickMenuName)
$MenuKey.SetValue('icon', $IconValue)
$CommandKey = $MenuKey.CreateSubKey('command')
$CommandKey.SetValue('', "`"$CommandExe`" -NoProfile -ExecutionPolicy Bypass -WindowStyle Minimized -Command `"$($DestinationFolder.FullName)\$($SaveAsScriptName)`" -FilePath '%1'")
$CommandKey.Close()
$MenuKey.Close()
Write-Host "Registry Modified: [HKCU:\Software\Classes\*\shell\$($RightClickMenuName)]"
Write-Host "Installation Complete"
}
#endregion Functions
#############################################
################# Main Script ################
#############################################
# Relaunch the script in PowerShell 7.4 or higher if available.
Invoke-LaunchAsPwsh
# Load Assemblies
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName System.Windows.Forms
# Build the GUI
[xml]$XAMLformMSIProperties = @"
"@
# Create a new XML node reader for reading the XAML content
$readerformMSIProperties = New-Object System.Xml.XmlNodeReader $XAMLformMSIProperties
# Load the XAML content into a WPF window object using the XAML reader
[System.Windows.Window]$formMSIProperties = [Windows.Markup.XamlReader]::Load($readerformMSIProperties)
# Create Variables for all the controls in the XAML form
$XAMLformMSIProperties.SelectNodes("//*[@Name]") | ForEach-Object { Set-Variable -Name ($_.Name) -Value $formMSIProperties.FindName($_.Name) -Scope Script }
# Disable all action buttons until a file is loaded
Disable-AllButtons
#############################################
############## Event Handlers ###############
#############################################
#region Event Handlers
#### Form Load #####
$formMSIProperties.Add_Loaded({
try {
# Covert the AppIcon byte array to an Icon and set as the form icon
$WindowIconBitmap = [System.Windows.Media.Imaging.BitmapImage]::new()
$WindowIconBitmap.BeginInit()
$WindowIconBitmap.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($Script:WindowIconBase64)
$WindowIconBitmap.EndInit()
$WindowIconBitmap.Freeze()
$formMSIProperties.Icon = $WindowIconBitmap
}
catch {
# Write the error to the host but continue on. It's an icon, who cares.
Write-Host "Error setting form icon: $_"
}
# Check if the script is running as an administrator
if (($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {
Write-Warning "The script is running as an administrator."
Write-Warning "Drag and Drog will not work while running as an administrator."
# Clear the listbox
$lsbox_FilePath.Items.Clear()
# Add a warning message to the listbox
$lsbox_FilePath.Items.Add("WARNING: Running as Administrator | Drag and Drop will not work.")
# Make the warning message bold and yellow
$lsbox_FilePath.Background = [System.Windows.Media.Brushes]::Yellow
$lsbox_FilePath.Foreground = [System.Windows.Media.Brushes]::Black
$lsbox_FilePath.FontWeight = 'Bold'
}
# Update Version Information
$formMSIProperties.Title = "MSI Properties - Version $($ScriptVersion)"
$MenuItem_Version.Header = "Version $($ScriptVersion)"
$txtblk_TitleVersion.Text = " $($ScriptVersion)"
# Defer the update check until the window has rendered and come to the foreground.
$formMSIProperties.Dispatcher.InvokeAsync({
Start-BackgroundUpdateCheck
}, [System.Windows.Threading.DispatcherPriority]::ApplicationIdle) | Out-Null
# Check if the FilePath parameter is provided to script
if ($FilePath) {
# Check if $FilePath is locked
if (Test-FileLock -Path $FilePath) {
Write-Warning "The file is locked: [$FilePath]"
Show-FileLockedMessage -Path $FilePath
}
else {
Write-Host "FilePath passed: [$FilePath]"
# Show the UI
$lsbox_FilePath.Items.Clear()
$lsbox_FilePath.Items.Add("Loading: [$FilePath]")
# Process the File
$formMSIProperties.Dispatcher.InvokeAsync({
Invoke-GetMSIInformation -MSIPath $FilePath
}, [System.Windows.Threading.DispatcherPriority]::Background) | Out-Null
}
}
})
#### Listbox Drag and Drop ####
$lsbox_FilePath.Add_Drop({
$Script:DroppedFilePath = $_.Data.GetData([Windows.Forms.DataFormats]::FileDrop)
Write-Host "File Dropped: [$Script:DroppedFilePath]"
if ($Script:DroppedFilePath) {
# Reset the form
Invoke-FormReset
# Check if the dropped file is locked
if (Test-FileLock -Path "$($Script:DroppedFilePath)") {
Write-Warning "The file is locked: [$Script:DroppedFilePath]"
Show-FileLockedMessage -Path "$($Script:DroppedFilePath)"
}
else {
# Reset listbox
$lsbox_FilePath.Items.Clear()
$lsbox_FilePath.Items.Add("Loading: [$Script:DroppedFilePath]")
# Process the File
$formMSIProperties.Dispatcher.InvokeAsync({
Invoke-GetMSIInformation -MSIPath $Script:DroppedFilePath
}, [System.Windows.Threading.DispatcherPriority]::Background) | Out-Null
}
}
})
$btn_FilePath_Retry.add_Click({
$retryPath = $Script:RetryFilePath
if ([string]::IsNullOrWhiteSpace($retryPath)) {
return
}
if (Test-FileLock -Path $retryPath) {
Show-FileLockedMessage -Path $retryPath
Set-StatusMessage -Message 'File is still locked. Close the application using it, then retry.' -Type Danger
return
}
$btn_FilePath_Retry.Visibility = [System.Windows.Visibility]::Collapsed
$lsbox_FilePath.Items.Clear()
$lsbox_FilePath.Items.Add("Loading: [$retryPath]")
$formMSIProperties.Dispatcher.InvokeAsync({
Invoke-GetMSIInformation -MSIPath $Script:RetryFilePath
}, [System.Windows.Threading.DispatcherPriority]::Background) | Out-Null
})
$lsbox_FilePath.Add_DragOver({
# Check if the dragged data contains file drop data
if ($_.Data.GetDataPresent([Windows.Forms.DataFormats]::FileDrop)) {
# Allow any file type to be dropped
$_.Effects = [System.Windows.DragDropEffects]::Copy
$_.Handled = $true
}
})
$btn_IconNext.add_Click({
Set-IconImageNavigation -IconObjects $Script:IconImageControlsList -Next
})
$btn_IconPrevious.add_Click({
Set-IconImageNavigation -IconObjects $Script:IconImageControlsList -Previous
})
#### Menu Items ####
$MenuItem_Open.add_Click({
# Open the Icon Temp Folder
if (-not (Test-Path $Script:IconTempFolderPath)) {
New-Item -ItemType Directory -Path $Script:IconTempFolderPath -ErrorAction SilentlyContinue
}
Invoke-Item -Path $Script:IconTempFolderPath
})
$MenuItem_ScreenshotCopy.add_Click({
try {
$bitmap = Get-WindowBitmap
[System.Windows.Clipboard]::SetImage($bitmap)
Set-StatusMessage -Message "Screenshot copied to clipboard." -Type Success
}
catch {
Write-Warning "Failed to copy screenshot: $_"
Set-StatusMessage -Message "Failed to copy screenshot." -Type Danger
}
})
$MenuItem_ScreenshotSave.add_Click({
try {
$dialog = New-Object System.Windows.Forms.SaveFileDialog
$dialog.Filter = "PNG Image (*.png)|*.png"
$dialog.Title = "Save Screenshot"
$dialog.FileName = "MSIProperties_$(Get-Date -Format 'yyyyMMdd_HHmmss').png"
if ($dialog.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { return }
$bitmap = Get-WindowBitmap
$encoder = New-Object System.Windows.Media.Imaging.PngBitmapEncoder
$encoder.Frames.Add([System.Windows.Media.Imaging.BitmapFrame]::Create($bitmap))
$stream = [System.IO.File]::Create($dialog.FileName)
try { $encoder.Save($stream) } finally { $stream.Dispose() }
Write-Host "Screenshot saved: [$($dialog.FileName)]"
Set-StatusMessage -Message "Screenshot saved." -Type Success
}
catch {
Write-Warning "Failed to save screenshot: $_"
Set-StatusMessage -Message "Failed to save screenshot." -Type Danger
}
})
$MenuItem_Install.add_Click({
Write-Host "Menu Item Install Clicked"
Install-RightClickMenu
Set-StatusMessage -Message "Right-click menu installed." -Type Success
})
$MenuItem_Uninstall.add_Click({
Write-Host "Menu Item Uninstall Clicked"
# Remove the script folder from the LOCALAPPDATA folder
Remove-item "$env:LOCALAPPDATA\GetMSIInformation" -Force -Recurse -ErrorAction SilentlyContinue
Write-Host "Deleted Folder: [$env:LOCALAPPDATA\GetMSIInformation]"
# Remove the all-files '*' entry. '*' is a wildcard to the PowerShell registry provider,
# so use the .NET registry API to delete the key tree reliably.
[Microsoft.Win32.Registry]::CurrentUser.DeleteSubKeyTree("Software\Classes\*\shell\$RightClickMenuName", $false)
Write-Host "Deleted Registry: [HKCU:\Software\Classes\*\shell\$($RightClickMenuName)]"
# Clean up the legacy .msi-only entry if an older version left one behind.
if ((Test-Path -LiteralPath "HKCU:\Software\Classes\SystemFileAssociations\.msi\shell\$RightClickMenuName") -eq $true) {
Remove-Item "HKCU:\Software\Classes\SystemFileAssociations\.msi\shell\$RightClickMenuName" -Force -Recurse -ErrorAction SilentlyContinue
}
Write-Host "Uninstallation Complete"
Set-StatusMessage -Message "Right-click menu removed." -Type Danger
})
$MenuItem_Open_RCM.add_Click({
# Open the Right Click Menu Folder
$RightClickMenuFolderPath = "$env:LOCALAPPDATA\GetMSIInformation"
if (-not (Test-Path $RightClickMenuFolderPath)) {
New-Item -ItemType Directory -Path $RightClickMenuFolderPath -ErrorAction SilentlyContinue
}
Invoke-Item -Path $RightClickMenuFolderPath
})
$MenuItem_GitHub.add_Click({
# Open Github Project Page
Start-Process "https://github.com/MichaelEscamilla/GetMSIInformation"
})
$MenuItem_About.add_Click({
# Open Blog
Start-Process "https://michaeltheadmin.com"
})
$MenuItem_CheckForUpdates.add_Click({
Write-Host "Checking for updates: [$($Script:ReleasesApiUrl)]"
Start-BackgroundUpdateCheck -Manual
})
$MenuItem_UpdateAvailable.add_Click({
# Currently loaded MSI (if any) so the relaunched instance reopens it.
$currentFile = $Script:LoadedMSIPath
switch ($Script:UpdateChannel) {
'PSGallery' {
# Let the Gallery replace the installed copy, then relaunch from its location.
try {
Write-Host "Updating via PowerShell Gallery: [Update-Script GetMSIInformation -Force]"
Update-Script -Name 'GetMSIInformation' -Force -ErrorAction Stop
$installed = Get-InstalledScript -Name 'GetMSIInformation' -ErrorAction SilentlyContinue
$updatedPath = if ($installed) { Join-Path $installed.InstalledLocation $Script:ScriptName } else { $PSCommandPath }
Restart-Script -ScriptPath $updatedPath -FilePath $currentFile
}
catch {
Write-Host "PSGallery update failed: $($_.Exception.Message)"
Open-ReleasePage
}
}
'RightClick' {
# The LOCALAPPDATA copy is the running file: refresh it, re-apply the registry entry, relaunch.
if ($Script:LatestReleaseTag -and (Update-ScriptFile -ScriptPath $PSCommandPath -Tag $Script:LatestReleaseTag)) {
Install-RightClickMenu
Restart-Script -ScriptPath $PSCommandPath -FilePath $currentFile
}
else {
Open-ReleasePage
}
}
'LooseFile' {
# Replace the launched .ps1 in place, then relaunch; fall back to the releases page on failure.
if ($Script:LatestReleaseTag -and (Update-ScriptFile -ScriptPath $PSCommandPath -Tag $Script:LatestReleaseTag)) {
Restart-Script -ScriptPath $PSCommandPath -FilePath $currentFile
}
else {
Open-ReleasePage
}
}
default {
# Web (and any failure): open the releases page.
Open-ReleasePage
}
}
})
#### Title Bar Handlers ####
$titlebar.add_MouseLeftButtonDown({
try { $formMSIProperties.DragMove() } catch { }
})
$titlebar_Minimize.add_Click({
$formMSIProperties.WindowState = [System.Windows.WindowState]::Minimized
})
$titlebar_Close.add_Click({
$formMSIProperties.Close()
})
#### Button Handlers ####
$btn_AllProperties.add_Click({
$SelectedProperty = Get-MsiProperties -Path $lsbox_FilePath.Items[0] | Out-GridView -Title "MSI Database Properties for $($lsbox_FilePath.Items[0])" -OutputMode Single
$SelectedProperty.Value | Set-Clipboard
})
#### Label Handlers ####
$lbl_UpgradeCode.add_MouseLeftButtonUp({
# Only react when an Upgrade Code is loaded (label is in its clickable state)
if ($txt_UpgradeCode.Text) {
Show-UpgradeCodeDetails
}
})
$Button_Copy_Handler = {
# Get the button name
$ButtonName = $_.Source.Name
# Get the property name from the button name by parsing between the underscores
$PropertyName = [regex]::Match($ButtonName, "_(.*?)_").Groups[1].Value
# Get the variable for the textbox with the same name as the property name
$TextboxVariable = Get-Variable -Name "txt_$($propertyName)" -ValueOnly -ErrorAction SilentlyContinue
if ($TextboxVariable) {
Write-Host "Textbox [$($TextboxVariable.Name)] Value Copied to Clipboard : [$($TextboxVariable.Text)]"
# Copy the text from the textbox with the same name as the property name
[System.Windows.Forms.Clipboard]::SetText($TextboxVariable.Text)
}
else {
# Try getting a Listbox variable with the same name as the property name
$ListboxVariable = Get-Variable -Name "lsbox_$($propertyName)" -ValueOnly -ErrorAction SilentlyContinue
if ($ListboxVariable) {
# Check if the item in the listbox contains spaces
if ($lsbox_FilePath.Items[0] -match "\s") {
# Copy the item in the listbox to the clipboard with quotes
[System.Windows.Forms.Clipboard]::SetText("`"$($lsbox_FilePath.Items[0])`"")
Write-Host "Copied to Clipboard: [`"$($lsbox_FilePath.Items[0])`"]"
}
else {
# Copy the item in the listbox to the clipboard without quotes
[System.Windows.Forms.Clipboard]::SetText($lsbox_FilePath.Items[0])
Write-Host "Copied to Clipboard: [$($lsbox_FilePath.Items[0])]"
}
}
}
}
$Button_ExportToPNG_Handler = {
# Get the Icon Control
$CurrentIconControl = ($Script:IconImageControlsList | Where-Object { $_.Index -eq $Script:CurrentIconIndex })
# Create a SaveFileDialog to get the export path
$SaveFileDialog = New-Object Microsoft.Win32.SaveFileDialog
$SaveFileDialog.Filter = "PNG Image (*.png)|*.png|ICO File (*.ico)|*.ico|All Files (*.*)|*.*"
$SaveFileDialog.Title = "Export Icon"
$SaveFileDialog.FileName = "$($CurrentIconControl.IconName)"
if ($SaveFileDialog.ShowDialog()) {
$SourcePath = $CurrentIconControl.IconBitmapPath
Copy-Item -Path $SourcePath -Destination $SaveFileDialog.FileName -Force
Write-Host "Icon exported to: $($SaveFileDialog.FileName)"
}
}
# Get all button variables that contain the word "Copy"
$Buttons = Get-Variable -Name "*Copy" -ValueOnly -ErrorAction SilentlyContinue
foreach ($Button in $Buttons) {
# Add a click event handler to the button
$Button.add_Click($Button_Copy_Handler)
}
#endregion Event Handlers
# Set the PowerShell Window Title
$Host.UI.RawUI.WindowTitle = "MSI Properties"
#Show the WPF Window
$formMSIProperties.Add_ContentRendered({
$formMSIProperties.Activate()
# Toggling Topmost raises the z-order without leaving the window pinned on top.
$formMSIProperties.Topmost = $true
$formMSIProperties.Topmost = $false
$formMSIProperties.Focus() | Out-Null
})
$formMSIProperties.WindowStartupLocation = "CenterScreen"
$formMSIProperties.ShowDialog() | Out-Null