mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Add stub page target implementation to jsinspector-modern (#42397)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/42397 Changelog: [Internal] Adds a stub `PageTarget` class to serve as the entry point to the modern CDP backend in React Native. The primary method exposed by `PageTarget` is `connect()` which is designed to fit directly as a connection callback passed to `InspectorPackagerConnection::addPage()`. This constructs a `PageTargetSession` containing a `PageAgent` where the actual CDP message handling/routing will occur. For now, `PageAgent` implements no CDP methods, and always responds with a "not implemented" error. Basic unit tests are included, though we might want to migrate to a more integration-style test suite (with fewer mocks and real bindings to RN) once we've implemented more of the protocol. ## What is a Page In Chrome's implementation of CDP, a Page represents a single browser tab. A Chrome DevTools session connects to one Page at a time (though it can potentially inspect multiple JavaScript contexts owned by that Page, such as those found in frames and workers). In our system, a Page will correspond 1:1 to React Native's concept of a *Host* (implemented as `RCTHost`, `RCTBridge`, `ReactHostImpl` or `ReactInstanceManager`, depending on the platform). In all cases, the Host is the object that has a stable identity across reloads, and manages the lifetime of an *Instance* where the JSVM and other application state lives. There can be multiple Hosts in a React Native process, though this is somewhat unusual; those would be treated as independent "tabs" from the perspective of the debugger. NOTE: The concepts of Target, Session and Agent are new (to this codebase) and are *broadly* inspired by the [corresponding Chromium / V8 concepts](https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/public/devtools_protocol/#Agents_Targets-and-Sessions), though some details differ. ## Next steps Each core platform implementation in React Native (iOS Bridgeless, iOS Bridge, Android Bridgeless, Android Bridge), as well as out-of-tree platforms that want to support the new debugger, will need to create and register a `PageTarget` instance. We'll do this piecemeal in subsequent diffs. We'll also gradually add APIs and logic to `PageTarget` / `PageAgent` to allow us to implement some "interesting" CDP methods - some of them directly (e.g. handling reload commands) and others by dispatching to nested agents (e.g. a JS debugging agent powered by Hermes). Reviewed By: huntie Differential Revision: D50936932 fbshipit-source-id: ebe5856d7badb361d4971dd9aabeb9982f8aed1b
This commit is contained in:
committed by
Facebook GitHub Bot
parent
965f2eb1fb
commit
7886abd243
@@ -0,0 +1,15 @@
|
||||
# jsinspector-modern concepts
|
||||
|
||||
## CDP object model
|
||||
|
||||
### Target
|
||||
|
||||
A debuggable entity that a debugger frontend can connect to.
|
||||
|
||||
### Session
|
||||
|
||||
A single connection between a debugger frontend and a target. There can be multiple active sessions connected to the same target.
|
||||
|
||||
### Agent
|
||||
|
||||
A handler for a subset of CDP messages for a specific target as part of a specific session.
|
||||
@@ -129,4 +129,11 @@ extern IInspector& getInspectorInstance();
|
||||
/// should only be used in tests.
|
||||
extern std::unique_ptr<IInspector> makeTestInspectorInstance();
|
||||
|
||||
/**
|
||||
* A callback that can be used to send debugger messages (method responses and
|
||||
* events) to the frontend. The message must be a JSON-encoded string.
|
||||
* The callback may be called from any thread.
|
||||
*/
|
||||
using FrontendChannel = std::function<void(std::string_view messageJson)>;
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 "InspectorUtilities.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
CallbackLocalConnection::CallbackLocalConnection(
|
||||
std::function<void(std::string)> handler)
|
||||
: handler_(std::move(handler)) {}
|
||||
|
||||
void CallbackLocalConnection::sendMessage(std::string message) {
|
||||
assert(handler_ && "Handler has been disconnected");
|
||||
handler_(std::move(message));
|
||||
}
|
||||
|
||||
void CallbackLocalConnection::disconnect() {
|
||||
handler_ = nullptr;
|
||||
}
|
||||
|
||||
RAIIRemoteConnection::RAIIRemoteConnection(
|
||||
std::unique_ptr<IRemoteConnection> remote)
|
||||
: remote_(std::move(remote)) {}
|
||||
|
||||
void RAIIRemoteConnection::onMessage(std::string message) {
|
||||
remote_->onMessage(std::move(message));
|
||||
}
|
||||
|
||||
RAIIRemoteConnection::~RAIIRemoteConnection() {
|
||||
remote_->onDisconnect();
|
||||
}
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "InspectorInterfaces.h"
|
||||
|
||||
// Utilities that are useful when integrating with InspectorInterfaces.h but
|
||||
// do not need to be exported.
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
/**
|
||||
* Wraps a callback function in ILocalConnection.
|
||||
*/
|
||||
class CallbackLocalConnection : public ILocalConnection {
|
||||
public:
|
||||
/**
|
||||
* Creates a new Connection that uses the given callback to send messages to
|
||||
* the backend.
|
||||
*/
|
||||
explicit CallbackLocalConnection(std::function<void(std::string)> handler);
|
||||
|
||||
void sendMessage(std::string message) override;
|
||||
|
||||
void disconnect() override;
|
||||
|
||||
private:
|
||||
std::function<void(std::string)> handler_;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps an IRemoteConnection in a simpler interface that calls `onDisconnect`
|
||||
* implicitly upon destruction.
|
||||
*/
|
||||
class RAIIRemoteConnection {
|
||||
public:
|
||||
explicit RAIIRemoteConnection(std::unique_ptr<IRemoteConnection> remote);
|
||||
|
||||
void onMessage(std::string message);
|
||||
|
||||
~RAIIRemoteConnection();
|
||||
|
||||
private:
|
||||
std::unique_ptr<IRemoteConnection> remote_;
|
||||
};
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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 <folly/dynamic.h>
|
||||
#include <folly/json.h>
|
||||
#include <jsinspector-modern/PageAgent.h>
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
PageAgent::PageAgent(FrontendChannel frontendChannel)
|
||||
: frontendChannel_(frontendChannel) {}
|
||||
|
||||
void PageAgent::handleRequest(const cdp::PreparsedRequest& req) {
|
||||
folly::dynamic res = folly::dynamic::object("id", req.id)(
|
||||
"error",
|
||||
folly::dynamic::object("code", -32601)(
|
||||
"message", req.method + " not implemented yet"));
|
||||
std::string json = folly::toJson(res);
|
||||
frontendChannel_(json);
|
||||
}
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jsinspector-modern/InspectorInterfaces.h>
|
||||
#include <jsinspector-modern/Parsing.h>
|
||||
#include <functional>
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
/**
|
||||
* An Agent that handles requests from the Chrome DevTools Protocol for the
|
||||
* given page.
|
||||
* The constructor, destructor and all public methods must be called on the
|
||||
* same thread.
|
||||
*/
|
||||
class PageAgent {
|
||||
public:
|
||||
/**
|
||||
* \param frontendChannel A channel used to send responses and events to the
|
||||
* frontend.
|
||||
*/
|
||||
explicit PageAgent(FrontendChannel frontendChannel);
|
||||
|
||||
/**
|
||||
* Handle a CDP request. The response will be sent over the provided
|
||||
* \c FrontendChannel synchronously or asynchronously.
|
||||
* \param req The parsed request.
|
||||
*/
|
||||
void handleRequest(const cdp::PreparsedRequest& req);
|
||||
|
||||
private:
|
||||
FrontendChannel frontendChannel_;
|
||||
};
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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 "PageTarget.h"
|
||||
#include "InspectorUtilities.h"
|
||||
#include "PageAgent.h"
|
||||
#include "Parsing.h"
|
||||
|
||||
#include <folly/dynamic.h>
|
||||
#include <folly/json.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* A Session connected to a PageTarget, passing CDP messages to and from a
|
||||
* PageAgent which it owns.
|
||||
*/
|
||||
class PageTargetSession {
|
||||
public:
|
||||
explicit PageTargetSession(std::unique_ptr<IRemoteConnection> remote)
|
||||
: remote_(std::make_shared<RAIIRemoteConnection>(std::move(remote))),
|
||||
frontendChannel_(
|
||||
[remoteWeak = std::weak_ptr(remote_)](std::string_view message) {
|
||||
if (auto remote = remoteWeak.lock()) {
|
||||
remote->onMessage(std::string(message));
|
||||
}
|
||||
}),
|
||||
pageAgent_(frontendChannel_) {}
|
||||
/**
|
||||
* Called by CallbackLocalConnection to send a message to this Session's
|
||||
* Agent.
|
||||
*/
|
||||
void operator()(std::string message) {
|
||||
cdp::PreparsedRequest request;
|
||||
// Messages may be invalid JSON, or have unexpected types.
|
||||
try {
|
||||
request = cdp::preparse(message);
|
||||
} catch (const cdp::ParseError& e) {
|
||||
frontendChannel_(folly::toJson(folly::dynamic::object("id", nullptr)(
|
||||
"error",
|
||||
folly::dynamic::object("code", -32700)("message", e.what()))));
|
||||
return;
|
||||
} catch (const cdp::TypeError& e) {
|
||||
frontendChannel_(folly::toJson(folly::dynamic::object("id", nullptr)(
|
||||
"error",
|
||||
folly::dynamic::object("code", -32600)("message", e.what()))));
|
||||
return;
|
||||
}
|
||||
|
||||
// Catch exceptions that may arise from accessing dynamic params during
|
||||
// request handling.
|
||||
try {
|
||||
pageAgent_.handleRequest(request);
|
||||
} catch (const cdp::TypeError& e) {
|
||||
frontendChannel_(folly::toJson(folly::dynamic::object("id", request.id)(
|
||||
"error",
|
||||
folly::dynamic::object("code", -32600)("message", e.what()))));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// Owned by this instance, but shared (weakly) with the frontend channel
|
||||
std::shared_ptr<RAIIRemoteConnection> remote_;
|
||||
FrontendChannel frontendChannel_;
|
||||
PageAgent pageAgent_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<ILocalConnection> PageTarget::connect(
|
||||
std::unique_ptr<IRemoteConnection> connectionToFrontend) {
|
||||
return std::make_unique<CallbackLocalConnection>(
|
||||
PageTargetSession(std::move(connectionToFrontend)));
|
||||
}
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jsinspector-modern/InspectorInterfaces.h>
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
/**
|
||||
* The top-level Target in a React Native app. This is equivalent to the
|
||||
* "Host" in React Native's architecture - the entity that manages the
|
||||
* lifecycle of a React Instance.
|
||||
*/
|
||||
class PageTarget {
|
||||
public:
|
||||
/**
|
||||
* Creates a new Session connected to this PageTarget, wrapped in an
|
||||
* interface which is compatible with \c IInspector::addPage.
|
||||
* The caller is responsible for destroying the connection before PageTarget
|
||||
* is destroyed, on the same thread where PageTarget's constructor and
|
||||
* destructor execute.
|
||||
*/
|
||||
std::unique_ptr<ILocalConnection> connect(
|
||||
std::unique_ptr<IRemoteConnection> connectionToFrontend);
|
||||
};
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 <folly/dynamic.h>
|
||||
#include <folly/json.h>
|
||||
#include <jsinspector-modern/Parsing.h>
|
||||
|
||||
namespace facebook::react::jsinspector_modern::cdp {
|
||||
|
||||
PreparsedRequest preparse(std::string_view message) {
|
||||
folly::dynamic parsed = folly::parseJson(message);
|
||||
return PreparsedRequest{
|
||||
.id = parsed["id"].getInt(),
|
||||
.method = parsed["method"].getString(),
|
||||
.params = parsed.count("params") ? parsed["params"] : nullptr};
|
||||
}
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern::cdp
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <folly/dynamic.h>
|
||||
#include <folly/json.h>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
namespace cdp {
|
||||
using RequestId = long long;
|
||||
|
||||
/**
|
||||
* An incoming CDP request that has been parsed into a more usable form.
|
||||
*/
|
||||
struct PreparsedRequest {
|
||||
public:
|
||||
/**
|
||||
* The ID of the request.
|
||||
*/
|
||||
RequestId id;
|
||||
|
||||
/**
|
||||
* The name of the method being invoked.
|
||||
*/
|
||||
std::string method;
|
||||
|
||||
/**
|
||||
* The parameters passed to the method, if any.
|
||||
*/
|
||||
folly::dynamic params;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a JSON-encoded CDP request into its constituent parts.
|
||||
* \throws ParseError If the input cannot be parsed.
|
||||
* \throws TypeError If the input does not conform to the expected format.
|
||||
*/
|
||||
PreparsedRequest preparse(std::string_view message);
|
||||
|
||||
/**
|
||||
* A type error that may be thrown while preparsing a request, or while
|
||||
* accessing dynamic params on a request.
|
||||
*/
|
||||
using TypeError = folly::TypeError;
|
||||
|
||||
/**
|
||||
* A parse error that may be thrown while preparsing a request.
|
||||
*/
|
||||
using ParseError = folly::json::parse_error;
|
||||
} // namespace cdp
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jsinspector-modern/PageTarget.h>
|
||||
@@ -49,6 +49,15 @@ class MockWebSocket : public IWebSocket {
|
||||
MOCK_METHOD(void, send, (std::string_view message), (override));
|
||||
};
|
||||
|
||||
class MockRemoteConnection : public IRemoteConnection {
|
||||
public:
|
||||
MockRemoteConnection() = default;
|
||||
|
||||
// IRemoteConnection methods
|
||||
MOCK_METHOD(void, onMessage, (std::string message), (override));
|
||||
MOCK_METHOD(void, onDisconnect, (), (override));
|
||||
};
|
||||
|
||||
class MockLocalConnection : public ILocalConnection {
|
||||
public:
|
||||
explicit MockLocalConnection(
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 <folly/dynamic.h>
|
||||
#include <folly/json.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <jsinspector-modern/InspectorInterfaces.h>
|
||||
#include <jsinspector-modern/PageTarget.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "FollyDynamicMatchers.h"
|
||||
#include "InspectorMocks.h"
|
||||
#include "UniquePtrFactory.h"
|
||||
|
||||
using namespace ::testing;
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Simplified test harness focused on sending messages to and from a PageTarget.
|
||||
*/
|
||||
class PageTargetProtocolTest : public Test {
|
||||
public:
|
||||
PageTargetProtocolTest() {
|
||||
toPage_ = page_.connect(remoteConnections_.make_unique());
|
||||
|
||||
// In protocol tests, we'll always get an onDisconnect call when we tear
|
||||
// down the test. Expect it in order to satisfy the strict mock.
|
||||
EXPECT_CALL(*remoteConnections_[0], onDisconnect());
|
||||
}
|
||||
|
||||
protected:
|
||||
std::unique_ptr<ILocalConnection> toPage_;
|
||||
|
||||
MockRemoteConnection& fromPage() {
|
||||
return *remoteConnections_[0];
|
||||
}
|
||||
|
||||
private:
|
||||
PageTarget page_;
|
||||
UniquePtrFactory<StrictMock<MockRemoteConnection>> remoteConnections_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(PageTargetProtocolTest, UnrecognizedMethod) {
|
||||
EXPECT_CALL(
|
||||
fromPage(),
|
||||
onMessage(JsonParsed(AllOf(
|
||||
AtJsonPtr("/error/code", Eq(-32601)), AtJsonPtr("/id", Eq(1))))))
|
||||
.RetiresOnSaturation();
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": "SomeUnrecognizedMethod",
|
||||
"params": [1, 2]
|
||||
})");
|
||||
}
|
||||
|
||||
TEST_F(PageTargetProtocolTest, TypeErrorInMethodName) {
|
||||
EXPECT_CALL(
|
||||
fromPage(),
|
||||
onMessage(JsonParsed(AllOf(
|
||||
AtJsonPtr("/error/code", Eq(-32600)),
|
||||
AtJsonPtr("/id", Eq(nullptr))))))
|
||||
.RetiresOnSaturation();
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": 42,
|
||||
"params": [1, 2]
|
||||
})");
|
||||
}
|
||||
|
||||
TEST_F(PageTargetProtocolTest, MissingId) {
|
||||
EXPECT_CALL(
|
||||
fromPage(),
|
||||
onMessage(JsonParsed(AllOf(
|
||||
AtJsonPtr("/error/code", Eq(-32600)),
|
||||
AtJsonPtr("/id", Eq(nullptr))))))
|
||||
.RetiresOnSaturation();
|
||||
toPage_->sendMessage(R"({
|
||||
"method": "SomeUnrecognizedMethod",
|
||||
"params": [1, 2]
|
||||
})");
|
||||
}
|
||||
|
||||
TEST_F(PageTargetProtocolTest, MalformedJson) {
|
||||
EXPECT_CALL(
|
||||
fromPage(),
|
||||
onMessage(JsonParsed(AllOf(
|
||||
AtJsonPtr("/error/code", Eq(-32700)),
|
||||
AtJsonPtr("/id", Eq(nullptr))))))
|
||||
.RetiresOnSaturation();
|
||||
toPage_->sendMessage("{");
|
||||
}
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
Reference in New Issue
Block a user