Home | History | Annotate | Download | only in base
      1 // Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "build/build_config.h"
      6 
      7 #include <windows.h>
      8 #include <mmsystem.h>
      9 
     10 #include "base/event_recorder.h"
     11 #include "base/file_util.h"
     12 #include "base/logging.h"
     13 
     14 // A note about time.
     15 // For perfect playback of events, you'd like a very accurate timer
     16 // so that events are played back at exactly the same time that
     17 // they were recorded.  However, windows has a clock which is only
     18 // granular to ~15ms.  We see more consistent event playback when
     19 // using a higher resolution timer.  To do this, we use the
     20 // timeGetTime API instead of the default GetTickCount() API.
     21 
     22 namespace base {
     23 
     24 EventRecorder* EventRecorder::current_ = NULL;
     25 
     26 LRESULT CALLBACK StaticRecordWndProc(int nCode, WPARAM wParam,
     27                                      LPARAM lParam) {
     28   CHECK(EventRecorder::current());
     29   return EventRecorder::current()->RecordWndProc(nCode, wParam, lParam);
     30 }
     31 
     32 LRESULT CALLBACK StaticPlaybackWndProc(int nCode, WPARAM wParam,
     33                                        LPARAM lParam) {
     34   CHECK(EventRecorder::current());
     35   return EventRecorder::current()->PlaybackWndProc(nCode, wParam, lParam);
     36 }
     37 
     38 EventRecorder::~EventRecorder() {
     39   // Try to assert early if the caller deletes the recorder
     40   // while it is still in use.
     41   DCHECK(!journal_hook_);
     42   DCHECK(!is_recording_ && !is_playing_);
     43 }
     44 
     45 bool EventRecorder::StartRecording(const FilePath& filename) {
     46   if (journal_hook_ != NULL)
     47     return false;
     48   if (is_recording_ || is_playing_)
     49     return false;
     50 
     51   // Open the recording file.
     52   DCHECK(file_ == NULL);
     53   file_ = file_util::OpenFile(filename, "wb+");
     54   if (!file_) {
     55     DLOG(ERROR) << "EventRecorder could not open log file";
     56     return false;
     57   }
     58 
     59   // Set the faster clock, if possible.
     60   ::timeBeginPeriod(1);
     61 
     62   // Set the recording hook.  JOURNALRECORD can only be used as a global hook.
     63   journal_hook_ = ::SetWindowsHookEx(WH_JOURNALRECORD, StaticRecordWndProc,
     64                                      GetModuleHandle(NULL), 0);
     65   if (!journal_hook_) {
     66     DLOG(ERROR) << "EventRecorder Record Hook failed";
     67     file_util::CloseFile(file_);
     68     return false;
     69   }
     70 
     71   is_recording_ = true;
     72   return true;
     73 }
     74 
     75 void EventRecorder::StopRecording() {
     76   if (is_recording_) {
     77     DCHECK(journal_hook_ != NULL);
     78 
     79     if (!::UnhookWindowsHookEx(journal_hook_)) {
     80       DLOG(ERROR) << "EventRecorder Unhook failed";
     81       // Nothing else we can really do here.
     82       return;
     83     }
     84 
     85     ::timeEndPeriod(1);
     86 
     87     DCHECK(file_ != NULL);
     88     file_util::CloseFile(file_);
     89     file_ = NULL;
     90 
     91     journal_hook_ = NULL;
     92     is_recording_ = false;
     93   }
     94 }
     95 
     96 bool EventRecorder::StartPlayback(const FilePath& filename) {
     97   if (journal_hook_ != NULL)
     98     return false;
     99   if (is_recording_ || is_playing_)
    100     return false;
    101 
    102   // Open the recording file.
    103   DCHECK(file_ == NULL);
    104   file_ = file_util::OpenFile(filename, "rb");
    105   if (!file_) {
    106     DLOG(ERROR) << "EventRecorder Playback could not open log file";
    107     return false;
    108   }
    109   // Read the first event from the record.
    110   if (fread(&playback_msg_, sizeof(EVENTMSG), 1, file_) != 1) {
    111     DLOG(ERROR) << "EventRecorder Playback has no records!";
    112     file_util::CloseFile(file_);
    113     return false;
    114   }
    115 
    116   // Set the faster clock, if possible.
    117   ::timeBeginPeriod(1);
    118 
    119   // Playback time is tricky.  When playing back, we read a series of events,
    120   // each with timeouts.  Simply subtracting the delta between two timers will
    121   // lead to fast playback (about 2x speed).  The API has two events, one
    122   // which advances to the next event (HC_SKIP), and another that requests the
    123   // event (HC_GETNEXT).  The same event will be requested multiple times.
    124   // Each time the event is requested, we must calculate the new delay.
    125   // To do this, we track the start time of the playback, and constantly
    126   // re-compute the delay.   I mention this only because I saw two examples
    127   // of how to use this code on the net, and both were broken :-)
    128   playback_start_time_ = timeGetTime();
    129   playback_first_msg_time_ = playback_msg_.time;
    130 
    131   // Set the hook.  JOURNALPLAYBACK can only be used as a global hook.
    132   journal_hook_ = ::SetWindowsHookEx(WH_JOURNALPLAYBACK, StaticPlaybackWndProc,
    133                                      GetModuleHandle(NULL), 0);
    134   if (!journal_hook_) {
    135     DLOG(ERROR) << "EventRecorder Playback Hook failed";
    136     return false;
    137   }
    138 
    139   is_playing_ = true;
    140 
    141   return true;
    142 }
    143 
    144 void EventRecorder::StopPlayback() {
    145   if (is_playing_) {
    146     DCHECK(journal_hook_ != NULL);
    147 
    148     if (!::UnhookWindowsHookEx(journal_hook_)) {
    149       DLOG(ERROR) << "EventRecorder Unhook failed";
    150       // Nothing else we can really do here.
    151     }
    152 
    153     DCHECK(file_ != NULL);
    154     file_util::CloseFile(file_);
    155     file_ = NULL;
    156 
    157     ::timeEndPeriod(1);
    158 
    159     journal_hook_ = NULL;
    160     is_playing_ = false;
    161   }
    162 }
    163 
    164 // Windows callback hook for the recorder.
    165 LRESULT EventRecorder::RecordWndProc(int nCode, WPARAM wParam, LPARAM lParam) {
    166   static bool recording_enabled = true;
    167   EVENTMSG* msg_ptr = NULL;
    168 
    169   // The API says we have to do this.
    170   // See http://msdn2.microsoft.com/en-us/library/ms644983(VS.85).aspx
    171   if (nCode < 0)
    172     return ::CallNextHookEx(journal_hook_, nCode, wParam, lParam);
    173 
    174   // Check for the break key being pressed and stop recording.
    175   if (::GetKeyState(VK_CANCEL) & 0x8000) {
    176     StopRecording();
    177     return ::CallNextHookEx(journal_hook_, nCode, wParam, lParam);
    178   }
    179 
    180   // The Journal Recorder must stop recording events when system modal
    181   // dialogs are present. (see msdn link above)
    182   switch (nCode) {
    183     case HC_SYSMODALON:
    184       recording_enabled = false;
    185       break;
    186     case HC_SYSMODALOFF:
    187       recording_enabled = true;
    188       break;
    189   }
    190 
    191   if (nCode == HC_ACTION && recording_enabled) {
    192     // Aha - we have an event to record.
    193     msg_ptr = reinterpret_cast<EVENTMSG*>(lParam);
    194     msg_ptr->time = timeGetTime();
    195     fwrite(msg_ptr, sizeof(EVENTMSG), 1, file_);
    196     fflush(file_);
    197   }
    198 
    199   return CallNextHookEx(journal_hook_, nCode, wParam, lParam);
    200 }
    201 
    202 // Windows callback for the playback mode.
    203 LRESULT EventRecorder::PlaybackWndProc(int nCode, WPARAM wParam,
    204                                        LPARAM lParam) {
    205   static bool playback_enabled = true;
    206   int delay = 0;
    207 
    208   switch (nCode) {
    209     // A system modal dialog box is being displayed.  Stop playing back
    210     // messages.
    211     case HC_SYSMODALON:
    212       playback_enabled = false;
    213       break;
    214 
    215     // A system modal dialog box is destroyed.  We can start playing back
    216     // messages again.
    217     case HC_SYSMODALOFF:
    218       playback_enabled = true;
    219       break;
    220 
    221     // Prepare to copy the next mouse or keyboard event to playback.
    222     case HC_SKIP:
    223       if (!playback_enabled)
    224         break;
    225 
    226       // Read the next event from the record.
    227       if (fread(&playback_msg_, sizeof(EVENTMSG), 1, file_) != 1)
    228         this->StopPlayback();
    229       break;
    230 
    231     // Copy the mouse or keyboard event to the EVENTMSG structure in lParam.
    232     case HC_GETNEXT:
    233       if (!playback_enabled)
    234         break;
    235 
    236       memcpy(reinterpret_cast<void*>(lParam), &playback_msg_,
    237              sizeof(playback_msg_));
    238 
    239       // The return value is the amount of time (in milliseconds) to wait
    240       // before playing back the next message in the playback queue.  Each
    241       // time this is called, we recalculate the delay relative to our current
    242       // wall clock.
    243       delay = (playback_msg_.time - playback_first_msg_time_) -
    244               (timeGetTime() - playback_start_time_);
    245       if (delay < 0)
    246         delay = 0;
    247       return delay;
    248 
    249     // An application has called PeekMessage with wRemoveMsg set to PM_NOREMOVE
    250     // indicating that the message is not removed from the message queue after
    251     // PeekMessage processing.
    252     case HC_NOREMOVE:
    253       break;
    254   }
    255 
    256   return CallNextHookEx(journal_hook_, nCode, wParam, lParam);
    257 }
    258 
    259 }  // namespace base
    260