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