Skip to content

Instantly share code, notes, and snippets.

@rileyz
Last active April 9, 2021 12:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rileyz/a0af6b81c15d40d5075b to your computer and use it in GitHub Desktop.
Save rileyz/a0af6b81c15d40d5075b to your computer and use it in GitHub Desktop.
<#
.SYNOPSIS
Menu driven options to assist with testing Windows Installer packages and writing the
read me text file.
.DESCRIPTION
Intended Use
This script is intended to be used as a quick method to test Windows Installer packagers
with the added option of creating the read me text file. The use of the script aids in
testing by removing the need to type the commands to install and uninstall repeatedly.
The creation on the read me text file with associated Product Codes, Upgrade Codes and
install/uninstall commands insure it is free from human error.
This script should be used with a shortcut which passes the parameters to the script. The
customised shortcut, which may include Transforms and additional Properties should be left
at the root of each package installer.
Example
The shortcut should be created with one of the following 'Targets' depending on use. Please
note that the '–ExecutionPolicy Bypass' must be inculded to allow scripts to run unrestricted.
The shortcut should not be set to 'Run as Administrator' in the advanced option.
* Using the below command to test a package.
powershell.exe –ExecutionPolicy Bypass -NoExit -Command ".\#Test-Package.ps1 -MSI 'Contoso.msi'"
* Using the below command to test a package with a transform.
powershell.exe –ExecutionPolicy Bypass -NoExit -Command ".\#Test-Package.ps1 -MSI 'Contoso.msi' -MSIProperties 'TRANSFORMS=Merge.mst ALLUSERS=1'"
* Using the below command to test a package with a transform and addtional properties.
powershell.exe –ExecutionPolicy Bypass -NoExit -Command ".\#Test-Package.ps1 -MSI 'Contoso.msi' -MSIProperties 'TRANSFORMS=Merge.mst ALLUSERS=1'"
About
Why did I created this script? Basically because I got sick and tired of repeatedly typing
the same commands over and over again in DOS and I was sick of creating read me's where I
had fat fingered a key which resulted in an error of some shorts. It was very easy to make
a mistake when typing the GUID or install/uninstall command for the read me, especially when
when you have a large number of apps - it all just becomes a blur.
I had been meaning to create this script for sometime, the last two years in fact and it
was only recently that I had free time.
The points I wanted to resolve were...
- Menu driven install and uninstall.
- Auto generates the read me file with required information.
- Scaleable.
The script has been written for easy interpretation and understanding, not for efficiency.
Known Defects/Bugs
* The Windows Installer exit code had to be obtained from the log file. This was due to
PowerShell session not having elevated privileges to read the object.
ie. $Process.ExitCode will not work.
* An incorrect Return Code will be returned if the user selects 'No' to the UAC prompt while
installing the MSI and there is an existing log file from previous testing.
* The ErrorActionPreference had to be set to suppress errors before launching the Windows Installer
process, this reversed after the process has been executed. The '-EA SilentlyContine' option for
Start-Process does not work for some reason.
* This bug will present itself when can't find the search string in the text file.
Exception calling "Substring" with "2" argument(s): "StartIndex cannot be less than zero.
Parameter name: startIndex"
At C:\Scripts\#Test-Package.ps1:67 char:46
+ $StringFromLog = $StringFromLog.substring <<<< ($StringFromLog.length - 5, 5)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException
* $EnvironmentalVariableRootForLogs... variables duplicated. We need to account for how PowerShell
and Command Prompt handle environmental variables differently. The preference is always to use a
environmental variable. So instead of the '$EnvironmentalVariableRootForLogs_PowerShell' variable
expanded and hard-coded to C:\Windows..., we will use %SystemRoot% in its place when writing the
read me text file, deal for SCCM.
Code Snippet Credits
* http://stackoverflow.com/questions/8743122/how-to-find-msi-product-version-number-using-powershell
Version History
1.0 03/12/2014
Initial release.
Copyright & Intellectual Property
Feel to copy, modify and redistribute, but please pay credit where it is due.
Feed back is welcome, please contact me on linkedin.
.LINK
Author:.......http://www.linkedin.com/in/rileylim
Source Code:..https://gist.github.com/rileyz/a0af6b81c15d40d5075b
.EXAMPLE
#Test-Package.ps1 -MSI 'Contoso.msi'
Using the above command to test a package.
.EXAMPLE
#Test-Package.ps1 -MSI 'Contoso.msi' -MSIProperties 'TRANSFORMS=Merge.mst'
Using the above command to test a package with a transform.
.EXAMPLE
#Test-Package.ps1 -MSI 'Contoso.msi' -MSIProperties 'TRANSFORMS=Merge.mst ALLUSERS=1'
Using the above command to test a package with a transform and addtional properties.
#>
Param ([Parameter(Mandatory=$True)][IO.FileInfo]$MSI, [String]$MSIProperties)
# Function List ###################################################################################
Function DisplayMenuOptions {
Do {#Comment Clear-Host so you can debug.
Clear-Host
$CurrentExecutionPolicy = Get-ExecutionPolicy
Write-Host "Execution Policy in $CurrentExecutionPolicy mode for this console session."
Write-Host ' No System Modules have been imported.'
Write-Host ''
Write-Host ''
Write-Host " Running for product: $MSIProductName"
Write-Host " Windows Installer file: $MSI"
Write-Host ''
Write-Host ' [1] Install'
Write-Host ' [2] Uninstall'
Write-Host ' [T] Toggle Windows Installer UI Level'
Write-Host " UI Level Currently: $MSIUILevel"
Write-Host ' [W] Write Plain Text Information File'
Write-Host ' [X] Exit'
Write-Host ''
$FunctionOption = Read-Host ' What do you want to do?'
If ($FunctionOption -eq 1 -or ($FunctionOption -eq 2) -or ($FunctionOption -eq 'T') -or
($FunctionOption -eq 'W') -or($FunctionOption -eq 'X'))
{Return $FunctionOption}
}
Until ($FunctionOption -eq 1 -or ($FunctionOption -eq 2) -or ($FunctionOption -eq 3) -or
($FunctionOption -eq 4) -or ($FunctionOption -eq 'W') -or ($FunctionOption -eq 'X'))
}
Function ToggleWindowsInstallerUILevel {
Param ([String]$CurrentUILevel)
If ($CurrentUILevel -eq '/qb')
{$CurrentUILevel = '/qn'}
Else {$CurrentUILevel = '/qb'}
Return $CurrentUILevel
}
Function Get-ReturnCodeFromLog {
Param ([String]$Log)
$StringFromLog = Select-String -path $Log -pattern "success or error status" | Out-String
$StringFromLog = $StringFromLog -replace "`n|`r"
$StringFromLog = $StringFromLog.substring($StringFromLog.length - 5, 5)
$StringFromLog = $StringFromLog.trim(' ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz:.')
Return $StringFromLog
}
Function PressAnyKey {
Write-Host 'Press any key...'
$HOST.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") | Out-Null
$HOST.UI.RawUI.Flushinputbuffer()
}
Function Get-MsiProductName {
Param ([IO.FileInfo] $FilePath)
Try {$windowsInstaller = New-Object -com WindowsInstaller.Installer
$database = $windowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null,
$windowsInstaller, @($FilePath.FullName, 0))
$q = "SELECT Value FROM Property WHERE Property = 'ProductName'"
$View = $database.GetType().InvokeMember("OpenView", "InvokeMethod", $Null, $database, ($q))
$View.GetType().InvokeMember("Execute", "InvokeMethod", $Null, $View, $Null)
$record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $Null, $View, $Null)
$ProductName = $record.GetType().InvokeMember("StringData", "GetProperty", $Null, $record, 1)
Return $ProductName}
catch
{Throw "Failed to get MSI file version the error was: {0}." -f $_}
}
Function Get-MsiProductCode {
Param ([IO.FileInfo] $FilePath)
Try {$windowsInstaller = New-Object -com WindowsInstaller.Installer
$database = $windowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null,
$windowsInstaller, @($FilePath.FullName, 0))
$q = "SELECT Value FROM Property WHERE Property = 'ProductCode'"
$View = $database.GetType().InvokeMember("OpenView", "InvokeMethod", $Null, $database, ($q))
$View.GetType().InvokeMember("Execute", "InvokeMethod", $Null, $View, $Null)
$record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $Null, $View, $Null)
$ProductCode = $record.GetType().InvokeMember("StringData", "GetProperty", $Null, $record, 1)
Return $ProductCode}
catch
{Throw "Failed to get MSI file version the error was: {0}." -f $_}
}
Function Get-MsiProductVersion {
Param ([IO.FileInfo] $FilePath)
Try {$windowsInstaller = New-Object -com WindowsInstaller.Installer
$database = $windowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null,
$windowsInstaller, @($FilePath.FullName, 0))
$q = "SELECT Value FROM Property WHERE Property = 'ProductVersion'"
$View = $database.GetType().InvokeMember("OpenView", "InvokeMethod", $Null, $database, ($q))
$View.GetType().InvokeMember("Execute", "InvokeMethod", $Null, $View, $Null)
$record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $Null, $View, $Null)
$ProductVersion = $record.GetType().InvokeMember("StringData", "GetProperty", $Null, $record, 1)
Return $ProductVersion}
catch
{Throw "Failed to get MSI file version the error was: {0}." -f $_}
}
Function Get-MsiUpgradeCode {
Param ([IO.FileInfo] $FilePath)
Try {$windowsInstaller = New-Object -com WindowsInstaller.Installer
$database = $windowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null,
$windowsInstaller, @($FilePath.FullName, 0))
$q = "SELECT Value FROM Property WHERE Property = 'UpgradeCode'"
$View = $database.GetType().InvokeMember("OpenView", "InvokeMethod", $Null, $database, ($q))
$View.GetType().InvokeMember("Execute", "InvokeMethod", $Null, $View, $Null)
$record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $Null, $View, $Null)
$UpgradeCode = $record.GetType().InvokeMember("StringData", "GetProperty", $Null, $record, 1)
Return $UpgradeCode}
catch
{Throw "Failed to get MSI file version the error was: {0}." -f $_}
}
Function WaitForWindowsInstallerToFinish {
Param ([String]$IgnoreWindowsInstallerServicePID)
$ProcessID = $IgnoreWindowsInstallerServicePID
Do {
$RunningInstallers = Get-Process -Name msiexec | Where-Object -FilterScript {$_.Id -ne $ProcessID}
#Debug if (!$RunningInstallers) {Write-Host 'RunningInstallers variable is null'}
#Debug if ($RunningInstallers) {Write-Host 'RunningInstallers variable is NOT null'}
#Debug $RunningInstallers #This shows running msiexec processes.
#Debug Write-Host ' Waiting...'
Start-Sleep -Seconds 1
}
Until (!$RunningInstallers)
}
#<<< End Of Function List >>>
# Setting up housekeeping #########################################################################
#Updating Console Window Informaion.
$host.ui.RawUI.WindowTitle = "Powershell Assist For Testing Packages"
#Discovering launch/script location.
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
#Starting work and prepping variables.
$OrginalSessionErrorActionPreference = $ErrorActionPreference
$WindowsInstallerStatus = Get-Service -Name msiserver
$Date = (Get-Date -format dd\/MM\/yyyy | Out-String)
$MSI = $scriptPath + '\' + $MSI.name
$MSIProductName = (Get-MsiProductName $MSI | Out-String)
$MSIProductVersion = (Get-MsiProductVersion $MSI | Out-String)
$MSIProductCode = (Get-MsiProductCode $MSI | Out-String)
$MSIUpgradeCode = (Get-MsiUpgradeCode $MSI | Out-String)
$MSIUILevel = '/qb'
$MSILogLevel = '/l*vx'
$EnvironmentalVariableRootForLogs_PowerShell = "$env:systemroot\Logs\"
$EnvironmentalVariableRootForLogs_CommandPrompt = "%SystemRoot%\Logs\"
$InstallLog = $EnvironmentalVariableRootForLogs_PowerShell + ($MSI.Basename -replace " ","_") + "_Install.log"
$UninstallLog = $EnvironmentalVariableRootForLogs_PowerShell + ($MSI.Basename -replace " ","_") + "_Uninstall.log"
$ReadMeInstallLog = $EnvironmentalVariableRootForLogs_CommandPrompt + ($MSI.Basename -replace " ","_") + "_Install.log"
$ReadMeUninstallLog = $EnvironmentalVariableRootForLogs_CommandPrompt + ($MSI.Basename -replace " ","_") + "_Uninstall.log"
#Removing carriage returns from varibles.
$Date = $Date -replace "`n|`r"
$MSIProductName = $MSIProductName -replace "`n|`r"
$MSIProductVersion = $MSIProductVersion -replace "`n|`r"
$MSIProductCode = $MSIProductCode -replace "`n|`r"
$MSIUpgradeCode = $MSIUpgradeCode -replace "`n|`r"
#Start Windows Installer service.
If ($WindowsInstallerStatus.status -eq 'Stopped')
{Write-Host 'Starting Windows Installer Service to get Process ID'
Start-Process "$psHome\powershell.exe" -Verb Runas -ArgumentList '-command "Start-Service MsiServer"'
Do {Start-Sleep -Seconds 1
$WindowsInstallerStatus = Get-Service -Name msiserver}
Until ($WindowsInstallerStatus.status -eq 'Running')}
$WindowsInstallerService = Get-Process msiexec
#<<< End of Setting up housekeeping >>>
# Start of script work ############################################################################
$MenuOption = DisplayMenuOptions
Do {Switch($MenuOption)
{
1 {#Install routine.
$MSIArguments = $MSIProperties + ' ' + $MSIUILevel + ' ' + $MSILogLevel + ' ' + $InstallLog
Write-Host
Write-Host 'Status:'
Write-Host " Installing using arguments: $MSIArguments"
Write-Host " Toggling ErrorActionPreference from $OrginalSessionErrorActionPreference to" ($ErrorActionPreference = "SilentlyContinue")
$Process = Start-Process -FilePath "$env:systemroot\system32\msiexec.exe" -ArgumentList "/i `"$MSI`" $MSIArguments" -Wait -PassThru -Verb RunAs
WaitForWindowsInstallerToFinish $WindowsInstallerService.id
Write-Host " Windows Installer process has finished"
Write-Host " Toggling ErrorActionPreference from $ErrorActionPreference to" ($ErrorActionPreference = "Continue")
$ReturnCode = Get-ReturnCodeFromLog $InstallLog
Write-Host " Return Code: $ReturnCode"
PressAnyKey
$MenuOption = DisplayMenuOptions}
2 {#Uninstall routine.
$MSIArguments = $MSIUILevel + ' ' + $MSILogLevel + ' ' + $UninstallLog
Write-Host
Write-Host 'Status:'
Write-Host " Uninstalling using arguments: {GUID} $MSIArguments"
Write-Host " Toggling ErrorActionPreference from $OrginalSessionErrorActionPreference to" ($ErrorActionPreference = "SilentlyContinue")
$Process = Start-Process -FilePath "$env:systemroot\system32\msiexec.exe" -ArgumentList "/x $MSIProductCode $MSIArguments" -Wait -PassThru -Verb RunAs
WaitForWindowsInstallerToFinish $WindowsInstallerService.id
Write-Host " Windows Installer process has finished"
Write-Host " Toggling ErrorActionPreference from $ErrorActionPreference to" ($ErrorActionPreference = "Continue")
$ReturnCode = Get-ReturnCodeFromLog $UninstallLog
Write-Host " Return Code: $ReturnCode"
PressAnyKey
$MenuOption = DisplayMenuOptions}
T {Write-Host 'Toggle UI Level'
$MSIUILevel = ToggleWindowsInstallerUILevel $MSIUILevel
$MenuOption = DisplayMenuOptions}
W {#Write Plain Text Information File routine.
$ReadMefile = $scriptPath + '\' + $MSI.basename + '.txt'
If ((Test-Path $ReadMefile) -eq $True)
{Write-Host
Write-Host 'Status:'
Write-Host ' File Exists, not overwriting.'
PressAnyKey}
Else
{$MSIFileName = $MSI.name | out-string
$MSIFileName = $MSIFileName -replace "`n|`r"
Add-content $ReadMefile -value "Application Name: $MSIProductName"
Add-content $ReadMefile -value "Version: $MSIProductVersion"
Add-content $ReadMefile -value "Product Code: $MSIProductCode"
Add-content $ReadMefile -value "Upgrade Code: $MSIUpgradeCode"
Add-content $ReadMefile -value ""
Add-content $ReadMefile -value "MSI Name: $MSIFileName"
Add-content $ReadMefile -value "Date: $Date"
Add-content $ReadMefile -value "Prerequisites: <please complete or type 'None'>"
Add-content $ReadMefile -value ""
Add-content $ReadMefile -value "Install Command:"
Add-content $ReadMefile -value "msiexec.exe /i $MSIFileName $MSIProperties /qn /l*vx $ReadMeInstallLog"
Add-content $ReadMefile -value ""
Add-content $ReadMefile -value "Uninstall Command"
Add-content $ReadMefile -value "msiexec.exe /x $MSIProductCode /qn /l*vx $ReadMeUninstallLog"
Write-Host
Write-Host 'Status:'
Write-Host " File has been written to '$ReadMefile'"
PressAnyKey}
$MenuOption = DisplayMenuOptions}
X {#Exit routine.
}
}
}
Until ($MenuOption -eq 'X')
#Exiting the menu system now. Clean up and setup the Powershell console for the user.
$CurrentExecutionPolicy = Get-ExecutionPolicy
$host.ui.RawUI.WindowTitle = "Powershell"
Clear-Host
Write-Host "Execution Policy in $CurrentExecutionPolicy mode for this console session."
Write-Host ' No System Modules have been imported.'
Write-Host
#<<< End of script work >>>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment