防止从 BeginInvoke 抛出时丢弃外部异常

发布于 2024-12-21 23:28:48 字数 2978 浏览 1 评论 0原文

我有一个 Application.ThreadException< 的处理程序/a>,但我发现异常并不总是正确地传递给它。具体来说,如果我从 BeginInvoke 回调中抛出异常与内部异常,我的 ThreadException 处理程序不会获取 outer 异常 - - 它只获取内部异常。

示例代码:

public Form1()
{
    InitializeComponent();
    Application.ThreadException += (sender, e) =>
        MessageBox.Show(e.Exception.ToString());
}
private void button1_Click(object sender, EventArgs e)
{
    var inner = new Exception("Inner");
    var outer = new Exception("Outer", inner);
    //throw outer;
    BeginInvoke(new Action(() => { throw outer; }));
}

如果我取消注释 throw external; 行并单击按钮,则消息框将显示外部异常(及其内部异常):

System.Exception:外部 --->系统异常:内部
--- 内部异常堆栈跟踪结束 ---
在 C:\svn\trunk\Code Base\Source.NET\WindowsFormsApplication1\Form1.cs 中的 WindowsFormsApplication1.Form1.button1_Click(Object sender, EventArgs e) 处:第 55 行
在 System.Windows.Forms.Control.OnClick(EventArgs e)
在 System.Windows.Forms.Button.OnClick(EventArgs e)
在 System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
在 System.Windows.Forms.Control.WmMouseUp(Message&m, MouseButtons 按钮, Int32 点击)
在 System.Windows.Forms.Control.WndProc(Message&m)
在 System.Windows.Forms.ButtonBase.WndProc(Message&m)
在 System.Windows.Forms.Button.WndProc(Message&m)
在 System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message&m)
在 System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message&m)
在System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd,Int32 msg,IntPtr wparam,IntPtr lparam)

但如果 throw external; 位于 BeginInvoke 调用内,如在上面的代码中,ThreadException 处理程序获取内部异常。在调用 ThreadException 之前,外部异常被剥离,我得到的只是:

系统异常:内部

(这里没有调用堆栈,因为inner从未被抛出。在一个更现实的示例中,我捕获了一个异常并将其包装以重新抛出,将会有一个调用堆栈。)

如果我使用 SynchronizationContext.Current.Post 而不是 BeginInvoke,也会发生同样的事情:外部异常被剥离,并且 ThreadException 处理程序只获取内部异常。

我尝试在外部包裹更多层异常,以防它只是剥离最外层的异常,但这没有帮助:显然某个地方有一个循环沿着 while (e.InnerException != null) e = e.InnerException;

我使用 BeginInvoke 是因为我的代码需要抛出未处理的异常以立即由 ThreadException 处理,但此代码位于 catch< /code> 阻止调用堆栈的更高层(具体来说,它位于 Task 的操作内部,并且 Task 将捕获异常并阻止其传播)。我正在尝试使用 BeginInvoke 来延迟 throw 直到下次在消息循环中处理消息时,此时我不再处于该 catch< 内/代码>。我不执着于 BeginInvoke 的特定解决方案;我只是想抛出一个未处理的异常。

如何导致异常(包括其内部异常)到达 ThreadException,即使我位于其他人的 catch-all 内部?

(由于程序集依赖性,我无法直接调用我的 ThreadException 处理程序方法:该处理程序由 EXE 的启动代码挂钩,而我当前的问题是在较低层 DLL 中。)

I have a handler for Application.ThreadException, but I'm finding that exceptions aren't always getting passed to it correctly. Specifically, if I throw an exception-with-inner-exception from a BeginInvoke callback, my ThreadException handler doesn't get the outer exception -- it only gets the inner exception.

Example code:

public Form1()
{
    InitializeComponent();
    Application.ThreadException += (sender, e) =>
        MessageBox.Show(e.Exception.ToString());
}
private void button1_Click(object sender, EventArgs e)
{
    var inner = new Exception("Inner");
    var outer = new Exception("Outer", inner);
    //throw outer;
    BeginInvoke(new Action(() => { throw outer; }));
}

If I uncomment the throw outer; line and click the button, then the messagebox shows the outer exception (along with its inner exception):

