| OLD | NEW |
| (Empty) |
| 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 | |
| 21 namespace base { | |
| 22 | |
| 23 EventRecorder* EventRecorder::current_ = NULL; | |
| 24 | |
| 25 LRESULT CALLBACK StaticRecordWndProc(int nCode, WPARAM wParam, | |
| 26 LPARAM lParam) { | |
| 27 DCHECK(EventRecorder::current()); | |
| 28 return EventRecorder::current()->RecordWndProc(nCode, wParam, lParam); | |
| 29 } | |
| 30 | |
| 31 LRESULT CALLBACK StaticPlaybackWndProc(int nCode, WPARAM wParam, | |
| 32 LPARAM lParam) { | |
| 33 DCHECK(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_ = 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 | |
| 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 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_ = 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 | |
| 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 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 | |
| OLD | NEW |