О костылях
Автор: Вадим К
Вступление
Часто бывает, что программу сдать нужно вчера, а вы нашли глюк и никак не можете справиться с ним. Мало того, вы не можете отследить зависимость, когда он появляется. Сегодня мы поговорим о том, как отлавливаются такие глюки, как бороться и побеждать их. Конечно, руководство не претендует на универсальное решение, но немного развеет боязнь.
Итак. Исходные условия. Программа компилируется, и возможно не только запускается, а и делает то, что планировалось. Но выкидывает случайные ошибки. Или хуже, даже не запускается. Как узнать, где и почему?
Способ первый. Лог файл.
Этот способ древний как мир. Это наверно самый первый способ, который использовали программисты для отладки, но часто он актуальный и на данный момент. Для этого способа нам понадобится простая процедура, которую можно добавить в отдельный модуль и подключить к другим отлаживаемым приложениям.
procedure AddToLog(s:string);
var
fn:string;
F:TextFile;
begin
Fn:=ExtractFilePath(ParamStr(0))+'log.txt';
assignFile(f,fn);
if FileExists(fn) then Append(f) else Rewrite(f);
Writeln(f,s);
Flush(f);
Closefile(f);
end;
Многие скажут, можно вынести создание и закрытие в отдельные процедуры и вызывать их в начале работы программы и в конце. И что код жуть неоптимальный…
Всё это неверно. Код предельно выверен. Ведь присмотритесь, он открыл файл, записал строку, потом принудительно сбросил буфер на диск (процедура Flush, хоть она и немного игнорируется в современных системах, но даже если наше приложение завалится, то строка в файл попадёт, а это важно) и закрыть файл. Таким образом, хоть и медленно, но зато всё записывается. Ведь при поиске ошибки больше важно найти саму ошибку, чем скорость, с которой программа выполнит свой расчёт. Даже для конечного пользователя часто важна стабильность и корректность расчётов, чем скорость, с которой они выполняются. За время тестирования этой нехитрой процедуры я не заметил негативных сторон и оптимизировал только строку вывода, переделав её к виду:
Writeln(f,DataTimeToStr(Now));
Writeln(f,s);
Теперь у меня в логе было ещё и время, когда произошло событие. Сколько нервов сохранила эта процедура мне…
Что же писать в лог? Писать слишком много – плохо, не разберёшься потом с километрами логов. Писать мало – слишком расплывчато будет.
Я делаю так. В начало каждой процедуры, которая хоть как-то может быть виноватой в неверной работе, добавляю вывод названия этой процедуры, и, по возможности параметры, которые ей были переданы, например так:
procedure SameFoo(a:integer; b:double);
begin
AddToLog(format(‘unit1,sameFoo, a=%d, b=%f’,[a,b]));
Функция Format позволяет сделать код более компактным и не писать долгие IntToStr и FloatToStr. Теперь, просмотрев лог программы, по последней строке можно сделать вывод, где именно она падает. А так как «стек вызовов» тоже виден (то есть последовательность, кто кого вызывал) то можно и отследить, кто отослал неверный параметр.
Один раз я наткнулся на то, что одна моя процедура время от времени выдавала ошибку, но принести отладчик мне было нереально (в силу многих независимых от меня причин), но к офису было близко :-) Параметры, которые передавались в процедуру, мне тоже ни о чём не говорили. Решение было простым. Через каждые две-три строки я вставил вызов записи в лог с указанием номера строки и некоторых параметров. После просмотра лога я уже видел строку, где происходит ошибка :-) Итого я потратил больше времени на компиляцию, чем на хождение.
Но у этого способа есть два недостатка. Невозможно указать на автомате номер строки, где произошла ошибка, имя процедуры и тому подобное. А также, когда программа полностью отлажена, все отладочные вызовы убрать очень сложно.
Второй недостаток убирается двумя способами. Или просто комментируется код самой процедуры вывода в лог (компилятор достаточно неплохо оптимизирует пустые вызовы) или использование директив условной компиляции, но наставлять их по коду очень долго, поэтому думать надо сначала об этом.
Несмотря на предельную простоту приведённого кода, он будет работать практически везде. И не только в Delphi. Можно будет использовать полученный опыт и в других языках, только процедуру придётся перевести на соответствующий язык.
Также эта процедура поможет в отладке сервисов, кода в dll. Есть только одно ограничение – она не может корректно работать в многопоточных приложениях. То есть нельзя вызывать из двух потоков одновременно. Но критические секции помогут. Достаточно добавить ещё пару строк и одну переменную типа TCriticalSection (объявлена в SyncObjs).
Способ второй. Используем специальную API-функцию.
Было бы удивительно, если бы разработчики ОС Windows не предусмотрели специальной функции для ведения лога. И такая есть! Главное уметь правильно ей пользоваться.
Называется она OutputDebugString. Как понятно из названия, она выводит переданную ей строку в лог. Так как параметр имеет тип PAnsiChar, то просто так передать строку не получается. Ничего, напишем обёртку.
procedure ODS(s:string);
begin
OutputDebugString(PAnsiChar(s));
end;
Или каждый раз придётся писать приведение типов.
Вставим в код любой программы и запустим её. Процедура скомпилировалась и даже выполняется, но визуально никак не видно последствий её работы. В папке с проектом никаких новых файлов не появилось. Где же искать лог? Если вы запускаете программу из-под отладчика (коим Delphi тоже является), то она самостоятельно перехватывает это сообщение. Посмотреть вывод можно, вызвав соответствующее окно среды (View -> Debug Window -> Event log, Ctrl+Alt+V). Delphi старается и туда попадают дополнительные сообщения о загруженных библиотеках и многое другое? и всё это можно настроить в соответствующем окне (для Delphi 7: Tools->Debugger options - > EventLog, Для 2005 и старше: Tools-> Options). В дереве слева выбираем Debugger options – Event log. А там уже играемся галочками. Скажу только что галочку Windows Message нужно ставить в исключительных случаях, иначе лог будет завален информацией о сообщениях Windows, и разобраться в тысячах строк будет очень сложно.
Ок. Это хорошо. Без отладчика ничего не видно, да и производительность вроде не страдает. А что делать, если у заказчика начали происходить странности в поведении программы, а нести к нему Delphi невозможно (заказчик может быть за сотни километров от вас)?
Тут нас спасёт утилита от Руссиновича. Качаем её с его сайта, который после покупки его компании переехал на сайт Microsoft. Последний раз её видели здесь: http://www.microsoft.com/technet/sysinternals/utilities/debugview.mspx :-) Утилита маленькая (301 Кб в архиве), предельно простая и бесплатная. Запускается без инсталляции. Будучи запущенной, начинает перехватывать сообщения от нашей программы (и не только от неё). Замечено, что многие программы пользуются этой возможностью, например Emule, так что будьте готовы настраивать фильтры в программе).
Сообщения можно сохранить в текстовый файл, который потом хорошо загружается Excel'ем как CSV файл. И, применив автофильтр, выбираем интересующие нас сообщения. Пищи для размышления программа выдает предостаточно, так что запасайтесь кофе :-)
Способ три. Проверяться и проверяться.
Во многих случаях вы знаете о допустимых диапазонах значений многих переменных. Для этого можно использовать так называемые утверждения. То есть в коде вы вставляете утверждение. Программа будет проверять их, и, если они неверны, то будет выведено сообщение с указанным вами текстом и сгенерировано исключение. Но большим преимуществом будет то, что кроме указанного вами сообщения будет также добавлено имя файла и номер строки, где произошло исключение. Согласитесь, это много.
Применяем так. Пусть мы знаем, что x содержит делитель и не может по понятным причинам быть равным нулю. Вставим перед вычислением выражения строку вида:
Assert(x<>0,'почему это x равен нулю?');
Если x действительно окажется равным нулю, то получим такое сообщение (если правильно текст сообщения написать, то даже классическая блондинка из анекдотов сможет вам по телефону объяснить, что там написано).
Очень удобно то, что это сообщение перехватывается с помощью блока try .. except. А соединив наш первый способ с этим, получаем очень удобный инструмент. Произошло исключение – в файл его. Только помните, что во многих случаях, раз уж произошло сообщение, то нужно подумать, а стоит ли продолжать выполнять процедуру? Итак, улучшенный вариант использования нашей процедуры выглядит так
var x:integer;
begin
x :=0;
try
Assert(x <> 0,'Почему это x равен нулю?');
except
on E:Exception do AddToLog(e.Message);
end;
end;
Но оборачивать каждую процедуру в блок try ... except затруднительно, да и ошибки любят появляться там, где их не ждешь. А хотелось бы всё в файл, и централизованно.
Для этого ставим на главную форму нашего приложения компонент ApplicationEvents, взятый с вкладки Additional. Свойств у этого компонента мало (имя и Tag), зато событий много. И все эти события связаны с приложением в целом. Нас интересует событие OnException. Его второй параметр содержит собственно само исключение и в простейшем случае обработка может быть сведена до AddToLog(e.Message);
О бедных dll замолвите слово
Часто приходится отлаживать код не только в основном приложении, но параллельно и в нескольких dll. И тут неискушённого программиста подстерегает множество неожиданностей. Первая и самая большая заключается в том, что если в обычном приложении все обработчики событий (нажатия на кнопку, к примеру) завёрнуты в невидимые try ... except и исключение, возникшее в процедуре, за её пределы не выходит, а вы получаете сообщение об ошибке, то в dll всё не так. Там никто таким образом вас не контролирует. Поэтому не ленитесь добавлять к коду обработку исключений. Подобное касается и потоков (TThread). Только в них всё еще интересней. Если происходит необработанное исключение, то поток умирает без каких-либо предупреждений. То есть, поделили нечаянно на ноль - и всё, поток умер. Об этом многие не знают и жалуются, что потоки мрут как мухи, процедура Terminate не выполняется и проконтролировать никак не могут…
Альтернативное средство: Jedi Code Library
Если у вас установлена эта чудесная библиотека, то можно сделать обработку ошибок более простой. Для этого нужно сделать следующее. Вначале подключить отладочную информация к выполнимому файлу. Для этого выбираем пункт Project - Insert JCL Debug data.
Теперь нужно только создать окно для отображения ошибок. В последних версиях библиотеки была добавлена в репозитарий готовая форма, которую просто нужно создать (File – New – Delphi Files – Jcl Exception dialog for Delphi). В появившемся мастере заполняем параметры, и, вуаля, у нас есть даже отсылка сообщений на электронную почту!
Самое удобное, что не нужно больше писать никакого кода. При возникновении ошибки появится диалог, в котором будет приведён стёк вызовов. Более подробно об особенностях использования этого диалога можно почитать здесь: http://rsdn.ru/article/Delphi/DelphiJCL.xml.
Большими плюсами этого средства являются:
- Бесплатность и открытость;
- Возможность изменять поведение в больших пределах;
- Сам Borland/CodeGear пользуется этим средством в BDS 2006 и BDS 2007.
Недостаток – увеличение объема кода где-то на 30-50% за счёт включения отладочной инфомации.
Альтернативное средство: MadException
Это не просто эксперт или набор файлов. Это целая студия для отлова исключений. И хотя для коммерческого использования она платна, для себя её можно использовать свободно. На сайте данного средства (http://www.madshi.net/madExceptDescription.htm) есть даже видеоролики по его использованию, так что даже у неанглоязычных людей проблем почти не возникает.
Вот такое окошко появляется, когда в приложении происходит исключение.
Если нажать на «Show bug report», то можно увидеть столько информации, что некоторые пользователи, наверное, не разрешат её отправку. Тут собрана всё подноготная, начиная с вашего приложения (где находится выполнимый файл, сколько памяти потратил), и завершая подробностями операционной системы (список процессов, список текущего установленного оборудования). Всё очень гибко настраивается, так что особых проблем не должно возникнуть. К тому же, есть возможность писать плагины, так что если какая-то информация не попала в лог, можно поправить это дело.
Основное преимущество, оно же и недостаток – слишком много информации собирает. Каждый делает свои выводы.
Альтернативное средство: EurekaLog
И ещё одно, на этот раз полностью платное решение. Оно также обладает широкими возможностями (не знаю, намного ли опережает MadException). Основная особенность, которую я заметил - это то, что когда вы видите трассировку стёка исключений, то сделав двойной клик, можно открыть данный файл в нужной строке. Но зачем на клиентской машине такая возможность?
Также есть возможность контролировать утечки памяти (правда за счёт падения производительности в 5 раз), контролировать попытки модификации кода. Если заинтересовало - смотрим здесь.
Автор: Вадим К
Статья добавлена: 21 сентября 2007
Следующая статья: Сохранение настроек »
Зарегистрируйтесь/авторизируйтесь,
чтобы оценивать статьи.
Для вставки ссылки на данную статью на другом сайте используйте следующий HTML-код:
Ссылка для форумов (BBCode):
Быстрая вставка ссылки на статью в сообщениях на сайте:
{{a:40}} (буква a — латинская) — только адрес статьи (URL);
{{статья:40}} — полноценная HTML-ссылка на статью (текст ссылки — название статьи).
Поделитесь ссылкой в социальных сетях:
Комментарии читателей к данной статье
Репутация: +359 |
Вадим К (1 ноября 2007, 11:47): Иногда, отладочные средства насколько различны, что удалить их можно только ручным поиском, никакие регулярные выражения не помогут.
НО! Вам прийдётся делать копию своего проекта. ведь проект могут быть найдены ошибки позже и добавлять отладочные средства снова - может быть накладно. Создавать отдельно копию проекта (в отдельной папке) часто тоже может быть накладно, ведь бывают зависимости от текущего каталога... С другой стороны автоматическое удаление может сколько неудалять, что потом ещё полгода прийдётся искать... В целом, пользуйтесь {$IFDEF} {$ENDIF} и будет счастье:) |
Оставлять комментарии к статьям могут только зарегистрированные пользователи.