271 lines
9.5 KiB
C++
271 lines
9.5 KiB
C++
/*
|
|
* 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 <JavaScriptCore/FrameTracers.h>
|
|
#include <JavaScriptCore/JSPromise.h>
|
|
#include <wtf/RunLoop.h>
|
|
|
|
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 <https://bugs.webkit.org/show_bug.cgi?id=218840>.
|
|
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<Ref<JSON::Value>>&& 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<Ref<JSON::Value>>&& arguments)
|
|
{
|
|
if (m_suspended)
|
|
return makeUnexpected(EvaluationError::ExecutionSuspended);
|
|
|
|
return evaluateExpression(expressionForEvaluatingCommand(command, WTFMove(arguments)));
|
|
}
|
|
|
|
void InspectorFrontendAPIDispatcher::dispatchCommandWithResultAsync(const String& command, Vector<Ref<JSON::Value>>&& 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<JSC::JSPromise*>(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<DOMPromise> 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
|