blob: e61f6bc0eaf8b0691f71dc34f90e4f430fe996ed [file] [log] [blame]
initial.commitd7cae122008-07-26 21:49:381// Copyright 2008, Google Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8// * Redistributions of source code must retain the above copyright
9// notice, this list of conditions and the following disclaimer.
10// * Redistributions in binary form must reproduce the above
11// copyright notice, this list of conditions and the following disclaimer
12// in the documentation and/or other materials provided with the
13// distribution.
14// * Neither the name of Google Inc. nor the names of its
15// contributors may be used to endorse or promote products derived from
16// this software without specific prior written permission.
17//
18// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30#include "base/event_recorder.h"
31
32#include <mmsystem.h>
33
34#include "base/logging.h"
35#include "base/time.h"
36
37// A note about time.
38// For perfect playback of events, you'd like a very accurate timer
39// so that events are played back at exactly the same time that
40// they were recorded. However, windows has a clock which is only
41// granular to ~15ms. We see more consistent event playback when
42// using a higher resolution timer. To do this, we use the
43// timeGetTime API instead of the default GetTickCount() API.
44
45namespace base {
46
47EventRecorder* EventRecorder::current_ = NULL;
48
49LRESULT CALLBACK StaticRecordWndProc(int nCode, WPARAM wParam,
50 LPARAM lParam) {
51 CHECK(EventRecorder::current());
52 return EventRecorder::current()->RecordWndProc(nCode, wParam, lParam);
53}
54
55LRESULT CALLBACK StaticPlaybackWndProc(int nCode, WPARAM wParam,
56 LPARAM lParam) {
57 CHECK(EventRecorder::current());
58 return EventRecorder::current()->PlaybackWndProc(nCode, wParam, lParam);
59}
60
61EventRecorder::~EventRecorder() {
62 // Try to assert early if the caller deletes the recorder
63 // while it is still in use.
64 DCHECK(!journal_hook_);
65 DCHECK(!is_recording_ && !is_playing_);
66}
67
68bool EventRecorder::StartRecording(std::wstring& filename) {
69 if (journal_hook_ != NULL)
70 return false;
71 if (is_recording_ || is_playing_)
72 return false;
73
74 // Open the recording file.
75 DCHECK(file_ == NULL);
76 if (_wfopen_s(&file_, filename.c_str(), L"wb+") != 0) {
77 DLOG(ERROR) << "EventRecorder could not open log file";
78 return false;
79 }
80
81 // Set the faster clock, if possible.
82 ::timeBeginPeriod(1);
83
84 // Set the recording hook. JOURNALRECORD can only be used as a global hook.
85 journal_hook_ = ::SetWindowsHookEx(WH_JOURNALRECORD, StaticRecordWndProc,
86 GetModuleHandle(NULL), 0);
87 if (!journal_hook_) {
88 DLOG(ERROR) << "EventRecorder Record Hook failed";
89 fclose(file_);
90 return false;
91 }
92
93 is_recording_ = true;
94 return true;
95}
96
97void EventRecorder::StopRecording() {
98 if (is_recording_) {
99 DCHECK(journal_hook_ != NULL);
100
101 if (!::UnhookWindowsHookEx(journal_hook_)) {
102 DLOG(ERROR) << "EventRecorder Unhook failed";
103 // Nothing else we can really do here.
104 return;
105 }
106
107 ::timeEndPeriod(1);
108
109 DCHECK(file_ != NULL);
110 fclose(file_);
111 file_ = NULL;
112
113 journal_hook_ = NULL;
114 is_recording_ = false;
115 }
116}
117
118bool EventRecorder::StartPlayback(std::wstring& filename) {
119 if (journal_hook_ != NULL)
120 return false;
121 if (is_recording_ || is_playing_)
122 return false;
123
124 // Open the recording file.
125 DCHECK(file_ == NULL);
126 if (_wfopen_s(&file_, filename.c_str(), L"rb") != 0) {
127 DLOG(ERROR) << "EventRecorder Playback could not open log file";
128 return false;
129 }
130 // Read the first event from the record.
131 if (fread(&playback_msg_, sizeof(EVENTMSG), 1, file_) != 1) {
132 DLOG(ERROR) << "EventRecorder Playback has no records!";
133 fclose(file_);
134 return false;
135 }
136
137 // Set the faster clock, if possible.
138 ::timeBeginPeriod(1);
139
140 // Playback time is tricky. When playing back, we read a series of events,
141 // each with timeouts. Simply subtracting the delta between two timers will
142 // lead to fast playback (about 2x speed). The API has two events, one
143 // which advances to the next event (HC_SKIP), and another that requests the
144 // event (HC_GETNEXT). The same event will be requested multiple times.
145 // Each time the event is requested, we must calculate the new delay.
146 // To do this, we track the start time of the playback, and constantly
147 // re-compute the delay. I mention this only because I saw two examples
148 // of how to use this code on the net, and both were broken :-)
149 playback_start_time_ = timeGetTime();
150 playback_first_msg_time_ = playback_msg_.time;
151
152 // Set the hook. JOURNALPLAYBACK can only be used as a global hook.
153 journal_hook_ = ::SetWindowsHookEx(WH_JOURNALPLAYBACK, StaticPlaybackWndProc,
154 GetModuleHandle(NULL), 0);
155 if (!journal_hook_) {
156 DLOG(ERROR) << "EventRecorder Playback Hook failed";
157 return false;
158 }
159
160 is_playing_ = true;
161
162 return true;
163}
164
165void EventRecorder::StopPlayback() {
166 if (is_playing_) {
167 DCHECK(journal_hook_ != NULL);
168
169 if (!::UnhookWindowsHookEx(journal_hook_)) {
170 DLOG(ERROR) << "EventRecorder Unhook failed";
171 // Nothing else we can really do here.
172 }
173
174 DCHECK(file_ != NULL);
175 fclose(file_);
176 file_ = NULL;
177
178 ::timeEndPeriod(1);
179
180 journal_hook_ = NULL;
181 is_playing_ = false;
182 }
183}
184
185// Windows callback hook for the recorder.
186LRESULT EventRecorder::RecordWndProc(int nCode, WPARAM wParam, LPARAM lParam) {
187 static bool recording_enabled = true;
188 EVENTMSG *msg_ptr = NULL;
189
190 // The API says we have to do this.
191 // See http://msdn2.microsoft.com/en-us/library/ms644983(VS.85).aspx
192 if (nCode < 0)
193 return ::CallNextHookEx(journal_hook_, nCode, wParam, lParam);
194
195 // Check for the break key being pressed and stop recording.
196 if (::GetKeyState(VK_CANCEL) & 0x8000) {
197 StopRecording();
198 return ::CallNextHookEx(journal_hook_, nCode, wParam, lParam);
199 }
200
201 // The Journal Recorder must stop recording events when system modal
202 // dialogs are present. (see msdn link above)
203 switch(nCode)
204 {
205 case HC_SYSMODALON:
206 recording_enabled = false;
207 break;
208 case HC_SYSMODALOFF:
209 recording_enabled = true;
210 break;
211 }
212
213 if (nCode == HC_ACTION && recording_enabled) {
214 // Aha - we have an event to record.
215 msg_ptr = reinterpret_cast<EVENTMSG*>(lParam);
216 msg_ptr->time = timeGetTime();
217 fwrite(msg_ptr, sizeof(EVENTMSG), 1, file_);
218 fflush(file_);
219 }
220
221 return CallNextHookEx(journal_hook_, nCode, wParam, lParam);
222}
223
224// Windows callback for the playback mode.
225LRESULT EventRecorder::PlaybackWndProc(int nCode, WPARAM wParam, LPARAM lParam)
226{
227 static bool playback_enabled = true;
228 int delay = 0;
229
230 switch(nCode)
231 {
232 // A system modal dialog box is being displayed. Stop playing back
233 // messages.
234 case HC_SYSMODALON:
235 playback_enabled = false;
236 break;
237
238 // A system modal dialog box is destroyed. We can start playing back
239 // messages again.
240 case HC_SYSMODALOFF:
241 playback_enabled = true;
242 break;
243
244 // Prepare to copy the next mouse or keyboard event to playback.
245 case HC_SKIP:
246 if (!playback_enabled)
247 break;
248
249 // Read the next event from the record.
250 if (fread(&playback_msg_, sizeof(EVENTMSG), 1, file_) != 1)
251 this->StopPlayback();
252 break;
253
254 // Copy the mouse or keyboard event to the EVENTMSG structure in lParam.
255 case HC_GETNEXT:
256 if (!playback_enabled)
257 break;
258
259 memcpy(reinterpret_cast<void*>(lParam), &playback_msg_, sizeof(playback_msg_));
260
261 // The return value is the amount of time (in milliseconds) to wait
262 // before playing back the next message in the playback queue. Each
263 // time this is called, we recalculate the delay relative to our current
264 // wall clock.
265 delay = (playback_msg_.time - playback_first_msg_time_) -
266 (timeGetTime() - playback_start_time_);
267 if (delay < 0)
268 delay = 0;
269 return delay;
270
271 // An application has called PeekMessage with wRemoveMsg set to PM_NOREMOVE
272 // indicating that the message is not removed from the message queue after
273 // PeekMessage processing.
274 case HC_NOREMOVE:
275 break;
276 }
277
278 return CallNextHookEx(journal_hook_, nCode, wParam, lParam);
279}
280
[email protected]22921502008-08-14 11:44:17281} // namespace base