• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
StaticRecordWndProc(int nCode,WPARAM wParam,LPARAM lParam)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 
StaticPlaybackWndProc(int nCode,WPARAM wParam,LPARAM lParam)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 
~EventRecorder()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 
StartRecording(const FilePath & filename)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 
StopRecording()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 
StartPlayback(const FilePath & filename)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 
StopPlayback()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.
RecordWndProc(int nCode,WPARAM wParam,LPARAM lParam)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.
PlaybackWndProc(int nCode,WPARAM wParam,LPARAM lParam)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