blob: 92128af70358e2b6b6e4df84c3f652b82ae37e6a [file] [log] [blame]
license.botbf09a502008-08-24 00:55:551// 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.
initial.commitd7cae122008-07-26 21:49:384
[email protected]238a20d2009-01-26 16:33:295#include "build/build_config.h"
initial.commitd7cae122008-07-26 21:49:386
[email protected]238a20d2009-01-26 16:33:297#include <windows.h>
initial.commitd7cae122008-07-26 21:49:388#include <mmsystem.h>
9
[email protected]238a20d2009-01-26 16:33:2910#include "base/event_recorder.h"
[email protected]836f1342008-10-01 17:40:1311#include "base/file_util.h"
initial.commitd7cae122008-07-26 21:49:3812#include "base/logging.h"
initial.commitd7cae122008-07-26 21:49:3813
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
22namespace base {
23
24EventRecorder* EventRecorder::current_ = NULL;
25
26LRESULT CALLBACK StaticRecordWndProc(int nCode, WPARAM wParam,
27 LPARAM lParam) {
28 CHECK(EventRecorder::current());
29 return EventRecorder::current()->RecordWndProc(nCode, wParam, lParam);
30}
31
32LRESULT CALLBACK StaticPlaybackWndProc(int nCode, WPARAM wParam,
33 LPARAM lParam) {
34 CHECK(EventRecorder::current());
35 return EventRecorder::current()->PlaybackWndProc(nCode, wParam, lParam);
36}
37
38EventRecorder::~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
[email protected]4f093932009-05-01 04:31:2245bool EventRecorder::StartRecording(const FilePath& filename) {
initial.commitd7cae122008-07-26 21:49:3846 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);
[email protected]836f1342008-10-01 17:40:1353 file_ = file_util::OpenFile(filename, "wb+");
54 if (!file_) {
initial.commitd7cae122008-07-26 21:49:3855 DLOG(ERROR) << "EventRecorder could not open log file";
56 return false;
57 }
58
[email protected]cccd51a2008-09-09 09:05:3859 // Set the faster clock, if possible.
60 ::timeBeginPeriod(1);
61
initial.commitd7cae122008-07-26 21:49:3862 // 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";
[email protected]836f1342008-10-01 17:40:1367 file_util::CloseFile(file_);
initial.commitd7cae122008-07-26 21:49:3868 return false;
69 }
70
71 is_recording_ = true;
72 return true;
73}
74
75void 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
[email protected]cccd51a2008-09-09 09:05:3885 ::timeEndPeriod(1);
86
initial.commitd7cae122008-07-26 21:49:3887 DCHECK(file_ != NULL);
[email protected]836f1342008-10-01 17:40:1388 file_util::CloseFile(file_);
initial.commitd7cae122008-07-26 21:49:3889 file_ = NULL;
90
91 journal_hook_ = NULL;
92 is_recording_ = false;
93 }
94}
95
[email protected]4f093932009-05-01 04:31:2296bool EventRecorder::StartPlayback(const FilePath& filename) {
initial.commitd7cae122008-07-26 21:49:3897 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);
[email protected]836f1342008-10-01 17:40:13104 file_ = file_util::OpenFile(filename, "rb");
105 if (!file_) {
initial.commitd7cae122008-07-26 21:49:38106 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!";
[email protected]836f1342008-10-01 17:40:13112 file_util::CloseFile(file_);
initial.commitd7cae122008-07-26 21:49:38113 return false;
114 }
115
[email protected]cccd51a2008-09-09 09:05:38116 // Set the faster clock, if possible.
117 ::timeBeginPeriod(1);
118
initial.commitd7cae122008-07-26 21:49:38119 // 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
144void 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);
[email protected]836f1342008-10-01 17:40:13154 file_util::CloseFile(file_);
initial.commitd7cae122008-07-26 21:49:38155 file_ = NULL;
156
[email protected]cccd51a2008-09-09 09:05:38157 ::timeEndPeriod(1);
158
initial.commitd7cae122008-07-26 21:49:38159 journal_hook_ = NULL;
160 is_playing_ = false;
161 }
162}
163
164// Windows callback hook for the recorder.
165LRESULT EventRecorder::RecordWndProc(int nCode, WPARAM wParam, LPARAM lParam) {
166 static bool recording_enabled = true;
[email protected]c7f4b6272008-09-30 20:50:51167 EVENTMSG* msg_ptr = NULL;
initial.commitd7cae122008-07-26 21:49:38168
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)
[email protected]c7f4b6272008-09-30 20:50:51182 switch(nCode) {
initial.commitd7cae122008-07-26 21:49:38183 case HC_SYSMODALON:
[email protected]c7f4b6272008-09-30 20:50:51184 recording_enabled = false;
initial.commitd7cae122008-07-26 21:49:38185 break;
186 case HC_SYSMODALOFF:
[email protected]c7f4b6272008-09-30 20:50:51187 recording_enabled = true;
initial.commitd7cae122008-07-26 21:49:38188 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.
[email protected]c7f4b6272008-09-30 20:50:51203LRESULT EventRecorder::PlaybackWndProc(int nCode, WPARAM wParam,
204 LPARAM lParam) {
initial.commitd7cae122008-07-26 21:49:38205 static bool playback_enabled = true;
206 int delay = 0;
207
[email protected]c7f4b6272008-09-30 20:50:51208 switch(nCode) {
initial.commitd7cae122008-07-26 21:49:38209 // A system modal dialog box is being displayed. Stop playing back
210 // messages.
211 case HC_SYSMODALON:
[email protected]c7f4b6272008-09-30 20:50:51212 playback_enabled = false;
213 break;
initial.commitd7cae122008-07-26 21:49:38214
215 // A system modal dialog box is destroyed. We can start playing back
216 // messages again.
217 case HC_SYSMODALOFF:
[email protected]c7f4b6272008-09-30 20:50:51218 playback_enabled = true;
219 break;
initial.commitd7cae122008-07-26 21:49:38220
221 // Prepare to copy the next mouse or keyboard event to playback.
222 case HC_SKIP:
[email protected]c7f4b6272008-09-30 20:50:51223 if (!playback_enabled)
224 break;
initial.commitd7cae122008-07-26 21:49:38225
226 // Read the next event from the record.
227 if (fread(&playback_msg_, sizeof(EVENTMSG), 1, file_) != 1)
228 this->StopPlayback();
[email protected]c7f4b6272008-09-30 20:50:51229 break;
initial.commitd7cae122008-07-26 21:49:38230
231 // Copy the mouse or keyboard event to the EVENTMSG structure in lParam.
232 case HC_GETNEXT:
[email protected]c7f4b6272008-09-30 20:50:51233 if (!playback_enabled)
initial.commitd7cae122008-07-26 21:49:38234 break;
235
[email protected]c7f4b6272008-09-30 20:50:51236 memcpy(reinterpret_cast<void*>(lParam), &playback_msg_,
237 sizeof(playback_msg_));
initial.commitd7cae122008-07-26 21:49:38238
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
[email protected]22921502008-08-14 11:44:17259} // namespace base