Implementing Tester


Now that you have an idea of how to use both sides of Tester to record and play back your automation scripts, I want to go over some of the high points of the implementation. If you add up the source and build sizes of the Tester source code and binaries, which include both TESTER.DLL and TESTREC.EXE, you'll see that Tester is the biggest utility in this book. Not only is it the biggest, but it's easily the most complicated because of COM, parsing recursion, and background timers.

The TESTER.DLL Notification and Playback Implementation

In the first version of this book, I implemented TESTER.DLL in Visual Basic 6, because that was the hot COM programming language and environment de jour. However, requiring you to keep Visual Basic 6 installed just to compile a single COM DLL didn't seem like a great idea. My first inclination was to move the TESTER.DLL code over to .NET. Since some of the core code, specifically the portion that played back keystrokes, was in C++, I thought it'd be easier to re-implement the Visual Basic 6 portion of Tester in C++ and take advantage of the new attributed COM programming.

In all, attributed COM is quite nice, but it did take me a while to find the idl_quote attribute to get my forward interface declarations to work. One very pleasant surprise with the attributed COM was how clean everything felt when combining the IDL/ODL and the C++ code. Additionally, the hugely improved wizards made it a snap to add interfaces and methods and properties to those interfaces. I certainly remember my fair share of times when the wizards broke in prior releases of Visual Studio.

Back when I first started thinking about doing an automated playback utility, I thought I could use the original SendKeys statement from Visual Basic 6. After a bit of testing, I found that that implementation didn't suffice, because it did not correctly send keystrokes to programs such as Microsoft Outlook. That meant I needed to implement my own version that would properly send the keystrokes and allow mouse input in the future. Fortunately, I ran across the SendInput function, which is part of Microsoft Active Accessibility (MSAA) and replaces all the previous low-level event functions, such as keybd_event. It also places all the input information in the keyboard or mouse input stream as a contiguous unit, ensuring that your input isn't interspersed with any extraneous user input. This functionality was especially attractive for Tester.

Once I knew how to send the keystrokes properly, I needed to develop the keystroke input format. Because the Visual Basic 6 SendKeys statement or .NET System.Windows.Forms.SendKeys class already provides a nice input format, I thought I'd duplicate it for my PlayInput function. I used everything but the repeat key code, though as I mentioned earlier, I extended the format to support mouse playback as well. There's nothing too thrilling about the parsing code—if you want to see it, look in the Tester\Tester\ParsePlayInputString.CPP file that accompanies the sample files for this book. Additionally, if you want to see the code in action, you might want to debug through the ParsePlayKeysTest program in the Tester\Tester\Tests\ParsePlayKeysTest directory. As you can tell by the name, this program is one of the unit tests for the Tester DLL.

The TWindow, TWindows, and TSystem objects are straightforward, and you should be able to understand them just by reading their source code. These three classes are essentially wrappers around the appropriate Windows API functions. The only slightly interesting portion of the implementation was writing the code to ensure the TWindow.SetFocusTWindow and TSystem.SetSpecificFocus methods could bring a window to the foreground. Those functions entailed attaching to the input thread by using the AttachThreadInput API before being able to set the focus.

I ran into some interesting obstacles in the TNotify class. When I first started thinking about what it would take to determine whether a window with a specific caption was created or destroyed, I didn't expect that creating such a class would be too hard. I discovered that not only was the job moderately difficult, but the window creation notifications can't be made foolproof without heroic effort.

My first idea was to implement a systemwide computer-based training (CBT) hook. The SDK documentation seemed to say that a CBT hook was the best method for determining when windows are created and destroyed. I whipped up a quick sample but soon hit a snag. When my hook got the HCBT_CREATEWND notification, I couldn't retrieve the window caption consistently. After I thought about the problem a bit, it started to make sense; the CBT hook is probably called as part of the WM_CREATE processing, and very few windows have set their captions at that point. The only windows I could get reliably with the HCBT_CREATEWND notification were dialog boxes. The window destruction surveillance always worked with the CBT hook.

After looking through all the other types of hooks, I extended my quick sample to try them all. As I suspected, just watching WM_CREATE wasn't going to tell me the caption reliably. A friend suggested that I watch only the WM_SETTEXT messages. Eventually, to set the caption in a title bar, almost every window will use a WM_SETTEXT message. Of course, if you're doing your own non-client painting and bit blitting, you won't use the WM_SETTEXT message. One interesting behavior I did notice was that some programs, Microsoft Internet Explorer in particular, post WM_SETTEXT messages with the same text many times consecutively.

