Сообщений 0    Оценка 195        Оценить  
Система Orphus

Использование библиотеки CrashRpt

Доставка и обработка отчетов об ошибках в Windows-приложениях

Автор: Олег Тарасенко
Источник: RSDN Magazine #4-2009
Опубликовано: 20.07.2010
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Подходы к проблеме доставки отчетов об ошибке
Библиотека CrashRpt
Введение в CrashRpt
Исключения и обработка исключений
Работа с CrashRpt на примере простого приложения
Запуск приложения
Подготовка к выпуску ПО
Анализ отчетов об ошибке
Использование XML-описания ошибки (crashrpt.xml)
Использование минидампа (crashdump.dmp)
Автоматизация обработки отчетов об ошибках
Заключение
Приложение 1. Компиляция CrashRpt
Приложение 2. Установка настроек построения проекта
Компоновка с CRT как с многопоточной DLL в конфигурации Release
Включение отладочных символов (/Zi, /DEBUG) в конфигурации Release

Введение

Если вы когда-нибудь сталкивались с отладкой ошибок, приводящих к фатальным сбоям программы на машине пользователя, то вы должны знать, насколько сложно исправить ошибку, основываясь только на описании пользователя: «Я открыл то-то, потом нажал то-то, и после этого программа завершила работу». Большинство пользователей не станут сообщать вам о произошедшей ошибке, и просто оставят попытки использовать вашу программу после нескольких сбоев. Даже если пользователь решит сообщить об ошибке, скорее всего он не обладает техническими знаниями, поэтому исправить баг, основываясь на его инструкциях, будет нетривиальной проблемой. Становится очевидна необходимость специальных средств, которые позволили бы автоматически собирать и доставлять разработчику информацию о произошедшей ошибке.

Эту статью мы начнем с очень краткого упоминания о двух хорошо известных средствах, предназначенных для перехвата исключений и доставки и пост-обработки отчетов об ошибках, WER и Breakpad. Затем мы рассмотрим еще один инструмент, CrashRpt – свободно распространяемую библиотеку для обработки исключений, доставки и пост-обработки отчетов об ошибках, разработанную специально для Windows-приложений, написанных на Microsoft Visual C++. В заключении мы дадим несколько ссылок, по которым можно найти свежую версию исходного кода, бинарных файлов и документации для библиотеки CrashRpt.

Подходы к проблеме доставки отчетов об ошибке

Вероятно, многие пользователи сталкивались с программой-отладчиком Dr. Watson, входившей в состав операционной системы Windows вплоть до версии Windows Vista. Dr. Watson может обрабатывать ошибки в клиентских приложениях и в модулях операционной системы. Он собирает техническую информацию об ошибке (минидамп и текстовое описание), и позволяет отправить ее на сервер Microsoft. Для того чтобы получить информацию о поступивших ошибках, разработчику необходимо зарегистрироваться на данном сервере (требуется цифровой сертификат). Недостатком Dr.Watson является невозможность использования своего собственного сервера для хранения отчетов об ошибках, а также отсутствие API, который бы позволил настраивать отладчик согласно требованиям приложения.

В Windows XP наряду с Dr.Watson появилась служба (service) Windows Error Reporting (WER), которая стала развитием идей, заложенных в Dr.Watson. Кроме самого WER, появился API, позволяющий добавлять в отчет об ошибке минидамп, дополнительные файлы, блоки памяти, настраивать пользовательский интерфейс, отправлять отчеты немедленно либо ставить их в очередь на отправление. Можно задавать, чтобы WER позволил приложению сохранить данные при сбое и перезапустить приложение (Application Recovery and Restart). Можно создавать отчет о некритической ошибке (которая не приводит к падению программы, но требует отладки). WER также оповещает пользователя, если решение обнаруженной проблемы уже существует.

Достоинства WER:

Недостатки WER:

Альтернативой Windows Error Reporting является инструмент Breakpad, используемый в браузерах Mozilla Firefox и Google Chrome. Breakpad является кросс-платформенной открытой С++-библиотекой для перехвата исключений, доставки и пост-обработки отчетов об ошибках. В настоящее время поддерживаются платформы Windows (x86) и Mac OS X (x86 и Power PC). На момент написания данной статьи существовала также экспериментальная версия Breakpad под Linux.

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

Достоинства Breakpad:

Недостатки Breakpad:

Итак, библиотеки WER и Breakpad – это средства для перехвата, доставки и обработки ошибок, которые уже давно и успешно используются. Однако использование WER требует приобретения цифрового сертификата, что связано с финансовыми расходами. Breakpad, в отличие от WER, не требует денежных расходов и поддерживает несколько операционных систем. Но если вы разрабатываете приложения в Visual C++, то используемый Breakpad кроссплатформенный формат хранения отладочной информации потребует дополнительных усилий на конвертацию данных. Кроме того, Breakpad не полностью адаптирован к Visual C++, например, не отлавливает ошибки CRT. Поэтому, если вы ориентируетесь только на операционную систему Windows и на среду разработки Visual C++, то, вероятно, использование рассматриваемой далее библиотеки CrashRpt будет для вас более удобно.

Библиотека CrashRpt

Введение в CrashRpt

Библиотека CrashRpt – это библиотека с открытым исходным кодом, предназначенная для обработки исключений, доставки и пост-обработки отчетов об ошибках. Она разработана для приложений, созданных в Microsoft Visual C++ и работающих в ОС Windows. CrashRpt поддерживает только одну платформу (Windows), только один язык программирования (C++) и только один компилятор (MS Visual C++), что позволяет предположить, что она хорошо приспособлена к данной среде.

