*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

четверг, 12 февраля 2009 г.

Универсальная информация об ошибках в Windows и UNIX

Достоверная информация об ошибках во время исполнения программы является залогом простой ее эксплуатации и поддержки. Вместо выслушивания от клиента стенаний на тему "я тут что-то нажал... а тут все не работает..." можно просто попросить его прислать файл журнала программы (log), и с большой вероятностью этого будет достаточно для локализации проблемы. С логическими ошибками бизнес логики программы все понятно — тут все зависит от вас, и вы точно знаете, какая ошибка произошла. Хуже обстоит дело с ошибками системными. Тут надо максималько точно опросить систему, что произошло, и по возможности, получить расшифровку когда ошибки.

Как я уже писал, я работаю одновременно на совершенно разных платформах — от виндов до встраеваемого QNX'а. Поэтому мне всегда хочется иметь максимально простые и переносимые исходники (и желательно с минимумом нестандартных зависимостей), чтобы модули можно было просто кидать из проекта в проект, с платформы на платформу, не допиливая каждый раз что-то напильником.

Итак, привожу ниже класс, который я использую для получения информации об ошибке, произошедшей в операционной системе. Можно узнать код ошибки и его текстовое объяснение, если оно предоставляется. Это не бог весть какой сложный и оригинальный класс, но у меня он работает без каких-либо "допиливаний" на Windows 32- и 64-бит, Linux 2.6 32- 64-бит SPARC и Intel, Sun OS 5.10 SPARC и Intel, AIX, HP-UX и HP-UX IA64. К тому же, этот класс безопасен для мультипотокового использования (что лично для меня, например, очень важно).

Итак, класс SystemMessage. Все члены статические, так что можно работать с ними без создания экземпляра класса.
Пространство имен, как обычно, ext, так что измените, если необходимо.
Файл systemmessage.h:
#ifndef _EXT_SYSTEM_MESSAGE_H
#define _EXT_SYSTEM_MESSAGE_H

#include <string>

namespace ext {

class SystemMessage {
public:
// Эта функция возращает код ошибки.
static int code();
// Эта функция по коду ошибки возвращает ее текстовое описание, если
// таковое предоставляется операционной системой. Если нет, то
// возвращается строка "?".
static std::string message(int code);
};

} // namespace ext

#endif // _EXT_SYSTEM_MESSAGE_H
Файл systemmessage.cpp
#include "SystemMessage.h"

#ifdef WIN32
#include <windows.h>
#else
#include <string.h>
#include <unistd.h>
#include <errno.h>
#endif

namespace ext {

int SystemMessage::code() {
#ifdef WIN32
return GetLastError();
#else
return errno;
#endif
}

// Если система по какой-то причине не имеет функции strerror_r,
// то придется лазить напрямую в таблицу сообщений об ошибках.
// Для этого надо при компиляции определить макрос LIBC_NO_STRERROR_R.
// Пока я видел такое только на HP-UX IA64 v2.
#ifndef WIN32
#ifndef LIBC_NO_STRERROR_R
extern "C" int sys_nerr;
extern "C" char* sys_errlist[];
#endif
#endif

std::string SystemMessage::message(int code) {
char msg[1024];

#ifdef WIN32

// Версия для Windows
FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
code,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
msg,
sizeof(msg) - 1,
NULL
);

char* p = msg + strlen(msg);

// Обрезаем c конца '\r', '\n' и '.'
for(p = msg + strlen(msg) - 1;
p >= msg && (*p == '\n' || *p == '\r' || *p == '.'); --p)
*p = 0;

#elif LIBC_NO_STRERROR_R

// Если UNIX-платформа не имеет функции strerror_r, то делаем ее
// работу вручную. Пока я встретил такое только на HP-UX IA64 v2.
if (code < 0 || code >= sys_nerr)
return "?";

strncpy(msg, sys_errlist[code], sizeof(msg) - 1);

// Если сообщение об ошибке длинее чем sizeof(msg)-1, то '\0'
// не будет скопирован, поэтому добавляем его вручну.
msg[sizeof(msg) - 1] = 0;

#else

// Для нормальной UNIX-системы просто вызываем strerror_r.
if (strerror_r(code, msg, sizeof(msg) - 1) < 0)
return "?";

#endif

// Поможем компилятору по возможности оптимизировать
// возвращаемое значение как rvalue.
return std::string(msg);
}

} // namespace ext
Теперь посмострим это в работе.
Я как-то пока не придумал, как универсально написать unit-тест для этого класса, так как предсказуемые результаты будут все равно различны для каждой платформы. А писать тесты под все платформы как-то топорно. Хочется гармонии, а тут пока ее нет. Если кто имеет идею, как универсально тестировать этот класс на всех платформах — поделитесь, пожалуйста.
Тестовая программа systemmessage_test.cpp:
#include <iostream>
#include <fstream>

#include "systemmessage.h"

int main(int argc, char* argv[]) {
// Пытаемся открыть заведомо несуществующий файл.
std::ifstream is("__non_existing_file__");

// Печатаем ошибку.
int error = ext::SystemMessage::code();
std::cout
<< error << ", "
<< ext::SystemMessage::message(error)
<< std::endl;

return 0;
}
Компилируем в Visual Studio:
cl /EHsc /Fesystemmessage_test_vs2008.exe /DWIN32 systemmessage_test.cpp systemmessage.cpp
Запускаем systemmessage_test_vs2008.exe:
2, The system cannot find the file specified
Получили примерно ожидаемое виндовое сообщение об ошибке.

Теперь компилируем в Cygwin:
g++ -o systemmessage_test_cygwin.exe systemmessage_test.cpp systemmessage.cpp
Запускаем systemmessage_test_cygwin.exe:
2, No such file or directory
Получили сообщение об ошибке в стиле UNIX.
Повторюсь, в данном классе нет ничего удивительного и сложного. Просто это весьма универсальный и переносимый исходник.
И небольшая ремарка. В мире UNIX существует два диалекта функции strerror_r: XSI-версия (когда определен макрос _XOPEN_SOURCE, и он равен 600) и GNU-версия (доступная в libc, начиная с версии 2.0). Разница в том, что первая (XSI-версия) просто кладет сообщение об ошибке в предоставленный буфер и также возвращает код успешности или неуспешности своей работы в виде int'а. Нормальный UNIX-подход. Вторая версия (GNU) возвращает не int, а, собственно, указатель на строку с ошибкой, причем указываеть он может как на предоставленный функции буфер, так и куда-то еще, например, на какой-то внутренний буфер. Данный класс рассчитан на работу с XSI-версией функции strerror_r. Поэтому, если вдруг при компиляции этого класс на UNIX-системах вы получите сообщение об ошибке в использовании этой функции, то определите макрос _XOPEN_SOURCE в значение 600 (-D_XOPEN_SOURCE=600 для компилятора), тем самым будет принудительно использоваться XSI-версия этой болезной функции.

Комментариев нет:

Отправить комментарий