Логируем необрабатываемые исключения
Пятница, 21. Декабрь 2012
Раздел: C/C++, Windows, автор: Kaimi
Надоело созерцать зелёную морду на титульной странице блога, поэтому настало время эту морду сместить.
Иногда при разработке мелких сетевых утилит приходится сталкиваться с ситуацией, когда софт был загружен на некий удаленный сервер, поработал немного и через какое-то время прекратил работать по неизвестной причине. Конечно, можно было бы вручную заходить на каждый сервер и пытаться разобраться, что произошло, но это несколько утомительное занятие. Для автоматизации процесса я решил набросать небольшую статическую библиотеку, которая будет заниматься логированием подобных ошибок и отправкой их на сервер.
Основу библиотеки фактически будет составлять одна функция - MiniDumpWriteDump, которая делает дамп памяти процесса. Этот дамп впоследствии можно открыть, например, в WinDBG и посмотреть причину неожиданного падения процесса. Также хочу отметить, что мы "покладем" на замечание из MSDN о необходимости вызова данной функции из отдельного процесса.
Итак, начнем с основной функции, которая будет обрабатывать исключение и создавать мини-дамп.
/* Функция получает на вход информацию об исключении, путь для сохранения мини-дампа, включая имя файла, *//* тип мини-дампа, адрес хоста, куда будет отправлен мини-дамп, и путь к скрипту, который его примет */ 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;} |
Основной код в общем-то написан, осталось реализовать функцию отправки дампа на сервер и проверить работоспособность методов. Сначала отправка:
BOOL send_report(PTCHAR host, PTCHAR uri, PTCHAR file_path){ BOOL status = TRUE; HINTERNET sess = NULL, conn = NULL, req = NULL; HANDLE fh;/* Заголовки для формирования multipart запроса на загрузку файла */conststatic TCHAR headers[]= _T("Content-Type: multipart/form-data; boundary=0123456789");conststaticchar 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";conststaticchar 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;} |
Всё, у нас есть всё, что необходимо для нашего небольшого логгера исключений. Ах да, забыли про инклюды и один дефайн:
#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, в котором будем ловить наше исключение.
Получим такой вот простой код:
#include <Windows.h>#include <DbgHelp.h> #pragma comment(lib, "report_lib.lib") #ifdef __cplusplusextern"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"); return0L;} int main(){ SetUnhandledExceptionFilter(SEH); *(DWORD *)0=1; return0;} |
Также для тестирования пригодится примитивный PHP-скрипт, который будет обрабатывать переданный на сервер файл, например, такой:
<?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';elseecho'err';?> |
Переходим к проверке. Запускаем программу, наблюдаем сообщение Windows об ошибке, лезем на сервер и забираем дамп. Теперь открываем дамп в WinDbg, пишем !analyze -v и видим причину падения.
Кстати, результат анализа дампа может быть менее понятным в зависимости от отсутствия/наличия PDB файлов и Debug-информации в самом файле.
Исходный код и проект для VS2010: скачать