Как пройти цепочку try/catch.NET, чтобы решить создать minidump

Мы столкнулись с проблемой смешивания задач с нашим обработчиком аварийных ситуаций верхнего уровня и пытаемся найти обходной путь. Я надеюсь, у кого-то есть некоторые идеи.

В наших инструментах используется обработчик аварийных ситуаций верхнего уровня (из события AppDomain UnhandledException), который мы используем для создания отчетов об ошибках с помощью мини-дисков. Он работает чудесно. К сожалению, Задачи бросают в этом ключ.

Мы только начали использовать 4.0 Tasks и обнаружили, что внутри кода выполнения задачи Task есть try/catch, который захватывает исключение и сохраняет его для передачи вниз по цепочке задач. К сожалению, существование catch (Exception) раскручивает стек, и когда мы создаем мини-накопитель, сайт вызова теряется. Это означает, что мы не имеем ни одной из локальных переменных во время сбоя, или они были собраны и т.д.

Фильтры исключений, по-видимому, являются правильным инструментом для этой работы. Мы могли бы обернуть некоторый код действия задачи в фильтр с помощью метода расширения, а в случае исключения, вызванного вызовом нашего кода обработчика сбоев, сообщить об ошибке с мини-помпой. Однако мы не хотим делать это при каждом исключении, потому что в нашем собственном коде может быть try-catch, который игнорирует определенные исключения. Мы хотим сделать отчет о сбое, если catch в Task будет обрабатывать его.

Есть ли способ подойти к цепочке обработчиков try/catch? Я думаю, что если бы я мог это сделать, я мог бы подняться вверх, ища уловы, пока не нажмет "Задачу", а затем уволит обработчик аварии, если это правда.

(Это кажется длинным выстрелом, но я решил, что я все равно прошу.)

Или, если у кого-нибудь есть лучшие идеи, я бы хотел их услышать!

UPDATE

Я создал небольшую пробную программу, демонстрирующую проблему. Мои извинения, я постарался сделать это как можно короче, но он все еще большой.:/

В приведенном ниже примере вы можете #define USETASK или #define USEWORKITEM (или none) проверить один из трех вариантов.

В случае не-асинхронного и USEWORKITEM генерируемый minidump строится на сайте вызова точно так, как нам нужно. Я могу загрузить его в VS и (после некоторого просмотра, чтобы найти нужный поток), я вижу, что у меня есть моментальный снимок, сделанный на сайте вызова. Высокий.

В случае USETASK моментальный снимок извлекается из потока финализатора, который очищает задачу. Это долго после того, как исключение было выброшено, и поэтому захват мини-насоса в этот момент бесполезен. Я могу сделать Wait() в задаче, чтобы сделать исключение обработанным раньше, или я могу получить доступ к его Исключению напрямую, или я могу создать minidump из оболочки вокруг самого TestCrash, но все они все еще имеют одинаковую проблему: это слишком поздно, потому что стек был размотан до одного или другого.

Обратите внимание, что я преднамеренно поставил try/catch в TestCrash, чтобы продемонстрировать, как мы хотим, чтобы некоторые исключения обрабатывались нормально, а другие были пойманы. Случаи USEWORKITEM и non-async работают именно так, как нам нужно. Задачи почти все делают правильно! Если бы я мог каким-то образом использовать фильтр исключений, который позволяет мне подойти к цепочке try/catch (без фактического размотки), пока я не нажму на catch внутри Task, я мог бы сам сделать необходимые тесты, чтобы проверить, нужно ли мне запускать обработчик сбоя или нет. Отсюда мой оригинальный вопрос.

Здесь образец.

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);
}

Ответы

Ответ 1

Похоже, если вы можете обернуть задачи верхнего уровня в фильтр исключений (написанный на VB.NET?), вы сможете делать то, что хотите. Так как ваш фильтр будет запускаться непосредственно перед собственным фильтром исключений Task, он будет вызван только в том случае, если ничто в вашей задаче не обработает исключение, но до того, как Task получит его.

Здесь рабочий образец. Создайте проект библиотеки 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

Затем создайте проект С# и добавьте ссылку на ExceptionFilter. Здесь программа, которую я использовал:

using System;
using System.Diagnostics;
using System.Net;
using System.Threading.Tasks;
using ExceptionFilter;

class Program
{
    static void Main()
    {
        new Task(new Action(TestCrash).CrashFilter()).RunSynchronously();
    }

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

Я запустил программу на С#, открыл файл DMP и проверил стек вызовов. Функция TestCrash находилась в стеке (несколько кадров вверх) с throw new в качестве текущей строки.

FYI, я думаю, что я использовал бы Environment.FailFast() в вашей операции minidump/kill, но это может не сработать и в вашем рабочем процессе.

Ответ 2

Две возможности spring:

Вы можете использовать API профилирования, чтобы действовать как отладчик и обнаруживать, какой блок catch должен поймать исключение.

Вы можете обернуть каждую "критическую задачу" Action/Func в свою собственную упаковку try/catch.

Тем не менее, один из них - это довольно много работы. Чтобы ответить на ваш конкретный вопрос, я не думаю, что можно ходить по стопке вручную.

EDIT: Подробнее о API-интерфейсе профилирования: вы можете рассмотреть TypeMock, который был (CThru - это библиотека, написанная над API TypeMock, и там по крайней мере один человек, который использовал TypeMock во время выполнения). Там также статья MSDN об использовании API-интерфейса профилирования для ввода кода, но IMO TypeMock сэкономит вам деньги, сделав это самостоятельно.

Ответ 3

Похоже, вы хотите создать minidump, если задача выдает необработанное исключение. Возможно, стек еще не размотан во время события UnobservedTaskException:

Ответ 4

Я разместил аналогичный вопрос на параллельных компьютерных форумах. Обходной путь, предложенный там (Стивеном Тубом), заключался в том, чтобы добавить обработчик исключений вокруг тела Задачи, который улавливает все исключения и вызывает 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());

Поскольку это гарантированно будет на один уровень ниже обработчика исключений по умолчанию, вам не нужно ходить в цепочке обработки исключений, и вам не нужно беспокоиться, если какой-либо другой код обработает его. (Если исключение поймано и обработано, оно не достигнет этого обработчика.)

В качестве альтернативы, поскольку (как отмечает Гейб в комментариях) это приведет к запуску блоков finally, должно быть возможно (как было предложено в вашем вопросе, я считаю) добавить (используя IL, VB.NET или динамическую методы) фильтр исключений (в этом методе расширения), который обнаруживает необработанные исключения. Поскольку вы знаете, что исключения, обработанные в блоке catch, не достигнут этого уровня, и поскольку единственный обработчик исключений выше вас - это сам Task, должно быть хорошо прекратить процесс в этот момент (и сообщите о необработанном исключении). Единственное исключение (без каламбура) было бы, если бы вы ожидали возможности исключения и проверки свойства Task.Exception в коде, который создал задачу. В этом случае вы не захотите рано завершить процесс.