Can I get detailed exception stacktrace in PowerShell?

前端 未结 12 1116
暗喜
暗喜 2020-12-02 12:23

Runing such script:

 1: function foo()
 2: {
 3:    bar
 4: }
 5: 
 6: function bar()
 7: {
 8:     throw \"test\"
 9: }
10: 
11: foo

I see

相关标签:
12条回答
  • 2020-12-02 12:45

    There are cases where PowerShell doesn't seem to keep a backtrace, like calling a method or calling a function with .Invoke(). For that, Set-PSDebug -Trace 2 may come in handy. It will print every executed line of the running script.

    Try flipping # on (1) and (2) and running WrapStackTraceLog({ function f{ 1/0 } ; & f }) # let's divide by zero

    Function WrapStackTraceLog($func) {
        try {
            # return $func.Invoke($args)  # (1)
            return (& $func $args)  # (2)
        } catch {
            Write-Host ('=' * 70)
            Write-Host $_.Exception.Message
            Write-Host ('-' * 70)
            Write-Host $_.ScriptStackTrace
            Write-Host ('-' * 70)
            Write-Host "$StackTrace"
            Write-Host ('=' * 70)
        }
    }
    

    Branch (1) exception caught:

    Exception calling "Invoke" with "1" argument(s): "Attempted to divide by zero."
    

    Branch (2) is more informative:

    at f, <No file>: line 1
    at <ScriptBlock>, <No file>: line 1
    at global:WrapStackTraceLog, <No file>: line 4
    at <ScriptBlock>, <No file>: line 1
    

    But, you can still trace your Invokes with tracing on, branch (1):

    DEBUG:     ! CALL function 'f'
    DEBUG:    1+ WrapStackTraceLog({ function f{  >>>> 1/0 } ; & f })
    DEBUG:    6+          >>>> Write-Host ('=' * 70)
    ======================================================================
    DEBUG:    7+          >>>> Write-Host $_.Exception.Message
    Exception calling "Invoke" with "1" argument(s): "Attempted to divide by zero."
    
    0 讨论(0)
  • 2020-12-02 12:47

    You can not get a stack trace from exceptions of the PowerShell code of scripts, only from .NET objects. To do that, you will need to get the Exception object like one of these:

    $Error[0].Exception.StackTrace
    $Error[0].Exception.InnerException.StackTrace
    $Error[0].StackTrace
    
    0 讨论(0)
  • 2020-12-02 12:49

    Powershell 3.0 adds a ScriptStackTrace property to the ErrorRecord object. I use this function for error reporting:

    function Write-Callstack([System.Management.Automation.ErrorRecord]$ErrorRecord=$null, [int]$Skip=1)
    {
        Write-Host # blank line
        if ($ErrorRecord)
        {
            Write-Host -ForegroundColor Red "$ErrorRecord $($ErrorRecord.InvocationInfo.PositionMessage)"
    
            if ($ErrorRecord.Exception)
            {
                Write-Host -ForegroundColor Red $ErrorRecord.Exception
            }
    
            if ((Get-Member -InputObject $ErrorRecord -Name ScriptStackTrace) -ne $null)
            {
                #PS 3.0 has a stack trace on the ErrorRecord; if we have it, use it & skip the manual stack trace below
                Write-Host -ForegroundColor Red $ErrorRecord.ScriptStackTrace
                return
            }
        }
    
        Get-PSCallStack | Select -Skip $Skip | % {
            Write-Host -ForegroundColor Yellow -NoNewLine "! "
            Write-Host -ForegroundColor Red $_.Command $_.Location $(if ($_.Arguments.Length -le 80) { $_.Arguments })
        }
    }
    

    The Skip parameter lets me leave Write-Callstack or any number of error-handling stack frames out of the Get-PSCallstack listing.

    Note that if called from a catch block, Get-PSCallstack will miss any frames between the throw site and the catch block. Hence I prefer the PS 3.0 method even though we have fewer details per frame.

    0 讨论(0)
  • 2020-12-02 12:50

    There is the automatic variable $StackTrace but it seems to be a little more specific to internal PS details than actually caring about your script, so that won't be of much help.

    There is also Get-PSCallStack but that's gone as soon as you hit the exception, unfortunately. You could, however, put a Get-PSCallStack before every throw in your script. That way you get a stack trace immediately before hitting an exception.

    I think one could script such functionality by using the debugging and tracing features of Powershell but I doubt it'd be easy.

    0 讨论(0)
  • 2020-12-02 12:51

    Maybe I've misunderstood something, but my issue here is that I've not been seeing powershell script stack traces for inner exceptions.

    In the end I ended up searching $Global:Error for an exception's Error Eecord object to retrieve the script stack trace.

    <#
    .SYNOPSIS
       Expands all inner exceptions and provides Script Stack Traces where available by mapping Exceptions to ErrorRecords
    
    .NOTES
       Aggregate exceptions aren't full supported, and their child exceptions won't be traversed like regular inner exceptions
       
    #>
    function Get-InnerErrors ([Parameter(ValueFrompipeline)] $ErrorObject=$Global:Error[0])
    {
       # Get the first exception object from the input error
       $ex = $null
       if( $ErrorObject -is [System.Management.Automation.ErrorRecord] ){
           $ex = $ErrorObject.Exception 
       }
       elseif( $ErrorObject -is [System.Exception] ){
           $ex = $ErrorObject
       } 
       else
       {
           throw "Unexpected error type for Get-InnerErrors: $($ErrorObject.GetType()):`n $ErrorObject"
       }
    
       Write-Debug "Walking inner exceptions from exception"
       for ($i = 0; $ex; $i++, ($ex = $ex.InnerException))
       {  
           $ErrorRecord = $null
    
           if( $ex -is [System.Management.Automation.IContainsErrorRecord] ){ 
    
               Write-Debug "Inner Exception $i : Skipping search for ErrorRecord in `$Global:Error, exception type implements IContainsErrorRecord" 
               $ErrorRecord = ([System.Management.Automation.IContainsErrorRecord]$ex).ErrorRecord
           }
           else {
    
               # Find ErrorRecord for exception by mapping exception to ErrorRecrods in $Global:Error
    
               ForEach( $err in $Global:Error ){# Need to use Global scope when referring from a module
                   if( $err -is [System.Management.Automation.ErrorRecord] ){
                      if( $err.Exception -eq $ex ){
                           $ErrorRecord = $err 
                           Write-Debug "Inner Exception $i : Found ErrorRecord in `$Global:Error" 
                           break
                       }
                   } 
                   elseif( $err -is [System.Management.Automation.IContainsErrorRecord] ) {
                       if( $err -eq $ex -or $err.ErrorRecord.Exception -eq $ex ){
                           $ErrorRecord = $err.ErrorRecord
                           Write-Debug "Inner Exception $i : Found ErrorRecord in `$Global:Error" 
                           break
                       }
                   } 
                   else {
                       Write-Warning "Unexpected type in `$Global:Error[]. Expected `$Global:Error to always be an ErrorRecrod OR exception implementing IContainsErrorRecord but found type: $($err.GetType())"
                   }
               }
           }
    
           if( -not($ErrorRecord) ){
               Write-Debug "Inner Exception $i : No ErrorRecord could be found for exception of type: $($ex.GetType())" 
           }
           
          
           # Return details as custom object
    
           [PSCustomObject] @{
               ExceptionDepth      = $i
               Message             = $ex.Message
               ScriptStackTrace    = $ErrorRecord.ScriptStackTrace  # Note ErrorRecord will be null if exception was not from Powershell
               ExceptionType       = $ex.GetType().FullName
               ExceptionStackTrace = $ex.StackTrace
           }
       }
    }
    

    Example Usage:

    function Test-SqlConnection
    {
        $ConnectionString = "ThisConnectionStringWillFail"
        try{
            $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $ConnectionString;
            $sqlConnection.Open();
        }
        catch
        {
            throw [System.Exception]::new("Sql connection failed with connection string: '$ConnectionString'", $_.Exception)
        }
        finally
        {
            if($sqlConnection){
                $sqlConnection.Close();
            }
        }
    }
    
    
    try{
        Test-SqlConnection
    }
    catch {
        Get-InnerErrors $_
    }
    

    Example output:

    
    
    ExceptionDepth      : 0
    Message             : Sql connection failed with connection string: 'ThisConnectionStringWillFail'
    ScriptStackTrace    : at Test-SqlConnection, <No file>: line 11
                          at <ScriptBlock>, <No file>: line 23
    ExceptionType       : System.Exception
    ExceptionStackTrace : 
    
    ExceptionDepth      : 1
    Message             : Exception calling ".ctor" with "1" argument(s): "Format of the initialization string does not conform to specification starting at index 0."
    ScriptStackTrace    : 
    ExceptionType       : System.Management.Automation.MethodInvocationException
    ExceptionStackTrace :    at System.Management.Automation.DotNetAdapter.AuxiliaryConstructorInvoke(MethodInformation methodInformation, Object[] arguments, Object[] originalArguments)
                             at System.Management.Automation.DotNetAdapter.ConstructorInvokeDotNet(Type type, ConstructorInfo[] constructors, Object[] arguments)
                             at Microsoft.PowerShell.Commands.NewObjectCommand.CallConstructor(Type type, ConstructorInfo[] constructors, Object[] args)
    
    ExceptionDepth      : 2
    Message             : Format of the initialization string does not conform to specification starting at index 0.
    ScriptStackTrace    : 
    ExceptionType       : System.ArgumentException
    ExceptionStackTrace :    at System.Data.Common.DbConnectionOptions.GetKeyValuePair(String connectionString, Int32 currentPosition, StringBuilder buffer, Boolean useOdbcRules, String& keyname, String& 
                          keyvalue)
                             at System.Data.Common.DbConnectionOptions.ParseInternal(Hashtable parsetable, String connectionString, Boolean buildChain, Hashtable synonyms, Boolean firstKey)
                             at System.Data.Common.DbConnectionOptions..ctor(String connectionString, Hashtable synonyms, Boolean useOdbcRules)
                             at System.Data.SqlClient.SqlConnectionString..ctor(String connectionString)
                             at System.Data.SqlClient.SqlConnectionFactory.CreateConnectionOptions(String connectionString, DbConnectionOptions previous)
                             at System.Data.ProviderBase.DbConnectionFactory.GetConnectionPoolGroup(DbConnectionPoolKey key, DbConnectionPoolGroupOptions poolOptions, DbConnectionOptions& userConnectionOptions)
                             at System.Data.SqlClient.SqlConnection.ConnectionString_Set(DbConnectionPoolKey key)
                             at System.Data.SqlClient.SqlConnection.set_ConnectionString(String value)
                             at System.Data.SqlClient.SqlConnection..ctor(String connectionString, SqlCredential credential)
    
    0 讨论(0)
  • 2020-12-02 13:00

    Here's a way: Tracing the script stack

    The core of it is this code:

        1..100 | %{ $inv = &{ gv -sc $_ myinvocation }
    
    0 讨论(0)
提交回复
热议问题