/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "RuntimeAgent.h" #include "SessionState.h" namespace facebook::react::jsinspector_modern { RuntimeAgent::RuntimeAgent( FrontendChannel frontendChannel, RuntimeTargetController& targetController, const ExecutionContextDescription& executionContextDescription, SessionState& sessionState, std::unique_ptr delegate) : frontendChannel_(std::move(frontendChannel)), targetController_(targetController), sessionState_(sessionState), delegate_(std::move(delegate)), executionContextDescription_(executionContextDescription) { for (auto& [name, contextSelectors] : sessionState_.subscribedBindings) { if (matchesAny(executionContextDescription_, contextSelectors)) { targetController_.installBindingHandler(name); } } if (sessionState_.isRuntimeDomainEnabled && sessionState_.isLogDomainEnabled) { targetController_.notifyDebuggerSessionCreated(); } } bool RuntimeAgent::handleRequest(const cdp::PreparsedRequest& req) { if (req.method == "Runtime.addBinding") { std::string bindingName = req.params["name"].getString(); ExecutionContextSelector contextSelector = ExecutionContextSelector::all(); // TODO: Eventually, move execution context targeting out of RuntimeAgent. // Right now, there's only ever one context (Runtime) in a Host, so we can // handle it here for simplicity, and use session state to propagate // bindings to the next RuntimeAgent. if (req.params.count("executionContextId")) { auto executionContextId = req.params["executionContextId"].getInt(); if (executionContextId < (int64_t)std::numeric_limits::min() || executionContextId > (int64_t)std::numeric_limits::max()) { frontendChannel_(cdp::jsonError( req.id, cdp::ErrorCode::InvalidParams, "Invalid execution context id")); return true; } contextSelector = ExecutionContextSelector::byId((int32_t)executionContextId); if (req.params.count("executionContextName")) { frontendChannel_(cdp::jsonError( req.id, cdp::ErrorCode::InvalidParams, "executionContextName is mutually exclusive with executionContextId")); return true; } } else if (req.params.count("executionContextName")) { contextSelector = ExecutionContextSelector::byName( req.params["executionContextName"].getString()); } if (contextSelector.matches(executionContextDescription_)) { targetController_.installBindingHandler(bindingName); } sessionState_.subscribedBindings[bindingName].insert(contextSelector); frontendChannel_(cdp::jsonResult(req.id)); return true; } if (req.method == "Runtime.removeBinding") { // @cdp Runtime.removeBinding has no targeting by execution context. We // interpret it to mean "unsubscribe, and stop installing the binding on // all new contexts". This diverges slightly from V8, which continues // to install the binding on new contexts after it's "removed", but *only* // if the subscription is targeted by context name. sessionState_.subscribedBindings.erase(req.params["name"].getString()); frontendChannel_(cdp::jsonResult(req.id)); return true; } if (req.method == "Runtime.enable" && sessionState_.isLogDomainEnabled) { targetController_.notifyDebuggerSessionCreated(); } if (req.method == "Log.enable" && sessionState_.isRuntimeDomainEnabled) { targetController_.notifyDebuggerSessionCreated(); } if (req.method == "Runtime.disable" && sessionState_.isLogDomainEnabled) { targetController_.notifyDebuggerSessionDestroyed(); } if (req.method == "Log.disable" && sessionState_.isRuntimeDomainEnabled) { targetController_.notifyDebuggerSessionDestroyed(); } if (delegate_) { return delegate_->handleRequest(req); } return false; } void RuntimeAgent::notifyBindingCalled( const std::string& bindingName, const std::string& payload) { // NOTE: When dispatching @cdp Runtime.bindingCalled notifications, we don't // re-check whether the session is expecting notifications from the current // context - only that it's subscribed to that binding name. // Theoretically, this can result in over-sending notifications from contexts // that the client no longer cares about, or never cared about to begin with // (e.g. if the binding handler was installed by a previous session). // // React Native intentionally replicates this behavior for the sake of // bug-for-bug compatibility with Chrome, but clients should probably not rely // on it. if (!sessionState_.subscribedBindings.count(bindingName)) { return; } frontendChannel_(cdp::jsonNotification( "Runtime.bindingCalled", folly::dynamic::object( "executionContextId", executionContextDescription_.id)( "name", bindingName)("payload", payload))); } RuntimeAgent::ExportedState RuntimeAgent::getExportedState() { return { .delegateState = delegate_ ? delegate_->getExportedState() : nullptr, }; } RuntimeAgent::~RuntimeAgent() { if (sessionState_.isRuntimeDomainEnabled && sessionState_.isLogDomainEnabled) { targetController_.notifyDebuggerSessionDestroyed(); } // TODO: Eventually, there may be more than one Runtime per Page, and we'll // need to store multiple agent states here accordingly. For now let's do // the simple thing and assume (as we do elsewhere) that only one Runtime // per Page can exist at a time. sessionState_.lastRuntimeAgentExportedState = getExportedState(); } } // namespace facebook::react::jsinspector_modern