Functions/Install-MSI.ps1
function Install-MSI { <# .SYNOPSIS Installs one or more MSIs using msiexec. .DESCRIPTION Installs one or more MSIs using msiexec. Assumes Windows Installer 3.0 or latter is installed, which is true with Windows XP SP2 and latter, and Windows Server 2003 R2 and latter. Aliases: ismsi .INPUTS System.String .OUTPUTS System.Boolean .NOTES Author : Dan Thompson Copyright : 2020 Case Western Reserve University #> [CmdletBinding(SupportsShouldProcess)] [OutputType([System.Boolean])] param( # The path to the MSI. A collection of paths can be passed via the pipeline. # # Aliases: p, msi [Parameter( Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True, Mandatory = $True )] [Alias('p', 'msi')] [ValidateNotNullOrEmpty()] [ValidateScript({ $_ | Test-Path -PathType 'Leaf' })] [string]$MSIFilePath, # What to display on the screen. Must be one of the following: # - Full (Regular, full UI is shown on screen, just as if the user double-clicked the MSI.) # - Passive (Only a progress bar is shown on the screen. This is the default option.) # - Quiet (Nothing is shown on the screen. This is useful for when this is run from a startup, shutdown, # login, or logout script.) # # Aliases: dm [Alias('dm')] [ValidateNotNullOrEmpty()] [ValidateSet('Full', 'Passive', 'Quiet')] [string]$DisplayMode = 'Passive', # Controls if the computer is restarted after installing the MSI. Must be one of the following: # - Default (Restarts automatically if a restart is needed. This is the default.) # - Suppress (No restart is ever done, even if the MSI would normally require it.) # - Force (Forces the computer to restart after the MSI is installed, even if it isn't needed.) # - Prompt (Prompts the suer if they want to restart. This CANNOT be used if $DisplayMode is set to # 'Quiet'.) # # Aliases: rb [Alias('rb')] [ValidateNotNullOrEmpty()] [ValidateSet('Default', 'Suppress', 'Force', 'Prompt')] [ValidateScript({ -not ( ($_ -eq 'Prompt') -and ($DisplayMode -eq 'Quiet') ) })] [string]$RestartBehavior = 'Default', # The directory to place MSI log files in. Each MSI specified via either $MSIFilePath or the pipe will log # to a file named after the MSI in this directory. # # See https://docs.microsoft.com/en-us/windows/win32/msi/standard-installer-command-line-options for what # is logged. # # Aliases: l, log [Alias('l', 'log')] [ValidateNotNullOrEmpty()] [ValidateScript({ $_ | Test-Path -PathType 'Container' })] [string]$LogDirectoryPath, # By default, this function will consider it to be an error if the product in $MSIFilePath is already # installed. This changes things so it is no longer considered an error. # # Aliases: soe [Alias('soe')] [switch]$SuccessOnExists, # Public properties to pass to the MSI. # # Aliases: ap [Alias('pp')] [ValidateNotNullOrEmpty()] [string]$PublicProperties ) begin { # Determine if MSIEXEC exists and is in the path. $MSIExec = Get-Command -Name 'msiexec' -CommandType 'Application' -ErrorAction 'SilentlyContinue' if ($Null -eq $MSIExec) { throw 'MSIExec is not available.' } # If the logging directory is specified, and it doesn't already exist, we need to create it ahead of time # as MSIEXEC will not do this for us. $LogDirectoryExists = $False if ([string]::IsNullOrWhiteSpace($LogDirectoryPath)) { Write-Verbose -Message 'MSI logging not enabled.' } else { if ($LogDirectoryPath | Test-Path) { Write-Verbose -Message """$LogDirectoryPath"" already exists, so not creating it." $LogDirectoryExists = $True } else { Write-Verbose -Message """$LogDirectoryPath"" doesn't exist, so creating it ..." if ($Null -eq ($LogDirectoryPath | New-Item -ItemType 'Directory')) { Write-Error -Message "Failed to create ""$LogDirectoryPath"". MSI logging will not be done." } else { Write-Verbose -Message "Successfully created ""$LogDirectoryPath""." $LogDirectoryExists = $True } } } # Set display mode argument. Write-Verbose -Message "Display mode of $DisplayMode detected." $DisplayModeArgument = $Null switch ($DisplayMode) { 'Passive' { $DisplayModeArgument = '/passive' } 'Quiet' { $DisplayModeArgument = '/quiet' } } # Set restart argument. Write-Verbose -Message "Restart behavior of $RestartBehavior detected." $RestartBehaviorArgument = $Null switch ($RestartBehavior) { 'Suppress' { $RestartBehaviorArgument = '/norestart' } 'Force' { $RestartBehaviorArgument = '/forcerestart' } 'Prompt' { $RestartBehaviorArgument = '/promptrestart' } } } process { # Determine if this MSI is already installed. $MSIProperties = $MSIFilePath | Get-MSIProperties Write-Verbose -Message """$MSIFilePath"" has the following properties:" Write-Verbose -Message "`tProductName: $($MSIProperties.ProductName)" Write-Verbose -Message "`tProductVersion: $($MSIProperties.ProductVersion)" $InstalledProperties = Get-InstalledProduct | Where-Object { $_.DisplayName -eq $MSIProperties.ProductName -and $_.DisplayVersion -eq $MSIProperties.ProductVersion } if ($Null -eq $InstalledProperties) { Write-Verbose -Message 'Product is not installed. Continuing with install ...' # Create the (currently empty) log file if needed. (MSIEXEC won't write to a log file that doesn't exist.) $LogFilePath = $Null $LogFileExists = $False if ($LogDirectoryExists) { $LogFilePath = $LogDirectoryPath | Join-Path -ChildPath ($MSIFilePath | Split-Path -Leaf) $LogMessageSuffix = "for ""$MSIFilePath""." $LogVerboseMessageSuffix = "Logging $LogMessageSuffix." $LogErrorMessageSuffix = "Not logging $LogMessageSuffix." if ($LogFilePath | Test-Path) { if (-not ($LogFilePath | Test-Path -PathType 'Leaf')) { Write-Error -Message """$LogFilePath"" is not a file. $LogErrorMessageSuffix" } else { Write-Verbose -Message """$LogFilePath"" exists and is a file. $LogVerboseMessageSuffix" $LogFileExists = $True } } else { Write-Verbose """$LogFilePath"" doesn't exist. Attempting to create it ..." if ($Null -eq ($LogFilePath | New-Item -ItemType 'File')) { Write-Error -Message "Unable to create ""$LogFilePath"". $LogErrorMessageSuffix" } else { Write-Verbose -Message "Successfully created ""$LogFilePath"". $LogVerboseMessageSuffix" $LogFileExists = $True } } } # Install the MSI. $ArgumentList = @("/package ""$MSIFilePath""") if ($Null -ne $DisplayModeArgument) { $ArgumentList += $DisplayModeArgument } if ($Null -ne $RestartBehaviorArgument) { $ArgumentList += $RestartBehaviorArgument } if ($LogFileExists) { $ArgumentList += "/log ""$LogFilePath""" } if ($Null -ne $PublicProperties) { $ArgumentList += $PublicProperties } if ($PSCmdlet.ShouldProcess('msiexec', 'Start-Process')) { $RunMessageSuffix = "msiexec $ArgumentList" Write-Verbose -Message "Attempting to run: $RunMessageSuffix" $MSIExecProcess = Start-Process -FilePath 'msiexec' -ArgumentList $ArgumentList -Wait -PassThru if ($MSIExecProcess.ExitCode -eq 0) { Write-Verbose -Message "Successfully ran: $RunMessageSuffix" $True } else { Write-Error -Message "Encountered error code $($MSIExecProcess.ExitCode) when running: $RunMessageSuffix" $False } } else { $True } } else { $ExistsMessage = "The product in ""$MSIFilePath"" is already installed. Not installing." if ($SuccessOnExists.IsPresent) { Write-Verbose -Message $ExistsMessage $True } else { Write-Error -Message $ExistsMessage $False } } } } New-Alias -Name 'ismsi' -Value 'Install-MSI' |