Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,13 @@ class Stream : public NetworkRequestListener,
};
} // namespace

NetworkIOAgent::~NetworkIOAgent() {
if (networkAgentId_) {
NetworkHandler::getInstance().disableAgent(*networkAgentId_);
networkAgentId_ = std::nullopt;
}
}

bool NetworkIOAgent::handleRequest(
const cdp::PreparsedRequest& req,
LoadNetworkResourceDelegate& delegate) {
Expand All @@ -278,15 +285,17 @@ bool NetworkIOAgent::handleRequest(

// @cdp Network.enable support is experimental.
if (req.method == "Network.enable") {
networkHandler.setFrontendChannel(frontendChannel_);
networkHandler.enable();
networkAgentId_ = networkHandler.enableAgent(frontendChannel_);
// NOTE: Domain enable/disable responses are sent by HostAgent.
return false;
}

// @cdp Network.disable support is experimental.
if (req.method == "Network.disable") {
networkHandler.disable();
if (networkAgentId_) {
networkHandler.disableAgent(*networkAgentId_);
networkAgentId_ = std::nullopt;
}
// NOTE: Domain enable/disable responses are sent by HostAgent.
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,13 @@ class NetworkIOAgent {
{
}

~NetworkIOAgent();

NetworkIOAgent(const NetworkIOAgent &) = delete;
NetworkIOAgent &operator=(const NetworkIOAgent &) = delete;
NetworkIOAgent(NetworkIOAgent &&) = delete;
NetworkIOAgent &operator=(NetworkIOAgent &&) = delete;

/**
* Handle a CDP request. The response will be sent over the provided
* \c FrontendChannel synchronously or asynchronously.
Expand Down Expand Up @@ -247,6 +254,12 @@ class NetworkIOAgent {
*/
unsigned long nextStreamId_{0};

/**
* If non-nullopt, indicates that this agent has enabled the Network domain
* via NetworkHandler, storing the agent ID for cleanup.
*/
std::optional<size_t> networkAgentId_;

/**
* Begin loading an HTTP resource, delegating platform-specific
* implementation, responding to the frontend on headers received or on error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ bool TracingAgent::handleRequest(const cdp::PreparsedRequest& req) {
bool didNotHaveAlreadyRunningRecording = hostTargetController_.startTracing(
tracing::Mode::CDP, std::move(enabledCategories));
if (!didNotHaveAlreadyRunningRecording) {
// @cdp Tracing.start fails if there is a tracing session already running
// in the current target. This matches Chrome's behavior.
frontendChannel_(
cdp::jsonError(
req.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,31 @@ NetworkHandler& NetworkHandler::getInstance() {
return instance;
}

void NetworkHandler::setFrontendChannel(FrontendChannel frontendChannel) {
frontendChannel_ = std::move(frontendChannel);
size_t NetworkHandler::enableAgent(FrontendChannel frontendChannel) {
std::lock_guard<std::mutex> lock(agentsMutex_);
size_t id = nextAgentId_++;
agents_.push_back({id, std::move(frontendChannel)});
enabled_.store(true, std::memory_order_release);
return id;
}

bool NetworkHandler::enable() {
if (enabled_.load(std::memory_order_acquire)) {
return false;
}
void NetworkHandler::disableAgent(size_t agentId) {
std::lock_guard<std::mutex> lock(agentsMutex_);
agents_.remove_if(
[agentId](const AgentRecord& r) { return r.id == agentId; });
if (agents_.empty()) {
enabled_.store(false, std::memory_order_release);

enabled_.store(true, std::memory_order_release);
return true;
std::lock_guard<std::mutex> lock2(requestBodyMutex_);
responseBodyBuffer_.clear();
}
}

bool NetworkHandler::disable() {
if (!enabled_.load(std::memory_order_acquire)) {
return false;
void NetworkHandler::sendToAllAgents(std::string_view message) {
std::lock_guard<std::mutex> lock(agentsMutex_);
for (auto& agent : agents_) {
agent.channel(message);
}

enabled_.store(false, std::memory_order_release);
responseBodyBuffer_.clear();
return true;
}

void NetworkHandler::onRequestWillBeSent(
Expand Down Expand Up @@ -88,7 +92,7 @@ void NetworkHandler::onRequestWillBeSent(
.redirectResponse = redirectResponse,
};

frontendChannel_(
sendToAllAgents(
cdp::jsonNotification("Network.requestWillBeSent", params.toDynamic()));
}

Expand All @@ -105,7 +109,7 @@ void NetworkHandler::onRequestWillBeSentExtraInfo(
.connectTiming = {.requestTime = getCurrentUnixTimestampSeconds()},
};

frontendChannel_(
sendToAllAgents(
cdp::jsonNotification(
"Network.requestWillBeSentExtraInfo", params.toDynamic()));
}
Expand All @@ -132,7 +136,7 @@ void NetworkHandler::onResponseReceived(
.hasExtraInfo = false,
};

frontendChannel_(
sendToAllAgents(
cdp::jsonNotification("Network.responseReceived", params.toDynamic()));
}

Expand All @@ -151,7 +155,7 @@ void NetworkHandler::onDataReceived(
.encodedDataLength = encodedDataLength,
};

frontendChannel_(
sendToAllAgents(
cdp::jsonNotification("Network.dataReceived", params.toDynamic()));
}

Expand All @@ -168,7 +172,7 @@ void NetworkHandler::onLoadingFinished(
.encodedDataLength = encodedDataLength,
};

frontendChannel_(
sendToAllAgents(
cdp::jsonNotification("Network.loadingFinished", params.toDynamic()));
}

Expand All @@ -191,7 +195,7 @@ void NetworkHandler::onLoadingFailed(
.canceled = cancelled,
};

frontendChannel_(
sendToAllAgents(
cdp::jsonNotification("Network.loadingFailed", params.toDynamic()));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <folly/dynamic.h>

#include <atomic>
#include <list>
#include <mutex>
#include <string>
#include <tuple>
Expand All @@ -36,24 +37,17 @@ class NetworkHandler {
static NetworkHandler &getInstance();

/**
* Set the channel used to send CDP events to the frontend. This should be
* supplied before calling `enable`.
* Register a frontend channel for receiving Network domain events.
* Implicitly enables the domain if this is the first agent.
* \returns An agent ID to be passed to disableAgent() on cleanup.
*/
void setFrontendChannel(FrontendChannel frontendChannel);
size_t enableAgent(FrontendChannel frontendChannel);

/**
* Enable network debugging. Returns `false` if already enabled.
*
* @cdp Network.enable
*/
bool enable();

/**
* Disable network debugging. Returns `false` if not initially enabled.
*
* @cdp Network.disable
* Unregister a frontend channel by its ID.
* Implicitly disables the domain if this was the last agent.
*/
bool disable();
void disableAgent(size_t agentId);

/**
* Returns whether network debugging is currently enabled.
Expand Down Expand Up @@ -134,7 +128,19 @@ class NetworkHandler {

std::optional<folly::dynamic> consumeStoredRequestInitiator(const std::string &requestId);

FrontendChannel frontendChannel_;
/**
* Send a message to all registered frontend channels.
*/
void sendToAllAgents(std::string_view message);

struct AgentRecord {
size_t id;
FrontendChannel channel;
};

std::list<AgentRecord> agents_;
size_t nextAgentId_{0};
std::mutex agentsMutex_;

std::map<std::string, std::string> resourceTypeMap_{};
std::map<std::string, folly::dynamic> requestInitiatorById_{};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,40 @@ TEST_P(NetworkReporterTest, testCreateRequestIdWithoutNetworkDomain) {
EXPECT_NE(id1, id2);
}

TEST_P(NetworkReporterTest, testTwoSessionsReceiveNetworkEvents) {
auto secondary = this->connectSecondary();

this->expectMessageFromPage(JsonEq(R"({"id": 1, "result": {}})"));
this->toPage_->sendMessage(R"({"id": 1, "method": "Network.enable"})");

EXPECT_CALL(
secondary.fromPage(), onMessage(JsonEq(R"({"id": 2, "result": {}})")));
secondary.toPage().sendMessage(R"({"id": 2, "method": "Network.enable"})");

// Both sessions should receive the network event
this->expectMessageFromPage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSent"),
AtJsonPtr("/params/requestId", "multi-session-request"))));
EXPECT_CALL(
secondary.fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/method", "Network.requestWillBeSent"),
AtJsonPtr("/params/requestId", "multi-session-request")))));

