vscode-powershell icon indicating copy to clipboard operation
vscode-powershell copied to clipboard

Debug Support for Any Exceptions and Uncaught Exceptions

Open sgtoj opened this issue 8 years ago • 40 comments

Is it possible to debug support to enable break points for: Any Exceptions or Uncaught Exceptions?

VS Code Debug Breakpoints

sgtoj avatar Sep 20 '16 11:09 sgtoj

I'm not sure how this would work in the case of PowerShell. What do you think, @rkeithhill?

daviwil avatar Sep 20 '16 14:09 daviwil

I've wanted this for a while but I think PowerShell would need to be modified to support this. Who's the debugger expert on the team? Paul H?

FYI, I filed a UserVoice suggestion on this back in March - please vote it up: https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/12843021-enhance-set-psbreakpoint-to-allow-us-to-set-except

rkeithhill avatar Sep 20 '16 15:09 rkeithhill

Yep, @PaulHigin is the PowerShell debugging expert.

daviwil avatar Sep 20 '16 17:09 daviwil

This is a fairly common request and something we have in our backlog, but is impossible to implement with our current script debugger. The reason is that the script debugger runs on the script execution thread and requires running script to work. But once an exception is thrown on the script execution thread the script is no longer running and the script debugger no longer works. At this point you need a managed/native debugger. We could have script debugger break for non-terminating errors since script execution continues and the script debugger is working. But I am not sure how useful that would be.

Jason, @lzybkr, has experimented with a mixed managed code/script debugger and it would be ideal for this kind of thing. When the managed exception is thrown this debugger could show both managed and PowerShell script stacks.

PaulHigin avatar Sep 20 '16 18:09 PaulHigin

The mixed code/script debugger would be extremely nice to have and would be pretty easy to surface in VS Code.

daviwil avatar Sep 20 '16 18:09 daviwil

That prototype generated a pdb and code that the C# debugger could consume - it was useful, but not a PowerShell experience.

lzybkr avatar Sep 20 '16 18:09 lzybkr

@PaulHigin What do you think about this workaround by @nightroman? https://github.com/nightroman/PowerShelf/blob/master/Debug-Error.ps1

rkeithhill avatar Sep 23 '16 15:09 rkeithhill

Very clever. I like it. But I believe this just handles the case where PowerShell is throwing the exception (either through throw keyword or -ErrorAction Stop). @lzybkr can you confirm? I was thinking in terms of a general exception being thrown in managed code. Still this looks to be very useful and something we could support internally.

PaulHigin avatar Sep 23 '16 17:09 PaulHigin

Unfortunately, Debug-Error.ps1 does not show the error itself, it just breaks into the debugger. The error has not been written yet (i.e. added to $Error) by PowerShell. So you have to guess what has happened. Sometimes this is clear, sometimes not. The tool is useful in special cases, rather rare.

nightroman avatar Sep 23 '16 19:09 nightroman

Any movement on this? At the moment I'm reduced to stepping through my module (and all of its dependencies) line by line trying to find where the exception is being thrown. Or adding a load of Write-Host statements to see which ones get hit.

andrewducker avatar Nov 10 '17 11:11 andrewducker

There is a way to break before the terminating error is thrown, but after it's been written to $Error. It isn't very clean though.

Set-PSBreakpoint -Variable StackTrace -Mode Write -Action {
    $null = Set-PSBreakpoint -Variable ErrorActionPreference -Mode Read
}

throw 'test'
# In debugger:
$Error[0]
# Returns:
# test
# At line:1 char:1
# + throw 'test'
# + ~~~~~~~~~~~~
#     + CategoryInfo          : OperationStopped: (test:String) [], RuntimeException
#     + FullyQualifiedErrorId : test

After $StackTrace is written to, $ErrorActionPreference is checked to see if the error should be terminating. When it checks $ErrorActionPreference it has already written to $Error regardless of the preference value.

For this to be reliable you would need to store state between the two breakpoints to ensure they fire on the same sequence point (in case the script later reads the preference directly). It would also need to handle situations where the break point already exists and be able to remove itself after the first hit.

I'll start looking into the feasibility of adding support for this.

SeeminglyScience avatar Nov 10 '17 14:11 SeeminglyScience

