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