C# 如何遍历.NET try/catch链以决定生成小型转储

C# 如何遍历.NET try/catch链以决定生成小型转储,c#,task-parallel-library,C#,Task Parallel Library,我们的顶级崩溃处理程序遇到了一个混合任务的难题,我们正试图找到一个解决办法。我希望有人有一些想法 我们的工具有一个顶级崩溃处理程序(来自AppDomain的UnhandledException事件),我们用它来用小型转储文件bug报告。它工作得很好。不幸的是,任务在这方面起了作用 我们刚开始使用4.0任务,发现在任务操作执行代码的内部,有一个try/catch,它捕获异常并存储异常,以便传递到任务链。不幸的是,catch(Exception)的存在使堆栈展开,当我们创建迷你转储时,调用站点丢失。

我们的顶级崩溃处理程序遇到了一个混合任务的难题,我们正试图找到一个解决办法。我希望有人有一些想法

我们的工具有一个顶级崩溃处理程序(来自AppDomain的UnhandledException事件),我们用它来用小型转储文件bug报告。它工作得很好。不幸的是,任务在这方面起了作用

我们刚开始使用4.0任务,发现在任务操作执行代码的内部,有一个try/catch,它捕获异常并存储异常,以便传递到任务链。不幸的是,
catch(Exception)
的存在使堆栈展开,当我们创建迷你转储时,调用站点丢失。这意味着我们在崩溃时没有任何局部变量,或者它们已经被收集,等等

异常筛选器似乎是此作业的正确工具。我们可以通过扩展方法将一些任务操作代码封装在过滤器中,在抛出异常时,调用崩溃处理程序代码以使用minidump报告错误。但是,我们不希望对每个异常都这样做,因为在我们自己的代码中可能存在忽略特定异常的try-catch。我们只想在任务中的
catch
要处理崩溃报告时才执行它

有没有办法沿着try/catch处理程序的链条走?我想,如果我能做到这一点,我可以向上走去寻找捕获物,直到击中任务,然后如果是真的,就启动碰撞处理程序

(这似乎不太可能,但我想我还是会问。)

或者,如果有人有更好的想法,我很乐意听到

更新

我创建了一个小的示例程序来演示这个问题。很抱歉,我试着把它尽量短一些,但还是很大/

在下面的示例中,您可以
#定义USETASK
#定义USEWORKITEM
(或无)来测试三个选项之一

在非异步情况和USEWORKITEM情况下,生成的minidump是在调用站点构建的,这正是我们需要的。我可以将它加载到VS中(在浏览了一些以找到正确的线程之后),我看到我在调用站点上拍摄了快照。太棒了

在USETASK情况下,快照是从正在清理任务的终结器线程中获取的。这是在抛出异常很久之后的事情,因此此时获取一个小型转储是没有用的。我可以对任务执行Wait(),以使异常得到更快的处理,或者我可以直接访问它的异常,或者我可以从TestCrash本身的包装中创建minidump,但所有这些都存在相同的问题:因为堆栈已解缠到一个或另一个捕获,所以为时已晚

请注意,我特意在TestCrash中添加了一个try/catch,以演示如何正常处理一些异常,以及如何捕获其他异常。USEWORKITEM和非异步案例正是我们所需要的工作方式。任务几乎做对了!如果我能以某种方式使用一个异常过滤器,让我沿着try/catch链(而不是实际展开)走到任务内部的catch,我可以自己做必要的测试,看看是否需要运行崩溃处理程序。因此,我提出了最初的问题

这是样品

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        AppDomain.CurrentDomain.UnhandledException += (_, __) =>
            {
                using (var stream = File.Create(@"c:\temp\test.dmp"))
                {
                    var process = Process.GetCurrentProcess();
                    MiniDumpWriteDump(
                        process.Handle,
                        process.Id,
                        stream.SafeFileHandle.DangerousGetHandle(),
                        MiniDumpType.MiniDumpWithFullMemory,
                        IntPtr.Zero,
                        IntPtr.Zero,
                        IntPtr.Zero);
                }
                Process.GetCurrentProcess().Kill();
            };
        TaskScheduler.UnobservedTaskException += (_, __) =>
            Debug.WriteLine("If this is called, the call site has already been lost!");

        // must be in separate func to permit collecting the task
        RunTest();

        GC.Collect();
        GC.WaitForPendingFinalizers();
    }

    static void RunTest()
    {

#if USETASK
        var t = new Task(TestCrash);
        t.RunSynchronously();
#elif USEWORKITEM
        var done = false;
        ThreadPool.QueueUserWorkItem(_ => { TestCrash(); done = true; });
        while (!done) { }
#else
        TestCrash();
#endif
    }

    static void TestCrash()
    {
        try
        {
            new WebClient().DownloadData("http://filenoexist");
        }
        catch (WebException)
        {
            Debug.WriteLine("Caught a WebException!");
        }
        throw new InvalidOperationException("test");
    }

    enum MiniDumpType
    {
        //...
        MiniDumpWithFullMemory = 0x00000002,
        //...
    }

    [DllImport("Dbghelp.dll")]
    static extern bool MiniDumpWriteDump(
        IntPtr hProcess,
        int processId,
        IntPtr hFile,
        MiniDumpType dumpType,
        IntPtr exceptionParam,
        IntPtr userStreamParam,
        IntPtr callbackParam);
}

