OLD | NEW |
| (Empty) |
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 | |
OLD | NEW |