System.Exception: Outer ---> System.Exception: Inner
--- End of inner exception stack trace ---
at WindowsFormsApplication1.Form1.button1_Click(Object sender, EventArgs e) in C:\svn\trunk\Code Base\Source.NET\WindowsFormsApplication1\Form1.cs:line 55
at System.Windows.Forms.Control.OnClick(EventArgs e)
at System.Windows.Forms.Button.OnClick(EventArgs e)
at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
at System.Windows.Forms.Control.WndProc(Message& m)
at System.Windows.Forms.ButtonBase.WndProc(Message& m)
at System.Windows.Forms.Button.WndProc(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

But if the throw outer; is inside a BeginInvoke call, as in the above code, then the ThreadException handler only gets the inner exception. The outer exception gets stripped away before ThreadException is called, and all I get is:

System.Exception: Inner

(There's no call stack here because inner never got thrown. In a more realistic example, where I caught one exception and wrapped it to re-throw, there would be a call stack.)

The same thing happens if I use SynchronizationContext.Current.Post instead of BeginInvoke: the outer exception is stripped off, and the ThreadException handler only gets the inner exception.

I tried wrapping more layers of exceptions around the outside, in case it was just stripping off the outermost exception, but it didn't help: apparently somewhere there's a loop doing something along the lines of while (e.InnerException != null) e = e.InnerException;.

I'm using BeginInvoke because I've got code that needs to throw an unhandled exception to be immediately handled by ThreadException, but this code is inside a catch block higher up the call stack (specifically, it's inside the action for a Task, and Task will catch the exception and stop it from propagating). I'm trying to use BeginInvoke to delay the throw until the next time messages are processed in the message loop, when I'm no longer inside that catch. I'm not attached to the particular solution of BeginInvoke; I just want to throw an unhandled exception.

How can I cause an exception -- including its inner exception -- to reach ThreadException even when I'm inside somebody else's catch-all?

(I can't call my ThreadException-handler method directly, due to assembly dependencies: the handler is hooked by the EXE's startup code, whereas my current problem is in a lower-layer DLL.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

谁把谁当真 2024-12-28 23:28:48

一种方法是将内部异常引用放入自定义属性或 Data 字典 - 即保留 InnerException 属性为 null,并以其他方式携带引用。

当然,这需要建立某种可以在抛出代码和处理代码之间共享的约定。最好的方法可能是在两个代码段都引用的项目中定义具有自定义属性的自定义异常类。

示例代码(尽管它需要更多注释来解释为什么它正在做这些疯狂的事情):

public class ExceptionDecorator : Exception {
    public ExceptionDecorator(Exception exception) : base(exception.Message) {
        Exception = exception;
    }
    public Exception Exception { get; private set; }
}

// To throw an unhandled exception without losing its InnerException:
BeginInvoke(new Action(() => { throw new ExceptionDecorator(outer); }));

// In the ThreadException handler:
private void OnUnhandledException(object sender, ThreadExceptionEventArgs e) {
    var exception = e.Exception;
    if (exception is ExceptionDecorator)
        exception = ((ExceptionDecorator) exception).Exception;
    // ...
}

One way to do it is to put the inner-exception reference in a custom property or the Data dictionary -- i.e., leave the InnerException property null, and carry the reference some other way.

Of course, this requires establishing some kind of convention that can be shared between the throwing code and the handling code. The best would probably be to define a custom exception class with a custom property, in a project that's referenced by both pieces of code.

Sample code (though it needs more comments to explain why it's doing the crazy things it's doing):

public class ExceptionDecorator : Exception {
    public ExceptionDecorator(Exception exception) : base(exception.Message) {
        Exception = exception;
    }
    public Exception Exception { get; private set; }
}

// To throw an unhandled exception without losing its InnerException:
BeginInvoke(new Action(() => { throw new ExceptionDecorator(outer); }));

// In the ThreadException handler:
private void OnUnhandledException(object sender, ThreadExceptionEventArgs e) {
    var exception = e.Exception;
    if (exception is ExceptionDecorator)
        exception = ((ExceptionDecorator) exception).Exception;
    // ...
}
泅人 2024-12-28 23:28:48

我假设您在 x64 Windows 系统上看到此行为,并且这是 x64 Windows 的一个相当未知的实现细节。阅读它 这里

这篇文章详细介绍了如何通过应用一些修补程序来解决这个问题,据称该修补程序是随 Win7 SP1 一起提供的,但我遇到了这个问题几周前的 Win7 SP1。

此外,您可以附加到 AppDomain.FirstChanceException 事件,该事件给出在将异常传递给 CLR 进行处理之前,您可以访问每个异常

I am assuming that you are seeing this behaviour on an x64 Windows system and this is a - rather unknown - implementation detail of x64 Windows. Read up on it here

The article goes into details on how to solve this problem by applying some hotfix, that was allegedly shipped with Win7 SP1, but I ran into this issue a few weeks back on Win7 SP1.

Additionally you could attach to AppDomain.FirstChanceException event which gives you access to every exception before it is passed to the CLR for handling

心头的小情儿 2024-12-28 23:28:48

将异常传播到更高层的推荐方法(除了通过等待任务隐式重新抛出之外)是删除任务主体中的全部内容,并使用 Task.ContinueWith,指定TaskContinuationOptions.OnlyOnFaulted。如果您正在通过中间层工作并且无法访问任务,则可以进一步将其包装在您自己的 UnhandledException 事件中以向上传递 Exception 对象。

The recommended way to propagate the Exception to a higher layer (aside from implicitly rethrowing by Waiting on the Task) is to remove the catch-all in the Task body and instead register a Fault continuation on the Task using Task.ContinueWith, specifying TaskContinuationOptions.OnlyOnFaulted. If you're working through an intermediate layer and don't have access to the Task, you can further wrap this in your own UnhandledException events to pass the Exception object upward.

缘字诀 2024-12-28 23:28:48

这无疑是一种 hack,但它是我能想到的最佳解决方案,它支持 WinForms 中的全局异常处理和所有异常,甚至包括内部异常。

特别感谢 yas4891 的回答启发了这个解决方案。

Program.cs 中:

internal static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();

        AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
        Application.ThreadException += Application_ThreadException;
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException, true);

        Application.Run(new MyMainForm());
    }


    private static void CurrentDomain_FirstChanceException(object sender, FirstChanceExceptionEventArgs e)
    {
        _outermostExceptionCache.AddException(e.Exception);
    }


    private static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
    {
        Exception exception = null;
        if (e?.Exception != null)
            exception = _outermostExceptionCache.GetOutermostException(e.Exception);
        // Handle exception
    }


    private static OutermostExceptionCache _outermostExceptionCache = new();
}

为此,您需要 OutermostExceptionCache 类:

public class OutermostExceptionCache
{
    public void AddException(Exception ex)
    {
        if ((ex != null) && (ex is not TargetInvocationException))
        {
            Exception innermostException = GetInnermostException(ex);
            lock (_syncRoot)
            {
                RemoveOldEntries();
                _cache[innermostException] = new CacheEntry(ex);
            }
        }
    }


    public Exception GetOutermostException(Exception ex)
    {
        Exception innermostException = GetInnermostException(ex);
        Exception outermostException = null;
        lock (_syncRoot)
        {
            if (_cache.TryGetValue(innermostException, out CacheEntry entry))
            {
                outermostException = entry.Exception;
                _cache.Remove(innermostException);
            }
            else
            {
                outermostException = ex;
            }
        }
        return outermostException;
    }


    private void RemoveOldEntries()
    {
        DateTime now = DateTime.Now;
        foreach (KeyValuePair<Exception, CacheEntry> pair in _cache)
        {
            TimeSpan timeSinceAdded = now - pair.Value.AddedTime;
            if (timeSinceAdded.TotalMinutes > 3)
                _cache.Remove(pair.Key);
        }
    }


    private Exception GetInnermostException(Exception ex)
    {
        return ex.GetBaseException() ?? ex;
    }


    private readonly object _syncRoot = new();
    private readonly Dictionary<Exception, CacheEntry> _cache = new();


    private class CacheEntry
    {
        public CacheEntry(Exception ex)
        {
            Exception = ex;
            AddedTime = DateTime.Now;
        }


        public Exception Exception { get; }
        public DateTime AddedTime { get; }
    }
}

其工作方式是在运行时甚至在抛出异常时监视每个异常将异常冒泡到最近的 catch 块。每次抛出异常时,都会将其添加到缓存中,并按最里面(即基本)异常进行索引。因此,当捕获异常并抛出新异常时,原始异常作为其内部异常,缓存将使用该外部异常进行更新。然后,当为 Application.ThreadException 事件处理程序提供未包装的最内层异常时,该处理程序可以从缓存中查找最外层异常。

注意:由于即使是本地捕获的异常也会添加到缓存中(因此永远不会通过调用 GetOutermostException 来删除),因此它会为每个异常添加时间戳,并自动放弃任何早于 3 分钟的异常。这是一个任意超时,可以根据需要进行调整。如果超时时间太短,可能会导致调试问题,因为如果在调试器中暂停进程太久(在引发异常之后但在处理异常之前),可能会导致异常处理恢复为仅处理最里面的异常。 )。

