#
.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!"
}