Having figured out that I needed to watch WM_SETTEXT messages, I took a harder look at the different hooks I could use. In the end, the call window procedure hook (WH_CALLWNDPROCRET) was the best choice. It allows me to watch WM_CREATE and WM_SETTEXT messages easily. I can also watch WM_DESTROY messages. At first, I expected to have some trouble with WM_DESTROY because I thought that the window caption might have been deallocated by the time this message showed up. Fortunately, the window caption is valid until the WM_NCDESTROY message is received.

After considering the pros and cons of handling WM_SETTEXT messages only for windows that didn't yet have a caption, I decided to just go ahead and process all WM_SETTEXT messages. The alternative would've involved writing a state machine to keep track of created windows and the times they get their captions set, and this solution sounded error prone and difficult to implement. The drawback to handling all WM_SETTEXT messages is that you can receive multiple creation notifications for the same window. For example, if you set a TNotify handler for windows that contained "Notepad" anywhere in their captions, you'd get a notification when NOTEPAD.EXE launched, but you'd also get a notification every time NOTEPAD.EXE opened a new file. In the end, I felt it was better to accept a less-than-optimal implementation rather than spend days and days debugging the "correct" solution. Also, writing the hook was only about a quarter of the implementation of the final TNotify class; the other three-quarters addressed the problem of how to let the user know that the window was created or destroyed.

Earlier, I mentioned that using the TNotify object isn't completely hands-off and that you have to call the CheckNotification method every once in a while. The reason you have to call CheckNotification periodically is that Tester supports only the apartment threading model that can't be multithreaded, and I needed a way to check whether a window was created or destroyed and still use the same thread in which the rest of Tester was running.

After sketching out some ideas about the notification mechanisms, I narrowed down the implementation needs to the following basic requirements:

  • The WH_CALLWNDPROCRET hook has to be systemwide, so it must be implemented in its own DLL.

  • The Tester DLL obviously can't be that DLL because I don't want to drag the entire Tester DLL and, in turn, all the COM code into each address space on the user's computer. This condition means that the hook DLL probably has to set a flag or something that the Tester DLL can read to know that a condition is met.

  • Tester can't be multithreaded, so I need to do all the processing in the same thread.

The first ramification of the basic requirements is that the hook function had to be written in C. Because the hook function is loaded into all address spaces, the hook DLL couldn't call any functions in the TESTER.DLL that were written in apartment-threaded COM. Consequently, my code would need to check the results of the hook-generated data periodically.

If you've ever developed 16-bit Windows applications, you know that getting some background processing done in a single-threaded, non-preemptive environment seems like the perfect job for the SetTimer API function. With SetTimer, you can get the background-processing capabilities yet still keep your application single-threaded. Consequently, I set up a timer notification as part of the TNotify object to determine when the windows I needed to monitor were created or destroyed.

What made the TNotify background processing interesting was that the timer procedure solution seemed like the answer, but in reality, it usually works only in the TNotify case. Depending on the length of the script and on whether your language of choice implements a message loop, the WM_TIMER message might not get through, so you'll need to call the CheckNotification method, which checks the hook data as well.

All these implementation details might seem confusing, but you'll be surprised at how little code it really takes to implement Tester. Listing 16-3 shows the hook function code from TNOTIFYHLP.CPP. On the Tester side, TNOTIFY.CPP is the module in which the timer procedure resides along with the COM code necessary for the object. The TNotify class has a couple of C++ methods that the TNotify object can access to get the events fired and to determine what types of notifications the user wants. The interesting part of the hook code is the globally shared data segment, .HOOKDATA, which holds the array of notification data. When looking at the code, keep in mind that the notification data is global but all the rest of the data is on a per-process basis.

Listing 16-3: TNOTIFYHLP.CPP

