1 $ErrorActionPreference = "Stop"
2 
3 $Separator = "--------------------------------------------------------------------------------------------------------------------------------"
4 $DefaultDownloadFolder = "C:\Downloads"
5 
6 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
7 
8 
9 #####################################################################################################
10 # Start-Setup
11 #####################################################################################################
12 
13 <#
14     .SYNOPSIS
15         Sets up the context for the build script to work.
16     .DESCRIPTION
17         Prints out disk size information and sets up the downloaded content folder.
18 #>
Start-Setupnull19 function Start-Setup
20 {
21     Write-Host $Separator
22 
23     Trace-Message "Starting installation"
24 
25     Trace-Message "Checking disk space"
26     gwmi win32_logicaldisk | Format-Table DeviceId, MediaType, {$_.Size /1GB}, {$_.FreeSpace /1GB}
27 
28     Trace-Message "Creating download location C:\Downloads"
29     New-Item -Path $DefaultDownloadFolder -ItemType Container -ErrorAction SilentlyContinue
30 }
31 
32 #####################################################################################################
33 # Stop-Setup
34 #####################################################################################################
35 
36 <#
37     .SYNOPSIS
38         Shuts down the build script.
39     .DESCRIPTION
40         Deletes the downloaded content folder. Cleans the contents of the TEMP folder. Prints
41         out a list of the installed software on the image by querying WMIC.
42     .PARAMETER PreserveDownloads
43         Preserves the downloaded content folder.
44     .PARAMETER PreserveTemp
45         Preserves the temp folder contents.
46 #>
Stop-Setupnull47 function Stop-Setup
48 {
49     param
50     (
51         [Parameter(Mandatory=$false)]
52         [switch]$PreserveDownloads,
53 
54         [Parameter(Mandatory=$false)]
55         [switch]$PreserveTemp
56     )
57 
58     Write-Host $Separator
59 
60     if (-not $PreserveDownloads)
61     {
62         Trace-Message "Deleting download location C:\Downloads"
63         Remove-Item -Path "C:\Downloads" -Recurse -ErrorAction SilentlyContinue
64     }
65 
66     if (-not $PreserveTemp)
67     {
68         Reset-TempFolders
69     }
70 
71     Trace-Message "Checking disk space"
72     gwmi win32_logicaldisk | Format-Table DeviceId, MediaType, {$_.Size /1GB}, {$_.FreeSpace /1GB}
73 
74     Trace-Message "Listing installed 32-bit software"
75     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
76 
77     Trace-Message "Listing installed 64-bit software"
78     Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName,DisplayVersion,Publisher,InstallDate | Sort-Object DisplayName,DisplayVersion,Publisher,InstallDate | out-string -width 300
79 
80     Trace-Message "Finished installation."
81     Write-Host $Separator
82 }
83 
84 #####################################################################################################
85 # Get-File
86 #####################################################################################################
87 
88 <#
89     .SYNOPSIS
90         Downloads a file from a URL to the downloaded contents folder.
91     .DESCRIPTION
92         Fetches the contents of a file from a URL to the downloaded contents folder (C:\Downloads).
93         If a specific FilePath is specified, then skips the cache folder and downloads to the
94         specified path.
95     .PARAMETER Url
96         The URL of the content to fetch.
97     .PARAMETER FileName
98         The name of the file to write the fetched content to.
99     .OUTPUTS
100         The full path to the downloaded file.
101 #>
Get-File()102 function Get-File
103 {
104     param
105     (
106         [Parameter(Mandatory=$true)]
107         [ValidateNotNullOrEmpty()]
108         [string]$Url,
109 
110         [Parameter(Mandatory=$true)]
111         [ValidateNotNullOrEmpty()]
112         [string]$FileName
113     )
114 
115     Write-Host $Separator
116 
117     $file = [System.IO.Path]::Combine("C:\Downloads", $FileName)
118 
119     Trace-Message "Downloading from $Url to file $File"
120     Invoke-WebRequest -Uri $Url -UseBasicParsing -OutFile $file -UserAgent "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)"
121 
122     Trace-Message "Finished download"
123     Write-Host $Separator
124 
125     return $file
126 }
127 
128 #####################################################################################################
129 # Add-EnvironmentVariable
130 #####################################################################################################
131 
132 <#
133     .SYNOPSIS
134         Defines a new or redefines an existing environment variable.
135     .DESCRIPTION
136         There are many ways to set environment variables. However, the default mechanisms do not
137         work when the change has to be persisted. This implementation writes the change into
138         the registry, invokes the .NET SetEnvironmentVariable method with Machine scope and then
139         invokes setx /m to force persistence of the change.
140     .PARAMETER Name
141         The name of the environment variable.
142     .PARAMETER Value
143         The value of the environment variable.
144     .NOTES
145         This does NOT work with PATH.
146 #>
Add-EnvironmentVariablenull147 function Add-EnvironmentVariable
148 {
149     param
150     (
151         [Parameter(Mandatory=$true)]
152         [ValidateNotNullOrEmpty()]
153         [string]$Name,
154 
155         [Parameter(Mandatory=$true)]
156         [string]$Value
157     )
158 
159     Write-Host $Separator
160 
161     Trace-Message "Setting environment variable $name := $value"
162 
163     Set-Item -Path Env:$Name -Value $Value
164     New-Item -Path "HKLM:\System\CurrentControlSet\Control\Session Manager\Environment" -ItemType String -Force -Name $Name -Value $Value
165 
166     [System.Environment]::SetEnvironmentVariable($Name, $Value, [EnvironmentVariableTarget]::Machine)
167 
168     &setx.exe /m $Name $Value
169 
170     Write-Host $Separator
171 }
172 
173 #####################################################################################################
174 #  Update-Path
175 #####################################################################################################
176 
177 <#
178     .SYNOPSIS
179         Redefines the PATH.
180     .DESCRIPTION
181         There are many ways to set environment variables. However, the default mechanisms do not
182         work when the change has to be persisted. This implementation writes the change into
183         the registry, invokes the .NET SetEnvironmentVariable method with Machine scope and then
184         invokes setx /m to force persistence of the change.
185     .PARAMETER PathNodes
186         An array of changes to the PATH. These values are appended to the existing value of PATH at the end.
187     .NOTES
188         This does NOT seem to work at all in Windows containers. Yet to be tested on RS5, but
189         definitely did not work in RS1 through RS4.
190 #>
Update-Pathnull191 function Update-Path
192 {
193     param
194     (
195         [Parameter(Mandatory=$true)]
196         [string[]]$PathNodes
197     )
198 
199     Write-Host $Separator
200 
201     $NodeToAppend=$null
202 
203     $path = $env:Path
204 
205     Trace-Message "Current value of PATH := $path"
206     Trace-Message "Appending $Update to PATH"
207 
208     if (!$path.endswith(";"))
209     {
210       $path = $path + ";"
211     }
212 
213     foreach ($PathNode in $PathNodes)
214     {
215        if (!$PathNode.endswith(";"))
216        {
217        $PathNode = $PathNode + ";"
218        }
219     $NodesToAppend += $PathNode
220     }
221 # add the new nodes
222     $path = $path + $NodesToAppend
223 
224 #prettify it because there is some cruft from base images and or path typos i.e. foo;;
225     $path = $path -replace ";+",";"
226 
227 #pull these in a hack until remove nodes is implemented
228     $path = $path.Replace("C:\Program Files\NuGet;","")
229     $path = $path.Replace("C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin;","")
230     $path = $path.Replace("C:\Program Files (x86)\Microsoft Visual Studio\2019\TestAgent\Common7\IDE\CommonExtensions\Microsoft\TestWindow;","")
231 
232 #and set it
233     Trace-Message "Setting PATH to $path"
234     [System.Environment]::SetEnvironmentVariable("PATH", $path, [EnvironmentVariableTarget]::Machine)
235 
236     Write-Host $Separator
237 }
238 
239 
240 #####################################################################################################
241 # Add-WindowsFeature
242 #####################################################################################################
243 
244 <#
245     .SYNOPSIS
246         Simple wrapper around the Install-WindowsFeature cmdlet.
247     .DESCRIPTION
248         A simple wrapper around the Install-WindowsFeature cmdlet that writes log lines and
249         data to help trace what happened.
250     .PARAMETER Name
251         The name of the feature to install.
252 
253     .PARAMETER SourceString
254         The full -Source parameter with location to pass into install-WindowsFeature
255 #>
Add-WindowsFeature()256 function Add-WindowsFeature
257 {
258     param
259     (
260         [Parameter(Mandatory=$true)]
261         [ValidateNotNullOrEmpty()]
262         [string]$Name,
263 
264         [Parameter(Mandatory=$false)]
265         [ValidateNotNullOrEmpty()]
266         [string]$SourceLocation=$null
267 
268 
269     )
270 
271     Write-Host $Separator
272 
273     Trace-Message "Installing Windows feature $Name"
274 
275     if ($SourceLocation)
276     {
277       Install-WindowsFeature -Name $Name -Source $SourceLocation -IncludeAllSubFeature -IncludeManagementTools -Restart:$false -Confirm:$false
278     }
279     else
280     {
281       Install-WindowsFeature -Name $Name -IncludeAllSubFeature -IncludeManagementTools -Restart:$false -Confirm:$false
282     }
283 
284     Trace-Message "Finished installing Windows feature $Name"
285 
286     Write-Host $Separator
287 }
288 
289 #####################################################################################################
290 # Remove-WindowsFeature
291 #####################################################################################################
292 
293 
294 <#
295     .SYNOPSIS
296         Simple wrapper around the Uninstall-WindowsFeature cmdlet.
297     .DESCRIPTION
298         A simple wrapper around the Uninstall-WindowsFeature cmdlet that writes log lines and
299         data to help trace what happened.
300     .PARAMETER Name
301         The name of the feature to uninstall.
302 #>
Remove-WindowsFeaturenull303 function Remove-WindowsFeature
304 {
305     param
306     (
307         [Parameter(Mandatory=$true)]
308         [ValidateNotNullOrEmpty()]
309         [string]$Name
310     )
311 
312     Write-Host $Separator
313 
314     Trace-Message "Removing Windows feature $Name"
315 
316     Uninstall-WindowsFeature -Name $Name -IncludeManagementTools -Restart:$false -Confirm:$false
317 
318     Trace-Message "Finished removing Windows feature $Name"
319 
320     Write-Host $Separator
321 }
322 
323 #####################################################################################################
324 # Install-FromMSI
325 #####################################################################################################
326 
327 <#
328     .SYNOPSIS
329         Executes a Microsoft Installer package (MSI) in quiet mode.
330     .DESCRIPTION
331         Uses the msiexec tool with the appropriate arguments to execute the specified installer
332         package in quiet non-interactive mode with full verbose logging enabled.
333     .PARAMETER Path
334         The full path to the installer package file.
335     .PARAMETER Arguments
336         The optioal arguments to pass to the MSI installer package.
337     .PARAMETER IgnoreExitCodes
338         An array of exit codes to ignore. By default 3010 is always ignored because that indicates
339         a restart is required. Docker layers are an implied restart. In other scenarios such as
340         image builds or local runs, a restart can be easily triggered by the invoking script or
341         user.
342     .PARAMETER IgnoreFailures
343         Flag to force all failures (including actual failing exit codes) to be ignored. Notably
344         1603 is a very common one that indicates that an actual error occurred.
345 #>
Install-FromMSI()346 function Install-FromMSI
347 {
348     param
349     (
350         [Parameter(Mandatory=$true)]
351         [ValidateNotNullOrEmpty()]
352         [string]$Path,
353 
354         [Parameter(Mandatory=$false)]
355         [string[]]$Arguments,
356 
357         [Parameter(Mandatory=$false)]
358         [int[]]$IgnoreExitCodes,
359 
360         [switch]$IgnoreFailures
361     )
362 
363     Write-Host $Separator
364 
365     if (-not (Test-Path $Path))
366     {
367         throw "CDPXERROR: Could not find the MSI installer package at $Path"
368     }
369 
370     $fileNameOnly = [System.IO.Path]::GetFileNameWithoutExtension($Path)
371 
372     $log = [System.IO.Path]::Combine($env:TEMP, $fileNameOnly + ".log")
373 
374     $args = "/quiet /qn /norestart /lv! `"$log`" /i `"$Path`" $Arguments"
375 
376     Trace-Message "Installing from $Path"
377     Trace-Message "Running msiexec.exe $args"
378 
379     $ex = Start-ExternalProcess -Path "msiexec.exe" -Arguments $args
380 
381     if ($ex -eq 3010)
382     {
383         Trace-Message "Install from $Path exited with code 3010. Ignoring since that is just indicating restart required."
384         Write-Host $Separator
385         return
386     }
387     elseif ($ex -ne 0)
388     {
389         foreach ($iex in $IgnoreExitCodes)
390         {
391             if ($ex -eq $iex)
392             {
393                 Trace-Message "Install from $Path succeeded with exit code $ex"
394                 Write-Host $Separator
395                 return
396             }
397         }
398 
399         Trace-Error "Failed to install from $Path. Process exited with code $ex"
400 
401         if (-not $IgnoreFailures)
402         {
403             throw "Failed to install from $Path. Process exited with code $ex"
404         }
405     }
406 }
407 
408 #####################################################################################################
409 # Install-FromEXE
410 #####################################################################################################
411 
412 <#
413     .SYNOPSIS
414         Executes any arbitrary executable installer.
415     .DESCRIPTION
416         A simple wrapper function to kick off an executable installer and handle failures, logging etc.
417     .PARAMETER Path
418         The path to the installer package file.
419     .PARAMETER Arguments
420         The optioal arguments to pass to the installer package.
421     .PARAMETER IgnoreExitCodes
422         An array of exit codes to ignore. By default 3010 is always ignored because that indicates
423         a restart is required. Docker layers are an implied restart. In other scenarios such as
424         image builds or local runs, a restart can be easily triggered by the invoking script or
425         user.
426     .PARAMETER IgnoreFailures
427         Flag to force all failures (including actual failing exit codes) to be ignored. Notably
428         1603 is a very common one that indicates that an actual error occurred.
429 #>
Install-FromEXEnull430 function Install-FromEXE
431 {
432     param
433     (
434         [Parameter(Mandatory=$true)]
435         [ValidateNotNullOrEmpty()]
436         [string]$Path,
437 
438         [Parameter(Mandatory=$false)]
439         [int[]]$IgnoreExitCodes,
440 
441         [Parameter(Mandatory=$false)]
442         [string[]]$Arguments,
443 
444         [switch]$IgnoreFailures
445     )
446 
447     Write-Host $Separator
448 
449     Trace-Message "Running $Path"
450 
451     $ex = Start-ExternalProcess -Path $Path -Arguments $Arguments
452 
453     if ($ex -eq 3010)
454     {
455         Trace-Message "Install from $Path exited with code 3010. Ignoring since that is just indicating restart required."
456         Write-Host $Separator
457         return
458     }
459     elseif ($ex -ne 0)
460     {
461         foreach ($iex in $IgnoreExitCodes)
462         {
463             if ($ex -eq $iex)
464             {
465                 Trace-Message "Install from $Path succeeded with exit code $ex"
466                 Write-Host $Separator
467                 return
468             }
469         }
470 
471         Trace-Error "Failed to install from $Path. Process exited with code $ex"
472 
473         if (-not $IgnoreFailures)
474         {
475             throw "Failed to install from $Path. Process exited with code $ex"
476         }
477     }
478 }
479 
480 #####################################################################################################
481 # Install-FromInnoSetup
482 #####################################################################################################
483 
484 <#
485     .SYNOPSIS
486         A shorthand function for running a Inno Setup installer package with the appropriate options.
487     .DESCRIPTION
488         Inno Setup installer packages can be run in silent mode with the options
489         /VERYSILENT /NORESTART /CLOSEAPPLICATIONS /TYPE=full. In most cases, these options are the
490         same for every Inno Setup installer. This function is hence a short hand for Inno Setup.
491     .PARAMETER Path
492         The path to the Inno Setup installer package file.
493     .PARAMETER Arguments
494         The optioal arguments to pass to the installer package.
495     .PARAMETER IgnoreExitCodes
496         An array of exit codes to ignore.
497     .PARAMETER IgnoreFailures
498         Flag to force all failures (including actual failing exit codes) to be ignored.
499 
500 #>
Install-FromInnoSetup()501 function Install-FromInnoSetup
502 {
503     param
504     (
505         [Parameter(Mandatory=$true)]
506         [ValidateNotNullOrEmpty()]
507         [string]$Path,
508 
509         [Parameter(Mandatory=$false)]
510         [int[]]$IgnoreExitCodes,
511 
512         [Parameter(Mandatory=$false)]
513         [string[]]$Arguments,
514 
515         [switch]$IgnoreFailures
516     )
517 
518     $fileNameOnly = [System.IO.Path]::GetFileNameWithoutExtension($Path)
519     $logName = $fileNameOnly + ".log"
520     $logFile = Join-Path $Env:TEMP -ChildPath $logName
521 
522     $args = "/QUIET /SP- /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /CLOSEAPPLICATIONS /NOICONS /TYPE=full /LOG `"$logFile`" "
523     $args += $Arguments
524 
525     Install-FromEXE -Path $Path -Arguments $args -IgnoreExitCodes $IgnoreExitCodes -IgnoreFailures:$IgnoreFailures
526 }
527 
528 #####################################################################################################
529 # Install-FromDevToolsInstaller
530 #####################################################################################################
531 
532 <#
533     .SYNOPSIS
534         A shorthand function for running a DevDiv Tools installer package with the appropriate options.
535     .DESCRIPTION
536         DevDiv Tools installer packages can be run in silent mode with the options
537         /quiet /install /norestart. In most cases, these options are the
538         same for every DevDiv Tools installer. This function is hence a short hand for DevDiv Tools
539         installer packages.
540     .PARAMETER Path
541         The path to the DevDiv Tools installer package file.
542     .PARAMETER Arguments
543         The optional arguments to pass to the installer package.
544     .PARAMETER IgnoreExitCodes
545         An array of exit codes to ignore. 3010 is added by default by this function.
546     .PARAMETER IgnoreFailures
547         Flag to force all failures (including actual failing exit codes) to be ignored.
548 
549 #>
Install-FromDevDivToolsInstaller()550 function Install-FromDevDivToolsInstaller
551 {
552     param
553     (
554         [Parameter(Mandatory=$true)]
555         [ValidateNotNullOrEmpty()]
556         [string]$Path,
557 
558         [Parameter(Mandatory=$false)]
559         [int[]]$IgnoreExitCodes,
560 
561         [Parameter(Mandatory=$false)]
562         [string[]]$Arguments,
563 
564         [switch]$IgnoreFailures
565     )
566 
567     $fileNameOnly = [System.IO.Path]::GetFileNameWithoutExtension($Path)
568     $logName = $fileNameOnly + ".log"
569     $logFile = Join-Path $Env:TEMP -ChildPath $logName
570 
571     $args = "/QUIET /INSTALL /NORESTART `"$logFile`" "
572     $args += $Arguments
573 
574     $iec = (3010)
575     $iec += $IgnoreExitCodes
576 
577     Install-FromEXE -Path $Path -Arguments $args -IgnoreExitCodes $iec -IgnoreFailures:$IgnoreFailures
578 }
579 
580 #####################################################################################################
581 # Install-FromChocolatey
582 #####################################################################################################
583 
584 <#
585     .SYNOPSIS
586         Installs a Chocolatey package.
587     .DESCRIPTION
588         Installs a package using Chocolatey in silent mode with no prompts.
589     .PARAMETER Name
590         The name of the package to install.
591 
592 #>
Install-FromChocolatey()593 function Install-FromChocolatey
594 {
595     param
596     (
597         [Parameter(Mandatory=$true)]
598         [ValidateNotNullOrEmpty()]
599         [string]$Name
600     )
601 
602     Write-Host $Separator
603 
604     Write-Host "Installing chocolatey package $Name"
605     Start-ExternalProcess -Path "C:\ProgramData\chocolatey\bin\choco.exe" -Arguments @("install","-y",$Name)
606 
607     Write-Host $Separator
608 }
609 
610 
611 #####################################################################################################
612 # Install-FromEXEAsyncWithDevenvKill
613 #####################################################################################################
614 
615 <#
616     .SYNOPSIS
617         Starts an installer asynchronously and waits in the background for rogue child processes
618         and kills them after letting them finish.
619     .DESCRIPTION
620         Visual Studio installers start a number of child processes. Notable amongst them is the devenv.exe
621         process that attempts to initialize the VS IDE. Containers do not support UIs so this part hangs.
622         There might be other related processes such as msiexec as well that hang. Invariable, these
623         child processes complete quite fast, but never exit potentially becuase they are attempting
624         to display some UI and hang. This helper function will kick off the installer and then monitor
625         the task list to find those child processes by name and then it will kill them.
626     .PARAMETER Path
627     .PARAMETER StuckProcessNames
628     .PARAMETER IgnoreExitCodes
629     .PARAMETER IgnoreFailures
630     .PARAMETER Arguments
631     .PARAMETER WaitMinutes
632 #>
Install-FromEXEAsyncWithDevenvKill()633 function Install-FromEXEAsyncWithDevenvKill
634 {
635     param
636     (
637         [Parameter(Mandatory=$true)]
638         [ValidateNotNullOrEmpty()]
639         [string]$Path,
640 
641         [Parameter(Mandatory=$true)]
642         [string[]]$StuckProcessNames,
643 
644         [Parameter(Mandatory=$false)]
645         [int[]]$IgnoreExitCodes,
646 
647         [Parameter()]
648         [switch]$IgnoreFailures,
649 
650         [Parameter(Mandatory=$false)]
651         [ValidateRange(1, [int]::MaxValue)]
652         [int]$WaitMinutes = 5,
653 
654         [string[]]$Arguments
655     )
656 
657     Write-Host $Separator
658 
659     Trace-Message "Running $Path with $Arguments"
660 
661     $process = Start-Process $Path -PassThru -Verbose -NoNewWindow -ArgumentList $Arguments
662     $pid = $process.Id
663     $pn = [System.IO.Path]::GetFileNameWithoutExtension($Path)
664 
665     Trace-Message "Started EXE asynchronously. Process ID is $pid"
666 
667     Wait-ForProcess -Process $process -Minutes $WaitMinutes
668 
669     Trace-Message "Walking task list and killing any processes in the stuck process list $StuckProcessNames"
670 
671     foreach ($stuckProcessName in $StuckProcessNames)
672     {
673         Stop-ProcessByName -Name $stuckProcessName -WaitBefore 3 -WaitAfter 3
674     }
675 
676     Trace-Message "Also killing any rogue msiexec processes"
677 
678     Stop-ProcessByName -Name "msiexec" -WaitBefore 3 -WaitAfter 3
679 
680     Wait-WithMessage -Message "Waiting for process with ID $pid launched from $Path to finish now that children have been killed off" -Minutes 2
681 
682     Stop-ProcessByName -Name $pn -WaitBefore 3 -WaitAfter 3
683 
684     $ex = $process.ExitCode;
685 
686     if ($ex -eq 0)
687     {
688         Trace-Message "Install from $Path succeeded with exit code 0"
689         Write-Host $Separator
690         return
691     }
692 
693     foreach ($iex in $ignoreExitCodes)
694     {
695         if ($ex -eq $iex)
696         {
697             Trace-Message "Install from $Path succeeded with exit code $ex"
698             Write-Host $Separator
699             return;
700         }
701     }
702 
703     Trace-Error "Failed to install from $Path. Process exited with code $ex"
704 
705     if (-not $IgnoreFailures)
706     {
707         throw "CDPXERROR: Failed to install from $Path. Process exited with exit code $ex"
708     }
709 }
710 
711 #####################################################################################################
712 # Confirm-PresenceOfVisualStudioErrorLogFile
713 #####################################################################################################
714 
715 <#
716     .SYNOPSIS
717         Throws an exception if a known Visual Studio installation error log file is found.
718     .DESCRIPTION
719         Visual Studio installers do not exit with appropriate error codes in case of component
720         install failures. Often, any errors are indicated by the presence of a non-zero size
721         error log file in the TEMP folder. This function checks for the existence of such files
722         and throws an exception if any are found.
723     .PARAMETER Path
724         The folder in which to check for the presence of the error log files. Defaults to $Env:TEMP
725     .PARAMETER Filter
726         The filename filter to apply to search for error log files.
727     .PARAMETER ThrowIfExists
728         If set, then fails if an error log file is found on disk even if the size is zero. Defaults to false.
729     .PARAMETER ThrowIfNotEmpty
730         If set, then fails if an error log file is found on disk and its size is non-zero. Defaults to true.
731 #>
Confirm-PresenceOfVisualStudioErrorLogFile()732 function Confirm-PresenceOfVisualStudioErrorLogFile
733 {
734     param
735     (
736         [Parameter(Mandatory = $true)]
737         [ValidateNotNullOrEmpty()]
738         [string]$Filter,
739 
740         [Parameter(Mandatory = $false)]
741         [ValidateNotNullOrEmpty()]
742         [string]$Path = $Env:TEMP,
743 
744         [Parameter(Mandatory = $false)]
745         [switch]$ThrowIfExists = $false,
746 
747         [Parameter(Mandatory = $false)]
748         [switch]$ThrowIfNotEmpty = $true
749     )
750 
751     if (Test-Path $Path)
752     {
753         Trace-Message "Checking if error log files matching the filter $Filter exist in $Path"
754 
755         Get-ChildItem -Path $Path -Filter $Filter |
756             ForEach-Object
757             {
758                 $file = $_.FullName
759                 $len = $_.Length
760 
761                 Trace-Warning "Found error log file $file with size $len"
762 
763                 if ($ThrowIfExists)
764                 {
765                     throw "CDPXERROR: At least one error log file $file matching $Filter was found in $Path."
766                 }
767 
768                 if ($ThrowIfNotEmpty -and ($len -gt 0))
769                 {
770                     throw "At least one non-empty log file $file matching $filter was found in $folder"
771                 }
772             }
773     }
774     else
775     {
776         Trace-Warning "Folder $Path does not exist. Skipping checks."
777     }
778 }
779 
780 #####################################################################################################
781 # Stop-ProcessByName
782 #####################################################################################################
783 
784 <#
785     .SYNOPSIS
786         Kills all processes with a given name.
787     .DESCRIPTION
788         Some installers start multiple instances of other applications to perform various
789         post-installer or initialization actions. The most notable is devenv.exe. This function
790         provides a mechanism to brute force kill all such instances.
791     .PARAMETER Name
792         The name of the process to kill.
793     .PARAMETER WaitBefore
794         The optional number of minutes to wait before killing the process. This provides time for
795         the process to finish its processes.
796     .PARAMETER WaitAfter
797         The optional number of minutes to wait after killing the process. This provides time for
798         the process to exit and any handles to expire.
799 #>
Stop-ProcessByName()800 function Stop-ProcessByName
801 {
802     param
803     (
804         [Parameter(Mandatory=$true)]
805         [ValidateNotNullOrEmpty()]
806         [string]$Name,
807 
808         [Parameter(Mandatory=$false)]
809         [ValidateRange(1, [int]::MaxValue)]
810         [int]$WaitBefore = 3,
811 
812         [Parameter(Mandatory=$false)]
813         [ValidateRange(1, [int]::MaxValue)]
814         [int]$WaitAfter = 3
815     )
816 
817     Wait-WithMessage -Message "Waiting for $WaitBefore minutes before killing all processes named $processName" -Minutes $WaitBefore
818     &tasklist /v
819 
820     $count = 0
821 
822     Get-Process -Name $Name -ErrorAction SilentlyContinue |
823         ForEach-Object
824         {
825             $process = $_
826             Trace-Warning "Killing process with name $Name and ID $($process.Id)"
827             $process.Kill()
828             ++$count
829         }
830 
831     Trace-Warning "Killed $count processes with name $Name"
832 
833     Wait-WithMessage -Message "Waiting for $WaitAfter minutes after killing all processes named $Name" -Minutes $WaitAfter
834 
835     &tasklist /v
836 }
837 
838 #####################################################################################################
839 # Wait-WithMessage
840 #####################################################################################################
841 
842 <#
843     .SYNOPSIS
844         Performs a synchronous sleep.
845     .DESCRIPTION
846         Some asynchronous and other operations require a wait time before
847         assuming a failure. This function forces the caller to sleep. The sleep is
848         performed in 1-minute intervals and a message is printed on each wakeup.
849     .PARAMETER Message
850         The message to print after each sleep period.
851     .PARAMETER Minutes
852         The number of minutes to sleep.
853 #>
Wait-WithMessagenull854 function Wait-WithMessage
855 {
856     param
857     (
858         [Parameter(Mandatory=$true)]
859         [ValidateNotNullOrEmpty()]
860         [string]$Message,
861 
862         [Parameter(Mandatory=$true)]
863         [ValidateRange(1, [int]::MaxValue)]
864         [int]$Minutes
865     )
866 
867     $elapsed = 0
868 
869     while ($true)
870     {
871         if ($elapsed -ge $Minutes)
872         {
873             Write-Host "Done waiting for $elapsed minutes"
874             break
875         }
876 
877         Trace-Message $Message
878         Start-Sleep -Seconds 60
879         ++$elapsed
880     }
881 }
882 
883 
884 #####################################################################################################
885 # Wait-WithMessageAndMonitor
886 #####################################################################################################
887 
888 <#
889     .SYNOPSIS
890         Performs a synchronous sleep and on each wakeup runs a script block that may contain some
891         monitoring code.
892     .DESCRIPTION
893         Some asynchronous and other operations require a wait time before
894         assuming a failure. This function forces the caller to sleep. The sleep is performed
895         in 1-minute intervals and a message is printed and a script block is run on each wakeup.
896     .PARAMETER Message
897         The message to print after each sleep period.
898     .PARAMETER Block
899         The script block to run after each sleep period.
900     .PARAMETER Minutes
901         The number of minutes to sleep.
902 #>
Wait-WithMessageAndMonitor()903 function Wait-WithMessageAndMonitor
904 {
905     param
906     (
907         [Parameter(Mandatory=$true)]
908         [ValidateNotNullOrEmpty()]
909         [string]$Message,
910 
911         [Parameter(Mandatory=$true)]
912         [ValidateNotNull()]
913         [ScriptBlock]$Monitor,
914 
915         [Parameter(Mandatory=$true)]
916         [ValidateRange(1, [int]::MaxValue)]
917         [int]$Minutes
918     )
919 
920     $elapsed = 0
921 
922     while ($true)
923     {
924         if ($elapsed -ge $Minutes)
925         {
926             Write-Host "Done waiting for $elapsed minutes"
927             break
928         }
929 
930         Trace-Message $Message
931         Start-Sleep -Seconds 60
932         $Monitor.Invoke()
933         ++$elapsed
934     }
935 }
936 
937 #####################################################################################################
938 # Reset-TempFolders
939 #####################################################################################################
940 
941 <#
942     .SYNOPSIS
943         Deletes the contents of well known temporary folders.
944     .DESCRIPTION
945         Installing lots of software can leave the TEMP folder built up with crud. This function
946         wipes the well known temp folders $Env:TEMP and C:\Windows\TEMP of all contentes. The
947         folders are preserved however.
948 #>
Reset-TempFoldersnull949 function Reset-TempFolders
950 {
951     try
952     {
953         Trace-Message "Wiping contents of the $($Env:TEMP) and C:\Windows\TEMP folders."
954 
955         Get-ChildItem -Directory -Path $Env:TEMP |  ForEach-Object {
956                 $p = $_.FullName
957                 Trace-Message "Removing temporary file $p"
958                 Remove-Item -Recurse -Force -Path $p -ErrorAction SilentlyContinue
959             }
960 
961         Get-ChildItem -File -Path $Env:TEMP | ForEach-Object {
962                 $p = $_.FullName
963                 Trace-Message "Removing temporary file $p"
964                 Remove-Item -Force -Path $_.FullName -ErrorAction SilentlyContinue
965             }
966 
967         Get-ChildItem -Directory -Path "C:\Windows\Temp" | ForEach-Object {
968                 $p = $_.FullName
969                 Trace-Message "Removing temporary file $p"
970                 Remove-Item -Recurse -Force -Path $_.FullName -ErrorAction SilentlyContinue
971             }
972 
973         Get-ChildItem -File -Path "C:\Windows\Temp" | ForEach-Object {
974                 $p = $_.FullName
975                 Trace-Message "Removing temporary file $p"
976                 Remove-Item -Force -Path $_.FullName -ErrorAction SilentlyContinue
977             }
978     }
979     catch
980     {
981         Trace-Warning "Errors occurred while trying to clean up temporary folders."
982         $_.Exception | Format-List
983     }
984     finally
985     {
986         Trace-Message "Cleaned up temporary folders at $Env:TEMP and C:\Windows\Temp"
987     }
988 }
989 
990 #####################################################################################################
991 # Confirm-FileHash
992 #####################################################################################################
993 
994 <#
995     .SYNOPSIS
996         Verifies the content hash of downloaded content.
997     .DESCRIPTION
998         By default computes the SHA256 hash of downloaded content and compares it against
999         a given hash assuming it to be a SHA256 hash as well.
1000     .PARAMETER FileName
1001         The name of the file. If the IsFullPath switch is not specified, assumes a file within
1002         the downloaded content cache.
1003     .PARAMETER ExpectedHash
1004         The expected hash value of the content.
1005     .PARAMETER Algorithm
1006         The optional hash algorithm to hash. Defaults to SHA256.
1007     .OUTPUTS
1008 #>
Confirm-FileHash()1009 function Confirm-FileHash
1010 {
1011     param
1012     (
1013         [Parameter(Mandatory=$true)]
1014         [ValidateNotNullOrEmpty()]
1015         [string]$Path,
1016 
1017         [Parameter(Mandatory=$true)]
1018         [ValidateNotNullOrEmpty()]
1019         [string]$ExpectedHash,
1020 
1021         [Parameter(Mandatory=$false)]
1022         [ValidateNotNullOrEmpty()]
1023         [string]$Algorithm = "sha256"
1024     )
1025 
1026     Trace-Message "Verifying content hash for file $Path"
1027 
1028     $exists = Test-Path -Path $Path -PathType Leaf
1029 
1030     if (-not $exists)
1031     {
1032         throw "CDPXERROR: Failed to find file $Path in order to verify hash."
1033     }
1034 
1035     $hash = Get-FileHash $Path -Algorithm $Algorithm
1036 
1037     if ($hash.Hash -ne $ExpectedHash)
1038     {
1039         throw "File $Path hash $hash.Hash did not match expected hash $expectedHash"
1040     }
1041 }
1042 
1043 #####################################################################################################
1044 # Start-ExternalProcess
1045 #####################################################################################################
1046 
1047 <#
1048     .SYNOPSIS
1049         Executes an external application
1050     .DESCRIPTION
1051         PowerShell does not deal well with applications or scripts that write to
1052         standard error. This wrapper function handles starting the process,
1053         waiting for output and then captures the standard output/error streams and
1054         reports them without writing them to stderr.
1055     .PARAMETER Path
1056         The path to the application to run.
1057     .PARAMETER Arguments
1058         The array of arguments to pass to the external application.
1059     .OUTPUTS
1060         Returns the exit code that the application exited with.
1061 #>
Start-ExternalProcess()1062 function Start-ExternalProcess
1063 {
1064     param
1065     (
1066         [Parameter(Mandatory=$true)]
1067         [ValidateNotNullOrEmpty()]
1068         [string]$Path,
1069 
1070         [Parameter(Mandatory=$false)]
1071         [string[]]$Arguments
1072     )
1073 
1074     Trace-Message "Executing application: $Path $Arguments"
1075 
1076     $guid = [System.Guid]::NewGuid().ToString("N")
1077     $errLogFileName = -join($guid, "-stderr.log")
1078     $outLogFileName = -join($guid, "-stdout.log")
1079     $errLogFile = Join-Path -Path $Env:TEMP -ChildPath $errLogFileName
1080     $outLogFile = Join-Path -Path $Env:TEMP -ChildPath $outLogFileName
1081     $workDir = [System.IO.Path]::GetDirectoryName($Path)
1082     [System.Diagnostics.Process]$process = $null
1083 
1084     if (($Arguments -ne $null) -and ($Arguments.Length -gt 0))
1085     {
1086         $process = Start-Process -FilePath $Path -ArgumentList $Arguments -NoNewWindow -PassThru -RedirectStandardError $errLogFile -RedirectStandardOutput $outLogFile
1087     }
1088     else
1089     {
1090         $process = Start-Process -FilePath $Path -NoNewWindow -PassThru -RedirectStandardError $errLogFile -RedirectStandardOutput $outLogFile
1091     }
1092 
1093     $handle = $process.Handle
1094     $pid = $process.Id
1095     $ex = 0
1096 
1097     Trace-Message -Message "Started process from $Path with PID $pid (and cached handle $handle)"
1098 
1099     while ($true)
1100     {
1101         Trace-Message -Message "Waiting for PID $pid to exit ..."
1102 
1103         if ($process.HasExited)
1104         {
1105             Trace-Message -Message "PID $pid has exited!"
1106             break
1107         }
1108 
1109         Sleep -Seconds 60
1110     }
1111 
1112     Trace-Message "STDERR ---------------------------"
1113     Get-Content $errLogFile | Write-Host
1114 
1115     Trace-Message "STDOUT ---------------------------"
1116     Get-Content $outLogFile | Write-Host
1117 
1118     $ex = $process.ExitCode
1119 
1120     if ($ex -eq $null)
1121     {
1122         Trace-Warning -Message "The process $pid returned a null or invalid exit code value. Assuming and returning 0"
1123         $ex = 0
1124     }
1125     else
1126     {
1127         Trace-Message "Process $pid exited with exit code $ex"
1128     }
1129 
1130     return $ex
1131 }
1132 
1133 #####################################################################################################
1134 # Run-ExternalProcessWithWaitAndKill
1135 #####################################################################################################
1136 
1137 <#
1138     .SYNOPSIS
1139         Executes an external application, waits for a specified amount of time and then kills it.
1140     .DESCRIPTION
1141         Some applications get stuck when running for the first time. This function starts the
1142         application, then waits and then kills it so that a subsequent run can succeed.
1143     .PARAMETER Path
1144         The path to the application to run.
1145     .PARAMETER Arguments
1146         The array of arguments to pass to the external application.
1147     .PARAMETER Minutes
1148         The amount of time to wait in minutes before killing the external application.
1149     .OUTPUTS
1150         The exit code if one is available from the process.
1151 #>
Run-ExternalProcessWithWaitAndKillnull1152 function Run-ExternalProcessWithWaitAndKill
1153 {
1154     param
1155     (
1156         [Parameter(Mandatory=$true)]
1157         [ValidateNotNullOrEmpty()]
1158         [string]$Path,
1159 
1160         [Parameter(Mandatory=$false)]
1161         [string[]]$Arguments,
1162 
1163         [Parameter(Mandatory=$false)]
1164         [ScriptBlock]$Monitor,
1165 
1166         [Parameter(Mandatory=$false)]
1167         [ValidateRange(1, [int]::MaxValue)]
1168         [int]$Minutes
1169     )
1170 
1171     Trace-Message "Executing application: $Path $Arguments. Will wait $Minutes minutes before killing it."
1172 
1173     $guid = [System.Guid]::NewGuid().ToString("N")
1174     $errLogFileName = -join($guid, "-stderr.log")
1175     $outLogFileName = -join($guid, "-stdout.log")
1176     $errLogFile = Join-Path -Path $Env:TEMP -ChildPath $errLogFileName
1177     $outLogFile = Join-Path -Path $Env:TEMP -ChildPath $outLogFileName
1178     $workDir = [System.IO.Path]::GetDirectoryName($Path)
1179     [System.Diagnostics.Process]$process = $null
1180 
1181     if (-not $Arguments)
1182     {
1183         $process = Start-Process -FilePath $Path -NoNewWindow -PassThru -RedirectStandardError $errLogFile -RedirectStandardOutput $outLogFile
1184     }
1185     else
1186     {
1187         $process = Start-Process -FilePath $Path -ArgumentList $Arguments -NoNewWindow -PassThru -RedirectStandardError $errLogFile -RedirectStandardOutput $outLogFile
1188     }
1189 
1190     $handle = $process.Handle
1191     $pid = $process.Id
1192     $ex = 0
1193 
1194     Trace-Message -Message "Started process from $Path with PID $pid (and cached handle $handle)"
1195 
1196     $exited = Wait-ForProcess -Process $process -Minutes $Minutes -Monitor $Monitor
1197 
1198     if (-not $exited)
1199     {
1200         Trace-Warning "CDPXERROR: Process with ID $pid failed to exit within $Minutes minutes. Killing it."
1201 
1202         try
1203         {
1204             $process.Kill()
1205             Trace-Warning "Killed PID $pid"
1206         }
1207         catch
1208         {
1209             Trace-Warning "Exception raised while attempting to kill PID $pid. Perhaps the process has already exited."
1210             $_.Exception | Format-List
1211         }
1212     }
1213     else
1214     {
1215         $ex = $process.ExitCode
1216         Trace-Message "Application $Path exited with exit code $ex"
1217     }
1218 
1219     Trace-Message "STDERR ---------------------------"
1220     Get-Content $errLogFile | Write-Host
1221 
1222     Trace-Message "STDOUT ---------------------------"
1223     Get-Content $outLogFile | Write-Host
1224 
1225     if ($ex -eq $null)
1226     {
1227         Trace-Warning -Message "The process $pid returned a null or invalid exit code value. Assuming and returning 0"
1228         return 0
1229     }
1230 
1231     return $ex
1232 }
1233 
1234 #####################################################################################################
1235 # Wait-ForProcess
1236 #####################################################################################################
1237 
1238 <#
1239     .SYNOPSIS
1240         Waits for a previously started process until it exits or there is a timeout.
1241     .DESCRIPTION
1242         Waits for a started process until it exits or a certain amount of time has elapsed.
1243     .PARAMETER Process
1244         The [System.Process] project to wait for.
1245     .PARAMETER Minutes
1246         The amount of time to wait for in minutes.
1247     .PARAMETER Monitor
1248         An optional script block that will be run after each wait interval.
1249 #>
Wait-ForProcessnull1250 function Wait-ForProcess
1251 {
1252     param
1253     (
1254         [Parameter(Mandatory=$true)]
1255         [ValidateNotNull()]
1256         [System.Diagnostics.Process]$Process,
1257 
1258         [Parameter(Mandatory=$true)]
1259         [ValidateRange(1, [int]::MaxValue)]
1260         [int]$Minutes = 10,
1261 
1262         [Parameter(Mandatory=$false)]
1263         [ScriptBlock]$Monitor
1264     )
1265 
1266     $waitTime = $Minutes
1267 
1268     $handle = $process.Handle
1269     $pid = $Process.Id
1270 
1271     while ($waitTime -gt 0)
1272     {
1273         Trace-Message -Message "Waiting for process with ID $pid to exit in $waitTime minutes."
1274 
1275         if ($Process.HasExited)
1276         {
1277             $ex = $Process.ExitCode
1278             Trace-Message "Process with ID $pid has already exited with exit code $ex"
1279             return $true
1280         }
1281 
1282         Sleep -Seconds 60
1283 
1284         if ($Monitor)
1285         {
1286             try
1287             {
1288                 Trace-Message "Invoking monitor script: $Monitor"
1289                 $Monitor.Invoke()
1290             }
1291             catch
1292             {
1293                 Trace-Warning "Exception occurred invoking monitoring script"
1294                 $_.Exception | Format-List
1295             }
1296         }
1297 
1298         --$waitTime
1299     }
1300 
1301     return $false
1302 }
1303 
1304 #####################################################################################################
1305 # Trace-Message
1306 #####################################################################################################
1307 
1308 <#
1309     .SYNOPSIS
1310         Logs an informational message to the console.
1311     .DESCRIPTION
1312         Writes a message to the console with the current timestamp and an information tag.
1313     .PARAMETER Message
1314         The message to write.
1315 #>
Trace-Messagenull1316 function Trace-Message
1317 {
1318     param
1319     (
1320         [Parameter(Mandatory=$true, Position=0)]
1321         [ValidateNotNullOrEmpty()]
1322         [string]$Message
1323     )
1324 
1325     $Message = $Message -replace "##vso", "__VSO_DISALLOWED"
1326     $timestamp = Get-Date
1327     Write-Host "[INFO] [$timestamp] $Message"
1328 }
1329 
1330 #####################################################################################################
1331 # Trace-Warning
1332 #####################################################################################################
1333 
1334 <#
1335     .SYNOPSIS
1336         Logs a warning message to the console.
1337     .DESCRIPTION
1338         Writes a warning to the console with the current timestamp and a warning tag.
1339     .PARAMETER Message
1340         The warning to write.
1341 #>
Trace-Warning()1342 function Trace-Warning
1343 {
1344     param
1345     (
1346         [Parameter(Mandatory=$true, Position=0)]
1347         [ValidateNotNullOrEmpty()]
1348         [string]$Message
1349     )
1350 
1351     $timestamp = Get-Date
1352     $Message = $Message -replace "##vso", "__VSO_DISALLOWED"
1353     Write-Host "[WARN] [$timestamp] $Message" -ForegroundColor Yellow
1354     Write-Host "##vso[task.logissue type=warning]$Message"
1355 }
1356 
1357 #####################################################################################################
1358 # Trace-Error
1359 #####################################################################################################
1360 
1361 <#
1362     .SYNOPSIS
1363         Logs an error message to the console.
1364     .DESCRIPTION
1365         Writes an error to the console with the current timestamp and an error tag.
1366     .PARAMETER Message
1367         The error to write.
1368 #>
Trace-Errornull1369 function Trace-Error
1370 {
1371     param
1372     (
1373         [Parameter(Mandatory=$true, Position=0)]
1374         [ValidateNotNullOrEmpty()]
1375         [string]$Message
1376     )
1377 
1378     $timestamp = Get-Date
1379     $Message = $Message -replace "##vso", "__VSO_DISALLOWED"
1380     Write-Host "[ERROR] [$timestamp] $Message" -ForegroundColor Red
1381     Write-Host "##vso[task.logissue type=error]$Message"
1382 }
1383 
1384 #####################################################################################################
1385 # Expand-ArchiveWith7Zip
1386 #####################################################################################################
1387 
1388 <#
1389     .SYNOPSIS
1390         Uses 7-Zip to expand an archive instead of the standard Expand-Archive cmdlet.
1391     .DESCRIPTION
1392         The Expand-Archive cmdlet is slow compared to using 7-Zip directly. This function
1393         assumes that 7-Zip is installed at C:\7-Zip.
1394     .PARAMETER -Source
1395         The path to the archive file.
1396     .PARAMETER -Destination
1397         The folder to expand into.
1398     .PARAMETER ToolPath
1399         The path to where the 7z.exe tool is available.
1400 #>
Expand-ArchiveWith7Zipnull1401 function Expand-ArchiveWith7Zip
1402 {
1403     param
1404     (
1405         [Parameter(Mandatory=$true)]
1406         [ValidateNotNullOrEmpty()]
1407         [string]$Source,
1408 
1409         [Parameter(Mandatory=$false)]
1410         [ValidateNotNullOrEmpty()]
1411         [string]$Destination,
1412 
1413         [Parameter(Mandatory=$false)]
1414         [ValidateNotNullOrEmpty()]
1415         [string]$ToolPath = "C:\7-Zip\7z.exe",
1416 
1417         [Parameter(Mandatory=$false)]
1418         [switch]$IgnoreFailures=$false
1419     )
1420 
1421     Write-Host $Separator
1422 
1423     if (-not $ToolPath)
1424     {
1425         throw "CDPXERROR: The 7-Zip tool was not found at $ToolPath."
1426     }
1427 
1428     if (-not (Test-Path $Source))
1429     {
1430         throw "CDPXERROR: The specified archive file $Source could not be found."
1431     }
1432 
1433     if (-not $Destination)
1434     {
1435         $sourceDir = [System.IO.Path]::GetDirectoryName($Source);
1436         $Destination = $sourceDir
1437 
1438         Trace-Message "No destination was specified so the default location $Destination was chosen."
1439     }
1440 
1441     Trace-Message "Uncompressing archive $Source into folder $Destination using 7-Zip at $ToolPath"
1442 
1443     Install-FromEXE -Path $ToolPath -Arguments "x -aoa -y `"$Source`" -o`"$Destination`"" -IgnoreFailures:$IgnoreFailures
1444 
1445     Trace-Message "Successfully uncompressed archive at $Source into $Destination"
1446     Write-Host $Separator
1447 }
1448 
1449 #####################################################################################################
1450 # Get-BlobPackageFromBase
1451 #####################################################################################################
1452 
1453 <#
1454     .SYNOPSIS
1455         Uses AzCopy to download a blob package from blob store.
1456     .DESCRIPTION
1457         Some very large content such as Visual Studio offline installer files are stored in
1458         a CDPX hosted blob store. This method fetches the contents of such blob packages
1459         using AzCopy.
1460 #>
Get-BlobPackageFromBase()1461 function Get-BlobPackageFromBase
1462 {
1463     param
1464     (
1465         [Parameter(Mandatory=$false)]
1466         [ValidateNotNullOrEmpty()]
1467         [string]$ContainerName,
1468 
1469         [Parameter(Mandatory=$true)]
1470         [ValidateNotNullOrEmpty()]
1471         [string]$nodePath,
1472 
1473         [Parameter(Mandatory=$false)]
1474         [ValidateNotNullOrEmpty()]
1475         [string]$downloadPath="C:\Downloads"
1476 
1477     )
1478 
1479     Write-Host $Separator
1480 
1481     [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
1482 
1483     $Env:AZCOPY_LOG_LOCATION = $Env:TEMP
1484 
1485     $url = Get-BlobPackageBaseUrl -ContainerName $ContainerName
1486 
1487     Trace-Message "Invoking AzCopy CLI to download package $Name version $Version to $Path from $url"
1488 
1489     $Arguments = @("copy", $url, $downloadPath, "--recursive", "--include-path $nodePath", "--include-pattern *")
1490 
1491     Run-ExternalProcessWithWaitAndKill -Path "C:\AzCopy\azcopy.exe" -Arguments $Arguments -Minutes 30
1492 
1493     Trace-Message "Finished downloading blob package"
1494 
1495     Write-Host $Separator
1496 
1497     return $Path
1498 }
1499 
1500 #####################################################################################################
1501 # Get-BlobPackageFromEdge
1502 #####################################################################################################
1503 
1504 <#
1505     .SYNOPSIS
1506         Uses a HTTP/S request to download a blob package from CDN.
1507     .DESCRIPTION
1508         Some content such as third party OSS or free software are hosted on a CDPX hosted
1509         blob store which is replicated to a CDN. This function fetches the blob package from
1510         the CDN.
1511 #>
Get-BlobPackageFromEdgenull1512 function Get-BlobPackageFromEdge
1513 {
1514     param
1515     (
1516         [Parameter(Mandatory=$true)]
1517         [ValidateNotNullOrEmpty()]
1518         [string]$Name,
1519 
1520         [Parameter(Mandatory=$true)]
1521         [ValidateNotNullOrEmpty()]
1522         [string]$Version,
1523 
1524         [Parameter(Mandatory=$true)]
1525         [ValidateNotNullOrEmpty()]
1526         [string]$FileName,
1527 
1528         [Parameter(Mandatory=$false)]
1529         [ValidateNotNullOrEmpty()]
1530         [string]$Path="C:\Downloads",
1531 
1532         [Parameter(Mandatory=$false)]
1533         [ValidateNotNullOrEmpty()]
1534         [string]$ContainerName
1535     )
1536 
1537     Write-Host $Separator
1538 
1539     [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
1540 
1541     $url = Get-BlobPackageEdgeUrl -Name $Name -Version $Version -Container $ContainerName
1542 
1543     Trace-Message "Downloading blob package $Name and $Version from $url"
1544 
1545     $path = Get-File -Url $url -FileName $FileName
1546 
1547     Trace-Message "Finished downloading blob package to $FileName"
1548 
1549     Write-Host $Separator
1550 
1551     return $path
1552 }
1553 
1554 #####################################################################################################
1555 # Enum HostEnvironment
1556 #####################################################################################################
1557 
1558 enum HostEnvironment
1559 {
1560     Dev
1561     Test
1562     Prod
1563 }
1564 
1565 #####################################################################################################
1566 # Get-HostEnvironment
1567 #####################################################################################################
1568 
1569 <#
1570     .SYNOPSIS
1571         Uses some heuristics about the underlying host to determine what kind of environment the
1572         host is in.
1573     .DESCRIPTION
1574         Leverages CDPX host naming conventions to determine if a host is a test or production host. If
1575         neither is true, this function always assumes that the host is a developer box.
1576     .OUTPUTS
1577         An instance of the enumeration HostEnvironment.
1578 #>
Get-HostEnvironmentnull1579 function Get-HostEnvironment
1580 {
1581     $ctrHost = $Env:TEMP_CONTAINER_HOST_NAME
1582 
1583     if ($ctrHost)
1584     {
1585         if ($ctrHost.StartsWith("XWT"))
1586         {
1587             Trace-Message -Message "Running on CDPX test host."
1588             return [HostEnvironment]::Test
1589         }
1590         elseif ($ctrHost.StartsWith("XWP"))
1591         {
1592             Trace-Message -Message "Running on CDPX prod host."
1593             return [HostEnvironment]::Prod
1594         }
1595     }
1596 
1597     Trace-Message "Unsure what kind of CDPX environment underlying host `"$ctrHost`" is in. Assuming development box."
1598     return [HostEnvironment]::Dev
1599 }
1600 
1601 #####################################################################################################
1602 # Get-BlobContainerName
1603 #####################################################################################################
1604 
1605 <#
1606     .SYNOPSIS
1607         Returns the container name to use for blob packages.
1608     .DESCRIPTION
1609         Returns a OS specific container name within which blob packages specific to that OS are
1610         stored.
1611     .OUTPUTS
1612         Returns a lower case string that is the container name within the blob store in which
1613         blob packages are stored.
1614 #>
Get-BlobContainerNamenull1615 function Get-BlobContainerName
1616 {
1617     if ($Env:os -eq "Windows_NT")
1618     {
1619         return "windows"
1620     }
1621     elseif ($Env:OS -eq "Linux")
1622     {
1623         return "linux"
1624     }
1625 
1626     throw "CDPXERROR: Only supported operating systems are Windows and Linux. Unknown OS $($Env:OS)"
1627 }
1628 
1629 #####################################################################################################
1630 # Get-BlobAccountName
1631 #####################################################################################################
1632 
1633 <#
1634     .SYNOPSIS
1635         Returns the base storage account in which blob packages are stored.
1636     .DESCRIPTION
1637         Returns an environment specific base storage account in which blob packages are stored.
1638     .OUTPUTS
1639         Returns a string that is an environment specific value for the blob storage account
1640         in which blob packages are stored.
1641 #>
Get-BlobAccountName()1642 function Get-BlobAccountName
1643 {
1644     $hostEnv = Get-HostEnvironment
1645     $hostEnvStr = $hostEnv.ToString().ToLowerInvariant()
1646     $prefix = "cxswdist"
1647     $accountName = $prefix + $hostEnvStr
1648 
1649     Trace-Warning "Currently overriding blob storage account to cxswdisttest for all host environments."
1650     return "cxswdisttest"
1651 }
1652 
1653 
1654 #####################################################################################################
1655 # Get-PackageFullName
1656 #####################################################################################################
1657 
1658 <#
1659     .SYNOPSIS
1660         Gets the full name of a blob or universal package that can be downloaded by the functions
1661         in this module.
1662     .DESCRIPTION
1663         Given a package name and a version, returns a full name to the package for use with
1664         AzCopy or Az UPack CLI. The returned version is packagename-packageversion in lower case.
1665     .OUTPUTS
1666         The name of the package to use with blob store.
1667 #>
1668 
Get-PackageFullName()1669 function Get-PackageFullName
1670 {
1671     param
1672     (
1673         [Parameter(Mandatory=$true)]
1674         [ValidateNotNullOrEmpty()]
1675         [string]$Name,
1676 
1677         [Parameter(Mandatory=$true)]
1678         [ValidateNotNullOrEmpty()]
1679         [string]$Version
1680     )
1681 
1682     $packageFullName = -join($Name, "-", $Version)
1683     return $packageFullName.ToLowerInvariant()
1684 }
1685 
1686 #####################################################################################################
1687 # Get-LatestInstalledNetFrameworkVersion
1688 #####################################################################################################
1689 
1690 <#
1691     .SYNOPSIS
1692         Gets the latest installed version of the .NET Framework.
1693     .DESCRIPTION
1694         Retrieves information from the registry based on the documentation at this link:
1695         https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed#net_b.
1696         Returns the entire child object from the registry.
1697     .OUTPUTS
1698         The child registry entry for the .NET framework installation.
1699 #>
Get-LatestInstalledNetFrameworkVersion()1700 function Get-LatestInstalledNetFrameworkVersion
1701 {
1702     Trace-Message -Message "Retrieving latest installed .NET Framework version from registry entry: HKLM:`\SOFTWARE`\Microsoft`\NET Framework Setup`\NDP`\v4`\Full"
1703 
1704     $item = Get-ChildItem HKLM:"\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"
1705 
1706     return $item
1707 }
1708 
1709 #####################################################################################################
1710 # Run-VisualStudioInstallerProcessMonitor
1711 #####################################################################################################
1712 
1713 <#
1714     .SYNOPSIS
1715         Monitors progress of Visual Studio installation.
1716     .DESCRIPTION
1717         Checks if VS installer processes named vs_installer or vs_enteprise are still running.
1718         Returns true if processes with those names were found. Otherwise returns false. In addition,
1719         lists all dd_setup* log files found in $Env:TEMP where VS installers traditionally place
1720         log files. Finally, if any error log files are present, prints out the contents of those
1721         files.
1722     .OUTPUTS
1723         True if VS installer or bootstrapper processes are still running. Otherwise false.
1724 #>
Run-VisualStudioInstallerProcessMonitornull1725 function Run-VisualStudioInstallerProcessMonitor
1726 {
1727     Write-Host $Separator
1728 
1729     $processes = Get-Process
1730     $numTotalProcesses = 0
1731     $numVSIProcesses = 0
1732 
1733     $processes | ForEach-Object {
1734 
1735         $process = $_
1736         $handle = $process.Handle
1737         $pid = $process.Id
1738         $ppath = $process.Path
1739 
1740         if ($process.Name.StartsWith("vs_installer") -or
1741             $process.Name.StartsWith("vs_enterprise"))
1742         {
1743             $numVSIProcesses++
1744 
1745             Trace-Message -Message "Found VS Installer process with PID $pid launched from $ppath"
1746         }
1747 
1748         ++$numTotalProcesses
1749     }
1750 
1751     Trace-Message "Total processes: $numTotalProcesses. VS Installer processes: $numVSIProcesses"
1752 
1753     $setupLogs = Get-ChildItem $Env:TEMP -Filter "dd_setup*.log"
1754     $setupLogs | Write-Host
1755 
1756     $setupLogs | ForEach-Object {
1757 
1758         $setupLog = $_
1759         $setupLogPath = $setupLog.FullName
1760 
1761         if ($setupLog.Name.Contains("errors"))
1762         {
1763             Trace-Message "Contents of VS installer error log: $setupLogPath"
1764             Get-Content -Path $setupLogPath | Write-Host
1765         }
1766     }
1767 
1768     Write-Host $Separator
1769 
1770     if ($numVSIProcesses -gt 0)
1771     {
1772         return $true
1773     }
1774 
1775     return $false
1776 }
1777 
1778 #####################################################################################################
1779 # Monitor-VisualStudioInstallation
1780 #####################################################################################################
1781 
1782 <#
1783 #>
Monitor-VisualStudioInstallation()1784 function Monitor-VisualStudioInstallation
1785 {
1786     param
1787     (
1788         [Parameter(Mandatory=$true)]
1789         [ValidateRange(1, [int]::MaxValue)]
1790         [int]$WaitBefore,
1791 
1792         [Parameter(Mandatory=$true)]
1793         [ValidateRange(1, [int]::MaxValue)]
1794         [int]$WaitAfter
1795     )
1796 
1797     $minutes = $WaitBefore
1798 
1799     while ($minutes -gt 0)
1800     {
1801         Trace-Message -Message "WAITING for VS installer kickoff."
1802 
1803         Run-VisualStudioInstallerProcessMonitor
1804 
1805         Sleep -Seconds 60
1806 
1807         --$minutes
1808     }
1809 
1810     $minutes = $WaitAfter
1811 
1812     while ($minutes -gt 0)
1813     {
1814         Trace-Message -Message "WAITING for VS installer kickoff."
1815 
1816         $ex = Run-VisualStudioInstallerProcessMonitor
1817 
1818         if (-not $ex)
1819         {
1820             Trace-Message -Message "DONE Looks like VS installer processes are no longer running."
1821             break
1822         }
1823 
1824         Sleep -Seconds 120
1825 
1826         --$minutes
1827     }
1828 }
1829