Логируем необрабатываемые исключения Пятница, 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-информации в самом файле. http://kaimi.ru/wp-content/uploads/2012/12/crash.png Исходный код и проект для VS2010: скачать