1// Copyright (c) 2011 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 <stddef.h>
6#include <windows.h>
7#include <mmsystem.h>
8
9#include "base/event_recorder.h"
10#include "base/files/file_util.h"
11#include "base/logging.h"
12
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) {
27  DCHECK(EventRecorder::current());
28  return EventRecorder::current()->RecordWndProc(nCode, wParam, lParam);
29}
30
31LRESULT CALLBACK StaticPlaybackWndProc(int nCode, WPARAM wParam,
32                                       LPARAM lParam) {
33  DCHECK(EventRecorder::current());
34  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
44bool EventRecorder::StartRecording(const FilePath& filename) {
45  if (journal_hook_ != NULL)
46    return false;
47  if (is_recording_ || is_playing_)
48    return false;
49
50  // Open the recording file.
51  DCHECK(!file_);
52  file_ = OpenFile(filename, "wb+");
53  if (!file_) {
54    DLOG(ERROR) << "EventRecorder could not open log file";
55    return false;
56  }
57
58  // Set the faster clock, if possible.
59  ::timeBeginPeriod(1);
60
61  // 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";
66    CloseFile(file_);
67    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
84    ::timeEndPeriod(1);
85
86    DCHECK(file_ != NULL);
87    CloseFile(file_);
88    file_ = NULL;
89
90    journal_hook_ = NULL;
91    is_recording_ = false;
92  }
93}
94
95bool EventRecorder::StartPlayback(const FilePath& filename) {
96  if (journal_hook_ != NULL)
97    return false;
98  if (is_recording_ || is_playing_)
99    return false;
100
101  // Open the recording file.
102  DCHECK(!file_);
103  file_ = OpenFile(filename, "rb");
104  if (!file_) {
105    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!";
111    CloseFile(file_);
112    return false;
113  }
114
115  // Set the faster clock, if possible.
116  ::timeBeginPeriod(1);
117
118  // 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);
153    CloseFile(file_);
154    file_ = NULL;
155
156    ::timeEndPeriod(1);
157
158    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;
166  EVENTMSG* msg_ptr = NULL;
167
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)
181  switch (nCode) {
182    case HC_SYSMODALON:
183      recording_enabled = false;
184      break;
185    case HC_SYSMODALOFF:
186      recording_enabled = true;
187      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.
202LRESULT EventRecorder::PlaybackWndProc(int nCode, WPARAM wParam,
203                                       LPARAM lParam) {
204  static bool playback_enabled = true;
205  int delay = 0;
206
207  switch (nCode) {
208    // A system modal dialog box is being displayed.  Stop playing back
209    // messages.
210    case HC_SYSMODALON:
211      playback_enabled = false;
212      break;
213
214    // A system modal dialog box is destroyed.  We can start playing back
215    // messages again.
216    case HC_SYSMODALOFF:
217      playback_enabled = true;
218      break;
219
220    // Prepare to copy the next mouse or keyboard event to playback.
221    case HC_SKIP:
222      if (!playback_enabled)
223        break;
224
225      // Read the next event from the record.
226      if (fread(&playback_msg_, sizeof(EVENTMSG), 1, file_) != 1)
227        this->StopPlayback();
228      break;
229
230    // Copy the mouse or keyboard event to the EVENTMSG structure in lParam.
231    case HC_GETNEXT:
232      if (!playback_enabled)
233        break;
234
235      memcpy(reinterpret_cast<void*>(lParam), &playback_msg_,
236             sizeof(playback_msg_));
237
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
258}  // namespace base
259