blob: 23c092f89afb1f4472ba5853bf706d6b17ea3d11 [file] [log] [blame]
license.botf003cfe2008-08-24 09:55:55 +09001// 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.commit3f4a7322008-07-27 06:49:38 +09004
5#include "base/event_recorder.h"
6
7#include <mmsystem.h>
8
9#include "base/logging.h"
10#include "base/time.h"
11
12// A note about time.
13// For perfect playback of events, you'd like a very accurate timer
14// so that events are played back at exactly the same time that
15// they were recorded. However, windows has a clock which is only
16// granular to ~15ms. We see more consistent event playback when
17// using a higher resolution timer. To do this, we use the
18// timeGetTime API instead of the default GetTickCount() API.
19
20namespace base {
21
22EventRecorder* EventRecorder::current_ = NULL;
23
24LRESULT CALLBACK StaticRecordWndProc(int nCode, WPARAM wParam,
25 LPARAM lParam) {
26 CHECK(EventRecorder::current());
27 return EventRecorder::current()->RecordWndProc(nCode, wParam, lParam);
28}
29
30LRESULT CALLBACK StaticPlaybackWndProc(int nCode, WPARAM wParam,
31 LPARAM lParam) {
32 CHECK(EventRecorder::current());
33 return EventRecorder::current()->PlaybackWndProc(nCode, wParam, lParam);
34}
35
36EventRecorder::~EventRecorder() {
37 // Try to assert early if the caller deletes the recorder
38 // while it is still in use.
39 DCHECK(!journal_hook_);
40 DCHECK(!is_recording_ && !is_playing_);
41}
42
43bool EventRecorder::StartRecording(std::wstring& filename) {
44 if (journal_hook_ != NULL)
45 return false;
46 if (is_recording_ || is_playing_)
47 return false;
48
49 // Open the recording file.
50 DCHECK(file_ == NULL);
51 if (_wfopen_s(&file_, filename.c_str(), L"wb+") != 0) {
52 DLOG(ERROR) << "EventRecorder could not open log file";
53 return false;
54 }
55
56 // Set the faster clock, if possible.
57 ::timeBeginPeriod(1);
58
59 // Set the recording hook. JOURNALRECORD can only be used as a global hook.
60 journal_hook_ = ::SetWindowsHookEx(WH_JOURNALRECORD, StaticRecordWndProc,
61 GetModuleHandle(NULL), 0);
62 if (!journal_hook_) {
63 DLOG(ERROR) << "EventRecorder Record Hook failed";
64 fclose(file_);
65 return false;
66 }
67
68 is_recording_ = true;
69 return true;
70}
71
72void EventRecorder::StopRecording() {
73 if (is_recording_) {
74 DCHECK(journal_hook_ != NULL);
75
76 if (!::UnhookWindowsHookEx(journal_hook_)) {
77 DLOG(ERROR) << "EventRecorder Unhook failed";
78 // Nothing else we can really do here.
79 return;
80 }
81
82 ::timeEndPeriod(1);
83
84 DCHECK(file_ != NULL);
85 fclose(file_);
86 file_ = NULL;
87
88 journal_hook_ = NULL;
89 is_recording_ = false;
90 }
91}
92
93bool EventRecorder::StartPlayback(std::wstring& filename) {
94 if (journal_hook_ != NULL)
95 return false;
96 if (is_recording_ || is_playing_)
97 return false;
98
99 // Open the recording file.
100 DCHECK(file_ == NULL);
101 if (_wfopen_s(&file_, filename.c_str(), L"rb") != 0) {
102 DLOG(ERROR) << "EventRecorder Playback could not open log file";
103 return false;
104 }
105 // Read the first event from the record.
106 if (fread(&playback_msg_, sizeof(EVENTMSG), 1, file_) != 1) {
107 DLOG(ERROR) << "EventRecorder Playback has no records!";
108 fclose(file_);
109 return false;
110 }
111
112 // Set the faster clock, if possible.
113 ::timeBeginPeriod(1);
114
115 // Playback time is tricky. When playing back, we read a series of events,
116 // each with timeouts. Simply subtracting the delta between two timers will
117 // lead to fast playback (about 2x speed). The API has two events, one
118 // which advances to the next event (HC_SKIP), and another that requests the
119 // event (HC_GETNEXT). The same event will be requested multiple times.
120 // Each time the event is requested, we must calculate the new delay.
121 // To do this, we track the start time of the playback, and constantly
122 // re-compute the delay. I mention this only because I saw two examples
123 // of how to use this code on the net, and both were broken :-)
124 playback_start_time_ = timeGetTime();
125 playback_first_msg_time_ = playback_msg_.time;
126
127 // Set the hook. JOURNALPLAYBACK can only be used as a global hook.
128 journal_hook_ = ::SetWindowsHookEx(WH_JOURNALPLAYBACK, StaticPlaybackWndProc,
129 GetModuleHandle(NULL), 0);
130 if (!journal_hook_) {
131 DLOG(ERROR) << "EventRecorder Playback Hook failed";
132 return false;
133 }
134
135 is_playing_ = true;
136
137 return true;
138}
139
140void EventRecorder::StopPlayback() {
141 if (is_playing_) {
142 DCHECK(journal_hook_ != NULL);
143
144 if (!::UnhookWindowsHookEx(journal_hook_)) {
145 DLOG(ERROR) << "EventRecorder Unhook failed";
146 // Nothing else we can really do here.
147 }
148
149 DCHECK(file_ != NULL);
150 fclose(file_);
151 file_ = NULL;
152
153 ::timeEndPeriod(1);
154
155 journal_hook_ = NULL;
156 is_playing_ = false;
157 }
158}
159
160// Windows callback hook for the recorder.
161LRESULT EventRecorder::RecordWndProc(int nCode, WPARAM wParam, LPARAM lParam) {
162 static bool recording_enabled = true;
163 EVENTMSG *msg_ptr = NULL;
164
165 // The API says we have to do this.
166 // See http://msdn2.microsoft.com/en-us/library/ms644983(VS.85).aspx
167 if (nCode < 0)
168 return ::CallNextHookEx(journal_hook_, nCode, wParam, lParam);
169
170 // Check for the break key being pressed and stop recording.
171 if (::GetKeyState(VK_CANCEL) & 0x8000) {
172 StopRecording();
173 return ::CallNextHookEx(journal_hook_, nCode, wParam, lParam);
174 }
175
176 // The Journal Recorder must stop recording events when system modal
177 // dialogs are present. (see msdn link above)
178 switch(nCode)
179 {
180 case HC_SYSMODALON:
181 recording_enabled = false;
182 break;
183 case HC_SYSMODALOFF:
184 recording_enabled = true;
185 break;
186 }
187
188 if (nCode == HC_ACTION && recording_enabled) {
189 // Aha - we have an event to record.
190 msg_ptr = reinterpret_cast<EVENTMSG*>(lParam);
191 msg_ptr->time = timeGetTime();
192 fwrite(msg_ptr, sizeof(EVENTMSG), 1, file_);
193 fflush(file_);
194 }
195
196 return CallNextHookEx(journal_hook_, nCode, wParam, lParam);
197}
198
199// Windows callback for the playback mode.
200LRESULT EventRecorder::PlaybackWndProc(int nCode, WPARAM wParam, LPARAM lParam)
201{
202 static bool playback_enabled = true;
203 int delay = 0;
204
205 switch(nCode)
206 {
207 // A system modal dialog box is being displayed. Stop playing back
208 // messages.
209 case HC_SYSMODALON:
210 playback_enabled = false;
211 break;
212
213 // A system modal dialog box is destroyed. We can start playing back
214 // messages again.
215 case HC_SYSMODALOFF:
216 playback_enabled = true;
217 break;
218
219 // Prepare to copy the next mouse or keyboard event to playback.
220 case HC_SKIP:
221 if (!playback_enabled)
222 break;
223
224 // Read the next event from the record.
225 if (fread(&playback_msg_, sizeof(EVENTMSG), 1, file_) != 1)
226 this->StopPlayback();
227 break;
228
229 // Copy the mouse or keyboard event to the EVENTMSG structure in lParam.
230 case HC_GETNEXT:
231 if (!playback_enabled)
232 break;
233
234 memcpy(reinterpret_cast<void*>(lParam), &playback_msg_, sizeof(playback_msg_));
235
236 // The return value is the amount of time (in milliseconds) to wait
237 // before playing back the next message in the playback queue. Each
238 // time this is called, we recalculate the delay relative to our current
239 // wall clock.
240 delay = (playback_msg_.time - playback_first_msg_time_) -
241 (timeGetTime() - playback_start_time_);
242 if (delay < 0)
243 delay = 0;
244 return delay;
245
246 // An application has called PeekMessage with wRemoveMsg set to PM_NOREMOVE
247 // indicating that the message is not removed from the message queue after
248 // PeekMessage processing.
249 case HC_NOREMOVE:
250 break;
251 }
252
253 return CallNextHookEx(journal_hook_, nCode, wParam, lParam);
254}
255
deanm@google.com97137862008-08-14 20:44:17 +0900256} // namespace base
license.botf003cfe2008-08-24 09:55:55 +0900257