Artifacts/windows-powershellcore/Install-PowerShellCore.ps1 (232 lines of code) (raw):

[CmdletBinding()] param ( [String] $PackageUrl, [switch] $InstallCRuntime ) ################################################################################################### # # PowerShell configurations # # NOTE: Because the $ErrorActionPreference is "Stop", this script will stop on first failure. # This is necessary to ensure we capture errors inside the try-catch-finally block. $ErrorActionPreference = 'Stop' # Suppress progress bar output. $ProgressPreference = 'SilentlyContinue' # Ensure we force use of TLS 1.2 for all downloads. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ################################################################################################### # # Handle all errors in this script. # trap { # NOTE: This trap will handle all errors. There should be no need to use a catch below in this # script, unless you want to ignore a specific error. $message = $Error[0].Exception.Message if ($message) { Write-Host -Object "`nERROR: $message" -ForegroundColor Red } Write-Host "`nThe artifact failed to apply.`n" # IMPORTANT NOTE: Throwing a terminating error (using $ErrorActionPreference = "Stop") still # returns exit code zero from the PowerShell script when using -File. The workaround is to # NOT use -File when calling this script and leverage the try-catch-finally block and return # a non-zero exit code from the catch block. exit -1 } ################################################################################################### # # Functions used in this script. # function Get-PowerShellCore { [CmdletBinding()] param ( [String] $PackageUrl ) $coreMsi = "${env:Temp}\PowerShellCore.msi" $null = Invoke-WebRequest -Uri $PackageUrl -OutFile $coreMsi if (-not (Test-Path -Path $coreMsi)) { throw "Failed to download $PackageUrl." } return $coreMsi } function Install-CRuntime { [CmdletBinding()] param() $osVersion = (Get-WmiObject -Class Win32_OperatingSystem).Name.Split('|')[0] if ($osVersion -like "Microsoft Windows Server 2012 R2*") { $ucRuntimeUri = 'https://download.microsoft.com/download/3/1/1/311C06C1-F162-405C-B538-D9DC3A4007D1/WindowsUCRT.zip' $ucArchive = "${env:Temp}\WindowsUCRT.zip" $ucFiles = "${env:Temp}\ucFiles" $msuPath = "${env:Temp}\ucFiles\Windows8.1-KB3118401-x64.msu" try { Write-Host 'Acquiring C runtime installer.' Invoke-WebRequest -Uri $ucRuntimeUri -OutFile $ucArchive | Out-Null Write-Host 'Extracting downloaded archive to $ucFiles.' Add-Type -assembly 'System.IO.Compression.FileSystem' [IO.Compression.ZipFile]::ExtractToDirectory($ucArchive, $ucFiles) Write-Host "Installing Universal C runtime from $msuPath." Invoke-Process -FilePath wusa.exe -ArgumentList "/install $msuPath /quiet" } finally { if ($msuPath) { Remove-Item -Path $msuPath -ErrorAction SilentlyContinue -Force } if ($ucFiles) { Remove-Item -Path $ucFiles -ErrorAction SilentlyContinue -Recurse -Force } if ($ucArchive) { Remove-Item -Path $ucArchive -ErrorAction SilentlyContinue -Force } } } } function Install-PowerShellCore { [CmdletBinding()] param ( [String] $Msi ) Invoke-Process -FilePath msiexec.exe -ArgumentList "/i $Msi /quiet /qn /lvx* PowerShellCore.log" } function Invoke-Process { param ( [String] $FilePath = $(throw "The FileName must be provided."), [String] $ArgumentList = '' ) # Prepare specifics for starting the process that will install the component. $startInfo = New-Object System.Diagnostics.ProcessStartInfo $startInfo.Arguments = $ArgumentList $startInfo.CreateNoWindow = $true $startInfo.ErrorDialog = $false $startInfo.FileName = $FilePath $startInfo.RedirectStandardError = $true $startInfo.RedirectStandardInput = $true $startInfo.RedirectStandardOutput = $true $startInfo.UseShellExecute = $false $startInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden $startInfo.WorkingDirectory = $wd # Initialize a new process. $process = New-Object System.Diagnostics.Process try { # Configure the process so we can capture all its output. $process.EnableRaisingEvents = $true # Hook into the standard output and error stream events $errEvent = Register-ObjectEvent -SourceIdentifier OnErrorDataReceived $process "ErrorDataReceived" ` ` { param ( [System.Object] $sender, [System.Diagnostics.DataReceivedEventArgs] $e ) foreach ($s in $e.Data) { if ($s) { Write-Host "$s" -ForegroundColor Red -NoNewline } } } $outEvent = Register-ObjectEvent -SourceIdentifier OnOutputDataReceived $process "OutputDataReceived" ` ` { param ( [System.Object] $sender, [System.Diagnostics.DataReceivedEventArgs] $e ) foreach ($s in $e.Data) { if ($s) { Write-Host "$s" -NoNewline } } } $process.StartInfo = $startInfo; Write-Host "Executing $FilePath $ArgumentList" # Attempt to start the process. if ($process.Start()) { # Read from all redirected streams before waiting to prevent deadlock. $process.BeginErrorReadLine() $process.BeginOutputReadLine() # Wait for the application to exit for no more than 5 minutes. $process.WaitForExit(300000) | Out-Null } # Determine if process failed to execute. if ($process.ExitCode -eq 3010) { Write-Host 'The recent changes indicate a reboot is necessary. Please reboot at your earliest convenience.' } elseif ($process.ExitCode -eq 2359302) { # Ignore it as valid, as it means that a patch has already been applied. } elseif ($process.ExitCode -ne 0) { # Throwing an exception at this point will stop any subsequent # attempts for deployment. throw New-Object System.Exception($startInfo.FileName + ' exited with code: ' + $process.ExitCode) } } finally { # Free all resources associated to the process. $process.Close(); # Remove any previous event handlers. Unregister-Event OnErrorDataReceived -Force | Out-Null Unregister-Event OnOutputDataReceived -Force | Out-Null } } function Test-PackageUrl { [CmdletBinding()] param ( [String] $PackageUrl ) if (-not (Split-Path -Path $PackageUrl -Leaf).EndsWith('.msi')) { throw "$PackageUrl is not the path to a PowerShell Core MSI." } } ################################################################################################### # # Main execution block. # [string] $coreMsi try { pushd $PSScriptRoot Write-Host 'Validating input parameters.' Test-PackageUrl -PackageUrl $PackageUrl Write-Host "Downloading $PackageUrl." $coreMsi = Get-PowerShellCore -PackageUrl $PackageUrl if ($InstallCRuntime) { Install-CRuntime } Write-Host 'Installing PowerShell Core.' Install-PowerShellCore -Msi $coreMsi Write-Host "`nThe artifact was applied successfully.`n" } finally { if ($coreMsi) { Remove-Item -Path $coreMsi -ErrorAction SilentlyContinue -Force } Pop-Location }