Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Windows 95 Journaling and Playback


MAR96: Windows 95 Journaling and Playback

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


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.