start example
 /*---------------------------------------------------------------------- Debugging Applications for Microsoft .NET and Microsoft Windows Copyright   1997-2003 John Robbins -- All rights reserved. ----------------------------------------------------------------------*/ #include "stdafx.h"     /*//////////////////////////////////////////////////////////////////////                     File Scope Defines and Constants //////////////////////////////////////////////////////////////////////*/ // The maximum number of notification slots static const int TOTAL_NOTIFY_SLOTS = 5 ; // The mutex name static const LPCTSTR k_MUTEX_NAME  = _T ( "TNotifyHlp_Mutex" ) ; // The longest amount of time I'll wait on the mutex static const int k_WAITLIMIT = 5000 ;     // I have my own trace here because I don't want to drag // BugslayerUtil.DLL into each address space. #ifdef _DEBUG #define TRACE   ::OutputDebugString #else #define TRACE   __noop #endif     /*////////////////////////////////////////////////////////////////////// // File Scope Typedefs //////////////////////////////////////////////////////////////////////*/ // The structure for an individual window to look for typedef struct tag_TNOTIFYITEM {     // The PID for the process that created this item     DWORD   dwOwnerPID  ;     // The notification type     int     iNotifyType ;     // The search parameter     int     iSearchType ;     // The handle to the HWND being created     HWND    hWndCreate  ;     // The destroy Boolean     BOOL    bDestroy    ;     // The title string     TCHAR   szTitle [ MAX_PATH ] ; } TNOTIFYITEM , * PTNOTIFYITEM ;     /*////////////////////////////////////////////////////////////////////// // File Scope Global Variables //////////////////////////////////////////////////////////////////////*/ // This data is **NOT** shared across processes, so each process gets // its own copy. // The HINSTANCE for this module. Setting global system hooks requires // a DLL. static HINSTANCE g_hInst = NULL ;     // The mutex that protects the g_NotifyData table static HANDLE g_hMutex = NULL ;     // The hook handle. I don't keep this handle in the shared section because // multiple instances could set the hook when running multiple scripts. static HHOOK g_hHook = NULL ;     // The number of items added by this process. This number lets me know // how to handle the hook. static int  g_iThisProcessItems = 0 ;     /*////////////////////////////////////////////////////////////////////// // File Scope Function Prototypes //////////////////////////////////////////////////////////////////////*/ // Our happy hook LRESULT CALLBACK CallWndRetProcHook ( int    nCode  ,                                       WPARAM wParam ,                                       LPARAM lParam  ) ;     // The internal check function static LONG_PTR __stdcall CheckNotifyItem ( HANDLE hItem , BOOL bCreate ) ;     /*////////////////////////////////////////////////////////////////////// // Funky Shared Data Across All Hook Instances //////////////////////////////////////////////////////////////////////*/ #pragma data_seg ( ".HOOKDATA" ) // The notification items table 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' }     } ; // The master count static int g_shared_iUsedSlots = 0 ; #pragma data_seg ( )     /*////////////////////////////////////////////////////////////////////// // EXTERNAL IMPLEMENTATION STARTS HERE //////////////////////////////////////////////////////////////////////*/     extern "C" BOOL WINAPI DllMain ( HINSTANCE hInst       ,                                  DWORD     dwReason    ,                                  LPVOID    /*lpReserved*/ ) { #ifdef _DEBUG     BOOL bCHRet ; #endif         BOOL bRet = TRUE ;     switch ( dwReason )     {         case DLL_PROCESS_ATTACH :             // Set the global module instance.             g_hInst = hInst ;             // I don't need the thread notifications.             DisableThreadLibraryCalls ( g_hInst ) ;             // Create the mutex for this process. The mutex is created             // here but isn't owned yet.             g_hMutex = CreateMutex ( NULL , FALSE , k_MUTEX_NAME ) ;             if ( NULL == g_hMutex )             {                 TRACE ( _T ( "Unable to create the mutex!\n" ) ) ;                 // If I can't create the mutex, I can't continue, so                 // fail the DLL load.                 bRet = FALSE ;             }             break ;         case DLL_PROCESS_DETACH :                 // Check to see whether this process has any items in the             // notification array. If it does, remove them to avoid             // leaving orphaned items.             if ( 0 != g_iThisProcessItems )             {                 DWORD dwProcID = GetCurrentProcessId ( ) ;                 // I don't need to grab the mutex here because only a                 // single thread will ever call with the                 // DLL_PROCESS_DETACH reason.                     // Loop through and take a gander.                 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                         // Get rid of it.                         RemoveNotifyTitle ( (HANDLE)i ) ;                     }                 }             }                 // Close the mutex handle. #ifdef _DEBUG             bCHRet = #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     ) {     // Ensure that the notify type range is correct.     if ( ( iNotifyType < ANTN_DESTROYWINDOW     ) ||          ( iNotifyType > ANTN_CREATEANDDESTROY  )   )     {         TRACE (              _T( "AddNotify Title : iNotifyType is out of range!\n" ) ) ;         return ( INVALID_HANDLE_VALUE ) ;     }     // Ensure that the search type range is correct.     if ( ( iSearchType < ANTS_EXACTMATCH  ) ||          ( iSearchType > ANTS_ANYLOCMATCH )   )     {         TRACE (              _T( "AddNotify Title : iSearchType is out of range!\n" ) ) ;         return ( INVALID_HANDLE_VALUE ) ;     }     // Ensure that the string is valid.     if ( TRUE == IsBadStringPtr ( szString , MAX_PATH ) )     {         TRACE ( _T( "AddNotify Title : szString is invalid!\n" ) ) ;         return ( INVALID_HANDLE_VALUE ) ;     }         // Wait to acquire the mutex.     DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ;     if ( WAIT_TIMEOUT == dwRet )     {         TRACE (_T( "AddNotifyTitle : Wait on mutex timed out!!\n"));         return ( INVALID_HANDLE_VALUE ) ;     }         // If the slots are used up, abort now.     if ( TOTAL_NOTIFY_SLOTS == g_shared_iUsedSlots )     {         ReleaseMutex ( g_hMutex ) ;         return ( INVALID_HANDLE_VALUE ) ;     }         // Find the first free slot.     for ( INT_PTR i = 0 ; i < TOTAL_NOTIFY_SLOTS ; i++ )     {         if ( _T ( '\0' ) == g_shared_NotifyData[ i ].szTitle[ 0 ] )         {             break ;         }     }         // Add this data.     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 ) ;         // Bump up the master count.     g_shared_iUsedSlots++ ;         // Bump up the count for this process.     g_iThisProcessItems++ ;         TRACE ( _T( "AddNotifyTitle - Added a new item!\n" ) ) ;         ReleaseMutex ( g_hMutex ) ;         // If this is the first notification request, enable the hook.     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" ),                        GetLastError ( ) ) ;             TRACE ( szBuff ) ;         } #endif     }         return ( (HANDLE)i ) ;     }     void TNOTIFYHLP_DLLINTERFACE __stdcall     RemoveNotifyTitle ( HANDLE hItem ) {     // Check the value.     INT_PTR i = (INT_PTR)hItem ;     if ( ( i < 0 ) || ( i > TOTAL_NOTIFY_SLOTS ) )     {         TRACE ( _T ( "RemoveNotifyTitle : Invalid handle!\n" ) ) ;         return ;     }         // Get the mutex.     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 ;     }         // Before removing anything, make sure this index points to a     // NotifyData entry that contains a valid value. If I     // didn't check, you could call this function with the same value     // over and over, which would mess up the used-slots counts.     if ( 0 == g_shared_NotifyData[ i ].dwOwnerPID )     {         TRACE ( _T ( "RemoveNotifyTitle : ") \                 _T ( "Attempting to double remove!\n" ) ) ;         ReleaseMutex ( g_hMutex ) ;         return ;     }         // Remove this item from the array.     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' ) ;         // Bump down the master item count.     g_shared_iUsedSlots-- ;         // Bump down this process's item count.     g_iThisProcessItems-- ;         TRACE ( _T ( "RemoveNotifyTitle - Removed an item!\n" ) ) ;         ReleaseMutex ( g_hMutex ) ;         // If this is the last item for this process, unhook this process's     // hook.     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 ) ) ; }     /*////////////////////////////////////////////////////////////////////// // INTERNAL IMPLEMENTATION STARTS HERE //////////////////////////////////////////////////////////////////////*/     static LONG_PTR __stdcall CheckNotifyItem ( HANDLE hItem , BOOL bCreate ) {     // Check the value.     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 ;         // Get the mutex.     DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ;     if ( WAIT_TIMEOUT == dwRet )     {         TRACE ( _T ( "CheckNotifyItem : Wait on mutex timed out!\n" ) );         return ( NULL ) ;     }         // If all slots are empty, there's nothing to do.     if ( 0 == g_shared_iUsedSlots )     {         ReleaseMutex ( g_hMutex ) ;         return ( NULL ) ;     }         // Check the item requested.     if ( TRUE == bCreate )     {         // If the HWND value isn't NULL, return that value and NULL it         // out in the table.         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 ,                                         HWND    hWnd        ,                                         LPCTSTR szTitle      ) {     // Grab the mutex.     DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ;     if ( WAIT_TIMEOUT == dwRet )     {         TRACE ( _T ( "CheckTableMatch : Wait on mutex timed out!\n" ) );         return ;     }         // The table shouldn't be empty, but never assume anything.     if ( 0 == g_shared_iUsedSlots )     {         ReleaseMutex ( g_hMutex ) ;         TRACE ( _T ( "CheckTableMatch called on an empty table!\n" ) ) ;         return ;     }         // Search through the table.     for ( int i = 0 ; i < TOTAL_NOTIFY_SLOTS ; i++ )     {         // Does this entry have something in it, and does the type of         // notification match?         if ( ( _T ( '\0' ) != g_shared_NotifyData[ i ].szTitle[ 0 ] ) &&              ( g_shared_NotifyData[ i ].iNotifyType & iNotifyType   )  )         {             BOOL bMatch = FALSE ;             // Perform the match.             switch ( g_shared_NotifyData[ i ].iSearchType )             {                 case ANTS_EXACTMATCH    :                     // This is simple.                     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 ;                 case ANTS_ANYLOCMATCH   :                     if ( NULL != _tcsstr ( szTitle                    ,                                        g_shared_NotifyData[i].szTitle ))                     {                         bMatch = TRUE ;                     }                     break ;                 default                 :                     TRACE ( _T ( "CheckTableMatch invalid " ) \                             _T ( "search type!!!\n" ) ) ;                     ReleaseMutex ( g_hMutex ) ;                     return ;                     break ;             }             // Tell them, Johnny. Do we have a match?             if ( TRUE == bMatch )             {                 // If this is a destroy notification, stick "1" in the                 // table.                 if ( ANTN_DESTROYWINDOW == iNotifyType )                 {                     g_shared_NotifyData[ i ].bDestroy = TRUE ;                 }                 else                 {                     // Otherwise, stick the HWND in the table.                     g_shared_NotifyData[ i ].hWndCreate = hWnd ;                 }             }         }     }     ReleaseMutex ( g_hMutex ) ; }     LRESULT CALLBACK CallWndRetProcHook ( int    nCode  ,                                       WPARAM wParam ,                                       LPARAM lParam  ) {     // Buffer for storing the window title     TCHAR szBuff[ MAX_PATH ] ;         // Always pass the message to the next hook before I do any     // processing. This way I don't forget and I can do my processing     // in peace.     LRESULT lRet = CallNextHookEx ( g_hHook , nCode , wParam , lParam );         // The docs say never to mess around with a negative code, so I     // don't.     if ( nCode < 0 )     {         return ( lRet ) ;     }         // Get the message structure. Why are there three (or more)     // different message structures?  What's wrong with consistently     // using the stock ol' MSG for all message/proc hooks?     PCWPRETSTRUCT pMsg = (PCWPRETSTRUCT)lParam ;         // No caption, no work to do     LONG lStyle = GetWindowLong ( pMsg->hwnd , GWL_STYLE ) ;     if ( WS_CAPTION != ( lStyle & WS_CAPTION ) )     {         return ( lRet ) ;     }         // The WM_DESTROY messages are copacetic for both dialog boxes and     // normal windows. Just get the caption and check for a match.     if ( WM_DESTROY == pMsg->message )     {         if ( 0 != GetWindowText ( pMsg->hwnd , szBuff , MAX_PATH ) )         {             CheckTableMatch ( ANTN_DESTROYWINDOW , pMsg->hwnd , szBuff ) ;         }         return ( lRet ) ;     }         // Window creation isn't as clean as window destruction.         // Get the window class. If it is a true dialog box, the     // WM_INITDIALOG is all I need.     if ( 0 == GetClassName ( pMsg->hwnd , szBuff , MAX_PATH ) )     { #ifdef _DEBUG         TCHAR szBuff[ 50 ] ;         wsprintf ( szBuff                                           ,                    _T ( "GetClassName failed for HWND : 0x%08X\n" ) ,                    pMsg->hwnd                                        ) ;         TRACE ( szBuff ) ; #endif         // There's not much point in going on.         return ( lRet ) ;     }     if ( 0 == lstrcmpi ( szBuff , _T ( "#32770" ) ) )     {         // The only message I need to check is WM_INITDIALOG.         if ( WM_INITDIALOG == pMsg->message )         {             // Get the caption of the dialog box.             if ( 0 != GetWindowText ( pMsg->hwnd , szBuff , MAX_PATH ) )             {                 CheckTableMatch ( ANTN_CREATEWINDOW ,                                   pMsg->hwnd        ,                                   szBuff             ) ;             }         }         return ( lRet ) ;     }     // That took care of true dialog boxes. Start figuring out what to do     // for actual windows.         if ( WM_CREATE == pMsg->message )     {         // Very few windows set the title in WM_CREATE.         // However, a few do and they don't use WM_SETTEXT, so I have         // to check.         if ( 0 != GetWindowText ( pMsg->hwnd , szBuff , MAX_PATH ) )         {             CheckTableMatch ( ANTN_CREATEWINDOW ,                               pMsg->hwnd        ,                               szBuff             ) ;         }     }     else if ( WM_SETTEXT == pMsg->message )     {         // I always default to WM_SETTEXT because that's how captions         // get set. Unfortunately, some applications, such as Internet         // Explorer, seem to call WM_SETTEXT a bunch of times with the         // same title. To keep this hook simple, I just report         // the WM_SETTEXT instead of maintaining all sorts of weird,         // hard-to-debug data structures that keep track of the windows         // that called WM_SETTEXT previously.         if ( NULL != pMsg->lParam )         {             CheckTableMatch ( ANTN_CREATEWINDOW     ,                               pMsg->hwnd            ,                               (LPCTSTR)pMsg->lParam  ) ;         }     }         return ( lRet ) ; }
