My main PowerShell code runs a function that logs to the Windows eventlog. If the level is error it uses a separate event ID which then our monitoring will pick up that exact ID and run an action. However, if I want to specify in the parameter of the main script (not the function) that this time running it use a different Event ID so it will NOT action monitoring, I don't know where to even start on that.
Is there a way to provide a switch parameter in the main script like $NoAlert which then changes the Event ID in the function?
The function of logging lives in a PowerShell module I created. I am importing the module at the beginning of the script and then calling the function during the main script body.
Here is the function:
function WriteLog-SRTProd {
Param(
[string]$logT,
[Parameter(Mandatory=$true)][string]$level,
[String]$LogFileDirT = "\\ServerA\Logs"
)
$RSLogfileT = (Get-ChildItem -Path $LogFileDirT |
sort LastWriteTime |
select -Last 1).Name
## make sure a level is correctly selected (mandatory)
if ("Error","Info","Warn" -NotContains $Level) {
throw "$($Environment) is not a valid name! Please use 'Error', 'Warn', or 'Info'"
}
if ($Level -eq "Info") {
Add-Content -Path "$LogFileDirT\$RSLogFileT" -Value "$(Get-Date -format MM-dd-yyyy::HH:mm:ss) INFO $logT"
Write-EventLog -LogName Application -Source TEST_MAINT -EntryType Information -EventId 100 -Message $logT -Category 0
}
if ($Level -eq "Warn") {
Add-Content -Path "$LogFileDirT\$RSLogFileT" -Value "$(Get-Date -format MM-dd-yyyy::HH:mm:ss) WARN $logT"
Write-EventLog -LogName Application -Source TEST_MAINT -EntryType Warning -EventId 200 -Message $logT -Category 0
}
if ($Level -eq "Error") {
Add-Content -Path "$LogFileDirT\$RSLogFileT" -Value "$(Get-Date -format MM-dd-yyyy::HH:mm:ss) ERROR $logT"
Write-EventLog -LogName Application -Source TEST_MAINT -EntryType Error -EventId 300 -Message $logT -Category 0
}
}
I'd like to run my script like this. When the $NoAlert is passed, it will send that switch to the function. Is this possible? Can I just add the switch in both places and use an if statement in the function for when the NoAlert switch is used?
PS C:\> .\Maintenance.ps1 -NoAlert
Param(
[switch]$NoAlert
)
WriteLog-SRTProd -level Error -logT "Custom Error Message"
I have created own function for logging and stored/installed as module, below is the part of my log module :
you can customize the write statements and add your code for event log. I have added 'NoAction' enum member as per your requirements.
I have used one Enum to separate the log levels
Enum Severity
{
Error = 3
Warning = 4
Informational = 6
Debug = 7
Verbose = 8
NoAction = 0 # AS PER YOUR REQUIREMENTS
}
function Write-Log()
{
[cmdletbinding()]
param
(
[Parameter(Position=0,mandatory=$true)]
[Severity] $LogLevel,
[Parameter(Position=1,mandatory=$true)]
[String] $Message
)
$TimeStamp = "$(Get-Date -format HH:mm:ss)" ;
Switch($LogLevel)
{
([Severity]::Error.ToString())
{
Write-Error "`t$TimeStamp : $Message`n" -ErrorAction Stop
break;
}
([Severity]::Warning.ToString())
{
Write-Warning "`t$TimeStamp : $Message`n" -WarningAction Continue
break;
}
([Severity]::Informational.ToString())
{
Write-Information "INROMATION:`t$TimeStamp : $Message`n" -InformationAction Continue
break;
}
([Severity]::Verbose.ToString())
{
Write-Verbose "`t$TimeStamp : $Message`n"
break;
}
([Severity]::NoAction.ToString())
{
Write-Verbose "`t$TimeStamp : $Message`n"
break;
}
} # END OF SWITCH
} # END OF FUNCTION
Sample Call :
Write-Log -LogLevel ([Severity]::Informational) -Message "test log message using info level"
Output :
INROMATION: 09:40:15 : test log message using info level
I have decided to just add a new parameter to both function and main script named $NoAlert. I have added an If($NoAlert){WriteLog-SRPProd -NoAlert} to the main script (messy, but its what I needed done). then in the Function, If($NoAlert){EventID 111}. so basically I am using the switch in the main script that then calls the NoAlert switch in the function. This is all done with a few added If/Else statements.
Hopefully that makes sense. Like I said its not the best answer, but I wanted to get it done and still provide an answer here in this post.
I've created a function "Query-ComDomElements.ps1" to query HTML objects.
This works quite well when querying only one object and querying that again.
When I try calling it in recursion it however fails and I don't understand why. The code/objects is/are the very same.
Could anyone please enlighten me why the query .container>img is not working, but querying .container and with that img is?
The error I get when querying both (and thus calling the function recursively) is:
Exception calling "InvokeMember" with "5" argument(s): "Unknown name. (Exception from HRESULT: 0x80020006 (DISP_E_UNKNOWNNAME))"
At C:\path\to\Query-ComDomElements.ps1:31 char:5
+ ... $result = [System.__ComObject].InvokeMember("getElementsB ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : COMException
Here my sample script (function Query-ComDomElements.ps1 not included but on github):
. C:\path\to\Query-ComDomElements.ps1
$ie = New-Object -ComObject "InternetExplorer.Application"
$ie.Navigate2("https://www.gpunktschmitz.de/")
while($ie.Busy) {
Start-Sleep -Seconds 1
}
#this works
$imgContainer = Query-ComDomElements -Query '.container' -Dom $ie.Document
$image = Query-ComDomElements -Query 'img' -Dom $imgContainer -Property 'src'
#this fails
$image = Query-ComDomElements -Query '.container>img' -Dom $ie.Document -Property 'src'
$ie.quit()
I think the problem is occurring because $dom ends up being an array with two elements when it is passed in on the second iteration. One (dirty) fix for this would be to use Select-Object to just get the first element (suggest using Select rather than [0] so that if its not an array it doesn't error):
if($SecondQuery -eq $false) {
if($Property -ne $false -and $Property -ne '*') {
return $result.$Property
} else {
return $result
}
} else {
return Query-ComDomElements -Query $SecondQuery -Dom ($result | select -first 1) -Property $Property
}
I'm having issues downloading files from an ftp in powershell, this script tries to setup the connection, search for some files (I got this part right) and then download it in the working directory, I got issues don't know why, please help!!
Here's the code:
#IP address of DNS of the target % protocol
$protocol="ftp"
$target = "XXXX"
$connectionString = $protocol+"://"+$target
#Method to connect
$Request = [System.Net.WebRequest]::Create($connectionString)
$Request.Method = [System.Net.WebRequestMethods+Ftp]::ListDirectoryDetails
# Set Credentials "username",password
$username = "XXXXXXX"
$password = "XXXXXX"
# Read Username/password
$Request.Credentials = New-Object System.Net.NetworkCredential $username,$password
$Response = $Request.GetResponse()
$ResponseStream = $Response.GetResponseStream()
# Select Pattern to search
$pattern = "CCS"
# Set directory for download Files
$directory = [IO.Directory]::GetCurrentDirectory()
# Read and display the text in the file
$Reader = new-object System.Io.StreamReader $Responsestream
$files = ($Reader.ReadToEnd()) -split "`n" | Select-String "$pattern" | foreach { $_.ToString().split(” “)[28]}
$uri = (New-Object System.Uri($connectionString+"/"+$file))
$download = New-Object System.Net.WebRequestMethods+Ftp
foreach ($file in $files) {
$destinationFile = $directory+"\"+$file
$sourceFile = $uri.OriginalString
$download.DownloadFile($sourceFile, $destinationFile)
}
# Close Reader and Response objects
$Reader.Close()
$Response.Close()
When I run it I got this output:
Exception calling "DownloadFile" with "2" argument(s): "An exception occurred during a WebClient request."
At C:\CRIF\BatchScripts\FTPCHECK\01.FTP_Check.ps1:44 char:5
+ $download.DownloadFile($sourceFile, $destinationFile)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : WebException
I'm running this on Powershell 3.0 (Windows Server 2012). Please help!
Details about the problem are hidden in the inner exception of this generic exception. You should dig a bit deeper in the error to find out what the real problem is.
Since PowerShell errors are stores into $error you could, immediatly after getting the error, try the following command to check out the inner exception of the last error
$error[0].Exception.InnerException
To get the most out of error messages you could use functions people wrote like Resolve-Error.
If you want your script in this case to always display a better error message, you could use a try catch block to catch the error and display it better. Something like this:
try {
$download.DownloadFile($sourceFile, $destinationFile)
}
catch [System.Net.WebException] {
if ($_.Exception.InnerException) {
Write-Error $_.Exception.InnerException.Message
} else {
Write-Error $_.Exception.Message
}
}
I'm trying this to catch an exception but it just doesn't work rather just shows me error in the script editor I'm running the script from:-
Path I mentioned in the script "\server\abc" doesn't really exist, so it should catch it as an exception which it is not. HELP HELP
Try
{
Get-ChildItem -Path "\\server\abc"
}
Catch
{
Write-Host "error"
}
You need to set the erroraction to STOP for the error to be terminating - only terminating errors are raised to a catch block.
Try
{
Get-ChildItem -Path "\\server\abc" -ErrorAction Stop
}
Catch
{
Write-Host "error"
}
Runing such script:
1: function foo()
2: {
3: bar
4: }
5:
6: function bar()
7: {
8: throw "test"
9: }
10:
11: foo
I see
test
At C:\test.ps1:8 char:10
Can I get a detailed stack trace instead?
At bar() in C:\test.ps1:8
At foo() in C:\test.ps1:3
At C:\test.ps1:11
There is a function up on the PowerShell Team blog called Resolve-Error which will get you all kinds of details
Note that $error is an array of all the errors you have encountered in your PSSession. This function will give you details on the last error you encountered.
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
}
}
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.
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.
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
I took what I found here as inspiration and created a nice function anyone can drop into their code.
This is how I call it:
Write-Host "Failed to write to the log file `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 {}
}
This code:
try {
...
}
catch {
Write-Host $_.Exception.Message -Foreground "Red"
Write-Host $_.ScriptStackTrace -Foreground "DarkGray"
exit 1
}
Will echo an error in a following format:
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
Here's a way: Tracing the script stack
The core of it is this code:
1..100 | %{ $inv = &{ gv -sc $_ myinvocation }
I just figured it out. The $_ is the exception caught in the catch block.
$errorString= $_ | Out-String
You can also change the default formatting for the error object to include the stack trace. Basically, make your format file by copying the chunk for System.Management.Automation.ErrorRecord from $PSHOME\PowerShellCore.format.ps1xml and add your own
element that adds the trace. Then load it with Update-FormatData. For more details, I've just written a blog post about it: https://blogs.msdn.microsoft.com/sergey_babkins_blog/2016/12/28/getting-a-stack-trace-in-powershell/
Oh, one more thing: this doesn't propagate automatically into the remote sessions. The objects get formatted into strings on the remote side. For stack traces in the remote sessions you'll have to upload this file there and call Update-FormatData there again.
Stumbled upon this looking for a built in solution. I am going with simple solution. Just add trace block before using any powershell. This will ensure a call stack is shown. Down side of this is the stack will be displayed before the error message.
Trace {
$_.ScriptStackTrace
}
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)
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."