Основные особенности CrashRpt:

Исключения и обработка исключений

Дадим очень краткий обзор механизмов обработки исключений, предоставляемых Visual C++. Рассмотренные механизмы используются CrashRpt для перехвата исключений.

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

Существует два типа исключений: SEH-исключения и типизированные C++ исключения.

SEH-исключения изначально предназначались для языка C, но они могут быть использованы и в C++. SEH-исключения обрабатываются с помощью конструкции __try{}__except(){} или c помощью обработчика исключений, устанавливаемого при помощи функции SetUnhandledExceptionFilter(), например так, как это сделано в приведенном ниже примере.

        #include <windows.h>

  int* p = NULL;   // Указатель на NULL__try
  {
    *p = 13; // Эта операция приводит к исключению Access Violation
  }
  __except(EXCEPTION_EXECUTE_HANDLER)
  {  
    // Обработчик исключения// Просто завершим программу
    ExitProcess(1);
  }

Если же не установить обработчик SEH-исключения, то оно будет обработано Windows, и вы увидите окно WER, который предложит отправить отчет об ошибке на сервер Microsoft.


Рисунок 1. Окно WER, появляющееся при возникновении ошибки в программе

SEH-исключения специфичны для операционной системы Windows. Поэтому, если вы пишете кроссплатформенное приложение, то придется использовать директивы препроцессора для удаления кода SEH-обработчика при компиляции в иной ОС.

Типизированное исключение C++ обрабатывается с помощью конструкции try{}catch(){} (см. пример ниже).

        #include <iostream>
usingnamespace std;

int main () 
{
  try
  {
    throw 20;
  }
  catch (int e)
  {
    cout << "Произошло ислючение. Номер исключения: " << e << endl;
  }
  return 0;
}

Если типизированное исключение произошло в блоке кода, не огражденного try{}catch(){}, то будет вызван обработчик по умолчанию, который находится внутри CRT.

Библиотеки времени выполнения CRT предоставляют свой собственный механизм обработки ошибок (в том числе для перехвата необработанных типизированных исключений), который должен быть учтен в программе. Ниже перечислены функции CRT, которые могут быть использованы для обработки ошибок.

Название функции

Примечание

set_terminate()

Устанавливает обработчик типизированных исключений С++

set_unexpected()

Устанавливает обработчик неожиданной ошибки. Не используется в текущих версиях Visual C++, но на всякий случай такой обработчик стоит установить.

_set_purecall_handler()

Устанавливает обработчик чисто виртуальных вызовов. Эта функция присутствует в Visual C++ .NET 2003 и более поздних версий.

_set_new_handler()

Устанавливает обработчик ошибок выделения памяти оператором new. Данная функция присутствует в Visual C++ .NET 2003 и более поздних версий.

_set_security_error_handler()

Устанавливает обработчик ошибок переполнения буфера. Эта функция присутствует только в Visual C++ .NET 2003, в более поздних версиях она объявлена устаревшей.

_set_invalid_parameter_handler()

Устанавливает обработчик ошибок, связанных с передачей неверного параметра в одну из функций CRT. Эта функция присутствует в Visual C++ 2005 и более поздних версий.

В Visual C++ некоторые исключения, связанные с безопасностью, не могут быть обработаны. Например, начиная с версии CRT 8.0, вы не сможете отловить исключение, связанное с переполнением буфера (buffer overrun). В таком случае CRT просто вызывает WER напрямую, вместо того чтобы вызвать обработчик исключений. Это сделано специально, и в Microsoft не планируют менять этот механизм.

CRT предоставляет механизм прерывания программы, называемый сигналами. Сигнал – это тоже сообщение об ошибке, поэтому обработка сигналов необходима наряду с ошибками CRT и SEH-исключениями.

Вы можете обрабатывать сигналы с помощью функции signal(). Всего есть шесть типов сигналов:

В MSDN говорится, что часть сигналов (SIGSEGV, SIGILL, SIGINT) не используются в OC Windows, но существуют для совместимости со стандартом ANSI С. Однако на всякий случай стоит установить обработчики для всех шести типов сигналов.

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

Также важно обратить внимание на способ компоновки с библиотеками CRT. CRT – это отдельный модуль со своим собственным состоянием. Поэтому установленный обработчик исключения будет работать только в том модуле CRT, с которым скомпонован ваш модуль. Если в вашем приложении несколько модулей, то нужно компоновать их с одним и тем же модулем CRT DLL, в противном случае часть возможных исключений может остаться неперехваченной.

Работа с CrashRpt на примере простого приложения

Покажем основы использования CrashRpt на примере простого приложения.

Первый шаг, который необходимо выполнить – это скомпилировать библиотеку CrashRpt в вашей версии Visual Studio. Компиляция библиотеки – это очень важный, но несколько скучный процесс, поэтому мы вынесли ее подробное описание в приложение 1.

После того как CrashRpt успешно скомпилирована, создайте в Visual C++ консольное WinAPI-приложение и назовите его MyApp. Чтобы CrashRpt могла перехватывать все исключения этого приложения, нужно настроить свойства проекта, а именно – включить генерацию отладочной информации (symbols). Подробно эта процедура описана в приложении 2.

Теперь приступим к написанию кода приложения. Допустим, приложение будет иметь два потока выполнения. В главном потоке будет выполняться функция main(), а рабочий поток предназначен для выполнения некой протяженной по времени вычислительной работы. Создадим заготовку кода.

        #include <windows.h>
