2

We have a .NET 3.5 assembly (dll) being executed by a VB6 "Agent" (exe) via COM interfaces. The VB6 code does call:

' Ensure that no system dialog comes up when we GPF.
PreviousErrorMode = SetErrorMode(SEM_NOGPFAULTERRORBOX)
SetErrorMode PreviousErrorMode Or SEM_NOGPFAULTERRORBOX

' Assign our Exception Handler for this process.
PreviousExceptionFilter = SetUnhandledExceptionFilter(AddressOf UnhandledExceptionFilter)

And the VB6 UnhandledExceptionFilter converts exceptions into VB6 raised errors. (But that's not being called here either, and this VB6 code hasn't been changed -- or even recompiled -- for years.)

In the constructor of the first .NET object created by the VB6 code, I add a handler to AppDomain.CurrentDomain.UnhandledException (and, in the attempt to debug this problem, I've added one for Windows.Forms.Application.ThreadException, but it didn't help, not that we have any UI displayed by .NET anyway).

I'm sure the UnhandledException event has caught stuff when developing this code in VS2008, but I'm not sure I've seen it yet in VS2015.

The code is running multiple threads, some of which are performing the main purpose, which involves inserting and updating SQL Server data, and also communicating over a TCP protocol, which is handled by other threads, including a BackgroundWorker.

Those performing the main purpose are created with:

taskThread = New Thread(AddressOf EnsureTaskCompletes)
taskThread.SetApartmentState(Threading.ApartmentState.STA)
taskThread.Start()

It's too proprietary and complex to show you any more code, but I have adjusted the code while debugging this issue to simply Throw New Exception instead of the complex code that had the same effect in the end.(*)

This "simple" exception was not processed by the UnhandledExceptionHandler and would have just silently terminated the program except that I have turned on unmanaged debugging.

Is there a documented change to the behaviour of AppDomain.CurrentDomain.UnhandledException when the .NET Framework is "only" loaded as a dll?

