/* * Copyright (C) 2014-2021 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 APPLE INC. AND ITS 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 APPLE INC. OR ITS 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 "InspectorFrontendAPIDispatcher.h" #include "Frame.h" #include "InspectorController.h" #include "JSDOMPromise.h" #include "Page.h" #include "ScriptController.h" #include "ScriptDisallowedScope.h" #include "ScriptSourceCode.h" #include "ScriptState.h" #include #include #include namespace WebCore { using EvaluationError = InspectorFrontendAPIDispatcher::EvaluationError; InspectorFrontendAPIDispatcher::InspectorFrontendAPIDispatcher(Page& frontendPage) : m_frontendPage(makeWeakPtr(frontendPage)) { } InspectorFrontendAPIDispatcher::~InspectorFrontendAPIDispatcher() { invalidateQueuedExpressions(); invalidatePendingResponses(); } void InspectorFrontendAPIDispatcher::reset() { m_frontendLoaded = false; m_suspended = false; invalidateQueuedExpressions(); invalidatePendingResponses(); } void InspectorFrontendAPIDispatcher::frontendLoaded() { ASSERT(m_frontendPage); m_frontendLoaded = true; // In some convoluted WebKitLegacy-only scenarios, the backend may try to dispatch events to the frontend // underneath InspectorFrontendHost::loaded() when it is unsafe to execute script, causing suspend() to // be called before the frontend has fully loaded. See . if (!m_suspended) evaluateQueuedExpressions(); } void InspectorFrontendAPIDispatcher::suspend(UnsuspendSoon unsuspendSoon) { if (m_suspended) return; m_suspended = true; if (unsuspendSoon == UnsuspendSoon::Yes) { RunLoop::main().dispatch([protectedThis = makeRef(*this)] { // If the frontend page has been deallocated, there's nothing to do. if (!protectedThis->m_frontendPage) return; protectedThis->unsuspend(); }); } } void InspectorFrontendAPIDispatcher::unsuspend() { if (!m_suspended) return; m_suspended = false; if (m_frontendLoaded) evaluateQueuedExpressions(); } JSDOMGlobalObject* InspectorFrontendAPIDispatcher::frontendGlobalObject() { if (!m_frontendPage) return nullptr; return m_frontendPage->mainFrame().script().globalObject(mainThreadNormalWorld()); } static String expressionForEvaluatingCommand(const String& command, Vector>&& arguments) { StringBuilder expression; expression.append("InspectorFrontendAPI.dispatch([\"", command, '"'); for (auto& argument : arguments) { expression.append(", "); argument->writeJSON(expression); } expression.append("])"); return expression.toString(); } InspectorFrontendAPIDispatcher::EvaluationResult InspectorFrontendAPIDispatcher::dispatchCommandWithResultSync(const String& command, Vector>&& arguments) { if (m_suspended) return makeUnexpected(EvaluationError::ExecutionSuspended); return evaluateExpression(expressionForEvaluatingCommand(command, WTFMove(arguments))); } void InspectorFrontendAPIDispatcher::dispatchCommandWithResultAsync(const String& command, Vector>&& arguments, EvaluationResultHandler&& resultHandler) { evaluateOrQueueExpression(expressionForEvaluatingCommand(command, WTFMove(arguments)), WTFMove(resultHandler)); } void InspectorFrontendAPIDispatcher::dispatchMessageAsync(const String& message) { evaluateOrQueueExpression(makeString("InspectorFrontendAPI.dispatchMessageAsync(", message, ")")); } void InspectorFrontendAPIDispatcher::evaluateOrQueueExpression(const String& expression, EvaluationResultHandler&& optionalResultHandler) { // If the frontend page has been deallocated, then there is nothing to do. if (!m_frontendPage) { if (optionalResultHandler) optionalResultHandler(makeUnexpected(EvaluationError::ContextDestroyed)); return; } // Sometimes we get here by sending messages for events triggered by DOM mutations earlier in the call stack. // If this is the case, then it's not safe to evaluate script synchronously, so do it later. This only affects // WebKit1 and some layout tests that use a single web process for both the inspector and inspected page. if (!ScriptDisallowedScope::InMainThread::isScriptAllowed()) suspend(UnsuspendSoon::Yes); if (!m_frontendLoaded || m_suspended) { m_queuedEvaluations.append(std::make_pair(expression, WTFMove(optionalResultHandler))); return; } ValueOrException result = evaluateExpression(expression); if (!optionalResultHandler) return; if (!result.has_value()) { optionalResultHandler(result); return; } JSDOMGlobalObject* globalObject = frontendGlobalObject(); if (!globalObject) { optionalResultHandler(makeUnexpected(EvaluationError::ContextDestroyed)); return; } auto& vm = globalObject->vm(); auto* castedPromise = JSC::jsDynamicCast(vm, result.value()); if (!castedPromise) { // Simple case: result is NOT a promise, just return the JSValue. optionalResultHandler(result); return; } // If the result is a promise, call the result handler when the promise settles. Ref promise = DOMPromise::create(*globalObject, *castedPromise); m_pendingResponses.set(promise.copyRef(), WTFMove(optionalResultHandler)); auto isRegistered = promise->whenSettled([promise = promise.copyRef(), weakThis = makeWeakPtr(*this)] { // If `this` is cleared or the responses map is empty, then the promise settled // beyond the time when we care about its result. Ignore late-settled promises. // We clear out completion handlers for pending responses during teardown. if (!weakThis) return; auto strongThis = makeRef(*weakThis); if (!strongThis->m_pendingResponses.size()) return; EvaluationResultHandler resultHandler = strongThis->m_pendingResponses.take(promise); ASSERT(resultHandler); JSDOMGlobalObject* globalObject = strongThis->frontendGlobalObject(); if (!globalObject) { resultHandler(makeUnexpected(EvaluationError::ContextDestroyed)); return; } resultHandler({ promise->promise()->result(globalObject->vm()) }); }); if (isRegistered == DOMPromise::IsCallbackRegistered::No) optionalResultHandler(makeUnexpected(EvaluationError::InternalError)); } void InspectorFrontendAPIDispatcher::invalidateQueuedExpressions() { auto queuedEvaluations = std::exchange(m_queuedEvaluations, { }); for (auto& pair : queuedEvaluations) { auto resultHandler = WTFMove(pair.second); if (resultHandler) resultHandler(makeUnexpected(EvaluationError::ContextDestroyed)); } } void InspectorFrontendAPIDispatcher::invalidatePendingResponses() { auto pendingResponses = std::exchange(m_pendingResponses, { }); for (auto& callback : pendingResponses.values()) callback(makeUnexpected(EvaluationError::ContextDestroyed)); // No more pending responses should have been added while erroring out the callbacks. ASSERT(m_pendingResponses.isEmpty()); } void InspectorFrontendAPIDispatcher::evaluateQueuedExpressions() { // If the frontend page has been deallocated, then there is nothing to do. if (!m_frontendPage) return; if (m_queuedEvaluations.isEmpty()) return; auto queuedEvaluations = std::exchange(m_queuedEvaluations, { }); for (auto& pair : queuedEvaluations) { auto result = evaluateExpression(pair.first); if (auto resultHandler = WTFMove(pair.second)) resultHandler(result); } } ValueOrException InspectorFrontendAPIDispatcher::evaluateExpression(const String& expression) { ASSERT(m_frontendPage); ASSERT(!m_suspended); ASSERT(m_queuedEvaluations.isEmpty()); JSC::SuspendExceptionScope scope(&m_frontendPage->inspectorController().vm()); return m_frontendPage->mainFrame().script().evaluateInWorld(ScriptSourceCode(expression), mainThreadNormalWorld()); } void InspectorFrontendAPIDispatcher::evaluateExpressionForTesting(const String& expression) { evaluateOrQueueExpression(expression); } } // namespace WebKit