source

PowerShell에서 자세한 예외 스택 추적을 받을 수 있습니까?

factcode 2023. 9. 26. 22:35
반응형

PowerShell에서 자세한 예외 스택 추적을 받을 수 있습니까?

다음 스크립트 실행:

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

그렇군요.

test
At C:\test.ps1:8 char:10

대신 자세한 스택 트레이스를 받을 수 있을까요?

At bar() in C:\test.ps1:8
At foo() in C:\test.ps1:3 
At C:\test.ps1:11

PowerShell Team 블로그에는 Resolve-Error라는 기능이 있는데, 이 기능을 통해 모든 종류의 세부 정보를 얻을 수 있습니다.

$error는 PSSession에서 발생한 모든 오류의 배열입니다.이 기능은 마지막에 발생한 오류에 대한 세부 정보를 제공합니다.

function Resolve-Error ($ErrorRecord=$Error[0])
{
   $ErrorRecord | Format-List * -Force
   $ErrorRecord.InvocationInfo |Format-List *
   $Exception = $ErrorRecord.Exception
   for ($i = 0; $Exception; $i++, ($Exception = $Exception.InnerException))
   {   "$i" * 80
       $Exception |Format-List * -Force
   }
}

자동 변수가 있습니다.$StackTrace하지만 실제로 대본에 신경쓰는 것보다 내부 PS 세부사항에 좀 더 구체적인 것 같아서 큰 도움이 되지 않을 것 같습니다.

또한 있습니다.Get-PSCallStack불행하게도 예외를 두자마자 사라졌죠하지만, 당신은, 당신은,Get-PSCallStack대본을 쓰기 전에 말입니다.이렇게 하면 예외가 발생하기 직전에 스택 추적을 받을 수 있습니다.

파워셸의 디버깅과 추적 기능을 이용하면 그런 기능을 스크립팅할 수 있을 것 같은데 쉽진 않을 것 같습니다.

Powershell 3.0은 ScriptStackTrace 속성을 ErrorRecord 개체에 추가합니다.오류 보고를 위해 이 기능을 사용합니다.

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 })
    }
}

Skip 매개 변수를 사용하면 Write-Call stack 또는 임의 수의 오류 처리 스택 프레임을 Get-PSCall stack 목록에서 제외할 수 있습니다.

캐치 블록에서 호출되는 경우 Get-PSCallstack은 스로우 사이트와 캐치 블록 사이의 프레임을 모두 놓칩니다.따라서 프레임당 세부사항은 적지만 PS 3.0 방식을 선호합니다.

에서만 PowerShell 스크립트 코드의 예외에서 스택 추적을 가져올 수 없습니다.NET 개체.이를 위해서는 다음 중 하나와 같은 Exception 개체를 가져와야 합니다.

$Error[0].Exception.StackTrace
$Error[0].Exception.InnerException.StackTrace
$Error[0].StackTrace

여기서 발견한 것을 영감으로 삼아 누구나 코드에 넣을 수 있는 좋은 함수를 만들었습니다.

이렇게 부릅니다. 쓰기-호스트 "로그 파일 'n$(Resolve-Error)'에 쓰기 실패" - ForegroundColor Red