This is admittedly a hack, but it's the best solution I was able to come up with which supports both global exception handling in WinForms and all exceptions, even with inner exceptions.

Special thanks to yas4891 for the answer which inspired this workaround solution.

In the Program.cs:

internal static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();

        AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
        Application.ThreadException += Application_ThreadException;
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException, true);

        Application.Run(new MyMainForm());
    }


    private static void CurrentDomain_FirstChanceException(object sender, FirstChanceExceptionEventArgs e)
    {
        _outermostExceptionCache.AddException(e.Exception);
    }


    private static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
    {
        Exception exception = null;
        if (e?.Exception != null)
            exception = _outermostExceptionCache.GetOutermostException(e.Exception);
        // Handle exception
    }


    private static OutermostExceptionCache _outermostExceptionCache = new();
}

And for that you'll need the OutermostExceptionCache class:

public class OutermostExceptionCache
{
    public void AddException(Exception ex)
    {
        if ((ex != null) && (ex is not TargetInvocationException))
        {
            Exception innermostException = GetInnermostException(ex);
            lock (_syncRoot)
            {
                RemoveOldEntries();
                _cache[innermostException] = new CacheEntry(ex);
            }
        }
    }


    public Exception GetOutermostException(Exception ex)
    {
        Exception innermostException = GetInnermostException(ex);
        Exception outermostException = null;
        lock (_syncRoot)
        {
            if (_cache.TryGetValue(innermostException, out CacheEntry entry))
            {
                outermostException = entry.Exception;
                _cache.Remove(innermostException);
            }
            else
            {
                outermostException = ex;
            }
        }
        return outermostException;
    }


