PowerShell / vscode-powershell

Provides PowerShell language and debugging support for Visual Studio Code
https://marketplace.visualstudio.com/items/ms-vscode.PowerShell
MIT License
1.68k stars 479 forks source link

Debug Support for Any Exceptions and Uncaught Exceptions #298

Open sgtoj opened 7 years ago

sgtoj commented 7 years ago

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

VS Code Debug Breakpoints

daviwil commented 7 years ago

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

rkeithhill commented 7 years ago

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

daviwil commented 7 years ago

Yep, @PaulHigin is the PowerShell debugging expert.

PaulHigin commented 7 years ago

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.

daviwil commented 7 years ago

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

lzybkr commented 7 years ago

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

rkeithhill commented 7 years ago

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

PaulHigin commented 7 years ago

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.

nightroman commented 7 years ago

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.

andrewducker commented 6 years ago

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.

SeeminglyScience commented 6 years ago

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.

JustinGrote commented 5 years ago

To follow up on @SeeminglyScience's work:

Effective Workaround EDIT: See next post

JustinGrote commented 4 years ago

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?

SydneyhSmith commented 4 years ago

@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!

bormm commented 4 years ago

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.

JustinGrote commented 4 years ago

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.

rkeithhill commented 4 years ago

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

SydneyhSmith commented 4 years ago

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

KirkMunro commented 4 years ago

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.

rkeithhill commented 4 years ago

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.

KirkMunro commented 4 years ago

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?

SeeminglyScience commented 4 years ago

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

KirkMunro commented 4 years ago

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

SeeminglyScience commented 4 years ago

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.

JustinGrote commented 4 years ago

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'

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 :).
rjmholt commented 4 years ago

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 commented 4 years ago

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.

SeeminglyScience commented 4 years ago

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

JustinGrote commented 4 years ago

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

SeeminglyScience commented 4 years ago

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

rjmholt commented 4 years ago

since ErrorActionPreference = 'stop' doesn't stop on uncaught exceptions so the behavior is inconsistent

Do you mean $ErrorActionPreference = 'Stop' doesn't stop on caught exceptions?

If so, what really happens is that it does "Stop" (by which I mean throw a terminating error), but then that error is caught.

To change the behaviour of $ErrorActionPreference = 'Debug' I feel that's more of a consistency break, since unlike other error actions it would need to search up the stack to determine if it's going to be caught or not. Honestly that's a pretty complex behaviour.

Anyway, didn't mean to sidetrack you.

SeeminglyScience commented 4 years ago

since unlike other error actions it would need to search up the stack to determine if it's going to be caught or not. Honestly that's a pretty complex behaviour.

My thought here is to add something to the compiler to emit something when processing a TryStatementAst that would allow tracking this easier. Still pretty complex but would be incredibly useful.

Edit: You were talking about Stop, not a hypothetical BreakOnUncaughtOrSomething. Oops

markm77 commented 4 years ago

[Aside]

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.

I noticed this comment because I'm looking for a way to step through C# in my own PS module (dll) called by PowerShell script being debugged in vscode-powershell. At the moment I can't debug the PowerShell and C# together so @lzybkr if you ever chose to make available something that supports this I'd be grateful for one. 😊

TylerLeonhardt commented 4 years ago

@markm77 you are able to have the PowerShell debugger and C# debugger running at the same time.... so feasibly you can launch debug a PowerShell script and attach the C# debugger... but it doesn't give you the ideal behavior of "stepping into C# from PowerShell".

lzybkr commented 4 years ago

@markm77 - mixed debugging is a little painful but possible today if you attach a C# debugger, set breakpoints where you care about, then debug the PowerShell code with the PowerShell debugger. You don't get a nice shared stack but it's better than nothing.

My prototypes were based on the Desktop version of .Net, I have no idea if they are even possible now.

markm77 commented 4 years ago

Thanks guys for interesting comments. To do mixed debugging do I have to re-attach the debugger each time the PowerShell script calls into a C# module cmdlet? Or can I attach the debugger just once following module import and it will re-attach as the DLL is called (much better)?

Also is there any recommended way for dropping the lock on the DLL to allow DLL re-build following any C# update? At the moment I run pwsh inside PowerShell before module import, then run exit to drop the lock on the DLL before C# re-build.

Cheers!

lzybkr commented 4 years ago

Attaching once is fine, but you must exit the process to rebuild - this is a limitation in .Net.

markm77 commented 4 years ago

Thanks! I think I'm going to experiment with debugging an outer PS script that calls pwsh to do the import and run the inner PS script, hopefully the PowerShell debugger can hop to the inner process.... Otherwise I'll set up VS Code tasks. Have a great day!

SetTrend commented 4 months ago

I'm not sure if I get the current situation right …

According to https://github.com/PowerShell/PowerShell/issues/2830, PowerShell seems to now support breaking into the debugger on uncaught exceptions. Yet, I can't find this working in the current version of VS Code PowerShell extension.

I have written a recursive function and I have no clue what's causing a "Collection was modified; enumeration operation may not execute." error without seeing the current stack trace and variable values.

JustinGrote commented 4 months ago

@SetTrend I don't know if we've wired up the button specifically but if you do $ErrorActionPreference = 'break' it should still break in debugging when an error occurs.

@SeeminglyScience has a breakpoint PR we are waiting that changes a lot of things that we are waiting on before we make any breakpoint feature changes. https://github.com/PowerShell/vscode-powershell/pull/4065