#include <stdio.h>

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
  // Зададим бесконечный цикл, в котором будет// выполняться некая вычислительная работаfor(;;)
  {
    // Где-то внутри цикла есть скрытая ошибка...int* p = NULL;
    *p = 13; // Это вызовет access violation
  }    
   
  return 0;
}

void main()
{
  // Создадим рабочий поток
  DWORD dwThreadId = 0;
  HANDLE hWorkingThread = CreateThread(NULL, 0, 
           ThreadProc, (LPVOID)NULL, 0, &dwThreadId);

  // Где-то внутри функции main также есть скрытая ошибка// Вызов printf c нулевым параметромchar* formatString = NULL;
  printf(formatString);

  // Ждем, пока не завершится рабочий поток выполнения
  WaitForSingleObject(hWorkingThread, INFINITE);
}

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

Чтобы CrashRpt мог отловить исключения в нашей программе и отправить отчет об ошибке, воспользуемся функциями API, которые предоставляет CrashRpt. Ниже представлена заготовка кода, в которую вставлены вызовы API-функций CrashRpt.

        #include <windows.h>
#include <stdio.h>
// Включим заголовочный файл CrashRpt#include"CrashRpt.h"// Зададим Callback-функцию, которая будет вызвана при сбое
BOOL WINAPI CrashCallback(LPVOID /*lpvState*/)
{  
  // Добавим два файла в отчет об ошибке: // лог-файл и параметры конфигурации приложения  
  crAddFile("log.txt", "Log File");  
  crAddFile("config.ini", "Configuration File");

  // Добавим в отчет скриншот рабочего стола
  crAddScreenshot(CR_AS_VIRTUAL_SCREEN);   

  // Добавим в отчет именованное свойство
  crAddProperty("VideoCard", "nVidia GeForce 8600 GTS");

  return TRUE;
}

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
  // При входе в процедуру потока добавим обработчики исключений
  crInstallToCurrentThread2(0);
  
  // Зададим бесконечный цикл, в котором будет// выполняться некая вычислительная работаfor(;;)
  {
    // Где-то внутри цикла есть скрытая ошибка...int* p = NULL;
    *p = 13; // Это вызовет access violation
  }

  // Перед выходом из процедуры потока удалим обработчики исключений
  crUninstallFromCurrentThread();    
   
  return 0;
}

void main()
{
  // Задаем параметры для CrashRpt
  CR_INSTALL_INFO info;  
  memset(&info, 0, sizeof(CR_INSTALL_INFO));  
  info.cb = sizeof(CR_INSTALL_INFO);    
  info.pszAppName = "MyApp";  
  info.pszAppVersion = "1.0.0";  
  info.pszEmailSubject = "Отчет об ошибке от MyApp 1.0.0";  
  info.pszEmailTo = "myapp_support@hotmail.com";    
  info.pszUrl = "http://myapp.com/tools/crashrpt.php";  
  info.pfnCrashCallback = CrashCallback;   
  info.uPriorities[CR_HTTP] = 3;  // Сначала пробуем HTTP  
  info.uPriorities[CR_SMTP] = 2;  // Затем SMTP  
  info.uPriorities[CR_SMAPI] = 1; // Наконец Simple MAPI    
  info.dwFlags = 0; // Установить все доступные обработчики исключений  // URL для политики конфиденциальности
  info.pszPrivacyPolicyURL = "http://myapp.com/privacypolicy.html"; 
  
  // Устанавливаем обработчики исключений  int nResult = crInstall(&info);

  if (nResult != 0)
  {    
    // Что-то пошло не так! Получим сообщение об ошибке.   char szErrorMsg[512];
    szErrorMsg[0] = 0;
    crGetLastErrorMsg(szErrorMsg, 512);    
    printf("%s\n", szErrorMsg);    
    return;
  } 

  // Далее следует основной код...// Создадим рабочий поток
  DWORD dwThreadId = 0;
  HANDLE hWorkingThread = CreateThread(NULL, 0, 
    ThreadProc, (LPVOID)NULL, 0, &dwThreadId);

 // Где-то внутри функции main также есть скрытая ошибка// Вызов printf c нулевым параметромchar* formatString = NULL;
 printf(formatString);

 // Ждем, пока не завершится рабочий поток выполнения
  WaitForSingleObject(hWorkingThread, INFINITE);

  // Перед выходом из функции удалим обработчики исключений
  crUninstall();
}

Рассмотрим подробнее использованные нами функции CrashRpt.

Прежде всего мы включили заголовочный файл CrashRpt.h в начало кода:

        #include
        "CrashRpt.h"
      

Заметим, что в нашем простом приложении мы используем multi-byte набор символов (character set). Все функции и структуры CrashRpt, аргументы которых зависят от используемого набора символов имеют две версии названия (multi-byte версия имеет суффикс A, а wide-character версия имеет суффикс W). Например, функция crInstall() имеет два варианта имени: crInstallA() и crInstallW(). Обычно в программе нужно использовать представление имени функции, независимое от набора символов, например, используйте имя crInstall(), которое расширяется в crInstallW(), если вы используете wide-char набор символов или в crInstallA(), если вы используете multi-byte набор символов.

Для установки обработчиков исключений мы воспользовались функциями crInstall() и crInstallToCurrentThread2(). Их прототипы приведены ниже.

        int crInstall(PCR_INSTALL_INFO  pInfo); 

int crInstallToCurrentThread2(DWORD dwFlags);  

