Thursday, December 31, 2020

PowerShell: Handling/Logging Exceptions

This post introduces a class, ErrorRecordExtension, that handles PowerShell exceptions and supports the logging of said exceptions. All source code is provided at the end of the post in Appendix A. The ErrorRecordExtension class provides methods for cleaning  up the formatting of the information provided when an exception is handled. The constructor for the ErrorRecordExtension class is defined at line 16 in the code snippet below:


The first parameter to the ErrorRecordExtension constructor is of type ErrorRecord. When an exception is caught in PowerShell, the pipeline variable ($_ or $PSItem) is a value of type ErrorRecord describing the exception. The class name, ErrorRecordExtension, was chosen paying homage to C# extensions which allow methods to be added to existing classes (extension methods).

The $exceptionHandledAtLineNumber parameter corresponds to the caller's $MyInvocation's ScriptLineNumber property. The $exceptionHandledAtFilename parameter corresponds to the caller's $MyInvocation's ScriptName property. 

The code to generate a test exception and more elegantly handle the exception using the class, ErrorRecordExtension, is as follows:


The exception is thrown is a caused by a divide-by-zero operation being performed at line 91. The exception is caught and an instance of ErrorRecordExtension is constructed immediately (lines 100-104). The pipeline variable ($_) should be used/assigned right away as its value could be changed depending on the context in which is it is used. The methods exposed by the ErrorRecordExtension are demonstrated at line 106 to line 112.

GetException

The ErrorRecord class contains an Exception property of type System.Exception. One purpose of the ErrorRecordExtension is to facilitate logging so it remans to be seen if invoking the ToString method of the ErrorRecord's Exception property generates a suitable message for logging. The Exception property's ToString method displays text as follows:


The above text includes the Exception class's Message and StackeTrace property. The StackTrace when displayed as text spans multiple lines. This is acceptable for Xml or Json based logging. When logging to CSV/TSV file a single line error message simplifies that output. The following is an example of the text generated by invoking the GetException method of the ErrorRecordExtension  class:


The GetException method is overloaded with a publicly visible method implemented at line 49 and a hidden implemented (method overloading) at line 34 (see below). The version of GetException decorated by the hidden keyword is designed to be used recursively:


The Flatten method (line 26 above) is used to remove the carriage return and linefeed characters from strings such as Exception.StackTrace. The GetStackTrace method invokes Flatten and then use the -replace operator to finalize formatting:

[ErrorRecordExtension]::Flatten($exception.StackTrace) -replace '\s+', ' '

The operation "-replace '\s+', ' '" replaces any multiple sequences of spaces with a single space. The GetException method at line 34 creates a string from the Message, TargetSite and StrackTrace of an Exception (lines 35-380). The text associated with the InnerException is appended to the message using recursion (see lines 41-43).

GetInvocationPosition

The ErrorRecord's InvocationInfo property exposes the PositionMessage property. An example of the output from PositionMessage is as follows:

+     return $n / $d
+            ~~~~~~~

The PositionMessage property spans multiple lines. The GetInvocationPosition method of the ErrorRecordExtension class converts the PositionMessage multiline text into a single line representation:

At C:\Blog\Diff\ErrorRecordExtension.ps1:43 char:12     return $n / $d

The GetInvocationPosition method is implemented as follows (see line 30):


The GetInvocationPosition method removes the carriage return and linefeed characters from the PositionMessage property using the Flatten method. The replace method of the string class removes unnecessary characters (see code demarcated in boldface below):

    [string] GetInvocationPosition() {
        return [ErrorRecordExtension]::Flatten(
            $this.errorRecord.InvocationInfo.PositionMessage).
                Replace('~', '').Replace('+', '')
    }

GetInvocationLine, GetInvocationScriptLineNumber, GetInvocationScriptName

The output from the GetInvocationLine, GetInvocationScriptLineNumber, and GetInvocationScriptName methods is as follows (each line is a the output from a different method):

    return $n / $d
59
ErrorRecordExtension.ps1

The aforementioned three methods are implemented as follows (line 40, line 45, and line 49):


The GetFilename method is defined at line 12 above. This method returns the filename from the path specified by the parameter, $fullPath.

GetHandledAtLineNumber/GetHandledAtScriptName

The output from the GetHandledAtLineNumber and GetHandledAtScriptName methods is as follows:

122
ErrorRecordExtension.ps1 