end example

Although the TNotify implementation was a brainteaser in some ways, I was pleased at how few troubles I experienced implementing it. If you do want to extend the hook code, be aware that debugging systemwide hooks isn't a simple endeavor. Your best bet is to use remote debugging as I described in Chapter 5. The other way you can debug systemwide hooks is to resort to printf-style debugging. Using DebugView from www.sysinternals.com, you can watch all the OutputDebugString calls to see the state of your hook.

Implementing TESTREC.EXE

With the Tester DLL out of the way, it was time to provide the keystroke and mouse recording capabilities in TESTREC.EXE. When it comes to recording input, there's only one clean way to do it on Windows operating systems: use a journal record hook. There's nothing very exciting about journal hooks except handling the WM_CANCELJOURNAL message properly. When the user presses Ctrl+Alt+Delete, the operating system terminates any active journal record hooks. This makes sense because it would be a pretty serious security breach to allow an application to record the keystrokes that make up the user's password. To handle WM_CANCELJOURNAL in a manner that keeps the implementation details hidden, I used a message filter to monitor for it coming through. You can see all the hook details in HOOKCODE.CPP in the Tester\TestRec directory.

Processing Keystrokes

The keystroke recording code mainly involves keeping straight what's going on with the Shift, Ctrl, and Alt keys. Before I discuss some of the particulars of wrestling with the individual keystrokes, you'll probably want to look at Figures 16-2 through 16-4, which is a simplified graph of all the keystroke states that the recording code handles.

