Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004

.pdf
Скачиваний:
322
Добавлен:
13.08.2013
Размер:
3.3 Mб
Скачать

 

 

 

ГЛАВА 3 Отладка при кодировании

81

 

 

 

 

 

 

Листинг 3-2.

Пример исчерпывающего утверждения

 

 

 

 

 

 

 

 

 

HANDLE DEBUGINTERFACE_DLLINTERFACE __stdcall

 

 

 

 

StartDebugging

( LPCTSTR

szDebuggee

,

 

 

 

 

 

LPCTSTR

szCmdLine

,

 

 

 

 

 

LPDWORD

lpPID

,

 

 

 

 

 

CDebugBaseUser * pUserClass

,

 

 

 

 

 

LPHANDLE

lpDebugSyncEvents

)

 

 

 

{

 

 

 

 

 

 

 

// Утверждаем параметры.

 

 

 

 

 

ASSERT

( FALSE

== IsBadStringPtr ( szDebuggee , MAX_PATH ) ) ;

 

 

ASSERT

( FALSE

== IsBadStringPtr ( szCmdLine , MAX_PATH ) ) ;

 

 

ASSERT

( FALSE

== IsBadWritePtr ( lpPID , sizeof ( DWORD ) ) ) ;

 

ASSERT

( FALSE

== IsBadReadPtr ( pUserClass ,

 

 

 

 

 

 

 

sizeof ( CDebugBaseUser * ) ) ) ;

 

ASSERT

( FALSE

== IsBadWritePtr ( lpDebugSyncEvents ,

 

 

 

 

 

 

sizeof ( HANDLE ) *

 

 

 

 

 

 

NUM_DEBUGEVENTS ) ) ;

 

 

// Проверяем их существование.

 

 

 

 

 

if ( (

TRUE ==

IsBadStringPtr ( szDebuggee , MAX_PATH )

)

||

 

(

TRUE ==

IsBadStringPtr ( szCmdLine , MAX_PATH )

)

||

 

(

TRUE ==

IsBadWritePtr ( lpPID , sizeof ( DWORD )

) )

||

 

(

TRUE ==

IsBadReadPtr ( pUserClass ,

 

 

 

 

 

 

 

sizeof ( CDebugBaseUser * ) ) )

||

 

(

TRUE ==

IsBadWritePtr ( lpDebugSyncEvents ,

 

 

 

 

 

 

 

sizeof ( HANDLE ) *

 

 

 

 

 

 

 

NUM_DEBUGEVENTS )

)

)

 

{

 

 

 

 

 

 

 

SetLastError ( ERROR_INVALID_PARAMETER ) ; return ( INVALID_HANDLE_VALUE ) ;

}

//Строка для события стартового подтверждения. TCHAR szStartAck [ MAX_PATH ] = _T ( "\0" ) ;

//Загружаем строку для стартового подтверждения.

if ( 0 == LoadString ( GetDllHandle ( )

,

IDS_DBGEVENTINIT

,

szStartAck

,

MAX_PATH

) )

{

 

ASSERT ( !"LoadString IDS_DBGEVENTINIT failed!" ) ; return ( INVALID_HANDLE_VALUE ) ;

}

//Описатель стартового подтверждения, которого будет ждать

//эта функция, пока не запустится отладочный поток.

HANDLE hStartAck = NULL ;

// Создаем событие стартового подтверждения.

см. след. стр.

82 ЧАСТЬ I Сущность отладки

hStartAck = CreateEvent ( NULL

,

// Безопасность по умолчанию.

TRUE

,

//

Событие с ручным сбросом.

FALSE

,

//

Начальное состояние=Not signaled.

szStartAck ) ; // Имя события. ASSERT ( NULL != hStartAck ) ;

if ( NULL == hStartAck )

{

return ( INVALID_HANDLE_VALUE ) ;

}

// Связываем параметры.

 

 

THREADPARAMS stParams ;

 

 

stParams.lpPID = lpPID ;

 

 

stParams.pUserClass = pUserClass ;

 

stParams.szDebuggee

= szDebuggee

;

 

stParams.szCmdLine

= szCmdLine

;

 

// Описатель для отладочного потока.

 

HANDLE hDbgThread = INVALID_HANDLE_VALUE ;

 

// Пробуем создать поток.

 

 

UINT dwTID = 0 ;

 

 

 

hDbgThread = (HANDLE)_beginthreadex ( NULL

,

 

 

0

,

 

 

DebugThread

,

 

 

&stParams

,

 

 

0

,

 

 

&dwTID

) ;

ASSERT ( INVALID_HANDLE_VALUE != hDbgThread ) ;

 

if (INVALID_HANDLE_VALUE == hDbgThread )

 

{

 

 

 

VERIFY ( CloseHandle ( hStartAck ) ) ; return ( INVALID_HANDLE_VALUE ) ;

}

//Ждем, пока отладочный поток не придет в норму и продолжаем. DWORD dwRet = ::WaitForSingleObject ( hStartAck , INFINITE ) ; ASSERT (WAIT_OBJECT_0 == dwRet ) ;

if (WAIT_OBJECT_0 != dwRet )

{

VERIFY ( CloseHandle ( hStartAck ) ) ; VERIFY ( CloseHandle ( hDbgThread ) ) ; return ( INVALID_HANDLE_VALUE ) ;

}

//Избавляемся от описателя подтверждения.

VERIFY ( CloseHandle ( hStartAck ) ) ;

//Проверяем, что отладочный поток еще выполняется. Если это не так,

//отлаживаемое приложение, вероятно, не может запуститься.

 

ГЛАВА 3 Отладка при кодировании

83

 

 

 

 

 

 

DWORD dwExitCode = ~STILL_ACTIVE ;

 

if ( FALSE

== GetExitCodeThread ( hDbgThread , &dwExitCode ) )

 

{

 

 

ASSERT

( !"GetExitCodeThread failed!" ) ;

 

VERIFY

( CloseHandle ( hDbgThread ) ) ;

 

return

( INVALID_HANDLE_VALUE ) ;

 

}

 

 

ASSERT ( STILL_ACTIVE == dwExitCode ) ; if ( STILL_ACTIVE != dwExitCode )

{

VERIFY ( CloseHandle ( hDbgThread ) ) ; return ( INVALID_HANDLE_VALUE ) ;

}

//Создаем события синхронизации, чтобы главный поток

//мог сообщить отладочному циклу, что делать.

BOOL bCreateDbgSyncEvts =

CreateDebugSyncEvents ( lpDebugSyncEvents , *lpPID ) ; ASSERT ( TRUE == bCreateDbgSyncEvts ) ;

if ( FALSE == bCreateDbgSyncEvts )

{

//Это серьезная проблема. Отладочный поток выполняется, но

//я не смог создать события синхронизации, необходимые потоку

//пользовательского интерфейса для управления отладочным потоком.

//Мое единственное мнение — выходить. Я закрою отладочный поток

//и просто выйду. Больше я ничего не могу сделать.

TRACE ( "StartDebugging : CreateDebugSyncEvents failed\n" ) ;

VERIFY ( TerminateThread ( hDbgThread , (DWORD) 1 ) ) ;

VERIFY ( CloseHandle ( hDbgThread ) ) ;

return ( INVALID_HANDLE_VALUE ) ;

}

//Просто на случай, если кто то изменит функцию

//и не сможет правильно указать возвращаемое значение. ASSERT ( INVALID_HANDLE_VALUE != hDbgThread ) ;

//Жизнь прекрасна!

return ( hDbgThread ) ;

}

