Пазл для обработки исключений

Недавно я столкнулся с поведением, которое я никогда раньше не видел. Я не могу понять, что происходит, скорее всего, из-за отсутствия фундаментальных знаний в отношении внутренней обработки Exception Handling - или, может быть, я просто пропущу что-то очевидное.

Недавно я добавил обработку исключений в приложение как своего рода резерв в случае необработанных исключений. Я в основном обрабатываю ThreadException и UnhandledException, как показано ниже:

// Add the event handler for handling UI thread exceptions to the event.
Application.ThreadException += new ThreadExceptionEventHandler(ExceptionHandler.OnUIThreadException);

// Set the unhandled exception mode to force all Windows Forms errors to go through
// our handler.
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);

// Add the event handler for handling non-UI thread exceptions to the event. 
AppDomain.CurrentDomain.UnhandledException +=
    new UnhandledExceptionEventHandler(ExceptionHandler.OnUnhandledException);

// Runs the application.
Application.Run(new ErrorHandlerForm());

Некоторый другой фрагмент кода, который у меня был в приложении, уже улавливал исключения - и поскольку у меня не было обработки исключений, я просто перестраивал исключение, чтобы убедиться, что он не проглотил:

//code in some method of the Program
try
{
   foo.SomeFooCall();
}
catch(Exception ex)
{
  logger.Log(ex.Message);
  // I don't swallow!
  throw;
}

Как только у меня была обработка исключений на месте (которая также регистрируется), я должен был удалить этот блок try catch выше, но я забыл сделать это, и я испытываю странное поведение, которое является предметом этого вопроса.

Когда и исключение выбрасывается где-то внутри вызова foo, он, очевидно, попадает в код выше, записывается и снова бросается. На этом этапе ExceptionHandling запускается, происходит ли какое-то ведение журнала и уведомление (простое сообщение), а затем идет Application.Exit(). Что будет дальше, так это то, что приложение вернется к тому же throw, что приведет к обработке ошибок с теми же результатами, и это будет продолжаться несколько раз, пока оно не сработает, предположительно, потому что трассировка стека заполнена или она каким-то образом обнаруживает бесконечный цикл.

РЕДАКТОР: Вышеприведенный режим находится в режиме отладки - если я его запустил, он будет обрабатывать исключение один раз (показать сообщение, журнал и т.д.), тогда он просто сработает (я предполагаю, что переполнение стека).

Я ожидаю, что ответ на этот вопрос может быть тривиальным (или я могу пропустить что-то очевидное), но любые указатели/объяснения будут высоко оценены.

EDIT: Методы обработчиков исключений принимают оба метода вызова к методу OnException, который выглядит примерно так:

private void OnUIThreadException(object sender, ThreadExceptionEventArgs e)
{
   OnException(e.Exception);
}

private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
   OnException((Exception)e.ExceptionObject);
}

private void OnException(Exception exception)
{
   MessageBox.Show("Fatal Exception: " + exception.Message);

   logger.Log(LoggingLevel.FATAL, "myLousyApp", exception.Message);

   Application.Exit();
}

Я на самом деле делаю smt больше, чем просто, - например, спрашивать у пользователя, хочет ли он перезапустить приложение, и если он перезапустил его с идентификатором процесса как cmd arg, так что когда он перезагрузится, он будет ждать, пока старый процесс exit (он защищен от экземпляров дубликатов через мьютекс). Но для этого вопроса это не имеет значения, так как я не перезапускаю приложение, когда испытываю описанное поведение.

EDIT: Я создал еще одно простое приложение для воспроизведения этих условий - у меня есть простой компонент, который генерирует исключения (я выбрасываю произвольное количество исключений в цикле), но во всех моих тестах Application.Exit приложение просто прекращает работу, и я не могу воспроизвести его. Озадаченный тем, что я должен искать!

Ответы

Ответ 1

tl; dr: Это отладчик. Отсоедините, и вы не получите это странное поведение.


Хорошо. Я немного экспериментировал с новым проектом Brand Spankin'a и придумал результат. Я начну с публикации кода, чтобы вы тоже могли присоединиться к веселью и увидеть его на собственном опыте.

Teh Codez

plz напишите им мне (не связанный)

Form1.cs

Example form
Вам понадобятся две кнопки в форме. Подпишите их соответствующим образом, чтобы он ослепительно очевиден, что вы делаете.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        throw new InvalidOperationException("Exception thrown from UI thread");
    }

    private void button2_Click(object sender, EventArgs e)
    {
        new Thread(new ThreadStart(ThrowThreadStart)).Start();
    }

    private static void ThrowThreadStart()
    {
        throw new InvalidOperationException("Exception thrown from background thread");
    }
}

Program.cs