    private void RemoveOldEntries()
    {
        DateTime now = DateTime.Now;
        foreach (KeyValuePair<Exception, CacheEntry> pair in _cache)
        {
            TimeSpan timeSinceAdded = now - pair.Value.AddedTime;
            if (timeSinceAdded.TotalMinutes > 3)
                _cache.Remove(pair.Key);
        }
    }


    private Exception GetInnermostException(Exception ex)
    {
        return ex.GetBaseException() ?? ex;
    }


    private readonly object _syncRoot = new();
    private readonly Dictionary<Exception, CacheEntry> _cache = new();


    private class CacheEntry
    {
        public CacheEntry(Exception ex)
        {
            Exception = ex;
            AddedTime = DateTime.Now;
        }


        public Exception Exception { get; }
        public DateTime AddedTime { get; }
    }
}

The way this works is by watching every exception, as it is thrown, before the runtime even bubbles the exception up to the nearest catch block. Each time an exception is thrown, it is added to a cache, indexed by the innermost (i.e. base) exception. Therefore, when an exception is caught and a new exception is thrown, with the original one as its inner exception, the cache is updated with that outer exception. Then, when Application.ThreadException event handler is provided with the unwrapped, innermost, exception, the handler can look up the outermost one from the cache.

Note: Since even locally-caught exceptions will get added to the cache (and therefore never removed via a call to GetOutermostException), it timestamps each one and automatically ditches any that are older than 3 minutes. That's an arbitrary timeout which can be adjusted as needed. If you make the timeout too short, it could cause problems with debugging since it can cause the exception handling to revert to handling only the innermost exception if you pause the process too long in the debugger (after the exception is thrown but before it is handled).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文