Skip to content

Commit 3271e57

Browse files
motiz88facebook-github-bot
authored andcommitted
Handle Runtime.addBinding and Runtime.removeBinding before a RuntimeAgent is present (facebook#52813)
Summary: Pull Request resolved: facebook#52813 Changelog: [General][Changed] CDP backend now accepts addBinding and removeBinding methods earlier, before a Runtime exists. Allows managing CDP binding subscriptions at any point in time, including at the very beginning of a session when a `RuntimeAgent` doesn't yet exist (due to `registerRuntime` not having been called yet). "Adding" a binding with the `Runtime.addBinding` method is actually two actions in one: 1. Subscribing the current session to events from a particular binding name, from one or more Runtime(s) that may exist now or in the future (optionally targetable by execution context ID/name). 2. Installing a JSI function with the given `name` on the global object of the targeted Runtime(s), if they exist at the time of the method call. NOTE: "Removing" a binding only involves managing the subscription and never causes the corresponding JSI function to be uninstalled - presumably because it may have been captured by user code anyway. We currently do both (1) and (2) in `RuntimeAgent`, but this means subscriptions can't be managed before a `RuntimeTarget` has been created, which can cause problems if we want to set up certain bindings programmatically during React Native's initialisation. As this is an unnecessary restriction, here we move (1) to `HostAgent` and keep only (2) in `RuntimeAgent`. Reviewed By: huntie Differential Revision: D78739067 fbshipit-source-id: 9a39f4503d1b5e7c7a6e4c80dfbaabdd2549fb8d
1 parent dbdf39a commit 3271e57

File tree

3 files changed

+131
-49
lines changed

3 files changed

+131
-49
lines changed

packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,11 @@ class HostAgent::Impl final {
141141
.shouldSendOKResponse = true,
142142
};
143143
}
144-
// Methods other than domain enables/disables: handle anything we know how
145-
// to handle, and delegate to the InstanceAgent otherwise.
146144

