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