<# .SYNOPSIS This script runs an executable and collects and logs or process dumps as necessary. .PARAMETER Path The path to the executable. .PARAMETER Arguments The arguments to pass to the executable. .PARAMETER KeepOutputOnSuccess Don't discard console output or logs on success. .PARAMETER GenerateXmlResults Generates an xml Test report for the run. .PARAMETER Debugger Attaches the debugger to the process. .PARAMETER InitialBreak Debugger starts broken into the process to allow setting breakpoints, etc. .PARAMETER LogProfile The name of the profile to use for log collection. .PARAMETER CompressOutput Compresses the output files generated for failed test cases. .PARAMETER ShowOutput Prints the standard output/error to the console. .Parameter EnableAppVerifier Enables all basic Application Verifier checks on the executable. .Parameter CodeCoverage Collect code coverage for the binary being run. .Parameter AZP Runs in Azure Pipelines mode. .Parameter GHA Runs in Github Workflow mode. #> param ( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $false)] [string]$Arguments = "", [Parameter(Mandatory = $false)] [switch]$KeepOutputOnSuccess = $false, [Parameter(Mandatory = $false)] [switch]$GenerateXmlResults = $false, [Parameter(Mandatory = $false)] [switch]$Debugger = $false, [Parameter(Mandatory = $false)] [switch]$InitialBreak = $false, [Parameter(Mandatory = $false)] [ValidateSet("None", "Basic.Light", "Basic.Verbose", "Full.Light", "Full.Verbose", "SpinQuic.Light", "SpinQuicWarnings.Light")] [string]$LogProfile = "None", [Parameter(Mandatory = $false)] [switch]$CompressOutput = $false, [Parameter(Mandatory = $false)] [switch]$ShowOutput = $false, [Parameter(Mandatory = $false)] [switch]$EnableAppVerifier = $false, [Parameter(Mandatory = $false)] [switch]$CodeCoverage = $false, [Parameter(Mandatory = $false)] [switch]$AZP = $false, [Parameter(Mandatory = $false)] [switch]$GHA = $false, [Parameter(Mandatory = $false)] [string]$ExtraArtifactDir = "" ) Set-StrictMode -Version 'Latest' $PSDefaultParameterValues['*:ErrorAction'] = 'Stop' $global:ExeFailed = $false function Log($msg) { Write-Host "[$(Get-Date)] $msg" } function LogWrn($msg) { if ($AZP) { Write-Host "##vso[task.LogIssue type=warning;][$(Get-Date)] $msg" } elseif ($GHA) { Write-Host "::warning::[$(Get-Date)] $msg" } else { Write-Warning "[$(Get-Date)] $msg" } } function LogErr($msg) { if ($AZP ) { Write-Host "##vso[task.LogIssue type=error;][$(Get-Date)] $msg" } elseif ($GHA) { Write-Host "::error::[$(Get-Date)] $msg" } else { Write-Warning "[$(Get-Date)] $msg" } } function Test-Administrator { $user = [Security.Principal.WindowsIdentity]::GetCurrent(); (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } # Make sure the executable is present. if (!(Test-Path $Path)) { Write-Error "$($Path) does not exist!" } # Validate the code coverage switch if ($CodeCoverage) { if (!$IsWindows) { Write-Error "-CodeCoverage switch only supported on Windows"; } if ($Debugger) { Write-Error "-CodeCoverage switch is not supported with debugging"; } if (!(Test-Path "C:\Program Files\OpenCppCoverage\OpenCppCoverage.exe")) { Write-Error "Code coverage tools are not installed"; } } # Root directory of the project. $RootDir = Split-Path $PSScriptRoot -Parent # Script for controlling loggings. $LogScript = Join-Path $RootDir "scripts" "log.ps1" # Executable name. $ExeName = Split-Path $Path -Leaf $CoverageName = "$(Split-Path $Path -LeafBase).cov" $ExeLogFolder = $ExeName if (![string]::IsNullOrWhiteSpace($ExtraArtifactDir)) { $ExeLogFolder += "_$ExtraArtifactDir" } # Path for log files. $LogDir = Join-Path $RootDir "artifacts" "logs" $ExeLogFolder (Get-Date -UFormat "%m.%d.%Y.%T").Replace(':','.') New-Item -Path $LogDir -ItemType Directory -Force | Out-Null # Folder for coverage files $CoverageDir = $null if ($CodeCoverage) { $CoverageDir = Join-Path $RootDir "artifacts" "coverage" New-Item -Path $CoverageDir -ItemType Directory -Force | Out-Null } # XML for creating a failure result data. $FailXmlText = @" "@ # XML for creating a success result data. $SuccessXmlText = @" "@ # Path to the WER registry key used for collecting dumps. $WerDumpRegPath = "HKLM:\Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\$ExeName" # Asynchronously starts the executable with the given arguments. function Start-Executable { $Now = (Get-Date -UFormat "%Y-%m-%dT%T") if ($LogProfile -ne "None" -and !$CodeCoverage) { & $LogScript -Start -Profile $LogProfile | Out-Null } $pinfo = New-Object System.Diagnostics.ProcessStartInfo if ($IsWindows) { if ($EnableAppVerifier) { where.exe appverif.exe | Out-Null if ($LastExitCode -eq 0) { appverif.exe /verify $Path | Out-Null } else { Write-Warning "Application Verifier not installed!" $EnableAppVerifier = $false; } } if ($Debugger) { if (Get-Command "windbgx.exe" -ErrorAction SilentlyContinue) { $pinfo.FileName = "windbgx.exe" } else { $pinfo.FileName = "windbg.exe" } if ($InitialBreak) { $pinfo.Arguments = "-G $($Path) $($Arguments)" } else { $pinfo.Arguments = "-g -G $($Path) $($Arguments)" } } elseif ($CodeCoverage) { $pinfo.FileName = "C:\Program Files\OpenCppCoverage\OpenCppCoverage.exe" $pinfo.Arguments = "--modules=$(Split-Path $Path -Parent) --cover_children --sources src\core --excluded_sources unittest --working_dir $($LogDir) --export_type binary:$(Join-Path $CoverageDir $CoverageName) -- $($Path) $($Arguments)" $pinfo.WorkingDirectory = $LogDir } else { $pinfo.FileName = $Path $pinfo.Arguments = $Arguments # Enable WER dump collection. New-ItemProperty -Path $WerDumpRegPath -Name DumpType -PropertyType DWord -Value 2 -Force | Out-Null New-ItemProperty -Path $WerDumpRegPath -Name DumpFolder -PropertyType ExpandString -Value $LogDir -Force | Out-Null } } else { if ($Debugger) { $pinfo.FileName = "gdb" if ($InitialBreak) { $pinfo.Arguments = "--args $($Path) $($Arguments)" } else { $pinfo.Arguments = "-ex=r --args $($Path) $($Arguments)" } } else { $pinfo.FileName = "bash" $pinfo.Arguments = "-c `"ulimit -c unlimited && LSAN_OPTIONS=report_objects=1 ASAN_OPTIONS=disable_coredump=0:abort_on_error=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 $($Path) $($Arguments) && echo Done`"" $pinfo.WorkingDirectory = $LogDir } } if (!$Debugger) { $pinfo.RedirectStandardOutput = $true $pinfo.RedirectStandardError = $true } $pinfo.UseShellExecute = $false $p = New-Object System.Diagnostics.Process $p.StartInfo = $pinfo $p.Start() | Out-Null [pscustomobject]@{ Timestamp = $Now Process = $p } } # Uses CDB.exe to print the crashing callstack in the dump file. function PrintDumpCallStack($DumpFile) { $env:_NT_SYMBOL_PATH = Split-Path $Path try { if ($env:BUILD_BUILDNUMBER -ne $null) { $env:PATH += ";c:\Program Files (x86)\Windows Kits\10\Debuggers\x64" } $Output = cdb.exe -z $File -c "kn;q" | Join-String -Separator "`n" $Output = ($Output | Select-String -Pattern " # Child-SP(?s).*quit:").Matches[0].Groups[0].Value Write-Host "==================================================================================" Write-Host " $(Split-Path $DumpFile -Leaf)" Write-Host "==================================================================================" $Output -replace "quit:", "==================================================================================" $Output | Out-File "$DumpFile.txt" } catch { # Silently fail } } function PrintLldbCoreCallStack($CoreFile) { try { $Output = lldb $Path -c $CoreFile -b -o "`"bt all`"" Write-Host "==================================================================================" Write-Host " $(Split-Path $CoreFile -Leaf)" Write-Host "==================================================================================" # Find line containing Current thread $Found = $false $LastThreadStart = 0 for ($i = 0; $i -lt $Output.Length; $i++) { if ($Output[$i] -like "*stop reason =*") { if ($Found) { break } $LastThreadStart = $i } if ($Output[$i] -like "*quic_bugcheck*") { $Found = $true for ($j = $LastThreadStart; $j -lt $i; $j++) { $Output[$j] } } if ($Found) { $Output[$i] } } if (!$Found) { $Output | Join-String -Separator "`n" } $Output | Join-String -Separator "`n" | Out-File "$CoreFile.txt" } catch { # Silently Fail } } function PrintGdbCoreCallStack($CoreFile) { try { $Output = gdb $Path $CoreFile -batch -ex "`"bt`"" -ex "`"quit`"" Write-Host "==================================================================================" Write-Host " $(Split-Path $CoreFile -Leaf)" Write-Host "==================================================================================" # Find line containing Current thread $Found = $false for ($i = 0; $i -lt $Output.Length; $i++) { if ($Output[$i] -like "*Current thread*") { $Found = $true } if ($Found) { $Output[$i] } } if (!$Found) { $Output | Join-String -Separator "`n" } $Output | Join-String -Separator "`n" | Out-File "$CoreFile.txt" } catch { # Silently Fail } } # Waits for the executable to finish and processes the results. function Wait-Executable($Exe) { $stdout = $null $stderr = $null $KeepOutput = $KeepOutputOnSuccess try { if ($CodeCoverage) { # When measuring code coverage, wait a little bit and then force a few # other code paths... Sleep -Seconds 5 if ($LogProfile -ne "None") { # Start logs to trigger the rundown code paths. & $LogScript -Start -Profile $LogProfile | Out-Null } # Set a registry key to trigger the settings code paths. reg.exe add HKLM\SYSTEM\CurrentControlSet\Services\MsQuic\Parameters\Apps\spinquic /v InitialWindowPackets /t REG_DWORD /d 20 /f | Out-Null Sleep -Seconds 1 reg.exe delete HKLM\SYSTEM\CurrentControlSet\Services\MsQuic\Parameters\Apps\spinquic /f } if (!$Debugger) { $stdout = $Exe.Process.StandardOutput.ReadToEnd() $stderr = $Exe.Process.StandardError.ReadToEnd() if (!$IsWindows) { $KeepOutput = $stderr.Contains("Aborted") } } else { if ($IsWindows -and $EnableAppVerifier) { # Wait 10 seconds for the debugger to launch the application Start-Sleep -Seconds 10 # Turn off App Verifier appverif.exe -disable * -for $Path } } $Exe.Process.WaitForExit() if ($Exe.Process.ExitCode -ne 0) { LogErr "Process had nonzero exit code: $($Exe.Process.ExitCode)" $KeepOutput = $true } $DumpFiles = (Get-ChildItem $LogDir) | Where-Object { $_.Extension -eq ".dmp" } if ($DumpFiles) { LogErr "Dump file(s) generated" foreach ($File in $DumpFiles) { PrintDumpCallStack($File) } $KeepOutput = $true } $CoreFiles = (Get-ChildItem $LogDir) | Where-Object { $_.Extension -eq ".core" } if ($CoreFiles) { LogWrn "Core file(s) generated" foreach ($File in $CoreFiles) { if ($IsMacOS) { PrintLldbCoreCallStack $File } else { PrintGdbCoreCallStack $File } } $KeepOutput = $true } } catch { LogWrn $_ LogErr "Treating exception as failure!" $KeepOutput = $true throw } finally { $XmlText = $null if ($KeepOutput) { $XmlText = $FailXmlText; $global:ExeFailed = $true } else { $XmlText = $SuccessXmlText; } if ($GenerateXmlResults) { $XmlText = $XmlText.Replace("ExeName", $ExeName) $XmlText = $XmlText.Replace("date", $Exe.Timestamp) # TODO - Update time fields. $XmlResults = [xml]($XmlText) $XmlResults.Save($LogDir + "-results.xml") | Out-Null } if ($ShowOutput) { if ($null -ne $stdout -and "" -ne $stdout) { Write-Host $stdout } if ($null -ne $stderr -and "" -ne $stderr) { Write-Host $stderr } } if ($CodeCoverage) { # Copy coverage log $LogName = "LastCoverageResults-$(Split-Path $Path -LeafBase).log" Copy-Item (Join-Path $LogDir "LastCoverageResults.log") (Join-Path $CoverageDir $LogName) -Force } if ($KeepOutput) { if ($LogProfile -ne "None") { if ($CodeCoverage) { & $LogScript -Cancel | Out-Null } else { & $LogScript -Stop -OutputPath (Join-Path $LogDir "quic") -Tmfpath (Join-Path $RootDir "artifacts" "tmf") } } if ($null -ne $stdout -and "" -ne $stdout) { $stdout > (Join-Path $LogDir "stdout.txt") } if ($null -ne $stderr -and "" -ne $stderr) { $stderr > (Join-Path $LogDir "stderr.txt") } if ($CompressOutput) { # Zip the output. CompressOutput-Archive -Path "$($LogDir)\*" -DestinationPath "$($LogDir).zip" | Out-Null Remove-Item $LogDir -Recurse -Force | Out-Null } Log "Output available at $($LogDir)" } else { if ($LogProfile -ne "None") { & $LogScript -Cancel | Out-Null } Remove-Item $LogDir -Recurse -Force | Out-Null } } } # Initialize WER dump registry key if necessary. if ($IsWindows -and !(Test-Path $WerDumpRegPath) -and (Test-Administrator)) { New-Item -Path $WerDumpRegPath -Force | Out-Null } # Start the executable, wait for it to complete and then generate any output. Wait-Executable (Start-Executable) if ($IsWindows) { # Cleanup the WER registry. Remove-Item -Path $WerDumpRegPath -Force | Out-Null } # Fail execution as necessary. if ($global:ExeFailed -and $AZP) { Write-Error "Run executable failed!" }