Функция crInstall() устанавливает обработчики исключений, которые работают для всего процесса целиком (а не только для вызвавшего функцию потока). Обычно она вызывается в начале функции main() или WinMain(). В функцию crInstall() через структуру CR_INSTALL_INFO передается разнообразная информация, которая включает в себя имя нашего приложения, номер версии приложения, адрес электронной почты для пересылки отчетов, URL для отправки отчетов по HTTP и так далее. Определение структуры приведено ниже.

        typedef
        struct tagCR_INSTALL_INFOA 
{   
  // Размер этой структуры в байтах; должен быть   // установлен перед использованием!
  WORD cb;                      
  // Имя приложения.
  LPCSTR pszAppName;            
  // Версия приложения.
  LPCSTR pszAppVersion;         // E-mail адрес для отправки отчетов об ошибке.
  LPCSTR pszEmailTo;            // Тема письма при отправке отчета по e-mail
  LPCSTR pszEmailSubject;       . // URL скрипта (используется для доставки отчета через соединение HTTP).
  LPCSTR pszUrl;                
  // Имя каталога, где находится CrashSender.exe.
  LPCSTR pszCrashSenderPath;    
  // Пользовательская callback-функция (будет вызвана при сбое).
  LPGETLOGFILE pfnCrashCallback;// Массив приоритетов отправки отчета.
  UINT uPriorities[5];          
  // Набор флагов.
  DWORD dwFlags;                // URL для политики конфиденциальности.
  LPCSTR pszPrivacyPolicyURL;
  // Путь для поиска dbghelp.dll.
  LPCSTR pszDebugHelpDLL;
  // Тип минидампа (влияет на размер отчета).
  MINIDUMP_TYPE uMiniDumpType;
} 
CR_INSTALL_INFOA;

Мы также задали callback-функцию, которая будет вызвана при сбое (ее прототип, LPGETLOGFILE, приведен ниже). Указатель на эту callback-функцию мы передали в crInstall().

        typedef BOOL(*LPGETLOGFILE)(LPVOID lpvState);  

Перед выходом из функции main() мы выключили обработчики исключений, установленных ранее, вызвав функцию crUninstall(). Ее прототип представлен ниже.

        int crUninstall();  

Функция crInstallToCurrentThread2() устанавливает обработчики исключений, которые работают только для установившего их потока. В многопоточной программе нужно вызывать crInstallToCurrentThread2() в процедуре каждого потока, кроме главного. Обычно эта функция вызывается в начале процедуры потока. Функция crInstallToCurrentThread2() принимает набор флагов, задающих типы обработчиков исключений, которые вы желаете установить. Полный список флагов можно найти в документации CrashRpt. Если же передать в качестве параметра нуль, то будут установлены все доступные обработчики исключений.

Перед возвращением из процедуры потока необходимо вызвать crUninstallFromCurrentThread() для отключения обработчиков исключений, установленных для потока.

        int crUninstallFromCurrentThread();   

Обычно API-функции CrashRpt возвращают нулевое значение, если операция выполнена успешно, и ненулевое значение, если операция потерпела неудачу. Чтобы получить текст сообщения об ошибке в последний вызванной API-функции, используйте функцию crGetLastErrorMsg().

        int crGetLastErrorMsgA(LPSTR pszBuffer, UINT uBuffSize);

Обычно приложение создает и ведет лог-файл, куда записываются выполняемые операции и произошедшие ошибки логики. Такой файл может быть полезен для анализа ошибки, и поэтому его стоит добавить в отчет об ошибке с помощью функции crAddFile().

        int crAddFileA(LPCSTR pszFile, LPCSTR pszDesc);

Иногда может потребоваться добавить свое собственное именованное текстовое свойство в XML-файл описания ошибки. Например, вам может понадобиться добавить информацию о количестве свободного места на диске в момент отказа программы, или информацию о версии драйвера видеокарты, установленной на компьютере пользователя. Это можно сделать с помощью функции crAddProperty().

        int crAddPropertyA(LPCSTR pszPropName, LPCSTR pszPropValue);

Иногда полезно посмотреть, что происходило на рабочем столе пользователя, когда в вашем приложении произошла ошибка. С помощью функции crAddScreenshot() можно добавить в отчет об ошибке снимок всего рабочего стола или снимок главного окна приложения.

        int crAddScreenshot(DWORD dwFlags);

При использовании функции crAddScreenshot() нужно быть осторожным, потому что рабочий стол может содержать идентифицирующие пользователя данные или конфиденциальную информацию. Поэтому в Политике конфиденциальности всегда нужно указывать цели, с которыми вы собираетесь использовать собранную на компьютере пользователя информацию. Нажав кнопку Отправить отчет, пользователь подтверждает, что он знаком с содержанием отчета об ошибке и принимает условия Политики конфиденциальности. Так будет спокойнее и вам, и пользователю.

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

Чтобы упростить установку и удаление обработчиков исключений, можно использовать классы-обертки CrashRpt. Используйте класс CrAutoInstallHelper для установки обработчиков в функции main(). Определение этого класса приведено ниже. В многопоточной программе также используйте класс CrThreadAutoInstallHelper для установки обработчиков исключений в каждый рабочий поток.

        class CrAutoInstallHelper 
{  
public:   
  // Устанавливает обработчики исключений для всего процесса
  CrAutoInstallHelper(PCR_INSTALL_INFOA pInfo)    
  {      
     m_nInstallStatus = crInstallA(pInfo);   
  } 
   
