The authors are researchers in the computer science department at the University of Oregon. Mark can be reached at [email protected], and Bryce, at [email protected].
One of the basic applications that comes with Windows 3.1 is the Recorder program in the Main program group. This utility uses built-in Windows journaling and playback hooks to record mouse and keyboard inputs into a file that can be used for later playback, appearing to the system as if a user were entering the input again. If you used Recorder, you probably did so only to see what it does--not for anything meaningful. However, journaling and playback are useful for tasks such as automated testing, online software demoing, input filtering, and macro development. In fact, several successful commercial products are built on this journaling mechanism.
In this article, we'll describe an implementation of a Windows 95 journaling and playback facility that overcomes major shortcomings in the Windows 3.1 implementation. To demonstrate the power and flexibility of the new facility, we'll present a macro recorder program that supports keyboard and mouse macros that can be used anywhere and anytime. (The complete source for Recorder is provided electronically; see "Availability," page 3.) Other Windows 95 programming techniques demonstrated by the application include the use of multithreading and having virtual devices notify a Win32 program of an asynchronous event.
Application-Level Journaling
Windows 3.1 journaling (application level) hooks work at the application-message level and have been carried forward into Windows 95 generally unchanged (although Microsoft no longer provides a recorder program). While these hooks are sufficient for many applications, they have significant shortcomings. For example, they do not journal keyboard input going into DOS windows or input going into a DOS full-screen session, so DOS programs and sessions can't be recorded or played back. If you never run DOS, you won't care; but for most of us, the DOS prompt will be part of our lives for some time to come.
Another problem is that the old hooks force all input coming from the mouse and keyboard to be funneled into a journaling program for it to save, before being passed on to the target application(s). The inverse occurs on playback, with Windows asking the journaler for input to simulate. A major breakthrough with Windows 95 is its decentralized input scheme. In the decentralized approach, higher reliability and faster performance are achieved by removing the bottleneck of having input pass through a common point before being distributed. By having Windows 95 feed input directly to the input queues of applications, no one application can hang the system by locking the input queue indefinitely, as was possible in Windows 3.1. (For 16-bit programs running on Windows 95, the single input queue is still in effect.) It is therefore undesirable to use the application-level mechanism because it uses the older, less-robust, and less-efficient approach.
Finally, the traditional journaling hooks require a Windows program to actively participate in the journaling process. This also can degrade performance because the program must wake up at every mouse or keyboard input, using up cycles that other programs might need.
Windows 95 System-Level Journaling
The designers of Windows 95 recognized the drawbacks of the old journaling hooks and introduced new, more-efficient mechanisms for journaling and playback. Unfortunately, the hooks have no Windows application-level API, making them inaccessible to most developers. The new hooks have been placed at the lowest level possible--the virtual device (VxD) level--giving users the ultimate in input visibility and control.
Hooks for the mouse and keyboard are necessary since separate VxDs deal with each type of input. Both VxDs have a VxD-level API allowing other VxDs to request to see inputs as they are received by the system, as well as to generate simulated input from a device.
To demonstrate the new facilities, we've developed Recorder, a macro-recorder application. Recorder consists of a Windows 95 32-bit GUI that serves as the user interface and a VxD that serves as the recording and playback controller. First, let's see how the Recorder VxD simulates input from the mouse and keyboard.
Recorder sees system input via "service hooking," an obscure feature of the Windows VxD architecture that lets you see just about everything going on inside the guts of Windows and gives you the control to completely change its behavior. Once a service is hooked, any VxD or application calling the service gets redirected to the hooker VxD first. The hooker can choose to pass the request on to the next VxD on the chain, change the parameters to the request, or service the request itself.
To make hooking VxD services possible, each VxD has a memory location assigned for each of its services that points to the top of a hook chain. When a new hooker is registered for a service, the appropriate address is modified to point to the new hook routine. The hook routine itself must be declared as a Hook_Proc in its assembly-language declaration like this: BeginProc Hook_Routine, Hook_Proc Chain_Save. In this case, Chain_Save is a double-word variable that you assign and to which Hook_Device_Service stores the previous top of the hook chain. The Hook_Proc tag causes the code in Example 1 to appear at the start of the procedure.
When a hook is removed from the chain, the link to the next service in the chain is obtained from the Chain_Save location, making hooking and unhooking transparent to the VxD programmer. The Chain_Save variable can also be used by the hooking routine to chain to the previous hooker if it wants to pass the request on.
To view keyboard input, Recorder must hook the keyboard VxD (VKD) service VKD_Filter_Keyboard_Input. This service is called by VKD itself upon keyboard input. The service was written with the sole intention that some other VxD would hook it to record, and possibly alter, keyboard input. Listing One shows the VKD's call to the service and the service itself.
To alter input, a hooking VxD simply changes the value of the CL register to a new scancode; to kill input, the hooking routine sets the carry flag before it returns. The Recorder VxD hook procedure saves the value of the CL register and the current time into a buffer. Listing Two hooks the service, while Listing Three is a skeleton keyboard-recording routine.
A second service is used to record mouse input. Whenever there is mouse input, the VMD_Post_Pointer_Message service of the VMOUSE VxD is invoked with parameters indicating mouse location and the state of the mouse buttons. Recorder must hook this service to see this input.
Unlike the VKD filter function, where the returned carry flag determines whether the event is passed on to applications, VMD_Post_Pointer_Message actually notifies applications itself. To kill mouse input, the hook function should not chain the request (instead of setting the carry bit, as is done for the VKD filter). Listing Four is the Recorder VxD code that hooks the service, and Listing Five is a skeleton of the mouse recording-hook procedure.
The DDK documentation for VMD_Post_Pointer_Message includes another parameter, the mouse-instance structure pointer, that is supposed to be passed in the EDX register. Stepping through VMD_Post_Pointer_Message with a debugger reveals that the EDX register is not used, so the documentation is not accurate and this parameter can be ignored.
Once a Recorder-type application has saved mouse and keyboard input, it must be able to play it back. To play back or simulate keyboard input, you use the VKD service VKD_Force_Keys. This service takes an array of virtual scan codes and sends them into the system as if a user had typed them on the keyboard. Scan codes are normally generated by the keyboard for both key presses and key releases with the key-release scan code being identical to the key-press scan code except that the high bit of the scan-code byte is set. Listing Six is the Recorder code that simulates
one keystroke.
Mouse input is simulated just as easily, but VMOUSE has no separate service for input simulation. Instead, the Recorder must use the VMD_Post_Pointer_Message service to generate input because VMD provides this service for mouse minidrivers. Mouse minidrivers are for mouse devices that Windows does not know how to deal with. Rather than have mouse-driver developers implement the entire functionality of a mouse driver, Windows 95 lets them focus on the hardware-specific details of the device while using the system interface services provided by VMOUSE (VMD_Post_Pointer_Message, for instance). A program simulating mouse input acts like a mouse minidriver calling this input routine. Listing Seven is a code fragment from Recorder that replays mouse input.
One final issue to be addressed is the speed at which applications simulate input. Recorder plays input at the same speed it was recorded. This is necessary in cases where playing back the input too quickly will not produce the desired effect, such as starting an application and then pulling down a menu. If the menu doesn't exist when the pull-down action is played, the macro won't do what it was supposed to. Timing of input is obtained by using the Virtual Machine Manager (VMM) service Get_System_Time_Address, which returns a pointer to a memory location where Windows keeps track of the number of milliseconds since Windows booted. Accessing this location directly avoids the overhead of calling a service like Get_System_Time. Once timestamps are recorded, the playback mechanism ensures the same relative timing between inputs using the Set_Global_Timeout service.
The Recorder Application
The GUI Win32 portion of the Recorder application presents a dialog-box main window (see Figure 1) with buttons for recording and deleting macros, as well as a button that allows one to save recorded macros to disk. Up to four macros can be defined and assigned function keys F1-F4. To record a macro, the user clicks on the Record button in the dialog box. Recorder then pops up a dialog box indicating that macro recording will start when one of the function keys F1-F4 is pressed and will finish when the same function key is pressed a second time. So that mouse macros always begin with the mouse at the same location, the mouse cursor moves to the center of the screen whenever macro recording or playback starts.
Once a macro has been recorded, it can be assigned a name by editing the listbox associated with its assigned function key. A set of macros can be saved to disk with the Save button and will be automatically loaded the next time Recorder is run in the directory where the saved macro files have been stored. Individual macros can be deleted by selecting the desired listbox and then clicking on the Delete button.
One useful Win32 feature demonstrated by the Recorder program is multithreading. When a macro playback has started, Recorder launches a separate thread to wait for the VxD to indicate that the macro has finished playback. The new thread blocks on a Win32 event waiting for the VxD to toggle the event and let it continue. After it continues, the thread plays a beep to inform the user that the macro is done and then exits. This structure allows the Win32 program to continue to update its display while waiting for the macro replay to finish. General asynchronous communication from a VxD to a Win32 application can be constructed on this framework.
Figure 1: Dialog box main window of the Recorder.
Example 1: When the Hook_Proc tag is used, this code appears at the top of the procedure.
jmp Hook_Routine ; skip over the book-keeping info jmp Chain_Save ; bogus code - its just here to point Windows at ; the variable storing the previous service address dd 0 ; not used Hook_Routine: ; hooking routine is here
Listing One
... ; VKD calls its own service with input mov cl, scancode ; scancode is the keyboard input VxDCall VKD_Filter_Keyboard_Input ; call the service jc noinput ; if carry set, kill the input ... ; the default filter service BeginProc VKD_Filter_Keyboard_Input clc ; clear flag to let input through ret EndProc VKD_Filter_Keyboard_Input
Listing Two
; hook the keyboard input GetVxDServiceOrdinal eax, VKD_Filter_Keyboard_Input ; get the id of the filter service mov esi, offset32 Record_Keyboard ; address of our hook routine VMMCall Hook_Device_Service ; hook mov Keyboard_Proc, esi ; save previous hook
Listing Three
Public Record_Keyboard BeginProc Record_Keyboard, Hook_Proc Keyboard_Proc ; save CL and timestamp to recording buffer here ; call the previous hooker call Keyboard_Proc ; let other VxDs massage input clc ; clear carry to let the key through ret EndProc Record_Keyboard
Listing Four
; hook the mouse input routine GetVxDServiceOrdinal eax, VMD_Post_Pointer_Message ; get the id of the service mov esi, offset32 Record_Mouse ; pass our hook routine address VMMCall Hook_Device_Service ; hook it mov Mouse_Proc, esi ; save service address
Listing Five
Public Record_Mouse BeginProc Record_Mouse, Hook_Proc Mouse_Proc ; record the mouse input here. The movement parameters passed in are: ; esi - mouse delta x ; edi - mouse delta y ; al - mouse button status ; call the previous service call Mouse_Proc ; pass event through to system clc pop esi ret EndProc Record_Mouse
Listing Six
mov ecx, 1 ; number of keys lea esi, [ebx].scancode ; address of scancode array VxDCall VKD_Force_Keys
Listing Seven
mov esi, [ebx].deltax mov edi, [ebx].deltay mov al, [ebx].button VxDCall VMD_Post_Pointer_Message