mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Implement Runtime.callFunctionOn
Summary: [Hermes][Inspector] Implement the CDP API for calling a function on an object. Changelog: [Internal] Reviewed By: avp Differential Revision: D33722301 fbshipit-source-id: da26e865cf29920be77c5c602dde1b443b4c64da
This commit is contained in:
committed by
Facebook GitHub Bot
parent
cdfddb4dad
commit
eb08af57a9
@@ -14,6 +14,7 @@
|
||||
#include <folly/Conv.h>
|
||||
#include <folly/Executor.h>
|
||||
#include <folly/Function.h>
|
||||
#include <folly/json.h>
|
||||
#include <glog/logging.h>
|
||||
#include <hermes/inspector/Inspector.h>
|
||||
#include <hermes/inspector/chrome/MessageConverters.h>
|
||||
@@ -100,11 +101,15 @@ class Connection::Impl : public inspector::InspectorObserver,
|
||||
void handle(
|
||||
const m::heapProfiler::GetObjectByHeapObjectIdRequest &req) override;
|
||||
void handle(const m::heapProfiler::GetHeapObjectIdRequest &req) override;
|
||||
void handle(const m::runtime::CallFunctionOnRequest &req) override;
|
||||
void handle(const m::runtime::EvaluateRequest &req) override;
|
||||
void handle(const m::runtime::GetPropertiesRequest &req) override;
|
||||
void handle(const m::runtime::RunIfWaitingForDebuggerRequest &req) override;
|
||||
|
||||
private:
|
||||
// The execution context id reported back by the ExecutionContextCreated
|
||||
// notification. We only ever expect this execution context id.
|
||||
static constexpr int32_t kHermesExecutionContextId = 1;
|
||||
std::vector<m::runtime::PropertyDescriptor> makePropsFromScope(
|
||||
std::pair<uint32_t, uint32_t> frameAndScopeIndex,
|
||||
const std::string &objectGroup,
|
||||
@@ -291,7 +296,7 @@ void Connection::Impl::onContextCreated(Inspector &inspector) {
|
||||
// Right now, Hermes only has the notion of one JS context per VM instance,
|
||||
// so we just always name the single JS context with id=1 and name=hermes.
|
||||
m::runtime::ExecutionContextCreatedNotification note;
|
||||
note.context.id = 1;
|
||||
note.context.id = kHermesExecutionContextId;
|
||||
note.context.name = "hermes";
|
||||
|
||||
sendNotificationToClientViaExecutor(note);
|
||||
@@ -370,6 +375,8 @@ void Connection::Impl::onScriptParsed(
|
||||
m::debugger::ScriptParsedNotification note;
|
||||
note.scriptId = folly::to<std::string>(info.fileId);
|
||||
note.url = info.fileName;
|
||||
// TODO(jpporto): fix test cases sending invalid context id.
|
||||
// note.executionContextId = kHermesExecutionContextId;
|
||||
|
||||
if (!info.sourceMappingUrl.empty()) {
|
||||
note.sourceMapURL = info.sourceMappingUrl;
|
||||
@@ -397,6 +404,8 @@ void Connection::Impl::onMessageAdded(
|
||||
const ConsoleMessageInfo &info) {
|
||||
m::runtime::ConsoleAPICalledNotification apiCalledNote;
|
||||
apiCalledNote.type = info.level;
|
||||
// TODO(jpporto): fix test cases sending invalid context id.
|
||||
// apiCalledNote.executionContextId = kHermesExecutionContextId;
|
||||
|
||||
size_t argsSize = info.args.size(getRuntime());
|
||||
for (size_t index = 0; index < argsSize; ++index) {
|
||||
@@ -711,6 +720,315 @@ void Connection::Impl::handle(
|
||||
.thenError<std::exception>(sendErrorToClient(req.id));
|
||||
}
|
||||
|
||||
namespace {
|
||||
/// Runtime.CallArguments can have their values specified "inline", or they can
|
||||
/// have remote references. The inline values are eval'd together with the
|
||||
/// Runtime.CallFunctionOn.functionDeclaration (see CallFunctionOnBuilder
|
||||
/// below), while remote object Ids need to be resolved outside of the VM.
|
||||
class CallFunctionOnArgument {
|
||||
public:
|
||||
explicit CallFunctionOnArgument(
|
||||
folly::Optional<m::runtime::RemoteObjectId> maybeObjectId)
|
||||
: maybeObjectId_(std::move(maybeObjectId)) {}
|
||||
|
||||
/// Computes the real value for this argument, which can be an object
|
||||
/// referenced by maybeObjectId_, or the given evaldValue. Throws if
|
||||
/// maybeObjectId_ is not empty but references an unknown object.
|
||||
jsi::Value value(
|
||||
jsi::Runtime &rt,
|
||||
RemoteObjectsTable &objTable,
|
||||
jsi::Value evaldValue) const {
|
||||
if (maybeObjectId_) {
|
||||
assert(evaldValue.isUndefined() && "expected undefined placeholder");
|
||||
return getValueFromId(rt, objTable, *maybeObjectId_);
|
||||
}
|
||||
|
||||
return evaldValue;
|
||||
}
|
||||
|
||||
private:
|
||||
/// Returns the jsi::Object for the given objId. Throws if such object can't
|
||||
/// be found.
|
||||
static jsi::Value getValueFromId(
|
||||
jsi::Runtime &rt,
|
||||
RemoteObjectsTable &objTable,
|
||||
m::runtime::RemoteObjectId objId) {
|
||||
if (const jsi::Value *ptr = objTable.getValue(objId)) {
|
||||
return jsi::Value(rt, *ptr);
|
||||
}
|
||||
|
||||
throw std::runtime_error("unknown object id " + objId);
|
||||
}
|
||||
|
||||
folly::Optional<m::runtime::RemoteObjectId> maybeObjectId_;
|
||||
};
|
||||
|
||||
/// Functor that should be used to run the result of eval-ing a CallFunctionOn
|
||||
/// request.
|
||||
class CallFunctionOnRunner {
|
||||
public:
|
||||
static constexpr size_t kJsThisIndex = 0;
|
||||
static constexpr size_t kFirstArgIndex = 1;
|
||||
|
||||
// N.B.: constexpr char[] broke react-native-oss-android.
|
||||
static const char *kJsThisArgPlaceholder;
|
||||
|
||||
CallFunctionOnRunner() = default;
|
||||
CallFunctionOnRunner(CallFunctionOnRunner &&) = default;
|
||||
CallFunctionOnRunner &operator=(CallFunctionOnRunner &&) = default;
|
||||
|
||||
/// Performs the actual Runtime.CallFunctionOn request. It assumes.
|
||||
/// \p evalResult is the result of invoking the Inspector's evaluate() method
|
||||
/// on the expression built by the CallFunctionOnBuilder below.
|
||||
jsi::Value operator()(
|
||||
jsi::Runtime &rt,
|
||||
RemoteObjectsTable &objTable,
|
||||
const facebook::hermes::debugger::EvalResult &evalResult) {
|
||||
// The eval result is an array [a0, a1, ..., an, func] (see
|
||||
// CallFunctionOnBuilder below).
|
||||
auto argsAndFunc = evalResult.value.getObject(rt).getArray(rt);
|
||||
assert(
|
||||
argsAndFunc.length(rt) == thisAndArguments_.size() + 1 &&
|
||||
"Unexpected result size");
|
||||
|
||||
// now resolve the arguments to the call, including "this".
|
||||
std::vector<jsi::Value> arguments(thisAndArguments_.size() - 1);
|
||||
|
||||
jsi::Object jsThis =
|
||||
getJsThis(rt, objTable, argsAndFunc.getValueAtIndex(rt, kJsThisIndex));
|
||||
|
||||
int i = kFirstArgIndex;
|
||||
for (/*i points to the first param*/; i < thisAndArguments_.size(); ++i) {
|
||||
arguments[i - kFirstArgIndex] = thisAndArguments_[i].value(
|
||||
rt, objTable, argsAndFunc.getValueAtIndex(rt, i));
|
||||
}
|
||||
|
||||
// i is now func's index.
|
||||
jsi::Function func =
|
||||
argsAndFunc.getValueAtIndex(rt, i).getObject(rt).getFunction(rt);
|
||||
|
||||
return func.callWithThis(
|
||||
rt,
|
||||
std::move(jsThis),
|
||||
static_cast<const jsi::Value *>(arguments.data()),
|
||||
arguments.size());
|
||||
}
|
||||
|
||||
private:
|
||||
friend class CallFunctionOnBuilder;
|
||||
|
||||
CallFunctionOnRunner(const CallFunctionOnRunner &) = delete;
|
||||
CallFunctionOnRunner &operator=(const CallFunctionOnRunner &) = delete;
|
||||
|
||||
CallFunctionOnRunner(
|
||||
std::vector<CallFunctionOnArgument> thisAndArguments,
|
||||
folly::Optional<m::runtime::ExecutionContextId> executionContextId)
|
||||
: thisAndArguments_(std::move(thisAndArguments)),
|
||||
executionContextId_(std::move(executionContextId)) {}
|
||||
|
||||
/// Resolves the js "this" for the request, which lives in
|
||||
/// thisAndArguments_[kJsThisIndex]. \p evaldThis should either be undefined,
|
||||
/// or the placeholder indicating that globalThis should be used.
|
||||
jsi::Object getJsThis(
|
||||
jsi::Runtime &rt,
|
||||
RemoteObjectsTable &objTable,
|
||||
jsi::Value evaldThis) const {
|
||||
// In the future we may support multiple execution context ids; for now,
|
||||
// there's only one.
|
||||
(void)executionContextId_;
|
||||
|
||||
// Either evaldThis is undefined (because the request had an object id
|
||||
// specifying "this"), or it should be a string (i.e., the placeholder
|
||||
// kJsThisArgPlaceholder).
|
||||
assert(
|
||||
(evaldThis.isUndefined() ||
|
||||
(evaldThis.isString() &&
|
||||
evaldThis.getString(rt).utf8(rt) == kJsThisArgPlaceholder)) &&
|
||||
"unexpected value for jsThis argument placeholder");
|
||||
|
||||
// Need to save this information because of the std::move() below.
|
||||
const bool useGlobalThis = evaldThis.isString();
|
||||
jsi::Value value = thisAndArguments_[kJsThisIndex].value(
|
||||
rt, objTable, std::move(evaldThis));
|
||||
|
||||
return useGlobalThis ? rt.global() : value.getObject(rt);
|
||||
}
|
||||
|
||||
std::vector<CallFunctionOnArgument> thisAndArguments_;
|
||||
folly::Optional<m::runtime::ExecutionContextId> executionContextId_;
|
||||
};
|
||||
|
||||
/*static*/ const char *CallFunctionOnRunner::kJsThisArgPlaceholder =
|
||||
"jsThis is Execution Context";
|
||||
|
||||
/// Returns true if \p str is a number-like string value (e.g., Infinity),
|
||||
/// and false otherwise.
|
||||
bool unserializableValueLooksLikeNumber(const std::string &str) {
|
||||
return str == "Infinity" || str == "-Infinity" || str == "NaN";
|
||||
}
|
||||
|
||||
/// Helper class that processes a Runtime.CallFunctionOn request, and
|
||||
/// builds an expression string that, once eval()d, yields an Array with the
|
||||
/// CallArguments as well as the function to run. The generated array is
|
||||
///
|
||||
/// [JsThis, P0, P1, P2, P3, Pn, F]
|
||||
///
|
||||
/// where:
|
||||
/// * F is the functionDeclaration in the request
|
||||
/// * JsThis is either:
|
||||
/// * undefined (if the request has an object ID); or
|
||||
/// * the placeholder kJsThisArgPlaceholder
|
||||
/// * Pi is either:
|
||||
/// * the string in CallArgument[i].unserializableValue; or
|
||||
/// * the string in CallArgument[i].value; or
|
||||
/// * arguments[j] (i.e., the j-th argument passed to the newly built
|
||||
/// function), j being the j-th CallArgument with an ObjectId. This is
|
||||
/// needed because there's no easy way to express the objects referred
|
||||
/// to by object ids by name.
|
||||
class CallFunctionOnBuilder {
|
||||
public:
|
||||
explicit CallFunctionOnBuilder(const m::runtime::CallFunctionOnRequest &req)
|
||||
: executionContextId_(req.executionContextId) {
|
||||
out_ << "[";
|
||||
thisAndArguments_.emplace_back(CallFunctionOnArgument(req.objectId));
|
||||
if (req.objectId) {
|
||||
out_ << "undefined, ";
|
||||
} else {
|
||||
out_ << '\'' << CallFunctionOnRunner::kJsThisArgPlaceholder << "', ";
|
||||
}
|
||||
|
||||
addParams(req.arguments);
|
||||
out_ << req.functionDeclaration;
|
||||
out_ << "]";
|
||||
};
|
||||
|
||||
/// Extracts the functions that handles the CallFunctionOn requests, as well
|
||||
/// as the list of object ids that must be passed when calling it.
|
||||
std::pair<std::string, CallFunctionOnRunner> expressionAndRunner() && {
|
||||
return std::make_pair(
|
||||
std::move(out_).str(),
|
||||
CallFunctionOnRunner(
|
||||
std::move(thisAndArguments_), std::move(executionContextId_)));
|
||||
}
|
||||
|
||||
private:
|
||||
void addParams(const folly::Optional<std::vector<m::runtime::CallArgument>>
|
||||
&maybeArguments) {
|
||||
if (maybeArguments) {
|
||||
for (const auto &ca : *maybeArguments) {
|
||||
addParam(ca);
|
||||
thisAndArguments_.emplace_back(CallFunctionOnArgument(ca.objectId));
|
||||
out_ << ", ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addParam(const m::runtime::CallArgument &ca) {
|
||||
if (ca.objectId) {
|
||||
out_ << "undefined";
|
||||
} else if (ca.value) {
|
||||
// TODO: this may throw if ca.value is a CBOR (see RFC 8949), but the
|
||||
// chrome debugger doesn't seem to send those.
|
||||
out_ << "(" << folly::toJson(*ca.value) << ")";
|
||||
} else if (ca.unserializableValue) {
|
||||
if (unserializableValueLooksLikeNumber(*ca.unserializableValue)) {
|
||||
out_ << "+(" << *ca.unserializableValue << ")";
|
||||
} else {
|
||||
out_ << *ca.unserializableValue;
|
||||
}
|
||||
} else {
|
||||
throw std::runtime_error("unknown payload for CallParam");
|
||||
}
|
||||
}
|
||||
|
||||
std::ostringstream out_;
|
||||
|
||||
std::vector<CallFunctionOnArgument> thisAndArguments_;
|
||||
folly::Optional<m::runtime::ExecutionContextId> executionContextId_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void Connection::Impl::handle(const m::runtime::CallFunctionOnRequest &req) {
|
||||
std::string expression;
|
||||
CallFunctionOnRunner runner;
|
||||
|
||||
auto validateAndParseRequest =
|
||||
[&expression, &runner](const m::runtime::CallFunctionOnRequest &req)
|
||||
-> folly::Optional<std::string> {
|
||||
if (req.objectId.hasValue() == req.executionContextId.hasValue()) {
|
||||
return std::string(
|
||||
"The request must specify either object id or execution context id.");
|
||||
}
|
||||
|
||||
if (!req.objectId) {
|
||||
assert(
|
||||
req.executionContextId &&
|
||||
"should not be here if both object id and execution context id are missing");
|
||||
if (*req.executionContextId != kHermesExecutionContextId) {
|
||||
return "unknown execution context id " +
|
||||
std::to_string(*req.executionContextId);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
std::tie(expression, runner) =
|
||||
CallFunctionOnBuilder(req).expressionAndRunner();
|
||||
} catch (const std::exception &e) {
|
||||
return std::string(e.what());
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
if (auto errMsg = validateAndParseRequest(req)) {
|
||||
sendErrorToClientViaExecutor(req.id, *errMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
auto remoteObjPtr = std::make_shared<m::runtime::RemoteObject>();
|
||||
inspector_
|
||||
->evaluate(
|
||||
0, // Top of the stackframe
|
||||
expression,
|
||||
[this,
|
||||
remoteObjPtr,
|
||||
objectGroup = req.objectGroup,
|
||||
jsThisId = req.objectId,
|
||||
executionContextId = req.executionContextId,
|
||||
byValue = req.returnByValue.value_or(false),
|
||||
runner =
|
||||
std::move(runner)](const facebook::hermes::debugger::EvalResult
|
||||
&evalResult) mutable {
|
||||
if (evalResult.isException) {
|
||||
return;
|
||||
}
|
||||
|
||||
*remoteObjPtr = m::runtime::makeRemoteObject(
|
||||
getRuntime(),
|
||||
runner(getRuntime(), objTable_, evalResult),
|
||||
objTable_,
|
||||
objectGroup.value_or("ConsoleObjectGroup"),
|
||||
byValue);
|
||||
})
|
||||
.via(executor_.get())
|
||||
.thenValue(
|
||||
[this, id = req.id, remoteObjPtr](debugger::EvalResult result) {
|
||||
m::debugger::EvaluateOnCallFrameResponse resp;
|
||||
resp.id = id;
|
||||
|
||||
if (result.isException) {
|
||||
resp.exceptionDetails =
|
||||
m::runtime::makeExceptionDetails(result.exceptionDetails);
|
||||
} else {
|
||||
resp.result = *remoteObjPtr;
|
||||
}
|
||||
|
||||
sendResponseToClient(resp);
|
||||
})
|
||||
.thenError<std::exception>(sendErrorToClient(req.id));
|
||||
}
|
||||
|
||||
void Connection::Impl::handle(const m::runtime::EvaluateRequest &req) {
|
||||
auto remoteObjPtr = std::make_shared<m::runtime::RemoteObject>();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
// @generated SignedSource<<522f29c54f207a4f7b5c33af07cf64d0>>
|
||||
// @generated <<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@IsG>>
|
||||
|
||||
#include "MessageTypes.h"
|
||||
|
||||
@@ -60,6 +60,7 @@ std::unique_ptr<Request> Request::fromJsonThrowOnError(const std::string &str) {
|
||||
makeUnique<heapProfiler::StopTrackingHeapObjectsRequest>},
|
||||
{"HeapProfiler.takeHeapSnapshot",
|
||||
makeUnique<heapProfiler::TakeHeapSnapshotRequest>},
|
||||
{"Runtime.callFunctionOn", makeUnique<runtime::CallFunctionOnRequest>},
|
||||
{"Runtime.evaluate", makeUnique<runtime::EvaluateRequest>},
|
||||
{"Runtime.getProperties", makeUnique<runtime::GetPropertiesRequest>},
|
||||
{"Runtime.runIfWaitingForDebugger",
|
||||
@@ -274,6 +275,21 @@ dynamic heapProfiler::SamplingHeapProfile::toDynamic() const {
|
||||
return obj;
|
||||
}
|
||||
|
||||
runtime::CallArgument::CallArgument(const dynamic &obj) {
|
||||
assign(value, obj, "value");
|
||||
assign(unserializableValue, obj, "unserializableValue");
|
||||
assign(objectId, obj, "objectId");
|
||||
}
|
||||
|
||||
dynamic runtime::CallArgument::toDynamic() const {
|
||||
dynamic obj = dynamic::object;
|
||||
|
||||
put(obj, "value", value);
|
||||
put(obj, "unserializableValue", unserializableValue);
|
||||
put(obj, "objectId", objectId);
|
||||
return obj;
|
||||
}
|
||||
|
||||
runtime::ExecutionContextDescription::ExecutionContextDescription(
|
||||
const dynamic &obj) {
|
||||
assign(id, obj, "id");
|
||||
@@ -931,6 +947,49 @@ void heapProfiler::TakeHeapSnapshotRequest::accept(
|
||||
handler.handle(*this);
|
||||
}
|
||||
|
||||
runtime::CallFunctionOnRequest::CallFunctionOnRequest()
|
||||
: Request("Runtime.callFunctionOn") {}
|
||||
|
||||
runtime::CallFunctionOnRequest::CallFunctionOnRequest(const dynamic &obj)
|
||||
: Request("Runtime.callFunctionOn") {
|
||||
assign(id, obj, "id");
|
||||
assign(method, obj, "method");
|
||||
|
||||
dynamic params = obj.at("params");
|
||||
assign(functionDeclaration, params, "functionDeclaration");
|
||||
assign(objectId, params, "objectId");
|
||||
assign(arguments, params, "arguments");
|
||||
assign(silent, params, "silent");
|
||||
assign(returnByValue, params, "returnByValue");
|
||||
assign(userGesture, params, "userGesture");
|
||||
assign(awaitPromise, params, "awaitPromise");
|
||||
assign(executionContextId, params, "executionContextId");
|
||||
assign(objectGroup, params, "objectGroup");
|
||||
}
|
||||
|
||||
dynamic runtime::CallFunctionOnRequest::toDynamic() const {
|
||||
dynamic params = dynamic::object;
|
||||
put(params, "functionDeclaration", functionDeclaration);
|
||||
put(params, "objectId", objectId);
|
||||
put(params, "arguments", arguments);
|
||||
put(params, "silent", silent);
|
||||
put(params, "returnByValue", returnByValue);
|
||||
put(params, "userGesture", userGesture);
|
||||
put(params, "awaitPromise", awaitPromise);
|
||||
put(params, "executionContextId", executionContextId);
|
||||
put(params, "objectGroup", objectGroup);
|
||||
|
||||
dynamic obj = dynamic::object;
|
||||
put(obj, "id", id);
|
||||
put(obj, "method", method);
|
||||
put(obj, "params", std::move(params));
|
||||
return obj;
|
||||
}
|
||||
|
||||
void runtime::CallFunctionOnRequest::accept(RequestHandler &handler) const {
|
||||
handler.handle(*this);
|
||||
}
|
||||
|
||||
runtime::EvaluateRequest::EvaluateRequest() : Request("Runtime.evaluate") {}
|
||||
|
||||
runtime::EvaluateRequest::EvaluateRequest(const dynamic &obj)
|
||||
@@ -1187,6 +1246,25 @@ dynamic heapProfiler::StopSamplingResponse::toDynamic() const {
|
||||
return obj;
|
||||
}
|
||||
|
||||
runtime::CallFunctionOnResponse::CallFunctionOnResponse(const dynamic &obj) {
|
||||
assign(id, obj, "id");
|
||||
|
||||
dynamic res = obj.at("result");
|
||||
assign(result, res, "result");
|
||||
assign(exceptionDetails, res, "exceptionDetails");
|
||||
}
|
||||
|
||||
dynamic runtime::CallFunctionOnResponse::toDynamic() const {
|
||||
dynamic res = dynamic::object;
|
||||
put(res, "result", result);
|
||||
put(res, "exceptionDetails", exceptionDetails);
|
||||
|
||||
dynamic obj = dynamic::object;
|
||||
put(obj, "id", id);
|
||||
put(obj, "result", std::move(res));
|
||||
return obj;
|
||||
}
|
||||
|
||||
runtime::EvaluateResponse::EvaluateResponse(const dynamic &obj) {
|
||||
assign(id, obj, "id");
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
// @generated SignedSource<<a541d174394c8959b9fb6a7c575e7040>>
|
||||
// @generated <<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@IsG>>
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -48,7 +48,10 @@ struct StepOverRequest;
|
||||
} // namespace debugger
|
||||
|
||||
namespace runtime {
|
||||
struct CallArgument;
|
||||
struct CallFrame;
|
||||
struct CallFunctionOnRequest;
|
||||
struct CallFunctionOnResponse;
|
||||
struct ConsoleAPICalledNotification;
|
||||
struct EvaluateRequest;
|
||||
struct EvaluateResponse;
|
||||
@@ -122,6 +125,7 @@ struct RequestHandler {
|
||||
virtual void handle(
|
||||
const heapProfiler::StopTrackingHeapObjectsRequest &req) = 0;
|
||||
virtual void handle(const heapProfiler::TakeHeapSnapshotRequest &req) = 0;
|
||||
virtual void handle(const runtime::CallFunctionOnRequest &req) = 0;
|
||||
virtual void handle(const runtime::EvaluateRequest &req) = 0;
|
||||
virtual void handle(const runtime::GetPropertiesRequest &req) = 0;
|
||||
virtual void handle(const runtime::RunIfWaitingForDebuggerRequest &req) = 0;
|
||||
@@ -156,6 +160,7 @@ struct NoopRequestHandler : public RequestHandler {
|
||||
void handle(
|
||||
const heapProfiler::StopTrackingHeapObjectsRequest &req) override {}
|
||||
void handle(const heapProfiler::TakeHeapSnapshotRequest &req) override {}
|
||||
void handle(const runtime::CallFunctionOnRequest &req) override {}
|
||||
void handle(const runtime::EvaluateRequest &req) override {}
|
||||
void handle(const runtime::GetPropertiesRequest &req) override {}
|
||||
void handle(const runtime::RunIfWaitingForDebuggerRequest &req) override {}
|
||||
@@ -281,6 +286,16 @@ struct heapProfiler::SamplingHeapProfile : public Serializable {
|
||||
std::vector<heapProfiler::SamplingHeapProfileSample> samples;
|
||||
};
|
||||
|
||||
struct runtime::CallArgument : public Serializable {
|
||||
CallArgument() = default;
|
||||
explicit CallArgument(const folly::dynamic &obj);
|
||||
folly::dynamic toDynamic() const override;
|
||||
|
||||
folly::Optional<folly::dynamic> value;
|
||||
folly::Optional<runtime::UnserializableValue> unserializableValue;
|
||||
folly::Optional<runtime::RemoteObjectId> objectId;
|
||||
};
|
||||
|
||||
struct runtime::ExecutionContextDescription : public Serializable {
|
||||
ExecutionContextDescription() = default;
|
||||
explicit ExecutionContextDescription(const folly::dynamic &obj);
|
||||
@@ -546,6 +561,24 @@ struct heapProfiler::TakeHeapSnapshotRequest : public Request {
|
||||
folly::Optional<bool> treatGlobalObjectsAsRoots;
|
||||
};
|
||||
|
||||
struct runtime::CallFunctionOnRequest : public Request {
|
||||
CallFunctionOnRequest();
|
||||
explicit CallFunctionOnRequest(const folly::dynamic &obj);
|
||||
|
||||
folly::dynamic toDynamic() const override;
|
||||
void accept(RequestHandler &handler) const override;
|
||||
|
||||
std::string functionDeclaration;
|
||||
folly::Optional<runtime::RemoteObjectId> objectId;
|
||||
folly::Optional<std::vector<runtime::CallArgument>> arguments;
|
||||
folly::Optional<bool> silent;
|
||||
folly::Optional<bool> returnByValue;
|
||||
folly::Optional<bool> userGesture;
|
||||
folly::Optional<bool> awaitPromise;
|
||||
folly::Optional<runtime::ExecutionContextId> executionContextId;
|
||||
folly::Optional<std::string> objectGroup;
|
||||
};
|
||||
|
||||
struct runtime::EvaluateRequest : public Request {
|
||||
EvaluateRequest();
|
||||
explicit EvaluateRequest(const folly::dynamic &obj);
|
||||
@@ -658,6 +691,15 @@ struct heapProfiler::StopSamplingResponse : public Response {
|
||||
heapProfiler::SamplingHeapProfile profile{};
|
||||
};
|
||||
|
||||
struct runtime::CallFunctionOnResponse : public Response {
|
||||
CallFunctionOnResponse() = default;
|
||||
explicit CallFunctionOnResponse(const folly::dynamic &obj);
|
||||
folly::dynamic toDynamic() const override;
|
||||
|
||||
runtime::RemoteObject result{};
|
||||
folly::Optional<runtime::ExceptionDetails> exceptionDetails;
|
||||
};
|
||||
|
||||
struct runtime::EvaluateResponse : public Response {
|
||||
EvaluateResponse() = default;
|
||||
explicit EvaluateResponse(const folly::dynamic &obj);
|
||||
|
||||
@@ -407,7 +407,7 @@ std::unordered_map<std::string, std::string> expectProps(
|
||||
m::runtime::PropertyDescriptor &desc = resp.result[i];
|
||||
|
||||
auto infoIt = infos.find(desc.name);
|
||||
EXPECT_FALSE(infoIt == infos.end());
|
||||
EXPECT_FALSE(infoIt == infos.end()) << desc.name;
|
||||
|
||||
if (infoIt != infos.end()) {
|
||||
const PropInfo &info = infoIt->second;
|
||||
@@ -460,6 +460,25 @@ void expectEvalResponse(
|
||||
expectProps(conn, id + 1, resp.result.objectId.value(), infos);
|
||||
}
|
||||
|
||||
m::runtime::CallArgument makeValueCallArgument(folly::dynamic val) {
|
||||
m::runtime::CallArgument ret;
|
||||
ret.value = val;
|
||||
return ret;
|
||||
}
|
||||
|
||||
m::runtime::CallArgument makeUnserializableCallArgument(std::string val) {
|
||||
m::runtime::CallArgument ret;
|
||||
ret.unserializableValue = std::move(val);
|
||||
return ret;
|
||||
}
|
||||
|
||||
m::runtime::CallArgument makeObjectIdCallArgument(
|
||||
m::runtime::RemoteObjectId objectId) {
|
||||
m::runtime::CallArgument ret;
|
||||
ret.objectId = std::move(objectId);
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ConnectionTests, testRespondsOkToUnknownRequests) {
|
||||
@@ -1981,6 +2000,231 @@ TEST(ConnectionTests, testScopeVariables) {
|
||||
expectNotification<m::debugger::ResumedNotification>(conn);
|
||||
}
|
||||
|
||||
TEST(ConnectionTests, testRuntimeCallFunctionOnObject) {
|
||||
TestContext context;
|
||||
AsyncHermesRuntime &asyncRuntime = context.runtime();
|
||||
SyncConnection &conn = context.conn();
|
||||
int msgId = 1;
|
||||
|
||||
asyncRuntime.executeScriptAsync(R"(
|
||||
debugger;
|
||||
)");
|
||||
|
||||
send<m::debugger::EnableRequest>(conn, msgId++);
|
||||
expectExecutionContextCreated(conn);
|
||||
expectNotification<m::debugger::ScriptParsedNotification>(conn);
|
||||
|
||||
// create a new Object() that will be used as "this" below.
|
||||
m::runtime::RemoteObjectId thisId;
|
||||
{
|
||||
sendRuntimeEvalRequest(conn, msgId, "new Object()");
|
||||
auto resp = expectResponse<m::runtime::EvaluateResponse>(conn, msgId++);
|
||||
ASSERT_TRUE(resp.result.objectId) << resp.toDynamic();
|
||||
thisId = *resp.result.objectId;
|
||||
}
|
||||
|
||||
// expectedPropInfos are properties that are expected to exist in thisId. It
|
||||
// is modified by addMember (below).
|
||||
std::unordered_map<std::string, PropInfo> expectedPropInfos;
|
||||
|
||||
// Add __proto__ as it always exists.
|
||||
expectedPropInfos.emplace("__proto__", PropInfo("object"));
|
||||
|
||||
/// addMember sends Runtime.callFunctionOn() requests with a function
|
||||
/// declaration that simply adds a new property called \p propName with type
|
||||
/// \p type to the remote object \p id. \p ca is the property's value.
|
||||
/// The new property must not exist in \p id unless \p allowRedefinition is
|
||||
/// true.
|
||||
auto addMember = [&](const m::runtime::RemoteObjectId id,
|
||||
const char *type,
|
||||
const char *propName,
|
||||
const m::runtime::CallArgument &ca,
|
||||
bool allowRedefinition = false) {
|
||||
m::runtime::CallFunctionOnRequest req;
|
||||
req.id = msgId++;
|
||||
req.functionDeclaration =
|
||||
std::string("function(e){const r=\"") + propName + "\"; this[r]=e,r}";
|
||||
req.arguments = std::vector<m::runtime::CallArgument>{ca};
|
||||
req.objectId = thisId;
|
||||
conn.send(req.toJson());
|
||||
expectResponse<m::runtime::CallFunctionOnResponse>(conn, req.id);
|
||||
|
||||
auto it = expectedPropInfos.emplace(propName, PropInfo(type));
|
||||
|
||||
EXPECT_TRUE(allowRedefinition || it.second)
|
||||
<< "property \"" << propName << "\" redefined.";
|
||||
|
||||
if (ca.value) {
|
||||
it.first->second.setValue(*ca.value);
|
||||
}
|
||||
|
||||
if (ca.unserializableValue) {
|
||||
it.first->second.setUnserializableValue(*ca.unserializableValue);
|
||||
}
|
||||
};
|
||||
|
||||
addMember(thisId, "boolean", "b", makeValueCallArgument(true));
|
||||
addMember(thisId, "number", "num", makeValueCallArgument(12));
|
||||
addMember(thisId, "string", "str", makeValueCallArgument("string value"));
|
||||
addMember(thisId, "object", "self_ref", makeObjectIdCallArgument(thisId));
|
||||
addMember(
|
||||
thisId, "number", "inf", makeUnserializableCallArgument("Infinity"));
|
||||
addMember(
|
||||
thisId, "number", "ni", makeUnserializableCallArgument("-Infinity"));
|
||||
addMember(thisId, "number", "nan", makeUnserializableCallArgument("NaN"));
|
||||
|
||||
/// ensures that \p objId has all of the expected properties; Returns the
|
||||
/// runtime::RemoteObjectId for the "self_ref" property (which must exist).
|
||||
auto verifyObjShape = [&](const m::runtime::RemoteObjectId &objId)
|
||||
-> folly::Optional<std::string> {
|
||||
auto objProps = expectProps(conn, msgId++, objId, expectedPropInfos);
|
||||
EXPECT_TRUE(objProps.count("__proto__"));
|
||||
auto objPropIt = objProps.find("self_ref");
|
||||
if (objPropIt == objProps.end()) {
|
||||
EXPECT_TRUE(false) << "missing \"self_ref\" property.";
|
||||
return {};
|
||||
}
|
||||
return objPropIt->second;
|
||||
};
|
||||
|
||||
// Verify that thisId has the correct shape.
|
||||
auto selfRefId = verifyObjShape(thisId);
|
||||
ASSERT_TRUE(selfRefId);
|
||||
// Then verify that the self reference has the correct shape. If thisId does
|
||||
// not have the "self_ref" property the call to verifyObjShape will return an
|
||||
// empty Optional, as well as report an error.
|
||||
selfRefId = verifyObjShape(*selfRefId);
|
||||
ASSERT_TRUE(selfRefId);
|
||||
|
||||
// Now we modify the self reference, which should cause thisId to change
|
||||
// as well.
|
||||
const bool kAllowRedefinition = true;
|
||||
|
||||
addMember(
|
||||
*selfRefId,
|
||||
"number",
|
||||
"num",
|
||||
makeValueCallArgument(42),
|
||||
kAllowRedefinition);
|
||||
|
||||
addMember(
|
||||
*selfRefId, "number", "neg_zero", makeUnserializableCallArgument("-0"));
|
||||
|
||||
verifyObjShape(thisId);
|
||||
|
||||
send<m::debugger::ResumeRequest>(conn, msgId++);
|
||||
expectNotification<m::debugger::ResumedNotification>(conn);
|
||||
}
|
||||
|
||||
TEST(ConnectionTests, testRuntimeCallFunctionOnExecutionContext) {
|
||||
TestContext context;
|
||||
AsyncHermesRuntime &asyncRuntime = context.runtime();
|
||||
SyncConnection &conn = context.conn();
|
||||
int msgId = 1;
|
||||
|
||||
asyncRuntime.executeScriptAsync(R"(
|
||||
debugger;
|
||||
)");
|
||||
|
||||
/// helper that returns a map with all of \p objId 's members.
|
||||
auto getProps = [&msgId, &conn](const m::runtime::RemoteObjectId &objId) {
|
||||
m::runtime::GetPropertiesRequest req;
|
||||
req.id = msgId++;
|
||||
req.objectId = objId;
|
||||
conn.send(req.toJson());
|
||||
auto resp = expectResponse<m::runtime::GetPropertiesResponse>(conn, req.id);
|
||||
std::unordered_map<std::string, folly::Optional<m::runtime::RemoteObject>>
|
||||
properties;
|
||||
for (auto propertyDescriptor : resp.result) {
|
||||
properties[propertyDescriptor.name] = propertyDescriptor.value;
|
||||
}
|
||||
return properties;
|
||||
};
|
||||
|
||||
send<m::debugger::EnableRequest>(conn, msgId++);
|
||||
expectExecutionContextCreated(conn);
|
||||
expectNotification<m::debugger::ScriptParsedNotification>(conn);
|
||||
|
||||
// globalThisId is the inspector's object Id for globalThis.
|
||||
m::runtime::RemoteObjectId globalThisId;
|
||||
{
|
||||
sendRuntimeEvalRequest(conn, msgId, "globalThis");
|
||||
auto resp = expectResponse<m::runtime::EvaluateResponse>(conn, msgId++);
|
||||
ASSERT_TRUE(resp.result.objectId) << resp.toDynamic();
|
||||
globalThisId = *resp.result.objectId;
|
||||
}
|
||||
|
||||
// This test table has all of the new fields we want to add to globalThis,
|
||||
// plus the Runtime.CallArgument to be sent to the inspector.
|
||||
struct {
|
||||
const char *propName;
|
||||
const m::runtime::CallArgument callArg;
|
||||
} tests[] = {
|
||||
{"callFunctionOnTestMember1", makeValueCallArgument(10)},
|
||||
{"callFunctionOnTestMember2", makeValueCallArgument("string")},
|
||||
{"callFunctionOnTestMember3", makeUnserializableCallArgument("NaN")},
|
||||
{"callFunctionOnTestMember4", makeUnserializableCallArgument("-0")},
|
||||
};
|
||||
|
||||
// sanity-check that our test fields don't exist in global this.
|
||||
{
|
||||
auto currProps = getProps(globalThisId);
|
||||
for (const auto &test : tests) {
|
||||
EXPECT_EQ(currProps.count(test.propName), 0) << test.propName;
|
||||
}
|
||||
}
|
||||
|
||||
auto addMember = [&msgId, &conn](
|
||||
const char *propName,
|
||||
const m::runtime::CallArgument &ca) {
|
||||
m::runtime::CallFunctionOnRequest req;
|
||||
req.id = msgId++;
|
||||
req.functionDeclaration =
|
||||
std::string("function(e){const r=\"") + propName + "\"; this[r]=e,r}";
|
||||
req.arguments = std::vector<m::runtime::CallArgument>{ca};
|
||||
req.executionContextId = 1;
|
||||
conn.send(req.toJson());
|
||||
expectResponse<m::runtime::CallFunctionOnResponse>(conn, req.id);
|
||||
};
|
||||
|
||||
for (const auto &test : tests) {
|
||||
addMember(test.propName, test.callArg);
|
||||
}
|
||||
|
||||
{
|
||||
auto currProps = getProps(globalThisId);
|
||||
for (const auto &test : tests) {
|
||||
auto it = currProps.find(test.propName);
|
||||
|
||||
// there should be a property named test.propName in globalThis.
|
||||
ASSERT_TRUE(it != currProps.end()) << test.propName;
|
||||
|
||||
// and it should have a value.
|
||||
ASSERT_TRUE(it->second) << test.propName;
|
||||
|
||||
if (it->second->value.hasValue()) {
|
||||
// the property has a value, so make sure that's what's being expected.
|
||||
auto actual = it->second->value;
|
||||
auto expected = test.callArg.value;
|
||||
ASSERT_TRUE(expected.hasValue()) << test.propName;
|
||||
EXPECT_EQ(*actual, *expected) << test.propName;
|
||||
} else if (it->second->unserializableValue.hasValue()) {
|
||||
// the property has an unserializable value, so make sure that's what's
|
||||
// being expected.
|
||||
auto actual = it->second->unserializableValue;
|
||||
auto expected = test.callArg.unserializableValue;
|
||||
ASSERT_TRUE(expected.hasValue()) << test.propName;
|
||||
EXPECT_EQ(*actual, *expected) << test.propName;
|
||||
} else {
|
||||
FAIL() << "No value or unserializable value in " << test.propName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send<m::debugger::ResumeRequest>(conn, msgId++);
|
||||
expectNotification<m::debugger::ResumedNotification>(conn);
|
||||
}
|
||||
|
||||
TEST(ConnectionTests, testConsoleLog) {
|
||||
TestContext context;
|
||||
AsyncHermesRuntime &asyncRuntime = context.runtime();
|
||||
@@ -2489,9 +2733,9 @@ TEST(ConnectionTests, heapProfilerSampling) {
|
||||
req.id = msgId++;
|
||||
// Sample every 256 bytes to ensure there are some samples. The default is
|
||||
// 32768, which is too high for a small example. Note that sampling is a
|
||||
// random process, so there's no guarantee there will be any samples in any
|
||||
// finite number of allocations. In practice the likelihood is so high that
|
||||
// there shouldn't be any issues.
|
||||
// random process, so there's no guarantee there will be any samples in
|
||||
// any finite number of allocations. In practice the likelihood is so high
|
||||
// that there shouldn't be any issues.
|
||||
req.samplingInterval = 256;
|
||||
send(conn, req);
|
||||
}
|
||||
@@ -2582,9 +2826,9 @@ TEST(ConnectionTests, heapSnapshotRemoteObject) {
|
||||
testObject(storedObjID, "object", "Array", "Array(3)", "array");
|
||||
// Force a collection to move the heap.
|
||||
runtime->instrumentation().collectGarbage("test");
|
||||
// A collection should not disturb the unique ID lookup, and it should be the
|
||||
// same object as before. Note that it won't have the same remote ID, because
|
||||
// Hermes doesn't do uniquing.
|
||||
// A collection should not disturb the unique ID lookup, and it should be
|
||||
// the same object as before. Note that it won't have the same remote ID,
|
||||
// because Hermes doesn't do uniquing.
|
||||
testObject(globalObjID, "object", "Object", "Object", nullptr);
|
||||
testObject(storedObjID, "object", "Array", "Array(3)", "array");
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ HeapProfiler.heapStatsUpdate
|
||||
HeapProfiler.lastSeenObjectId
|
||||
HeapProfiler.getObjectByHeapObjectId
|
||||
HeapProfiler.getHeapObjectId
|
||||
Runtime.callFunctionOn
|
||||
Runtime.consoleAPICalled
|
||||
Runtime.evaluate
|
||||
Runtime.executionContextCreated
|
||||
|
||||
Reference in New Issue
Block a user