  // Устанавливает обработчики исключений для всего процесса
  CrAutoInstallHelper(PCR_INSTALL_INFOW pInfo)    
  {     
     m_nInstallStatus = crInstallW(pInfo);    
  }   
   
  // Удаляет обработчики исключений    
  ~CrAutoInstallHelper()   
  {     
     crUninstall();   
  }
    
  // Статус установки обработчиков исключений   int m_nInstallStatus; 
};

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

        int crGenerateErrorReport(CR_EXCEPTION_INFO* pExceptionInfo);

typedefstruct tagCR_EXCEPTION_INFO 
{   
   WORD cb;                   // Размер структуры в байтах; 
                              // должен быть установлен перед использованием.
   PEXCEPTION_POINTERS pexcptrs; // Информация об исключении.int exctype;               // Тип исключения.    
   DWORD code;                // Код SEH-исключения.    unsignedint fpe_subcode;  // Подкод исключения чисел с плавающей точкой.const wchar_t* expression; // Выражение, которое привело к ошибке.    const wchar_t* function;   // Функция, в которой произошла ошибка.   const wchar_t* file;       // Файл, в котором произошла ошибка.    unsignedint line;         // Номер строки кода. 
}
CR_EXCEPTION_INFO; 

После того как вы установили обработчики исключений в программе, важно убедиться, что CrashRpt перехватывает исключения. Для этой цели можно использовать функцию crEmulateCrash(), которая способна вызвать исключение. На этапе тестирования вы можете вызвать эту функцию в каждом потоке вашей программы, чтобы удостовериться, что все исключения будут перехвачены.

        int crEmulateCrash(unsigned ExceptionType);

Запуск приложения

После того как в код приложения добавлены функции установки обработчиков исключений и другие функции CrashRpt, необходимо добавить библиотечный файл CrashRpt.lib в список входных библиотек. В окне Solution Explorer щелкните правой кнопкой мыши название проекта и выберите пункт Properties в контекстном меню. Затем откройте Configuration Properties-> Linker-> Input->Advanced и добавьте CrashRpt.lib в список подключаемых библиотек.

Наконец, попробуем собрать наше решение (solution) в конфигурации Release. Когда решение успешно собрано, в выходном каталоге можно обнаружить бинарный файл MyApp.exe и файл отладочной информации MyApp.pdb. Дополнительно создайте в этом каталоге два пустых текстовых файла log.txt и config.ini, которые будут имитировать включаемые в отчет об ошибке лог приложения и его конфигурационные настройки.

Также скопируйте в этот каталог следующие файлы из каталога CrashRpt: CrashRpt.dll, CrashSender.exe, dbghelp.dll и crashrpt_lang.ini. CrashRpt.dll содержит код для обработки исключений в клиентском приложении. CrashSender.exe содержит код для создания ZIP-файла и доставки отчета об ошибке через Интернет. dbghelp.dll (Microsoft Debug Help Library) – это вспомогательный модуль, который нужен для работы CrashRpt.

Язык пользовательского интерфейса зависит от языкового файла crashrpt_lang.ini. Вы можете скачать языковой файл для нужного вам языка по этой ссылке: http://crashrpt.googlecode.com/svn/lang_files/ Он представляет собой текстовый документ в формате UNICODE с расширением INI. Языковой файл содержит локализованные строки, используемые диалогами CrashRpt.

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

Когда в программе происходит исключение, осуществлять в ней сложные операции не безопасно, так как программа может быть нестабильной. CrashRpt разделена на два модуля, чтобы иметь возможность закрыть приложение, в котором произошло исключение, и продолжить отправку отчета в фоновом режиме с помощью модуля CrashSender.exe (кстати, таким же образом работает и WER).

CrashRpt сохраняет файлы отчета об ошибке в каталоге %LOCAL_APP_DATA%\CrashRpt\UnsentErrorReports\ %AppName%_%AppVersion%.


Рисунок 2. Окно отчета об ошибке.

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

Кроме того, CrashRpt генерирует описание ошибки в формате XML (crashrpt.xml). Этот файл содержит различную информацию, которая может быть полезна для анализа ошибки.

Также в отчете могут присутствовать пользовательские файлы.

В диалоге отчета об ошибке пользователь может дополнительно предоставить свой адрес электронной почты и краткое описание проблемы. Адрес электронной почты может быть использован позже для обеспечения контакта с пользователем и запроса дополнительной информации о проблеме.

Ссылка Что содержится в отчете? открывает диалог, на котором отображено подробно содержимое отчета. Двойной щелчок по имени файла открывает файл, используя ассоциированную с ним программу.


Рисунок 3. Окно деталей отчета об ошибке.

Рекомендуется всегда задавать политику конфиденциальности (Privacy Policy), описывающую, какую информацию собирает ваше программное обеспечение в случае фатального сбоя, и для чего может быть использована собранная информация. В качестве шаблона для такого документа можно использовать вот эту страницу.

Если пользователь удовлетворен содержимым отчета, он может закрыть диалог и нажать на кнопку Отправить отчет в основном диалоге. Если пользователь не хочет отправлять отчет, он нажимает кнопку Закрыть программу.

Диалог Отправка отчета об ошибке (рисунок 4) показывает прогресс отправки отчета об ошибке. Как уже говорилось, отчет об ошибке может быть отправлен различными способами. Клиентское приложение может программно задавать порядок выполнения попыток отправки отчета. По умолчанию первым используется HTTP, затем – SMTP, и последним – Simple MAPI. Подробнее об этих способах мы упомянем несколько позже.

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


Рисунок 4. Окно прогресса отправки отчета об ошибке.