click to expand
Figure 16-2: Keystroke recording start, tab key, and check break state machines

click to expand
Figure 16-3: Keystroke recording normal key state machine

click to expand
Figure 16-4: Keystroke recording Alt+Tab state machine

The first challenge to keystroke recording is getting the keystrokes in a human-readable form from the journal hook. If you've never had the joy of messing with virtual codes and scan codes, you don't know what you're in for! Additionally, I found that the keys for some characters received by the journal record hook were quite a bit different from what I expected.

The last time I messed around with keyboard processing at this level was way back in the old MS-DOS days. (I think my age is showing!) Consequently, I brought some misconceptions to the problem. For example, the first time I typed an exclamation point, I expected to see that exact character come through the journal record hook. What I got instead was a Shift character followed by a 1. That's exactly what the keystroke sequence is on the U.S. English keyboard. The problem is that I wanted any key sequences that I output to be mostly understandable. The SendKeys sequence "+1" is technically correct, but you have to go through some mental gymnastics to realize that you're really looking at the "!" character.

To make TESTREC.EXE as useful as possible, I needed to do some special processing to make the output strings readable. Basically, you check the keyboard state to see whether the shift key is down, and if it is, convert the key to its readable form. Fortunately, the GetKeyboardState and ToUnicode API functions take care of the problem of getting the real key. The best way for you to get the gist of this keystroke processing is to look at CRecordingEngine::NormalKeyState in Listing 16-4.

