blob: 39c4220897e2bd3033f92c02a5ef6e376a011641 [file] [log] [blame]
[email protected]2edc2862011-04-04 18:04:371// Copyright (c) 2011 The Chromium Authors. All rights reserved.
license.botbf09a502008-08-24 00:55:552// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
initial.commitd7cae122008-07-26 21:49:384
[email protected]2edc2862011-04-04 18:04:375#include <stddef.h>
[email protected]238a20d2009-01-26 16:33:296#include <windows.h>
initial.commitd7cae122008-07-26 21:49:387#include <mmsystem.h>
8
[email protected]238a20d2009-01-26 16:33:299#include "base/event_recorder.h"
[email protected]836f1342008-10-01 17:40:1310#include "base/file_util.h"
initial.commitd7cae122008-07-26 21:49:3811#include "base/logging.h"
initial.commitd7cae122008-07-26 21:49:3812
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
21namespace base {
22
23EventRecorder* EventRecorder::current_ = NULL;
24
25LRESULT CALLBACK StaticRecordWndProc(int nCode, WPARAM wParam,
26 LPARAM lParam) {
[email protected]a42d4632011-10-26 21:48:0027 DCHECK(EventRecorder::current());
initial.commitd7cae122008-07-26 21:49:3828 return EventRecorder::current()->RecordWndProc(nCode, wParam, lParam);
29}
30
31LRESULT CALLBACK StaticPlaybackWndProc(int nCode, WPARAM wParam,
32 LPARAM lParam) {
[email protected]a42d4632011-10-26 21:48:0033 DCHECK(EventRecorder::current());
initial.commitd7cae122008-07-26 21:49:3834 return EventRecorder::current()->PlaybackWndProc(nCode, wParam, lParam);
35}
36
37EventRecorder::~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
[email protected]4f093932009-05-01 04:31:2244bool EventRecorder::StartRecording(const FilePath& filename) {
initial.commitd7cae122008-07-26 21:49:3845 if (journal_hook_ != NULL)
46 return false;
47 if (is_recording_ || is_playing_)
48 return false;
49
50 // Open the recording file.
[email protected]5d2b4492011-03-01 02:48:0551 DCHECK(!file_);
[email protected]7600d0b2013-12-08 21:43:3052 file_ = OpenFile(filename, "wb+");
[email protected]836f1342008-10-01 17:40:1353 if (!file_) {
initial.commitd7cae122008-07-26 21:49:3854 DLOG(ERROR) << "EventRecorder could not open log file";
55 return false;
56 }
57
[email protected]cccd51a2008-09-09 09:05:3858 // Set the faster clock, if possible.
59 ::timeBeginPeriod(1);
60
initial.commitd7cae122008-07-26 21:49:3861 // 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";
[email protected]7600d0b2013-12-08 21:43:3066 CloseFile(file_);
initial.commitd7cae122008-07-26 21:49:3867 return false;
68 }
69
70 is_recording_ = true;
71 return true;
72}
73
74void 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
[email protected]cccd51a2008-09-09 09:05:3884 ::timeEndPeriod(1);
85
initial.commitd7cae122008-07-26 21:49:3886 DCHECK(file_ != NULL);
[email protected]7600d0b2013-12-08 21:43:3087 CloseFile(file_);
initial.commitd7cae122008-07-26 21:49:3888 file_ = NULL;
89
90 journal_hook_ = NULL;
91 is_recording_ = false;
92 }
93}
94
[email protected]4f093932009-05-01 04:31:2295bool EventRecorder::StartPlayback(const FilePath& filename) {
initial.commitd7cae122008-07-26 21:49:3896 if (journal_hook_ != NULL)
97 return false;
98 if (is_recording_ || is_playing_)
99 return false;
100
101 // Open the recording file.
[email protected]5d2b4492011-03-01 02:48:05102 DCHECK(!file_);
[email protected]7600d0b2013-12-08 21:43:30103 file_ = OpenFile(filename, "rb");
[email protected]836f1342008-10-01 17:40:13104 if (!file_) {
initial.commitd7cae122008-07-26 21:49:38105 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!";
[email protected]7600d0b2013-12-08 21:43:30111 CloseFile(file_);
initial.commitd7cae122008-07-26 21:49:38112 return false;
113 }
114
[email protected]cccd51a2008-09-09 09:05:38115 // Set the faster clock, if possible.
116 ::timeBeginPeriod(1);
117
initial.commitd7cae122008-07-26 21:49:38118 // 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
143void 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);
[email protected]7600d0b2013-12-08 21:43:30153 CloseFile(file_);
initial.commitd7cae122008-07-26 21:49:38154 file_ = NULL;
155
[email protected]cccd51a2008-09-09 09:05:38156 ::timeEndPeriod(1);
157
initial.commitd7cae122008-07-26 21:49:38158 journal_hook_ = NULL;
159 is_playing_ = false;
160 }
161}
162
163// Windows callback hook for the recorder.
164LRESULT EventRecorder::RecordWndProc(int nCode, WPARAM wParam, LPARAM lParam) {
165 static bool recording_enabled = true;
[email protected]c7f4b6272008-09-30 20:50:51166 EVENTMSG* msg_ptr = NULL;
initial.commitd7cae122008-07-26 21:49:38167
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)
[email protected]2fdc86a2010-01-26 23:08:02181 switch (nCode) {
initial.commitd7cae122008-07-26 21:49:38182 case HC_SYSMODALON:
[email protected]c7f4b6272008-09-30 20:50:51183 recording_enabled = false;
initial.commitd7cae122008-07-26 21:49:38184 break;
185 case HC_SYSMODALOFF:
[email protected]c7f4b6272008-09-30 20:50:51186 recording_enabled = true;
initial.commitd7cae122008-07-26 21:49:38187 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.
[email protected]c7f4b6272008-09-30 20:50:51202LRESULT EventRecorder::PlaybackWndProc(int nCode, WPARAM wParam,
203 LPARAM lParam) {
initial.commitd7cae122008-07-26 21:49:38204 static bool playback_enabled = true;
205 int delay = 0;
206
[email protected]2fdc86a2010-01-26 23:08:02207 switch (nCode) {
initial.commitd7cae122008-07-26 21:49:38208 // A system modal dialog box is being displayed. Stop playing back
209 // messages.
210 case HC_SYSMODALON:
[email protected]c7f4b6272008-09-30 20:50:51211 playback_enabled = false;
212 break;
initial.commitd7cae122008-07-26 21:49:38213
214 // A system modal dialog box is destroyed. We can start playing back
215 // messages again.
216 case HC_SYSMODALOFF:
[email protected]c7f4b6272008-09-30 20:50:51217 playback_enabled = true;
218 break;
initial.commitd7cae122008-07-26 21:49:38219
220 // Prepare to copy the next mouse or keyboard event to playback.
221 case HC_SKIP:
[email protected]c7f4b6272008-09-30 20:50:51222 if (!playback_enabled)
223 break;
initial.commitd7cae122008-07-26 21:49:38224
225 // Read the next event from the record.
226 if (fread(&playback_msg_, sizeof(EVENTMSG), 1, file_) != 1)
227 this->StopPlayback();
[email protected]c7f4b6272008-09-30 20:50:51228 break;
initial.commitd7cae122008-07-26 21:49:38229
230 // Copy the mouse or keyboard event to the EVENTMSG structure in lParam.
231 case HC_GETNEXT:
[email protected]c7f4b6272008-09-30 20:50:51232 if (!playback_enabled)
initial.commitd7cae122008-07-26 21:49:38233 break;
234
[email protected]c7f4b6272008-09-30 20:50:51235 memcpy(reinterpret_cast<void*>(lParam), &playback_msg_,
236 sizeof(playback_msg_));
initial.commitd7cae122008-07-26 21:49:38237
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
[email protected]22921502008-08-14 11:44:17258} // namespace base