Надоело созерцать зелёную морду на титульной странице блога, поэтому настало время эту морду сместить.
Иногда при разработке мелких сетевых утилит приходится сталкиваться с ситуацией, когда софт был загружен на некий удаленный сервер, поработал немного и через какое-то время прекратил работать по неизвестной причине. Конечно, можно было бы вручную заходить на каждый сервер и пытаться разобраться, что произошло, но это несколько утомительное занятие. Для автоматизации процесса я решил набросать небольшую статическую библиотеку, которая будет заниматься логированием подобных ошибок и отправкой их на сервер.
Основу библиотеки фактически будет составлять одна функция - MiniDumpWriteDump, которая делает дамп памяти процесса. Этот дамп впоследствии можно открыть, например, в WinDBG и посмотреть причину неожиданного падения процесса. Также хочу отметить, что мы "покладем" на замечание из MSDN о необходимости вызова данной функции из отдельного процесса.
Итак, начнем с основной функции, которая будет обрабатывать исключение и создавать мини-дамп.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
/* Функция получает на вход информацию об исключении, путь для сохранения мини-дампа, включая имя файла, */ /* тип мини-дампа, адрес хоста, куда будет отправлен мини-дамп, и путь к скрипту, который его примет */ 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; } |
Основной код в общем-то написан, осталось реализовать функцию отправки дампа на сервер и проверить работоспособность методов. Сначала отправка:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
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; } |
Всё, у нас есть всё, что необходимо для нашего небольшого логгера исключений. Ах да, забыли про инклюды и один дефайн:
1 2 3 4 5 6 7 8 9 10 |
#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, в котором будем ловить наше исключение.
Получим такой вот простой код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#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.io", L"test.php"); return 0L; } int main() { SetUnhandledExceptionFilter(SEH); *(DWORD *)0 = 1; return 0; } |
Также для тестирования пригодится примитивный PHP-скрипт, который будет обрабатывать переданный на сервер файл, например, такой:
1 2 3 4 5 6 7 8 |
<?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-информации в самом файле.
Исходный код и проект для VS2010: скачать
Полезная тема, спасибо.
Збс чё.
У вас очень интересный блог, подписываюсь!