ГЛАВА 16 Автоматизированное тестирование
583
Ловушка WH_CALLWNDPROCRET должна быть системной, поэтому ее необходимо реализовать в ее собственной DLL.
Очевидно, что DLL приложения Tester не подходит на эту роль, так как я не хочу размещать всю эту DLL и, соответственно, весь код COM в каждом адресном пространстве на компьютере пользователя. Это значит, что DLL ловушки, на! верное, должна устанавливать что!то вроде флага, который DLL приложения Tester могла бы читать, узнавая об удовлетворении нужного условия.
Tester не может быть многопоточным, поэтому мне нужно выполнять всю об! работку в одном потоке.
Первое следствие из этих требований заключалось в том, что функцию!ловушку нужно было написать на C. Так как функция!ловушка загружается во все адрес! ные пространства, ее DLL не может вызывать функции из TESTER.DLL, написан! ные при помощи разделенных потоков COM. Поэтому мой код должен был пери! одически проверять результаты работы ловушки.
Если вы писали программы для 16!разрядных ОС Windows, то знаете, что ка! кая!нибудь фоновая обработка в однопоточной среде с невытесняющей много! задачностью — прекрасная работа для API!функции SetTimer . Благодаря SetTimer вы можете выполнять фоновую задачу, сохраняя приложение однопоточным. С этой целью я включил в объект TNotify уведомление таймера, указывающее на созда! ние или уничтожение интересующих меня окон.
Фоновая обработка при помощи TNotify интересна тем, что процедура тайме! ра казалась решением на все случаи жизни, однако на самом деле она обычно работает только при наличии TNotify . В зависимости от объема сценария и от того, реализован ли в выбранном вами языке цикл сообщений, сообщение WM_TIMER может до вас не добраться, поэтому вам нужно вызывать метод CheckNotification , кото! рый также проверяет данные ловушки.
Все эти подробности могут казаться запутанными, но вы удивитесь, как мало кода понадобилось для фактической реализации Tester. В листинге 16!3 показан код функции!ловушки из файла TNOTIFYHLP.CPP. С точки зрения Tester, файл TNOTIFY.CPP — это модуль, в котором находится процедура таймера и код COM, необходимый для объекта. Класс TNotify имеет несколько методов C++, которые объект TNotify может использовать для возбуждения событий и определения ин! тересующих пользователя типов уведомлений. Один из интересных фрагментов кода функции!ловушки — глобально разделяемый сегмент данных, .HOOKDATA , со! держащий массив данных об уведомлениях. При изучении кода помните, что дан! ные об уведомлениях глобальны, в то время как все остальные данные уникальны для каждого процесса.
Листинг 16-3. TNOTIFYHLP.CPP
/*——————————————————————————————————————————————————————————————————————
Отладка приложений для Microsoft .NET и Microsoft Windows Copyright © 1997 2003 John Robbins — All rights reserved.
——————————————————————————————————————————————————————————————————————*/ #include "stdafx.h"
/*//////////////////////////////////////////////////////////////////////
584 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
Определения и константы с областью видимости файла
//////////////////////////////////////////////////////////////////////*/
// Максимальное число слотов уведомлений static const int TOTAL_NOTIFY_SLOTS = 5 ;
// Имя мьютекса
static const LPCTSTR k_MUTEX_NAME = _T ( "TNotifyHlp_Mutex" ) ;
// Наибольшее время ожидания мьютекса static const int k_WAITLIMIT = 5000 ;
// Здесь я определяю свою директиву TRACE, потому что не хочу
// размещать BugslayerUtil.DLL в каждом адресном пространстве. #ifdef _DEBUG
#define TRACE ::OutputDebugString #else
#define TRACE __noop #endif
/*//////////////////////////////////////////////////////////////////////
// Объявления typedef с областью видимости файла
//////////////////////////////////////////////////////////////////////*/
// Структура данных интересующего нас окна.
typedef struct tag_TNOTIFYITEM
{
// PID процесса, создавшего структуру
DWORD
dwOwnerPID
;
// Тип уведомления
int
iNotifyType ;
// Параметр сравнения заголовка
int
iSearchType ;
// Описатель создаваемого окна
HWND
hWndCreate
;
//
Флаг, указывающий на уничтожение окна
BOOL
bDestroy
;
//
Строка заголовка
TCHAR
szTitle [ MAX_PATH ] ;
} TNOTIFYITEM , * PTNOTIFYITEM ;
/*//////////////////////////////////////////////////////////////////////
// Глобальные переменные с областью видимости файла
//////////////////////////////////////////////////////////////////////*/
// Эти данные **НЕ** являются общими для процессов,
// поэтому каждый процесс получает собственную их копию.
// HINSTANCE этого модуля. Установка глобальных системных
// ловушек должна выполняться при помощи DLL.
static HINSTANCE g_hInst = NULL ;
// Мьютекс, защищающий таблицу g_NotifyData static HANDLE g_hMutex = NULL ;
// Описатель ловушки. Я не включил его в раздел общих
ГЛАВА 16 Автоматизированное тестирование
585
// данных, потому что при выполнении нескольких сценариев
// процессы могут устанавливать собственные ловушки. static HHOOK g_hHook = NULL ;
// Число элементов, добавленных в таблицу этим процессом. Это
// число нужно для того, чтобы я знал, как обрабатывать ловушку. static int g_iThisProcessItems = 0 ;
/*//////////////////////////////////////////////////////////////////////
// Прототипы функций с областью видимости файла
//////////////////////////////////////////////////////////////////////*/
// Наша функция ловушка
LRESULT CALLBACK CallWndRetProcHook ( int
nCode
,
WPARAM
wParam ,
LPARAM
lParam
) ;
// Внутренняя функция проверки
static LONG_PTR __stdcall CheckNotifyItem ( HANDLE hItem , BOOL bCreate ) ;
/*//////////////////////////////////////////////////////////////////////
// Данные, общие для всех экземпляров ловушек
//////////////////////////////////////////////////////////////////////*/
#pragma data_seg ( ".HOOKDATA" )
// Таблица уведомлений
static TNOTIFYITEM g_shared_NotifyData [ TOTAL_NOTIFY_SLOTS ] =
{
{ 0 , 0 , 0 , NULL , 0 , '\0' } ,
{ 0 , 0 , 0 , NULL , 0 , '\0' } ,
{ 0 , 0 , 0 , NULL , 0 , '\0' } ,
{ 0 , 0 , 0 , NULL , 0 , '\0' } ,
{ 0 , 0 , 0 , NULL , 0 , '\0' }
} ;
// Счетчик использованных слотов уведомлений static int g_shared_iUsedSlots = 0 ;
#pragma data_seg ( )
/*////////////////////////////////////////////////////////////////////// // ЗДЕСЬ НАЧИНАЕТСЯ ВНЕШНЯЯ РЕАЛИЗАЦИЯ
//////////////////////////////////////////////////////////////////////*/
extern "C" BOOL WINAPI DllMain ( HINSTANCE
hInst
,
DWORD
dwReason
,
LPVOID
/*lpReserved*/ )
{
#ifdef _DEBUG BOOL bCHRet ;
#endif
BOOL bRet = TRUE ; switch ( dwReason )
586 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
{
case DLL_PROCESS_ATTACH :
// Присвоение значения глобальному описателю модуля. g_hInst = hInst ;
// Мне не нужны уведомления, связанные с потоками. DisableThreadLibraryCalls ( g_hInst ) ;
// Создание мьютекса для данного процесса. Мьютекс
// создается, но его получение пока не выполняется.
g_hMutex = CreateMutex ( NULL , FALSE , k_MUTEX_NAME ) ; if ( NULL == g_hMutex )
{
TRACE ( _T ( "Unable to create the mutex!\n" ) ) ;
// Если я не могу создать мьютекс, продолжение
// невозможно, и загрузка DLL завершилась неудачей. bRet = FALSE ;
}
break ;
case DLL_PROCESS_DETACH :
// Имеет ли этот процесс какие нибудь элементы
// в массиве уведомлений? Если да, я их удаляю,
// чтобы не оставлять осиротевшие элементы.
if ( 0 != g_iThisProcessItems )
{
DWORD dwProcID = GetCurrentProcessId ( ) ;
// В этом случае мне не нужно получать мьютекс,
// потому что только один поток может вызывать
// DLL по причине DLL_PROCESS_DETACH.
// Нахождение элементов, относящихся к этому процессу. for ( INT_PTR i = 0 ; i < TOTAL_NOTIFY_SLOTS ; i++ )
{
if ( g_shared_NotifyData[i].dwOwnerPID == dwProcID )
{
#ifdef _DEBUG
TCHAR szBuff[ 50 ] ; wsprintf ( szBuff ,
_T("DLL_PROCESS_DETACH removing : #%d\n"), i ) ;
TRACE ( szBuff ) ;
#endif
// И их удаление. RemoveNotifyTitle ( (HANDLE)i ) ;
}
}
}
// Закрытие описателя мьютекса. #ifdef _DEBUG
bCHRet =
ГЛАВА 16 Автоматизированное тестирование
587
#endif
CloseHandle ( g_hMutex ) ; #ifdef _DEBUG
if ( FALSE == bCHRet )
{
TRACE ( _T ( "!!!!!!!!!!!!!!!!!!!!!!!!\n" ) ) ;
TRACE ( _T ( "CloseHandle(g_hMutex) " ) \
_T ( "failed!!!!!!!!!!!!!!!!!!\n" ) ) ; TRACE ( _T ( "!!!!!!!!!!!!!!!!!!!!!!!!\n" ) ) ;
}
#endif
break ;
default
:
break ;
}
return ( bRet ) ;
}
HANDLE TNOTIFYHLP_DLLINTERFACE __stdcall
AddNotifyTitle ( int
iNotifyType ,
int
iSearchType ,
LPCTSTR szString
)
{
// Проверка корректности типа уведомления.
if ( ( iNotifyType < ANTN_DESTROYWINDOW
) ||
( iNotifyType > ANTN_CREATEANDDESTROY
)
)
{
TRACE (
_T( "AddNotify Title : iNotifyType is out of range!\n" ) ) ;
return ( INVALID_HANDLE_VALUE ) ;
}
// Проверка
корректности метода сравнения заголовка.
if ( ( iSearchType < ANTS_EXACTMATCH ) ||
( iSearchType > ANTS_ANYLOCMATCH )
)
{
TRACE (
_T(
"AddNotify Title : iSearchType is out of range!\n" ) ) ;
return ( INVALID_HANDLE_VALUE ) ;
}
// Проверка
корректности заголовка.
if ( TRUE == IsBadStringPtr ( szString , MAX_PATH ) )
{
TRACE (
_T( "AddNotify Title : szString is invalid!\n" ) ) ;
return ( INVALID_HANDLE_VALUE ) ;
}
// Ожидание
получения мьютекса.
DWORD dwRet
= WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ;
if ( WAIT_TIMEOUT == dwRet )
588 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
{
TRACE (_T( "AddNotifyTitle : Wait on mutex timed out!!\n")); return ( INVALID_HANDLE_VALUE ) ;
}
// Если все слоты использованы, выполняется выход. if ( TOTAL_NOTIFY_SLOTS == g_shared_iUsedSlots )
{
ReleaseMutex ( g_hMutex ) ; return ( INVALID_HANDLE_VALUE ) ;
}
// Нахождение первого свободного слота.
for ( INT_PTR i = 0 ; i < TOTAL_NOTIFY_SLOTS ; i++ )
{
if ( _T ( '\0' ) == g_shared_NotifyData[ i ].szTitle[ 0 ] )
{
break ;
}
}
// Добавление данных.
g_shared_NotifyData[ i ].dwOwnerPID = GetCurrentProcessId ( ) ; g_shared_NotifyData[ i ].iNotifyType = iNotifyType ; g_shared_NotifyData[ i ].iSearchType = iSearchType ;
lstrcpy ( g_shared_NotifyData[ i ].szTitle , szString ) ;
// Увеличение счетчика использованных слотов. g_shared_iUsedSlots++ ;
// Увеличение счетчика элементов этого процесса. g_iThisProcessItems++ ;
TRACE ( _T( "AddNotifyTitle Added a new item!\n" ) ) ;
ReleaseMutex ( g_hMutex ) ;
// Если это первый запрос об уведомлениях, устанавливается ловушка. if ( NULL == g_hHook )
{
g_hHook = SetWindowsHookEx ( WH_CALLWNDPROCRET
,
CallWndRetProcHook
,
g_hInst
,
0
) ;
#ifdef _DEBUG
if ( NULL == g_hHook )
{
TCHAR szBuff[ 50 ] ;
wsprintf ( szBuff ,
_T ( "SetWindowsHookEx failed!!!! (0x%08X)\n" ),
ГЛАВА 16 Автоматизированное тестирование
589
GetLastError ( ) ) ;
TRACE ( szBuff ) ;
}
#endif
}
return ( (HANDLE)i ) ;
}
void TNOTIFYHLP_DLLINTERFACE __stdcall RemoveNotifyTitle ( HANDLE hItem )
{
// Проверка описателя. INT_PTR i = (INT_PTR)hItem ;
if ( ( i < 0 ) || ( i > TOTAL_NOTIFY_SLOTS ) )
{
TRACE ( _T ( "RemoveNotifyTitle : Invalid handle!\n" ) ) ; return ;
}
// Получение мьютекса.
DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ; if ( WAIT_TIMEOUT == dwRet )
{
TRACE ( _T ( "RemoveNotifyTitle : Wait on mutex timed out!\n")); return ;
}
if ( 0 == g_shared_iUsedSlots )
{
TRACE ( _T ( "RemoveNotifyTitle : Attempting to remove when " )\ _T ( "no notification handles are set!\n" ) ) ;
ReleaseMutex ( g_hMutex ) ;
return ;
}
// Перед удалением чего либо нужно убедиться в том, что индекс
// указывает на элемент NotifyData, имеющий корректное значение.
// Если бы я этого не делал, неоднократный вызов данной функции
// с одним и тем же параметром приводил бы к сбою счетчика
// использованных слотов.
if ( 0 == g_shared_NotifyData[ i ].dwOwnerPID )
{
TRACE ( _T (
"RemoveNotifyTitle : ") \
_T (
"Attempting to
double remove!\n" ) ) ;
ReleaseMutex
( g_hMutex ) ;
return ;
}
590
ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
// Удаление элемента из массива.
g_shared_NotifyData[ i ].dwOwnerPID
= 0
;
g_shared_NotifyData[ i ].iNotifyType
= 0
;
g_shared_NotifyData[ i ].hWndCreate
= NULL ;
g_shared_NotifyData[ i ].bDestroy
= FALSE ;
g_shared_NotifyData[ i ].iSearchType
= 0
;
g_shared_NotifyData[ i ].szTitle[ 0 ]
= _T ( '\0' ) ;
// Уменьшение счетчика использованных слотов. g_shared_iUsedSlots— ;
// Уменьшение счетчика элементов данного процесса. g_iThisProcessItems— ;
TRACE ( _T ( "RemoveNotifyTitle Removed an item!\n" ) ) ;
ReleaseMutex ( g_hMutex ) ;
// Если это последний элемент данного
// процесса, ловушка процесса удаляется.
if ( ( 0 == g_iThisProcessItems ) && ( NULL != g_hHook ) )
{
if ( FALSE == UnhookWindowsHookEx ( g_hHook ) )
{
TRACE ( _T ( "UnhookWindowsHookEx failed!\n" ) ) ;
}
g_hHook = NULL ;
}
}
HWND TNOTIFYHLP_DLLINTERFACE __stdcall
CheckNotifyCreateTitle ( HANDLE hItem )
{
return ( (HWND)CheckNotifyItem ( hItem , TRUE ) ) ;
}
BOOL TNOTIFYHLP_DLLINTERFACE __stdcall
CheckNotifyDestroyTitle ( HANDLE hItem )
{
return ( (BOOL)CheckNotifyItem ( hItem , FALSE ) ) ;
}
/*////////////////////////////////////////////////////////////////////// // ЗДЕСЬ НАЧИНАЕТСЯ ВНУТРЕННЯЯ РЕАЛИЗАЦИЯ
//////////////////////////////////////////////////////////////////////*/
static LONG_PTR __stdcall CheckNotifyItem ( HANDLE hItem , BOOL bCreate )
{
// Проверка описателя.
ГЛАВА 16 Автоматизированное тестирование
591
INT_PTR i = (INT_PTR)hItem ;
if ( ( i < 0 ) ||
( i > TOTAL_NOTIFY_SLOTS ) )
{
TRACE ( _T ( "CheckNotifyItem : Invalid handle!\n" ) ) ;
return ( NULL
) ;
}
LONG_PTR lRet = 0
;
// Получение мьютекса.
DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ; if ( WAIT_TIMEOUT == dwRet )
{
TRACE ( _T ( "CheckNotifyItem : Wait on mutex timed out!\n" ) ); return ( NULL ) ;
}
// Если все слоты пусты, делать нечего. if ( 0 == g_shared_iUsedSlots )
{
ReleaseMutex ( g_hMutex ) ; return ( NULL ) ;
}
// Проверка запрошенного элемента.
if ( TRUE == bCreate )
{
// Если HWND не равен NULL, выполняется возврат
// его значения и его обнуление в таблице.
if ( NULL != g_shared_NotifyData[ i ].hWndCreate )
{
lRet = (LONG_PTR)g_shared_NotifyData[ i ].hWndCreate ; g_shared_NotifyData[ i ].hWndCreate = NULL ;
}
}
else
{
if ( FALSE != g_shared_NotifyData[ i ].bDestroy )
{
lRet = TRUE ;
g_shared_NotifyData[ i ].bDestroy = FALSE ;
}
}
ReleaseMutex ( g_hMutex ) ;
return ( lRet ) ;
}
static void __stdcall CheckTableMatch ( int
iNotifyType ,
592
ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
HWND
hWnd
,
LPCTSTR szTitle
)
{
// Получение мьютекса.
DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ;
if ( WAIT_TIMEOUT == dwRet )
{
TRACE ( _T ( "CheckTableMatch : Wait on mutex timed out!\n" ) ); return ;
}
// Таблица не должна быть пустой, но никогда
// нельзя быть ни в чем уверенным. if ( 0 == g_shared_iUsedSlots )
{
ReleaseMutex ( g_hMutex ) ;
TRACE ( _T ( "CheckTableMatch called on an empty table!\n" ) ) ; return ;
}
// Просмотр элементов таблицы.
for ( int i = 0 ; i < TOTAL_NOTIFY_SLOTS ; i++ )
{
// Не пуст ли этот
элемент и совпадают ли типы уведомлений?
if ( ( _T ( '\0' )
!= g_shared_NotifyData[ i ].szTitle[ 0
]
) &&
( g_shared_NotifyData[ i ].iNotifyType & iNotifyType
) )
{
BOOL bMatch = FALSE ;
// Сопоставление заголовка окна
// с аналогичным полем элемента таблицы.
switch ( g_shared_NotifyData[ i ].iSearchType )
{
case ANTS_EXACTMATCH
:
// Это просто.
if ( 0 == lstrcmp ( g_shared_NotifyData[i].szTitle ,
szTitle
) )
{
bMatch = TRUE ;
}
break ;
case ANTS_BEGINMATCH
:
if ( 0 ==
_tcsnccmp ( g_shared_NotifyData[i].szTitle , szTitle , _tcslen(g_shared_NotifyData[i].szTitle)))
{
bMatch = TRUE ;
}
break ;