Утверждения в .NET Windows Forms или консольных приложениях

Перед тем как перейти к мелким подробностям утверждений .NET, хочу отметить одну ключевую ошибку, которую я встречал практически во всех кодах .NET, осо$ бенно во многих примерах, из которых разработчики берут код для создания своих приложений. Все забывают, что можно передать в объектном параметре значе$ ние null. Даже когда разработчики используют утверждения, код выглядит при$ мерно так:

84 ЧАСТЬ I Сущность отладки

void DoSomeWork ( string TheName )

{

Debug.Assert ( TheName.Length > 0 ) ;

Если TheName имеет значение null, то вместо срабатывания утверждения вызов свойства Length приводит к исключению System.NullReferenceException, тут же об$ рушивая ваше приложение. Это тот ужасный случай, когда утверждение вызывает нежелательный побочный эффект, нарушая основное правило утверждений. И, разумеется, отсюда следует, что если разработчики не проверяют наличие пустых объектов в утверждениях, то не делают этого и при обычной проверке парамет$ ров. Окажите себе огромную услугу: начните проверять объекты на null.

То, что приложения .NET не должны заботиться об указателях и блоках памя$ ти означает, что по крайней мере 60% утверждений, использовавшихся нами в дни C++, ушли в прошлое. В сфере утверждений команда .NET добавила в простран$ ство имен System.Diagnostic два объекта — Debug и Trace, активных, только если в компиляции приложения вы определили DEBUG или TRACE соответственно. Оба эти определения могут быть указаны в диалоговом окне Property Pages проекта. Как вы видели, метод Assert обрабатывает утверждения в .NET. Довольно интересно, что и Debug и Trace обладают похожими методами, включая Assert. Мне кажется, что наличие двух возможных утверждений, компилирующихся по разным усло$ виям, может сбить с толку. Следовательно, поскольку утверждения должны быть активны только в отладочных сборках, для утверждений я использую только Debug.Assert. Это позволяет избежать сюрпризов от конечных пользователей, зво$ нящих мне с вопросами о странных диалоговых окнах или сообщениях о том, что что$то пошло не так. Я настоятельно рекомендую вам делать то же самое, внося свой вклад в целостность мира утверждений.

Есть три перегруженных метода Assert. Все они принимают значение булев$ ского типа в качестве первого или единственного параметра, и, если оно равно false, инициируется утверждение. Как видно из предыдущих примеров, где я ис$ пользовал Debug.Assert, один из методов принимает второй параметр типа string, который отображается в выдаваемом сообщении. Последний перегруженный метод Assert принимает третий параметр типа string, предоставляющий еще больше дан$ ных при срабатывании утверждения. По моему опыту случай с двумя параметра$ ми — самый простой для использования, так как я просто копирую условие, про$ веряемое в первом параметре, и вставляю его как строку. Конечно, теперь, когда нужное в утверждении условное выражение находится в кавычках, проверяя пра$ вильность кода, следует контролировать, чтобы строковое значение всегда совпа$ дало с реальным условием. Следующий код демонстрирует все три метода Assert в действии.

Debug.Assert ( i > 3

)

 

 

 

 

 

Debug.Assert

(

i

>

3

, "i

>

3"

)

 

Debug.Assert

(

i

>

3

,

"i

>

3"

,

"This means I got a bad parameter")

Объект Debug в .NET интересен тем, что позволяет представлять результат раз$ ными способами. Исходящая информация от объекта Debug (и соответственно объекта Trace) проходит через другой объект — TraceListener. Классы$потомки

ГЛАВА 3 Отладка при кодировании

85

 

 

TraceListener добавляются в свойство объекта Debug — набор Listener. Прелесть такого подхода в том, что при каждом нарушении утверждения объект Debug перебирает набор Listener и по очереди вызывает каждый объект TraceListener. Благодаря этой удобной функциональности даже при появлении новых усовершенствованных способов уведомления для утверждений вам не придется вносить серьезных из$ менений в код, чтобы задействовать их преимущества. Более того, в следующем разделе я покажу, как добавить новые объекты TraceListener, вообще не изменяя код, что обеспечивает превосходную расширяемость!

Используемый по умолчанию объект TraceListener называется DefaultTraceListener. Он направляет исходящую информацию в два разных места, самым заметным из которых является диалоговое окно утверждения (рис. 3$1). Как видите, большая его часть занята информацией из стека и типами параметров. Также указаны ис$ точник и строка для каждого элемента. В верхних строках окна выводятся стро$ ковые значения, переданные вами в Debug.Assert. На рис. 3$1 я в качестве второго параметра передал в Debug.Assert строку «Debug.Assert assertion».

Результат нажатия каждой кнопки описан в строке заголовка информацион$ ного окна. Единственная интересная клавиша — Retry. Если вы исполняете код в отладчике, вы просто переходите в отладчик на строку, следующую за утвержде$ нием. Если вы не в отладчике, щелчок Retry инициирует специальное исключе$ ние и запускает селектор отладчика по требованию, позволяющий выбрать заре$ гистрированный отладчик для отладки утверждения.

В дополнение к выводу в информационном окне Debug.Assert также направля$ ет всю исходящую информацию через OutputDebugString, поэтому ее получает под$ ключенный отладчик. Эта информация предоставляется в схожем формате, кото$ рый показан в следующем коде. Поскольку DefaultTraceListener выполняет вывод через OutputDebugString, вы можете воспользоваться прекрасной программой Марка Руссиновича (Mark Russinovich) DebugView (www.sysinternals.com), чтобы просмот$ реть его, не находясь в отладчике. Ниже я расскажу об этом подробнее.

——DEBUG ASSERTION FAILED ——

——Assert Short Message —— Debug.Assert assertion

——Assert Long Message ——

at HappyAppy.Fum() d:\asserterexample\asserter.cs(15)

at HappyAppy.Fo(StringBuilder sb) d:\asserterexample\asserter.cs(20) at HappyAppy.Fi(IntPtr p) d:\asserterexample\asserter.cs(24)

at HappyAppy.Fee(String Blah) d:\asserterexample\asserter.cs(29) at HappyAppy.Baz(Double d) d:\asserterexample\asserter.cs(34)

at HappyAppy.Bar(Object o) d:\asserterexample\asserter.cs(39) at HappyAppy.Foo(Int32 i) d:\asserterexample\asserter.cs(46) at HappyAppy.Main() d:\\asserterexample\asserter.cs(76)

86 ЧАСТЬ I Сущность отладки

Рис. 3 1. Информационное окно DefaultTraceListener

Обладая информацией, предоставляемой Debug.Assert, вы никогда больше не будете раздумывать, почему сработало утверждение! .NET Framework также пре$ доставляет два других объекта TraceListener. Для записи исходящей информации в текстовый файл используйте класс TextWriterTraceListener, а для записи ее в журнал событий — класс EventLogTraceListener. К сожалению, классы TextWriterTraceListener

и EventLogTraceListener практически бесполезны, потому что записывают только поля сообщений ваших утверждений и не включают информацию о стеке. Хоро$ шая новость в том, что реализовать собственные объекты TraceListener неслож$ но, поэтому в рамках BugslayerUtil.NET.DLL я пошел дальше и написал для вас ис$ правленные версии TextWriterTraceListener и EventLogTraceListener: Bugslayer TextWriterTraceListener и BugslayerEventLogTraceListener соответственно.

И BugslayerTextWriterTraceListener, и BugslayerEventLogTraceListener — вполне заурядные классы. BugslayerTextWriterTraceListener наследует напрямую от TextWri terTraceListener, и все, что он делает, — переопределяет метод Fail, который Debug.Assert вызывает для вывода информации. Помните, что при использовании

BugslayerTextWriterTraceListener или TextWriterTraceListener соответствующий тек$ стовый файл с исходящей информацией не сбрасывается на диск, если не задать true атрибуту autoflush элемента trace в конфигурационном файле приложения, не вызвать явно Close для потока или файла или не задать Debug.AutoFlush значе$ ние true, чтобы каждая запись автоматически вызывала сброс на диск. По каким$ то причинам класс EventLogTraceListener является закрытым, поэтому я не мог на$ следовать от него напрямую и создал потомок прямо от абстрактного класса TraceListener. Однако я все$таки получил информацию о стеке весьма интересным способом. Как показано ниже, стандартный класс StackTrace, предоставляемый .NET, позволяет в любой момент легко получить информацию о стеке.

StackTrace StkTrc = new StackTrace ( ) ;

В сравнении с действиями, которые надо было выполнять в машинном коде, чтобы получить такую информацию, способ, предоставляемый .NET, служит пре$ красным примером того, как .NET облегчает вашу жизнь. StackTrace возвращает набор объектов StackFrame, представляющих стек. Просмотрев документацию на StackFrame, вы увидите, что в нем есть все виды интересных методов для получе$ ния строки и номера источника. Объект StackTrace содержит метод ToString, и я был абсолютно уверен, что через него как$то можно добавлять источник и стро$ ку в итоговую информацию о стеке. Увы, я ошибался. Поэтому мне пришлось 30

ГЛАВА 3 Отладка при кодировании

87

 

 

минут писать и тестировать класс BugslayerStackTrace, наследующий от StackTrace и переопределяющий ToString, чтобы иметь возможность добавить информацию об источнике и строке к каждому методу. В листинге 3$3 показаны два метода из BugslayerStackTrace, выполняющие эти действия.

Листинг 3-3. BugslayerStackTrace, собирающий полную информацию о стеке,

втом числе сведения об источнике и строке

///<summary>

///Создает читаемое представление информации о стеке.

///</summary>

///<returns>

///Читаемое представление информации о стеке.

///</returns>

public override string ToString ( )

{

//Обновляем StringBuilder для хранения всего необходимого. StringBuilder StrBld = new StringBuilder ( ) ;

//Первое, что надо внести, — перевод строки.

StrBld.Append ( DefaultLineEnd ) ;

//Зациклить и сделать! Здесь нельзя использовать foreach,

//так как StackTrace не наследует от IEnumerable.

for ( int i = 0 ; i < FrameCount ; i++ )

{

StackFrame StkFrame = GetFrame ( i ) ; if ( null != StkFrame )

{

BuildFrameInfo ( StrBld , StkFrame ) ;

}

}

return ( StrBld.ToString ( ) ) ;

}

/*///////////////////////////////////////////////////////////////// // Закрытые методы

/////////////////////////////////////////////////////////////////*/

///<summary>

///Выполняет мелкую работу по преобразованию фрейма

///в строку и внесению его в StringBuilder.

///</summary>

///<param name="StrBld">

///StringBuilder для внесения результатов.

///</param>

///<param name="StkFrame">

///Фрейм стека для преобразования.

///</param>

private void BuildFrameInfo ( StringBuilder StrBld

,

см. след. стр.

88 ЧАСТЬ I Сущность отладки

StackFrame StkFrame )

{

//Получаем метод через механизм отражения. MethodBase Meth = StkFrame.GetMethod ( ) ;

//Если ничего не получили, выходим отсюда. if ( null == Meth )

{

return ;

}

//Присваиваем метод.

String StrMethName = Meth.ReflectedType.Name ;

//Вносим отступ функции (function indent), если он есть. if ( null != FunctionIndent )

{

StrBld.Append ( FunctionIndent ) ;

}

//Получаем тип и имя класса.

StrBld.Append ( StrMethName ) ;

StrBld.Append ( "." ) ;

StrBld.Append ( Meth.Name ) ;

StrBld.Append ( "(" ) ;

//Вносим параметры, включая все их имена. ParameterInfo[] Params = Meth.GetParameters ( ) ; for ( int i = 0 ; i < Params.Length ; i++ )

{

ParameterInfo CurrParam = Params[ i ] ; StrBld.Append ( CurrParam.ParameterType.Name ) ; StrBld.Append ( " " ) ;

StrBld.Append ( CurrParam.Name ) ; if ( i != ( Params.Length 1 ) )

{

StrBld.Append ( ", " ) ;

}

}

//Закрываем список параметров.

StrBld.Append ( ")" ) ;

// Получаем источник и строку, только если они есть. if ( null != StkFrame.GetFileName ( ) )

{

//Мне надо определять источник? Если да, то нужно

//вставить в конце разрыв строки и отступ.

if ( null != SourceIndentString )

{

 

ГЛАВА 3 Отладка при кодировании

89

 

 

 

 

 

 

 

 

 

StrBld.Append (

LineEnd ) ;

 

StrBld.Append (

SourceIndentString ) ;

 

}

 

 

else

 

 

{

 

 

// Просто добавляем пробел.

 

StrBld.Append (

' ' ) ;

 

}

 

 

// Здесь получаем имя файла и строку с проблемой.

 

StrBld.Append ( StkFrame.GetFileName ( ) ) ;

 

StrBld.Append ( "("

) ;

 

StrBld.Append ( StkFrame.GetFileLineNumber().ToString());

 

StrBld.Append ( ")"

) ;

 

}

 

 

// Всегда добавляйте перевод строки.

 

StrBld.Append ( LineEnd

) ;

 

}

Теперь, когда у вас есть другие классы TraceListener, которые стоит добавить в набор Listeners, мы в коде можем добавлять и удалять объекты TraceListener. Как и в любом наборе .NET, чтобы добавить объект в набор, вызовите метод Add, а чтобы избавиться от объекта — метод Remove. Стандартный TraceListener называется «Default». Вот как добавить BugslayerTextWriterTraceListener и удалить Default TraceListener:

Stream AssertFile = File.Create ( "BSUNBTWTLTest.txt" ) ;

BugslayerTextWriterTraceListener tListener =

new BugslayerTextWriterTraceListener ( AssertFile ) ;

Debug.Listeners.Add ( tListener ) ;

Debug.Listeners.Remove ( "Default" ) ;

Управление объектом TraceListener через файлы конфигурации

Если вы разрабатываете консольные приложения и приложения Windows Forms, то по большей части DefaultTraceListener должен удовлетворить все ваши потреб$ ности. Однако появляющееся время от времени информационное окно может нарушить работу любых автоматизированных тестов. Или, может быть, вы исполь$ зуете компонент сторонних производителей в службе Win32, и его отладочная сборка правильно использует Debug.Assert. В обоих случаях вам потребуется от$ ключить информационное окно, вызываемое DefaultTraceListener. Можно добавить код для удаления объекта DefaultTraceListener, но его можно удалить и не прика$ саясь к коду.

Любому двоичному коду .NET может быть сопоставлен внешний конфигура$ ционный файл XML. Этот файл располагается в том же каталоге, что и двоичный файл, и имеет такое же имя с добавленным в конце словом .CONFIG. Например, конфигурационный файл для FOO.EXE называется FOO.EXE.CONFIG. Можно лег$

90 ЧАСТЬ I Сущность отладки

ко добавить конфигурационный файл к проекту, добавив новый XML$файл с именем APP.CONFIG. Этот файл будет автоматически скопирован в каталог конечных фай$ лов и назван в соответствии с именем двоичного файла.

Элемент assert, расположенный внутри system.diagnostics в конфигурацион$ ном файле XML, имеет два атрибута. Если задать false первому атрибуту — assertuie nabled, .NET не будет отображать информационные окна, но исходящая инфор$ мация по$прежнему будет направляться через OutputDebugString. Второй атрибут — logfilename — позволяет указать файл, в который следует записывать любой вы$ вод утверждений. Интересно что при указании файла в атрибуте logfilename, в этом файле также появятся все операторы трассировки, о которых я расскажу ниже. В следующем отрывке показан минимальный конфигурационный файл. Он демон$ стрирует, как просто отключить информационные окна утверждений. Не забудь$ те: главный конфигурационный файл MACHINE.CONFIG включает такие же пара$ метры, что и обычные конфигурационные файлы, так что с их помощью вы вправе отключить информационные окна на всей машине.

<?xml version="1.0" encoding="UTF 8" ?>

<configuration>

<system.diagnostics>

<assert assertuienabled="false"

logfilename="tracelog.txt" />

</system.diagnostics>

</configuration>

Как я уже отмечал, можно добавлять и удалять приемники информации (liste$ ners), не затрагивая код, и, как вы, вероятно, догадались, это как$то связано с кон$ фигурационным файлом. В документации он выглядит вполне очевидным, но на момент написания этой книги документация содержала ошибки. Экспериментально я выявил все нужные приемы для корректного управления приемниками без из$ менений кода.

Все действия выполняются над элементом trace конфигурационного файла. Этот элемент содержит один очень важный необязательный атрибут, которому всегда следует задавать true, — autoflush. Сделав так, вы предписываете сбрасывать ис$ ходящий буфер на диск при каждой операции записи. В противном случае вам придется добавлять в код вызовы для сброса информации.

Внутри trace содержится элемент listener, через который добавляются и уда$ ляются объекты TraceListener. Удалить объект TraceListener очень просто. Укажи$ те элемент remove и задайте его атрибуту name строковое имя нужного объекта TraceListener. Ниже приведен полный конфигурационный файл, удаляющий Default TraceListener.

<?xml version="1.0" encoding="UTF 8" ?>

<configuration>

<system.diagnostics>

<trace autoflush="true" indentsize="0">

<listeners>

<remove name="Default" />

</listeners>

</trace>

</system.diagnostics>

</configuration>

Соседние файлы в предмете Программирование на C++