/* * Copyright (C) 2011-2013 University of Washington. All rights reserved. * Copyright (C) 2014 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "ReplayController.h" #if ENABLE(WEB_REPLAY) #include "AllReplayInputs.h" #include "CapturingInputCursor.h" #include "DOMWindow.h" #include "DocumentLoader.h" #include "Frame.h" #include "FrameTree.h" #include "InspectorInstrumentation.h" #include "Location.h" #include "Logging.h" #include "MainFrame.h" #include "Page.h" #include "ReplaySession.h" #include "ReplaySessionSegment.h" #include "ReplayingInputCursor.h" #include "ScriptController.h" #include "SerializationMethods.h" #include "Settings.h" #include "UserInputBridge.h" #include "WebReplayInputs.h" #include #include #if ENABLE(ASYNC_SCROLLING) #include "ScrollingCoordinator.h" #endif namespace WebCore { static void logDispatchedDOMEvent(const Event& event, bool eventIsUnrelated) { #if !LOG_DISABLED EventTarget* target = event.target(); if (!target) return; // A DOM event is unrelated if it is being dispatched to a document that is neither capturing nor replaying. if (Node* node = target->toNode()) { LOG(WebReplay, "%-20s --->%s DOM event: type=%s, target=%lu/node[%p] %s\n", "ReplayEvents", (eventIsUnrelated) ? "Unrelated" : "Dispatching", event.type().string().utf8().data(), frameIndexFromDocument((node->inDocument()) ? &node->document() : node->ownerDocument()), node, node->nodeName().utf8().data()); } else if (DOMWindow* window = target->toDOMWindow()) { LOG(WebReplay, "%-20s --->%s DOM event: type=%s, target=%lu/window[%p] %s\n", "ReplayEvents", (eventIsUnrelated) ? "Unrelated" : "Dispatching", event.type().string().utf8().data(), frameIndexFromDocument(window->document()), window, window->location()->href().utf8().data()); } #else UNUSED_PARAM(event); UNUSED_PARAM(eventIsUnrelated); #endif } ReplayController::ReplayController(Page& page) : m_page(page) , m_loadedSegment(nullptr) , m_loadedSession(ReplaySession::create()) , m_emptyCursor(EmptyInputCursor::create()) , m_activeCursor(nullptr) , m_targetPosition(ReplayPosition(0, 0)) , m_currentPosition(ReplayPosition(0, 0)) , m_segmentState(SegmentState::Unloaded) , m_sessionState(SessionState::Inactive) , m_dispatchSpeed(DispatchSpeed::FastForward) { } void ReplayController::setForceDeterministicSettings(bool shouldForceDeterministicBehavior) { ASSERT(shouldForceDeterministicBehavior ^ (m_sessionState == SessionState::Inactive)); if (shouldForceDeterministicBehavior) { m_savedSettings.usesPageCache = m_page.settings().usesPageCache(); m_page.settings().setUsesPageCache(false); } else { m_page.settings().setUsesPageCache(m_savedSettings.usesPageCache); } #if ENABLE(ASYNC_SCROLLING) if (ScrollingCoordinator* scrollingCoordinator = m_page.scrollingCoordinator()) scrollingCoordinator->replaySessionStateDidChange(); #endif } void ReplayController::setSessionState(SessionState state) { ASSERT(state != m_sessionState); switch (m_sessionState) { case SessionState::Capturing: ASSERT(state == SessionState::Inactive); m_sessionState = state; m_page.userInputBridge().setState(UserInputBridge::State::Open); break; case SessionState::Inactive: m_sessionState = state; m_page.userInputBridge().setState(state == SessionState::Capturing ? UserInputBridge::State::Capturing : UserInputBridge::State::Replaying); break; case SessionState::Replaying: ASSERT(state == SessionState::Inactive); m_sessionState = state; m_page.userInputBridge().setState(UserInputBridge::State::Open); break; } } void ReplayController::switchSession(PassRefPtr session) { ASSERT(m_segmentState == SegmentState::Unloaded); ASSERT(m_sessionState == SessionState::Inactive); m_loadedSession = session; m_currentPosition = ReplayPosition(0, 0); LOG(WebReplay, "%-20sSwitching sessions from %p to %p.\n", "ReplayController", m_loadedSession.get(), session.get()); InspectorInstrumentation::sessionLoaded(&m_page, m_loadedSession); } void ReplayController::createSegment() { ASSERT(m_sessionState == SessionState::Capturing); ASSERT(m_segmentState == SegmentState::Unloaded); m_segmentState = SegmentState::Appending; // Create a new segment but don't associate it with the current session // until we stop appending to it. This preserves the invariant that // segments associated with a replay session have immutable data. m_loadedSegment = ReplaySessionSegment::create(); LOG(WebReplay, "%-20s Created segment: %p.\n", "ReplayController", m_loadedSegment.get()); InspectorInstrumentation::segmentCreated(&m_page, m_loadedSegment); m_activeCursor = m_loadedSegment->createCapturingCursor(m_page); m_activeCursor->appendInput(); std::unique_ptr navigationInput = InitialNavigation::createFromPage(m_page); // Dispatching this input schedules navigation of the main frame, causing a refresh. navigationInput->dispatch(*this); m_activeCursor->storeInput(WTF::move(navigationInput)); } void ReplayController::completeSegment() { ASSERT(m_sessionState == SessionState::Capturing); ASSERT(m_segmentState == SegmentState::Appending); m_activeCursor->appendInput(); // Hold on to a reference so unloading the segment doesn't deallocate it. RefPtr segment = m_loadedSegment; m_segmentState = SegmentState::Loaded; bool shouldSuppressNotifications = true; unloadSegment(shouldSuppressNotifications); LOG(WebReplay, "%-20s Completed segment: %p.\n", "ReplayController", segment.get()); InspectorInstrumentation::segmentCompleted(&m_page, segment); m_loadedSession->appendSegment(segment); InspectorInstrumentation::sessionModified(&m_page, m_loadedSession); } void ReplayController::loadSegmentAtIndex(size_t segmentIndex) { ASSERT(segmentIndex < m_loadedSession->size()); RefPtr segment = m_loadedSession->at(segmentIndex); ASSERT(m_sessionState == SessionState::Replaying); ASSERT(m_segmentState == SegmentState::Unloaded); ASSERT(segment); ASSERT(!m_loadedSegment); m_loadedSegment = segment; m_segmentState = SegmentState::Loaded; m_currentPosition.segmentOffset = segmentIndex; m_currentPosition.inputOffset = 0; m_activeCursor = m_loadedSegment->createReplayingCursor(m_page, this); LOG(WebReplay, "%-20sLoading segment: %p.\n", "ReplayController", segment.get()); InspectorInstrumentation::segmentLoaded(&m_page, segment); } void ReplayController::unloadSegment(bool suppressNotifications) { ASSERT(m_sessionState != SessionState::Inactive); ASSERT(m_segmentState == SegmentState::Loaded); m_segmentState = SegmentState::Unloaded; LOG(WebReplay, "%-20s Clearing input cursors for page: %p\n", "ReplayController", &m_page); m_activeCursor = nullptr; RefPtr unloadedSegment = m_loadedSegment.release(); for (Frame* frame = &m_page.mainFrame(); frame; frame = frame->tree().traverseNext()) { frame->script().globalObject(mainThreadNormalWorld())->setInputCursor(m_emptyCursor); frame->document()->setInputCursor(m_emptyCursor); } // When we stop capturing, don't send out segment unloaded events since we // didn't send out the corresponding segmentLoaded event at the start of capture. if (!suppressNotifications) { LOG(WebReplay, "%-20sUnloading segment: %p.\n", "ReplayController", unloadedSegment.get()); InspectorInstrumentation::segmentUnloaded(&m_page); } } void ReplayController::startCapturing() { ASSERT(m_sessionState == SessionState::Inactive); ASSERT(m_segmentState == SegmentState::Unloaded); setSessionState(SessionState::Capturing); setForceDeterministicSettings(true); LOG(WebReplay, "%-20s Starting capture.\n", "ReplayController"); InspectorInstrumentation::captureStarted(&m_page); m_currentPosition = ReplayPosition(0, 0); createSegment(); } void ReplayController::stopCapturing() { ASSERT(m_sessionState == SessionState::Capturing); ASSERT(m_segmentState == SegmentState::Appending); completeSegment(); setSessionState(SessionState::Inactive); setForceDeterministicSettings(false); LOG(WebReplay, "%-20s Stopping capture.\n", "ReplayController"); InspectorInstrumentation::captureStopped(&m_page); } void ReplayController::startPlayback() { ASSERT(m_sessionState == SessionState::Replaying); ASSERT(m_segmentState == SegmentState::Loaded); m_segmentState = SegmentState::Dispatching; LOG(WebReplay, "%-20s Starting playback to position (segment: %d, input: %d).\n", "ReplayController", m_targetPosition.segmentOffset, m_targetPosition.inputOffset); InspectorInstrumentation::playbackStarted(&m_page); dispatcher().setDispatchSpeed(m_dispatchSpeed); dispatcher().run(); } void ReplayController::pausePlayback() { ASSERT(m_sessionState == SessionState::Replaying); ASSERT(m_segmentState == SegmentState::Dispatching); if (dispatcher().isRunning()) dispatcher().pause(); m_segmentState = SegmentState::Loaded; LOG(WebReplay, "%-20s Pausing playback at position (segment: %d, input: %d).\n", "ReplayController", m_currentPosition.segmentOffset, m_currentPosition.inputOffset); InspectorInstrumentation::playbackPaused(&m_page, m_currentPosition); } void ReplayController::cancelPlayback() { ASSERT(m_sessionState == SessionState::Replaying); ASSERT(m_segmentState != SegmentState::Appending); if (m_segmentState == SegmentState::Unloaded) return; if (m_segmentState == SegmentState::Dispatching) pausePlayback(); ASSERT(m_segmentState == SegmentState::Loaded); unloadSegment(); m_sessionState = SessionState::Inactive; setForceDeterministicSettings(false); InspectorInstrumentation::playbackFinished(&m_page); } void ReplayController::replayToPosition(const ReplayPosition& position, DispatchSpeed speed) { ASSERT(m_sessionState != SessionState::Capturing); ASSERT(m_segmentState == SegmentState::Loaded || m_segmentState == SegmentState::Unloaded); ASSERT(position.segmentOffset < m_loadedSession->size()); m_dispatchSpeed = speed; if (m_sessionState != SessionState::Replaying) { setSessionState(SessionState::Replaying); setForceDeterministicSettings(true); } if (m_segmentState == SegmentState::Unloaded) loadSegmentAtIndex(position.segmentOffset); else if (position.segmentOffset != m_currentPosition.segmentOffset || m_currentPosition.inputOffset > position.inputOffset) { // If the desired segment is not loaded or we have gone past the desired input // offset, then unload the current segment and load the appropriate segment. unloadSegment(); loadSegmentAtIndex(position.segmentOffset); } ASSERT(m_currentPosition.segmentOffset == position.segmentOffset); ASSERT(m_loadedSession->at(position.segmentOffset) == m_loadedSegment); m_targetPosition = position; startPlayback(); } void ReplayController::frameNavigated(DocumentLoader* loader) { ASSERT(m_sessionState != SessionState::Inactive); // The initial capturing segment is created prior to main frame navigation. // Otherwise, the prior capturing segment was completed when the frame detached, // and it is now time to create a new segment. if (m_sessionState == SessionState::Capturing && m_segmentState == SegmentState::Unloaded) { m_currentPosition = ReplayPosition(m_currentPosition.segmentOffset + 1, 0); createSegment(); } // During playback, the next segment is loaded when the final input is dispatched, // so nothing needs to be done here. // We store the input cursor in both Document and JSDOMWindow, so that // replay state is accessible from JavaScriptCore and script-free layout code. loader->frame()->document()->setInputCursor(m_activeCursor.get()); loader->frame()->script().globalObject(mainThreadNormalWorld())->setInputCursor(m_activeCursor.get()); } void ReplayController::frameDetached(Frame* frame) { ASSERT(m_sessionState != SessionState::Inactive); ASSERT(frame); if (!frame->document()) return; // If the frame's cursor isn't capturing or replaying, we should do nothing. // This is the case for the "outbound" frame when starting capture, or when // we clear the input cursor to finish or prematurely unload a segment. if (frame->document()->inputCursor().isCapturing()) { ASSERT(m_segmentState == SegmentState::Appending); completeSegment(); } // During playback, the segments are unloaded and loaded when the final // input has been dispatched. So, nothing needs to be done here. } void ReplayController::willDispatchEvent(const Event& event, Frame* frame) { EventTarget* target = event.target(); if (!target && !frame) return; Document* document = frame ? frame->document() : nullptr; // Fetch the document from the event target, because the target could be detached. if (Node* node = target->toNode()) document = node->inDocument() ? &node->document() : node->ownerDocument(); else if (DOMWindow* window = target->toDOMWindow()) document = window->document(); ASSERT(document); InputCursor& cursor = document->inputCursor(); bool eventIsUnrelated = !cursor.isCapturing() && !cursor.isReplaying(); logDispatchedDOMEvent(event, eventIsUnrelated); #if ENABLE_AGGRESSIVE_DETERMINISM_CHECKS // To ensure deterministic JS execution, all DOM events must be dispatched deterministically. // If these assertions fail, then this DOM event is being dispatched by a nondeterministic EventLoop // cycle, and may cause program execution to diverge if any JS code runs because of the DOM event. if (cursor.isCapturing()) ASSERT(static_cast(cursor).withinEventLoopInputExtent()); else if (cursor.isReplaying()) ASSERT(dispatcher().isDispatching()); #endif } PassRefPtr ReplayController::loadedSession() const { return m_loadedSession; } PassRefPtr ReplayController::loadedSegment() const { return m_loadedSegment; } InputCursor& ReplayController::activeInputCursor() const { return m_activeCursor ? *m_activeCursor : *m_emptyCursor; } EventLoopInputDispatcher& ReplayController::dispatcher() const { ASSERT(m_sessionState == SessionState::Replaying); ASSERT(m_segmentState == SegmentState::Dispatching); ASSERT(m_activeCursor); ASSERT(m_activeCursor->isReplaying()); return static_cast(*m_activeCursor).dispatcher(); } void ReplayController::willDispatchInput(const EventLoopInputBase&) { ASSERT(m_sessionState == SessionState::Replaying); ASSERT(m_segmentState == SegmentState::Dispatching); m_currentPosition.inputOffset++; if (m_currentPosition == m_targetPosition) pausePlayback(); } void ReplayController::didDispatchInput(const EventLoopInputBase&) { ASSERT(m_sessionState == SessionState::Replaying); ASSERT(m_segmentState == SegmentState::Dispatching); InspectorInstrumentation::playbackHitPosition(&m_page, m_currentPosition); } void ReplayController::didDispatchFinalInput() { ASSERT(m_segmentState == SegmentState::Dispatching); // No more segments left to replay; stop. if (m_currentPosition.segmentOffset + 1 == m_loadedSession->size()) { // Normally the position is adjusted when loading the next segment. m_currentPosition.segmentOffset++; m_currentPosition.inputOffset = 0; cancelPlayback(); return; } unloadSegment(); loadSegmentAtIndex(m_currentPosition.segmentOffset + 1); startPlayback(); } } // namespace WebCore #endif // ENABLE(WEB_REPLAY)