Как пройти цепочку 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
в коде, который создал задачу. В этом случае вы не захотите рано завершить процесс.