The implementation of these methods is as follows (see line 40 and line 44):

GetErrorRecord

The GetErrorRecord method of the ErrorRecordExtension class returns the ErrorRecord passed to the class' constructor:

Appendix A: Code in its Entirety

Set-StrictMode -Version 3.0

class ErrorRecordExtension {
    hidden static [string] $messageSeparator

    hidden [System.Management.Automation.ErrorRecord] $errorRecord

    hidden [int] $exceptionHandledAtLineNumber 

    hidden [string] $exceptionHandledAtFilename 

    static ErrorRecordExtension() {
        [ErrorRecordExtension]::messageSeparator = ':'
    }
    
    ErrorRecordExtension(
        [System.Management.Automation.ErrorRecord] $errorRecord,        
        [int] $exceptionHandledAtLineNumber,
        [string] $exceptionHandledAtFilename) {

        $this.exceptionHandledAtLineNumber = 
            $exceptionHandledAtLineNumber
        $this.exceptionHandledAtFilename = 
            $exceptionHandledAtFilename            
        $this.errorRecord = $errorRecord
    }

    hidden static [string] Flatten([string] $text) {
        return $text.Replace("`n", '').Replace("`r", '')
    }

    hidden static [string] GetFilename([string] $fullPath) {
        if ([string]::IsNullOrEmpty($fullPath)) {
            return ''
        }

        return Split-Path $fullPath -Leaf
    }

    [int] GetHandledAtLineNumber() {
        return $this.exceptionHandledAtLineNumber
    }

    [string] GetHandledAtScriptName() {
        return [ErrorRecordExtension]::GetFilename(
                    $this.exceptionHandledAtFilename)
    }

    [string] GetInvocationLine() {
        return [ErrorRecordExtension]::Flatten(
                    $this.errorRecord.InvocationInfo.Line)
    }

    [int] GetInvocationScriptLineNumber() {
        return $this.errorRecord.InvocationInfo.ScriptLineNumber
    }

    [string] GetInvocationScriptName() {
        return [ErrorRecordExtension]::GetFilename(
                $this.errorRecord.InvocationInfo.ScriptName)
    }

    [string] GetInvocationPosition() {
        return [ErrorRecordExtension]::Flatten(
            $this.errorRecord.InvocationInfo.PositionMessage).
                Replace('~', '').Replace('+', '')
    }

    hidden static [string] GetStackTrace([Exception] $exception) {
        return [ErrorRecordExtension]::Flatten(
                    $exception.StackTrace) -replace '\s+', ' '
    }

    hidden static [string] GetException([Exception] $exception) {
        [string] $message =             "$($exception.Message)$([ErrorRecordExtension]::messageSeparator)" +
"$($exception.TargetSite)$([ErrorRecordExtension]::messageSeparator)" + 
            "$([ErrorRecordExtension]::GetStackTrace($exception))"

        if ($null -ne $exception.InnerException) {
            $message += 
                [ErrorRecordExtension]::messageSeparator +
                [ErrorRecordExtension]::GetException(
                              $exception.InnerException)
        }

        return $message
    }

    [string] GetException() {
        return [ErrorRecordExtension]::GetException(
                    $this.errorRecord.Exception)
    }

    [System.Management.Automation.ErrorRecord] GetErrorRecord() {
        return $this.errorRecord
    }
}

function New-DivideByZeroException {
    [int] $d = 0
    [int] $n = 10

    return $n / $d
}

function New-ActionToShowOffExceptoinHandler {
    try {
        New-DivideByZeroException
    }
    
    catch {
        [ErrorRecordExtension] $errorRecordExtension = 
                    [ErrorRecordExtension]::new(
                            $_, 
                            $MyInvocation.ScriptLineNumber, 
                            $MyInvocation.ScriptName)
    
        Write-Host $errorRecordExtension.GetHandledAtLineNumber()
        Write-Host $errorRecordExtension.GetHandledAtScriptName()
        Write-Host $errorRecordExtension.GetInvocationLine()
        Write-Host `
          $errorRecordExtension.GetInvocationScriptLineNumber()
        Write-Host $errorRecordExtension.GetInvocationScriptName()                                
        Write-Host $errorRecordExtension.GetInvocationPosition()
        Write-Host $errorRecordExtension.GetException()
    }
}

New-ActionToShowOffExceptoinHandler

No comments :

Post a Comment