// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <stddef.h> #include <windows.h> #include <mmsystem.h> #include "base/event_recorder.h" #include "base/file_util.h" #include "base/logging.h" // A note about time. // For perfect playback of events, you'd like a very accurate timer // so that events are played back at exactly the same time that // they were recorded. However, windows has a clock which is only // granular to ~15ms. We see more consistent event playback when // using a higher resolution timer. To do this, we use the // timeGetTime API instead of the default GetTickCount() API. namespace base { EventRecorder* EventRecorder::current_ = NULL; LRESULT CALLBACK StaticRecordWndProc(int nCode, WPARAM wParam, LPARAM lParam) { CHECK(EventRecorder::current()); return EventRecorder::current()->RecordWndProc(nCode, wParam, lParam); } LRESULT CALLBACK StaticPlaybackWndProc(int nCode, WPARAM wParam, LPARAM lParam) { CHECK(EventRecorder::current()); return EventRecorder::current()->PlaybackWndProc(nCode, wParam, lParam); } EventRecorder::~EventRecorder() { // Try to assert early if the caller deletes the recorder // while it is still in use. DCHECK(!journal_hook_); DCHECK(!is_recording_ && !is_playing_); } bool EventRecorder::StartRecording(const FilePath& filename) { if (journal_hook_ != NULL) return false; if (is_recording_ || is_playing_) return false; // Open the recording file. DCHECK(!file_); file_ = file_util::OpenFile(filename, "wb+"); if (!file_) { DLOG(ERROR) << "EventRecorder could not open log file"; return false; } // Set the faster clock, if possible. ::timeBeginPeriod(1); // Set the recording hook. JOURNALRECORD can only be used as a global hook. journal_hook_ = ::SetWindowsHookEx(WH_JOURNALRECORD, StaticRecordWndProc, GetModuleHandle(NULL), 0); if (!journal_hook_) { DLOG(ERROR) << "EventRecorder Record Hook failed"; file_util::CloseFile(file_); return false; } is_recording_ = true; return true; } void EventRecorder::StopRecording() { if (is_recording_) { DCHECK(journal_hook_ != NULL); if (!::UnhookWindowsHookEx(journal_hook_)) { DLOG(ERROR) << "EventRecorder Unhook failed"; // Nothing else we can really do here. return; } ::timeEndPeriod(1); DCHECK(file_ != NULL); file_util::CloseFile(file_); file_ = NULL; journal_hook_ = NULL; is_recording_ = false; } } bool EventRecorder::StartPlayback(const FilePath& filename) { if (journal_hook_ != NULL) return false; if (is_recording_ || is_playing_) return false; // Open the recording file. DCHECK(!file_); file_ = file_util::OpenFile(filename, "rb"); if (!file_) { DLOG(ERROR) << "EventRecorder Playback could not open log file"; return false; } // Read the first event from the record. if (fread(&playback_msg_, sizeof(EVENTMSG), 1, file_) != 1) { DLOG(ERROR) << "EventRecorder Playback has no records!"; file_util::CloseFile(file_); return false; } // Set the faster clock, if possible. ::timeBeginPeriod(1); // Playback time is tricky. When playing back, we read a series of events, // each with timeouts. Simply subtracting the delta between two timers will // lead to fast playback (about 2x speed). The API has two events, one // which advances to the next event (HC_SKIP), and another that requests the // event (HC_GETNEXT). The same event will be requested multiple times. // Each time the event is requested, we must calculate the new delay. // To do this, we track the start time of the playback, and constantly // re-compute the delay. I mention this only because I saw two examples // of how to use this code on the net, and both were broken :-) playback_start_time_ = timeGetTime(); playback_first_msg_time_ = playback_msg_.time; // Set the hook. JOURNALPLAYBACK can only be used as a global hook. journal_hook_ = ::SetWindowsHookEx(WH_JOURNALPLAYBACK, StaticPlaybackWndProc, GetModuleHandle(NULL), 0); if (!journal_hook_) { DLOG(ERROR) << "EventRecorder Playback Hook failed"; return false; } is_playing_ = true; return true; } void EventRecorder::StopPlayback() { if (is_playing_) { DCHECK(journal_hook_ != NULL); if (!::UnhookWindowsHookEx(journal_hook_)) { DLOG(ERROR) << "EventRecorder Unhook failed"; // Nothing else we can really do here. } DCHECK(file_ != NULL); file_util::CloseFile(file_); file_ = NULL; ::timeEndPeriod(1); journal_hook_ = NULL; is_playing_ = false; } } // Windows callback hook for the recorder. LRESULT EventRecorder::RecordWndProc(int nCode, WPARAM wParam, LPARAM lParam) { static bool recording_enabled = true; EVENTMSG* msg_ptr = NULL; // The API says we have to do this. // See http://msdn2.microsoft.com/en-us/library/ms644983(VS.85).aspx if (nCode < 0) return ::CallNextHookEx(journal_hook_, nCode, wParam, lParam); // Check for the break key being pressed and stop recording. if (::GetKeyState(VK_CANCEL) & 0x8000) { StopRecording(); return ::CallNextHookEx(journal_hook_, nCode, wParam, lParam); } // The Journal Recorder must stop recording events when system modal // dialogs are present. (see msdn link above) switch (nCode) { case HC_SYSMODALON: recording_enabled = false; break; case HC_SYSMODALOFF: recording_enabled = true; break; } if (nCode == HC_ACTION && recording_enabled) { // Aha - we have an event to record. msg_ptr = reinterpret_cast<EVENTMSG*>(lParam); msg_ptr->time = timeGetTime(); fwrite(msg_ptr, sizeof(EVENTMSG), 1, file_); fflush(file_); } return CallNextHookEx(journal_hook_, nCode, wParam, lParam); } // Windows callback for the playback mode. LRESULT EventRecorder::PlaybackWndProc(int nCode, WPARAM wParam, LPARAM lParam) { static bool playback_enabled = true; int delay = 0; switch (nCode) { // A system modal dialog box is being displayed. Stop playing back // messages. case HC_SYSMODALON: playback_enabled = false; break; // A system modal dialog box is destroyed. We can start playing back // messages again. case HC_SYSMODALOFF: playback_enabled = true; break; // Prepare to copy the next mouse or keyboard event to playback. case HC_SKIP: if (!playback_enabled) break; // Read the next event from the record. if (fread(&playback_msg_, sizeof(EVENTMSG), 1, file_) != 1) this->StopPlayback(); break; // Copy the mouse or keyboard event to the EVENTMSG structure in lParam. case HC_GETNEXT: if (!playback_enabled) break; memcpy(reinterpret_cast<void*>(lParam), &playback_msg_, sizeof(playback_msg_)); // The return value is the amount of time (in milliseconds) to wait // before playing back the next message in the playback queue. Each // time this is called, we recalculate the delay relative to our current // wall clock. delay = (playback_msg_.time - playback_first_msg_time_) - (timeGetTime() - playback_start_time_); if (delay < 0) delay = 0; return delay; // An application has called PeekMessage with wRemoveMsg set to PM_NOREMOVE // indicating that the message is not removed from the message queue after // PeekMessage processing. case HC_NOREMOVE: break; } return CallNextHookEx(journal_hook_, nCode, wParam, lParam); } } // namespace base