Add boilerplate for handling Overlay.setPausedInDebuggerMessage (#44078)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/44078

Changelog: [Internal]

Adds stub support for the [`Overlay.setPausedInDebuggerMessage`](https://cdpstatus.reactnative.dev/devtools-protocol/tot/Overlay#method-setPausedInDebuggerMessage) CDP method to `HostAgent` in the Fusebox backend, and propagates it into the Android and iOS integrations through `HostTargetDelegate`.

We take care to call `HostTargetDelegate::onSetPausedInDebuggerMessage()` a final time with a null `message` parameter, regardless of whether the client has actually sent the corresponding CDP message. Since multiple clients might be connected concurrently, we only send the `null` message when the *last* client which has requested a non-null message has disconnected.

Reviewed By: robhogan

Differential Revision: D56068444

fbshipit-source-id: c26e1cf17dec8d7dbb7edd5ab7fa3133642628ff
This commit is contained in:
Moti Zilberman
2024-04-16 04:52:13 -07:00
committed by Facebook GitHub Bot
parent d181f06b58
commit 8afa8beac2
17 changed files with 277 additions and 3 deletions
@@ -199,6 +199,11 @@ class RCTBridgeHostTargetDelegate : public facebook::react::jsinspector_modern::
[bridge_ reload];
}
void onSetPausedInDebuggerMessage(const OverlaySetPausedInDebuggerMessageRequest &) override
{
// TODO(moti): Implement this
}
private:
__weak RCTBridge *bridge_;
};
@@ -1175,6 +1175,7 @@ public class com/facebook/react/bridge/ReactInstanceManagerInspectorTarget : jav
public abstract interface class com/facebook/react/bridge/ReactInstanceManagerInspectorTarget$TargetDelegate {
public abstract fun onReload ()V
public abstract fun onSetPausedInDebuggerMessage (Ljava/lang/String;)V
}
public class com/facebook/react/bridge/ReactMarker {
@@ -1494,6 +1494,11 @@ public class ReactInstanceManager {
public void onReload() {
UiThreadUtil.runOnUiThread(() -> mDevSupportManager.handleReloadJS());
}
@Override
public void onSetPausedInDebuggerMessage(@Nullable String message) {
// TODO(moti): Implement this
}
});
}
@@ -10,11 +10,14 @@ package com.facebook.react.bridge;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStripAny;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
@DoNotStripAny
public class ReactInstanceManagerInspectorTarget implements AutoCloseable {
public interface TargetDelegate {
public void onReload();
public void onSetPausedInDebuggerMessage(@Nullable String message);
}
private final HybridData mHybridData;
@@ -26,6 +26,7 @@ import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.infer.annotation.ThreadConfined;
import com.facebook.infer.annotation.ThreadSafe;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.JSEngineResolutionAlgorithm;
import com.facebook.react.MemoryPressureRouter;
import com.facebook.react.ReactHost;
@@ -466,6 +467,11 @@ public class ReactHostImpl implements ReactHost {
.continueWithTask(Task::getResult);
}
@DoNotStrip
private void setPausedInDebuggerMessage(@Nullable String message) {
// TODO(moti): Implement this
}
/**
* Entrypoint to destroy the ReactInstance. If the ReactInstance is reloading, will wait until
* reload is finished, before destroying.
@@ -20,6 +20,14 @@ void ReactInstanceManagerInspectorTarget::TargetDelegate::onReload() const {
method(self());
}
void ReactInstanceManagerInspectorTarget::TargetDelegate::
onSetPausedInDebuggerMessage(
const OverlaySetPausedInDebuggerMessageRequest& request) const {
auto method = javaClassStatic()->getMethod<void(local_ref<JString>)>(
"onSetPausedInDebuggerMessage");
method(self(), request.message ? make_jstring(*request.message) : nullptr);
}
ReactInstanceManagerInspectorTarget::ReactInstanceManagerInspectorTarget(
jni::alias_ref<ReactInstanceManagerInspectorTarget::jhybridobject> jobj,
jni::alias_ref<JExecutor::javaobject> executor,
@@ -82,6 +90,11 @@ void ReactInstanceManagerInspectorTarget::onReload(
delegate_->onReload();
}
void ReactInstanceManagerInspectorTarget::onSetPausedInDebuggerMessage(
const OverlaySetPausedInDebuggerMessageRequest& request) {
delegate_->onSetPausedInDebuggerMessage(request);
}
HostTarget* ReactInstanceManagerInspectorTarget::getInspectorTarget() {
return inspectorTarget_.get();
}
@@ -22,6 +22,8 @@ class ReactInstanceManagerInspectorTarget
"Lcom/facebook/react/bridge/ReactInstanceManagerInspectorTarget$TargetDelegate;";
void onReload() const;
void onSetPausedInDebuggerMessage(
const OverlaySetPausedInDebuggerMessageRequest& request) const;
};
public:
@@ -44,9 +46,13 @@ class ReactInstanceManagerInspectorTarget
static void registerNatives();
void onReload(const PageReloadRequest& request) override;
jsinspector_modern::HostTarget* getInspectorTarget();
// HostTargetDelegate methods
void onReload(const PageReloadRequest& request) override;
void onSetPausedInDebuggerMessage(
const OverlaySetPausedInDebuggerMessageRequest&) override;
private:
friend HybridBase;
@@ -73,6 +73,11 @@ void JReactHostInspectorTarget::onReload(const PageReloadRequest& request) {
javaReactHostImpl_->reload("CDP Page.reload");
}
void JReactHostInspectorTarget::onSetPausedInDebuggerMessage(
const OverlaySetPausedInDebuggerMessageRequest& request) {
javaReactHostImpl_->setPausedInDebuggerMessage(request.message);
}
HostTarget* JReactHostInspectorTarget::getInspectorTarget() {
return inspectorTarget_ ? inspectorTarget_.get() : nullptr;
}
@@ -29,6 +29,13 @@ struct JReactHostImpl : public jni::JavaClass<JReactHostImpl> {
"reload");
return method(self(), reason);
}
void setPausedInDebuggerMessage(std::optional<std::string> message) {
static auto method =
javaClassStatic()->getMethod<void(jni::local_ref<jni::JString>)>(
"setPausedInDebuggerMessage");
method(self(), message ? jni::make_jstring(*message) : nullptr);
}
};
class JReactHostInspectorTarget
@@ -48,6 +55,8 @@ class JReactHostInspectorTarget
static void registerNatives();
void onReload(const PageReloadRequest& request) override;
void onSetPausedInDebuggerMessage(
const OverlaySetPausedInDebuggerMessageRequest&) override;
jsinspector_modern::HostTarget* getInspectorTarget();
@@ -105,6 +105,22 @@ void HostAgent::handleRequest(const cdp::PreparsedRequest& req) {
: std::nullopt,
});
shouldSendOKResponse = true;
isFinishedHandlingRequest = true;
} else if (req.method == "Overlay.setPausedInDebuggerMessage") {
auto message = req.params.isObject() && req.params.count("message")
? std::optional(req.params.at("message").asString())
: std::nullopt;
if (!isPausedInDebuggerOverlayVisible_ && message.has_value()) {
targetController_.incrementPauseOverlayCounter();
} else if (isPausedInDebuggerOverlayVisible_ && !message.has_value()) {
targetController_.decrementPauseOverlayCounter();
}
isPausedInDebuggerOverlayVisible_ = message.has_value();
targetController_.getDelegate().onSetPausedInDebuggerMessage({
.message = message,
});
shouldSendOKResponse = true;
isFinishedHandlingRequest = true;
} else if (req.method == "FuseboxClient.setClientMetadata") {
@@ -154,6 +170,20 @@ void HostAgent::handleRequest(const cdp::PreparsedRequest& req) {
req.method + " not implemented yet"));
}
HostAgent::~HostAgent() {
if (isPausedInDebuggerOverlayVisible_) {
// In case of a non-graceful shutdown of the session, ensure we clean up
// the "paused on debugger" overlay if we've previously asked the
// integrator to display it.
isPausedInDebuggerOverlayVisible_ = false;
if (!targetController_.decrementPauseOverlayCounter()) {
targetController_.getDelegate().onSetPausedInDebuggerMessage({
.message = std::nullopt,
});
}
}
}
void HostAgent::sendFuseboxNotice() {
static constexpr auto kFuseboxNotice = ANSI_COLOR_BG_YELLOW
"Welcome to the new React Native debugger (codename " ANSI_WEIGHT_BOLD
@@ -46,6 +46,13 @@ class HostAgent final {
HostTarget::SessionMetadata sessionMetadata,
SessionState& sessionState);
HostAgent(const HostAgent&) = delete;
HostAgent(HostAgent&&) = delete;
HostAgent& operator=(const HostAgent&) = delete;
HostAgent& operator=(HostAgent&&) = delete;
~HostAgent();
/**
* Handle a CDP request. The response will be sent over the provided
* \c FrontendChannel synchronously or asynchronously.
@@ -90,6 +97,7 @@ class HostAgent final {
const HostTarget::SessionMetadata sessionMetadata_;
std::shared_ptr<InstanceAgent> instanceAgent_;
FuseboxClientType fuseboxClientType_{FuseboxClientType::Unknown};
bool isPausedInDebuggerOverlayVisible_{false};
/**
* A shared reference to the session's state. This is only safe to access
@@ -162,4 +162,16 @@ bool HostTargetController::hasInstance() const {
return target_.hasInstance();
}
void HostTargetController::incrementPauseOverlayCounter() {
++pauseOverlayCounter_;
}
bool HostTargetController::decrementPauseOverlayCounter() {
assert(pauseOverlayCounter_ > 0 && "Pause overlay counter underflow");
if (--pauseOverlayCounter_ == 0) {
return false;
}
return true;
}
} // namespace facebook::react::jsinspector_modern
@@ -67,6 +67,21 @@ class HostTargetDelegate {
}
};
struct OverlaySetPausedInDebuggerMessageRequest {
/**
* The message to display in the overlay. If nullopt, hide the overlay.
*/
std::optional<std::string> message;
/**
* Equality operator, useful for unit tests
*/
inline bool operator==(
const OverlaySetPausedInDebuggerMessageRequest& rhs) const {
return message == rhs.message;
}
};
virtual ~HostTargetDelegate();
/**
@@ -75,6 +90,19 @@ class HostTargetDelegate {
* ILocalConnection::sendMessage was called).
*/
virtual void onReload(const PageReloadRequest& request) = 0;
/**
* Called when the debugger requests that the "paused in debugger" overlay be
* shown or hidden. If the message is nullopt, hide the overlay, otherwise
* show it with the given message. This is called on the inspector thread.
*
* If this method is called with a non-null message, it's guaranteed to
* eventually be called again with a null message. In all other respects,
* the timing and payload of these messages are fully controlled by the
* client.
*/
virtual void onSetPausedInDebuggerMessage(
const OverlaySetPausedInDebuggerMessageRequest& request) = 0;
};
/**
@@ -89,8 +117,28 @@ class HostTargetController final {
bool hasInstance() const;
/**
* Increments the target's pause overlay counter. The counter represents the
* exact number of Agents that have (concurrently) requested the pause
* overlay to be shown. It's the caller's responsibility to only call this
* when the pause overlay's requested state transitions from hidden to
* visible.
*/
void incrementPauseOverlayCounter();
/**
* Decrements the target's pause overlay counter. The counter represents the
* exact number of Agents that have (concurrently) requested the pause
* overlay to be shown. It's the caller's responsibility to only call this
* when the pause overlay's requested state transitions from hidden to
* visible.
* \returns false if the counter has reached 0, otherwise true.
*/
bool decrementPauseOverlayCounter();
private:
HostTarget& target_;
size_t pauseOverlayCounter_{0};
};
/**
@@ -42,13 +42,21 @@ class HostTargetTest : public Test {
void connect() {
ASSERT_FALSE(toPage_) << "Can only connect once in a HostTargetTest.";
toPage_ = page_->connect(
auto conn = makeConnection();
toPage_ = std::move(conn.first);
}
std::pair<std::unique_ptr<ILocalConnection>, MockRemoteConnection&>
makeConnection() {
size_t connectionIndex = remoteConnections_.objectsVended();
auto toPage = page_->connect(
remoteConnections_.make_unique(),
{.integrationName = "HostTargetTest"});
// 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());
EXPECT_CALL(*remoteConnections_[connectionIndex], onDisconnect());
return {std::move(toPage), *remoteConnections_[connectionIndex]};
}
MockHostTargetDelegate hostTargetDelegate_;
@@ -206,6 +214,108 @@ TEST_F(HostTargetProtocolTest, PageReloadMethod) {
})");
}
TEST_F(HostTargetProtocolTest, OverlaySetPausedInDebuggerMessageMethod) {
InSequence s;
EXPECT_CALL(
hostTargetDelegate_,
onSetPausedInDebuggerMessage(
Eq(HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest{
.message = std::nullopt})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Overlay.setPausedInDebuggerMessage"
})");
EXPECT_CALL(
hostTargetDelegate_,
onSetPausedInDebuggerMessage(
Eq(HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest{
.message = "Paused in debugger"})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 2,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 2,
"method": "Overlay.setPausedInDebuggerMessage",
"params": {
"message": "Paused in debugger"
}
})");
// A cleanup message is sent automatically when we destroy the session.
EXPECT_CALL(
hostTargetDelegate_,
onSetPausedInDebuggerMessage(
Eq(HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest{
.message = std::nullopt})))
.RetiresOnSaturation();
}
TEST_F(HostTargetProtocolTest, OverlaySetPausedInDebuggerMultipleClients) {
auto [toPage2, fromPage2] = makeConnection();
InSequence s;
EXPECT_CALL(
hostTargetDelegate_,
onSetPausedInDebuggerMessage(
Eq(HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest{
.message = "Paused in debugger - client 1"})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage_->sendMessage(R"({
"id": 1,
"method": "Overlay.setPausedInDebuggerMessage",
"params": {
"message": "Paused in debugger - client 1"
}
})");
EXPECT_CALL(
hostTargetDelegate_,
onSetPausedInDebuggerMessage(
Eq(HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest{
.message = "Paused in debugger - client 2"})))
.RetiresOnSaturation();
EXPECT_CALL(fromPage2, onMessage(JsonEq(R"({
"id": 1,
"result": {}
})")))
.RetiresOnSaturation();
toPage2->sendMessage(R"({
"id": 1,
"method": "Overlay.setPausedInDebuggerMessage",
"params": {
"message": "Paused in debugger - client 2"
}
})");
toPage2.reset();
// The cleanup message is sent exactly once.
EXPECT_CALL(
hostTargetDelegate_,
onSetPausedInDebuggerMessage(
Eq(HostTargetDelegate::OverlaySetPausedInDebuggerMessageRequest{
.message = std::nullopt})))
.Times(1)
.RetiresOnSaturation();
}
TEST_F(HostTargetProtocolTest, RegisterUnregisterInstanceWithoutEvents) {
auto& instanceTarget = page_->registerInstance(instanceTargetDelegate_);
@@ -119,6 +119,11 @@ class MockHostTargetDelegate : public HostTargetDelegate {
public:
// HostTargetDelegate methods
MOCK_METHOD(void, onReload, (const PageReloadRequest& request), (override));
MOCK_METHOD(
void,
onSetPausedInDebuggerMessage,
(const OverlaySetPausedInDebuggerMessageRequest& request),
(override));
};
class MockInstanceTargetDelegate : public InstanceTargetDelegate {};
@@ -183,6 +183,9 @@ class JsiIntegrationPortableTest : public ::testing::Test,
(void)request;
reload();
}
void onSetPausedInDebuggerMessage(
const OverlaySetPausedInDebuggerMessageRequest&) override {}
};
} // namespace facebook::react::jsinspector_modern
@@ -38,6 +38,11 @@ class RCTHostHostTargetDelegate : public facebook::react::jsinspector_modern::Ho
[static_cast<id<RCTReloadListener>>(host_) didReceiveReloadCommand];
}
void onSetPausedInDebuggerMessage(const OverlaySetPausedInDebuggerMessageRequest &) override
{
// TODO(moti): Implement this
}
private:
__weak RCTHost *host_;
};