Listing 16-4: CRecordingEngine::NormalKeyState

start example
 void CRecordingEngine :: NormalKeyState ( PEVENTMSG pMsg ) {     // The state to shift to after processing the key passed in.     eKeyStates eShiftToState = eNormalKey ;         UINT vkCode = LOBYTE ( pMsg->paramL ) ;     #ifdef _DEBUG     {         STATETRACE (_T("RECSTATE: Normal : ")) ;         if ( ( WM_KEYDOWN == pMsg->message    ) ||              ( WM_SYSKEYDOWN == pMsg->message )   )         {             STATETRACE ( _T( "KEYDOWN " ) ) ;         }         else         {             STATETRACE ( _T ( "KEYUP " ) ) ;         }         TCHAR szName [ 100 ] ;         GetKeyNameText ( pMsg->paramH << 16 , szName , 100 ) ;         CharUpper ( szName ) ;         STATETRACE ( _T ( "%c %d %04X (%s)\n" ) ,                           vkCode                ,                           vkCode                ,                           vkCode                ,                           szName                 ) ;         } #endif         // Check that this is not key that will cause a transition out.     switch ( vkCode )     {         case VK_CONTROL :             // An CTRL down can come through after a ALT key is already             // down.             if ( ( WM_KEYDOWN    == pMsg->message ) ||                  ( WM_SYSKEYDOWN == pMsg->message )   )             {                 eShiftToState = eCheckBreak ;                 STATETRACE ( _T ( "RECSTATE: Looking for BREAK key\n"));             }             else             {                 m_cKeyBuff += _T( "{CTRL UP}" ) ;                 m_iKeyBuffKeys++ ;             }             m_iKeyBuffKeys++ ;             break ;         case VK_MENU    :             if ( ( WM_KEYDOWN    == pMsg->message ) ||                  ( WM_SYSKEYDOWN == pMsg->message )   )             {                 eShiftToState = eIsTabKey ;                 STATETRACE (_T("RECSTATE: Looking for TAB key\n")) ;             }             else             {                 m_cKeyBuff += _T( "{ALT UP}" ) ;                 m_iKeyBuffKeys++ ;             }             m_iKeyBuffKeys++ ;             break ;             case VK_SHIFT   :             if ( ( WM_KEYDOWN    == pMsg->message ) ||                  ( WM_SYSKEYDOWN == pMsg->message )   )             {                 // Make sure I only do this once!                 if ( FALSE == m_bShiftDown )                 {                     // The shift key is down so set my flags.                     m_bShiftDown = TRUE ;                     m_bShiftDownInString = FALSE ;                 }             }             else             {                 // If I put a {SHIFT DOWN} earlier, I need to match up                 // with a {SHIFT UP}                 if ( TRUE == m_bShiftDownInString )                 {                     m_cKeyBuff += _T ( "{SHIFT UP}" ) ;                     m_iKeyBuffKeys++ ;                         m_bShiftDownInString = FALSE ;                 }                 // The shift key has popped up.                 m_bShiftDown = FALSE ;             }             break ;         default :             // It's a normal key.                 // If it's not a key down, I don't care.             if ( ( WM_KEYDOWN    == pMsg->message ) ||                  ( WM_SYSKEYDOWN == pMsg->message )   )             {                 //TRACE ( "vkCode = %04X\n" , vkCode ) ;                     // Is there a key string for the virtual code?                 if ( NULL != g_KeyStrings[ vkCode ].szString  )                 {                     // Is the shift key down?  If so, I need to ensure                     // that I add the {SHIFT DOWN} before adding this                     // key.                     if ( ( TRUE  == m_bShiftDown         ) &&                          ( FALSE == m_bShiftDownInString )   )                     {                         m_cKeyBuff += _T ( "{SHIFT DOWN}" ) ;                         m_iKeyBuffKeys++ ;                         m_bShiftDownInString = TRUE ;                     }                     // Add the key to the key list.                     m_cKeyBuff += g_KeyStrings[ vkCode ].szString ;                 }                 else                 {                     // I need to translate the key into it's character                     // equivalent. Getting the keyboard state and using                     // ToAscii will properly convert things like                     // "{SHIFT DOWN}1{SHIFT UP}" into "!"                         // First step is to get the current keyboard state.                     BYTE byState [ 256 ] ;                         GetKeyboardState ( byState ) ;                         // Now do the conversion                     TCHAR cConv[3] = { _T ( '\0' ) } ;                     TCHAR cChar ; #ifdef _UNICODE                     int iRet = ToUnicode ( vkCode               ,                                            pMsg->paramH         ,                                            byState              ,                                            (LPWORD)&cConv       ,                                            sizeof ( cConv ) /                                             sizeof ( TCHAR )    ,                                            0                     ) ;     #else                     int iRet = ToAscii ( vkCode         ,                                          pMsg->paramH   ,                                          byState        ,                                          (LPWORD)&cConv ,                                          0               ) ; #endif                     if ( 2 == iRet )                     {                         // This is an international keystroke.                         ASSERT ( !"I gotta handle this!" ) ;                     }                         // If it didn't convert, don't use cChar!                     if ( 0 == iRet )                     {                         cChar = (TCHAR)vkCode ;                     }                     else                     {                         // Before I can use the character, I need to                         // check if the CTRL key is down. If so,                         // ToAscii/ToUnicode return the ASCII control                         // code value. Since I want the key, I'll need                         // to do some work to get it.                         SHORT sCtrlDown =                                        GetAsyncKeyState ( VK_CONTROL ) ;                         if ( 0xFFFF8000 == ( 0xFFFF8000 & sCtrlDown ))                         {                             // Control is down so I need to look if                             // the CAPSLOCK and SHIFT keys are down.                             BOOL bCapsLock =                                    ( 0xFFFF8000 == ( 0xFFFF8000 &                                        GetAsyncKeyState ( VK_CAPITAL)));                             if ( TRUE == bCapsLock )                             {                                 // CAPSLOCK is down so if SHIFT is down,                                 // use the lowercase version of the key.                                 if ( TRUE == m_bShiftDown )                                 { // 'variable' : conversion from 'type' to 'type' of greater size #pragma warning ( disable : 4312 )                                     cChar = (TCHAR)                                         CharLower ( (LPTSTR)vkCode ); #pragma warning ( default : 4312 )                                 }                                 else                                 {                                     // Us the upper case version.                                     cChar = (TCHAR)vkCode ;                                 }                             }                             else                             {                                 // CAPSLOCK is not down so just check                                 // the shift state.                                 if ( TRUE == m_bShiftDown )                                 {                                     cChar = (TCHAR)vkCode ;                                 }                                 else                                 { // 'variable' : conversion from 'type' to 'type' of greater size #pragma warning ( disable : 4312 )                                     cChar = (TCHAR)                                         CharLower ( (LPTSTR)vkCode ); #pragma warning ( default : 4312 )                                 }                             }                         }                         else                         {                             // The CTRL key is not down so I can use the                             // converted key directly.                             cChar = cConv[ 0 ] ;                         }                     }                         switch ( cChar )                     {                         // Brackets, braces, and tildes have to be                         // specially handled. All other keys are                         // just slapped on the output list.                         case _T ( '[' ) :                             m_cKeyBuff += _T ( "{[}" ) ;                             break ;                         case _T ( ']' ) :                             m_cKeyBuff += _T ( "{]}" ) ;                             break ;                         case _T ( '~' ) :                             m_cKeyBuff += _T ( "{~}" ) ;                             break ;                         case _T ( '{' ) :                             m_cKeyBuff += _T ( "{{}" ) ;                             break ;                         case _T ( '}' ) :                             m_cKeyBuff += _T ( "{}}" ) ;                             break ;                         default :                             m_cKeyBuff += cChar ;                     }                 }                 // Bump up the number of keys processed.                 m_iKeyBuffKeys++ ;                     if ( ( m_iKeyBuffKeys > 20         ) ||                      (  m_cKeyBuff.Length ( ) > 50 )   )                 {                     DoKeyStrokes ( TRUE ) ;                 }             }                 break ;     }         // Set the state to move to after this key is processed.     eCurrKeyState = eShiftToState ; } 
end example

The only special processing I do when recording keystrokes is handle Alt+Tab operations. Although I could've recorded the actual Alt and Tab keys, doing that might have prevented the script from running the next time because I'd have to keep the application in the exact same location in the top window z-order with the same number of applications running. Consequently, instead of recording the keystrokes, I shift to a state in which I wait for you to release the Alt key and, on the next journal input of any kind, I determine which application has the focus and generate the appropriate code for the script.

Processing Mouse Input

When I first started looking at adding the mouse support, my first big surprise was realizing that the keystroke recording I had originally implemented wasn't going to handle adding the mouse input to it. My original keystroke recording code had gone to quite a bit of trouble to generate sequences that optimized the Ctrl, Shift, and Alt processing to ensure a single algorithm generated a complete statement for PlayInput that put the Ctrl, Shift, or Alt key down and popped it up and the end of a single statement. When I got to thinking about adding the mouse input, I realized that keeping that option might generate a PlayInput statement of tens of thousands of characters! To even begin handling the mouse input, I had to change the keystroke recording code to generate special codes such as {ALT DOWN} and {ALT UP} so that the generated statements would be chewable by whatever scripting language you used.

After taking care of the Ctrl, Alt, and Shift processing, I next had to figure out how I was going to handle single mouse clicks, double-clicks, and drags. What makes the processing fun is that the journal hook I'm using to record keys and mouse moves reports only WM_xBUTTONDOWN and WM_xBUTTONUP messages. I would've much preferred to get WM_xBUTTONDBLCLK messages to make my life easier. This processing was screaming to be a state machine like the one I developed for the keystroke recording. Figures 16-5 and 16-6 show the mouse state machine that I implemented in RECORDINGENGINE.H/.CPP. Keep in mind that I also had to do this state tracking for each button on the system. The mentions of Slot 0 and Slot 1 were to keep track of the previous event for comparison purposes.

click to expand
Figure 16-5: Mouse recording normal state machine

click to expand
Figure 16-6: Mouse recording double click state machine

After grinding through the code to implement the recording, everything looked good until I started hard testing. Immediately, I saw problems. Recording scripts as I was drawing items in Microsoft Paint worked just fine. Playing them back was a problem. For example, I'd draw a circle freehand but the playback would look like a straight line through the origin of the circle, then the rest of the circle would be drawn. I went back and thoroughly examined my recording and playback code, but found nothing wrong. As it turns out, the script was pumping a bunch of MOVETO instructions very quickly, and the Windows operating system input queue dumps extra input when it's getting full. What I needed to do was slow down the mouse message processing so that all the mouse events would have enough time to execute. I was using the SendInput function to do the actual playback, so my first idea was to set the time on each event in the INPUT structure to allow extra time for the mouse events. That didn't work, and I found that setting the timing long enough would cause the computer to kick into power-save mode, which certainly freaked me out the first time it happened.

I looked at another approach. I thought that since my code parses the input commands into an array of INPUT structures to pass to SendInput, I might be able to spin through the array one at a time and do extra pauses on the mouse events. The question of how long to wait became an experiment. After playing around, I found it best to sleep for 25 milliseconds before and after each mouse event. This means that recorded scripts will play back much slower with mouse input than when you recorded them.




Debugging Applications for Microsoft. NET and Microsoft Windows
Debugging Applications for MicrosoftВ® .NET and Microsoft WindowsВ® (Pro-Developer)
ISBN: 0735615365
EAN: 2147483647
Year: 2003
Pages: 177
Authors: John Robbins

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net