如果某个任务引发未处理的异常,听起来您希望创建一个小型转储。在
unobservedtaskeexception
事件期间,堆栈可能尚未展开:


听起来,如果您可以将顶级任务包装在异常过滤器(用VB.NET编写?)中,您就可以随心所欲了。由于您的筛选器将在
任务
自身的异常筛选器之前运行,因此只有在任务中除
任务
获得异常之前没有其他内容处理异常时,才会调用该筛选器

这是一个工作样本。在VB文件中创建名为
ExceptionFilter
的VB库项目:

Imports System.IO
Imports System.Diagnostics
Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices

Public Module ExceptionFilter
    Private Enum MINIDUMP_TYPE
        MiniDumpWithFullMemory = 2
    End Enum

    <DllImport("dbghelp.dll")>
    Private Function MiniDumpWriteDump(
            ByVal hProcess As IntPtr,
            ByVal ProcessId As Int32,
            ByVal hFile As IntPtr,
            ByVal DumpType As MINIDUMP_TYPE,
            ByVal ExceptionParam As IntPtr,
            ByVal UserStreamParam As IntPtr,
            ByVal CallackParam As IntPtr) As Boolean
    End Function

    Function FailFastFilter() As Boolean
        Dim proc = Process.GetCurrentProcess()
        Using stream As FileStream = File.Create("C:\temp\test.dmp")
            MiniDumpWriteDump(proc.Handle, proc.Id, stream.SafeFileHandle.DangerousGetHandle(),
                              MINIDUMP_TYPE.MiniDumpWithFullMemory, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero)
        End Using
        proc.Kill()
        Return False
    End Function

    <Extension()>
    Public Function CrashFilter(ByVal task As Action) As Action
        Return Sub()
                   Try
                       task()
                   Catch ex As Exception When _
                       FailFastFilter()
                   End Try
               End Sub
    End Function
End Module
我运行了C#程序,打开了DMP文件,并检查了调用堆栈。
TestCrash
函数在堆栈上(向上几帧),当前行为
throw new


仅供参考,我想我会使用
Environment.FailFast()
来处理您的小型转储/终止操作,但这在您的工作流程中可能不起作用。

想到两种可能性:

您可以使用来充当调试器,并检测哪个
catch
块将捕获异常

您可以将每个“关键任务”
Action
/
Func
包装在自己的
try
/
catch
包装中

不过,这两种方法中的任何一种都需要付出相当大的努力。为了回答你的具体问题,我认为不可能用手在堆栈上行走

<强>编辑:更多关于配置API方法:您可能需要考虑,它是为单元测试编写的,但公开了可以在运行时使用的钩子(是通过Type的API编写的库)。另外还有一个关于使用评测API进行代码注入的问题,但是IMO TypeMock会比您自己做省钱。

我在并行计算论坛上发布了一篇文章。Stephen Toub建议的解决方法是在任务主体周围添加一个异常处理程序,用于捕获所有异常和调用
Environment.FailFast
。(或者它可以用小型转储文件提交错误报告,等等)

例如:

public static Action FailOnException(this Action original)
{
    return () => 
    { 
        try { original(); } 
        catch(Exception ex) { Environment.FailFast("Unhandled exception", ex); }
    };
}
然后,不要写:

Task.Factory.StartNew(action);
你可以写:

Task.Factory.StartNew(action.FailOnException());
因为这保证是默认任务异常处理程序下面的一层,所以您不需要遍历异常处理链,也不需要担心其他代码是否会处理它。(如果捕获并处理了异常,它将不会到达此处理程序。)

或者,因为(正如Gabe在评论中所指出的那样)这将导致
最终运行
块,所以应该可以(正如我在您的问题中所建议的)添加(
Task.Factory.StartNew(action.FailOnException());