145+
// Methods other than domain enables/disables: handle anything we know how
146+
// to handle, and delegate to the InstanceAgent otherwise. (In some special
147+
// cases we may handle the request *and* delegate to the InstanceAgent for
148+
// some side effect.)
147149
if (req.method == "Page.reload") {
148150
targetController_.getDelegate().onReload({
149151
.ignoreCache =
@@ -207,6 +209,72 @@ class HostAgent::Impl final {
207209
.shouldSendOKResponse = true,
208210
};
209211
}
212+
if (req.method == "Runtime.addBinding") {
213+
// @cdp Runtime.addBinding and @cdp Runtime.removeBinding are explicitly
214+
// supported at any time during a session, even while the JS runtime
215+
// hasn't been created yet. For this reason they are handled by the
216+
// HostAgent.
217+
std::string bindingName = req.params["name"].getString();
218+
219+
ExecutionContextSelector contextSelector =
220+
ExecutionContextSelector::all();
221+
222+
if (req.params.count("executionContextId") != 0u) {
223+
auto executionContextId = req.params["executionContextId"].getInt();
224+
if (executionContextId < (int64_t)std::numeric_limits<int32_t>::min() ||
225+
executionContextId > (int64_t)std::numeric_limits<int32_t>::max()) {
226+
frontendChannel_(cdp::jsonError(
227+
req.id,
228+
cdp::ErrorCode::InvalidParams,
229+
"Invalid execution context id"));
230+
return {
231+
.isFinishedHandlingRequest = true,
232+
.shouldSendOKResponse = false,
233+
};
234+
}
235+
contextSelector =
236+
ExecutionContextSelector::byId((int32_t)executionContextId);
237+
238+
if (req.params.count("executionContextName") != 0u) {
239+
frontendChannel_(cdp::jsonError(
240+
req.id,
241+
cdp::ErrorCode::InvalidParams,
242+
"executionContextName is mutually exclusive with executionContextId"));
243+
return {
244+
.isFinishedHandlingRequest = true,
245+
.shouldSendOKResponse = false,
246+
};
247+
}
248+
} else if (req.params.count("executionContextName") != 0u) {
249+
contextSelector = ExecutionContextSelector::byName(
250+
req.params["executionContextName"].getString());
251+
}
252+
253+
sessionState_.subscribedBindings[bindingName].insert(contextSelector);
254+
255+
// We need this request to percolate down to the RuntimeAgent via the
256+
// InstanceAgent. If there isn't a RuntimeAgent, it's OK: the next
257+
// RuntimeAgent will pick up the binding via session state.
258+
return {
259+
.isFinishedHandlingRequest = false,
260+
.shouldSendOKResponse = true,
261+
};
262+
}
263+
if (req.method == "Runtime.removeBinding") {
264+
// @cdp Runtime.removeBinding has no targeting by execution context. We
265+
// interpret it to mean "unsubscribe, and stop installing the binding on
266+
// all new contexts". This diverges slightly from V8, which continues
267+
// to install the binding on new contexts after it's "removed", but *only*
268+
// if the subscription is targeted by context name.
269+
sessionState_.subscribedBindings.erase(req.params["name"].getString());
270+
271+
// Because of the above, we don't need to pass this request down to the
272+
// RuntimeAgent.
273+
return {
274+
.isFinishedHandlingRequest = true,
275+
.shouldSendOKResponse = true,
276+
};
277+
}
210278

211279
return {
212280
.isFinishedHandlingRequest = false,

packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp

Lines changed: 12 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -39,56 +39,21 @@ bool RuntimeAgent::handleRequest(const cdp::PreparsedRequest& req) {
3939
if (req.method == "Runtime.addBinding") {
4040
std::string bindingName = req.params["name"].getString();
4141

42-
ExecutionContextSelector contextSelector = ExecutionContextSelector::all();
43-
44-
// TODO: Eventually, move execution context targeting out of RuntimeAgent.
45-
// Right now, there's only ever one context (Runtime) in a Host, so we can
46-
// handle it here for simplicity, and use session state to propagate
47-
// bindings to the next RuntimeAgent.
48-
if (req.params.count("executionContextId") != 0u) {
49-
auto executionContextId = req.params["executionContextId"].getInt();
50-
if (executionContextId < (int64_t)std::numeric_limits<int32_t>::min() ||
51-
executionContextId > (int64_t)std::numeric_limits<int32_t>::max()) {
52-
frontendChannel_(cdp::jsonError(
53-
req.id,
54-
cdp::ErrorCode::InvalidParams,
55-
"Invalid execution context id"));
56-
return true;
42+
// Check if the binding has a context selector that matches the current
43+
// context. The state will have been updated by HostAgent by the time we
44+
// receive the request.
45+
// NOTE: We DON'T do the reverse for removeBinding, for reasons explained
46+
// in the implementation of HostAgent.
47+
auto it = sessionState_.subscribedBindings.find(bindingName);
48+
if (it != sessionState_.subscribedBindings.end()) {
49+
auto contextSelectors = it->second;
50+
if (matchesAny(executionContextDescription_, contextSelectors)) {
51+
targetController_.installBindingHandler(bindingName);
5752
}
58-
contextSelector =
59-
ExecutionContextSelector::byId((int32_t)executionContextId);
60-
61-
if (req.params.count("executionContextName") != 0u) {
62-
frontendChannel_(cdp::jsonError(
63-
req.id,
64-
cdp::ErrorCode::InvalidParams,
65-
"executionContextName is mutually exclusive with executionContextId"));
66-
return true;
67-
}
68-
} else if (req.params.count("executionContextName") != 0u) {
69-
contextSelector = ExecutionContextSelector::byName(
70-
req.params["executionContextName"].getString());
71-
}
72-
if (contextSelector.matches(executionContextDescription_)) {
73-
targetController_.installBindingHandler(bindingName);
7453
}
75-
sessionState_.subscribedBindings[bindingName].insert(contextSelector);
76-
77-
frontendChannel_(cdp::jsonResult(req.id));
78-
79-
return true;
80-
}
81-
if (req.method == "Runtime.removeBinding") {
82-
// @cdp Runtime.removeBinding has no targeting by execution context. We
83-
// interpret it to mean "unsubscribe, and stop installing the binding on
84-
// all new contexts". This diverges slightly from V8, which continues
85-
// to install the binding on new contexts after it's "removed", but *only*
86-
// if the subscription is targeted by context name.
87-
sessionState_.subscribedBindings.erase(req.params["name"].getString());
88-
89-
frontendChannel_(cdp::jsonResult(req.id));
9054

91-
return true;
55+
// We are not responding to this request, just processing a side effect.
56+
return false;
9257
}
9358
if (req.method == "Runtime.enable" && sessionState_.isLogDomainEnabled) {
9459
targetController_.notifyDebuggerSessionCreated();

packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,55 @@ TYPED_TEST(JsiIntegrationPortableTest, ReactNativeApplicationDisable) {
385385
})");
386386
}
387387

388+
#pragma region JsiIntegrationPortableTestWithEarlyAddedBinding
389+
// A test with some special setup to cover an edge case in the CDP backend:
390+
// bindings can be added before there's even a Runtime in the session, and
391+
// they should still work seamlessly once there is one.
392+
393+
TYPED_TEST_SUITE(JsiIntegrationPortableTestWithEarlyAddedBinding, AllEngines);
394+
395+
template <typename EngineAdapter>
396+
class JsiIntegrationPortableTestWithEarlyAddedBinding
397+
: public JsiIntegrationPortableTest<EngineAdapter> {
398+
void setupRuntimeBeforeRegistration(jsi::Runtime& /*unused*/) override {
399+
// Connect early, before the RuntimeTarget is created. This session will not
400+
// have a RuntimeAgent yet.
401+
this->connect();
402+
403+
// Add a binding.
404+
this->expectMessageFromPage(JsonEq(R"({
405+
"id": 1,
406+
"result": {}
407+
})"));
408+
this->toPage_->sendMessage(R"({
409+
"id": 1,
410+
"method": "Runtime.addBinding",
411+
"params": {"name": "foo"}
412+
})");
413+
414+
// The binding is not installed yet, because the runtime is not registered
415+
// with the CDP backend yet.
416+
EXPECT_TRUE(this->eval("typeof globalThis.foo === 'undefined'").getBool());
417+
}
418+
};
419+
420+
TYPED_TEST(
421+
JsiIntegrationPortableTestWithEarlyAddedBinding,
422+
AddBindingBeforeRuntimeRegistered) {
423+
// Now there is a RuntimeTarget / RuntimeAgent in the session, as per the
424+
// normal test setup. Note that we're already connect()ed - not repeating that
425+
// here.
426+
427+
// The binding is now installed, callable and working.
428+
this->expectMessageFromPage(JsonParsed(AllOf(
429+
AtJsonPtr("/method", "Runtime.bindingCalled"),
430+
AtJsonPtr("/params/name", "foo"),
431+
AtJsonPtr("/params/payload", "bar"))));
432+
this->eval("globalThis.foo('bar');");
433+
}
434+
435+
#pragma endregion // JsiIntegrationPortableTestWithEarlyAddedBinding
436+
388437
#pragma endregion // AllEngines
389438
#pragma region AllHermesVariants
390439

0 commit comments

Comments
 (0)