event_recorder.cc revision c7f5f8508d98d5952d42ed7648c2a8f30a4da156
1// 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.
4
5#include "build/build_config.h"
6
7#include <windows.h>
8#include <mmsystem.h>
9
10#include "base/event_recorder.h"
11#include "base/file_util.h"
12#include "base/logging.h"
13
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
45bool EventRecorder::StartRecording(const FilePath& filename) {
46  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);
53  file_ = file_util::OpenFile(filename, "wb+");
54  if (!file_) {
55    DLOG(ERROR) << "EventRecorder could not open log file";
56    return false;
57  }
58
59  // Set the faster clock, if possible.
60  ::timeBeginPeriod(1);
61
62  // 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";
67    file_util::CloseFile(file_);
68    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
85    ::timeEndPeriod(1);
86
87    DCHECK(file_ != NULL);
88    file_util::CloseFile(file_);
89    file_ = NULL;
90
91    journal_hook_ = NULL;
92    is_recording_ = false;
93  }
94}
95
96bool EventRecorder::StartPlayback(const FilePath& filename) {
97  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);
104  file_ = file_util::OpenFile(filename, "rb");
105  if (!file_) {
106    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!";
112    file_util::CloseFile(file_);
113    return false;
114  }
115
116  // Set the faster clock, if possible.
117  ::timeBeginPeriod(1);
118
119  // 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);
154    file_util::CloseFile(file_);
155    file_ = NULL;
156
157    ::timeEndPeriod(1);
158
159    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;
167  EVENTMSG* msg_ptr = NULL;
168
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)
182  switch (nCode) {
183    case HC_SYSMODALON:
184      recording_enabled = false;
185      break;
186    case HC_SYSMODALOFF:
187      recording_enabled = true;
188      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.
203LRESULT EventRecorder::PlaybackWndProc(int nCode, WPARAM wParam,
204                                       LPARAM lParam) {
205  static bool playback_enabled = true;
206  int delay = 0;
207
208  switch (nCode) {
209    // A system modal dialog box is being displayed.  Stop playing back
210    // messages.
211    case HC_SYSMODALON:
212      playback_enabled = false;
213      break;
214
215    // A system modal dialog box is destroyed.  We can start playing back
216    // messages again.
217    case HC_SYSMODALOFF:
218      playback_enabled = true;
219      break;
220
221    // Prepare to copy the next mouse or keyboard event to playback.
222    case HC_SKIP:
223      if (!playback_enabled)
224        break;
225
226      // Read the next event from the record.
227      if (fread(&playback_msg_, sizeof(EVENTMSG), 1, file_) != 1)
228        this->StopPlayback();
229      break;
230
231    // Copy the mouse or keyboard event to the EVENTMSG structure in lParam.
232    case HC_GETNEXT:
233      if (!playback_enabled)
234        break;
235
236      memcpy(reinterpret_cast<void*>(lParam), &playback_msg_,
237             sizeof(playback_msg_));
238
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
259}  // namespace base
260