From eb08af57a95e99d45d72e7c097c41b181c3130cd Mon Sep 17 00:00:00 2001 From: John Porto Date: Tue, 1 Feb 2022 16:48:54 -0800 Subject: [PATCH] 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 --- .../hermes/inspector/chrome/Connection.cpp | 320 +++++++++++++++++- .../hermes/inspector/chrome/MessageTypes.cpp | 80 ++++- .../hermes/inspector/chrome/MessageTypes.h | 44 ++- .../chrome/tests/ConnectionTests.cpp | 258 +++++++++++++- .../hermes/inspector/tools/message_types.txt | 1 + 5 files changed, 693 insertions(+), 10 deletions(-) diff --git a/ReactCommon/hermes/inspector/chrome/Connection.cpp b/ReactCommon/hermes/inspector/chrome/Connection.cpp index 7101eb22473..2fa87b53864 100644 --- a/ReactCommon/hermes/inspector/chrome/Connection.cpp +++ b/ReactCommon/hermes/inspector/chrome/Connection.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -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 makePropsFromScope( std::pair 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(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(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 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 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 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(arguments.data()), + arguments.size()); + } + + private: + friend class CallFunctionOnBuilder; + + CallFunctionOnRunner(const CallFunctionOnRunner &) = delete; + CallFunctionOnRunner &operator=(const CallFunctionOnRunner &) = delete; + + CallFunctionOnRunner( + std::vector thisAndArguments, + folly::Optional 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 thisAndArguments_; + folly::Optional 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 expressionAndRunner() && { + return std::make_pair( + std::move(out_).str(), + CallFunctionOnRunner( + std::move(thisAndArguments_), std::move(executionContextId_))); + } + + private: + void addParams(const folly::Optional> + &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 thisAndArguments_; + folly::Optional 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 { + 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(); + 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(sendErrorToClient(req.id)); +} + void Connection::Impl::handle(const m::runtime::EvaluateRequest &req) { auto remoteObjPtr = std::make_shared(); diff --git a/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp b/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp index 484719e130e..170f90593b9 100644 --- a/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp +++ b/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp @@ -1,5 +1,5 @@ // Copyright 2004-present Facebook. All Rights Reserved. -// @generated SignedSource<<522f29c54f207a4f7b5c33af07cf64d0>> +// @generated <> #include "MessageTypes.h" @@ -60,6 +60,7 @@ std::unique_ptr Request::fromJsonThrowOnError(const std::string &str) { makeUnique}, {"HeapProfiler.takeHeapSnapshot", makeUnique}, + {"Runtime.callFunctionOn", makeUnique}, {"Runtime.evaluate", makeUnique}, {"Runtime.getProperties", makeUnique}, {"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"); diff --git a/ReactCommon/hermes/inspector/chrome/MessageTypes.h b/ReactCommon/hermes/inspector/chrome/MessageTypes.h index 2184a223588..78468d6fb11 100644 --- a/ReactCommon/hermes/inspector/chrome/MessageTypes.h +++ b/ReactCommon/hermes/inspector/chrome/MessageTypes.h @@ -1,5 +1,5 @@ // Copyright 2004-present Facebook. All Rights Reserved. -// @generated SignedSource<> +// @generated <> #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 samples; }; +struct runtime::CallArgument : public Serializable { + CallArgument() = default; + explicit CallArgument(const folly::dynamic &obj); + folly::dynamic toDynamic() const override; + + folly::Optional value; + folly::Optional unserializableValue; + folly::Optional 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 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 objectId; + folly::Optional> arguments; + folly::Optional silent; + folly::Optional returnByValue; + folly::Optional userGesture; + folly::Optional awaitPromise; + folly::Optional executionContextId; + folly::Optional 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 exceptionDetails; +}; + struct runtime::EvaluateResponse : public Response { EvaluateResponse() = default; explicit EvaluateResponse(const folly::dynamic &obj); diff --git a/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp b/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp index 1338337779e..502b1f10f9d 100644 --- a/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp +++ b/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp @@ -407,7 +407,7 @@ std::unordered_map 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(conn); } +TEST(ConnectionTests, testRuntimeCallFunctionOnObject) { + TestContext context; + AsyncHermesRuntime &asyncRuntime = context.runtime(); + SyncConnection &conn = context.conn(); + int msgId = 1; + + asyncRuntime.executeScriptAsync(R"( + debugger; + )"); + + send(conn, msgId++); + expectExecutionContextCreated(conn); + expectNotification(conn); + + // create a new Object() that will be used as "this" below. + m::runtime::RemoteObjectId thisId; + { + sendRuntimeEvalRequest(conn, msgId, "new Object()"); + auto resp = expectResponse(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 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{ca}; + req.objectId = thisId; + conn.send(req.toJson()); + expectResponse(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 { + 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(conn, msgId++); + expectNotification(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(conn, req.id); + std::unordered_map> + properties; + for (auto propertyDescriptor : resp.result) { + properties[propertyDescriptor.name] = propertyDescriptor.value; + } + return properties; + }; + + send(conn, msgId++); + expectExecutionContextCreated(conn); + expectNotification(conn); + + // globalThisId is the inspector's object Id for globalThis. + m::runtime::RemoteObjectId globalThisId; + { + sendRuntimeEvalRequest(conn, msgId, "globalThis"); + auto resp = expectResponse(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{ca}; + req.executionContextId = 1; + conn.send(req.toJson()); + expectResponse(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(conn, msgId++); + expectNotification(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"); diff --git a/ReactCommon/hermes/inspector/tools/message_types.txt b/ReactCommon/hermes/inspector/tools/message_types.txt index 6267e82ee0e..8921a829256 100644 --- a/ReactCommon/hermes/inspector/tools/message_types.txt +++ b/ReactCommon/hermes/inspector/tools/message_types.txt @@ -28,6 +28,7 @@ HeapProfiler.heapStatsUpdate HeapProfiler.lastSeenObjectId HeapProfiler.getObjectByHeapObjectId HeapProfiler.getHeapObjectId +Runtime.callFunctionOn Runtime.consoleAPICalled Runtime.evaluate Runtime.executionContextCreated