Новости из Блогов Логируем необрабатываемые исключения

Discussion in 'Мировые новости. Обсуждения.' started by Suicide, 21 Dec 2012.

  1. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,482
    Likes Received:
    7,062
    Reputations:
    693
    Логируем необрабатываемые исключения
    Пятница, 21. Декабрь 2012
    автор: Kaimi
    http://kaimi.ru/2012/12/unhandled-exceptions-logging/
    http://kaimi.ru/



    Надоело созерцать зелёную морду на титульной странице блога, поэтому настало время эту морду сместить. (Угу.. мне тоже, thx за смещение. - ком.Suicide)

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

    Основу библиотеки фактически будет составлять одна функция - MiniDumpWriteDump, которая делает дамп памяти процесса. Этот дамп впоследствии можно открыть, например, в WinDBG и посмотреть причину неожиданного падения процесса. Также хочу отметить, что мы "покладем" на замечание из MSDN о необходимости вызова данной функции из отдельного процесса.

    Итак, начнем с основной функции, которая будет обрабатывать исключение и создавать мини-дамп.
    Code:
    /* Функция получает на вход информацию об исключении, путь для сохранения мини-дампа, включая имя файла, */
    /* тип мини-дампа, адрес хоста, куда будет отправлен мини-дамп, и путь к скрипту, который его примет */
    BOOL process_exception(EXCEPTION_POINTERS * exception, PTCHAR dump_path, MINIDUMP_TYPE type, PTCHAR host, PTCHAR uri)
    {
        MINIDUMP_EXCEPTION_INFORMATION ex_info;
        HANDLE file;
        TCHAR path[MAX_PATH];
        /* Заполним структуру, необходимую для создания дампа, информацией о нашем исключении */
        ex_info.ThreadId = GetCurrentThreadId();
        ex_info.ExceptionPointers = exception;
        ex_info.ClientPointers = FALSE;
     
        /* Разворачиваем переменные окружения, если они присутствуют */
        ExpandEnvironmentStrings(dump_path, path, _countof(path));
     
        /* Открываем хендл и создаем мини-дамп */
        file = CreateFile(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        if(file == INVALID_HANDLE_VALUE)
            return FALSE;
     
        if( MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file, type, &ex_info, 0, 0) == FALSE )
        {
            CloseHandle(file);
            return FALSE;
        }
     
        CloseHandle(file);
     
        /* Отправляем дамп на сервер */
        if(host != NULL && uri != NULL)
            send_report(host, uri, path);
     
        return TRUE;
    }
    
    Основной код в общем-то написан, осталось реализовать функцию отправки дампа на сервер и проверить работоспособность методов. Сначала отправка:
    Code:
    BOOL send_report(PTCHAR host, PTCHAR uri, PTCHAR file_path)
    {
        BOOL status = TRUE;
        HINTERNET sess = NULL, conn = NULL, req = NULL;
        HANDLE fh;
        /* Заголовки для формирования multipart запроса на загрузку файла */
        const static TCHAR headers[] = _T("Content-Type: multipart/form-data; boundary=0123456789");
        const static char data_head[] = "--0123456789\r\n"                                                               \
                                        "Content-Disposition: form-data; name=\"report\"; filename=\"crash.dmp\"\r\n"    \
                                        "Content-Type: application/octet-stream\r\n\r\n";
        const static char data_tail[] = "\r\n--0123456789--";
        void * post_data = NULL;
        DWORD file_size, post_data_size, aux;
     
        /* Номинальный while, чтобы не использовать goto и поменьше дублировать код */
        while(TRUE)
        {
            /* Открываем файл мини-дампа и определяем его размер */
            fh = CreateFile(file_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
            if(fh == INVALID_HANDLE_VALUE)
            {
                status = FALSE;
                break;
            }
     
            file_size = GetFileSize(fh, NULL);
            if(file_size == INVALID_FILE_SIZE)
            {
                status = FALSE;
                break;
            }
     
            post_data_size = sizeof_wo_null(data_head) + file_size + sizeof_wo_null(data_tail);
            /* Выделяем память под содержимое файла + заголовки */
            post_data = malloc(post_data_size);
            if(post_data == NULL)
            {
                status = FALSE;
                break;
            }
     
            ZeroMemory(post_data, post_data_size);
            /* Формируем тело multipart POST-запроса */
            CopyMemory(post_data, data_head, sizeof_wo_null(data_head));
     
            if( ReadFile(fh, (char *)post_data + sizeof_wo_null(data_head), file_size, &aux, NULL) == FALSE)
            {
                status = FALSE;
                break;
            }
     
            CopyMemory((char *)post_data + sizeof_wo_null(data_head) + file_size, data_tail, sizeof_wo_null(data_tail));
     
            /* Используем функции WinInet для отправки, чтобы не возиться с сокетами */
            sess = InternetOpen(_T("Crash Reporter"), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
            if(sess == NULL)
            {
                status = FALSE;
                break;
            }
     
            conn = InternetConnect(sess, host, INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 1);
            if(conn == NULL)
            {
                status = FALSE;
                break;
            }
     
            req = HttpOpenRequest(conn, _T("POST"), uri, NULL, NULL, NULL, INTERNET_FLAG_NO_COOKIES, 1);
            if(req == NULL)
            {
                status = FALSE;
                break;
            }
     
            status = HttpSendRequest(req, headers, -1L, post_data, post_data_size);
     
            break;
        }
     
        /* Закрываем хендлы, освобождаем память */
        if(fh != INVALID_HANDLE_VALUE)
            CloseHandle(fh);
        if(post_data != NULL)
            free(post_data);
        if(req != NULL)
            InternetCloseHandle(req);
        if(conn != NULL)
            InternetCloseHandle(conn);
        if(sess != NULL)
            InternetCloseHandle(sess);
     
     
        return status;
    }
    Всё, у нас есть всё, что необходимо для нашего небольшого логгера исключений. Ах да, забыли про инклюды и один дефайн:
    Code:
    #include <Windows.h>
    #include <DbgHelp.h>
    #include <WinInet.h>
    #include <tchar.h>
     
    #pragma comment(lib, "dbghelp.lib")
    #pragma comment(lib, "wininet.lib")
     
    /* Размер массива без учета нулл-байта (исключительно для char) */
    #define sizeof_wo_null(_Array) (sizeof(_Array) - 1)
    Теперь точно всё, проверим работоспособность методов. Напишем пару строк кода, которые будут вызывать падение программы, и добавим SEH [http://en.wikipedia.org/wiki/Structured_Exception_Handling#Structured_Exception_Handling], в котором будем ловить наше исключение.
    Получим такой вот простой код:
    Code:
    #include <Windows.h>
    #include <DbgHelp.h>
     
    #pragma comment(lib, "report_lib.lib")
     
    #ifdef __cplusplus
    extern "C"
    {
    #endif
    BOOL process_exception(EXCEPTION_POINTERS * exception, PTCHAR dump_path, MINIDUMP_TYPE type, PTCHAR host, PTCHAR uri);
    #ifdef __cplusplus
    }
    #endif
     
    LONG WINAPI SEH(EXCEPTION_POINTERS * lpTopLevelExceptionFilter)
    {
        process_exception(lpTopLevelExceptionFilter, L"%TEMP%\\crash.dmp", MiniDumpNormal, L"kaimi.ru", L"test.php");
     
        return 0L;
    }
     
    int main()
    {
        SetUnhandledExceptionFilter(SEH);
     
        *(DWORD *)0 = 1;
     
        return 0;
    }
    Также для тестирования пригодится примитивный PHP-скрипт, который будет обрабатывать переданный на сервер файл, например, такой:
    Code:
    <?php
    $target_path = 'reports/';
    $target_path = $target_path . mt_rand() . '_' .$_SERVER['REMOTE_ADDR'] . '_' . time() . '.dmp'; 
    if(move_uploaded_file($_FILES['report']['tmp_name'], $target_path))
        echo 'ok';
    else
        echo 'err';
    ?>
    Переходим к проверке. Запускаем программу, наблюдаем сообщение Windows об ошибке, лезем на сервер и забираем дамп. Теперь открываем дамп в WinDbg, пишем !analyze -v и видим причину падения.

    Кстати, результат анализа дампа может быть менее понятным в зависимости от отсутствия/наличия PDB файлов и Debug-информации в самом файле.

    [​IMG]
    http://kaimi.ru/wp-content/uploads/2012/12/crash.png

    Исходный код и проект для VS2010: скачать
     
    _________________________
    #1 Suicide, 21 Dec 2012
    Last edited: 22 Dec 2012