Function Resolve-Error
{
<#
.SYNOPSIS
    Enumerate error record details.

.DESCRIPTION
    Enumerate an error record, or a collection of error record, properties. By default, the details
    for the last error will be enumerated.

.PARAMETER ErrorRecord
    The error record to resolve. The default error record is the lastest one: $global:Error[0].
    This parameter will also accept an array of error records.

.PARAMETER Property
    The list of properties to display from the error record. Use "*" to display all properties.
    Default list of error properties is: Message, FullyQualifiedErrorId, ScriptStackTrace, PositionMessage, InnerException

    Below is a list of all of the possible available properties on the error record:

    Error Record:               Error Invocation:           Error Exception:                    Error Inner Exception(s):
    $_                          $_.InvocationInfo           $_.Exception                        $_.Exception.InnerException
    -------------               -----------------           ----------------                    ---------------------------
    writeErrorStream            MyCommand                   ErrorRecord                         Data
    PSMessageDetails            BoundParameters             ItemName                            HelpLink
    Exception                   UnboundArguments            SessionStateCategory                HResult
    TargetObject                ScriptLineNumber            StackTrace                          InnerException
    CategoryInfo                OffsetInLine                WasThrownFromThrowStatement         Message
    FullyQualifiedErrorId       HistoryId                   Message                             Source
    ErrorDetails                ScriptName                  Data                                StackTrace
    InvocationInfo              Line                        InnerException                      TargetSite
    ScriptStackTrace            PositionMessage             TargetSite                          
    PipelineIterationInfo       PSScriptRoot                HelpLink                            
                                PSCommandPath               Source                              
                                InvocationName              HResult                             
                                PipelineLength              
                                PipelinePosition            
                                ExpectingInput              
                                CommandOrigin               
                                DisplayScriptPosition       

.PARAMETER GetErrorRecord
    Get error record details as represented by $_
    Default is to display details. To skip details, specify -GetErrorRecord:$false

.PARAMETER GetErrorInvocation
    Get error record invocation information as represented by $_.InvocationInfo
    Default is to display details. To skip details, specify -GetErrorInvocation:$false

.PARAMETER GetErrorException
    Get error record exception details as represented by $_.Exception
    Default is to display details. To skip details, specify -GetErrorException:$false

.PARAMETER GetErrorInnerException
    Get error record inner exception details as represented by $_.Exception.InnerException.
    Will retrieve all inner exceptions if there is more then one.
    Default is to display details. To skip details, specify -GetErrorInnerException:$false

.EXAMPLE
    Resolve-Error

    Get the default error details for the last error

.EXAMPLE
    Resolve-Error -ErrorRecord $global:Error[0,1]

    Get the default error details for the last two errors

.EXAMPLE
    Resolve-Error -Property *

    Get all of the error details for the last error

.EXAMPLE
    Resolve-Error -Property InnerException

    Get the "InnerException" for the last error

.EXAMPLE
    Resolve-Error -GetErrorInvocation:$false

    Get the default error details for the last error but exclude the error invocation information

.NOTES
.LINK
#>
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$false, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullorEmpty()]
        [array]$ErrorRecord,

        [Parameter(Mandatory=$false, Position=1)]
        [ValidateNotNullorEmpty()]
        [string[]]$Property = ('Message','InnerException','FullyQualifiedErrorId','ScriptStackTrace','PositionMessage'),

        [Parameter(Mandatory=$false, Position=2)]
        [switch]$GetErrorRecord = $true,

        [Parameter(Mandatory=$false, Position=3)]
        [switch]$GetErrorInvocation = $true,

        [Parameter(Mandatory=$false, Position=4)]
        [switch]$GetErrorException = $true,

        [Parameter(Mandatory=$false, Position=5)]
        [switch]$GetErrorInnerException = $true
    )

    Begin
    {
        ## If function was called without specifying an error record, then choose the latest error that occured
        If (-not $ErrorRecord)
        {
            If ($global:Error.Count -eq 0)
            {
                # The `$Error collection is empty
                Return
            }
            Else
            {
                [array]$ErrorRecord = $global:Error[0]
            }
        }

        ## Define script block for selecting and filtering the properties on the error object
        [scriptblock]$SelectProperty = {
            Param
            (
                [Parameter(Mandatory=$true)]
                [ValidateNotNullorEmpty()]
                $InputObject,

                [Parameter(Mandatory=$true)]
                [ValidateNotNullorEmpty()]
                [string[]]$Property
            )
            [string[]]$ObjectProperty = $InputObject | Get-Member -MemberType *Property | Select-Object -ExpandProperty Name
            ForEach ($Prop in $Property)
            {
                If ($Prop -eq '*')
                {
                    [string[]]$PropertySelection = $ObjectProperty
                    Break
                }
                ElseIf ($ObjectProperty -contains $Prop)
                {
                    [string[]]$PropertySelection += $Prop
                }
            }
            Write-Output $PropertySelection
        }

        # Initialize variables to avoid error if 'Set-StrictMode' is set
        $LogErrorRecordMsg      = $null
        $LogErrorInvocationMsg  = $null
        $LogErrorExceptionMsg   = $null
        $LogErrorMessageTmp     = $null
        $LogInnerMessage        = $null
    }
    Process
    {
        ForEach ($ErrRecord in $ErrorRecord)
        {
            ## Capture Error Record
            If ($GetErrorRecord)
            {
                [string[]]$SelectedProperties = &$SelectProperty -InputObject $ErrRecord -Property $Property
                $LogErrorRecordMsg = $ErrRecord | Select-Object -Property $SelectedProperties
            }

            ## Error Invocation Information
            If ($GetErrorInvocation)
            {
                If ($ErrRecord.InvocationInfo)
                {
                    [string[]]$SelectedProperties = &$SelectProperty -InputObject $ErrRecord.InvocationInfo -Property $Property
                    $LogErrorInvocationMsg = $ErrRecord.InvocationInfo | Select-Object -Property $SelectedProperties
                }
            }

            ## Capture Error Exception
            If ($GetErrorException)
            {
                If ($ErrRecord.Exception)
                {
                    [string[]]$SelectedProperties = &$SelectProperty -InputObject $ErrRecord.Exception -Property $Property
                    $LogErrorExceptionMsg = $ErrRecord.Exception | Select-Object -Property $SelectedProperties
                }
            }

            ## Display properties in the correct order
            If ($Property -eq '*')
            {
                # If all properties were chosen for display, then arrange them in the order
                #  the error object displays them by default.
                If ($LogErrorRecordMsg)     {[array]$LogErrorMessageTmp += $LogErrorRecordMsg    }
                If ($LogErrorInvocationMsg) {[array]$LogErrorMessageTmp += $LogErrorInvocationMsg}
                If ($LogErrorExceptionMsg)  {[array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
            }
            Else
            {
                # Display selected properties in our custom order
                If ($LogErrorExceptionMsg)  {[array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
                If ($LogErrorRecordMsg)     {[array]$LogErrorMessageTmp += $LogErrorRecordMsg    }
                If ($LogErrorInvocationMsg) {[array]$LogErrorMessageTmp += $LogErrorInvocationMsg}
            }

            If ($LogErrorMessageTmp)
            {
                $LogErrorMessage  = 'Error Record:'
                $LogErrorMessage += "`n-------------"
                $LogErrorMsg      = $LogErrorMessageTmp | Format-List | Out-String
                $LogErrorMessage += $LogErrorMsg
            }

            ## Capture Error Inner Exception(s)
            If ($GetErrorInnerException)
            {
                If ($ErrRecord.Exception -and $ErrRecord.Exception.InnerException)
                {
                    $LogInnerMessage  = 'Error Inner Exception(s):'
                    $LogInnerMessage += "`n-------------------------"

                    $ErrorInnerException = $ErrRecord.Exception.InnerException
                    $Count = 0

                    While ($ErrorInnerException)
                    {
                        $InnerExceptionSeperator = '~' * 40

                        [string[]]$SelectedProperties = &$SelectProperty -InputObject $ErrorInnerException -Property $Property
                        $LogErrorInnerExceptionMsg = $ErrorInnerException | Select-Object -Property $SelectedProperties | Format-List | Out-String

                        If ($Count -gt 0)
                        {
                            $LogInnerMessage += $InnerExceptionSeperator
                        }
                        $LogInnerMessage += $LogErrorInnerExceptionMsg

                        $Count++
                        $ErrorInnerException = $ErrorInnerException.InnerException
                    }
                }
            }

            If ($LogErrorMessage) { $Output += $LogErrorMessage }
            If ($LogInnerMessage) { $Output += $LogInnerMessage }

            Write-Output $Output

            If (Test-Path -Path 'variable:Output'            ) { Clear-Variable -Name Output             }
            If (Test-Path -Path 'variable:LogErrorMessage'   ) { Clear-Variable -Name LogErrorMessage    }
            If (Test-Path -Path 'variable:LogInnerMessage'   ) { Clear-Variable -Name LogInnerMessage    }
            If (Test-Path -Path 'variable:LogErrorMessageTmp') { Clear-Variable -Name LogErrorMessageTmp }
        }
    }
    End {}
}

코드:

try {
    ...
}
catch {
    Write-Host $_.Exception.Message -Foreground "Red"
    Write-Host $_.ScriptStackTrace -Foreground "DarkGray"
    exit 1
}

다음 형식으로 오류를 반향합니다.

No match was found for the specified search criteria and module names 'psake'.
at Get-InstalledModule<Process>, ...\PSModule.psm1: line 9251
at Import-ModuleThirdparty, ...\Import-ModuleThirdparty.psm1: line 3
at <ScriptBlock>, ...\index.ps1: line 13

방법은 다음과 같습니다.스크립트 스택 추적

그 핵심은 다음과 같은 코드입니다.

1..100 | %{ $inv = &{ gv -sc $_ my invocation }

난 방금 알아냈다.$_은 캐치 블록에 포함된 예외입니다.

$errorString= $_ | Out-String 

또한 스택 추적을 포함하도록 오류 개체의 기본 형식을 변경할 수도 있습니다.기본적으로 시스템용 청크를 복사하여 포맷 파일을 만듭니다.관리.자동화.$PSHOME\PowerShellCore.format.ps1xml에서 오류를 기록하고 추적을 추가하는 고유 요소를 추가합니다.그런 다음 Update-Format Data와 함께 로드합니다.더 자세한 것은, 저는 방금 그것에 대한 블로그 포스트를 썼습니다: https://blogs.msdn.microsoft.com/sergey_babkins_blog/2016/12/28/getting-a-stack-trace-in-powershell/

아, 한 가지 더 말씀 드리자면 원격 세션에 자동으로 전파되지 않는다는 것입니다.개체는 원격에서 문자열로 포맷됩니다.원격 세션의 스택 트레이스에 대해서는 해당 파일을 업로드한 후 해당 파일에서 Update-FormatData를 다시 호출해야 합니다.

내장 솔루션을 찾고 있던 중 우연히 발견했습니다.간단한 해결책으로 진행하겠습니다.파워셸을 사용하기 전에 트레이스 블록만 추가하면 됩니다.그러면 콜 스택이 표시됩니다.아래쪽은 오류 메시지 전에 스택이 표시된다는 것입니다.

Trace {
   $_.ScriptStackTrace
}

제가 뭔가 잘못 이해했는지도 모르지만, 여기서 제 문제는 내부 예외에 대한 파워셸 스크립트 스택 추적을 보지 못했다는 것입니다.

결국 $Global을 검색하게 되었습니다.예외의 Error Eecord 개체가 스크립트 스택 추적을 검색하는 동안 오류가 발생했습니다.

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

사용 예시:

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 $_
}

출력 예시:



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)

호출이나 PowerShell 역추적을 ..Invoke()을 위해서, . 는.Set-PSDebug -Trace 2도움이 될 겁니다 인 스크립트의합니다.실행 중인 스크립트의 모든 행을 인쇄합니다.

#(를 #(1) (2) 를를 .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)
    }
}

분기(1) 예외가 발견됨:

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

분기(2)가 더 유용합니다.

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

그러나 분기(1): 추적을 통해 Invokes를 추적할 수 있습니다.

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."

언급URL : https://stackoverflow.com/questions/795751/can-i-get-detailed-exception-stacktrace-in-powershell

반응형