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