RequestInfo requestInfo;
requestInfo.url = "https://example.com/test";
requestInfo.httpMethod = "GET";
NetworkReporter::getInstance().reportRequestStart(
"multi-session-request", requestInfo, 0, std::nullopt);

this->expectMessageFromPage(JsonEq(R"({"id": 3, "result": {}})"));
this->toPage_->sendMessage(R"({"id": 3, "method": "Network.disable"})");

EXPECT_CALL(
secondary.fromPage(), onMessage(JsonEq(R"({"id": 4, "result": {}})")));
secondary.toPage().sendMessage(R"({"id": 4, "method": "Network.disable"})");
}

struct NetworkReporterTracingTestParams {
bool enableNetworkEventReporting;
bool enableNetworkDomain;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,76 @@ TEST_F(TracingTest, EmitsScreenshotEventWhenScreenshotValuePassed) {
EXPECT_THAT(allTraceEvents, Contains(AtJsonPtr("/name", "Screenshot")));
}

TEST_F(
TracingTest,
SecondSessionTracingStartIsRejectedWhileFirstSessionIsTracing) {
auto secondary = connectSecondary();
InSequence s;

// Session 1 starts tracing successfully
startTracing();

// Session 2 tries to start tracing - should get error
EXPECT_CALL(
secondary.fromPage(),
onMessage(JsonParsed(AllOf(
AtJsonPtr("/id", 2),
AtJsonPtr("/error/message", "Tracing has already been started")))));
secondary.toPage().sendMessage(R"({"id": 2, "method": "Tracing.start"})");

// Session 1 ends tracing normally
endTracingAndCollectEvents();

// Now Session 2 can start tracing
EXPECT_CALL(
secondary.fromPage(), onMessage(JsonEq(R"({"id": 3, "result": {}})")));
secondary.toPage().sendMessage(R"({"id": 3, "method": "Tracing.start"})");

// Clean up - end secondary's tracing
EXPECT_CALL(
secondary.fromPage(), onMessage(JsonEq(R"({"id": 4, "result": {}})")));
EXPECT_CALL(
secondary.fromPage(),
onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.dataCollected"))))
.Times(AtLeast(1));
EXPECT_CALL(
secondary.fromPage(),
onMessage(JsonParsed(AtJsonPtr("/method", "Tracing.tracingComplete"))));
secondary.toPage().sendMessage(R"({"id": 4, "method": "Tracing.end"})");
}

TEST_F(TracingTest, CDPTracingPreemptsBackgroundTracing) {
InSequence s;

// Start background tracing directly
page_->startTracing(tracing::Mode::Background, {});

// CDP Tracing.start should preempt background (succeed, not fail)
startTracing();

// End tracing normally
endTracingAndCollectEvents();
}

TEST_F(TracingTest, BackgroundTracingIsRejectedWhileCDPTracingIsRunning) {
InSequence s;

// Start CDP tracing
startTracing();

// Background tracing should be rejected
bool started = page_->startTracing(tracing::Mode::Background, {});
EXPECT_FALSE(started);

// End CDP tracing
endTracingAndCollectEvents();

// Now background tracing should succeed
started = page_->startTracing(tracing::Mode::Background, {});
EXPECT_TRUE(started);

// Clean up
page_->stopTracing();
}

} // namespace facebook::react::jsinspector_modern
Loading