|
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 |