mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Implement Network.loadNetworkResource etc in C++ (#44845)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/44845 ## Design - `NetworkIOAgent` is owned by the `HostAgent`. - `NetworkIOAgent` is passed any CDP requests not handled by the `HostAgent` itself, and before delegating to `InstanceAgent`. - It handles: - [`Network.loadNetworkResource`](https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-loadNetworkResource) - [`IO.read`](https://chromedevtools.github.io/devtools-protocol/tot/IO/#method-read) - [`IO.close`](https://chromedevtools.github.io/devtools-protocol/tot/IO/#method-close) - `NetworkIOAgent.loadNetworkResource` creates a `Stream` corresponding to a single resource download/upload. A reference is held in a map `streams_` until an error, the agent is disconnected (destroyed) or it is discarded by the frontend with `IO.close`. - `delegate.loadNetworkResource` is called with a `stream`-scoped executor, which it uses to call back with headers, data and errors. - Callbacks for `IO.read` requests are held by the `Stream` until the incoming data is complete or enough data is available to fill the request (an implementation choice to optimise for fewest round trips). Any incoming data or error causes any pending requests to be rechecked. {F1719616688} ## Unimplemented platforms - Platforms may optionally implement `HostTargetDelegate.networkRequest` (as of this diff, none do). If they don't we report a CDP "not implemented" error, similar to the status quo where it was unimplemented by the C++ agent. Changelog: [General][Added] Debugging: implement common C++ layer of CDP `Network.loadNetworkResource` Reviewed By: motiz88 Differential Revision: D54309633 fbshipit-source-id: 51e416e9d537b253f72693952d5fd520b6ae11b6
This commit is contained in:
committed by
Facebook GitHub Bot
parent
7d7d403ecf
commit
193cdc36f7
@@ -30,11 +30,13 @@ HostAgent::HostAgent(
|
||||
FrontendChannel frontendChannel,
|
||||
HostTargetController& targetController,
|
||||
HostTargetMetadata hostMetadata,
|
||||
SessionState& sessionState)
|
||||
SessionState& sessionState,
|
||||
VoidExecutor executor)
|
||||
: frontendChannel_(frontendChannel),
|
||||
targetController_(targetController),
|
||||
hostMetadata_(std::move(hostMetadata)),
|
||||
sessionState_(sessionState) {}
|
||||
sessionState_(sessionState),
|
||||
networkIOAgent_(NetworkIOAgent(frontendChannel, executor)) {}
|
||||
|
||||
void HostAgent::handleRequest(const cdp::PreparsedRequest& req) {
|
||||
bool shouldSendOKResponse = false;
|
||||
@@ -182,6 +184,12 @@ void HostAgent::handleRequest(const cdp::PreparsedRequest& req) {
|
||||
frontendChannel_(cdp::jsonNotification(
|
||||
"Tracing.tracingComplete",
|
||||
folly::dynamic::object("dataLossOccurred", false)));
|
||||
shouldSendOKResponse = true;
|
||||
isFinishedHandlingRequest = true;
|
||||
}
|
||||
|
||||
if (!isFinishedHandlingRequest &&
|
||||
networkIOAgent_.handleRequest(req, targetController_.getDelegate())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -195,10 +203,7 @@ void HostAgent::handleRequest(const cdp::PreparsedRequest& req) {
|
||||
return;
|
||||
}
|
||||
|
||||
frontendChannel_(cdp::jsonError(
|
||||
req.id,
|
||||
cdp::ErrorCode::MethodNotFound,
|
||||
req.method + " not implemented yet"));
|
||||
throw NotImplementedException(req.method);
|
||||
}
|
||||
|
||||
HostAgent::~HostAgent() {
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CdpJson.h"
|
||||
#include "HostTarget.h"
|
||||
#include "NetworkIOAgent.h"
|
||||
#include "SessionState.h"
|
||||
|
||||
#include <jsinspector-modern/InspectorInterfaces.h>
|
||||
@@ -39,12 +41,14 @@ class HostAgent final {
|
||||
* HostTargetDelegate and underlying HostTarget both outlive the agent.
|
||||
* \param hostMetadata Metadata about the host that created this agent.
|
||||
* \param sessionState The state of the session that created this agent.
|
||||
* \param exector A void executor to be used by async-aware handlers.
|
||||
*/
|
||||
HostAgent(
|
||||
FrontendChannel frontendChannel,
|
||||
HostTargetController& targetController,
|
||||
HostTargetMetadata hostMetadata,
|
||||
SessionState& sessionState);
|
||||
SessionState& sessionState,
|
||||
VoidExecutor executor);
|
||||
|
||||
HostAgent(const HostAgent&) = delete;
|
||||
HostAgent(HostAgent&&) = delete;
|
||||
@@ -104,6 +108,8 @@ class HostAgent final {
|
||||
* during handleRequest and other method calls on the same thread.
|
||||
*/
|
||||
SessionState& sessionState_;
|
||||
|
||||
NetworkIOAgent networkIOAgent_;
|
||||
};
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
|
||||
@@ -29,7 +29,8 @@ class HostTargetSession {
|
||||
explicit HostTargetSession(
|
||||
std::unique_ptr<IRemoteConnection> remote,
|
||||
HostTargetController& targetController,
|
||||
HostTargetMetadata hostMetadata)
|
||||
HostTargetMetadata hostMetadata,
|
||||
VoidExecutor executor)
|
||||
: remote_(std::make_shared<RAIIRemoteConnection>(std::move(remote))),
|
||||
frontendChannel_(
|
||||
[remoteWeak = std::weak_ptr(remote_)](std::string_view message) {
|
||||
@@ -41,7 +42,8 @@ class HostTargetSession {
|
||||
frontendChannel_,
|
||||
targetController,
|
||||
std::move(hostMetadata),
|
||||
state_) {}
|
||||
state_,
|
||||
executor) {}
|
||||
|
||||
/**
|
||||
* Called by CallbackLocalConnection to send a message to this Session's
|
||||
@@ -62,15 +64,22 @@ class HostTargetSession {
|
||||
return;
|
||||
}
|
||||
|
||||
// Catch exceptions that may arise from accessing dynamic params during
|
||||
// request handling.
|
||||
try {
|
||||
hostAgent_.handleRequest(request);
|
||||
} catch (const cdp::TypeError& e) {
|
||||
}
|
||||
// Catch exceptions that may arise from accessing dynamic params during
|
||||
// request handling.
|
||||
catch (const cdp::TypeError& e) {
|
||||
frontendChannel_(
|
||||
cdp::jsonError(request.id, cdp::ErrorCode::InvalidRequest, e.what()));
|
||||
return;
|
||||
}
|
||||
// Catch exceptions for unrecognised or partially implemented CDP methods.
|
||||
catch (const NotImplementedException& e) {
|
||||
frontendChannel_(
|
||||
cdp::jsonError(request.id, cdp::ErrorCode::MethodNotFound, e.what()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,7 +157,10 @@ HostTarget::HostTarget(HostTargetDelegate& delegate)
|
||||
std::unique_ptr<ILocalConnection> HostTarget::connect(
|
||||
std::unique_ptr<IRemoteConnection> connectionToFrontend) {
|
||||
auto session = std::make_shared<HostTargetSession>(
|
||||
std::move(connectionToFrontend), controller_, delegate_.getMetadata());
|
||||
std::move(connectionToFrontend),
|
||||
controller_,
|
||||
delegate_.getMetadata(),
|
||||
makeVoidExecutor(executorFromThis()));
|
||||
session->setCurrentInstance(currentInstance_.get());
|
||||
sessions_.insert(std::weak_ptr(session));
|
||||
return std::make_unique<CallbackLocalConnection>(
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "HostCommand.h"
|
||||
#include "InspectorInterfaces.h"
|
||||
#include "InstanceTarget.h"
|
||||
#include "NetworkIOAgent.h"
|
||||
#include "ScopedExecutor.h"
|
||||
#include "WeakList.h"
|
||||
|
||||
@@ -50,13 +51,13 @@ struct HostTargetMetadata {
|
||||
* React Native platform needs to implement in order to integrate with the
|
||||
* debugging stack.
|
||||
*/
|
||||
class HostTargetDelegate {
|
||||
class HostTargetDelegate : public LoadNetworkResourceDelegate {
|
||||
public:
|
||||
HostTargetDelegate() = default;
|
||||
HostTargetDelegate(const HostTargetDelegate&) = delete;
|
||||
HostTargetDelegate(HostTargetDelegate&&) = default;
|
||||
HostTargetDelegate(HostTargetDelegate&&) = delete;
|
||||
HostTargetDelegate& operator=(const HostTargetDelegate&) = delete;
|
||||
HostTargetDelegate& operator=(HostTargetDelegate&&) = default;
|
||||
HostTargetDelegate& operator=(HostTargetDelegate&&) = delete;
|
||||
|
||||
// TODO(moti): This is 1:1 the shape of the corresponding CDP message -
|
||||
// consider reusing typed/generated CDP interfaces when we have those.
|
||||
@@ -92,7 +93,7 @@ class HostTargetDelegate {
|
||||
}
|
||||
};
|
||||
|
||||
virtual ~HostTargetDelegate();
|
||||
virtual ~HostTargetDelegate() override;
|
||||
|
||||
/**
|
||||
* Returns a metadata object describing the host. This is called on an
|
||||
@@ -119,6 +120,19 @@ class HostTargetDelegate {
|
||||
*/
|
||||
virtual void onSetPausedInDebuggerMessage(
|
||||
const OverlaySetPausedInDebuggerMessageRequest& request) = 0;
|
||||
|
||||
/**
|
||||
* Called by NetworkIOAgent on handling a `Network.loadNetworkResource` CDP
|
||||
* request. Platform implementations should override this to perform a
|
||||
* network request of the given URL, and use listener's callbacks on receipt
|
||||
* of headers, data chunks, and errors.
|
||||
*/
|
||||
void loadNetworkResource(
|
||||
const LoadNetworkResourceRequest& /*params*/,
|
||||
ScopedExecutor<NetworkRequestListener> /*executor*/) override {
|
||||
throw NotImplementedException(
|
||||
"LoadNetworkResourceDelegate.loadNetworkResource is not implemented by this host target delegate.");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -134,6 +134,19 @@ class JSINSPECTOR_EXPORT IInspector : public IDestructible {
|
||||
std::weak_ptr<IPageStatusListener> listener) = 0;
|
||||
};
|
||||
|
||||
class NotImplementedException : public std::exception {
|
||||
public:
|
||||
explicit NotImplementedException(std::string message)
|
||||
: msg_(std::move(message)) {}
|
||||
|
||||
const char* what() const noexcept override {
|
||||
return msg_.c_str();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string msg_;
|
||||
};
|
||||
|
||||
/// getInspectorInstance retrieves the singleton inspector that tracks all
|
||||
/// debuggable pages in this process.
|
||||
extern IInspector& getInspectorInstance();
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
/*
|
||||
* 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 "NetworkIOAgent.h"
|
||||
#include <folly/base64.h>
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
static constexpr long DEFAULT_BYTES_PER_READ =
|
||||
1048576; // 1MB (Chrome v112 default)
|
||||
|
||||
namespace {
|
||||
|
||||
struct InitStreamResult {
|
||||
int httpStatusCode;
|
||||
Headers headers;
|
||||
std::shared_ptr<Stream> stream;
|
||||
};
|
||||
using InitStreamError = const std::string;
|
||||
|
||||
using StreamInitCallback =
|
||||
std::function<void(std::variant<InitStreamError, InitStreamResult>)>;
|
||||
using IOReadCallback =
|
||||
std::function<void(std::variant<IOReadError, IOReadResult>)>;
|
||||
|
||||
/**
|
||||
* Private class owning state and implementing the listener for a particular
|
||||
* request
|
||||
*
|
||||
* NetworkRequestListener overrides are thread safe, all other methods must be
|
||||
* called from the same thread.
|
||||
*/
|
||||
class Stream : public NetworkRequestListener,
|
||||
public EnableExecutorFromThis<Stream> {
|
||||
public:
|
||||
Stream(const Stream& other) = delete;
|
||||
Stream& operator=(const Stream& other) = delete;
|
||||
Stream(Stream&& other) = default;
|
||||
Stream& operator=(Stream&& other) = default;
|
||||
|
||||
/**
|
||||
* Factory method to create a Stream with a callback for the initial result
|
||||
* of a network request.
|
||||
* \param executor An executor on which all processing of callbacks from
|
||||
* the platform will be performed, and on which the passed callback will be
|
||||
* called.
|
||||
* \param initCb Will be called once either on receipt of HTTP headers or
|
||||
* any prior error, using the given executor.
|
||||
*/
|
||||
static std::shared_ptr<Stream> create(
|
||||
VoidExecutor executor,
|
||||
StreamInitCallback initCb) {
|
||||
std::shared_ptr<Stream> stream{new Stream(initCb)};
|
||||
stream->setExecutor(executor);
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent-facing API. Enqueue a read request for up to maxBytesToRead
|
||||
* bytes, starting from the end of the previous read.
|
||||
* \param maxBytesToRead The maximum number of bytes to read from the
|
||||
* source stream.
|
||||
* \param callback Will be called using the executor passed to create()
|
||||
* with the result of the read, or an error string.
|
||||
*/
|
||||
void read(long maxBytesToRead, const IOReadCallback& callback) {
|
||||
pendingReadRequests_.emplace_back(
|
||||
std::make_tuple(maxBytesToRead, callback));
|
||||
processPending();
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent-facing API. Call the platform-provided cancelFunction, if any,
|
||||
* call the error callbacks of any in-flight read requests, and the initial
|
||||
* error callback if it has not already fulfilled with success or error.
|
||||
*/
|
||||
void cancel() {
|
||||
if (cancelFunction_) {
|
||||
(*cancelFunction_)();
|
||||
}
|
||||
error_ = "Cancelled";
|
||||
if (initCb_) {
|
||||
auto cb = std::move(initCb_);
|
||||
(*cb)(InitStreamError{"Cancelled"});
|
||||
}
|
||||
// Respond to any in-flight read requests with an error.
|
||||
processPending();
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin implementation of NetworkRequestListener, to be called by platform
|
||||
* HostTargetDelegate. Any of these methods may be called from any thread.
|
||||
*/
|
||||
|
||||
void onData(std::string_view data) override {
|
||||
data_ << data;
|
||||
bytesReceived_ += data.length();
|
||||
processPending();
|
||||
}
|
||||
|
||||
void onHeaders(int httpStatusCode, const Headers& headers) override {
|
||||
// If we've already seen an error, the initial callback as already been
|
||||
// called with it.
|
||||
if (initCb_) {
|
||||
auto cb = std::move(initCb_);
|
||||
(*cb)(
|
||||
InitStreamResult{httpStatusCode, headers, this->shared_from_this()});
|
||||
}
|
||||
}
|
||||
|
||||
void onError(const std::string& message) override {
|
||||
// Only call the error callback once.
|
||||
if (!error_) {
|
||||
error_ = message;
|
||||
if (initCb_) {
|
||||
auto cb = std::move(initCb_);
|
||||
(*cb)(InitStreamError{message});
|
||||
}
|
||||
}
|
||||
processPending();
|
||||
}
|
||||
|
||||
void onCompletion() override {
|
||||
completed_ = true;
|
||||
processPending();
|
||||
}
|
||||
|
||||
void setCancelFunction(std::function<void()> cancelFunction) override {
|
||||
cancelFunction_ = std::move(cancelFunction);
|
||||
}
|
||||
|
||||
~Stream() override {
|
||||
// Cancel any incoming request, if the platform has provided a cancel
|
||||
// callback.
|
||||
if (cancelFunction_) {
|
||||
(*cancelFunction_)();
|
||||
}
|
||||
}
|
||||
|
||||
/* End NetworkRequestListener */
|
||||
|
||||
private:
|
||||
/**
|
||||
* Private constructor. The caller must call setExecutor immediately
|
||||
* afterwards.
|
||||
*/
|
||||
explicit Stream(const StreamInitCallback& initCb)
|
||||
: initCb_(std::make_unique<StreamInitCallback>(initCb)) {}
|
||||
|
||||
void processPending() {
|
||||
// Go through each pending request in insertion order - execute the
|
||||
// callback and remove it from pending if it can be satisfied.
|
||||
for (auto it = pendingReadRequests_.begin();
|
||||
it != pendingReadRequests_.end();) {
|
||||
auto maxBytesToRead = std::get<0>(*it);
|
||||
auto callback = std::get<1>(*it);
|
||||
|
||||
if (error_) {
|
||||
callback(IOReadError{*error_});
|
||||
} else if (
|
||||
completed_ || (bytesReceived_ - data_.tellg() >= maxBytesToRead)) {
|
||||
try {
|
||||
callback(respond(maxBytesToRead));
|
||||
} catch (const std::runtime_error& error) {
|
||||
callback(IOReadError{error.what()});
|
||||
}
|
||||
} else {
|
||||
// Not yet received enough data
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
it = pendingReadRequests_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
IOReadResult respond(long maxBytesToRead) {
|
||||
std::vector<char> buffer(maxBytesToRead);
|
||||
data_.read(buffer.data(), maxBytesToRead);
|
||||
auto bytesRead = data_.gcount();
|
||||
buffer.resize(bytesRead);
|
||||
return IOReadResult{
|
||||
.data =
|
||||
folly::base64Encode(std::string_view(buffer.data(), buffer.size())),
|
||||
.eof = bytesRead == 0 && completed_,
|
||||
// TODO: Support UTF-8 string responses
|
||||
.base64Encoded = true};
|
||||
}
|
||||
|
||||
bool completed_{false};
|
||||
std::optional<std::string> error_;
|
||||
std::stringstream data_;
|
||||
long bytesReceived_{0};
|
||||
std::optional<std::function<void()>> cancelFunction_{std::nullopt};
|
||||
std::unique_ptr<StreamInitCallback> initCb_;
|
||||
std::vector<std::tuple<long /* bytesToRead */, IOReadCallback>>
|
||||
pendingReadRequests_;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
bool NetworkIOAgent::handleRequest(
|
||||
const cdp::PreparsedRequest& req,
|
||||
LoadNetworkResourceDelegate& delegate) {
|
||||
if (req.method == "Network.loadNetworkResource") {
|
||||
handleLoadNetworkResource(req, delegate);
|
||||
return true;
|
||||
} else if (req.method == "IO.read") {
|
||||
handleIoRead(req);
|
||||
return true;
|
||||
} else if (req.method == "IO.close") {
|
||||
handleIoClose(req);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void NetworkIOAgent::handleLoadNetworkResource(
|
||||
const cdp::PreparsedRequest& req,
|
||||
LoadNetworkResourceDelegate& delegate) {
|
||||
long long requestId = req.id;
|
||||
|
||||
LoadNetworkResourceRequest params;
|
||||
|
||||
if (!req.params.isObject()) {
|
||||
frontendChannel_(cdp::jsonError(
|
||||
req.id,
|
||||
cdp::ErrorCode::InvalidParams,
|
||||
"Invalid params: not an object."));
|
||||
return;
|
||||
}
|
||||
if ((req.params.count("url") == 0u) || !req.params.at("url").isString()) {
|
||||
frontendChannel_(cdp::jsonError(
|
||||
requestId,
|
||||
cdp::ErrorCode::InvalidParams,
|
||||
"Invalid params: url is missing or not a string."));
|
||||
return;
|
||||
} else {
|
||||
params.url = req.params.at("url").asString();
|
||||
}
|
||||
|
||||
// This is an opaque identifier, but an incrementing integer in a string is
|
||||
// consistent with Chrome.
|
||||
StreamID streamId = std::to_string(nextStreamId_++);
|
||||
|
||||
auto stream = Stream::create(
|
||||
executor_,
|
||||
[streamId,
|
||||
requestId,
|
||||
frontendChannel = frontendChannel_,
|
||||
streamsWeak = std::weak_ptr(streams_)](auto resultOrError) {
|
||||
NetworkResource resource;
|
||||
std::string cdpError;
|
||||
if (auto* error = std::get_if<InitStreamError>(&resultOrError)) {
|
||||
resource = NetworkResource{.success = false, .netErrorName = *error};
|
||||
} else if (
|
||||
auto* result = std::get_if<InitStreamResult>(&resultOrError)) {
|
||||
if (result->httpStatusCode >= 200 && result->httpStatusCode < 300) {
|
||||
resource = NetworkResource{
|
||||
.success = true,
|
||||
.stream = streamId,
|
||||
.httpStatusCode = result->httpStatusCode,
|
||||
.headers = result->headers};
|
||||
} else if (result->httpStatusCode >= 400) {
|
||||
resource = NetworkResource{
|
||||
.success = false,
|
||||
.httpStatusCode = result->httpStatusCode,
|
||||
.headers = result->headers};
|
||||
} else {
|
||||
// We can't deal with <200 or 3xx reponses here (though they may be
|
||||
// transparently handled by the delegate). Return a CDP error (not
|
||||
// an unsuccesful resource) to the frontend so that it falls back to
|
||||
// a direct fetch.
|
||||
cdpError = "Handling of status " +
|
||||
std::to_string(result->httpStatusCode) + " not implemented.";
|
||||
}
|
||||
} else {
|
||||
assert(false && "Unhandled IO init result type");
|
||||
}
|
||||
if (cdpError.length() > 0 || !resource.success) {
|
||||
// Release and destroy the stream after the calling executor returns.
|
||||
// ~Stream will handle cancelling any download in progress.
|
||||
if (auto streams = streamsWeak.lock()) {
|
||||
streams->erase(streamId);
|
||||
}
|
||||
}
|
||||
frontendChannel(
|
||||
cdpError.length()
|
||||
? cdp::jsonError(
|
||||
requestId, cdp::ErrorCode::InternalError, cdpError)
|
||||
: cdp::jsonResult(
|
||||
requestId,
|
||||
folly::dynamic::object(
|
||||
"resource", resource.toDynamic())));
|
||||
});
|
||||
|
||||
// Begin the network request on the platform, passing an executor scoped to
|
||||
// a Stream (a NetworkRequestListener), which the implementation will call
|
||||
// back into.
|
||||
delegate.loadNetworkResource(params, stream->executorFromThis());
|
||||
|
||||
// Retain the stream only if delegate.loadNetworkResource does not throw.
|
||||
streams_->emplace(streamId, stream);
|
||||
}
|
||||
|
||||
void NetworkIOAgent::handleIoRead(const cdp::PreparsedRequest& req) {
|
||||
long long requestId = req.id;
|
||||
if (!req.params.isObject()) {
|
||||
frontendChannel_(cdp::jsonError(
|
||||
requestId,
|
||||
cdp::ErrorCode::InvalidParams,
|
||||
"Invalid params: not an object."));
|
||||
return;
|
||||
}
|
||||
if ((req.params.count("handle") == 0u) ||
|
||||
!req.params.at("handle").isString()) {
|
||||
frontendChannel_(cdp::jsonError(
|
||||
requestId,
|
||||
cdp::ErrorCode::InvalidParams,
|
||||
"Invalid params: handle is missing or not a string."));
|
||||
return;
|
||||
}
|
||||
std::optional<unsigned long> size = std::nullopt;
|
||||
if ((req.params.count("size") != 0u) && req.params.at("size").isInt()) {
|
||||
size = req.params.at("size").asInt();
|
||||
}
|
||||
|
||||
auto streamId = req.params.at("handle").asString();
|
||||
auto it = streams_->find(streamId);
|
||||
if (it == streams_->end()) {
|
||||
frontendChannel_(cdp::jsonError(
|
||||
requestId,
|
||||
cdp::ErrorCode::InternalError,
|
||||
"Stream not found with handle " + streamId));
|
||||
return;
|
||||
} else {
|
||||
it->second->read(
|
||||
size ? *size : DEFAULT_BYTES_PER_READ,
|
||||
[requestId, frontendChannel = frontendChannel_](auto resultOrError) {
|
||||
if (auto* error = std::get_if<IOReadError>(&resultOrError)) {
|
||||
frontendChannel(cdp::jsonError(
|
||||
requestId, cdp::ErrorCode::InternalError, *error));
|
||||
} else if (auto* result = std::get_if<IOReadResult>(&resultOrError)) {
|
||||
frontendChannel(cdp::jsonResult(requestId, result->toDynamic()));
|
||||
} else {
|
||||
assert(false && "Unhandled IO read result type");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkIOAgent::handleIoClose(const cdp::PreparsedRequest& req) {
|
||||
long long requestId = req.id;
|
||||
if (!req.params.isObject()) {
|
||||
frontendChannel_(cdp::jsonError(
|
||||
requestId,
|
||||
cdp::ErrorCode::InvalidParams,
|
||||
"Invalid params: not an object."));
|
||||
return;
|
||||
}
|
||||
if ((req.params.count("handle") == 0u) ||
|
||||
!req.params.at("handle").isString()) {
|
||||
frontendChannel_(cdp::jsonError(
|
||||
requestId,
|
||||
cdp::ErrorCode::InvalidParams,
|
||||
"Invalid params: handle is missing or not a string."));
|
||||
return;
|
||||
}
|
||||
auto streamId = req.params.at("handle").asString();
|
||||
|
||||
auto it = streams_->find(streamId);
|
||||
if (it == streams_->end()) {
|
||||
frontendChannel_(cdp::jsonError(
|
||||
requestId,
|
||||
cdp::ErrorCode::InternalError,
|
||||
"Stream not found: " + streamId));
|
||||
} else {
|
||||
it->second->cancel();
|
||||
streams_->erase(it->first);
|
||||
frontendChannel_(cdp::jsonResult(requestId));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* 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 "CdpJson.h"
|
||||
#include "InspectorInterfaces.h"
|
||||
#include "ScopedExecutor.h"
|
||||
|
||||
#include <folly/dynamic.h>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
namespace facebook::react::jsinspector_modern {
|
||||
|
||||
using StreamID = const std::string;
|
||||
using Headers = std::map<std::string, std::string>;
|
||||
using IOReadError = const std::string;
|
||||
|
||||
namespace {
|
||||
class Stream; // Defined in NetworkIOAgent.cpp
|
||||
using StreamsMap = std::unordered_map<std::string, std::shared_ptr<Stream>>;
|
||||
} // namespace
|
||||
|
||||
struct LoadNetworkResourceRequest {
|
||||
std::string url;
|
||||
};
|
||||
|
||||
struct ReadStreamParams {
|
||||
StreamID handle;
|
||||
std::optional<unsigned long> size;
|
||||
std::optional<unsigned long> offset;
|
||||
};
|
||||
|
||||
struct NetworkResource {
|
||||
bool success{};
|
||||
std::optional<std::string> stream;
|
||||
std::optional<int> httpStatusCode;
|
||||
std::optional<std::string> netErrorName;
|
||||
std::optional<Headers> headers;
|
||||
folly::dynamic toDynamic() const {
|
||||
auto dynamicResource = folly::dynamic::object("success", success);
|
||||
|
||||
if (success) { // stream IFF successful
|
||||
assert(stream);
|
||||
dynamicResource("stream", *stream);
|
||||
}
|
||||
|
||||
if (netErrorName) { // Only if unsuccessful
|
||||
assert(!success);
|
||||
dynamicResource("netErrorName", *netErrorName);
|
||||
}
|
||||
|
||||
if (httpStatusCode) { // Guaranteed if successful
|
||||
dynamicResource("httpStatusCode", *httpStatusCode);
|
||||
} else {
|
||||
assert(!success);
|
||||
}
|
||||
|
||||
if (headers) { // Guaranteed if successful
|
||||
auto dynamicHeaders = folly::dynamic::object();
|
||||
for (const auto& pair : *headers) {
|
||||
dynamicHeaders(pair.first, pair.second);
|
||||
}
|
||||
dynamicResource("headers", std::move(dynamicHeaders));
|
||||
} else {
|
||||
assert(!success);
|
||||
}
|
||||
return dynamicResource;
|
||||
}
|
||||
};
|
||||
|
||||
struct IOReadResult {
|
||||
std::string data;
|
||||
bool eof;
|
||||
bool base64Encoded;
|
||||
folly::dynamic toDynamic() const {
|
||||
auto obj = folly::dynamic::object("data", data);
|
||||
obj("eof", eof);
|
||||
obj("base64Encoded", base64Encoded);
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Passed to `loadNetworkResource`, provides callbacks for processing incoming
|
||||
* data and other events.
|
||||
*/
|
||||
class NetworkRequestListener {
|
||||
public:
|
||||
NetworkRequestListener() = default;
|
||||
NetworkRequestListener(const NetworkRequestListener&) = delete;
|
||||
NetworkRequestListener& operator=(const NetworkRequestListener&) = delete;
|
||||
NetworkRequestListener(NetworkRequestListener&&) noexcept = default;
|
||||
NetworkRequestListener& operator=(NetworkRequestListener&&) noexcept =
|
||||
default;
|
||||
virtual ~NetworkRequestListener() = default;
|
||||
|
||||
/**
|
||||
* To be called by the delegate on receipt of response headers, including
|
||||
* on "unsuccessful" status codes.
|
||||
*
|
||||
* \param httpStatusCode The HTTP status code received.
|
||||
* \param headers Response headers as an unordered_map.
|
||||
*/
|
||||
virtual void onHeaders(int httpStatusCode, const Headers& headers) = 0;
|
||||
|
||||
/**
|
||||
* To be called by the delegate on receipt of data chunks.
|
||||
* \param data The data received.
|
||||
*/
|
||||
virtual void onData(std::string_view data) = 0;
|
||||
|
||||
/**
|
||||
* To be called by the delegate on any error with the request, either before
|
||||
* headers are received or for a subsequent interrupion.
|
||||
*
|
||||
* \param message A short, human-readable message, which may be forwarded to
|
||||
* the CDP client either in the `loadNetworkResource` response (if headers
|
||||
* were not yet received), or as a CDP error in response to a subsequent
|
||||
* `IO.read`.
|
||||
*/
|
||||
virtual void onError(const std::string& message) = 0;
|
||||
|
||||
/**
|
||||
* To be called by the delegate on successful completion of the request.
|
||||
* Delegates must call *either* onCompletion() or onError() exactly once.
|
||||
*/
|
||||
virtual void onCompletion() = 0;
|
||||
|
||||
/**
|
||||
* Optionally (preferably) used to give NetworkIOAgent
|
||||
a way to cancel an
|
||||
* in-progress download.
|
||||
*
|
||||
* \param cancelFunction A function that can be called to cancel a download,
|
||||
* may be called before or after the download is complete.
|
||||
*/
|
||||
virtual void setCancelFunction(std::function<void()> cancelFunction) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Implemented by the HostTargetDelegate per-platform to perform network
|
||||
* requests.
|
||||
*/
|
||||
class LoadNetworkResourceDelegate {
|
||||
public:
|
||||
LoadNetworkResourceDelegate() = default;
|
||||
LoadNetworkResourceDelegate(const LoadNetworkResourceDelegate&) = delete;
|
||||
LoadNetworkResourceDelegate& operator=(const LoadNetworkResourceDelegate&) =
|
||||
delete;
|
||||
LoadNetworkResourceDelegate(LoadNetworkResourceDelegate&&) noexcept = delete;
|
||||
LoadNetworkResourceDelegate& operator=(
|
||||
LoadNetworkResourceDelegate&&) noexcept = delete;
|
||||
virtual ~LoadNetworkResourceDelegate() = default;
|
||||
|
||||
/**
|
||||
* Called by NetworkIOAgent on handling a
|
||||
* `Network.loadNetworkResource` CDP request. Platform implementations should
|
||||
* override this to perform a network request of the given URL, and use
|
||||
* listener's callbacks (on any thread) on receipt of headers, data chunks,
|
||||
* and errors.
|
||||
*
|
||||
* \param params A LoadNetworkResourceRequest, including the url.
|
||||
* \param listener The listener to call on headers, data chunks, and errors.
|
||||
* Implementations must ensure that they retain a shared_ptr to listener for
|
||||
* as long as its callbacks may be called, and should release it once the
|
||||
* network request is complete or cancelled. Implementations *should* call
|
||||
* listener->setCancelFunction() to provide a lambda that can be called to
|
||||
* abort any in-flight network operation that is no longer needed.
|
||||
*/
|
||||
virtual void loadNetworkResource(
|
||||
const LoadNetworkResourceRequest& /*params*/,
|
||||
ScopedExecutor<NetworkRequestListener> /*executor*/) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides an agent for handling CDP's Network.loadNetworkResource, IO.read and
|
||||
* IO.close.
|
||||
*
|
||||
* Owns state of all in-progress and completed HTTP requests - ensure
|
||||
* IO.close is used to free resources once consumed.
|
||||
*
|
||||
* Public methods must be called the same thread as the given executor.
|
||||
*/
|
||||
class NetworkIOAgent {
|
||||
public:
|
||||
/**
|
||||
* \param frontendChannel A channel used to send responses to the
|
||||
* frontend.
|
||||
* \param executor An executor used for any callbacks provided, and for
|
||||
* processing incoming data or other events from network operations.
|
||||
*/
|
||||
NetworkIOAgent(FrontendChannel frontendChannel, VoidExecutor executor)
|
||||
: frontendChannel_(frontendChannel),
|
||||
executor_(executor),
|
||||
streams_(std::make_shared<StreamsMap>()) {}
|
||||
|
||||
/**
|
||||
* Handle a CDP request. The response will be sent over the provided
|
||||
* \c FrontendChannel synchronously or asynchronously.
|
||||
* \param req The parsed request.
|
||||
*/
|
||||
bool handleRequest(
|
||||
const cdp::PreparsedRequest& req,
|
||||
LoadNetworkResourceDelegate& delegate);
|
||||
|
||||
private:
|
||||
/**
|
||||
* A channel used to send responses and events to the frontend.
|
||||
*/
|
||||
FrontendChannel frontendChannel_;
|
||||
|
||||
/**
|
||||
* An executor used to create NetworkRequestListener-scoped executors for the
|
||||
* delegate.
|
||||
*/
|
||||
VoidExecutor executor_;
|
||||
|
||||
/**
|
||||
* Map of stream objects, which contain data received, accept read requests
|
||||
* and listen for delegate events. Delegates have a scoped executor for Stream
|
||||
* instances, but Streams will not live beyond the destruction of this
|
||||
* NetworkIOAgent instance + executor scope.
|
||||
*
|
||||
* This is a shared_ptr so that we may capture a weak_ptr in our
|
||||
* Stream::create callback without creating a cycle.
|
||||
*/
|
||||
std::shared_ptr<StreamsMap> streams_;
|
||||
|
||||
/**
|
||||
* Stream IDs are strings of an incrementing integer, unique within each
|
||||
* NewtworkIOAgent instance. This stores the next one to use.
|
||||
*/
|
||||
unsigned long nextStreamId_{0};
|
||||
|
||||
/**
|
||||
* Begin loading an HTTP resource, delegating platform-specific
|
||||
* implementation, responding to the frontend on headers received or on error.
|
||||
* Does not catch exceptions thrown by the delegate (such as
|
||||
* NotImplementedException).
|
||||
*/
|
||||
void handleLoadNetworkResource(
|
||||
const cdp::PreparsedRequest& req,
|
||||
LoadNetworkResourceDelegate& delegate);
|
||||
|
||||
/**
|
||||
* Handle an IO.read CDP request. Emit a chunk of data from the stream, once
|
||||
* enough has been downloaded, or report an error.
|
||||
*/
|
||||
void handleIoRead(const cdp::PreparsedRequest& req);
|
||||
|
||||
/**
|
||||
* Handle an IO.close CDP request. Safely aborts any in-flight request.
|
||||
* Reports CDP ok if the stream is found, or a CDP error if not.
|
||||
*/
|
||||
void handleIoClose(const cdp::PreparsedRequest& req);
|
||||
};
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
@@ -698,4 +698,595 @@ TEST_F(HostTargetTest, HostCommands) {
|
||||
page_->unregisterInstance(instanceTarget);
|
||||
}
|
||||
|
||||
TEST_F(HostTargetTest, NetworkLoadNetworkResourceSuccess) {
|
||||
connect();
|
||||
|
||||
InSequence s;
|
||||
|
||||
ScopedExecutor<NetworkRequestListener> executor;
|
||||
EXPECT_CALL(
|
||||
hostTargetDelegate_,
|
||||
loadNetworkResource(
|
||||
Field(&LoadNetworkResourceRequest::url, "http://example.com"), _))
|
||||
.Times(1)
|
||||
.WillOnce([&executor](
|
||||
const LoadNetworkResourceRequest& /*params*/,
|
||||
ScopedExecutor<NetworkRequestListener> executorArg) {
|
||||
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
|
||||
executor = std::move(executorArg);
|
||||
})
|
||||
.RetiresOnSaturation();
|
||||
|
||||
// Load the resource, expect a CDP response as soon as headers are received.
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": "Network.loadNetworkResource",
|
||||
"params": {
|
||||
"url": "http://example.com"
|
||||
}
|
||||
})");
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 1,
|
||||
"result": {
|
||||
"resource": {
|
||||
"success": true,
|
||||
"stream": "0",
|
||||
"httpStatusCode": 200,
|
||||
"headers": {
|
||||
"x-test": "foo",
|
||||
"Content-Type": "text/plain"
|
||||
}
|
||||
}
|
||||
}
|
||||
})")));
|
||||
|
||||
executor([](NetworkRequestListener& listener) {
|
||||
listener.onHeaders(
|
||||
200, Headers{{"x-test", "foo"}, {"Content-Type", "text/plain"}});
|
||||
});
|
||||
|
||||
// Retrieve the first chunk of data.
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 2,
|
||||
"method": "IO.read",
|
||||
"params": {
|
||||
"handle": "0",
|
||||
"size": 8
|
||||
}
|
||||
})");
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 2,
|
||||
"result": {
|
||||
"data": "SGVsbG8sIFc=",
|
||||
"eof": false,
|
||||
"base64Encoded": true
|
||||
}
|
||||
})")));
|
||||
|
||||
executor([](NetworkRequestListener& listener) {
|
||||
listener.onData("Hello, World!");
|
||||
});
|
||||
|
||||
// Retrieve the remaining data.
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 3,
|
||||
"result": {
|
||||
"data": "b3JsZCE=",
|
||||
"eof": false,
|
||||
"base64Encoded": true
|
||||
}
|
||||
})")));
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 3,
|
||||
"method": "IO.read",
|
||||
"params": {
|
||||
"handle": "0",
|
||||
"size": 8
|
||||
}
|
||||
})");
|
||||
|
||||
// No more data - expect empty payload with eof: true.
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 4,
|
||||
"result": {
|
||||
"data": "",
|
||||
"eof": true,
|
||||
"base64Encoded": true
|
||||
}
|
||||
})")));
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 4,
|
||||
"method": "IO.read",
|
||||
"params": {
|
||||
"handle": "0",
|
||||
"size": 8
|
||||
}
|
||||
})");
|
||||
|
||||
executor([](NetworkRequestListener& listener) { listener.onCompletion(); });
|
||||
|
||||
// Close the stream.
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 5,
|
||||
"result": {}
|
||||
})")));
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 5,
|
||||
"method": "IO.close",
|
||||
"params": {
|
||||
"handle": "0"
|
||||
}
|
||||
})");
|
||||
}
|
||||
|
||||
TEST_F(HostTargetTest, NetworkLoadNetworkResourceStreamInterrupted) {
|
||||
connect();
|
||||
|
||||
InSequence s;
|
||||
|
||||
ScopedExecutor<NetworkRequestListener> executor;
|
||||
EXPECT_CALL(
|
||||
hostTargetDelegate_,
|
||||
loadNetworkResource(
|
||||
Field(&LoadNetworkResourceRequest::url, "http://example.com"), _))
|
||||
.Times(1)
|
||||
.WillOnce([&executor](
|
||||
const LoadNetworkResourceRequest& /*params*/,
|
||||
ScopedExecutor<NetworkRequestListener> executorArg) {
|
||||
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
|
||||
executor = std::move(executorArg);
|
||||
})
|
||||
.RetiresOnSaturation();
|
||||
|
||||
// Load the resource, receiving headers succesfully.
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": "Network.loadNetworkResource",
|
||||
"params": {
|
||||
"url": "http://example.com"
|
||||
}
|
||||
})");
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 1,
|
||||
"result": {
|
||||
"resource": {
|
||||
"success": true,
|
||||
"stream": "0",
|
||||
"httpStatusCode": 200,
|
||||
"headers": {
|
||||
"x-test": "foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
})")));
|
||||
|
||||
executor([](NetworkRequestListener& listener) {
|
||||
listener.onHeaders(200, Headers{{"x-test", "foo"}});
|
||||
});
|
||||
|
||||
// Retrieve the first chunk of data.
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 2,
|
||||
"method": "IO.read",
|
||||
"params": {
|
||||
"handle": "0",
|
||||
"size": 20
|
||||
}
|
||||
})");
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 2,
|
||||
"result": {
|
||||
"data": "VGhlIG1lYW5pbmcgb2YgbGlmZSA=",
|
||||
"eof": false,
|
||||
"base64Encoded": true
|
||||
}
|
||||
})")));
|
||||
executor([](NetworkRequestListener& listener) {
|
||||
listener.onData("The meaning of life is...");
|
||||
});
|
||||
|
||||
// Simulate an error mid-stream, expect in-flight IO.reads to return a CDP
|
||||
// error.
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 3,
|
||||
"method": "IO.read",
|
||||
"params": {
|
||||
"handle": "0",
|
||||
"size": 20
|
||||
}
|
||||
})");
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 3,
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": "Connection lost"
|
||||
}
|
||||
})")));
|
||||
executor([](NetworkRequestListener& listener) {
|
||||
listener.onError("Connection lost");
|
||||
});
|
||||
|
||||
// IO.close should be a successful no-op after an error.
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 4,
|
||||
"result": {}
|
||||
})")));
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 4,
|
||||
"method": "IO.close",
|
||||
"params": {
|
||||
"handle": "0"
|
||||
}
|
||||
})");
|
||||
}
|
||||
|
||||
TEST_F(HostTargetTest, NetworkLoadNetworkResource404) {
|
||||
connect();
|
||||
|
||||
InSequence s;
|
||||
|
||||
ScopedExecutor<NetworkRequestListener> executor;
|
||||
EXPECT_CALL(
|
||||
hostTargetDelegate_,
|
||||
loadNetworkResource(
|
||||
Field(&LoadNetworkResourceRequest::url, "http://example.com/404"), _))
|
||||
.Times(1)
|
||||
.WillOnce([&executor](
|
||||
const LoadNetworkResourceRequest& /*params*/,
|
||||
ScopedExecutor<NetworkRequestListener> executorArg) {
|
||||
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
|
||||
executor = std::move(executorArg);
|
||||
})
|
||||
.RetiresOnSaturation();
|
||||
|
||||
// A 404 response should trigger a CDP result with success: false, including
|
||||
// the status code, headers, but *no* stream handle.
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 1,
|
||||
"result": {
|
||||
"resource": {
|
||||
"success": false,
|
||||
"httpStatusCode": 404,
|
||||
"headers": {
|
||||
"x-test": "foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
})")));
|
||||
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": "Network.loadNetworkResource",
|
||||
"params": {
|
||||
"url": "http://example.com/404"
|
||||
}
|
||||
})");
|
||||
|
||||
executor([](NetworkRequestListener& listener) {
|
||||
listener.onHeaders(404, Headers{{"x-test", "foo"}});
|
||||
});
|
||||
|
||||
// Assuming a successful request would have assigned handle "0", verify that
|
||||
// handle has *not* been assigned.
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 2,
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": "Stream not found with handle 0"
|
||||
}
|
||||
})")));
|
||||
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 2,
|
||||
"method": "IO.read",
|
||||
"params": {
|
||||
"handle": "0",
|
||||
"size": 20
|
||||
}
|
||||
})");
|
||||
}
|
||||
|
||||
TEST_F(HostTargetTest, NetworkLoadNetworkResourceInitialNetError) {
|
||||
connect();
|
||||
|
||||
InSequence s;
|
||||
|
||||
ScopedExecutor<NetworkRequestListener> executor;
|
||||
EXPECT_CALL(
|
||||
hostTargetDelegate_,
|
||||
loadNetworkResource(
|
||||
Field(&LoadNetworkResourceRequest::url, "http://baddomain.com"), _))
|
||||
.Times(1)
|
||||
.WillOnce([&executor](
|
||||
const LoadNetworkResourceRequest& /*params*/,
|
||||
ScopedExecutor<NetworkRequestListener> executorArg) {
|
||||
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
|
||||
executor = std::move(executorArg);
|
||||
})
|
||||
.RetiresOnSaturation();
|
||||
|
||||
// Load the resource, expect a CDP resonse with no headers or status code,
|
||||
// but with success: false and a netErrorName
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": "Network.loadNetworkResource",
|
||||
"params": {
|
||||
"url": "http://baddomain.com"
|
||||
}
|
||||
})");
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 1,
|
||||
"result": {
|
||||
"resource": {
|
||||
"success": false,
|
||||
"netErrorName": "Arbitrary error string"
|
||||
}
|
||||
}
|
||||
})")));
|
||||
|
||||
executor([](NetworkRequestListener& listener) {
|
||||
listener.onError("Arbitrary error string");
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(HostTargetTest, NetworkLoadNetworkResourceStreamClosed) {
|
||||
connect();
|
||||
|
||||
InSequence s;
|
||||
|
||||
ScopedExecutor<NetworkRequestListener> executor;
|
||||
EXPECT_CALL(
|
||||
hostTargetDelegate_,
|
||||
loadNetworkResource(
|
||||
Field(&LoadNetworkResourceRequest::url, "http://example.com"), _))
|
||||
.Times(1)
|
||||
.WillOnce([&executor](
|
||||
const LoadNetworkResourceRequest& /*params*/,
|
||||
ScopedExecutor<NetworkRequestListener> executorArg) {
|
||||
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
|
||||
executor = std::move(executorArg);
|
||||
})
|
||||
.RetiresOnSaturation();
|
||||
|
||||
// Load the resource, receiving headers succesfully.
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": "Network.loadNetworkResource",
|
||||
"params": {
|
||||
"url": "http://example.com"
|
||||
}
|
||||
})");
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 1,
|
||||
"result": {
|
||||
"resource": {
|
||||
"success": true,
|
||||
"stream": "0",
|
||||
"httpStatusCode": 200,
|
||||
"headers": {
|
||||
"x-test": "foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
})")));
|
||||
|
||||
bool cancelFunctionCalled = false;
|
||||
executor([&cancelFunctionCalled](NetworkRequestListener& listener) {
|
||||
listener.setCancelFunction(
|
||||
[&cancelFunctionCalled]() { cancelFunctionCalled = true; });
|
||||
|
||||
listener.onHeaders(200, Headers{{"x-test", "foo"}});
|
||||
});
|
||||
|
||||
// Retrieve the first chunk of data.
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 2,
|
||||
"method": "IO.read",
|
||||
"params": {
|
||||
"handle": "0",
|
||||
"size": 20
|
||||
}
|
||||
})");
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 2,
|
||||
"result": {
|
||||
"data": "VGhlIG1lYW5pbmcgb2YgbGlmZSA=",
|
||||
"eof": false,
|
||||
"base64Encoded": true
|
||||
}
|
||||
})")));
|
||||
executor([](NetworkRequestListener& listener) {
|
||||
listener.onData("The meaning of life is...");
|
||||
});
|
||||
|
||||
EXPECT_FALSE(cancelFunctionCalled);
|
||||
|
||||
// Simulate the client closing the stream while data is still incoming.
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 3,
|
||||
"result": {}
|
||||
})")));
|
||||
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 3,
|
||||
"method": "IO.close",
|
||||
"params": {
|
||||
"handle": "0"
|
||||
}
|
||||
})");
|
||||
|
||||
EXPECT_TRUE(cancelFunctionCalled);
|
||||
}
|
||||
|
||||
TEST_F(HostTargetTest, NetworkLoadNetworkResourceAgentDisconnect) {
|
||||
connect();
|
||||
|
||||
InSequence s;
|
||||
|
||||
ScopedExecutor<NetworkRequestListener> executor;
|
||||
EXPECT_CALL(hostTargetDelegate_, loadNetworkResource(_, _))
|
||||
.Times(1)
|
||||
.WillOnce([&executor](
|
||||
const LoadNetworkResourceRequest& /*params*/,
|
||||
ScopedExecutor<NetworkRequestListener> executorArg) {
|
||||
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
|
||||
executor = std::move(executorArg);
|
||||
})
|
||||
.RetiresOnSaturation();
|
||||
|
||||
// Load the resource, receiving headers succesfully.
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": "Network.loadNetworkResource",
|
||||
"params": {
|
||||
"url": "http://example.com"
|
||||
}
|
||||
})");
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 1,
|
||||
"result": {
|
||||
"resource": {
|
||||
"success": true,
|
||||
"stream": "0",
|
||||
"httpStatusCode": 200,
|
||||
"headers": {
|
||||
"x-test": "foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
})")));
|
||||
|
||||
bool cancelFunctionCalled = false;
|
||||
executor([&cancelFunctionCalled](NetworkRequestListener& listener) {
|
||||
listener.setCancelFunction(
|
||||
[&cancelFunctionCalled]() { cancelFunctionCalled = true; });
|
||||
|
||||
listener.onHeaders(200, Headers{{"x-test", "foo"}});
|
||||
});
|
||||
|
||||
EXPECT_FALSE(cancelFunctionCalled);
|
||||
|
||||
// Simulate the frontend disconnecting while data is still incoming.
|
||||
toPage_->disconnect();
|
||||
|
||||
// Expect the destruction of the agent to notify the platform implementation
|
||||
// that it may cancel any download.
|
||||
EXPECT_TRUE(cancelFunctionCalled);
|
||||
|
||||
// The host may still hold a scoped executor, but our listener has now been
|
||||
// destroyed because it was owned by the (disconnected) agent, so we expect
|
||||
// a late executor call to be a) safe and b) never execute.
|
||||
bool callbackCalledAfterDisconnect = false;
|
||||
executor(
|
||||
[&callbackCalledAfterDisconnect](NetworkRequestListener& /*listener*/) {
|
||||
callbackCalledAfterDisconnect = true;
|
||||
});
|
||||
EXPECT_FALSE(callbackCalledAfterDisconnect);
|
||||
}
|
||||
|
||||
TEST_F(HostTargetTest, NetworkLoadNetworkResourceNotImplementedByDelegate) {
|
||||
connect();
|
||||
|
||||
InSequence s;
|
||||
|
||||
EXPECT_CALL(
|
||||
hostTargetDelegate_,
|
||||
loadNetworkResource(
|
||||
Field(&LoadNetworkResourceRequest::url, "http://example.com"), _))
|
||||
.Times(1)
|
||||
.WillOnce([](const LoadNetworkResourceRequest& /*params*/,
|
||||
ScopedExecutor<NetworkRequestListener> /*executor*/) {
|
||||
throw NotImplementedException(
|
||||
"This delegate does not implement loadNetworkResource.");
|
||||
})
|
||||
.RetiresOnSaturation();
|
||||
|
||||
// The delegate's loadNetworkResource may throw immediately - verify this is
|
||||
// handled and that we clean up.
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": "This delegate does not implement loadNetworkResource."
|
||||
}
|
||||
})")));
|
||||
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": "Network.loadNetworkResource",
|
||||
"params": {
|
||||
"url": "http://example.com"
|
||||
}
|
||||
})");
|
||||
|
||||
// Check no stream is retained
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 2,
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": "Stream not found: 0"
|
||||
}
|
||||
})")));
|
||||
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 2,
|
||||
"method": "IO.close",
|
||||
"params": {
|
||||
"handle": "0"
|
||||
}
|
||||
})");
|
||||
}
|
||||
|
||||
TEST_F(HostTargetTest, NetworkLoadNetworkResource3xx) {
|
||||
connect();
|
||||
|
||||
InSequence s;
|
||||
|
||||
ScopedExecutor<NetworkRequestListener> executor;
|
||||
EXPECT_CALL(
|
||||
hostTargetDelegate_,
|
||||
loadNetworkResource(
|
||||
Field(&LoadNetworkResourceRequest::url, "http://example.com/3xx"), _))
|
||||
.Times(1)
|
||||
.WillOnce([&executor](
|
||||
const LoadNetworkResourceRequest& /*params*/,
|
||||
ScopedExecutor<NetworkRequestListener> executorArg) {
|
||||
// Capture the ScopedExecutor<NetworkRequestListener> to use later.
|
||||
executor = std::move(executorArg);
|
||||
})
|
||||
.RetiresOnSaturation();
|
||||
|
||||
// We don't support 3xx responses, and treat them as a CDP error (as if not
|
||||
// implemented so that the frontend may fall back.
|
||||
toPage_->sendMessage(R"({
|
||||
"id": 1,
|
||||
"method": "Network.loadNetworkResource",
|
||||
"params": {
|
||||
"url": "http://example.com/3xx"
|
||||
}
|
||||
})");
|
||||
|
||||
EXPECT_CALL(fromPage(), onMessage(JsonEq(R"({
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": "Handling of status 301 not implemented."
|
||||
}
|
||||
})")));
|
||||
|
||||
executor([](NetworkRequestListener& listener) {
|
||||
listener.onHeaders(301, Headers{{"Location", "/new"}});
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern
|
||||
|
||||
@@ -127,6 +127,12 @@ class MockHostTargetDelegate : public HostTargetDelegate {
|
||||
onSetPausedInDebuggerMessage,
|
||||
(const OverlaySetPausedInDebuggerMessageRequest& request),
|
||||
(override));
|
||||
MOCK_METHOD(
|
||||
void,
|
||||
loadNetworkResource,
|
||||
(const LoadNetworkResourceRequest& params,
|
||||
ScopedExecutor<NetworkRequestListener> executor),
|
||||
(override));
|
||||
};
|
||||
|
||||
class MockInstanceTargetDelegate : public InstanceTargetDelegate {};
|
||||
|
||||
Reference in New Issue
Block a user