Требования к TraceSrv
Сначала сформулируем цели проектирования TraceSrv, потому что это вообще лучший способ понять, что именно должна выполнять любая программа. Итак:
1. TraceSrv должна быть совместима с общими языками программирования, включая, как минимум, C++, Visual Basic, Borland Delphi, Visual Basic for Applications, Java, Jscript и VBScript.
2. TraceSrv должна быть очень проста для использования внутри языка программирования.
3. TraceSrv должна всегда выполняться так, чтобы любое приложение могло в любой момент соединяться с ней.
4. Операторы трассировки программы, которая выполняется на нескольких машинах, должны направлять свои результаты в один каталог (файл).
5. Приложения просмотра трасс (trace viewer applications) должны видеть строки трасс от нескольких машин одновременно.
6. Должны быть доступны следующие операции обработки необязательных параметров (опций) трассировочных строк:
• добавление (в строку трассировки) префикса со временем получения строки;
• добавление префикса с номером строки;
• добавление префикса с идентификатором (ID) процесса, который послал строку трассировки;
• добавление в конец записи трассы символов возврата каретки и перевода строки, если необходимо;
• посылать предложения трассировки через основной отладчик, где выполняется процесс TraceSrv.
7. Если хотя бы один из параметров TraceSrv, перечисленных в п. 6 изменяется, все активные программы просмотра трасс должны быть уведомлены, для того чтобы все эти программы (даже на других машинах) были скоординированы с текущими опциями.
На первый взгляд, требования к TraceSrv могут показаться чрезмерно завышенными из-за необходимости многоязычного программирования и работы в сети. Я предполагал, что можно переадресовать многоязычную поддержку простой динамически компонуемой библиотеке (DLL), которую мог бы загружать кто угодно. Поскольку я — прежде всего системный программист, а не Web-разработчик, то сказалось незнание языков VBScript и Java. В частности, при ближайшем знакомстве с VBScript я понял, что никакие хакер-ские трюки не заставят VBScript напрямую вызывать DLL.
Свет, наконец,
забрезжил в конце тоннеля, когда выяснилось, что VBScript поддерживает функцию createObject; мне как раз был необходим СОМ-объект, a VBScript способен прекрасно его использовать. Поскольку СОМ-объект работает почти на всех языках, было решено сделать TraceSrv простым СОМ-объектом.
СОМ-технология легко решает проблему сетевого программирования. Имеется свободно распространяемый расширенный вариант СОМ-технологии — СОМ+, который поддерживает "непрерывное выполнение", потому что СОМ+-серверы могут выполняться как службы Microsoft Win32. Если применяется служба автоматического запуска, то СОМ-объект всегда готов к использованию.
Моя первая схватка с СОМ+-службами (известными в свое время как службы DCOM2) произошла в далекие времена альфа-версии Microsoft Windows NT 4 и была довольно неприятной. Мало того что нужно было написать службы (не самая легкая работа), но пришлось также изрядно повозиться с СОМ-объектами (перед их подключением). К счастью, всю черную работу по написанию СОМ+-служб выполняет библиотека активных шаблонов (ATL3), которая поставляется с Microsoft Visual C++ 6 и даже включает мастер, помогающий сгенерировать код.
Как только было обозначено главное направление разработки, возникла необходимость в определении интерфейса для TraceSrv. Главный интерфейс TraceSrv (itrace) определен в IDL-файле4TRACESRV.IDL, показанном в листинге 11-1. Для передачи в TraceSrv операторов трассировки я применяю метод Trace интерфейса iTrace, а чтобы приспособиться к разнообразию языков, установил специальный строчный тип BSTR (см. параметр bstrText в описании операторов трассировки).
COM+ технология сочетает использование модели компонентных объектов (СОМ) и сервера транзакций корпорации Microsoft (MTS — Microsoft Transaction Server). — Пер.
DCOM (Distributed Component Object Model) — распределенная модель компонентных объектов. — Пер.
3ATL — ActiveX Template Library. — Пер.
IDL-файл — специальный файл, автоматически генерируемый мастерами ATL при создании СОМ-компонента.
В нем определяется (в специальном формате) собственно СОМ-объект, его интерфейс и библиотека типов. Синтаксис IDL-файла очень похож на C++. Перед объявлением каждого объекта в квадратных скобках указываются его атрибуты. — Пер
Листинг 11-1.TRACERV.IDL
/*- - - - - - - - - - - - - - - - - - - - - - - - - -
"Debugging Applications" (Microsoft Press)
Copyright (c) 1997-2000 John Robbins — All rights reserved.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - */
import "oaidl.idl";
import "ocidl.idl";
[
object ,
uuid ( 4D42AOOC-7774-11D3-9F57-OOC04FA34F2C ) ,
dual ,
helpstring ( "ITrace Interface" ) ,
pointer_default ( unique )
]
interface ITrace : IDispatch
{
[ id ( 1 ) ,
helpstring ( "method Trace" ) ]
HRESULT Trace ( [ in ] BSTR bstrText ) ;
[ id ( 2 ) ,
helpstring ( "method FullTrace" ) ]
HRESULT FullTrace ( [ in ] BSTR bstrText , [ in ] long dwPID ) ;
[ propget, id ( 3 ) ,
helpstring ( "property ShowTimeStamps" ) ]
HRESULT ShowTimeStamps ( [ out, retval ] VARIANT_BOOL *pVal ) ;
[ propput, id ( 3 ) ,
helpstring ( "property ShowTimeStamps" ) ]
HRESULT ShowTimeStamps ( [ in ] VARIANT_BOOL newVal ) ;
[ propget,
id ( 4 ) ,
helpstring ( "property ShowTraceAsODS" ) ]
HRESULT ShowTraceAsODS ( [ out, retval ] VARIANT_BOOL *pVal ) ;
[ propput,
id ( 4 ) ,
helpstring ( "property ShowTraceAsODS" ) ]
HRESULT ShowTraceAsODS ( [ in ] VARIANT_BOOL newVal ) ;
[ propget,
id ( 5 ) ,
helpstring ( "property ShowItemNumber" ) ]
HRESULT ShowItemNumber ( [ out, retval ] VARIANT_BOOL *pVal ) ;
[ propput,
id ( 5 ) ,
helpstring ( "property ShowItemNumber" ) ]
HRESULT ShowItemNumber ( [ in ] VARIANT_BOOL newVal ) ;
[ propget,
id ( 6 ) ,
helpstring ( "property ShowPID" ). ]
HRESULT ShowPID ( [ out, retval ] VARIANT_BOOL *pVal ) ;
[ propput,
id ( 6 ) ,
helpstring ( "property ShowPID" ) ]
HRESULT ShowPID ( [ in ] VARIANTJ30OL newVal ) ;
[ propget,
id ( 7 ) ,
helpstring ( "property AddCRLF" ) ]
HRESULT AddCRLF ( [ out, retval ] VARIANT_BOOL *pVal ) ;
[ propput,
id ( 7 ) ,
helpstring ( "property AddCRLF" ) ]
HRESULT AddCRLF ( [ in ] VARIANT_BOOL newVal ) ;
} ;
[
uuid ( 4D42AOOO-7774-11D3-9F57-OOC04FA34F2C ) ,
version ( 1.0 ) ,
helpstring ( "TraceSrv 1.0 Type Library" ) ]
library TRACESRVLib
{
importlib ( "stdole32.tlb" ) ;
importlib ( "stdole2.tlb" ) ;
[
uuid ( 4D42AOOE-7774-11D3-9F57-OOC04FA34F2C ) ,
helpstring ( "_ITraceEvents Interface" )
]
dispinterface _ITraceEvents
{
properties: methods:
[ id ( 1 ) ,
helpstring ( "method TraceEvent" ) ] HRESULT TraceEvent ( BSTR bstrText ) ;
[ id ( 2 ) ,
helpstring ( "method ChangeShowTimeStamps" ) ]
HRESULT ChangeShowTimeStamps ( VARIANT_BOOL bNewVal ) ;
[ id ( 3 ) ,
helpstring ( "method ChangeShowTraceAsODS" ) ]
HRESULT ChangeShowTraceAsODS ( VARIANT_BOOL bNewVal ) ;
[ id ( 4 ) ,
helpstring ( "method ChangeShowItemNumber" ) ]
HRESULT ChangeShowItemNumber ( VARIANT_BOOL bNewVal ) ;
[ id ( 5 ) ,
helpstring ( "method ChangeShowPID" ) ]
HRESULT ChangeShowPID ( VARIANT_BOOL bNewVal ) ;
[ id ( 6 ) ,
helpstring ( "method ChangeAddCRLF" ) }
HRESULT ChangeAddCRLF ( VARIANT__BOOL bNewVal ) ;
} ;
[
uuid ( 4D42AOOD-7774-11D3-9F57-OOC04FA34F2C ) ,
helpstring ( "Trace Class" )
]
coclass Trace
{
[ default ] interface ITrace ;
[ default, source ] dispinterface _ITraceEvents ;
} ;
} ;
Для того чтобы написать программу просмотра операторов трассировки, нужно просто обрабатывать события интерфейса iTraceEvents.
В интерфейсе ITrace определены свойства1TraceSrv, которые реализуют перечисленные выше (в п. 6 списка требований) параметры операторов трассировки (на тот случай, если приложение, использующее TraceSrv, захочет изменить их). Когда свойство программы TraceSrv изменяется, она генерирует событие, которое должна обработать специальная программа просмотра трассы — TraceView. Эта программа (она рассмотрена чуть ниже) показывает, как следует обрабатывать каждое событие, которое генерирует TraceSrv.
Мастер AppWizard (создающий СОМ-приложения средствами ATL) строит почти 90% кода СОМ+-службы. Мне пришлось написать только интерфейс TraceSrv и обработчики. Большая часть этих кодов находится в файлах TraceSrvTRACE.H и TRACE.CPP на сопровождающем компакт-диске. Они, в основном, выполняют установку и получение свойств и запуск событий. Единственная неординарная функция CTrасе: :ProcessTrace (обрабатывающая строки трассы) показана в листинге 11-2.
Здесь речь идет о том, что атрибуты propput и propget IDL-файла информируют некоторые языки (типа Visual Basic), что с указанным в них методом нужно обращаться, как со свойством. — Пер.
Листинг 11-2. Функция CTrасе: :ProcessTrace
HRESULT CTrace :: ProcessTrace ( BSTR bstrText , long dwPID)
{
// Все перепроверяйте и ничему не верьте!
ASSERT ( this ) ;
ASSERT ( NULL != bstrText ) ;
// Длина входной строки. Длина вычисляется после того, как
// проверен указатель.
int ilnputLen = 0 ;
if ( NULL == bstrText )
{
return ( Error ( IDS_NULLSTRINGPASSED ,
GUID_NULL ,
E_INVALIDARG ) ) ;
}
// bstrText содержит некоторый указатель.
// Удостовериться, что указатель содержит правильное значение.
ASSERT ( FALSE = IsBadReadPtr ( bstrText , sizeof ( BSTR ) ) ) ;
ASSERT ( L';\0'; != *bstrText );
if ( ( TRUE == IsBadReadPtr ( bstrText , sizeof ( BSTR ) ) ) ||
( L';\0'; == *bstrText ) )
{
return ( Error ( IDS_INVALIDSTRING , GUID_NULL
E_INVALIDARG ) ) ;
}
// Теперь, когда указатель проверен, получить длину
// входной строки (в символах).
iInputLen = IstrlenW ( bstrText ) ;
// Вычислить максимальное число байт, необходимых для
// входной строки.
UINT uiSize = ( ilnputLen * sizeof ( OLECHAR ) ) +
k_SIZE_FULLFORMATBYTES ;
// Захватить объект lock, чтобы защитить класс m_cOutput.
// Grab the lock to protect the m_cOutput class.
ObjectLock lock ( this ) ;
// Если это первое обращение к ProcessTrace (m_lBuffSize - 0),
//то этот if-блок служит исходной точкой распределения памяти,
if ( uiSize >= m_cOutput.BufferSize ( ) )
{
// Удалить существующий буфер и распределить больший.
m_cOutput.Free ( ) ;
// Распределить буфер, вдвое превышающий размер входной строки
. // Это делается для того, чтобы выполнять распределение памяти
//не так часто. Это компромисс между неиспользуемой
// дополнительной памятью и временем на непрерывные распределения.
// Умножение размера буфера на 2 гарантирует также, что
//сохраняется четный размер памяти. Программа работает
// с символами Unicode, поэтому следует избегать нечетных // распределений памяти.
UINT uiAllocSize = uiSize * 2 ;
// Убедитесь, что получен минимальный размер буфера.
// Минимальный размер буфера 2 Кбайт, так что в большинстве
// случаев код в этом if-блоке выполняется только однажды.
if ( k_MIN_TRACE_BUFF_SIZE > uiAllocSize )
{
uiAllocSize = k_MIN_TRACE_BUFF_SIZE ;
}
OLECHAR * pTemp = m_cOutput.Allocate ( uiAllocSize ) ;
ASSERT ( NULL != pTemp ) ;
if ( NULL == pTemp )
{
return ( Error ( IDSJXJTOFMEMORY ,
GUID_NULL , EJDUTOFMEMORY ) ) ;
}
}
// Все проверено, теперь можно начать реальную работу.
// Увеличить на 1 итоговый счетчик.
m_dwCurrCount++ ;
if ( 100000 == m_dwCurrCount )
{
m_dwCurrCount = 0 ;
}
// Установить указатель маркера в начало буфера вьвода
OLECHAR * pCurr = m__cOutput.GetDataBuffer ( ) ;
if ( -1 = m_vbShowItemNumber )
{
pCurr += wsprintfW ( pCurr , L"%05d " , m_dwCurrCount ) ;
}
if ( -1 == m_vbShowTimeStamps )
{
// Показать метку времени в формате местного пользователя.
// (здесь сервер, а не в клиент!). Я устанавливаю метку
// в 24-часовом формате.
int iLen = GetTimeFormatW ( LOCALE_USER_DEFAULT ,
LOCALE_NOUSEROVERRIDE |
TIME_FORCE24HOURFORMAT |
TIME_NOTIMEMARKER ,
NULL
NULL ,
pCurr ,
k_SIZE_TIME ) ; ASSERT ( 0 != iLen ) ;
// Переместить указатель, но не забыть о наличии
// NULL-символа в конце строки.
pCurr 4= ( iLen - I ) ;
11 GetTimeFormat не добавляет дополнительного пробела,
// поэтому добавляем его сейчас.
*pCurr = L' ' ;
pCurr++ ;
}
if ( -1 == m_vbShowPID )
{
pCurr += wsprintfW ( pCurr , L"[%04X] " , dwPID ) ;
}
// Теперь поместите в буфер фактическое сообщение и копируйте
// NULL-терминатор в конец строки.
IstrcpynW ( pCurr , bstrText , IlnputLeri + 1 ) ;
// Переместить pCurr, чтобы указать на NULL-терминатор.
pCurr += ilnputLen ;
// Проверить, не нужны ли символы CRLF в конце строки,
if ( -1 == m_vbAddCRLF )
{
if ( ( L';\xOD'; != *( pCurr _ 2 ) ) ||
( L';\xOA'; != *( pCurr _ 1 ) ) )
{
*( pCurr ) = L';\xOD;;
*( pCurr + 1 } = L';\xOA'; ;
pCurr += 2 ;
*pCurr = YL';\0'; ;
}
}
// Предполагается ли получить снимок для отладчика режима ядра?
if ( -1 == m_vbShowTraceAsODS )
{
OutputDebugStringW ( (OLECHAR*) m_cOutput ) ;
}
// Подсчитать длину строки.
m_cOutput.GetStringByteLength ( ) ;
// Вывод сообщения о результате трассировки.
#ifdef _DEBUG
HRESULT hr =
#endif
Fire_TraceEvent ( m_cOutput ) ;
#ifdef _DEBUG
if ( ! SUCCEEDED ('hr ) )
{
ASSERT ( SUCCEEDED ( hr ) ) ;
TRACE ( Т ( "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" ) ) ;
TRACE ( _T ( "TraceSrv FireJTraceEvent failed!!\n" ) ) ;
TRACE ( Т ( "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" ) ) ;
}
#endif
return ( S_OK ) ;
}
В целом, реализация TraceSvr довольно проста. Команда Implement Connection Point меню ClassView делает обработку кода интерфейса IconnectionPoint очень приятной. По сравнению с ATL Proxy Generator из Microsoft Visual C++ 5, эта команда значительно усовершенствована.
Обработке строк типа BSTR было уделено много внимания. Поскольку имелись в виду сценарии, в которых количество операторов трассировки должно увеличиваться весьма интенсивно, то хотелось удостовериться, что строки Обрабатывались максимально быстро. Функция СТгасе: : ProcessTrace
в TRACE.CPP выполняет много манипуляций со строками, особенно если учитывать различные элементы, которые могут быть размещены в начале и конце заключительного строчного вывода программы TraceSrv. Первоначально для строчных манипуляций был предназначен класс CComBSTR. Но в результате пошаговой трассировки выяснилось, что почти для каждого метода и оператора в классе он каждый раз выделял или освобождал память с помощью функций Sysxxxstring. Хотя в некоторых приложениях использование ccomBSTR совершенно законно, в программах (таких как TraceSrv), манипулирующих строками, это может привести к снижению реальной производительности.
Для того чтобы ускорить обработку строк, я написал простой класс с именем CFastBSTR, который обрабатывает тип BSTR напрямую. Класс находится в файле FASTBSTR.H. Единственной его работой является выделение памяти для одиночного буфера данных и варьирование ведущего размера (DWORD) функции GetstringByteLength. Может показаться, что я должен был неминуемо увязнуть в семантике автоматизации типа BSTR, но в этом случае увеличение производительности было важнее, чем консервативное программирование. Если такой подход кажется вам неудобным, то код в CFastBSTR нетрудно изменить, чтобы использовать обычные функции Sysxxxstring.
Нужно указать еще на одну деталь: рабочее пространство проекта имеет четыре различные конфигурации для сборки приложений — отладочную (debug) и выпускную (release) для многобайтовых символов, и две аналогичных — для символов Unicode. Многобайтовые конфигурации позволяют регистрировать TraceSrv на машинах с Windows 98. Как указано в главе 5, если вы нацеливаетесь исключительно на Windows 2000, то следует компилировать программы, ориентируясь на полноценную работу с Unicode. Поскольку я проектировал TraceSrv как службу Windows 2000, которая определенно не будет выполняться под Windows 98, то версию, устанавливаемую на серверной машине, нужно компилировать в одной из Unicode-конфигураций.
Теперь, познакомившись с кодом TraceSrv, рассмотрим особенности работы с готовой утилитой TraceSrv. Проект, созданный в Visual C++ 6, который находится на сопровождающем компакт-диске, сгенерирован в основном с помощью мастера COM AppWizard библиотеки активных шаблонов (ATL), так что последний шаг построения должен регистрировать TraceSrv. Все регистрируемые компоненты TraceSrv являются частью свободно распространяемого ATL-кода, но сама программа TraceSrv регистрируется только как локальный ЕХЕ-сервер. TraceSrv не будет выполняться как служба Win32, если не указать в командной строке ключ -service. Можно было сделать регистрацию службы частью процедуры построения, но я выбрал не это, потому что отладка служб Win32 без отладчика режима ядра (типа SoftICE) не проста. Кроме того, если вы находитесь в середине цикла "исправление-компиляция-отладка", то выход в режим командной строки (т. е. на командный процессор) и выполнение команды net stop tracesrv только для того, чтобы заставить конструкцию работать — настоящая пытка. После того как вы осуществили достаточное количество сеансов отладки и тестирования, запуская TraceSrv как локальный сервер, его можно зарегистрировать и запустить как службу.