From 156e5d9837e059b69580d23e4076da264305f3bd Mon Sep 17 00:00:00 2001 From: Alexander Blom Date: Wed, 2 Nov 2016 12:18:11 -0700 Subject: [PATCH] Add on-device JSC inspector Summary: Introduces the inspector library supporting the Chrome Debugging Protocol for JavaScriptCore. Eventually this will mean that it is possible to attach the Chrome inspector directly to the JSC instance running on the device. This library doesn't define the actual transport but leaves that up to the platform layer. The main entry point (and the only exported header) is `Inspector.h`. This diff only introduces the basics supporting the `Schema` and `Inspector` domains meaning it doesn't have any features yet. These will come in following diffs. Reviewed By: michalgr Differential Revision: D4021490 fbshipit-source-id: 517fd9033051c11ba97d312b16382445ae85d3f3 --- ReactCommon/cxxreact/BUCK | 2 + ReactCommon/cxxreact/JSCExecutor.cpp | 12 ++ ReactCommon/inspector/Agent.cpp | 50 +++++ ReactCommon/inspector/Agent.h | 30 +++ ReactCommon/inspector/Android.mk | 28 +++ ReactCommon/inspector/BUCK | 54 ++++++ ReactCommon/inspector/Dispatcher.cpp | 3 + ReactCommon/inspector/Dispatcher.h | 33 ++++ ReactCommon/inspector/Error.cpp | 19 ++ ReactCommon/inspector/Error.h | 34 ++++ ReactCommon/inspector/Inspector.cpp | 162 ++++++++++++++++ ReactCommon/inspector/Inspector.h | 90 +++++++++ ReactCommon/inspector/InspectorAgent.cpp | 22 +++ ReactCommon/inspector/InspectorAgent.h | 22 +++ ReactCommon/inspector/InspectorController.cpp | 178 ++++++++++++++++++ ReactCommon/inspector/InspectorController.h | 42 +++++ ReactCommon/inspector/Protocol.cpp | 86 +++++++++ ReactCommon/inspector/Protocol.h | 77 ++++++++ ReactCommon/inspector/Util.cpp | 9 + ReactCommon/inspector/Util.h | 45 +++++ ReactCommon/jschelpers/JSCHelpers.cpp | 7 + ReactCommon/jschelpers/JSCHelpers.h | 2 + 22 files changed, 1007 insertions(+) create mode 100644 ReactCommon/inspector/Agent.cpp create mode 100644 ReactCommon/inspector/Agent.h create mode 100644 ReactCommon/inspector/Android.mk create mode 100644 ReactCommon/inspector/BUCK create mode 100644 ReactCommon/inspector/Dispatcher.cpp create mode 100644 ReactCommon/inspector/Dispatcher.h create mode 100644 ReactCommon/inspector/Error.cpp create mode 100644 ReactCommon/inspector/Error.h create mode 100644 ReactCommon/inspector/Inspector.cpp create mode 100644 ReactCommon/inspector/Inspector.h create mode 100644 ReactCommon/inspector/InspectorAgent.cpp create mode 100644 ReactCommon/inspector/InspectorAgent.h create mode 100644 ReactCommon/inspector/InspectorController.cpp create mode 100644 ReactCommon/inspector/InspectorController.h create mode 100644 ReactCommon/inspector/Protocol.cpp create mode 100644 ReactCommon/inspector/Protocol.h create mode 100644 ReactCommon/inspector/Util.cpp create mode 100644 ReactCommon/inspector/Util.h diff --git a/ReactCommon/cxxreact/BUCK b/ReactCommon/cxxreact/BUCK index 44f751d6dd8..efcf46c40ad 100644 --- a/ReactCommon/cxxreact/BUCK +++ b/ReactCommon/cxxreact/BUCK @@ -28,6 +28,7 @@ if THIS_IS_FBANDROID: ], deps = [ '//xplat/folly:molly', + react_native_xplat_target('inspector:inspector'), ]) cxx_library( @@ -39,6 +40,7 @@ if THIS_IS_FBANDROID: '-DWITH_JSC_MEMORY_PRESSURE=1', '-DWITH_REACT_INTERNAL_SETTINGS=1', '-DWITH_FB_MEMORY_PROFILING=1', + '-DWITH_INSPECTOR=1', ], deps = JSC_DEPS ) diff --git a/ReactCommon/cxxreact/JSCExecutor.cpp b/ReactCommon/cxxreact/JSCExecutor.cpp index 9918a0778c9..b49e9d8ee43 100644 --- a/ReactCommon/cxxreact/JSCExecutor.cpp +++ b/ReactCommon/cxxreact/JSCExecutor.cpp @@ -19,6 +19,10 @@ #include #include +#ifdef WITH_INSPECTOR +#include +#endif + #include "Platform.h" #include "SystraceSection.h" #include "JSCNativeModules.h" @@ -229,6 +233,10 @@ void JSCExecutor::initOnJSVMThread() throw(JSException) { // Add a pointer to ourselves so we can retrieve it later in our hooks JSObjectSetPrivate(JSContextGetGlobalObject(m_context), this); + #ifdef WITH_INSPECTOR + Inspector::instance().registerGlobalContext("main", m_context); + #endif + installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>("nativeFlushQueueImmediate"); installNativeHook<&JSCExecutor::nativeCallSyncHook>("nativeCallSyncHook"); @@ -283,6 +291,10 @@ void JSCExecutor::terminateOnJSVMThread() { m_nativeModules.reset(); + #ifdef WITH_INSPECTOR + Inspector::instance().unregisterGlobalContext(m_context); + #endif + JSGlobalContextRelease(m_context); m_context = nullptr; } diff --git a/ReactCommon/inspector/Agent.cpp b/ReactCommon/inspector/Agent.cpp new file mode 100644 index 00000000000..1a3720cd6a9 --- /dev/null +++ b/ReactCommon/inspector/Agent.cpp @@ -0,0 +1,50 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "Agent.h" + +#include "Error.h" +#include "Protocol.h" + +#include + +namespace facebook { +namespace react { + +void Agent::onConnect(std::shared_ptr channel) { + channel_ = std::move(channel); + + channel_->registerDomain(getDomain(), [this](std::string, int callId, const std::string& method, folly::dynamic args) { + auto result = handle(method, std::move(args)); + if (result.isNull()) { + result = folly::dynamic::object; + } + auto message = folly::dynamic::object("id", callId)("result", std::move(result)); + channel_->sendMessage(folly::toJson(std::move(message))); + }); +} + +void Agent::onDisconnect() { + channel_.reset(); +} + +folly::dynamic Agent::handle(const std::string& method, folly::dynamic args) { + try { + return methods_.at(method)(std::move(args)); + } catch (const std::out_of_range& e) { + throw InspectorException(ErrorCode::MethodNotFound, "Unknown method: '" + method + "'"); + } +} + +void Agent::registerMethod(std::string name, Method method) { + methods_.emplace(std::move(name), std::move(method)); +} + +void Agent::sendEvent(std::string name, folly::dynamic params) { + if (!channel_) { + return; + } + channel_->sendMessage(Event(getDomain(), std::move(name), std::move(params))); +} + +} +} diff --git a/ReactCommon/inspector/Agent.h b/ReactCommon/inspector/Agent.h new file mode 100644 index 00000000000..598f0f7dbcb --- /dev/null +++ b/ReactCommon/inspector/Agent.h @@ -0,0 +1,30 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include "Dispatcher.h" + +#include + +namespace facebook { +namespace react { + +class Agent : public Dispatcher { +public: + void onConnect(std::shared_ptr channel) override; + void onDisconnect() override; +protected: + using Method = std::function; + void registerMethod(std::string name, Method method); + void sendEvent(std::string name, folly::dynamic params = nullptr); + + virtual std::string getDomain() = 0; +private: + folly::dynamic handle(const std::string& method, folly::dynamic args); + + std::shared_ptr channel_; + std::unordered_map methods_; +}; + +} +} diff --git a/ReactCommon/inspector/Android.mk b/ReactCommon/inspector/Android.mk new file mode 100644 index 00000000000..bd13be66e81 --- /dev/null +++ b/ReactCommon/inspector/Android.mk @@ -0,0 +1,28 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := inspector + +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/.. +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES) + +LOCAL_CFLAGS := \ + -DLOG_TAG=\"ReactNative\" + +LOCAL_CFLAGS += -Wall -Werror -fexceptions -frtti +CXX11_FLAGS := -std=c++11 +LOCAL_CFLAGS += $(CXX11_FLAGS) +LOCAL_EXPORT_CPPFLAGS := $(CXX11_FLAGS) + +LOCAL_STATIC_LIBRARIES := jschelpers +LOCAL_SHARED_LIBRARIES := libfolly_json libjsc libglog + +include $(BUILD_STATIC_LIBRARY) + +$(call import-module,folly) +$(call import-module,jsc) +$(call import-module,glog) +$(call import-module,jschelpers) diff --git a/ReactCommon/inspector/BUCK b/ReactCommon/inspector/BUCK new file mode 100644 index 00000000000..e5844b775d7 --- /dev/null +++ b/ReactCommon/inspector/BUCK @@ -0,0 +1,54 @@ +EXPORTED_HEADERS = [ + 'Inspector.h', +] + +def library(**kwargs): + if THIS_IS_FBANDROID: + include_defs('//ReactAndroid/DEFS') + cxx_library( + force_static = True, + # We depend on JSC, support the same platforms + supported_platforms_regex = '^android-(armv7|x86)$', + deps = JSC_DEPS + [ + '//xplat/folly:molly', + react_native_xplat_target('jschelpers:jschelpers'), + ], + **kwargs + ) + elif THIS_IS_FBOBJC: + pass + # TODO: Currently untested on iOS, but might require linking against a + # custom version to ensure binary stability? + # ios_library( + # inherited_buck_flags = STATIC_LIBRARY_IOS_FLAGS, + # frameworks = [ + # '$SDKROOT/System/Library/Frameworks/JavaScriptCore.framework', + # ], + # deps = [ + # '//xplat/folly:molly', + # react_native_xplat_target('jschelpers:jschelpers'), + # ], + # **kwargs + # ) + else: + raise Error('Unknown repo') + +library( + name = 'inspector', + preprocessor_flags = [ + '-DENABLE_INSPECTOR=1', + ], + compiler_flags = [ + '-Wall', + '-fexceptions', + '-fvisibility=hidden', + '-std=gnu++1y', + ], + exported_headers = EXPORTED_HEADERS, + headers = glob(['*.h'], excludes=EXPORTED_HEADERS), + header_namespace = 'inspector', + srcs = glob(['*.cpp']), + visibility = [ + 'PUBLIC', + ], +) diff --git a/ReactCommon/inspector/Dispatcher.cpp b/ReactCommon/inspector/Dispatcher.cpp new file mode 100644 index 00000000000..54bbd03f2da --- /dev/null +++ b/ReactCommon/inspector/Dispatcher.cpp @@ -0,0 +1,3 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "Dispatcher.h" diff --git a/ReactCommon/inspector/Dispatcher.h b/ReactCommon/inspector/Dispatcher.h new file mode 100644 index 00000000000..327942198cd --- /dev/null +++ b/ReactCommon/inspector/Dispatcher.h @@ -0,0 +1,33 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include +// Both double-conversions and WTF define the ASSERT macro +#undef ASSERT + +namespace facebook { +namespace react { + +class Channel { +public: + using MessageHandler = std::function; + + virtual ~Channel() = default; + + virtual void sendMessage(std::string message) = 0; + virtual void registerDomain(std::string domain, MessageHandler handler) = 0; +}; + +class Dispatcher { +public: + virtual ~Dispatcher() {} + + virtual void onConnect(std::shared_ptr channel) = 0; + virtual void onDisconnect() = 0; +}; + +} +} diff --git a/ReactCommon/inspector/Error.cpp b/ReactCommon/inspector/Error.cpp new file mode 100644 index 00000000000..5bb780f4471 --- /dev/null +++ b/ReactCommon/inspector/Error.cpp @@ -0,0 +1,19 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "Error.h" + +namespace facebook { +namespace react { + +InspectorException::InspectorException(int callId, ErrorCode code, std::string message) + : error_(callId, code, std::move(message)) {} + +InspectorException::InspectorException(ErrorCode code, std::string message) + : error_(code, std::move(message)) {} + +InspectorException InspectorException::withCallId(int callId) const { + return InspectorException(callId, error_.code(), error_.message()); +} + +} +} diff --git a/ReactCommon/inspector/Error.h b/ReactCommon/inspector/Error.h new file mode 100644 index 00000000000..f373035a321 --- /dev/null +++ b/ReactCommon/inspector/Error.h @@ -0,0 +1,34 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include "Protocol.h" + +#include +#include + +#include + +namespace facebook { +namespace react { + +class InspectorException : public std::exception { +public: + InspectorException(int callId, ErrorCode code, std::string message); + explicit InspectorException(ErrorCode code, std::string message); + + const char* what() const throw() override { + return error_.message().c_str(); + } + + const Error& error() const { + return error_; + } + + InspectorException withCallId(int callId) const; +private: + Error error_; +}; + +} +} diff --git a/ReactCommon/inspector/Inspector.cpp b/ReactCommon/inspector/Inspector.cpp new file mode 100644 index 00000000000..68233b15e71 --- /dev/null +++ b/ReactCommon/inspector/Inspector.cpp @@ -0,0 +1,162 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "Inspector.h" + +#include "InspectorController.h" + +#include + +#include +#include +#include + +#include + +namespace facebook { +namespace react { + +namespace { + +JSC::JSGlobalObject& getGlobalObject(JSContextRef ctx) { + JSC::ExecState* exec = toJS(ctx); + JSC::JSLockHolder locker(exec); + + JSC::JSGlobalObject* globalObject = exec->vmEntryGlobalObject(); + return *globalObject; +} + +} + +Inspector::LocalConnection::LocalConnection(std::shared_ptr duplexConnection) + : duplexConnection_(std::move(duplexConnection)) {} + +void Inspector::LocalConnection::sendMessage(std::string message) { + duplexConnection_->sendToLocal(std::move(message)); +} + +void Inspector::LocalConnection::disconnect() { + duplexConnection_->terminate(false); +} + +Inspector::PageHolder::PageHolder(std::string name, std::unique_ptr controller) +: name(name) +, controller(std::move(controller)) {} + +Inspector::PageHolder::~PageHolder() = default; + +Inspector& Inspector::instance() { + static Inspector inspector; + return inspector; +} + +std::vector Inspector::getPages() const { + std::lock_guard lock(registrationMutex_); + std::vector pages; + pages.reserve(pages_.size()); + for (auto& entry : pages_) { + pages.emplace_back(Page{entry.first, entry.second.name}); + } + return pages; +} + +void Inspector::registerGlobalContext(std::string title, JSGlobalContextRef ctx) { + std::lock_guard lock(registrationMutex_); + auto controller = folly::make_unique(getGlobalObject(ctx)); + auto pageId = numPages_++; + pages_.emplace( + std::piecewise_construct, + std::forward_as_tuple(pageId), + std::forward_as_tuple(std::move(title), std::move(controller))); +} + +void Inspector::unregisterGlobalContext(JSGlobalContextRef ctx) { + std::lock_guard lock(registrationMutex_); + auto& globalObject = getGlobalObject(ctx); + for (auto it = pages_.begin(); it != pages_.end(); it++) { + auto& page = it->second; + if (page.controller->getGlobalObject().globalExec() == globalObject.globalExec()) { + if (page.connection_) { + page.connection_->terminate(true); + } + pages_.erase(it); + return; + } + } +} + +std::unique_ptr Inspector::connect(int pageId, std::unique_ptr remote) { + std::lock_guard lock(registrationMutex_); + return folly::make_unique(pages_.at(pageId).connect(std::move(remote))); +} + +void Inspector::disconnect(int pageId) { + std::lock_guard lock(registrationMutex_); + pages_.at(pageId).controller->onDisconnect(); +} + +std::shared_ptr Inspector::PageHolder::connect(std::unique_ptr remote) { + if (connection_) { + throw std::runtime_error("Already connected"); + } + connection_ = std::make_shared(*this, std::move(remote)); + controller->onConnect([connection = connection_](std::string message) { + connection->sendToRemote(std::move(message)); + }); + return connection_; +} + +Inspector::DuplexConnection::DuplexConnection(PageHolder& page, std::unique_ptr remoteConnection) + : page_(page) + , remoteConnection_(std::move(remoteConnection)) {} + +Inspector::DuplexConnection::~DuplexConnection() { + if (remoteConnection_) { + LOG(FATAL) << "DuplexConnection wasn't terminated before destruction"; + } +} + +void Inspector::DuplexConnection::sendToRemote(std::string message) { + std::lock_guard lock(remoteMutex_); + if (!remoteConnection_) { + return; + } + + remoteConnection_->onMessage(std::move(message)); +} + +void Inspector::DuplexConnection::sendToLocal(std::string message) { + std::lock_guard lock(localMutex_); + if (!remoteConnection_) { + return; + } + + page_.controller->onMessage(std::move(message)); +} + +void Inspector::DuplexConnection::terminate(bool local) { + std::lock_guard lockLocal(localMutex_); + { + // Temp lock here so we can still send going away message + std::lock_guard lockRemote(remoteMutex_); + if (!remoteConnection_) { + // Already disconnected + return; + } + } + + if (local) { + page_.controller->onGoingAway(); + } + std::lock_guard lockRemote(remoteMutex_); + + auto remoteConnection = std::move(remoteConnection_); + if (local) { + remoteConnection->onDisconnect(); + } + page_.controller->onDisconnect(); + // This kills us + page_.connection_.reset(); +} + +} +} diff --git a/ReactCommon/inspector/Inspector.h b/ReactCommon/inspector/Inspector.h new file mode 100644 index 00000000000..5d103acaeea --- /dev/null +++ b/ReactCommon/inspector/Inspector.h @@ -0,0 +1,90 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include +#include + +#include +#undef WTF_EXPORT_PRIVATE + +namespace facebook { +namespace react { + +class InspectorController; +class Sender; + +class Inspector { +private: + class DuplexConnection; +public: + struct Page { + const int id; + const std::string title; + }; + + struct RemoteConnection { + virtual ~RemoteConnection() = default; + virtual void onMessage(std::string message) = 0; + virtual void onDisconnect() = 0; + }; + + class LocalConnection { + public: + void sendMessage(std::string message); + void disconnect(); + + LocalConnection(std::shared_ptr duplexConnection); + private: + std::shared_ptr duplexConnection_; + }; + + static Inspector& instance(); + + void registerGlobalContext(std::string title, JSGlobalContextRef ctx); + void unregisterGlobalContext(JSGlobalContextRef ctx); + + std::vector getPages() const; + std::unique_ptr connect(int pageId, std::unique_ptr remote); +private: + struct PageHolder; + + class DuplexConnection { + public: + DuplexConnection(PageHolder& page, std::unique_ptr remoteConnection); + ~DuplexConnection(); + + void sendToRemote(std::string message); + void sendToLocal(std::string message); + void terminate(bool local); + private: + PageHolder& page_; + std::unique_ptr remoteConnection_; + std::mutex localMutex_; + std::mutex remoteMutex_; + }; + + struct PageHolder { + PageHolder(std::string name, std::unique_ptr controller); + ~PageHolder(); + + std::shared_ptr connect(std::unique_ptr remote); + + const std::string name; + std::unique_ptr controller; + std::shared_ptr connection_; + }; + + Inspector() {}; + void disconnect(int pageId); + + int numPages_ = 0; + std::unordered_map pages_; + mutable std::mutex registrationMutex_; +}; + +} +} diff --git a/ReactCommon/inspector/InspectorAgent.cpp b/ReactCommon/inspector/InspectorAgent.cpp new file mode 100644 index 00000000000..d94176059ae --- /dev/null +++ b/ReactCommon/inspector/InspectorAgent.cpp @@ -0,0 +1,22 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "InspectorAgent.h" + +namespace facebook { +namespace react { + +InspectorAgent::InspectorAgent() { + auto emptyMethod = [](folly::dynamic) -> folly::dynamic { + return nullptr; + }; + + registerMethod("enable", emptyMethod); + registerMethod("disable", emptyMethod); +} + +void InspectorAgent::detach() { + sendEvent("detached", folly::dynamic::object("reason", "target_closed")); +} + +} +} diff --git a/ReactCommon/inspector/InspectorAgent.h b/ReactCommon/inspector/InspectorAgent.h new file mode 100644 index 00000000000..569e73f79d5 --- /dev/null +++ b/ReactCommon/inspector/InspectorAgent.h @@ -0,0 +1,22 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include "Agent.h" + +namespace facebook { +namespace react { + +class InspectorAgent : public Agent { +public: + InspectorAgent(); + + void detach(); +private: + std::string getDomain() override { + return "Inspector"; + } +}; + +} +} diff --git a/ReactCommon/inspector/InspectorController.cpp b/ReactCommon/inspector/InspectorController.cpp new file mode 100644 index 00000000000..d590f8a7c97 --- /dev/null +++ b/ReactCommon/inspector/InspectorController.cpp @@ -0,0 +1,178 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "InspectorController.h" + +#include "Error.h" +#include "Agent.h" +#include "InspectorAgent.h" + +#include +#include +#include + +namespace facebook { +namespace react { + +class ConcreteChannel : public Channel { +public: + ConcreteChannel(Receiver receiver) + : receiver_(std::move(receiver)) {} + + void sendMessage(std::string message) override { + receiver_(std::move(message)); + } + + void registerDomain(std::string domain, MessageHandler handler) override { + domains_.emplace(std::move(domain), std::move(handler)); + } + + std::unordered_map& getDomains() { + return domains_; + } +private: + std::unordered_map domains_; + Receiver receiver_; +}; + +class MessageRouter { +public: + MessageRouter(ConcreteChannel* channel) + : channel_(channel) { + CHECK(channel_) << "Channel is null"; + } + + /* + * Messages are in JSON, formatted like: + * { + * "id": 1, + * "method": "Debugger.removeBreakpoint", + * "params": { "removeBreakpoint": "xyz" } + * } + */ + void route(std::string message) { + try { + auto json = parseJson(message); + auto callId = getCallId(json); + receive(callId, std::move(message), std::move(json)); + } catch (const InspectorException& e) { + channel_->sendMessage(e.error()); + } + } +private: + void receive(int callId, std::string message, folly::dynamic json) { + try { + auto method = Method::parse(json["method"].asString()); + auto& handler = getHandler(method.domain()); + handler(std::move(message), callId, method.name(), std::move(json["params"])); + } catch (const InspectorException& e) { + throw e.withCallId(callId); + } catch (const std::exception& e) { + LOG(ERROR) << "Dispatcher failed: " << e.what(); + throw InspectorException(callId, ErrorCode::ServerError, "Internal error"); + } catch (...) { + throw InspectorException(callId, ErrorCode::ServerError, "Internal error"); + } + } + + folly::dynamic parseJson(const std::string& message) { + try { + return folly::parseJson(message); + } catch (const std::runtime_error& e) { + throw InspectorException(ErrorCode::ParseError, "Message must be in JSON format"); + } + } + + int getCallId(folly::dynamic& json) { + auto& id = json["id"]; + if (!id.isInt()) { + throw InspectorException(ErrorCode::InvalidRequest, "The type of 'id' property must be number"); + } else { + return id.asInt(); + } + } + + Channel::MessageHandler& getHandler(const std::string& domain) { + try { + auto& domains = channel_->getDomains(); + return domains.at(domain); + } catch (const std::out_of_range& e) { + throw InspectorException(ErrorCode::MethodNotFound, folly::to("Unknown domain: '", domain, "'")); + } + } + + ConcreteChannel* channel_; +}; + +class SchemaAgent : public Agent { +public: + SchemaAgent() { + registerMethod("getDomains", [this](folly::dynamic) -> folly::dynamic { + CHECK(channel_) << "Channel is null"; + folly::dynamic names = folly::dynamic::array; + auto& domains = channel_->getDomains(); + for (auto& entry : domains) { + // TODO(blom): Actually get version? + names.push_back(folly::dynamic::object("name", entry.first)("version", "1.0")); + } + return names; + }); + } + + void onConnect(std::shared_ptr channel) override { + Agent::onConnect(channel); + channel_ = std::static_pointer_cast(channel); + } +private: + std::shared_ptr channel_; + + std::string getDomain() override { + return "Schema"; + } +}; + +InspectorController::InspectorController(JSC::JSGlobalObject& globalObject) + : globalObject_(globalObject) { + auto inspectorAgent = folly::make_unique(); + inspectorAgent_ = inspectorAgent.get(); + dispatchers_.push_back(std::move(inspectorAgent)); + dispatchers_.push_back(folly::make_unique()); +} + +InspectorController::~InspectorController() { + CHECK(!channel_) << "Wasn't disconnected"; +} + +void InspectorController::onConnect(Receiver receiver) { + CHECK(!channel_) << "Already connected"; + + channel_ = std::make_shared(std::move(receiver)); + + for (auto& dispatcher : dispatchers_) { + dispatcher->onConnect(channel_); + } +} + +void InspectorController::onMessage(std::string message) { + CHECK(channel_) << "Not connected"; + + MessageRouter(channel_.get()).route(message); +} + +void InspectorController::onGoingAway() { + CHECK(channel_) << "Not connected"; + + inspectorAgent_->detach(); +} + +void InspectorController::onDisconnect() { + CHECK(channel_) << "Not connected"; + + for (auto& dispatcher : dispatchers_) { + dispatcher->onDisconnect(); + } + + channel_.reset(); +} + +} +} diff --git a/ReactCommon/inspector/InspectorController.h b/ReactCommon/inspector/InspectorController.h new file mode 100644 index 00000000000..2281e992958 --- /dev/null +++ b/ReactCommon/inspector/InspectorController.h @@ -0,0 +1,42 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include "Dispatcher.h" + +#include +#include +#include + +namespace JSC { +class JSGlobalObject; +} + +namespace facebook { +namespace react { + +class ConcreteChannel; +class InspectorAgent; + +using Receiver = std::function; + +class InspectorController { +public: + InspectorController(JSC::JSGlobalObject& globalObject); + ~InspectorController(); + + JSC::JSGlobalObject& getGlobalObject() const { return globalObject_; } + + void onConnect(Receiver receiver); + void onMessage(std::string message); + void onGoingAway(); + void onDisconnect(); +private: + JSC::JSGlobalObject& globalObject_; + std::shared_ptr channel_; + std::vector> dispatchers_; + InspectorAgent* inspectorAgent_; +}; + +} +} diff --git a/ReactCommon/inspector/Protocol.cpp b/ReactCommon/inspector/Protocol.cpp new file mode 100644 index 00000000000..eb7428ede49 --- /dev/null +++ b/ReactCommon/inspector/Protocol.cpp @@ -0,0 +1,86 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "Protocol.h" + +#include "Error.h" + +#include +#include +#include + +#include + +namespace facebook { +namespace react { + +namespace { + +folly::dynamic getCallId(const folly::Optional& callId) { + if (callId.hasValue()) { + return callId.value(); + } else { + return nullptr; + } +} + +} + +Method Method::parse(const std::string& formatted) { + auto splitPos = formatted.find_first_of('.'); + if (splitPos == std::string::npos) { + throw InspectorException(ErrorCode::InvalidRequest, "Invalid method format"); + } + return Method(formatted.substr(0, splitPos), formatted.substr(splitPos + 1)); +} + +Method::Method(std::string domain, std::string name) + : domain_(std::move(domain)) + , name_(std::move(name)) {} + +std::string Method::formatted() const { + return folly::to(domain_, '.', name_); +} + +Event::Event(std::string domain, std::string method, folly::dynamic params) + : method_(std::move(domain), std::move(method)) + , params_(std::move(params)) {} + +Event::operator std::string() const { + auto event = folly::dynamic::object("method", method_.formatted()); + if (!params_.isNull()) { + event("params", params_); + } + + return folly::toJson(std::move(event)); +} + +namespace Timestamp { +double now() { + using duration = std::chrono::duration>; + auto epoch = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + ); + return epoch.count(); +} +} + +Error::Error(int callId, ErrorCode code, std::string message) + : callId_(callId) + , code_(code) + , message_(std::move(message)) {} + +Error::Error(ErrorCode code, std::string message) + : code_(code) + , message_(std::move(message)) {} + +Error::operator std::string() const { + auto errorCode = static_cast(code_); + return folly::toJson( + folly::dynamic::object("id", getCallId(callId_)) + ("error", folly::dynamic::object("code", errorCode)("message", message_)) + ); +} + + +} +} diff --git a/ReactCommon/inspector/Protocol.h b/ReactCommon/inspector/Protocol.h new file mode 100644 index 00000000000..3b36ae7977d --- /dev/null +++ b/ReactCommon/inspector/Protocol.h @@ -0,0 +1,77 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include +#include + +namespace facebook { +namespace react { + +class Method { +public: + static Method parse(const std::string& formatted); + + Method(std::string domain, std::string name); + + const std::string& domain() const { + return domain_; + } + + const std::string& name() const { + return name_; + } + + std::string formatted() const; +private: + std::string domain_; + std::string name_; +}; + +class Event { +public: + explicit Event(std::string domain, std::string method, folly::dynamic params); + + operator std::string() const; +private: + Method method_; + folly::dynamic params_; +}; + +namespace Timestamp { + double now(); +} + +enum class ErrorCode { + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, + ServerError = -32000, +}; + +class Error { +public: + Error(int callId, ErrorCode code, std::string message); + Error(ErrorCode code, std::string message); + + const std::string& message() const { + return message_; + } + + ErrorCode code() const { + return code_; + } + + operator std::string() const; +private: + folly::Optional callId_; + ErrorCode code_; + std::string message_; +}; + +} +} diff --git a/ReactCommon/inspector/Util.cpp b/ReactCommon/inspector/Util.cpp new file mode 100644 index 00000000000..23c9ee6e156 --- /dev/null +++ b/ReactCommon/inspector/Util.cpp @@ -0,0 +1,9 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "Util.h" + +namespace facebook { +namespace react { + +} +} diff --git a/ReactCommon/inspector/Util.h b/ReactCommon/inspector/Util.h new file mode 100644 index 00000000000..fb858bf30c6 --- /dev/null +++ b/ReactCommon/inspector/Util.h @@ -0,0 +1,45 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace facebook { +namespace react { + +inline std::string toStdString(const WTF::String& str) { + return std::string(str.utf8().data()); +} + +template +class AgentMap { +public: + void add(JSGlobalContextRef ctx, T* agent) { + map_[ctx] = agent; + } + + void remove(T* agent) { + auto it = std::find_if( + map_.begin(), + map_.end(), + [agent](const typename MapType::value_type& entry) { return entry.second == agent; }); + map_.erase(it); + } + + T* get(JSGlobalContextRef ctx) { + return map_.at(ctx); + } +private: + using MapType = std::unordered_map; + MapType map_; +}; + +} +} diff --git a/ReactCommon/jschelpers/JSCHelpers.cpp b/ReactCommon/jschelpers/JSCHelpers.cpp index 39b944d9be5..aa97dcb157e 100644 --- a/ReactCommon/jschelpers/JSCHelpers.cpp +++ b/ReactCommon/jschelpers/JSCHelpers.cpp @@ -108,6 +108,13 @@ void installGlobalProxy( JSClassRelease(proxyClass); } +void removeGlobal(JSGlobalContextRef ctx, const char* name) { + JSStringRef jsName = JSStringCreateWithUTF8CString(name); + JSObjectRef globalObject = JSContextGetGlobalObject(ctx); + JSObjectSetProperty(ctx, globalObject, jsName, nullptr, 0, nullptr); + JSStringRelease(jsName); +} + JSValueRef makeJSCException( JSContextRef ctx, const char* exception_text) { diff --git a/ReactCommon/jschelpers/JSCHelpers.h b/ReactCommon/jschelpers/JSCHelpers.h index 5819f2e465c..8932f017216 100644 --- a/ReactCommon/jschelpers/JSCHelpers.h +++ b/ReactCommon/jschelpers/JSCHelpers.h @@ -60,6 +60,8 @@ void installGlobalProxy( const char* name, JSObjectGetPropertyCallback callback); +void removeGlobal(JSGlobalContextRef ctx, const char* name); + JSValueRef makeJSCException( JSContextRef ctx, const char* exception_text);