(Because of the COM reregistration, I don't want to build a previous version in VS2008 yet to confirm there is a behaviour change. I have confirmed a .NET 3.5 Exe compiled by VS2015 that creates a Thread that throws an unhandled exception does process the exception in the UnhandledException event.)

The workaround for me is to add a Catch to an existing Try Finally in EnusreTaskCompletes that already attempted to at least log that the thread was terminated before the task was complete, and call the UnhandledExceptionHandler myself.

(*The actual problem was a missing table in a database that's only used when you've run out of credit -- I didn't add that code, or the table. This threw an exception that was not caught by the UnhandledExceptionHandler, making it rather hard to debug -- the SQL Server Profiler finally found the issue.)

Mark Hurd
  • 10,665
  • 10
  • 68
  • 101
  • I think I might have to [reset my Visual Studio settings](http://stackoverflow.com/a/13073939/256431) because now I'm not getting any break points, or `Debugger.Break`, to work unless I [detach (and reattach) the process](http://stackoverflow.com/a/2671673/256431) at a [Debugger.Launch](http://stackoverflow.com/a/2734204/256431). But I'll try rebooting first. – Mark Hurd Oct 11 '16 at 17:38

3 Answers3

5

It is an impossibly broad topic, at breakneck speed. No, you are never going to observe an UnhandledException event when your code is called from VB6. Passing exceptions across an interop boundary is strictly verboten.

The CLR complies, it uses a catch-em-all exception handler in the CCW to catch the exception. And translates it to an COM-compliant HRESULT error code. Every .NET exception has a distinct Exception.HResult property value.

The VB6 runtime in turn converts them to a VB6 error. You need to use the On Error statement to catch them. The CLR implements IErrorInfo to provide some information, the VB6 Err.Number property has the Exception.HResult value and the Err.Description gives you the Exception.Message property value. You will however not get the Holy Stacktrace and will be seriously inconvenienced if you need the InnerException property to diagnose the mishap. Consider using the AppDomain.FirstChanceException event to log the details.

Debugging such an exception is easy enough. Use the Project > Properties > Debugging > Start external program radio button. Select your compiled VB6 program or the VB6 IDE. Force the debugger to stop on the first-chance exception, in VS2015 use Debug > Windows > Exception Settings and tick "Common Language Runtime exceptions".

Do beware that this mechanism is not in effect for any code you run on your own worker threads. Dealing with those in an interop scenario is, well, tricky. Presumably that's what is getting you confused about the "sometimes it works" scenario.

Do be very careful with SetUnhandledExceptionFilter(), very easy to break .NET code with that. The CLR calls that function as well to install its own handler and uses it to raise some exceptions. Notably NullReferenceException and DivideByZeroException, the kind of exceptions that C# code might want to catch itself. Also the really bad stuff, like AccessViolationException, you never want to catch that. It could also interfere with VB6 errors btw, the runtime also uses SEH to raise exceptions.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
1

The Short (Probable) Answer, Looks like, an exception occurring in Form.Load doesn't get routed to Application.ThreadException or AppDomain.CurrentDomain.UnhandledException without a debugger attached.

The More accurate Answer/Story This is how I solved a similar problem. I can't say for sure how it does it, but here is what I think. Improvement suggestions are welcome.

The three events,

  1. AppDomain.CurrentDomain.FirstChanceException
  2. AppDomain.CurrentDomain.UnhandledException
  3. and Application.ThreadException

accumulatively catch most of the exceptions but not on a global scope (as said earlier). In one of my applications, I used a combination of these to catch all kinds of exceptions and even the unmanaged code exceptions like DirectX exception (through SharpDX). All exceptions, whether they are caught or not, seem to be invoking FirstChanceException without a doubt.

AppDomain.CurrentDomain.FirstChanceException += MyFirstChanceExceptionHandler;
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); // not sure if this is important or not.
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; // can't use Lambda here. need to Unsub this event later.
Application.ThreadException += (s, e) => MyUnhandledExceptionHandler(e.Exception);

static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    MyUnhandledExceptionHandler((Exception)e.ExceptionObject);
}
private void CurrentDomain_FirstChanceException(object sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs eventArgs)
{
    // detect the pattern of the exception which we won't be able to get in Fatal events.
    if (eventArgs.Exception.Message.StartsWith("HRESULT"))
        MyUnhandledExceptionHandler(eventArgs.Exception);
}

and the handler looks like

static void MyUnhandledExceptionHandler(Exception ex)
{
    AppDomain.CurrentDomain.UnhandledException -= MyUnhandledExceptionHandler;  // this is important. Any exception occuring in the logging mechanism can cause a stack overflow exception which triggers the window's own JIT message/App crash message if Win JIT is not available.
    // LogTheException()
    // Collect user data
    // inform the user in a civil way to restart/close the app
    Environment.Exit(0);
}

Unmanaged code exceptions like DirectX exceptions appeared only in FirstChanceException where I had to decide for myself if the exception was fatal or not. I then use MyUnhandledExceptionHandler to log and let the user know in a friendly way that everything was "under control".

IMPORTANT NOTE! The scheme still didn't catch one kind of exception. It did appear in FirstChanceException, but it was hard to distinguish it from other kinds of exceptions hitting this handler. Any exception occurring directly in Form.Load had this different behavior. When the VS debugger was attached, these were routed to the UnhandledException event. But without a debugger, an old-school windows message will pop up, showing the stack trace of the exception that occurred. The most annoying thing was that it didn't let MyUnhandledExceptionHandlerr get kicked once it was done and the app continued to work in an abnormal state. The final solution I did was to move all the code from Form_load to another thread using MyForm.Load += (s,e) => new Thread(()=>{/* My Form_Load code*/ }).Start();. This way, Application.ThreadException gets triggered which is routed to MyUnhandledExceptionHandler, my safe exit.

Umar Hassan
  • 192
  • 3
  • 11
0

(This is just a comment, especially because it's mostly extending my comment to the question describing a new issue, but it's getting too big.)

I have at least reproduced the problem I'm seeing in all builds (even after resetting my Visual Studio settings, etc.*) with a normal .NET console app in a Release build with "Suppress JIT optimisation on module load (Managed only)" turned OFF and "Just My Code" ON. Clearly, that's almost a fully non-debug run, and it does warn here that breakpoints won't be hit because symbols haven't been loaded.

(Actually, I only later noticed this failure was logged in the Immediate window, which mentioned "Just My Code". I do watch the Debug Output mostly and definitely turn off the redirection to the Immediate window option, but that would have been reset to ON, so I missed it originally.)

This caused me to note that, in its current state, breakpoints for my problematic solution now show the exclamation triangle when added or enabled at runtime, but don't show it on execution alone, like they do for the new .NET only project.

(*)Rebooting, resetting my settings, repairing my installation and deleting my .suo file didn't help.

The one thing I haven't explicitly stated is that, for the problematic solution, I am running VS2015 as administrator (on Windows 10 64-bit), because it needs to register the assembly for COM at the end of the compile. Of course, I've always needed this requirement and clearly before any of these debugging issues were noticed.

I will at some stage try debugging on a Win10 32-bit laptop and see how much of this is repeatable there.

Mark Hurd
  • 10,665
  • 10
  • 68
  • 101