Диалог прогресса становится скрытым через три секунды после старта процесса отправки. Затем он отображается в виде значка в системном лотке. Можно дважды щелкнуть на этом значке, чтобы восстановить диалог. Когда требуется взаимодействие с пользователем, диалог восстанавливается автоматически.

Рассмотрим кратко, каким образом осуществляется доставка отчета об ошибке по сети Интернет.

Многие программные продукты имеют Web-сайты в Интернете. Такие Web-серверы обычно поддерживают какой-нибудь скриптовый язык, например, PHP, Perl, ASP и так далее. CrashRpt может установить HTTP-соединение с сервером и передать отчет об ошибке в качестве POST-параметра для скрипта.

CrashRpt также содержит простой встроенный SMTP-клиент. Этот клиент может попытаться отправить отчет, используя протокол SMTP без какого-либо взаимодействия с пользователем. Отчет об ошибке в этом случае представляет собой сообщение электронной почты с прикрепленными к нему файлами. В большинстве случаев данный способ потерпит неудачу по ряду причин (если сетевой экран блокирует исходящий трафик на порту 25 или почтовый сервер требует авторизации). Но он может и сработать, когда на домене пользователя установлен локальный почтовый сервер, который не требует авторизации и доверяет пользователю (например, это может быть корпоративный SMTP-сервер).

Наконец, CrashRpt может воспользоваться программным интерфейсом Simple MAPI, чтобы запустить почтовый клиент, установленный на машине пользователя, например, Mozilla Thunderbird, и отправить отчет об ошибке, как сообщение электронной почты. Это требует небольшого взаимодействия с пользователем, и по данной причине данный способ имеет самый низкий приоритет по умолчанию. Кроме того, не у каждого пользователя настроен почтовый клиент (многие пользуются Web-интерфейсом для отправки почты).

Если один из способов отправки отчета терпит неудачу, CrashRpt попытается отправить отчет другими способами по очереди. Например, если HTTP-сервер сильно загружен и не принимает запросы, то отчет будет доставлен по E-mail. Это позволяет с большой вероятностью гарантировать, что отчет об ошибке будет доставлен по назначению.

Подготовка к выпуску ПО

Допустим, что мы полностью написали наше приложение MyApp v.1.0.0, отладили его, и теперь готовы опубликовать его дистрибутив на Web-сайте нашей компании и сделать этот дистрибутив доступным для скачивания пользователями. Но перед тем как выпустить наше ПО в открытый доступ, необходимо выполнить несколько дополнительных шагов.

Во-первых, не забудьте включить в дистрибутив следующие файлы (рекомендуется поместить эти файлы в каталог, где находится исполняемый файл приложения): CrashRpt.dll, CrashSender.exe, Dbghelp.dll, crashrpt_lang.ini. Если ваше приложение поддерживает несколько языков, то можно включить в дистрибутив несколько языковых файлов, а при установке скопировать необходимый.

ПРИМЕЧАНИЕ

Хотя файл dbghelp.dll поставляется вместе с ОС Windows XP и более поздними версиями, рекомендуется распространять dbghelp.dll с вашим ПО, поскольку на некоторых машинах конечных пользователей его версия может отличаться от нужной.

Во-вторых, нужно не забыть сохранить бинарные файлы и отладочную информацию на локальном компьютере (для нашего простого приложения это файлы MyApp.exe и MyApp.pdb). Рекомендуем следующую последовательность действий:

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

ПРЕДУПРЕЖДЕНИЕ

Файлы, скопированные в каталог CrashRptSaved, должны храниться в нем в течение времени жизни данной версии программного обеспечения. Эти сохраненные файлы НЕ должны распространяться вместе с версией вашего ПО, поскольку, попав в не те руки, они могут облегчить реверс-инжиниринг кода приложения.

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

В-четвертых, до релиза рекомендуется проверить, что отчеты об ошибках надлежащим образом доходят до адресата.

Анализ отчетов об ошибке

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

Количество полученных отчетов может зависеть от надежности вашего программного обеспечения при различных непредвиденных ситуациях и от его популярности. Так что ничего особенного, если вы получите несколько сотен отчетов об ошибках в день. Если приходит много отчетов об ошибках, можно контролировать и анализировать их в течение первых нескольких дней после выпуска ПО, а затем подготовить hot-fix релиз.

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

Нормальный сценарий – когда ваше программное обеспечение становится все более и более стабильным и с каждым эволюционным релизом ПО к вам приходит все меньше отчетов об ошибках.

Использование XML-описания ошибки (crashrpt.xml)

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

Описание ошибки для приложения MyApp v1.0.0 представлено ниже.

<?xmlversion="1.0"encoding="utf-8" ?>
<CrashRpt version="1201">
  <CrashGUID>7a2bf0bb-fd44-46eb-b674-56f0e5e38c15</CrashGUID>
  <AppName>MyApp</AppName>
  <AppVersion>1.0.0</AppVersion>
  <ImageName>D:\Projects\MyApp\MyApp\release\MyApp.exe</ImageName>
  <OperatingSystem>Windows 7 Ultimate Build 7100</OperatingSystem>
  <SystemTimeUTC>2010-02-02T15:47:11Z</SystemTimeUTC>
  <ExceptionType>6</ExceptionType>
  <InvParamLine>0</InvParamLine>
  <GUIResourceCount>4</GUIResourceCount>
  <OpenHandleCount>55</OpenHandleCount>
  <MemoryUsageKbytes>4500</MemoryUsageKbytes>
  <CustomProps>
    <Prop name="VideoCard" value="nVidia GeForce 8600 GTS"/>
  </CustomProps>
  < FileList>
    <FileItem name="config.ini" description="Configuration File" />
    <FileItem name="crashdump.dmp" description="Crash Dump" />
    <FileItem name="crashrpt.xml" description="Crash Log" />
    <FileItem name="log.txt" description="Log File" />
    <FileItem name="screenshot0.png" description="Desktop Screenshot" />
  </ FileList>
