19

I want to make an extension to quickly toggle breaking on CLR exceptions in debugger.
I have made tried several approaches, neither of which is satisfactory.

Here is what I have already tried:

  1. ExceptionSettings.SetBreakWhenThrown (MSDN)
    This is extremely slow (see this Connect issue). I have tried approaches from question "Toggle “Break when an exception is thrown.” using macro or keyboard shortcut" and neither seem to work reliably: in most cases only top level checkbox gets set, and it does not actually break on exceptions when debugging.

  2. Call DTE.ExecuteCommand("Debug.Exceptions") to show the window, and call SetWindowsHookEx (MSDN) just before that to intercept it before it appears (so that there is no flash to the user). This seems possible as I was able to intercept the message and get HWND. But it seems hacky and window is not that easy to manipulate properly (it has some weird combination of SysListView32 with custom checkboxes and SysTreeView32). So I am leaving it as a last chance solution.

  3. Somehow get IDebugEngine2 (MSDN) for managed code and call IDebugEngine2.SetException (MSDN) at the start of the debugging session. This seems possible, but I am having problems getting a debug engine. I have tried approach with IVsLoader described on MSDN forums, but I am pretty sure it gives me a new instance unrelated to the debugging session.

    I have also asked the question here: "Visual Studio: How to get IDebugEngine2 from VS Package (except IVsLoader)", but did not get a solution.

    I have tried using IVsDebugger.AdviseDebugEventCallback (MSDN) and passing in implementation of IDebugEventCallback2 (MSDN), but I am always getting null for pEngine (and no IDebugEngineCreateEvent2 either).

    I do get IDebugSessionCreateEvent2 (undocumented?) and can get IDebugSession2 from it, but its SetException call always gives me an HRESULT for wrong argument, so I might be missing something here (calling SetException on engine from IVsLoader gives OK, just does not work).

Is there some other approach that is better than those or have I missed something in the existing ones?


UPDATE/NOTE:
If you found this question because you want a faster "Break on All Exceptions", I have made a free extension you can get from Visual Studio Gallery: Exception Breaker.

Community
  • 1
  • 1
Andrey Shchekin
  • 21,101
  • 19
  • 94
  • 162
  • 2
    You deserve more than just +1 for the effort you put into this question, so I put in the past 3 hours working on a potential solution. :) I believe it may only apply to VS2012 right now but hopefully we can get it going in 2010 as well. – Sam Harwell Mar 23 '13 at 23:23
  • I really appreciate your help! My skills currently hit their limit at low-level debugging, so I was stuck there. I hope this extension will save people much more time than we spent researching it. – Andrey Shchekin Mar 24 '13 at 00:58

1 Answers1

10

The automation interfaces are out of the question. In an attempt to improve performance using them, I created a cache from exception group to ExceptionSettings object and exception name to the ExceptionSetting object. This allowed me to bypass ExceptionSettings.Item for rapid lookup of individual exceptions for calling SetBreakWhenThrown, but unfortunately the internal implementation of SetBreakWhenThrown includes a call to validate the arguments, which in turn triggers an internal enumeration process that plagues this entire approach. The cache is about 4 times faster than code not using a macro, but we're still talking about code that will hang the IDE for several minutes...

NOTE: The instructions below were only tested so far with Visual Studio 2012.

Stepping through SetBreakWhenThrown in disassembly view revealed that the critical internal call (after validation) is sdm::CDebugManager::SetException. It turns out that the shell debugger (SVsShellDebugger service which you cast to IVsDebugger) implements IDebuggerInternal which provides access to the current IDebugSession3. This property was non-null after I opened a solution but before I started debugging.

IDebuggerInternal debugger = Package.GetGlobalService(typeof(SVsShellDebugger)) as IDebuggerInternal;
IDebugSession3 session = debugger != null ? debugger.CurrentSession : null;

Note: The IDebuggerInternal interface is defined in:

Microsoft.VisualStudio.Debugger.Interop.Internal, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a

Using information returned by EnumSetExceptions, I created a structure that successfully alters the settings for a CLR exception! Call IDebugSession3.SetException to enable the debugger halting when the exception is thrown.

EXCEPTION_INFO[] exceptionInfo =
{
    new EXCEPTION_INFO()
    {
        bstrExceptionName = typeof(NullReferenceException).FullName,
        bstrProgramName = null,
        dwCode = 0,
        pProgram = null,
        guidType = VSConstants.DebugEnginesGuids.ManagedOnly_guid,
        dwState = enum_EXCEPTION_STATE.EXCEPTION_STOP_FIRST_CHANCE
            | enum_EXCEPTION_STATE.EXCEPTION_STOP_SECOND_CHANCE
            | enum_EXCEPTION_STATE.EXCEPTION_JUST_MY_CODE_SUPPORTED
            | enum_EXCEPTION_STATE.EXCEPTION_STOP_USER_FIRST_CHANCE
            | enum_EXCEPTION_STATE.EXCEPTION_STOP_USER_UNCAUGHT
    }
};
hr = session.SetException(exceptionInfo);

To disable the debugger halting, use IDebugSession3.RemoveSetException instead.

Sam Harwell
  • 97,721
  • 20
  • 209
  • 280
  • 1
    This is amazing, thanks a lot! I actually found that `AdviseDebugEventCallback` + `IDebugSessionCreateEvent2` work just as well, as long as I use your flags combination instead of `enum_EXCEPTION_STATE.EXCEPTION_STOP_ALL`. And the best part is that Exception Dialog also notices that change, so I do not have to replace it in the first version. – Andrey Shchekin Mar 24 '13 at 00:52
  • Found an issue. When I call `SetException`, this affects contents of `Debug->Exceptions` and generally works, but not during the current session. For example, if I open a console app, the session (let's call it Session1) is immediately created for `*.vshost.exe` — then when I press `Run`, same session is used, then it is re-created on `Stop` (Session2). So while `Debug->Exceptions` show my setting, Session1 does not break on it — but Session2 does even without setting it again. – Andrey Shchekin Mar 26 '13 at 10:47
  • So `SetException` seems to affect some internal option persisted between sessions, but the current session does not reload this option immediately. I assume `Debug->Exceptions` calls some kind of reinitialize/update method, but I can not find anything suitable in the APIs. – Andrey Shchekin Mar 26 '13 at 10:48
  • 1
    EPIC WIN: Wasted some hours on unmanaged debugging (never did that before), found out I have to call `session.RemoveAllSetExceptions` with `Guid.Empty` (!) beforehand. – Andrey Shchekin Mar 27 '13 at 11:23
  • 1
    Another question: I switched to using `IDebuggerInternal` in the latest version of the extension, but I can not find the `Microsoft.VisualStudio.Debugger.Interop.Internal, Version=11.0.0.0` in my VS2012 (Premium). I found it in VS2010, but the interface is different: I managed to solve this, but I am still curious where is that file for VS2012. – Andrey Shchekin Apr 13 '13 at 12:16
  • OK I found it, it is in GAC ;) – Andrey Shchekin Apr 16 '13 at 03:23
  • In VS 2017 there is no `*.vshost.exe` anymore. So everything is much simpler now. `SetException` affects Exception list window (non-modal) and current session too. – SergeyT Oct 29 '19 at 22:45