static class Program
{
    [STAThread]
    static void Main()
    {
        AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
        Application.ThreadException += Application_ThreadException;
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException, false);

        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }

    static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
    {
        if (e.Exception != null)
        {
            MessageBox.Show(string.Format("+++ Application.ThreadException: {0}", e.Exception.Message));
        }
        else
        {
            MessageBox.Show("Thread exception event fired, but object was not an exception");
        }
    }

    static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        Exception ex = e.ExceptionObject as Exception;

        if (ex != null)
        {
            MessageBox.Show(string.Format("*** AppDomain.UnhandledException: {0}", ex.Message));
        }
        else
        {
            MessageBox.Show("Unhandled exception event fired, but object was not an exception");
        }
    }
}

Файл проекта

Отключите хостинг-процесс, иначе AppDomain (и сами формы) не будет выгружен между сеансами отладки, что сделает строку Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException, false); throw InvalidOperationException, если вы измените аргумент UnhandledExceptionMode между прогонами. EDIT: или вообще, если установлено CatchException

Это не является строго необходимым для этого исследования, но если вы собираетесь играть и изменить UnhandledExceptionMode - что я ожидаю, вы, вероятно, будете, если вы сами запускаете этот код - этот параметр избавит вас от боли. Disable the hosting process

Тестирование

Внутри отладчика

Вбросить в поток пользовательского интерфейса

  • Нажмите "Бросить в пользовательский интерфейс"
  • Получите хелпер "необработанного исключения" в отладчике
  • F5 для продолжения выполнения
  • Диалог покажет, что обработчик приложения получил событие исключения из потока пользовательского интерфейса.
  • Нажмите "ОК"
  • Приложение не сбой, поэтому не стесняйтесь ополаскивать и повторять.

Выбросить фоновый поток

  • Нажмите "Бросок в фоновом режиме"
  • Диалог покажет, что обработчик AppDomain получил событие исключения из фонового потока
  • Получите хелпер "необработанного исключения" в отладчике
  • F5 для продолжения выполнения
  • goto 2. На самом деле.

Кажется, что обработчик AppDomain превосходит отладчик по любой причине. Однако после того, как приложение AppDomain с этим связано, отладчик действительно может получить необработанное исключение. Но то, что происходит дальше, вызывает недоумение: обработчик AppDomain снова запускается. И опять. И снова, до бесконечности. Помещение контрольной точки в обработчик указывает, что это не рекурсивно (по крайней мере, не в .NET), поэтому это, вероятно, не закончится переполнением стека. The background thread

Теперь попробуем...

За пределами отладчика

поток пользовательского интерфейса

Такая же процедура, тот же результат - за исключением того, что помощник отладчика заметно отсутствовал. Программа все еще не сработала, потому что UnhandledExceptionMode.CatchException делает то, что она говорит, что она будет делать, и обрабатывает исключение "внутренне" (по крайней мере, в Формах), а не увеличивая его до Feds AppDomain.

Фоновая нить

Теперь это интересно.

  • Нажмите "Бросок в фоновом режиме"
  • Диалог покажет, что обработчик AppDomain получил событие исключения из фонового потока
  • Получить диалоговое окно сбоя Windows
    Windows crash dialog
  • Нажмите Debug, чтобы вырвать исключение с помощью отладки JIT
  • Получить хелпер с необработанным исключением
  • F5 продолжить
  • Программа выходит

Во-первых, AppDomain не входит в циклы, как при подключенном отладчике, и, во-вторых, приведение отладчика точно в срок из диалогового окна ошибки Windows не вызывает это странное поведение.

Заключение

Кажется, что отладчик делает что-то странное относительно необработанных исключений, пробираясь к AppDomain. Я мало знаю о том, как отладчик делает свою магию, поэтому я не буду спекулировать, но цикл только возникает, когда отладчик подключен, поэтому, если цикл является проблемой, отсоединение является одним обходной путь, который вы могли бы использовать (возможно, используя Debugger.Launch(), чтобы дать себе время для повторного подключения позже, если вы этого потребуете).

< 3

Ответ 2

Это просто опция VS, которая включена по умолчанию (применимо только при отладке). Так что нет необходимости отделять отладчик, если все, что вам нужно, это (проверить?) Сбой и сгореть там, где будет выполняться ваш отлаживаемый процесс.

Where this behaviour is controlled

Ответ 3

Трудно сказать, не зная, что делает код для исходного броска. Возможно, что есть события выдающихся форм, которые обрабатываются как часть завершения работы приложения, что приводит к повторному вызову исходного фрагмента кода. Это также может быть виртуальной функцией для вашего класса приложения, которая вызывается при завершении работы.

Вы можете просто распечатать трассировку стека из вашего обработчика catch, чтобы понять это.

Ответ 4

Я подозреваю, что в ExceptionHandler.OnUnhandledException может возникнуть новое исключение, которое остается необработанным, получает пузырь до обработчика необработанных исключений, что повышает ваш метод ExceptionHandler.OnUnhandledException и т.д. (бесконечный цикл → переполнение стека).

Итак, дважды удалите свой метод OnUnhandledException для сброшенных ошибок.

Изменить: смешно, я пытаюсь воспроизвести вашу ошибку, но вместо того, чтобы снова и снова повторять ошибку, мое приложение. просто выходит после метода обработки необработанных исключений, независимо от того, что там происходит. Вы отлаживали или запускали? Это может вызвать разницу.