</CrashRpt>

Корневой элемент называется CrashRpt. Атрибут version представляет собой версию библиотеки CrashRpt, которая создала данный отчет об ошибке. Значение '1201 'означает, что этот отчет был сгенерирован версией CrashRpt 1.2.1.

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

Элемент AppName является именем приложения, которое было передано функции crInstall() как член структуры CR_INSTALL_INFO::pszAppName.

Элемент AppVersion – это версия приложения, которая была передана функции crInstall() как член структуры CR_INSTALL_INFO::pszAppVersion.

Элемент ImageName – это путь к исполняемому файлу программного обеспечения.

Элемент OperatingSystem – это версия операционной системы, установленной на машине конечного пользователя, включая номер сборки и Service Pack. Это полезная информация, поскольку многие ошибки происходят только на определенной версии операционной системы.

SystemTimeUTC – это время, когда произошел сбой, в формате UTC. Это значение может быть использовано для упорядочения сообщений об ошибках по времени создания.

UserEmail – это адрес электронной почты отправителя (конечно если пользователь его ввел при отправке отчета). Если этот адрес указан, то он может быть использован для связи с пользователем и запроса дополнительной информации об ошибке.

ProblemDescription является предоставленным пользователем описанием проблемы (если пользователь ввел его при отправке отчета).

FileList содержит список файлов, которые содержатся в отчете об ошибке.

ExceptionType обозначает тип ошибки. В данном случае номер 6 означает, что произошло исключение CRT invalid parameter. Подробнее о кодах типов ошибок рассказано в документации CrashRpt.

Элементы GUIResourceCount, OpenHandleCount и MemoryUsageKbytes указывают количество используемых приложением ресурсов (дескрипторов и оперативной памяти).

Использование минидампа (crashdump.dmp)

В отчете об ошибке всегда содержится минидамп-файл. Этот файл можно открыть с помощью Visual Studio, в программе WinDbg или в другом отладчике.

Минидамп содержит состояние регистров процессора, стек вызовов и локальные переменные для каждого потока выполнения, перечень загруженных в адресное пространство процесса и выгруженных модулей, а также системную информацию. Минидамп создается с помощью функции MiniDumpWriteDump() из dbghelp.dll.

Минидамп непригоден для анализа без отладочной информации (файла PDB). Чтобы открыть минидамп, скопируйте его в папку, где находятся выходные файлы вашего решения. Затем дважды щелкните по имени минидамп-файла. После этого он должен открыться с помощью ассоциированной с ним программы (например, Visual Studio). Когда минидамп загружен в Visual Studio, нажмите клавишу F5, чтобы запустить его. Если отладочная информация загружена успешно, то вы сможете увидеть место в исходном коде, где произошло исключение.

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


Рисунок 5. Снимок стека в момент, когда произошло исключение.

Автоматизация обработки отчетов об ошибках

Обработка отчета об ошибке означает открытие отчета об ошибке, чтение файла XML, открытие минидампа, выявление важной информации об исключении и, наконец, представление информации для разработчика в текстовом виде.

Если вы получаете много отчетов об ошибках после релиза программного обеспечения, автоматическая обработка отчетов об ошибках становится критически важной задачей. Сотни отчетов могут содержать лишь несколько проблем, многие отчеты будут просто дублировать информацию об этих проблемах. Открытие стольких отчетов вручную в Visual Studio или WinDbg для анализа их содержания может отнять много времени.

CrashRpt предоставляет средство для автоматизации обработки отчетов об ошибках. Утилита командной строки crprober.exe предназначена для обработки файлов отчета об ошибке и вывода результатов в текстовый файл или на терминал.

crprober.exe может извлечь информацию об отчете, приложении, которое отправило отчет (его название, версию, путь к исполняемому файлу, список загруженных модулей), информацию об операционной системе (название, версию, Service Pack, характеристики ЦП), а также сведения об исключении (адрес, код исключения, снимок стека).

Следующий пример показывает, как можно обработать отчет 'error_report.zip' и выдать данные на экран (терминал). Предполагается, что отладочная информация находятся в директории 'D:\Symbol Files'.

crprober.exe /f error_report.zip /o "" /sym "D:\Symbol Files"

CrashRpt также предоставляет API для обработки отчетов об ошибке, созданных CrashRpt. Он будет полезен, если вы захотите написать свое собственное приложение для обработки отчетов об ошибках. Этот API также используется внутри инструмента crprober.exe, представленного выше.

Функциональность обработки отчетов об ошибках инкапсулирована внутри библиотеки CrashRptProbe.dll. Внутренне CrashRptProbe использует функции, которые предоставляет dbghelp.dll (Microsoft Debug Help Library).

При использовании CrashRptProbe API можно представлять себе отчет об ошибке в виде набора свойств. Для удобства доступа все свойства организованы в таблицы. Каждое свойство представляет собой текстовую строку. В качестве примера ниже приведена таблица XmlDescFileItems, которая содержит список файлов, которые присутствуют в отчете об ошибке.

FileItemName

FileItemDescription

config.ini

Configuration File

crashdump.dmp

