diff --git a/.github/workflows/studio_demo_test.yml b/.github/workflows/studio_demo_test.yml index 8db7c389..9c136627 100644 --- a/.github/workflows/studio_demo_test.yml +++ b/.github/workflows/studio_demo_test.yml @@ -32,7 +32,7 @@ jobs: submodules: true - name: Build - run: scripts\build_studio_demo.cmd + run: scripts\build_guix_studio.cmd - name: Test run: scripts\test_studio_demo.cmd diff --git a/.github/workflows/studio_demo_test_compile.yml b/.github/workflows/studio_demo_test_compile.yml index 30ec0515..a8fcc50d 100644 --- a/.github/workflows/studio_demo_test_compile.yml +++ b/.github/workflows/studio_demo_test_compile.yml @@ -32,7 +32,7 @@ jobs: submodules: true - name: Build - run: scripts\build_studio_demo_compile.cmd + run: scripts\build_guix.cmd - name: Test run: scripts\test_studio_demo_compile.cmd diff --git a/.github/workflows/studio_installer.yml b/.github/workflows/studio_installer.yml new file mode 100644 index 00000000..c8fd0e08 --- /dev/null +++ b/.github/workflows/studio_installer.yml @@ -0,0 +1,45 @@ +# This is a basic workflow that is manually triggered + +name: GUIX Studio Installer + +# Controls when the action will run. +on: + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "linux_job" + generate_studio_installer: + permissions: + contents: read + issues: read + checks: write + pull-requests: write + + # The type of runner that the job will run on + runs-on: windows-2019 + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Check out the repository + uses: actions/checkout@v4 + with: + submodules: true + + - name: Install Inno Setup + run: scripts\run-pwsh.cmd scripts\install_inno_setup.ps1 + + - name: Build GUIX Studio + run: scripts\build_guix_studio.cmd + + - name: Download VC++ Redistributable + run: scripts\download_vc_redist.cmd + + - name: Generate installer + run: scripts\generate_studio_installer.cmd + + - name: Upload installer + uses: actions/upload-artifact@v3.1.3 + with: + name: guix_studio_installer + path: guix_studio\installer\output\*.exe diff --git a/guix_studio/installer/Readme.txt b/guix_studio/installer/Readme.txt new file mode 100644 index 00000000..28ac2936 --- /dev/null +++ b/guix_studio/installer/Readme.txt @@ -0,0 +1,17 @@ + +To generate GUIX Studio installer, follow these steps: + +1) Install Inno Setup from the following link: + +http://www.jrsoftware.org/isinfo.php + +2) Build GUIX Studio executable: +Execute script "build_guix_studio.cmd" from scripts folder. + +3) Download Microsoft Visual C++ Redistributable: +Execute script "download_vc_redist.cmd" from scripts folder to download the Microsoft Visual C++ Redistributable package. +This package is essential for installing Microsoft C and C++(MSVC) runtime libraries. + +4)Run the Inno Setup compiler: +Launch the Inno Setup compiler and open /GUIX/installer/guix_installer.iss. +Click the compiler button. When the compiler runs cleanly, the installer is generated in output folder. diff --git a/guix_studio/installer/guix_installer_release.iss b/guix_studio/installer/guix_installer_release.iss new file mode 100644 index 00000000..c3d558a3 --- /dev/null +++ b/guix_studio/installer/guix_installer_release.iss @@ -0,0 +1,65 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. +; Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{1D4932BC-ACD4-4292-9530-92C8BE2E58CF} +AppName= GUIX Studio +AppVersion=6.3.0.1 +;AppPublisher= +AppPublisherURL=https://azure.com/rtos +AppSupportURL=https://azure.com/rtos +AppUpdatesURL=https://azure.com/rtos +DefaultDirName={sd}\Azure_RTOS\GUIX_Studio_6.3 +DefaultGroupName=Azure RTOS +CloseApplications=no +;LicenseFile= +OutputBaseFilename=guix_studio_setup_version_6.3.0.1 +SetupIconFile=graphics\guix_1616icon.ico +Compression=lzma +SolidCompression=yes +ChangesAssociations=yes +UsePreviousAppDir=no +UsePreviousGroup=no +;SignedUninstaller=yes +;SignTool= + +SourceDir=..\ +OutputDir=installer\output + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[CustomMessages] +AskAssociate=Associate the GUIX Studio application with the .gxp file extension + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce +Name: "associate"; Description: "{cm:AskAssociate}"; GroupDescription: "Other tasks"; Flags: checkedonce + +[Files] +Source: "installer\vc_redist.x86.exe"; DestDir: "{tmp}"; Flags: nocompression createallsubdirs recursesubdirs deleteafterinstall +Source: "build\vs_2019\Release\guix_studio.exe"; DestDir: "{app}\studio"; DestName: "GUIX_Studio.exe"; Flags: ignoreversion + +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{group}\GUIX Studio 6.3\GUIX Studio"; Filename: "{app}\studio\GUIX_Studio.exe" +Name: "{group}\GUIX Studio 6.3\GUIX Studio User's Guide"; Filename: "https://aka.ms/azrtos-guix-studio-user-guide" +Name: "{group}\GUIX Studio 6.3\GUIX User's Guide"; Filename: "https://aka.ms/azrtos-guix-user-guide" +Name: "{group}\GUIX Studio 6.3\{cm:UninstallProgram,GUIX Studio}"; Filename: "{uninstallexe}" +Name: "{commondesktop}\GUIX Studio 6.3.0.1"; Filename: "{app}\studio\GUIX_Studio.exe"; Tasks: desktopicon + +[Registry] +Root: HKCR; Subkey: ".gxp"; ValueType: string; ValueName: ""; ValueData: "GUIX_Studio_Project"; Flags: uninsdeletevalue; Tasks: associate +Root: HKCR; Subkey: "GUIX_Studio_Project"; ValueType: string; ValueName: ""; ValueData: "GUIX Studio Project"; Flags: uninsdeletekey; Tasks: associate +Root: HKCR; Subkey: "GUIX_Studio_Project\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\studio\GUIX_Studio.exe,0"; Tasks: associate +Root: HKCR; Subkey: "GUIX_Studio_Project\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\studio\GUIX_Studio.exe"" ""%1"""; Tasks: associate + +Root: HKLM; Subkey: "Software\Microsoft\Azure_RTOS\GUIX\InstallDir"; ValueType: string; ValueName: ""; ValueData: "{app}"; + +[Run] +Filename: "{tmp}\vc_redist.x86.exe"; StatusMsg: "Installing Visual C++ 2015-2019 Redistributable(x86)"; Parameters:"/passive" + diff --git a/scripts/build_studio_demo_compile.cmd b/scripts/build_guix.cmd similarity index 100% rename from scripts/build_studio_demo_compile.cmd rename to scripts/build_guix.cmd diff --git a/scripts/build_studio_demo.cmd b/scripts/build_guix_studio.cmd similarity index 100% rename from scripts/build_studio_demo.cmd rename to scripts/build_guix_studio.cmd diff --git a/scripts/download_vc_redist.cmd b/scripts/download_vc_redist.cmd new file mode 100644 index 00000000..861a8e02 --- /dev/null +++ b/scripts/download_vc_redist.cmd @@ -0,0 +1,7 @@ +rem Save working directory so that we can restore it back after building everything. This will make developers happy and then +rem switch to the folder this script resides in. Don't assume absolute paths because on the build host and on the dev host the locations may be different. +pushd "%~dp0" + +"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File download_vc_redist.ps1 + +exit /B %ERRORLEVEL% \ No newline at end of file diff --git a/scripts/download_vc_redist.ps1 b/scripts/download_vc_redist.ps1 new file mode 100644 index 00000000..a9c7fed4 --- /dev/null +++ b/scripts/download_vc_redist.ps1 @@ -0,0 +1,3 @@ +cd ../guix_studio/installer +Invoke-WebRequest https://aka.ms/vs/16/release/vc_redist.x86.exe -O vc_redist.x86.exe +dir \ No newline at end of file diff --git a/scripts/generate_studio_installer.cmd b/scripts/generate_studio_installer.cmd new file mode 100644 index 00000000..d87a7fbd --- /dev/null +++ b/scripts/generate_studio_installer.cmd @@ -0,0 +1,10 @@ +rem Save working directory so that we can restore it back after building everything. This will make developers happy and then +rem switch to the folder this script resides in. Don't assume absolute paths because on the build host and on the dev host the locations may be different. +pushd "%~dp0" + +cd ..\guix_studio\installer + +rem generate studio installer +"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" /Q "guix_installer_release.iss" + +exit /B %ERRORLEVEL% \ No newline at end of file diff --git a/scripts/install_inno_setup.ps1 b/scripts/install_inno_setup.ps1 new file mode 100644 index 00000000..da750b8c --- /dev/null +++ b/scripts/install_inno_setup.ps1 @@ -0,0 +1,30 @@ +$ErrorActionPreference = "Stop" + +if (Test-Path "$PSScriptRoot\win-installer-helper.psm1") +{ + Import-Module "$PSScriptRoot\win-installer-helper.psm1" +} +elseif (Test-Path "C:\win-installer-helper.psm1") +{ + Import-Module "C:\win-installer-helper.psm1" +} + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +Start-Setup + +try { + + + Get-File -Url "http://www.jrsoftware.org/download.php/is.exe" -fileName "innosetup-6.0.3.exe" + Install-FromExe -Path "C:\Downloads\innosetup-6.0.3.exe" -Arguments "/VERYSILENT /SUPPRESSMSGBOXES" +} +catch +{ + $_.Exception | Format-List + exit -1 +} +finally +{ + Stop-Setup +} diff --git a/scripts/run-pwsh.cmd b/scripts/run-pwsh.cmd new file mode 100644 index 00000000..e9a09613 --- /dev/null +++ b/scripts/run-pwsh.cmd @@ -0,0 +1,5 @@ +@echo off +setlocal EnableDelayedExpansion +setlocal EnableExtensions +"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File "%1" +exit /B %ERRORLEVEL% \ No newline at end of file diff --git a/scripts/win-installer-helper.psm1 b/scripts/win-installer-helper.psm1 new file mode 100644 index 00000000..c6bd8830 --- /dev/null +++ b/scripts/win-installer-helper.psm1 @@ -0,0 +1,1828 @@ +$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 + } +}