Last active
April 9, 2021 12:47
-
-
Save rileyz/a0af6b81c15d40d5075b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.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