mirror of
https://github.com/azure-rtos/guix.git
synced 2025-02-04 07:13:17 +08:00
1829 lines
57 KiB
PowerShell
1829 lines
57 KiB
PowerShell
$ErrorActionPreference = "Stop"
|
|
|
|
$Separator = "--------------------------------------------------------------------------------------------------------------------------------"
|
|
$DefaultDownloadFolder = "C:\Downloads"
|
|
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
|
|
|
|
#####################################################################################################
|
|
# Start-Setup
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Sets up the context for the build script to work.
|
|
.DESCRIPTION
|
|
Prints out disk size information and sets up the downloaded content folder.
|
|
#>
|
|
function Start-Setup
|
|
{
|
|
Write-Host $Separator
|
|
|
|
Trace-Message "Starting installation"
|
|
|
|
Trace-Message "Checking disk space"
|
|
gwmi win32_logicaldisk | Format-Table DeviceId, MediaType, {$_.Size /1GB}, {$_.FreeSpace /1GB}
|
|
|
|
Trace-Message "Creating download location C:\Downloads"
|
|
New-Item -Path $DefaultDownloadFolder -ItemType Container -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Stop-Setup
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Shuts down the build script.
|
|
.DESCRIPTION
|
|
Deletes the downloaded content folder. Cleans the contents of the TEMP folder. Prints
|
|
out a list of the installed software on the image by querying WMIC.
|
|
.PARAMETER PreserveDownloads
|
|
Preserves the downloaded content folder.
|
|
.PARAMETER PreserveTemp
|
|
Preserves the temp folder contents.
|
|
#>
|
|
function Stop-Setup
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$false)]
|
|
[switch]$PreserveDownloads,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[switch]$PreserveTemp
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
if (-not $PreserveDownloads)
|
|
{
|
|
Trace-Message "Deleting download location C:\Downloads"
|
|
Remove-Item -Path "C:\Downloads" -Recurse -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
if (-not $PreserveTemp)
|
|
{
|
|
Reset-TempFolders
|
|
}
|
|
|
|
Trace-Message "Checking disk space"
|
|
gwmi win32_logicaldisk | Format-Table DeviceId, MediaType, {$_.Size /1GB}, {$_.FreeSpace /1GB}
|
|
|
|
Trace-Message "Listing installed 32-bit software"
|
|
Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName,DisplayVersion,Publisher,InstallDate | Sort-Object DisplayName,DisplayVersion,Publisher,InstallDate |out-string -width 300
|
|
|
|
Trace-Message "Listing installed 64-bit software"
|
|
Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName,DisplayVersion,Publisher,InstallDate | Sort-Object DisplayName,DisplayVersion,Publisher,InstallDate | out-string -width 300
|
|
|
|
Trace-Message "Finished installation."
|
|
Write-Host $Separator
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Get-File
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Downloads a file from a URL to the downloaded contents folder.
|
|
.DESCRIPTION
|
|
Fetches the contents of a file from a URL to the downloaded contents folder (C:\Downloads).
|
|
If a specific FilePath is specified, then skips the cache folder and downloads to the
|
|
specified path.
|
|
.PARAMETER Url
|
|
The URL of the content to fetch.
|
|
.PARAMETER FileName
|
|
The name of the file to write the fetched content to.
|
|
.OUTPUTS
|
|
The full path to the downloaded file.
|
|
#>
|
|
function Get-File
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Url,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$FileName
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
$file = [System.IO.Path]::Combine("C:\Downloads", $FileName)
|
|
|
|
Trace-Message "Downloading from $Url to file $File"
|
|
Invoke-WebRequest -Uri $Url -UseBasicParsing -OutFile $file -UserAgent "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)"
|
|
|
|
Trace-Message "Finished download"
|
|
Write-Host $Separator
|
|
|
|
return $file
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Add-EnvironmentVariable
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Defines a new or redefines an existing environment variable.
|
|
.DESCRIPTION
|
|
There are many ways to set environment variables. However, the default mechanisms do not
|
|
work when the change has to be persisted. This implementation writes the change into
|
|
the registry, invokes the .NET SetEnvironmentVariable method with Machine scope and then
|
|
invokes setx /m to force persistence of the change.
|
|
.PARAMETER Name
|
|
The name of the environment variable.
|
|
.PARAMETER Value
|
|
The value of the environment variable.
|
|
.NOTES
|
|
This does NOT work with PATH.
|
|
#>
|
|
function Add-EnvironmentVariable
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Name,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$Value
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
Trace-Message "Setting environment variable $name := $value"
|
|
|
|
Set-Item -Path Env:$Name -Value $Value
|
|
New-Item -Path "HKLM:\System\CurrentControlSet\Control\Session Manager\Environment" -ItemType String -Force -Name $Name -Value $Value
|
|
|
|
[System.Environment]::SetEnvironmentVariable($Name, $Value, [EnvironmentVariableTarget]::Machine)
|
|
|
|
&setx.exe /m $Name $Value
|
|
|
|
Write-Host $Separator
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Update-Path
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Redefines the PATH.
|
|
.DESCRIPTION
|
|
There are many ways to set environment variables. However, the default mechanisms do not
|
|
work when the change has to be persisted. This implementation writes the change into
|
|
the registry, invokes the .NET SetEnvironmentVariable method with Machine scope and then
|
|
invokes setx /m to force persistence of the change.
|
|
.PARAMETER PathNodes
|
|
An array of changes to the PATH. These values are appended to the existing value of PATH at the end.
|
|
.NOTES
|
|
This does NOT seem to work at all in Windows containers. Yet to be tested on RS5, but
|
|
definitely did not work in RS1 through RS4.
|
|
#>
|
|
function Update-Path
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[string[]]$PathNodes
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
$NodeToAppend=$null
|
|
|
|
$path = $env:Path
|
|
|
|
Trace-Message "Current value of PATH := $path"
|
|
Trace-Message "Appending $Update to PATH"
|
|
|
|
if (!$path.endswith(";"))
|
|
{
|
|
$path = $path + ";"
|
|
}
|
|
|
|
foreach ($PathNode in $PathNodes)
|
|
{
|
|
if (!$PathNode.endswith(";"))
|
|
{
|
|
$PathNode = $PathNode + ";"
|
|
}
|
|
$NodesToAppend += $PathNode
|
|
}
|
|
# add the new nodes
|
|
$path = $path + $NodesToAppend
|
|
|
|
#prettify it because there is some cruft from base images and or path typos i.e. foo;;
|
|
$path = $path -replace ";+",";"
|
|
|
|
#pull these in a hack until remove nodes is implemented
|
|
$path = $path.Replace("C:\Program Files\NuGet;","")
|
|
$path = $path.Replace("C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin;","")
|
|
$path = $path.Replace("C:\Program Files (x86)\Microsoft Visual Studio\2019\TestAgent\Common7\IDE\CommonExtensions\Microsoft\TestWindow;","")
|
|
|
|
#and set it
|
|
Trace-Message "Setting PATH to $path"
|
|
[System.Environment]::SetEnvironmentVariable("PATH", $path, [EnvironmentVariableTarget]::Machine)
|
|
|
|
Write-Host $Separator
|
|
}
|
|
|
|
|
|
#####################################################################################################
|
|
# Add-WindowsFeature
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Simple wrapper around the Install-WindowsFeature cmdlet.
|
|
.DESCRIPTION
|
|
A simple wrapper around the Install-WindowsFeature cmdlet that writes log lines and
|
|
data to help trace what happened.
|
|
.PARAMETER Name
|
|
The name of the feature to install.
|
|
|
|
.PARAMETER SourceString
|
|
The full -Source parameter with location to pass into install-WindowsFeature
|
|
#>
|
|
function Add-WindowsFeature
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Name,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$SourceLocation=$null
|
|
|
|
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
Trace-Message "Installing Windows feature $Name"
|
|
|
|
if ($SourceLocation)
|
|
{
|
|
Install-WindowsFeature -Name $Name -Source $SourceLocation -IncludeAllSubFeature -IncludeManagementTools -Restart:$false -Confirm:$false
|
|
}
|
|
else
|
|
{
|
|
Install-WindowsFeature -Name $Name -IncludeAllSubFeature -IncludeManagementTools -Restart:$false -Confirm:$false
|
|
}
|
|
|
|
Trace-Message "Finished installing Windows feature $Name"
|
|
|
|
Write-Host $Separator
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Remove-WindowsFeature
|
|
#####################################################################################################
|
|
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Simple wrapper around the Uninstall-WindowsFeature cmdlet.
|
|
.DESCRIPTION
|
|
A simple wrapper around the Uninstall-WindowsFeature cmdlet that writes log lines and
|
|
data to help trace what happened.
|
|
.PARAMETER Name
|
|
The name of the feature to uninstall.
|
|
#>
|
|
function Remove-WindowsFeature
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Name
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
Trace-Message "Removing Windows feature $Name"
|
|
|
|
Uninstall-WindowsFeature -Name $Name -IncludeManagementTools -Restart:$false -Confirm:$false
|
|
|
|
Trace-Message "Finished removing Windows feature $Name"
|
|
|
|
Write-Host $Separator
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Install-FromMSI
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Executes a Microsoft Installer package (MSI) in quiet mode.
|
|
.DESCRIPTION
|
|
Uses the msiexec tool with the appropriate arguments to execute the specified installer
|
|
package in quiet non-interactive mode with full verbose logging enabled.
|
|
.PARAMETER Path
|
|
The full path to the installer package file.
|
|
.PARAMETER Arguments
|
|
The optioal arguments to pass to the MSI installer package.
|
|
.PARAMETER IgnoreExitCodes
|
|
An array of exit codes to ignore. By default 3010 is always ignored because that indicates
|
|
a restart is required. Docker layers are an implied restart. In other scenarios such as
|
|
image builds or local runs, a restart can be easily triggered by the invoking script or
|
|
user.
|
|
.PARAMETER IgnoreFailures
|
|
Flag to force all failures (including actual failing exit codes) to be ignored. Notably
|
|
1603 is a very common one that indicates that an actual error occurred.
|
|
#>
|
|
function Install-FromMSI
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Path,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string[]]$Arguments,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[int[]]$IgnoreExitCodes,
|
|
|
|
[switch]$IgnoreFailures
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
if (-not (Test-Path $Path))
|
|
{
|
|
throw "CDPXERROR: Could not find the MSI installer package at $Path"
|
|
}
|
|
|
|
$fileNameOnly = [System.IO.Path]::GetFileNameWithoutExtension($Path)
|
|
|
|
$log = [System.IO.Path]::Combine($env:TEMP, $fileNameOnly + ".log")
|
|
|
|
$args = "/quiet /qn /norestart /lv! `"$log`" /i `"$Path`" $Arguments"
|
|
|
|
Trace-Message "Installing from $Path"
|
|
Trace-Message "Running msiexec.exe $args"
|
|
|
|
$ex = Start-ExternalProcess -Path "msiexec.exe" -Arguments $args
|
|
|
|
if ($ex -eq 3010)
|
|
{
|
|
Trace-Message "Install from $Path exited with code 3010. Ignoring since that is just indicating restart required."
|
|
Write-Host $Separator
|
|
return
|
|
}
|
|
elseif ($ex -ne 0)
|
|
{
|
|
foreach ($iex in $IgnoreExitCodes)
|
|
{
|
|
if ($ex -eq $iex)
|
|
{
|
|
Trace-Message "Install from $Path succeeded with exit code $ex"
|
|
Write-Host $Separator
|
|
return
|
|
}
|
|
}
|
|
|
|
Trace-Error "Failed to install from $Path. Process exited with code $ex"
|
|
|
|
if (-not $IgnoreFailures)
|
|
{
|
|
throw "Failed to install from $Path. Process exited with code $ex"
|
|
}
|
|
}
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Install-FromEXE
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Executes any arbitrary executable installer.
|
|
.DESCRIPTION
|
|
A simple wrapper function to kick off an executable installer and handle failures, logging etc.
|
|
.PARAMETER Path
|
|
The path to the installer package file.
|
|
.PARAMETER Arguments
|
|
The optioal arguments to pass to the installer package.
|
|
.PARAMETER IgnoreExitCodes
|
|
An array of exit codes to ignore. By default 3010 is always ignored because that indicates
|
|
a restart is required. Docker layers are an implied restart. In other scenarios such as
|
|
image builds or local runs, a restart can be easily triggered by the invoking script or
|
|
user.
|
|
.PARAMETER IgnoreFailures
|
|
Flag to force all failures (including actual failing exit codes) to be ignored. Notably
|
|
1603 is a very common one that indicates that an actual error occurred.
|
|
#>
|
|
function Install-FromEXE
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Path,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[int[]]$IgnoreExitCodes,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string[]]$Arguments,
|
|
|
|
[switch]$IgnoreFailures
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
Trace-Message "Running $Path"
|
|
|
|
$ex = Start-ExternalProcess -Path $Path -Arguments $Arguments
|
|
|
|
if ($ex -eq 3010)
|
|
{
|
|
Trace-Message "Install from $Path exited with code 3010. Ignoring since that is just indicating restart required."
|
|
Write-Host $Separator
|
|
return
|
|
}
|
|
elseif ($ex -ne 0)
|
|
{
|
|
foreach ($iex in $IgnoreExitCodes)
|
|
{
|
|
if ($ex -eq $iex)
|
|
{
|
|
Trace-Message "Install from $Path succeeded with exit code $ex"
|
|
Write-Host $Separator
|
|
return
|
|
}
|
|
}
|
|
|
|
Trace-Error "Failed to install from $Path. Process exited with code $ex"
|
|
|
|
if (-not $IgnoreFailures)
|
|
{
|
|
throw "Failed to install from $Path. Process exited with code $ex"
|
|
}
|
|
}
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Install-FromInnoSetup
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
A shorthand function for running a Inno Setup installer package with the appropriate options.
|
|
.DESCRIPTION
|
|
Inno Setup installer packages can be run in silent mode with the options
|
|
/VERYSILENT /NORESTART /CLOSEAPPLICATIONS /TYPE=full. In most cases, these options are the
|
|
same for every Inno Setup installer. This function is hence a short hand for Inno Setup.
|
|
.PARAMETER Path
|
|
The path to the Inno Setup installer package file.
|
|
.PARAMETER Arguments
|
|
The optioal arguments to pass to the installer package.
|
|
.PARAMETER IgnoreExitCodes
|
|
An array of exit codes to ignore.
|
|
.PARAMETER IgnoreFailures
|
|
Flag to force all failures (including actual failing exit codes) to be ignored.
|
|
|
|
#>
|
|
function Install-FromInnoSetup
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Path,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[int[]]$IgnoreExitCodes,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string[]]$Arguments,
|
|
|
|
[switch]$IgnoreFailures
|
|
)
|
|
|
|
$fileNameOnly = [System.IO.Path]::GetFileNameWithoutExtension($Path)
|
|
$logName = $fileNameOnly + ".log"
|
|
$logFile = Join-Path $Env:TEMP -ChildPath $logName
|
|
|
|
$args = "/QUIET /SP- /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /CLOSEAPPLICATIONS /NOICONS /TYPE=full /LOG `"$logFile`" "
|
|
$args += $Arguments
|
|
|
|
Install-FromEXE -Path $Path -Arguments $args -IgnoreExitCodes $IgnoreExitCodes -IgnoreFailures:$IgnoreFailures
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Install-FromDevToolsInstaller
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
A shorthand function for running a DevDiv Tools installer package with the appropriate options.
|
|
.DESCRIPTION
|
|
DevDiv Tools installer packages can be run in silent mode with the options
|
|
/quiet /install /norestart. In most cases, these options are the
|
|
same for every DevDiv Tools installer. This function is hence a short hand for DevDiv Tools
|
|
installer packages.
|
|
.PARAMETER Path
|
|
The path to the DevDiv Tools installer package file.
|
|
.PARAMETER Arguments
|
|
The optional arguments to pass to the installer package.
|
|
.PARAMETER IgnoreExitCodes
|
|
An array of exit codes to ignore. 3010 is added by default by this function.
|
|
.PARAMETER IgnoreFailures
|
|
Flag to force all failures (including actual failing exit codes) to be ignored.
|
|
|
|
#>
|
|
function Install-FromDevDivToolsInstaller
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Path,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[int[]]$IgnoreExitCodes,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string[]]$Arguments,
|
|
|
|
[switch]$IgnoreFailures
|
|
)
|
|
|
|
$fileNameOnly = [System.IO.Path]::GetFileNameWithoutExtension($Path)
|
|
$logName = $fileNameOnly + ".log"
|
|
$logFile = Join-Path $Env:TEMP -ChildPath $logName
|
|
|
|
$args = "/QUIET /INSTALL /NORESTART `"$logFile`" "
|
|
$args += $Arguments
|
|
|
|
$iec = (3010)
|
|
$iec += $IgnoreExitCodes
|
|
|
|
Install-FromEXE -Path $Path -Arguments $args -IgnoreExitCodes $iec -IgnoreFailures:$IgnoreFailures
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Install-FromChocolatey
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Installs a Chocolatey package.
|
|
.DESCRIPTION
|
|
Installs a package using Chocolatey in silent mode with no prompts.
|
|
.PARAMETER Name
|
|
The name of the package to install.
|
|
|
|
#>
|
|
function Install-FromChocolatey
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Name
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
Write-Host "Installing chocolatey package $Name"
|
|
Start-ExternalProcess -Path "C:\ProgramData\chocolatey\bin\choco.exe" -Arguments @("install","-y",$Name)
|
|
|
|
Write-Host $Separator
|
|
}
|
|
|
|
|
|
#####################################################################################################
|
|
# Install-FromEXEAsyncWithDevenvKill
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Starts an installer asynchronously and waits in the background for rogue child processes
|
|
and kills them after letting them finish.
|
|
.DESCRIPTION
|
|
Visual Studio installers start a number of child processes. Notable amongst them is the devenv.exe
|
|
process that attempts to initialize the VS IDE. Containers do not support UIs so this part hangs.
|
|
There might be other related processes such as msiexec as well that hang. Invariable, these
|
|
child processes complete quite fast, but never exit potentially becuase they are attempting
|
|
to display some UI and hang. This helper function will kick off the installer and then monitor
|
|
the task list to find those child processes by name and then it will kill them.
|
|
.PARAMETER Path
|
|
.PARAMETER StuckProcessNames
|
|
.PARAMETER IgnoreExitCodes
|
|
.PARAMETER IgnoreFailures
|
|
.PARAMETER Arguments
|
|
.PARAMETER WaitMinutes
|
|
#>
|
|
function Install-FromEXEAsyncWithDevenvKill
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Path,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[string[]]$StuckProcessNames,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[int[]]$IgnoreExitCodes,
|
|
|
|
[Parameter()]
|
|
[switch]$IgnoreFailures,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateRange(1, [int]::MaxValue)]
|
|
[int]$WaitMinutes = 5,
|
|
|
|
[string[]]$Arguments
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
Trace-Message "Running $Path with $Arguments"
|
|
|
|
$process = Start-Process $Path -PassThru -Verbose -NoNewWindow -ArgumentList $Arguments
|
|
$pid = $process.Id
|
|
$pn = [System.IO.Path]::GetFileNameWithoutExtension($Path)
|
|
|
|
Trace-Message "Started EXE asynchronously. Process ID is $pid"
|
|
|
|
Wait-ForProcess -Process $process -Minutes $WaitMinutes
|
|
|
|
Trace-Message "Walking task list and killing any processes in the stuck process list $StuckProcessNames"
|
|
|
|
foreach ($stuckProcessName in $StuckProcessNames)
|
|
{
|
|
Stop-ProcessByName -Name $stuckProcessName -WaitBefore 3 -WaitAfter 3
|
|
}
|
|
|
|
Trace-Message "Also killing any rogue msiexec processes"
|
|
|
|
Stop-ProcessByName -Name "msiexec" -WaitBefore 3 -WaitAfter 3
|
|
|
|
Wait-WithMessage -Message "Waiting for process with ID $pid launched from $Path to finish now that children have been killed off" -Minutes 2
|
|
|
|
Stop-ProcessByName -Name $pn -WaitBefore 3 -WaitAfter 3
|
|
|
|
$ex = $process.ExitCode;
|
|
|
|
if ($ex -eq 0)
|
|
{
|
|
Trace-Message "Install from $Path succeeded with exit code 0"
|
|
Write-Host $Separator
|
|
return
|
|
}
|
|
|
|
foreach ($iex in $ignoreExitCodes)
|
|
{
|
|
if ($ex -eq $iex)
|
|
{
|
|
Trace-Message "Install from $Path succeeded with exit code $ex"
|
|
Write-Host $Separator
|
|
return;
|
|
}
|
|
}
|
|
|
|
Trace-Error "Failed to install from $Path. Process exited with code $ex"
|
|
|
|
if (-not $IgnoreFailures)
|
|
{
|
|
throw "CDPXERROR: Failed to install from $Path. Process exited with exit code $ex"
|
|
}
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Confirm-PresenceOfVisualStudioErrorLogFile
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Throws an exception if a known Visual Studio installation error log file is found.
|
|
.DESCRIPTION
|
|
Visual Studio installers do not exit with appropriate error codes in case of component
|
|
install failures. Often, any errors are indicated by the presence of a non-zero size
|
|
error log file in the TEMP folder. This function checks for the existence of such files
|
|
and throws an exception if any are found.
|
|
.PARAMETER Path
|
|
The folder in which to check for the presence of the error log files. Defaults to $Env:TEMP
|
|
.PARAMETER Filter
|
|
The filename filter to apply to search for error log files.
|
|
.PARAMETER ThrowIfExists
|
|
If set, then fails if an error log file is found on disk even if the size is zero. Defaults to false.
|
|
.PARAMETER ThrowIfNotEmpty
|
|
If set, then fails if an error log file is found on disk and its size is non-zero. Defaults to true.
|
|
#>
|
|
function Confirm-PresenceOfVisualStudioErrorLogFile
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Filter,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Path = $Env:TEMP,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[switch]$ThrowIfExists = $false,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[switch]$ThrowIfNotEmpty = $true
|
|
)
|
|
|
|
if (Test-Path $Path)
|
|
{
|
|
Trace-Message "Checking if error log files matching the filter $Filter exist in $Path"
|
|
|
|
Get-ChildItem -Path $Path -Filter $Filter |
|
|
ForEach-Object
|
|
{
|
|
$file = $_.FullName
|
|
$len = $_.Length
|
|
|
|
Trace-Warning "Found error log file $file with size $len"
|
|
|
|
if ($ThrowIfExists)
|
|
{
|
|
throw "CDPXERROR: At least one error log file $file matching $Filter was found in $Path."
|
|
}
|
|
|
|
if ($ThrowIfNotEmpty -and ($len -gt 0))
|
|
{
|
|
throw "At least one non-empty log file $file matching $filter was found in $folder"
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Trace-Warning "Folder $Path does not exist. Skipping checks."
|
|
}
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Stop-ProcessByName
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Kills all processes with a given name.
|
|
.DESCRIPTION
|
|
Some installers start multiple instances of other applications to perform various
|
|
post-installer or initialization actions. The most notable is devenv.exe. This function
|
|
provides a mechanism to brute force kill all such instances.
|
|
.PARAMETER Name
|
|
The name of the process to kill.
|
|
.PARAMETER WaitBefore
|
|
The optional number of minutes to wait before killing the process. This provides time for
|
|
the process to finish its processes.
|
|
.PARAMETER WaitAfter
|
|
The optional number of minutes to wait after killing the process. This provides time for
|
|
the process to exit and any handles to expire.
|
|
#>
|
|
function Stop-ProcessByName
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Name,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateRange(1, [int]::MaxValue)]
|
|
[int]$WaitBefore = 3,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateRange(1, [int]::MaxValue)]
|
|
[int]$WaitAfter = 3
|
|
)
|
|
|
|
Wait-WithMessage -Message "Waiting for $WaitBefore minutes before killing all processes named $processName" -Minutes $WaitBefore
|
|
&tasklist /v
|
|
|
|
$count = 0
|
|
|
|
Get-Process -Name $Name -ErrorAction SilentlyContinue |
|
|
ForEach-Object
|
|
{
|
|
$process = $_
|
|
Trace-Warning "Killing process with name $Name and ID $($process.Id)"
|
|
$process.Kill()
|
|
++$count
|
|
}
|
|
|
|
Trace-Warning "Killed $count processes with name $Name"
|
|
|
|
Wait-WithMessage -Message "Waiting for $WaitAfter minutes after killing all processes named $Name" -Minutes $WaitAfter
|
|
|
|
&tasklist /v
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Wait-WithMessage
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Performs a synchronous sleep.
|
|
.DESCRIPTION
|
|
Some asynchronous and other operations require a wait time before
|
|
assuming a failure. This function forces the caller to sleep. The sleep is
|
|
performed in 1-minute intervals and a message is printed on each wakeup.
|
|
.PARAMETER Message
|
|
The message to print after each sleep period.
|
|
.PARAMETER Minutes
|
|
The number of minutes to sleep.
|
|
#>
|
|
function Wait-WithMessage
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Message,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateRange(1, [int]::MaxValue)]
|
|
[int]$Minutes
|
|
)
|
|
|
|
$elapsed = 0
|
|
|
|
while ($true)
|
|
{
|
|
if ($elapsed -ge $Minutes)
|
|
{
|
|
Write-Host "Done waiting for $elapsed minutes"
|
|
break
|
|
}
|
|
|
|
Trace-Message $Message
|
|
Start-Sleep -Seconds 60
|
|
++$elapsed
|
|
}
|
|
}
|
|
|
|
|
|
#####################################################################################################
|
|
# Wait-WithMessageAndMonitor
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Performs a synchronous sleep and on each wakeup runs a script block that may contain some
|
|
monitoring code.
|
|
.DESCRIPTION
|
|
Some asynchronous and other operations require a wait time before
|
|
assuming a failure. This function forces the caller to sleep. The sleep is performed
|
|
in 1-minute intervals and a message is printed and a script block is run on each wakeup.
|
|
.PARAMETER Message
|
|
The message to print after each sleep period.
|
|
.PARAMETER Block
|
|
The script block to run after each sleep period.
|
|
.PARAMETER Minutes
|
|
The number of minutes to sleep.
|
|
#>
|
|
function Wait-WithMessageAndMonitor
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Message,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNull()]
|
|
[ScriptBlock]$Monitor,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateRange(1, [int]::MaxValue)]
|
|
[int]$Minutes
|
|
)
|
|
|
|
$elapsed = 0
|
|
|
|
while ($true)
|
|
{
|
|
if ($elapsed -ge $Minutes)
|
|
{
|
|
Write-Host "Done waiting for $elapsed minutes"
|
|
break
|
|
}
|
|
|
|
Trace-Message $Message
|
|
Start-Sleep -Seconds 60
|
|
$Monitor.Invoke()
|
|
++$elapsed
|
|
}
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Reset-TempFolders
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Deletes the contents of well known temporary folders.
|
|
.DESCRIPTION
|
|
Installing lots of software can leave the TEMP folder built up with crud. This function
|
|
wipes the well known temp folders $Env:TEMP and C:\Windows\TEMP of all contentes. The
|
|
folders are preserved however.
|
|
#>
|
|
function Reset-TempFolders
|
|
{
|
|
try
|
|
{
|
|
Trace-Message "Wiping contents of the $($Env:TEMP) and C:\Windows\TEMP folders."
|
|
|
|
Get-ChildItem -Directory -Path $Env:TEMP | ForEach-Object {
|
|
$p = $_.FullName
|
|
Trace-Message "Removing temporary file $p"
|
|
Remove-Item -Recurse -Force -Path $p -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Get-ChildItem -File -Path $Env:TEMP | ForEach-Object {
|
|
$p = $_.FullName
|
|
Trace-Message "Removing temporary file $p"
|
|
Remove-Item -Force -Path $_.FullName -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Get-ChildItem -Directory -Path "C:\Windows\Temp" | ForEach-Object {
|
|
$p = $_.FullName
|
|
Trace-Message "Removing temporary file $p"
|
|
Remove-Item -Recurse -Force -Path $_.FullName -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Get-ChildItem -File -Path "C:\Windows\Temp" | ForEach-Object {
|
|
$p = $_.FullName
|
|
Trace-Message "Removing temporary file $p"
|
|
Remove-Item -Force -Path $_.FullName -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
Trace-Warning "Errors occurred while trying to clean up temporary folders."
|
|
$_.Exception | Format-List
|
|
}
|
|
finally
|
|
{
|
|
Trace-Message "Cleaned up temporary folders at $Env:TEMP and C:\Windows\Temp"
|
|
}
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Confirm-FileHash
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Verifies the content hash of downloaded content.
|
|
.DESCRIPTION
|
|
By default computes the SHA256 hash of downloaded content and compares it against
|
|
a given hash assuming it to be a SHA256 hash as well.
|
|
.PARAMETER FileName
|
|
The name of the file. If the IsFullPath switch is not specified, assumes a file within
|
|
the downloaded content cache.
|
|
.PARAMETER ExpectedHash
|
|
The expected hash value of the content.
|
|
.PARAMETER Algorithm
|
|
The optional hash algorithm to hash. Defaults to SHA256.
|
|
.OUTPUTS
|
|
#>
|
|
function Confirm-FileHash
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Path,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$ExpectedHash,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Algorithm = "sha256"
|
|
)
|
|
|
|
Trace-Message "Verifying content hash for file $Path"
|
|
|
|
$exists = Test-Path -Path $Path -PathType Leaf
|
|
|
|
if (-not $exists)
|
|
{
|
|
throw "CDPXERROR: Failed to find file $Path in order to verify hash."
|
|
}
|
|
|
|
$hash = Get-FileHash $Path -Algorithm $Algorithm
|
|
|
|
if ($hash.Hash -ne $ExpectedHash)
|
|
{
|
|
throw "File $Path hash $hash.Hash did not match expected hash $expectedHash"
|
|
}
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Start-ExternalProcess
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Executes an external application
|
|
.DESCRIPTION
|
|
PowerShell does not deal well with applications or scripts that write to
|
|
standard error. This wrapper function handles starting the process,
|
|
waiting for output and then captures the standard output/error streams and
|
|
reports them without writing them to stderr.
|
|
.PARAMETER Path
|
|
The path to the application to run.
|
|
.PARAMETER Arguments
|
|
The array of arguments to pass to the external application.
|
|
.OUTPUTS
|
|
Returns the exit code that the application exited with.
|
|
#>
|
|
function Start-ExternalProcess
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Path,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string[]]$Arguments
|
|
)
|
|
|
|
Trace-Message "Executing application: $Path $Arguments"
|
|
|
|
$guid = [System.Guid]::NewGuid().ToString("N")
|
|
$errLogFileName = -join($guid, "-stderr.log")
|
|
$outLogFileName = -join($guid, "-stdout.log")
|
|
$errLogFile = Join-Path -Path $Env:TEMP -ChildPath $errLogFileName
|
|
$outLogFile = Join-Path -Path $Env:TEMP -ChildPath $outLogFileName
|
|
$workDir = [System.IO.Path]::GetDirectoryName($Path)
|
|
[System.Diagnostics.Process]$process = $null
|
|
|
|
if (($Arguments -ne $null) -and ($Arguments.Length -gt 0))
|
|
{
|
|
$process = Start-Process -FilePath $Path -ArgumentList $Arguments -NoNewWindow -PassThru -RedirectStandardError $errLogFile -RedirectStandardOutput $outLogFile
|
|
}
|
|
else
|
|
{
|
|
$process = Start-Process -FilePath $Path -NoNewWindow -PassThru -RedirectStandardError $errLogFile -RedirectStandardOutput $outLogFile
|
|
}
|
|
|
|
$handle = $process.Handle
|
|
$pid = $process.Id
|
|
$ex = 0
|
|
|
|
Trace-Message -Message "Started process from $Path with PID $pid (and cached handle $handle)"
|
|
|
|
while ($true)
|
|
{
|
|
Trace-Message -Message "Waiting for PID $pid to exit ..."
|
|
|
|
if ($process.HasExited)
|
|
{
|
|
Trace-Message -Message "PID $pid has exited!"
|
|
break
|
|
}
|
|
|
|
Sleep -Seconds 60
|
|
}
|
|
|
|
Trace-Message "STDERR ---------------------------"
|
|
Get-Content $errLogFile | Write-Host
|
|
|
|
Trace-Message "STDOUT ---------------------------"
|
|
Get-Content $outLogFile | Write-Host
|
|
|
|
$ex = $process.ExitCode
|
|
|
|
if ($ex -eq $null)
|
|
{
|
|
Trace-Warning -Message "The process $pid returned a null or invalid exit code value. Assuming and returning 0"
|
|
$ex = 0
|
|
}
|
|
else
|
|
{
|
|
Trace-Message "Process $pid exited with exit code $ex"
|
|
}
|
|
|
|
return $ex
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Run-ExternalProcessWithWaitAndKill
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Executes an external application, waits for a specified amount of time and then kills it.
|
|
.DESCRIPTION
|
|
Some applications get stuck when running for the first time. This function starts the
|
|
application, then waits and then kills it so that a subsequent run can succeed.
|
|
.PARAMETER Path
|
|
The path to the application to run.
|
|
.PARAMETER Arguments
|
|
The array of arguments to pass to the external application.
|
|
.PARAMETER Minutes
|
|
The amount of time to wait in minutes before killing the external application.
|
|
.OUTPUTS
|
|
The exit code if one is available from the process.
|
|
#>
|
|
function Run-ExternalProcessWithWaitAndKill
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Path,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string[]]$Arguments,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ScriptBlock]$Monitor,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateRange(1, [int]::MaxValue)]
|
|
[int]$Minutes
|
|
)
|
|
|
|
Trace-Message "Executing application: $Path $Arguments. Will wait $Minutes minutes before killing it."
|
|
|
|
$guid = [System.Guid]::NewGuid().ToString("N")
|
|
$errLogFileName = -join($guid, "-stderr.log")
|
|
$outLogFileName = -join($guid, "-stdout.log")
|
|
$errLogFile = Join-Path -Path $Env:TEMP -ChildPath $errLogFileName
|
|
$outLogFile = Join-Path -Path $Env:TEMP -ChildPath $outLogFileName
|
|
$workDir = [System.IO.Path]::GetDirectoryName($Path)
|
|
[System.Diagnostics.Process]$process = $null
|
|
|
|
if (-not $Arguments)
|
|
{
|
|
$process = Start-Process -FilePath $Path -NoNewWindow -PassThru -RedirectStandardError $errLogFile -RedirectStandardOutput $outLogFile
|
|
}
|
|
else
|
|
{
|
|
$process = Start-Process -FilePath $Path -ArgumentList $Arguments -NoNewWindow -PassThru -RedirectStandardError $errLogFile -RedirectStandardOutput $outLogFile
|
|
}
|
|
|
|
$handle = $process.Handle
|
|
$pid = $process.Id
|
|
$ex = 0
|
|
|
|
Trace-Message -Message "Started process from $Path with PID $pid (and cached handle $handle)"
|
|
|
|
$exited = Wait-ForProcess -Process $process -Minutes $Minutes -Monitor $Monitor
|
|
|
|
if (-not $exited)
|
|
{
|
|
Trace-Warning "CDPXERROR: Process with ID $pid failed to exit within $Minutes minutes. Killing it."
|
|
|
|
try
|
|
{
|
|
$process.Kill()
|
|
Trace-Warning "Killed PID $pid"
|
|
}
|
|
catch
|
|
{
|
|
Trace-Warning "Exception raised while attempting to kill PID $pid. Perhaps the process has already exited."
|
|
$_.Exception | Format-List
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$ex = $process.ExitCode
|
|
Trace-Message "Application $Path exited with exit code $ex"
|
|
}
|
|
|
|
Trace-Message "STDERR ---------------------------"
|
|
Get-Content $errLogFile | Write-Host
|
|
|
|
Trace-Message "STDOUT ---------------------------"
|
|
Get-Content $outLogFile | Write-Host
|
|
|
|
if ($ex -eq $null)
|
|
{
|
|
Trace-Warning -Message "The process $pid returned a null or invalid exit code value. Assuming and returning 0"
|
|
return 0
|
|
}
|
|
|
|
return $ex
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Wait-ForProcess
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Waits for a previously started process until it exits or there is a timeout.
|
|
.DESCRIPTION
|
|
Waits for a started process until it exits or a certain amount of time has elapsed.
|
|
.PARAMETER Process
|
|
The [System.Process] project to wait for.
|
|
.PARAMETER Minutes
|
|
The amount of time to wait for in minutes.
|
|
.PARAMETER Monitor
|
|
An optional script block that will be run after each wait interval.
|
|
#>
|
|
function Wait-ForProcess
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNull()]
|
|
[System.Diagnostics.Process]$Process,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateRange(1, [int]::MaxValue)]
|
|
[int]$Minutes = 10,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ScriptBlock]$Monitor
|
|
)
|
|
|
|
$waitTime = $Minutes
|
|
|
|
$handle = $process.Handle
|
|
$pid = $Process.Id
|
|
|
|
while ($waitTime -gt 0)
|
|
{
|
|
Trace-Message -Message "Waiting for process with ID $pid to exit in $waitTime minutes."
|
|
|
|
if ($Process.HasExited)
|
|
{
|
|
$ex = $Process.ExitCode
|
|
Trace-Message "Process with ID $pid has already exited with exit code $ex"
|
|
return $true
|
|
}
|
|
|
|
Sleep -Seconds 60
|
|
|
|
if ($Monitor)
|
|
{
|
|
try
|
|
{
|
|
Trace-Message "Invoking monitor script: $Monitor"
|
|
$Monitor.Invoke()
|
|
}
|
|
catch
|
|
{
|
|
Trace-Warning "Exception occurred invoking monitoring script"
|
|
$_.Exception | Format-List
|
|
}
|
|
}
|
|
|
|
--$waitTime
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Trace-Message
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Logs an informational message to the console.
|
|
.DESCRIPTION
|
|
Writes a message to the console with the current timestamp and an information tag.
|
|
.PARAMETER Message
|
|
The message to write.
|
|
#>
|
|
function Trace-Message
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true, Position=0)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Message
|
|
)
|
|
|
|
$Message = $Message -replace "##vso", "__VSO_DISALLOWED"
|
|
$timestamp = Get-Date
|
|
Write-Host "[INFO] [$timestamp] $Message"
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Trace-Warning
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Logs a warning message to the console.
|
|
.DESCRIPTION
|
|
Writes a warning to the console with the current timestamp and a warning tag.
|
|
.PARAMETER Message
|
|
The warning to write.
|
|
#>
|
|
function Trace-Warning
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true, Position=0)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Message
|
|
)
|
|
|
|
$timestamp = Get-Date
|
|
$Message = $Message -replace "##vso", "__VSO_DISALLOWED"
|
|
Write-Host "[WARN] [$timestamp] $Message" -ForegroundColor Yellow
|
|
Write-Host "##vso[task.logissue type=warning]$Message"
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Trace-Error
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Logs an error message to the console.
|
|
.DESCRIPTION
|
|
Writes an error to the console with the current timestamp and an error tag.
|
|
.PARAMETER Message
|
|
The error to write.
|
|
#>
|
|
function Trace-Error
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true, Position=0)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Message
|
|
)
|
|
|
|
$timestamp = Get-Date
|
|
$Message = $Message -replace "##vso", "__VSO_DISALLOWED"
|
|
Write-Host "[ERROR] [$timestamp] $Message" -ForegroundColor Red
|
|
Write-Host "##vso[task.logissue type=error]$Message"
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Expand-ArchiveWith7Zip
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Uses 7-Zip to expand an archive instead of the standard Expand-Archive cmdlet.
|
|
.DESCRIPTION
|
|
The Expand-Archive cmdlet is slow compared to using 7-Zip directly. This function
|
|
assumes that 7-Zip is installed at C:\7-Zip.
|
|
.PARAMETER -Source
|
|
The path to the archive file.
|
|
.PARAMETER -Destination
|
|
The folder to expand into.
|
|
.PARAMETER ToolPath
|
|
The path to where the 7z.exe tool is available.
|
|
#>
|
|
function Expand-ArchiveWith7Zip
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Source,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Destination,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$ToolPath = "C:\7-Zip\7z.exe",
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[switch]$IgnoreFailures=$false
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
if (-not $ToolPath)
|
|
{
|
|
throw "CDPXERROR: The 7-Zip tool was not found at $ToolPath."
|
|
}
|
|
|
|
if (-not (Test-Path $Source))
|
|
{
|
|
throw "CDPXERROR: The specified archive file $Source could not be found."
|
|
}
|
|
|
|
if (-not $Destination)
|
|
{
|
|
$sourceDir = [System.IO.Path]::GetDirectoryName($Source);
|
|
$Destination = $sourceDir
|
|
|
|
Trace-Message "No destination was specified so the default location $Destination was chosen."
|
|
}
|
|
|
|
Trace-Message "Uncompressing archive $Source into folder $Destination using 7-Zip at $ToolPath"
|
|
|
|
Install-FromEXE -Path $ToolPath -Arguments "x -aoa -y `"$Source`" -o`"$Destination`"" -IgnoreFailures:$IgnoreFailures
|
|
|
|
Trace-Message "Successfully uncompressed archive at $Source into $Destination"
|
|
Write-Host $Separator
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Get-BlobPackageFromBase
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Uses AzCopy to download a blob package from blob store.
|
|
.DESCRIPTION
|
|
Some very large content such as Visual Studio offline installer files are stored in
|
|
a CDPX hosted blob store. This method fetches the contents of such blob packages
|
|
using AzCopy.
|
|
#>
|
|
function Get-BlobPackageFromBase
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$ContainerName,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$nodePath,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$downloadPath="C:\Downloads"
|
|
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
|
|
$Env:AZCOPY_LOG_LOCATION = $Env:TEMP
|
|
|
|
$url = Get-BlobPackageBaseUrl -ContainerName $ContainerName
|
|
|
|
Trace-Message "Invoking AzCopy CLI to download package $Name version $Version to $Path from $url"
|
|
|
|
$Arguments = @("copy", $url, $downloadPath, "--recursive", "--include-path $nodePath", "--include-pattern *")
|
|
|
|
Run-ExternalProcessWithWaitAndKill -Path "C:\AzCopy\azcopy.exe" -Arguments $Arguments -Minutes 30
|
|
|
|
Trace-Message "Finished downloading blob package"
|
|
|
|
Write-Host $Separator
|
|
|
|
return $Path
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Get-BlobPackageFromEdge
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Uses a HTTP/S request to download a blob package from CDN.
|
|
.DESCRIPTION
|
|
Some content such as third party OSS or free software are hosted on a CDPX hosted
|
|
blob store which is replicated to a CDN. This function fetches the blob package from
|
|
the CDN.
|
|
#>
|
|
function Get-BlobPackageFromEdge
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Name,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Version,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$FileName,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Path="C:\Downloads",
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$ContainerName
|
|
)
|
|
|
|
Write-Host $Separator
|
|
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
|
|
$url = Get-BlobPackageEdgeUrl -Name $Name -Version $Version -Container $ContainerName
|
|
|
|
Trace-Message "Downloading blob package $Name and $Version from $url"
|
|
|
|
$path = Get-File -Url $url -FileName $FileName
|
|
|
|
Trace-Message "Finished downloading blob package to $FileName"
|
|
|
|
Write-Host $Separator
|
|
|
|
return $path
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Enum HostEnvironment
|
|
#####################################################################################################
|
|
|
|
enum HostEnvironment
|
|
{
|
|
Dev
|
|
Test
|
|
Prod
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Get-HostEnvironment
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Uses some heuristics about the underlying host to determine what kind of environment the
|
|
host is in.
|
|
.DESCRIPTION
|
|
Leverages CDPX host naming conventions to determine if a host is a test or production host. If
|
|
neither is true, this function always assumes that the host is a developer box.
|
|
.OUTPUTS
|
|
An instance of the enumeration HostEnvironment.
|
|
#>
|
|
function Get-HostEnvironment
|
|
{
|
|
$ctrHost = $Env:TEMP_CONTAINER_HOST_NAME
|
|
|
|
if ($ctrHost)
|
|
{
|
|
if ($ctrHost.StartsWith("XWT"))
|
|
{
|
|
Trace-Message -Message "Running on CDPX test host."
|
|
return [HostEnvironment]::Test
|
|
}
|
|
elseif ($ctrHost.StartsWith("XWP"))
|
|
{
|
|
Trace-Message -Message "Running on CDPX prod host."
|
|
return [HostEnvironment]::Prod
|
|
}
|
|
}
|
|
|
|
Trace-Message "Unsure what kind of CDPX environment underlying host `"$ctrHost`" is in. Assuming development box."
|
|
return [HostEnvironment]::Dev
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Get-BlobContainerName
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Returns the container name to use for blob packages.
|
|
.DESCRIPTION
|
|
Returns a OS specific container name within which blob packages specific to that OS are
|
|
stored.
|
|
.OUTPUTS
|
|
Returns a lower case string that is the container name within the blob store in which
|
|
blob packages are stored.
|
|
#>
|
|
function Get-BlobContainerName
|
|
{
|
|
if ($Env:os -eq "Windows_NT")
|
|
{
|
|
return "windows"
|
|
}
|
|
elseif ($Env:OS -eq "Linux")
|
|
{
|
|
return "linux"
|
|
}
|
|
|
|
throw "CDPXERROR: Only supported operating systems are Windows and Linux. Unknown OS $($Env:OS)"
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Get-BlobAccountName
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Returns the base storage account in which blob packages are stored.
|
|
.DESCRIPTION
|
|
Returns an environment specific base storage account in which blob packages are stored.
|
|
.OUTPUTS
|
|
Returns a string that is an environment specific value for the blob storage account
|
|
in which blob packages are stored.
|
|
#>
|
|
function Get-BlobAccountName
|
|
{
|
|
$hostEnv = Get-HostEnvironment
|
|
$hostEnvStr = $hostEnv.ToString().ToLowerInvariant()
|
|
$prefix = "cxswdist"
|
|
$accountName = $prefix + $hostEnvStr
|
|
|
|
Trace-Warning "Currently overriding blob storage account to cxswdisttest for all host environments."
|
|
return "cxswdisttest"
|
|
}
|
|
|
|
|
|
#####################################################################################################
|
|
# Get-PackageFullName
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Gets the full name of a blob or universal package that can be downloaded by the functions
|
|
in this module.
|
|
.DESCRIPTION
|
|
Given a package name and a version, returns a full name to the package for use with
|
|
AzCopy or Az UPack CLI. The returned version is packagename-packageversion in lower case.
|
|
.OUTPUTS
|
|
The name of the package to use with blob store.
|
|
#>
|
|
|
|
function Get-PackageFullName
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Name,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$Version
|
|
)
|
|
|
|
$packageFullName = -join($Name, "-", $Version)
|
|
return $packageFullName.ToLowerInvariant()
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Get-LatestInstalledNetFrameworkVersion
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Gets the latest installed version of the .NET Framework.
|
|
.DESCRIPTION
|
|
Retrieves information from the registry based on the documentation at this link:
|
|
https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed#net_b.
|
|
Returns the entire child object from the registry.
|
|
.OUTPUTS
|
|
The child registry entry for the .NET framework installation.
|
|
#>
|
|
function Get-LatestInstalledNetFrameworkVersion
|
|
{
|
|
Trace-Message -Message "Retrieving latest installed .NET Framework version from registry entry: HKLM:`\SOFTWARE`\Microsoft`\NET Framework Setup`\NDP`\v4`\Full"
|
|
|
|
$item = Get-ChildItem HKLM:"\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"
|
|
|
|
return $item
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Run-VisualStudioInstallerProcessMonitor
|
|
#####################################################################################################
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Monitors progress of Visual Studio installation.
|
|
.DESCRIPTION
|
|
Checks if VS installer processes named vs_installer or vs_enteprise are still running.
|
|
Returns true if processes with those names were found. Otherwise returns false. In addition,
|
|
lists all dd_setup* log files found in $Env:TEMP where VS installers traditionally place
|
|
log files. Finally, if any error log files are present, prints out the contents of those
|
|
files.
|
|
.OUTPUTS
|
|
True if VS installer or bootstrapper processes are still running. Otherwise false.
|
|
#>
|
|
function Run-VisualStudioInstallerProcessMonitor
|
|
{
|
|
Write-Host $Separator
|
|
|
|
$processes = Get-Process
|
|
$numTotalProcesses = 0
|
|
$numVSIProcesses = 0
|
|
|
|
$processes | ForEach-Object {
|
|
|
|
$process = $_
|
|
$handle = $process.Handle
|
|
$pid = $process.Id
|
|
$ppath = $process.Path
|
|
|
|
if ($process.Name.StartsWith("vs_installer") -or
|
|
$process.Name.StartsWith("vs_enterprise"))
|
|
{
|
|
$numVSIProcesses++
|
|
|
|
Trace-Message -Message "Found VS Installer process with PID $pid launched from $ppath"
|
|
}
|
|
|
|
++$numTotalProcesses
|
|
}
|
|
|
|
Trace-Message "Total processes: $numTotalProcesses. VS Installer processes: $numVSIProcesses"
|
|
|
|
$setupLogs = Get-ChildItem $Env:TEMP -Filter "dd_setup*.log"
|
|
$setupLogs | Write-Host
|
|
|
|
$setupLogs | ForEach-Object {
|
|
|
|
$setupLog = $_
|
|
$setupLogPath = $setupLog.FullName
|
|
|
|
if ($setupLog.Name.Contains("errors"))
|
|
{
|
|
Trace-Message "Contents of VS installer error log: $setupLogPath"
|
|
Get-Content -Path $setupLogPath | Write-Host
|
|
}
|
|
}
|
|
|
|
Write-Host $Separator
|
|
|
|
if ($numVSIProcesses -gt 0)
|
|
{
|
|
return $true
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
#####################################################################################################
|
|
# Monitor-VisualStudioInstallation
|
|
#####################################################################################################
|
|
|
|
<#
|
|
#>
|
|
function Monitor-VisualStudioInstallation
|
|
{
|
|
param
|
|
(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateRange(1, [int]::MaxValue)]
|
|
[int]$WaitBefore,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateRange(1, [int]::MaxValue)]
|
|
[int]$WaitAfter
|
|
)
|
|
|
|
$minutes = $WaitBefore
|
|
|
|
while ($minutes -gt 0)
|
|
{
|
|
Trace-Message -Message "WAITING for VS installer kickoff."
|
|
|
|
Run-VisualStudioInstallerProcessMonitor
|
|
|
|
Sleep -Seconds 60
|
|
|
|
--$minutes
|
|
}
|
|
|
|
$minutes = $WaitAfter
|
|
|
|
while ($minutes -gt 0)
|
|
{
|
|
Trace-Message -Message "WAITING for VS installer kickoff."
|
|
|
|
$ex = Run-VisualStudioInstallerProcessMonitor
|
|
|
|
if (-not $ex)
|
|
{
|
|
Trace-Message -Message "DONE Looks like VS installer processes are no longer running."
|
|
break
|
|
}
|
|
|
|
Sleep -Seconds 120
|
|
|
|
--$minutes
|
|
}
|
|
}
|