To follow up on @SeeminglyScience's work:

  • Capturing $Error by breaking on erroractionpreference only works reliably on the "throw" keyword, other exceptions don't seem to call it.
  • The breakpoints don't get cleaned up after a session and can trigger again if you look at erroractionpreference, etc. so I'm looking into having a remove-breakpoint somewhere maybe, via delayed runspace or something, because you cant remove the breakpoint within itself as far as I can tell.
  • Foreach Loops get weird, some additional workarounds added there
  • The only way to capture $Error for non-throw items as far as I've found is to try-catch the failed line again to capture the error. This obviously can have issues if the command wasn't idempotent so I made it a toggleable variable.
  • You can't break on a write to $Error because its marked as a readonly constant, so it doesn't technically "change" from the perspective of the breakpoint watcher even though PS updates it in the background.

Effective Workaround EDIT: See next post

JustinGrote avatar Jul 21 '19 22:07 JustinGrote

OK, I've come up with a pretty effective function to break on exceptions in lieu of a proper implementation (https://github.com/PowerShell/PowerShell/issues/2830)

https://gist.github.com/JustinGrote/52cf75ea75f2888dbaf4b0c86519d0a6

Features

  1. Breaks on any terminating exception (doesn't catch non-terminating errors on purpose unless erroractionpreference='stop' is set)
  2. Re-Execution in order to capture the relevant exception (toggleable behavior)
  3. Cleans up after itself (usually)
  4. WinPS5 and PS6+ compatible
  5. Works pretty much anywhere Powershell does (tested: vscode, cloud shell, azure functions, pwsh, etc.)

Quick Start

iex (irm https://git.io/PSDebugOnException);Debug-OnException

Example launch.json entry

{
    "name": "PowerShell Trap Exceptions",
    "type": "PowerShell",
    "request": "launch",
    "script": "Debug-OnException",
    "args": [
        "${file}"
    ],
    "cwd": "${file}"
}

Demos

VSCode Unsaved File

DebugExceptionUnsavedFile

VSCode Run Selection (F8)

DebugExceptionF8

Foreach Loop

DebugExceptionForeach

Foreach (More Real-World Example)

DebugExceptionForEachRealWorld

Exception Inside Module

DebugOnModuleException

Azure Cloud Shell

DebugCloudShellException

Powershell 5 (Windows Terminal, Works in VSCode too)

PS5Debug

VSCode Extension Integration

Will need help here as I suck at typescript. The script itself or the breakpoints it generates need to be wired up to a custom breakpoint "Terminating Exceptions" per @sgtoj's example. @TylerLeonhardt @SeeminglyScience @rjmholt maybe?

JustinGrote avatar Jul 23 '19 01:07 JustinGrote

@JustinGrote thanks for all your work here, this looks great. The change here will need to happen in Editor Services, probably in this chunk of code , using this https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetExceptionBreakpoints ....it would be awesome if you opened up a PR in that repo and we can help you iron out the details...Thanks!

SydneyhSmith avatar Jul 26 '19 22:07 SydneyhSmith

The script @JustinGrote created did not helps if the code itself also throws exceptions that are catched by intention. The script terminates in that case, where it would continue without the script. I had an other solution: I just added a

trap { 
# whatever
}

and placed a breakpoint in that trap. This worked perfectly for me.

bormm avatar Sep 11 '19 14:09 bormm

There is also a PR to PowerShell 7 to specifically add this functionality to set-breakpoint, so it will be easier to support going forward.

JustinGrote avatar Sep 11 '19 17:09 JustinGrote

Three years later ... But hey, I'll be happy to finally have this support in the PS engine. :-)

rkeithhill avatar Sep 12 '19 01:09 rkeithhill

cc: @KirkMunro since he made the change in PowerShell 7

SydneyhSmith avatar Sep 12 '19 21:09 SydneyhSmith

Thanks @SydneyhSmith.

@JustinGrote It's not in Set-PSBreakpoint, actually. It's done with -ErrorAction Break, or by setting one of the *Preference variables to ActionPreference.Break.

That will break into the debugger on any error or exception. Even if exceptions are handled, it will still result in a break in the debugger the moment the exception is thrown.

KirkMunro avatar Sep 13 '19 03:09 KirkMunro

Even if exceptions are handled, it will still result in a break in the debugger the moment the exception is thrown.

Hopefully, that can be configured. Debuggers typically over the ability break on both unhandled (Uncaught exceptions in VSCode) exceptions and when first thrown (All exceptions in VSCode). Hopefully there is a way to enable the former.

rkeithhill avatar Sep 13 '19 03:09 rkeithhill

Not yet.

I break into the debugger the moment the exception is raised.

I'll have to think about how that could be done. The only thing that comes to mind at the moment is reverse traversal of the AST to check for traps or catch blocks, but figuring out what handles what via inspection could be pretty complicated.

Actually, how would that even be possible in an interpreted language? How could I reliably identify that a catch all does or does not handle an exception, for example, when variables used in that catch all could come from anywhere?

KirkMunro avatar Sep 13 '19 12:09 KirkMunro

@KirkMunro

I think a exception could be considered unhandled when it reaches this code block in LocalPipeline and maybe when IsNested is false? Not sure about the latter, maybe it's just enough to hit that block.

SeeminglyScience avatar Sep 13 '19 13:09 SeeminglyScience

@SeeminglyScience: Even if that would work, I'm also concerned about the user experience. Today ActionPreference.Break enters the debugger the moment the corresponding exception or stream message occurs. The debugger is on the very line where the exception came from, allowing users to inspect the environment at that point in time, whether the exception is handled or not.

If we defer entering the debugger for unhandled exceptions until such a time that we know the exception is considered unhandled, where are we in our call stack? We may have unwound quite a bit to get to that point. IMHO it is far better for this community to bring users to the origin of the exception, so that they can understand what is going on.

That said, I'm all for efficient debugging (it's something I continue to actively work on in many areas), so I'd like to offer both, but I would want to do so with the debugger still breaking at the point of exception. We could do more work in the script block compiler so that we know the explicit types of exceptions that can be caught when an exception is first raised, to identify whether or not that exception is going to be handled, but then I'm not sure how to reliably identify if a catch all will handle an exception or not.

KirkMunro avatar Sep 13 '19 14:09 KirkMunro

If we defer entering the debugger for unhandled exceptions until such a time that we know the exception is considered unhandled, where are we in our call stack? We may have unwound quite a bit to get to that point.

Maybe, but the snippet I linked is at the end of a very long chain of try/catch's. With some experimentation you can probably find one where the stack is mostly intact and you can still determine if it's unhandled.

SeeminglyScience avatar Sep 13 '19 15:09 SeeminglyScience

Now that Powershell 7 is out, perhaps it's as "easy" as just wiring up the "Uncaught Exceptions" button to set ErrorActionPreference = 'break' for now? image

My current "workaround" is: Start Interactive Session $ErroractionPreference = 'Break' <Run Script> Stops on exception as expected.

@SeeminglyScience @KirkMunro @rkeithhill

I'll be able to take a stab at a PR in a couple weeks, knowing basically no Typescript and just enough C# to read it and interpret it into powershell :).

JustinGrote avatar Mar 06 '20 18:03 JustinGrote

knowing basically no Typescript and just enough C# to read it and interpret it into powershell

Very happy to help with that. The hard won't be the language as much as navigating the code, but I think we have a fair idea of where things should go.

rjmholt avatar Mar 06 '20 19:03 rjmholt

The other hard part is managing the version-specificity. It's a pain writing code that works in 7 but fails gracefully and informatively in older PowerShells.

rjmholt avatar Mar 06 '20 19:03 rjmholt

@KirkMunro do you know if there's a way to only break on uncaught errors? Or otherwise determine that an error was uncaught? Also determine whether the error was terminating?

Right now it's all errors, even if caught. That's still awesome and way better than no error breakpoints period though.

SeeminglyScience avatar Mar 06 '20 20:03 SeeminglyScience

@SeeminglyScience You're right! For some reason I thought it was working on uncaught exceptions only. image

Sounds like more of a upstream (Powershell) bug to me, since ErrorActionPreference = 'stop' doesn't stop on uncaught exceptions so the behavior is inconsistent.

JustinGrote avatar Mar 06 '20 22:03 JustinGrote

@JustinGrote Yeah for sure, there's still great value in enabling "All Exceptions".

SeeminglyScience avatar Mar 06 '20 23:03 SeeminglyScience