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/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 21 namespace base { 22 23 EventRecorder* EventRecorder::current_ = NULL; 24 25 LRESULT CALLBACK StaticRecordWndProc(int nCode, WPARAM wParam, 26 LPARAM lParam) { 27 CHECK(EventRecorder::current()); 28 return EventRecorder::current()->RecordWndProc(nCode, wParam, lParam); 29 } 30 31 LRESULT CALLBACK StaticPlaybackWndProc(int nCode, WPARAM wParam, 32 LPARAM lParam) { 33 CHECK(EventRecorder::current()); 34 return EventRecorder::current()->PlaybackWndProc(nCode, wParam, lParam); 35 } 36 37 EventRecorder::~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 44 bool 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_ = file_util::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 file_util::CloseFile(file_); 67 return false; 68 } 69 70 is_recording_ = true; 71 return true; 72 } 73 74 void 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 file_util::CloseFile(file_); 88 file_ = NULL; 89 90 journal_hook_ = NULL; 91 is_recording_ = false; 92 } 93 } 94 95 bool 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_ = file_util::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 file_util::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 143 void 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 file_util::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. 164 LRESULT 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. 202 LRESULT 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