Crash Dump

crashrpt.xml

Crash Log

log.txt

Log File

screenshot0.png

Desktop Screenshot

CrashRptProbe предоставляет несколько API-функций, которые мы не будем обсуждать в этой статье. Полную информацию об API можно найти в документации CrashRpt. Ниже мы просто перечислим функции CrashRptProbe API.

Название функции

Примечание

crpOpenErrorReport()

Служит для открытия ZIP-файла отчета об ошибке

crpCloseErrorReport()

Закрывает предварительно открытый отчет

crpGetProperty()

Извлекает текстовое свойство из указанной таблицы

crpExtractFile()

Извлекает файл из ZIP-архива отчета об ошибке

crpGetLastErrorMsg()

Выдает статус последней выполненной операции

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

Пример обработки отчета с помощью CrashRptProbe API

Заключение

В этой статье мы представили библиотеку CrashRpt – открытую библиотеку для обработки исключений, отправки и последующей обработки отчетов об ошибках, которая предназначена специально для Visual C++-приложений.

На момент написания данной статьи последней версией CrashRpt является v.1.2.1. Чтобы получить последнюю версию исходного кода, бинарных файлов и документации CrashRpt, рекомендуем обратиться к странице проекта CrashRpt. Если вы обнаружили какую-либо проблему, связанную с работой CrashRpt, пожалуйста, сообщите о ней по этой ссылке, мы благодарим вас за отзыв.

Приложение 1. Компиляция CrashRpt

CrashRpt зависит от библиотеки WTL (Windows Template Library). Загрузить последнюю версию WTL можно отсюда. Распакуйте архив WTL в какую-нибудь папку. В Visual Studio, откройте меню Tools->Options->Projects and Solutions->VC++ Directories. В списке Show directories for выберите Include files и добавьте путь к [WTL_Folder]\include.

Вы можете скомпилировать CrashRpt в Visual C++ .NET 2003, Visual C++ 2005, Visual C++ 2008 и Visual C++ Express.

ПРИМЕЧАНИЕ

Для компиляции CrashRpt в Visual C++ Express дополнительно нужно иметь Platform SDK для Windows Server 2003, потому что этот Platform SDK содержит исходный код библиотеки ATL (WTL зависит от ATL). Скачайте и установите Microsoft Platform SDK для Windows Server 2003, например, отсюда.

Откройте один из файлов решений, доступных в каталоге верхнего уровня CrashRpt, в зависимости от вашей версии Visual Studio:

В решении CrashRpt содержится несколько проектов:

Для построения решения выберите конфигурацию Release и нажмите клавишу F7. CrashRpt может быть скомпилирован как для платформы Win32, так и для платформы x64. Вы можете выбрать нужную платформу, открыв меню Build->Configuration Manager... и выбрав желаемую платформу в списке Active platform.

ПРИМЕЧАНИЕ

Для компиляции в x64 необходимо иметь Visual C++ 2005 или более поздней версии. Также убедитесь в процессе установки Visual Studio, что вы установили инструменты компилятора x64 и необходимые SDK-файлы.

К сожалению в Visual C++ Express я не нашел легкого способа скомпилировать CrashRpt для платформы x64.

Чтобы предоставить компилятору и компоновщику Visual C++ информацию о местонахождении include- и lib-файлов CrashRpt, выполните следующие действия.

Здесь вместо [CRASHRPT_HOME] нужно подставить фактический путь к каталогу CrashRpt.

Приложение 2. Установка настроек построения проекта

Компоновка с CRT как с многопоточной DLL в конфигурации Release

Важно, чтобы проекты в вашем решении были скомпонованы с библиотеками времени выполнения языка С (CRT) как с многопоточной DLL (флаг /MD) для конфигурации Release. Такой способ привязки рекомендован в MSDN.

В окне Solution Explorer щелкните правой кнопкой мыши на названии вашего проекта и откройте окно Project Properties. Затем выберите Configuration Properties->C/C++->Code Generation. В поле Runtime library выберите Multi-threaded DLL (/MD) (см. рисунок 6).


Рисунок 6. Связывание с библиотекой CRT DLL.

ПРИМЕЧАНИЕ

В конфигурации Debug не важно, как вы компонуете CRT. Обычно в режиме Debug отладчик перехватывает исключения вместо CrashRpt.

Убедитесь, что все модули вашего приложения используют одну и ту же версию CRT. Если некоторые зависимые модули были скомпилированы с использованием старой версии CRT, необходимо перекомпилировать их, чтобы обеспечить единую версию CRT DLL.

Например, предположим, что вы используете Visual Studio 2005, и CRT 8.0 связана как DLL, но некоторые зависимые модули приложения были собраны в Visual Studio. NET 2003 и используют CRT 7.1, связанную как DLL. В такой ситуации ошибки в старом модуле CRT не будут перехвачены CrashRpt, потому что обработчики ошибок установлены вами лишь для CRT 8.0. Однако CrashRpt перехватывает SEH-исключения без каких-либо проблем в любом модуле, вне зависимости от компоновки и версии CRT.

Включение отладочных символов (/Zi, /DEBUG) в конфигурации Release

Чтобы получить максимальную отдачу от минидамп-файла, отладчику нужна отладочная информация (Program Database, PDB) для приложения. Для включения генерации отладочной информации выполните следующие действия:


Рисунок 7. Включение генерации отладочной информации.


Рисунок 8. Настройка компоновщика для генерации отладочной информаци


Эта статья опубликована в журнале RSDN Magazine #4-2009. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 195        Оценить