Skip to content

Commit 7886abd

Browse files
motiz88facebook-github-bot
authored andcommitted
Add stub page target implementation to jsinspector-modern (#42397)
Summary: Pull Request resolved: #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
1 parent 965f2eb commit 7886abd

File tree

13 files changed

+503
-0
lines changed

13 files changed

+503
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# jsinspector-modern concepts
2+
3+
## CDP object model
4+
5+
### Target
6+
7+
A debuggable entity that a debugger frontend can connect to.
8+
9+
### Session
10+
11+
A single connection between a debugger frontend and a target. There can be multiple active sessions connected to the same target.
12+
13+
### Agent
14+
15+
A handler for a subset of CDP messages for a specific target as part of a specific session.

packages/react-native/ReactCommon/jsinspector-modern/InspectorInterfaces.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,11 @@ extern IInspector& getInspectorInstance();
129129
/// should only be used in tests.
130130
extern std::unique_ptr<IInspector> makeTestInspectorInstance();
131131

132+
/**
133+
* A callback that can be used to send debugger messages (method responses and
134+
* events) to the frontend. The message must be a JSON-encoded string.
135+
* The callback may be called from any thread.
136+
*/
137+
using FrontendChannel = std::function<void(std::string_view messageJson)>;
138+
132139
} // namespace facebook::react::jsinspector_modern
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "InspectorUtilities.h"
9+
10+
#include <cassert>
11+
12+
namespace facebook::react::jsinspector_modern {
13+
14+
CallbackLocalConnection::CallbackLocalConnection(
15+
std::function<void(std::string)> handler)
16+
: handler_(std::move(handler)) {}
17+
18+
void CallbackLocalConnection::sendMessage(std::string message) {
19+
assert(handler_ && "Handler has been disconnected");
20+
handler_(std::move(message));
21+
}
22+
23+
void CallbackLocalConnection::disconnect() {
24+
handler_ = nullptr;
25+
}
26+
27+
RAIIRemoteConnection::RAIIRemoteConnection(
28+
std::unique_ptr<IRemoteConnection> remote)
29+
: remote_(std::move(remote)) {}
30+
31+
void RAIIRemoteConnection::onMessage(std::string message) {
32+
remote_->onMessage(std::move(message));
33+
}
34+
35+
RAIIRemoteConnection::~RAIIRemoteConnection() {
36+
remote_->onDisconnect();
37+
}
38+
39+
} // namespace facebook::react::jsinspector_modern
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include "InspectorInterfaces.h"
11+
12+
// Utilities that are useful when integrating with InspectorInterfaces.h but
13+
// do not need to be exported.
14+
15+
namespace facebook::react::jsinspector_modern {
16+
17+
/**
18+
* Wraps a callback function in ILocalConnection.
19+
*/
20+
class CallbackLocalConnection : public ILocalConnection {
21+
public:
22+
/**
23+
* Creates a new Connection that uses the given callback to send messages to
24+
* the backend.
25+
*/
26+
explicit CallbackLocalConnection(std::function<void(std::string)> handler);
27+
28+
void sendMessage(std::string message) override;
29+
30+
void disconnect() override;
31+
32+
private:
33+
std::function<void(std::string)> handler_;
34+
};
35+
36+
/**
37+
* Wraps an IRemoteConnection in a simpler interface that calls `onDisconnect`
38+
* implicitly upon destruction.
39+
*/
40+
class RAIIRemoteConnection {
41+
public:
42+
explicit RAIIRemoteConnection(std::unique_ptr<IRemoteConnection> remote);
43+
44+
void onMessage(std::string message);
45+
46+
~RAIIRemoteConnection();
47+
48+
private:
49+
std::unique_ptr<IRemoteConnection> remote_;
50+
};
51+
52+
} // namespace facebook::react::jsinspector_modern
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <folly/dynamic.h>
9+
#include <folly/json.h>
10+
#include <jsinspector-modern/PageAgent.h>
11+
12+
namespace facebook::react::jsinspector_modern {
13+
14+
PageAgent::PageAgent(FrontendChannel frontendChannel)
15+
: frontendChannel_(frontendChannel) {}
16+
17+
void PageAgent::handleRequest(const cdp::PreparsedRequest& req) {
18+
folly::dynamic res = folly::dynamic::object("id", req.id)(
19+
"error",
20+
folly::dynamic::object("code", -32601)(
21+
"message", req.method + " not implemented yet"));
22+
std::string json = folly::toJson(res);
23+
frontendChannel_(json);
24+
}
25+
26+
} // namespace facebook::react::jsinspector_modern
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <jsinspector-modern/InspectorInterfaces.h>
11+
#include <jsinspector-modern/Parsing.h>
12+
#include <functional>
13+
14+
namespace facebook::react::jsinspector_modern {
15+
16+
/**
17+
* An Agent that handles requests from the Chrome DevTools Protocol for the
18+
* given page.
19+
* The constructor, destructor and all public methods must be called on the
20+
* same thread.
21+
*/
22+
class PageAgent {
23+
public:
24+
/**
25+
* \param frontendChannel A channel used to send responses and events to the
26+
* frontend.
27+
*/
28+
explicit PageAgent(FrontendChannel frontendChannel);
29+
30+
/**
31+
* Handle a CDP request. The response will be sent over the provided
32+
* \c FrontendChannel synchronously or asynchronously.
33+
* \param req The parsed request.
34+
*/
35+
void handleRequest(const cdp::PreparsedRequest& req);
36+
37+
private:
38+
FrontendChannel frontendChannel_;
39+
};
40+
41+
} // namespace facebook::react::jsinspector_modern
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "PageTarget.h"
9+
#include "InspectorUtilities.h"
10+
#include "PageAgent.h"
11+
#include "Parsing.h"
12+
13+
#include <folly/dynamic.h>
14+
#include <folly/json.h>
15+
16+
#include <memory>
17+
18+
namespace facebook::react::jsinspector_modern {
19+
20+
namespace {
21+
22+
/**
23+
* A Session connected to a PageTarget, passing CDP messages to and from a
24+
* PageAgent which it owns.
25+
*/
26+
class PageTargetSession {
27+
public:
28+
explicit PageTargetSession(std::unique_ptr<IRemoteConnection> remote)
29+
: remote_(std::make_shared<RAIIRemoteConnection>(std::move(remote))),
30+
frontendChannel_(
31+
[remoteWeak = std::weak_ptr(remote_)](std::string_view message) {
32+
if (auto remote = remoteWeak.lock()) {
33+
remote->onMessage(std::string(message));
34+
}
35+
}),
36+
pageAgent_(frontendChannel_) {}
37+
/**
38+
* Called by CallbackLocalConnection to send a message to this Session's
39+
* Agent.
40+
*/
41+
void operator()(std::string message) {
42+
cdp::PreparsedRequest request;
43+
// Messages may be invalid JSON, or have unexpected types.
44+
try {
45+
request = cdp::preparse(message);
46+
} catch (const cdp::ParseError& e) {
47+
frontendChannel_(folly::toJson(folly::dynamic::object("id", nullptr)(
48+
"error",
49+
folly::dynamic::object("code", -32700)("message", e.what()))));
50+
return;
51+
} catch (const cdp::TypeError& e) {
52+
frontendChannel_(folly::toJson(folly::dynamic::object("id", nullptr)(
53+
"error",
54+
folly::dynamic::object("code", -32600)("message", e.what()))));
55+
return;
56+
}
57+
58+
// Catch exceptions that may arise from accessing dynamic params during
59+
// request handling.
60+
try {
61+
pageAgent_.handleRequest(request);
62+
} catch (const cdp::TypeError& e) {
63+
frontendChannel_(folly::toJson(folly::dynamic::object("id", request.id)(
64+
"error",
65+
folly::dynamic::object("code", -32600)("message", e.what()))));
66+
return;
67+
}
68+
}
69+
70+
private:
71+
// Owned by this instance, but shared (weakly) with the frontend channel
72+
std::shared_ptr<RAIIRemoteConnection> remote_;
73+
FrontendChannel frontendChannel_;
74+
PageAgent pageAgent_;
75+
};
76+
77+
} // namespace
78+
79+
std::unique_ptr<ILocalConnection> PageTarget::connect(
80+
std::unique_ptr<IRemoteConnection> connectionToFrontend) {
81+
return std::make_unique<CallbackLocalConnection>(
82+
PageTargetSession(std::move(connectionToFrontend)));
83+
}
84+
85+
} // namespace facebook::react::jsinspector_modern
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <jsinspector-modern/InspectorInterfaces.h>
11+
12+
namespace facebook::react::jsinspector_modern {
13+
14+
/**
15+
* The top-level Target in a React Native app. This is equivalent to the
16+
* "Host" in React Native's architecture - the entity that manages the
17+
* lifecycle of a React Instance.
18+
*/
19+
class PageTarget {
20+
public:
21+
/**
22+
* Creates a new Session connected to this PageTarget, wrapped in an
23+
* interface which is compatible with \c IInspector::addPage.
24+
* The caller is responsible for destroying the connection before PageTarget
25+
* is destroyed, on the same thread where PageTarget's constructor and
26+
* destructor execute.
27+
*/
28+
std::unique_ptr<ILocalConnection> connect(
29+
std::unique_ptr<IRemoteConnection> connectionToFrontend);
30+
};
31+
32+
} // namespace facebook::react::jsinspector_modern
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <folly/dynamic.h>
9+
#include <folly/json.h>
10+
#include <jsinspector-modern/Parsing.h>
11+
12+
namespace facebook::react::jsinspector_modern::cdp {
13+
14+
PreparsedRequest preparse(std::string_view message) {
15+
folly::dynamic parsed = folly::parseJson(message);
16+
return PreparsedRequest{
17+
.id = parsed["id"].getInt(),
18+
.method = parsed["method"].getString(),
19+
.params = parsed.count("params") ? parsed["params"] : nullptr};
20+
}
21+
22+
} // namespace facebook::react::jsinspector_modern::cdp
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <folly/dynamic.h>
11+
#include <folly/json.h>
12+
#include <string>
13+
#include <string_view>
14+
15+
namespace facebook::react::jsinspector_modern {
16+
17+
namespace cdp {
18+
using RequestId = long long;
19+
20+
/**
21+
* An incoming CDP request that has been parsed into a more usable form.
22+
*/
23+
struct PreparsedRequest {
24+
public:
25+
/**
26+
* The ID of the request.
27+
*/
28+
RequestId id;
29+
30+
/**
31+
* The name of the method being invoked.
32+
*/
33+
std::string method;
34+
35+
/**
36+
* The parameters passed to the method, if any.
37+
*/
38+
folly::dynamic params;
39+
};
40+
41+
/**
42+
* Parse a JSON-encoded CDP request into its constituent parts.
43+
* \throws ParseError If the input cannot be parsed.
44+
* \throws TypeError If the input does not conform to the expected format.
45+
*/
46+
PreparsedRequest preparse(std::string_view message);
47+
48+
/**
49+
* A type error that may be thrown while preparsing a request, or while
50+
* accessing dynamic params on a request.
51+
*/
52+
using TypeError = folly::TypeError;
53+
54+
/**
55+
* A parse error that may be thrown while preparsing a request.
56+
*/
57+
using ParseError = folly::json::parse_error;
58+
} // namespace cdp
59+
60+
} // namespace facebook::react::jsinspector_modern

0 commit comments

Comments
 (0)