This commit is contained in:
Yamozha
2021-04-02 02:24:13 +03:00
parent c23950b545
commit 7256d79e2c
31493 changed files with 3036630 additions and 0 deletions

View File

@ -0,0 +1,87 @@
---
AccessModifierOffset: -1
AlignAfterOpenBracket: AlwaysBreak
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlinesLeft: true
AlignOperands: false
AlignTrailingComments: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: true
BinPackArguments: false
BinPackParameters: false
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: false
ColumnLimit: 80
CommentPragmas: '^ IWYU pragma:'
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
ForEachMacros: [ FOR_EACH_RANGE, FOR_EACH, ]
IncludeCategories:
- Regex: '^<.*\.h(pp)?>'
Priority: 1
- Regex: '^<.*'
Priority: 2
- Regex: '.*'
Priority: 3
IndentCaseLabels: true
IndentWidth: 2
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBlockIndentWidth: 2
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: false
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
PointerAlignment: Right
ReflowComments: true
SortIncludes: true
SpaceAfterCStyleCast: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Cpp11
TabWidth: 8
UseTab: Never
...

View File

@ -0,0 +1,27 @@
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
LOCAL_PATH := $(call my-dir)
REACT_NATIVE := $(LOCAL_PATH)/../../..
include $(REACT_NATIVE)/ReactCommon/common.mk
include $(CLEAR_VARS)
LOCAL_MODULE := hermes-inspector
LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp $(LOCAL_PATH)/detail/*.cpp $(LOCAL_PATH)/chrome/*.cpp)
LOCAL_C_ROOT := $(LOCAL_PATH)/../..
LOCAL_CFLAGS := -DHERMES_ENABLE_DEBUGGER=1 -DHERMES_INSPECTOR_FOLLY_KLUDGE=1
LOCAL_C_INCLUDES := $(LOCAL_C_ROOT) $(REACT_NATIVE)/ReactCommon/jsi $(call find-node-module,$(LOCAL_PATH),hermes-engine)/android/include
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_ROOT)
LOCAL_CPP_FEATURES := exceptions
LOCAL_STATIC_LIBRARIES := libjsi
LOCAL_SHARED_LIBRARIES := jsinspector libfb libfbjni libfolly_futures libfolly_json libhermes
include $(BUILD_SHARED_LIBRARY)

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
namespace facebook {
namespace hermes {
namespace inspector {
/**
* AsyncPauseState is used to track whether we requested an async pause from a
* running VM, and whether the pause was initiated by us or by the client.
*/
enum class AsyncPauseState {
/// None means there is no pending async pause in the VM.
None,
/// Implicit means we requested an async pause from the VM to service an op
/// that can only be performed while paused, like setting a breakpoint. An
/// impliict pause can be upgraded to an explicit pause if the client later
/// explicitly requests a pause.
Implicit,
/// Explicit means that the client requested the pause by calling pause().
Explicit
};
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,224 @@
# Copyright 2004-present Facebook. All Rights Reserved.
load("@fbsource//tools/build_defs:fb_xplat_cxx_binary.bzl", "fb_xplat_cxx_binary")
load("@fbsource//tools/build_defs:fb_xplat_cxx_library.bzl", "fb_xplat_cxx_library")
load("@fbsource//tools/build_defs:fb_xplat_cxx_test.bzl", "fb_xplat_cxx_test")
load("@fbsource//tools/build_defs:platform_defs.bzl", "APPLE", "CXX")
load("@fbsource//tools/build_defs/oss:rn_defs.bzl", "react_native_xplat_target")
load("@fbsource//xplat/hermes/defs:hermes.bzl", "hermes_build_mode", "hermes_optimize_flag")
CFLAGS_BY_MODE = {
"dbg": [
"-fexceptions",
"-frtti",
hermes_optimize_flag("dbg"),
"-g",
],
"dev": [
"-fexceptions",
"-frtti",
hermes_optimize_flag("dev"),
"-g",
],
"opt": [
"-fexceptions",
"-frtti",
hermes_optimize_flag("opt"),
],
}
CHROME_EXPORTED_HEADERS = [
"chrome/AutoAttachUtils.h",
"chrome/Connection.h",
"chrome/ConnectionDemux.h",
"chrome/MessageConverters.h",
"chrome/MessageInterfaces.h",
"chrome/MessageTypes.h",
"chrome/Registration.h",
"chrome/RemoteObjectsTable.h",
]
fb_xplat_cxx_library(
name = "chrome",
srcs = glob(["chrome/*.cpp"]),
headers = glob(
[
"chrome/*.h",
],
exclude = CHROME_EXPORTED_HEADERS,
),
header_namespace = "hermes/inspector",
exported_headers = CHROME_EXPORTED_HEADERS,
compiler_flags = CFLAGS_BY_MODE[hermes_build_mode()],
fbobjc_header_path_prefix = "hermes/inspector/chrome",
macosx_tests_override = [],
tests = [":chrome-tests"],
visibility = [
"PUBLIC",
],
xcode_public_headers_symlinks = True,
deps = [
react_native_xplat_target("jsinspector:jsinspector"),
":detail",
":inspectorlib",
"//xplat/folly:futures",
"//xplat/folly:molly",
"//xplat/hermes/API:HermesAPI",
"//xplat/jsi:JSIDynamic",
"//xplat/jsi:jsi",
"//xplat/third-party/glog:glog",
],
)
fb_xplat_cxx_test(
name = "chrome-tests",
srcs = glob([
"chrome/tests/*.cpp",
]),
headers = glob([
"chrome/tests/*.h",
]),
compiler_flags = CFLAGS_BY_MODE[hermes_build_mode()],
cxx_deps = [react_native_xplat_target("jsinspector:jsinspector")],
fbandroid_deps = [react_native_xplat_target("jsinspector:jsinspector")],
fbobjc_deps = [react_native_xplat_target("jsinspector:jsinspector")],
visibility = [
"PUBLIC",
],
deps = [
":chrome",
":detail",
"//xplat/third-party/gmock:gtest",
],
)
DETAIL_EXPORTED_HEADERS = [
"detail/CallbackOStream.h",
"detail/SerialExecutor.h",
"detail/Thread.h",
]
fb_xplat_cxx_library(
name = "detail",
srcs = glob(["detail/*.cpp"]),
headers = glob(
[
"detail/*.h",
],
exclude = DETAIL_EXPORTED_HEADERS,
),
header_namespace = "hermes/inspector",
exported_headers = DETAIL_EXPORTED_HEADERS,
compiler_flags = CFLAGS_BY_MODE[hermes_build_mode()],
# This is required by lint, but must be False, because there is
# no JNI_Onload.
fbandroid_allow_jni_merging = False,
fbandroid_deps = [
"//fbandroid/native/fb:fb",
],
fbobjc_header_path_prefix = "hermes/inspector/detail",
macosx_tests_override = [],
tests = [":detail-tests"],
visibility = [
"PUBLIC",
],
xcode_public_headers_symlinks = True,
deps = [
"//xplat/folly:molly",
],
)
fb_xplat_cxx_test(
name = "detail-tests",
srcs = glob([
"detail/tests/*.cpp",
]),
headers = glob([
"detail/tests/*.h",
]),
compiler_flags = CFLAGS_BY_MODE[hermes_build_mode()],
visibility = [
"PUBLIC",
],
deps = [
":detail",
"//xplat/third-party/gmock:gmock",
"//xplat/third-party/gmock:gtest",
],
)
fb_xplat_cxx_binary(
name = "hermes-chrome-debug-server",
srcs = glob([
"chrome/cli/*.cpp",
]),
compiler_flags = CFLAGS_BY_MODE[hermes_build_mode()],
cxx_deps = [react_native_xplat_target("jsinspector:jsinspector")],
fbandroid_deps = [react_native_xplat_target("jsinspector:jsinspector")],
fbobjc_deps = [react_native_xplat_target("jsinspector:jsinspector")],
visibility = [
"PUBLIC",
],
deps = [
":chrome",
":inspectorlib",
"//xplat/hermes/API:HermesAPI",
],
)
INSPECTOR_EXPORTED_HEADERS = [
"AsyncPauseState.h",
"Exceptions.h",
"Inspector.h",
"RuntimeAdapter.h",
]
# can't be named "inspector" since JSC already uses it, causing a buck rulekey
# collision: P58794155
fb_xplat_cxx_library(
name = "inspectorlib",
srcs = glob(["*.cpp"]),
headers = glob(
[
"*.h",
],
exclude = INSPECTOR_EXPORTED_HEADERS,
),
header_namespace = "hermes/inspector",
exported_headers = INSPECTOR_EXPORTED_HEADERS,
compiler_flags = CFLAGS_BY_MODE[hermes_build_mode()],
cxx_tests = [":inspector-tests"],
fbobjc_header_path_prefix = "hermes/inspector",
macosx_tests_override = [],
visibility = [
"PUBLIC",
],
xcode_public_headers_symlinks = True,
deps = [
":detail",
"//xplat/folly:futures",
"//xplat/folly:molly",
"//xplat/hermes/API:HermesAPI",
"//xplat/jsi:jsi",
"//xplat/third-party/glog:glog",
],
)
fb_xplat_cxx_test(
name = "inspector-tests",
srcs = glob([
"tests/*.cpp",
]),
headers = glob([
"tests/*.h",
]),
compiler_flags = CFLAGS_BY_MODE[hermes_build_mode()],
platforms = (CXX, APPLE),
visibility = [
"PUBLIC",
],
deps = [
":inspectorlib",
"//xplat/third-party/gmock:gtest",
],
)

View File

@ -0,0 +1,7 @@
load("@fbsource//xplat/hermes/defs:hermes.bzl", "hermes_is_debugger_enabled")
def hermes_inspector_dep_list():
return [
"//xplat/js/react-native-github/ReactCommon/hermes/inspector:chrome",
"//xplat/js/react-native-github/ReactCommon/hermes/inspector:inspectorlib",
] if hermes_is_debugger_enabled() else []

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <stdexcept>
namespace facebook {
namespace hermes {
namespace inspector {
class AlreadyEnabledException : public std::runtime_error {
public:
AlreadyEnabledException()
: std::runtime_error("can't enable: debugger already enabled") {}
};
class NotEnabledException : public std::runtime_error {
public:
NotEnabledException(const std::string &cmd)
: std::runtime_error("debugger can't perform " + cmd + ": not enabled") {}
};
class InvalidStateException : public std::runtime_error {
public:
InvalidStateException(
const std::string &cmd,
const std::string &curState,
const std::string &expectedState)
: std::runtime_error(
"debugger can't perform " + cmd + ": in " + curState +
", expected " + expectedState) {}
};
class MultipleCommandsPendingException : public std::runtime_error {
public:
MultipleCommandsPendingException(const std::string &cmd)
: std::runtime_error(
"debugger can't perform " + cmd +
": a step or resume is already pending") {}
};
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,637 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "Inspector.h"
#include "Exceptions.h"
#include "InspectorState.h"
#include <functional>
#include <string>
#include <glog/logging.h>
#include <hermes/inspector/detail/SerialExecutor.h>
#include <hermes/inspector/detail/Thread.h>
#ifdef HERMES_INSPECTOR_FOLLY_KLUDGE
// <kludge> This is here, instead of linking against
// folly/futures/Future.cpp, to avoid pulling in another pile of
// dependencies, including the separate dependency libevent. This is
// likely specific to the version of folly RN uses, so may need to be
// changed. Even better, perhaps folly can be refactored to simplify
// this. Providing a RN-specific Timekeeper impl may also help.
template class folly::Future<folly::Unit>;
template class folly::Future<bool>;
namespace folly {
namespace futures {
SemiFuture<Unit> sleep(Duration, Timekeeper *) {
LOG(FATAL) << "folly::futures::sleep() not implemented";
}
} // namespace futures
namespace detail {
std::shared_ptr<Timekeeper> getTimekeeperSingleton() {
LOG(FATAL) << "folly::detail::getTimekeeperSingleton() not implemented";
}
} // namespace detail
} // namespace folly
// </kludge>
#endif
namespace facebook {
namespace hermes {
namespace inspector {
using folly::Unit;
namespace debugger = ::facebook::hermes::debugger;
/**
* Threading notes:
*
* 1. mutex_ must be held before using state_ or any InspectorState methods.
* 2. Methods that are callable by the client (like enable, resume, etc.) call
* various InspectorState methods via state_. This implies that they must
* acquire mutex_.
* 3. Since some InspectorState methods call back out to the client (e.g. via
* fulfilling promises, or via the InspectorObserver callbacks), we have to
* be careful about reentrancy from a callback causing a deadlock when (1)
* and (2) interact. Consider:
*
* 1) Debugger pauses, which causes InspectorObserve::onPause to fire.
* onPause is called by InspectorState::Paused::onEnter on the JS
* thread with mutex_ held.
* 2) Client calls setBreakpoint from the onPause callback.
* 3) If setBreakpoint directly tried to acquire mutex_ here, we would
* deadlock since our thread already owns the mutex_ (see 1).
*
* For this reason, all client-facing methods are executed on executor_, which
* runs on its own thread. The pattern is:
*
* 1. The client-facing method foo (e.g. enable) enqueues a call to
* fooOnExecutor (e.g. enableOnExecutor) on executor_.
* 2. fooOnExecutor is responsible for acquiring mutex_.
*
*/
// TODO: read this out of an env variable or config
static constexpr bool kShouldLog = true;
// Logging state transitions is done outside of transition() in a macro so that
// function and line numbers in the log will be accurate.
#define TRANSITION(nextState) \
do { \
if (kShouldLog) { \
if (state_ == nullptr) { \
LOG(INFO) << "Inspector::" << __func__ \
<< " transitioning to initial state " << *(nextState); \
} else { \
LOG(INFO) << "Inspector::" << __func__ << " transitioning from " \
<< *state_ << " to " << *(nextState); \
} \
} \
transition((nextState)); \
} while (0)
Inspector::Inspector(
std::shared_ptr<RuntimeAdapter> adapter,
InspectorObserver &observer,
bool pauseOnFirstStatement)
: adapter_(adapter),
debugger_(adapter->getDebugger()),
observer_(observer),
executor_(std::make_unique<detail::SerialExecutor>("hermes-inspector")) {
// TODO (t26491391): make tickleJs a real Hermes runtime API
std::string src = "function __tickleJs() { return Math.random(); }";
adapter->getRuntime().evaluateJavaScript(
std::make_shared<jsi::StringBuffer>(src), "__tickleJsHackUrl");
{
std::lock_guard<std::mutex> lock(mutex_);
if (pauseOnFirstStatement) {
TRANSITION(std::make_unique<InspectorState::RunningWaitEnable>(*this));
} else {
TRANSITION(std::make_unique<InspectorState::RunningDetached>(*this));
}
}
debugger_.setShouldPauseOnScriptLoad(true);
debugger_.setEventObserver(this);
}
Inspector::~Inspector() {
// TODO: think about expected detach flow
debugger_.setEventObserver(nullptr);
}
void Inspector::installConsoleFunction(
jsi::Object &console,
const std::string &name,
const std::string &chromeTypeDefault = "") {
jsi::Runtime &rt = adapter_->getRuntime();
auto chromeType = chromeTypeDefault == "" ? name : chromeTypeDefault;
auto nameID = jsi::PropNameID::forUtf8(rt, name);
auto weakInspector = std::weak_ptr<Inspector>(shared_from_this());
console.setProperty(
rt,
nameID,
jsi::Function::createFromHostFunction(
rt,
nameID,
1,
[weakInspector, chromeType](
jsi::Runtime &runtime,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count) {
if (auto inspector = weakInspector.lock()) {
jsi::Array argsArray(runtime, count);
for (size_t index = 0; index < count; ++index)
argsArray.setValueAtIndex(runtime, index, args[index]);
inspector->logMessage(
ConsoleMessageInfo{chromeType, std::move(argsArray)});
}
return jsi::Value::undefined();
}));
}
void Inspector::installLogHandler() {
jsi::Runtime &rt = adapter_->getRuntime();
auto console = jsi::Object(rt);
installConsoleFunction(console, "assert");
installConsoleFunction(console, "clear");
installConsoleFunction(console, "debug");
installConsoleFunction(console, "dir");
installConsoleFunction(console, "dirxml");
installConsoleFunction(console, "error");
installConsoleFunction(console, "group", "startGroup");
installConsoleFunction(console, "groupCollapsed", "startGroupCollapsed");
installConsoleFunction(console, "groupEnd", "endGroup");
installConsoleFunction(console, "info");
installConsoleFunction(console, "log");
installConsoleFunction(console, "profile");
installConsoleFunction(console, "profileEnd");
installConsoleFunction(console, "table");
installConsoleFunction(console, "trace");
installConsoleFunction(console, "warn", "warning");
rt.global().setProperty(rt, "console", console);
}
void Inspector::triggerAsyncPause(bool andTickle) {
// In order to ensure that we pause soon, we both set the async pause flag on
// the runtime, and we run a bit of dummy JS to ensure we enter the Hermes
// interpreter loop.
debugger_.triggerAsyncPause(
pendingPauseState_ == AsyncPauseState::Implicit
? debugger::AsyncPauseKind::Implicit
: debugger::AsyncPauseKind::Explicit);
if (andTickle) {
// We run the dummy JS on a background thread to avoid any reentrancy issues
// in case this thread is called with the inspector mutex held.
std::shared_ptr<RuntimeAdapter> adapter = adapter_;
detail::Thread tickleJsLater(
"inspectorTickleJs", [adapter]() { adapter->tickleJs(); });
tickleJsLater.detach();
}
}
void Inspector::notifyContextCreated() {
observer_.onContextCreated(*this);
}
ScriptInfo Inspector::getScriptInfoFromTopCallFrame() {
ScriptInfo info{};
auto stackTrace = debugger_.getProgramState().getStackTrace();
if (stackTrace.callFrameCount() > 0) {
debugger::SourceLocation loc = stackTrace.callFrameForIndex(0).location;
info.fileId = loc.fileId;
info.fileName = loc.fileName;
info.sourceMappingUrl = debugger_.getSourceMappingUrl(info.fileId);
}
return info;
}
void Inspector::addCurrentScriptToLoadedScripts() {
ScriptInfo info = getScriptInfoFromTopCallFrame();
if (!loadedScripts_.count(info.fileId)) {
loadedScriptIdByName_[info.fileName] = info.fileId;
loadedScripts_[info.fileId] = LoadedScriptInfo{std::move(info), false};
}
}
void Inspector::removeAllBreakpoints() {
debugger_.deleteAllBreakpoints();
}
void Inspector::resetScriptsLoaded() {
for (auto &it : loadedScripts_) {
it.second.notifiedClient = false;
}
}
void Inspector::notifyScriptsLoaded() {
for (auto &it : loadedScripts_) {
LoadedScriptInfo &loadedScriptInfo = it.second;
if (!loadedScriptInfo.notifiedClient) {
loadedScriptInfo.notifiedClient = true;
observer_.onScriptParsed(*this, loadedScriptInfo.info);
}
}
}
folly::Future<Unit> Inspector::disable() {
auto promise = std::make_shared<folly::Promise<Unit>>();
executor_->add([this, promise] { disableOnExecutor(promise); });
return promise->getFuture();
}
folly::Future<Unit> Inspector::enable() {
auto promise = std::make_shared<folly::Promise<Unit>>();
executor_->add([this, promise] { enableOnExecutor(promise); });
return promise->getFuture();
}
folly::Future<Unit> Inspector::executeIfEnabled(
const std::string &description,
folly::Function<void(const debugger::ProgramState &)> func) {
auto promise = std::make_shared<folly::Promise<Unit>>();
executor_->add(
[this, description, func = std::move(func), promise]() mutable {
executeIfEnabledOnExecutor(description, std::move(func), promise);
});
return promise->getFuture();
}
folly::Future<debugger::BreakpointInfo> Inspector::setBreakpoint(
debugger::SourceLocation loc,
folly::Optional<std::string> condition) {
auto promise = std::make_shared<folly::Promise<debugger::BreakpointInfo>>();
executor_->add([this, loc, condition, promise] {
setBreakpointOnExecutor(loc, condition, promise);
});
return promise->getFuture();
}
folly::Future<folly::Unit> Inspector::removeBreakpoint(
debugger::BreakpointID breakpointId) {
auto promise = std::make_shared<folly::Promise<folly::Unit>>();
executor_->add([this, breakpointId, promise] {
removeBreakpointOnExecutor(breakpointId, promise);
});
return promise->getFuture();
}
folly::Future<folly::Unit> Inspector::logMessage(ConsoleMessageInfo info) {
auto promise = std::make_shared<folly::Promise<folly::Unit>>();
executor_->add([this,
pInfo = std::make_unique<ConsoleMessageInfo>(std::move(info)),
promise] { logOnExecutor(std::move(*pInfo), promise); });
return promise->getFuture();
}
folly::Future<Unit> Inspector::setPendingCommand(debugger::Command command) {
auto promise = std::make_shared<folly::Promise<Unit>>();
executor_->add([this, promise, cmd = std::move(command)]() mutable {
setPendingCommandOnExecutor(std::move(cmd), promise);
});
return promise->getFuture();
}
folly::Future<Unit> Inspector::resume() {
return setPendingCommand(debugger::Command::continueExecution());
}
folly::Future<Unit> Inspector::stepIn() {
return setPendingCommand(debugger::Command::step(debugger::StepMode::Into));
}
folly::Future<Unit> Inspector::stepOver() {
return setPendingCommand(debugger::Command::step(debugger::StepMode::Over));
}
folly::Future<Unit> Inspector::stepOut() {
return setPendingCommand(debugger::Command::step(debugger::StepMode::Out));
}
folly::Future<Unit> Inspector::pause() {
auto promise = std::make_shared<folly::Promise<Unit>>();
executor_->add([this, promise]() { pauseOnExecutor(promise); });
return promise->getFuture();
}
folly::Future<debugger::EvalResult> Inspector::evaluate(
uint32_t frameIndex,
const std::string &src,
folly::Function<void(const facebook::hermes::debugger::EvalResult &)>
resultTransformer) {
auto promise = std::make_shared<folly::Promise<debugger::EvalResult>>();
executor_->add([this,
frameIndex,
src,
promise,
resultTransformer = std::move(resultTransformer)]() mutable {
evaluateOnExecutor(frameIndex, src, promise, std::move(resultTransformer));
});
return promise->getFuture();
}
folly::Future<folly::Unit> Inspector::setPauseOnExceptions(
const debugger::PauseOnThrowMode &mode) {
auto promise = std::make_shared<folly::Promise<Unit>>();
executor_->add([this, mode, promise]() mutable {
setPauseOnExceptionsOnExecutor(mode, promise);
});
return promise->getFuture();
};
debugger::Command Inspector::didPause(debugger::Debugger &debugger) {
std::unique_lock<std::mutex> lock(mutex_);
if (kShouldLog) {
LOG(INFO) << "received didPause for reason: "
<< static_cast<int>(debugger.getProgramState().getPauseReason())
<< " in state: " << *state_;
}
while (true) {
/*
* Keep sending the onPause event to the current state until we get a
* command to return. For instance, this handles the transition from
* Running to Paused to Running:
*
* 1) (R => P) We're currently in Running, so we call Running::didPause,
* which returns {nextState: Paused, command: null}. There isn't a
* command to return yet.
* 2) (P => R) Now we're in Paused, so we call Paused::didPause, which
* returns {nextState: Running, command: someCommand} where someCommand
* is non-null (e.g. continue or step over). This terminates the loop.
*/
auto result = state_->didPause(lock);
std::unique_ptr<InspectorState> nextState = std::move(result.first);
if (nextState) {
TRANSITION(std::move(nextState));
}
std::unique_ptr<debugger::Command> command = std::move(result.second);
if (command) {
return std::move(*command);
}
}
}
void Inspector::breakpointResolved(
debugger::Debugger &debugger,
debugger::BreakpointID breakpointId) {
std::unique_lock<std::mutex> lock(mutex_);
debugger::BreakpointInfo info = debugger.getBreakpointInfo(breakpointId);
observer_.onBreakpointResolved(*this, info);
}
void Inspector::transition(std::unique_ptr<InspectorState> nextState) {
assert(nextState);
assert(state_ != nextState);
std::unique_ptr<InspectorState> prevState = std::move(state_);
state_ = std::move(nextState);
state_->onEnter(prevState.get());
}
void Inspector::disableOnExecutor(
std::shared_ptr<folly::Promise<Unit>> promise) {
std::lock_guard<std::mutex> lock(mutex_);
debugger_.setIsDebuggerAttached(false);
state_->detach(promise);
}
void Inspector::enableOnExecutor(
std::shared_ptr<folly::Promise<Unit>> promise) {
std::lock_guard<std::mutex> lock(mutex_);
auto result = state_->enable();
/**
* We fulfill the promise before changing state because fulfilling the promise
* responds to the Debugger.enable request, and changing state could send a
* notification (like Debugger.paused). It seems like a good idea to respond
* to enable before sending out any notifications.
*/
bool enabled = result.second;
if (enabled) {
debugger_.setIsDebuggerAttached(true);
promise->setValue();
} else {
promise->setException(AlreadyEnabledException());
}
std::unique_ptr<InspectorState> nextState = std::move(result.first);
if (nextState) {
TRANSITION(std::move(nextState));
}
}
void Inspector::executeIfEnabledOnExecutor(
const std::string &description,
folly::Function<void(const debugger::ProgramState &)> func,
std::shared_ptr<folly::Promise<Unit>> promise) {
std::lock_guard<std::mutex> lock(mutex_);
if (!state_->isPaused() && !state_->isRunning()) {
promise->setException(InvalidStateException(
description, state_->description(), "paused or running"));
return;
}
folly::Func wrappedFunc = [this, func = std::move(func)]() mutable {
func(debugger_.getProgramState());
};
state_->pushPendingFunc(
[wrappedFunc = std::move(wrappedFunc), promise]() mutable {
wrappedFunc();
promise->setValue();
});
}
void Inspector::setBreakpointOnExecutor(
debugger::SourceLocation loc,
folly::Optional<std::string> condition,
std::shared_ptr<folly::Promise<debugger::BreakpointInfo>> promise) {
std::lock_guard<std::mutex> lock(mutex_);
bool pushed = state_->pushPendingFunc([this, loc, condition, promise] {
debugger::BreakpointID id = debugger_.setBreakpoint(loc);
debugger::BreakpointInfo info{debugger::kInvalidBreakpoint};
if (id != debugger::kInvalidBreakpoint) {
info = debugger_.getBreakpointInfo(id);
if (condition) {
debugger_.setBreakpointCondition(id, condition.value());
}
}
promise->setValue(std::move(info));
});
if (!pushed) {
promise->setException(NotEnabledException("setBreakpoint"));
}
}
void Inspector::removeBreakpointOnExecutor(
debugger::BreakpointID breakpointId,
std::shared_ptr<folly::Promise<folly::Unit>> promise) {
std::lock_guard<std::mutex> lock(mutex_);
bool pushed = state_->pushPendingFunc([this, breakpointId, promise] {
debugger_.deleteBreakpoint(breakpointId);
promise->setValue();
});
if (!pushed) {
promise->setException(NotEnabledException("removeBreakpoint"));
}
}
void Inspector::logOnExecutor(
ConsoleMessageInfo info,
std::shared_ptr<folly::Promise<folly::Unit>> promise) {
std::lock_guard<std::mutex> lock(mutex_);
state_->pushPendingFunc([this, info = std::move(info)] {
observer_.onMessageAdded(*this, info);
});
promise->setValue();
}
void Inspector::setPendingCommandOnExecutor(
debugger::Command command,
std::shared_ptr<folly::Promise<Unit>> promise) {
std::lock_guard<std::mutex> lock(mutex_);
state_->setPendingCommand(std::move(command), promise);
}
void Inspector::pauseOnExecutor(std::shared_ptr<folly::Promise<Unit>> promise) {
std::lock_guard<std::mutex> lock(mutex_);
bool canPause = state_->pause();
if (canPause) {
promise->setValue();
} else {
promise->setException(NotEnabledException("pause"));
}
}
void Inspector::evaluateOnExecutor(
uint32_t frameIndex,
const std::string &src,
std::shared_ptr<folly::Promise<debugger::EvalResult>> promise,
folly::Function<void(const facebook::hermes::debugger::EvalResult &)>
resultTransformer) {
std::lock_guard<std::mutex> lock(mutex_);
state_->pushPendingEval(
frameIndex, src, promise, std::move(resultTransformer));
}
void Inspector::setPauseOnExceptionsOnExecutor(
const debugger::PauseOnThrowMode &mode,
std::shared_ptr<folly::Promise<folly::Unit>> promise) {
std::lock_guard<std::mutex> local(mutex_);
state_->pushPendingFunc([this, mode, promise] {
debugger_.setPauseOnThrowMode(mode);
promise->setValue();
});
}
static const char *kSuppressionVariable = "_hermes_suppress_superseded_warning";
void Inspector::alertIfPausedInSupersededFile() {
if (isExecutingSupersededFile() &&
!shouldSuppressAlertAboutSupersededFiles()) {
ScriptInfo info = getScriptInfoFromTopCallFrame();
std::string warning =
"You have loaded the current file multiple times, and you are "
"now paused in one of the previous instances. The source "
"code you see may not correspond to what's being executed "
"(set JS variable " +
std::string(kSuppressionVariable) +
"=true to "
"suppress this warning. Filename: " +
info.fileName + ").";
jsi::Array jsiArray(adapter_->getRuntime(), 1);
jsiArray.setValueAtIndex(adapter_->getRuntime(), 0, warning);
ConsoleMessageInfo logMessage("warning", std::move(jsiArray));
observer_.onMessageAdded(*this, logMessage);
}
}
bool Inspector::shouldSuppressAlertAboutSupersededFiles() {
jsi::Runtime &rt = adapter_->getRuntime();
jsi::Value setting = rt.global().getProperty(rt, kSuppressionVariable);
if (setting.isUndefined() || !setting.isBool())
return false;
return setting.getBool();
}
bool Inspector::isExecutingSupersededFile() {
ScriptInfo info = getScriptInfoFromTopCallFrame();
if (info.fileName.empty())
return false;
auto it = loadedScriptIdByName_.find(info.fileName);
if (it != loadedScriptIdByName_.end()) {
return it->second > info.fileId;
}
return false;
}
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,322 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <memory>
#include <queue>
#include <unordered_map>
#include <folly/Executor.h>
#include <folly/Unit.h>
#include <folly/futures/Future.h>
#include <hermes/DebuggerAPI.h>
#include <hermes/hermes.h>
#include <hermes/inspector/AsyncPauseState.h>
#include <hermes/inspector/RuntimeAdapter.h>
namespace facebook {
namespace hermes {
namespace inspector {
class Inspector;
class InspectorState;
/**
* ScriptInfo contains info about loaded scripts.
*/
struct ScriptInfo {
uint32_t fileId{};
std::string fileName;
std::string sourceMappingUrl;
};
struct ConsoleMessageInfo {
std::string source;
std::string level;
std::string url;
int line;
int column;
jsi::Array args;
ConsoleMessageInfo(std::string level, jsi::Array args)
: source("console-api"),
level(level),
url(""),
line(-1),
column(-1),
args(std::move(args)) {}
};
/**
* InspectorObserver notifies the observer of events that occur in the VM.
*/
class InspectorObserver {
public:
virtual ~InspectorObserver() = default;
/// onContextCreated fires when the VM is created.
virtual void onContextCreated(Inspector &inspector) = 0;
/// onBreakpointResolve fires when a lazy breakpoint is resolved.
virtual void onBreakpointResolved(
Inspector &inspector,
const facebook::hermes::debugger::BreakpointInfo &info) = 0;
/// onPause fires when VM transitions from running to paused state. This is
/// called directly on the JS thread while the VM is paused, so the receiver
/// can call debugger::ProgramState methods safely.
virtual void onPause(
Inspector &inspector,
const facebook::hermes::debugger::ProgramState &state) = 0;
/// onResume fires when VM transitions from paused to running state.
virtual void onResume(Inspector &inspector) = 0;
/// onScriptParsed fires when after the VM parses a script.
virtual void onScriptParsed(Inspector &inspector, const ScriptInfo &info) = 0;
// onMessageAdded fires when new console message is added.
virtual void onMessageAdded(
Inspector &inspector,
const ConsoleMessageInfo &info) = 0;
};
/**
* Inspector implements a future-based interface over the low-level Hermes
* debugging API.
*/
class Inspector : public facebook::hermes::debugger::EventObserver,
public std::enable_shared_from_this<Inspector> {
public:
/**
* Inspector's constructor should be used to install the inspector on the
* provided runtime before any JS executes in the runtime.
*/
Inspector(
std::shared_ptr<RuntimeAdapter> adapter,
InspectorObserver &observer,
bool pauseOnFirstStatement);
~Inspector();
/**
* disable turns off the inspector. All of the subsequent methods will not do
* anything unless the inspector is enabled.
*/
folly::Future<folly::Unit> disable();
/**
* enable turns on the inspector. All of the subsequent methods will not do
* anything unless the inspector is enabled. The returned future succeeds when
* the debugger is enabled, or fails with AlreadyEnabledException if the
* debugger was already enabled.
*/
folly::Future<folly::Unit> enable();
/**
* installs console log handler. Ideally this should be done inside
* constructor, but because it uses shared_from_this we can't do this
* in constructor.
*/
void installLogHandler();
/**
* executeIfEnabled executes the provided callback *on the JS thread with the
* inspector lock held*. Execution can be implicitly requested while running.
* The inspector lock:
*
* 1) Protects VM state transitions. This means that the VM is guaranteed to
* stay in the paused or running state for the duration of the callback.
* 2) Protects InspectorObserver callbacks. This means that if some shared
* data is accessed only in InspectorObserver and executeIfEnabled
* callbacks, it does not need to be locked, since it's already protected
* by the inspector lock.
*
* The returned future resolves to true in the VM can be paused, or
* fails with IllegalStateException otherwise. The description is only used
* to populate the IllegalStateException with more useful info on failure.
*/
folly::Future<folly::Unit> executeIfEnabled(
const std::string &description,
folly::Function<void(const facebook::hermes::debugger::ProgramState &)>
func);
/**
* setBreakpoint can be called at any time after the debugger is enabled to
* set a breakpoint in the VM. The future is fulfilled with the resolved
* breakpoint info.
*
* Resolving a breakpoint takes an indeterminate amount of time since Hermes
* only resolves breakpoints when the debugger is able to actively pause JS
* execution.
*/
folly::Future<facebook::hermes::debugger::BreakpointInfo> setBreakpoint(
facebook::hermes::debugger::SourceLocation loc,
folly::Optional<std::string> condition = folly::none);
folly::Future<folly::Unit> removeBreakpoint(
facebook::hermes::debugger::BreakpointID loc);
/**
* logs console message.
*/
folly::Future<folly::Unit> logMessage(ConsoleMessageInfo info);
/**
* resume and step methods are only valid when the VM is currently paused. The
* returned future suceeds when the VM resumes execution, or fails with an
* InvalidStateException otherwise.
*/
folly::Future<folly::Unit> resume();
folly::Future<folly::Unit> stepIn();
folly::Future<folly::Unit> stepOver();
folly::Future<folly::Unit> stepOut();
/**
* pause can be issued at any time while the inspector is enabled. It requests
* the VM to asynchronously break execution. The returned future suceeds if
* the VM can be paused in this state and fails with InvalidStateException if
* otherwise.
*/
folly::Future<folly::Unit> pause();
/**
* evaluate runs JavaScript code within the context of a call frame. The
* returned promise is fulfilled with an eval result if it's possible to
* evaluate code in the current state or fails with InvalidStateException
* otherwise.
*/
folly::Future<facebook::hermes::debugger::EvalResult> evaluate(
uint32_t frameIndex,
const std::string &src,
folly::Function<void(const facebook::hermes::debugger::EvalResult &)>
resultTransformer);
folly::Future<folly::Unit> setPauseOnExceptions(
const facebook::hermes::debugger::PauseOnThrowMode &mode);
/**
* didPause implements the pause callback from Hermes. This callback arrives
* on the JS thread.
*/
facebook::hermes::debugger::Command didPause(
facebook::hermes::debugger::Debugger &debugger) override;
/**
* breakpointResolved implements the breakpointResolved callback from Hermes.
*/
void breakpointResolved(
facebook::hermes::debugger::Debugger &debugger,
facebook::hermes::debugger::BreakpointID breakpointId) override;
private:
friend class InspectorState;
void triggerAsyncPause(bool andTickle);
void notifyContextCreated();
ScriptInfo getScriptInfoFromTopCallFrame();
void addCurrentScriptToLoadedScripts();
void removeAllBreakpoints();
void resetScriptsLoaded();
void notifyScriptsLoaded();
folly::Future<folly::Unit> setPendingCommand(debugger::Command command);
void transition(std::unique_ptr<InspectorState> nextState);
// All methods that end with OnExecutor run on executor_.
void disableOnExecutor(std::shared_ptr<folly::Promise<folly::Unit>> promise);
void enableOnExecutor(std::shared_ptr<folly::Promise<folly::Unit>> promise);
void executeIfEnabledOnExecutor(
const std::string &description,
folly::Function<void(const facebook::hermes::debugger::ProgramState &)>
func,
std::shared_ptr<folly::Promise<folly::Unit>> promise);
void setBreakpointOnExecutor(
debugger::SourceLocation loc,
folly::Optional<std::string> condition,
std::shared_ptr<
folly::Promise<facebook::hermes::debugger::BreakpointInfo>> promise);
void removeBreakpointOnExecutor(
debugger::BreakpointID breakpointId,
std::shared_ptr<folly::Promise<folly::Unit>> promise);
void logOnExecutor(
ConsoleMessageInfo info,
std::shared_ptr<folly::Promise<folly::Unit>> promise);
void setPendingCommandOnExecutor(
facebook::hermes::debugger::Command command,
std::shared_ptr<folly::Promise<folly::Unit>> promise);
void pauseOnExecutor(std::shared_ptr<folly::Promise<folly::Unit>> promise);
void evaluateOnExecutor(
uint32_t frameIndex,
const std::string &src,
std::shared_ptr<folly::Promise<facebook::hermes::debugger::EvalResult>>
promise,
folly::Function<void(const facebook::hermes::debugger::EvalResult &)>
resultTransformer);
void setPauseOnExceptionsOnExecutor(
const facebook::hermes::debugger::PauseOnThrowMode &mode,
std::shared_ptr<folly::Promise<folly::Unit>> promise);
void installConsoleFunction(
jsi::Object &console,
const std::string &name,
const std::string &chromeType);
std::shared_ptr<RuntimeAdapter> adapter_;
facebook::hermes::debugger::Debugger &debugger_;
InspectorObserver &observer_;
// All client methods (e.g. enable, setBreakpoint, resume, etc.) are executed
// on executor_ to prevent deadlocking on mutex_. See the implementation for
// more comments on the threading invariants used in this class.
std::unique_ptr<folly::Executor> executor_;
// All of the following member variables are guarded by mutex_.
std::mutex mutex_;
std::unique_ptr<InspectorState> state_;
// See the InspectorState::Running implementation for an explanation for why
// this state is here rather than in the Running class.
AsyncPauseState pendingPauseState_ = AsyncPauseState::None;
// All scripts loaded in to the VM, along with whether we've notified the
// client about the script yet.
struct LoadedScriptInfo {
ScriptInfo info;
bool notifiedClient;
};
std::unordered_map<int, LoadedScriptInfo> loadedScripts_;
std::unordered_map<std::string, int> loadedScriptIdByName_;
// Returns true if we are executing a file instance that has since been
// reloaded. I.e. we are running an old version of the file.
bool isExecutingSupersededFile();
// Allow the user to suppress warnings about superseded files.
bool shouldSuppressAlertAboutSupersededFiles();
// Trigger a fake console.log if we're currently in a superseded file.
void alertIfPausedInSupersededFile();
};
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,485 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "InspectorState.h"
#include <glog/logging.h>
namespace facebook {
namespace hermes {
namespace inspector {
using folly::Unit;
namespace debugger = ::facebook::hermes::debugger;
namespace {
std::unique_ptr<debugger::Command> makeContinueCommand() {
return std::make_unique<debugger::Command>(
debugger::Command::continueExecution());
}
} // namespace
std::ostream &operator<<(std::ostream &os, const InspectorState &state) {
return os << state.description();
}
/*
* InspectorState::RunningDetached
*/
std::pair<NextStatePtr, CommandPtr> InspectorState::RunningDetached::didPause(
MonitorLock &lock) {
debugger::PauseReason reason = getPauseReason();
if (reason == debugger::PauseReason::DebuggerStatement) {
return std::make_pair<NextStatePtr, CommandPtr>(
InspectorState::PausedWaitEnable::make(inspector_), nullptr);
}
if (reason == debugger::PauseReason::ScriptLoaded) {
inspector_.addCurrentScriptToLoadedScripts();
}
return std::make_pair<NextStatePtr, CommandPtr>(
nullptr, makeContinueCommand());
}
std::pair<NextStatePtr, bool> InspectorState::RunningDetached::enable() {
return std::make_pair<NextStatePtr, bool>(
InspectorState::Running::make(inspector_), true);
}
/*
* InspectorState::RunningWaitEnable
*/
std::pair<NextStatePtr, CommandPtr> InspectorState::RunningWaitEnable::didPause(
MonitorLock &lock) {
// If we started in RWE, then we asked for the VM to break on the first
// statement, and the first pause should be because of a script load.
assert(getPauseReason() == debugger::PauseReason::ScriptLoaded);
inspector_.addCurrentScriptToLoadedScripts();
return std::make_pair<NextStatePtr, CommandPtr>(
InspectorState::PausedWaitEnable::make(inspector_), nullptr);
}
std::pair<NextStatePtr, bool> InspectorState::RunningWaitEnable::enable() {
return std::make_pair<NextStatePtr, bool>(
InspectorState::RunningWaitPause::make(inspector_), true);
}
/*
* InspectorState::RunningWaitPause
*/
std::pair<NextStatePtr, CommandPtr> InspectorState::RunningWaitPause::didPause(
MonitorLock &lock) {
// If we are in RWP, then we asked for the VM to break on the first
// statement, and the first pause should be because of a script load.
assert(getPauseReason() == debugger::PauseReason::ScriptLoaded);
inspector_.addCurrentScriptToLoadedScripts();
return std::make_pair<NextStatePtr, CommandPtr>(
InspectorState::Paused::make(inspector_), nullptr);
}
/*
* InspectorState::PausedWaitEnable
*/
std::pair<NextStatePtr, CommandPtr> InspectorState::PausedWaitEnable::didPause(
MonitorLock &lock) {
if (getPauseReason() == debugger::PauseReason::ScriptLoaded) {
inspector_.addCurrentScriptToLoadedScripts();
}
while (!enabled_) {
/*
* The call to wait temporarily relinquishes the inspector mutex. This is
* safe because no other PausedWaitEnable event handler directly transitions
* out of PausedWaitEnable. So we know that our state is the active state
* both before and after the call to wait. This preserves the invariant that
* the inspector state is not modified during the execution of this method.
*
* Instead, PausedWaitEnable::enable indirectly induces the state transition
* out of PausedWaitEnable by signaling us via enabledCondition_.
*/
enabledCondition_.wait(lock);
assert(inspector_.state_.get() == this);
}
return std::make_pair<NextStatePtr, CommandPtr>(
InspectorState::Paused::make(inspector_), nullptr);
}
std::pair<NextStatePtr, bool> InspectorState::PausedWaitEnable::enable() {
if (enabled_) {
// Someone already called enable before and we're just waiting for the
// condition variable to wake up didPause.
return std::make_pair<NextStatePtr, bool>(nullptr, false);
}
enabled_ = true;
enabledCondition_.notify_one();
return std::make_pair<NextStatePtr, bool>(nullptr, true);
}
/*
* InspectorState::Running
*
* # Async Pauses
*
* We distinguish between implicit and explicit async pauses. An implicit async
* pause is requested by the inspector itself to service a request that requires
* the VM to be paused (e.g. to set a breakpoint). This is different from an
* explicit async pause requested by the user by hitting the pause button in the
* debugger UI.
*
* The async pause state must live in the Inspector class instead of the Running
* class because of potential races between when the implicit pause is requested
* and when it's serviced. Consider:
*
* 1. We request an implicit pause (e.g. to set a breakpoint).
* 2. An existing breakpoint fires, moving us from Running => Paused.
* 3. Client resumes execution, moving us from Paused => Running.
* 4. Now the debugger notices the async pause flag we set in (1), which pauses
* us again, causing Running::didPause to run.
*
* In this case, the Running state instance from (1) is no longer the same as
* the Running state instance in (4). But the running state instance in (4)
* needs to know that we requested the async break sometime in the past so it
* knows to automatically continue in the didPause callback. Therefore the async
* break state has to be stored in the long-lived Inspector class rather than in
* the short-lived Running class.
*/
void InspectorState::Running::onEnter(InspectorState *prevState) {
if (prevState) {
if (prevState->isPaused()) {
inspector_.observer_.onResume(inspector_);
} else {
// send context created and script load notifications if we just enabled
// the debugger
inspector_.notifyContextCreated();
inspector_.notifyScriptsLoaded();
}
}
}
void InspectorState::Running::detach(
std::shared_ptr<folly::Promise<Unit>> promise) {
pushPendingFunc([this, promise] {
pendingDetach_ = promise;
inspector_.removeAllBreakpoints();
inspector_.resetScriptsLoaded();
});
}
std::pair<NextStatePtr, CommandPtr> InspectorState::Running::didPause(
MonitorLock &lock) {
debugger::PauseReason reason = getPauseReason();
for (auto &func : pendingFuncs_) {
func();
}
pendingFuncs_.clear();
if (pendingDetach_) {
// Clear any pending pause state back to no requests for the next attach
inspector_.pendingPauseState_ = AsyncPauseState::None;
// Ensure we fulfill any pending ScriptLoaded requests
if (reason == debugger::PauseReason::ScriptLoaded) {
inspector_.addCurrentScriptToLoadedScripts();
}
// Fail any in-flight Eval requests
if (pendingEvalPromise_) {
pendingEvalPromise_->setException(NotEnabledException("eval"));
}
// if we requested the break implicitly to clear state and detach,
// transition to RunningDetached
pendingDetach_->setValue();
return std::make_pair<NextStatePtr, CommandPtr>(
InspectorState::RunningDetached::make(inspector_),
makeContinueCommand());
}
if (reason == debugger::PauseReason::AsyncTrigger) {
AsyncPauseState &pendingPauseState = inspector_.pendingPauseState_;
switch (pendingPauseState) {
case AsyncPauseState::None:
// shouldn't ever async break without us asking first
assert(false);
break;
case AsyncPauseState::Implicit:
pendingPauseState = AsyncPauseState::None;
break;
case AsyncPauseState::Explicit:
// explicit break was requested by user, so go to Paused state
pendingPauseState = AsyncPauseState::None;
return std::make_pair<NextStatePtr, CommandPtr>(
InspectorState::Paused::make(inspector_), nullptr);
}
} else if (reason == debugger::PauseReason::ScriptLoaded) {
inspector_.addCurrentScriptToLoadedScripts();
inspector_.notifyScriptsLoaded();
} else if (reason == debugger::PauseReason::EvalComplete) {
assert(pendingEvalPromise_);
pendingEvalResultTransformer_(
inspector_.debugger_.getProgramState().getEvalResult());
pendingEvalPromise_->setValue(
inspector_.debugger_.getProgramState().getEvalResult());
pendingEvalPromise_.reset();
} else /* other cases imply a transition to Pause */ {
return std::make_pair<NextStatePtr, CommandPtr>(
InspectorState::Paused::make(inspector_), nullptr);
}
if (!pendingEvals_.empty()) {
assert(!pendingEvalPromise_);
auto eval = std::make_unique<PendingEval>(std::move(pendingEvals_.front()));
pendingEvals_.pop();
pendingEvalPromise_ = eval->promise;
pendingEvalResultTransformer_ = std::move(eval->resultTransformer);
return std::make_pair<NextStatePtr, CommandPtr>(
nullptr, std::make_unique<debugger::Command>(std::move(eval->command)));
}
return std::make_pair<NextStatePtr, CommandPtr>(
nullptr, makeContinueCommand());
}
bool InspectorState::Running::pushPendingFunc(folly::Func func) {
pendingFuncs_.emplace_back(std::move(func));
if (inspector_.pendingPauseState_ == AsyncPauseState::None) {
inspector_.pendingPauseState_ = AsyncPauseState::Implicit;
inspector_.triggerAsyncPause(true);
}
return true;
}
void InspectorState::Running::pushPendingEval(
uint32_t frameIndex,
const std::string &src,
std::shared_ptr<folly::Promise<debugger::EvalResult>> promise,
folly::Function<void(const facebook::hermes::debugger::EvalResult &)>
resultTransformer) {
PendingEval pendingEval{debugger::Command::eval(src, frameIndex),
promise,
std::move(resultTransformer)};
pendingEvals_.emplace(std::move(pendingEval));
if (inspector_.pendingPauseState_ == AsyncPauseState::None) {
inspector_.pendingPauseState_ = AsyncPauseState::Implicit;
}
inspector_.triggerAsyncPause(true);
}
bool InspectorState::Running::pause() {
AsyncPauseState &pendingPauseState = inspector_.pendingPauseState_;
bool canPause = false;
switch (pendingPauseState) {
case AsyncPauseState::None:
// haven't yet requested a pause, so do it now
pendingPauseState = AsyncPauseState::Explicit;
inspector_.triggerAsyncPause(false);
canPause = true;
break;
case AsyncPauseState::Implicit:
// already requested an implicit pause on our own, upgrade it to an
// explicit pause
pendingPauseState = AsyncPauseState::Explicit;
inspector_.triggerAsyncPause(false);
canPause = true;
break;
case AsyncPauseState::Explicit:
// client already requested a pause that hasn't occurred yet
canPause = false;
break;
}
return canPause;
}
/*
* InspectorState::Paused
*/
void InspectorState::Paused::onEnter(InspectorState *prevState) {
// send script load notifications if we just enabled the debugger
if (prevState && !prevState->isRunning()) {
inspector_.notifyContextCreated();
inspector_.notifyScriptsLoaded();
}
const debugger::ProgramState &state = inspector_.debugger_.getProgramState();
inspector_.alertIfPausedInSupersededFile();
inspector_.observer_.onPause(inspector_, state);
}
std::pair<NextStatePtr, CommandPtr> InspectorState::Paused::didPause(
std::unique_lock<std::mutex> &lock) {
switch (getPauseReason()) {
case debugger::PauseReason::AsyncTrigger:
inspector_.pendingPauseState_ = AsyncPauseState::None;
break;
case debugger::PauseReason::EvalComplete: {
assert(pendingEvalPromise_);
pendingEvalResultTransformer_(
inspector_.debugger_.getProgramState().getEvalResult());
pendingEvalPromise_->setValue(
inspector_.debugger_.getProgramState().getEvalResult());
pendingEvalPromise_.reset();
} break;
case debugger::PauseReason::ScriptLoaded:
inspector_.addCurrentScriptToLoadedScripts();
inspector_.notifyScriptsLoaded();
break;
default:
break;
}
std::unique_ptr<PendingEval> eval;
std::unique_ptr<PendingCommand> resumeOrStep;
while (!eval && !resumeOrStep && !pendingDetach_) {
{
while (!pendingCommand_ && pendingEvals_.empty() &&
pendingFuncs_.empty()) {
/*
* The call to wait temporarily relinquishes the inspector mutex. This
* is safe because no other Paused event handler directly transitions
* out of Paused. So we know that our state is the active state both
* before and after the call to wait. This preserves the invariant that
* the inspector state is not modified during the execution of this
* method.
*/
hasPendingWork_.wait(lock);
}
assert(inspector_.state_.get() == this);
}
if (!pendingEvals_.empty()) {
eval = std::make_unique<PendingEval>(std::move(pendingEvals_.front()));
pendingEvals_.pop();
} else if (pendingCommand_) {
resumeOrStep.swap(pendingCommand_);
}
for (auto &func : pendingFuncs_) {
func();
}
pendingFuncs_.clear();
}
if (pendingDetach_) {
if (pendingEvalPromise_) {
pendingEvalPromise_->setException(NotEnabledException("eval"));
}
if (resumeOrStep) {
resumeOrStep->promise->setValue();
}
pendingDetach_->setValue();
// Send resume so client-side UI doesn't stay stuck at the breakpoint UI
inspector_.observer_.onResume(inspector_);
return std::make_pair<NextStatePtr, CommandPtr>(
InspectorState::RunningDetached::make(inspector_),
makeContinueCommand());
}
if (eval) {
assert(!pendingEvalPromise_);
pendingEvalPromise_ = eval->promise;
pendingEvalResultTransformer_ = std::move(eval->resultTransformer);
return std::make_pair<NextStatePtr, CommandPtr>(
nullptr, std::make_unique<debugger::Command>(std::move(eval->command)));
}
assert(resumeOrStep);
resumeOrStep->promise->setValue();
return std::make_pair<NextStatePtr, CommandPtr>(
InspectorState::Running::make(inspector_),
std::make_unique<debugger::Command>(std::move(resumeOrStep->command)));
}
void InspectorState::Paused::detach(
std::shared_ptr<folly::Promise<Unit>> promise) {
pushPendingFunc([this, promise] {
pendingDetach_ = promise;
inspector_.removeAllBreakpoints();
inspector_.resetScriptsLoaded();
});
}
bool InspectorState::Paused::pushPendingFunc(folly::Func func) {
pendingFuncs_.emplace_back(std::move(func));
hasPendingWork_.notify_one();
return true;
}
void InspectorState::Paused::pushPendingEval(
uint32_t frameIndex,
const std::string &src,
std::shared_ptr<folly::Promise<debugger::EvalResult>> promise,
folly::Function<void(const facebook::hermes::debugger::EvalResult &)>
resultTransformer) {
// Shouldn't allow the client to eval if there's already a pending resume/step
if (pendingCommand_) {
promise->setException(MultipleCommandsPendingException("eval"));
return;
}
PendingEval pendingEval{debugger::Command::eval(src, frameIndex),
promise,
std::move(resultTransformer)};
pendingEvals_.emplace(std::move(pendingEval));
hasPendingWork_.notify_one();
}
void InspectorState::Paused::setPendingCommand(
debugger::Command command,
std::shared_ptr<folly::Promise<Unit>> promise) {
if (pendingCommand_) {
promise->setException(MultipleCommandsPendingException("cmd"));
return;
}
pendingCommand_ =
std::make_unique<PendingCommand>(std::move(command), promise);
hasPendingWork_.notify_one();
}
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,405 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <condition_variable>
#include <iostream>
#include <memory>
#include <mutex>
#include <queue>
#include <utility>
#include <folly/Unit.h>
#include <hermes/inspector/Exceptions.h>
#include <hermes/inspector/Inspector.h>
namespace facebook {
namespace hermes {
namespace inspector {
using NextStatePtr = std::unique_ptr<InspectorState>;
using CommandPtr = std::unique_ptr<facebook::hermes::debugger::Command>;
using MonitorLock = std::unique_lock<std::mutex>;
/**
* InspectorState encapsulates a single state in the Inspector FSM. Events in
* the FSM are modeled as methods in InspectorState.
*
* Some events may cause state transitions. The next state is returned via a
* pointer to the next InspectorState.
*
* We assume that the Inspector's mutex is held across all calls to
* InspectorState methods. For more threading notes, see the Inspector
* implementation.
*/
class InspectorState {
public:
InspectorState(Inspector &inspector) : inspector_(inspector) {}
virtual ~InspectorState() = default;
/**
* onEnter is called when entering the state. prevState may be null when
* transitioning into an initial state.
*/
virtual void onEnter(InspectorState *prevState) {}
/*
* Events that may cause a state transition.
*/
/**
* detach clears all debuger state and transitions to RunningDetached.
*/
virtual void detach(std::shared_ptr<folly::Promise<folly::Unit>> promise) {
// As we're not attached we'd like for the operation to be idempotent
promise->setValue();
}
/**
* didPause handles the didPause callback from the debugger. It takes the lock
* associated with the Inspector's mutex by reference in case we need to
* temporarily relinquish the lock (e.g. via condition_variable::wait).
*/
virtual std::pair<NextStatePtr, CommandPtr> didPause(MonitorLock &lock) = 0;
/**
* enable handles the enable event from the client.
*/
virtual std::pair<NextStatePtr, bool> enable() {
return std::make_pair<NextStatePtr, bool>(nullptr, false);
}
/*
* Events that don't cause a state transition.
*/
/**
* pushPendingFunc appends a function to run the next time the debugger
* pauses, either explicitly while paused or implicitly while running.
* Returns false if it's not possible to push a func in this state.
*/
virtual bool pushPendingFunc(folly::Func func) {
return false;
}
/**
* pushPendingEval appends an eval request to run the next time the debugger
* pauses, either explicitly while paused or implicitly while running.
* resultTransformer function will be called with EvalResult before returning
* result so that we can manipulate EvalResult while the VM is paused.
*/
virtual void pushPendingEval(
uint32_t frameIndex,
const std::string &src,
std::shared_ptr<folly::Promise<facebook::hermes::debugger::EvalResult>>
promise,
folly::Function<void(const facebook::hermes::debugger::EvalResult &)>
resultTransformer) {
promise->setException(
InvalidStateException("eval", description(), "paused or running"));
}
/**
* setPendingCommand sets a command to break the debugger out of the didPause
* run loop. If it's not possible to set a pending command in this state, the
* promise fails with InvalidStateException. Otherwise, the promise resolves
* to true when the command actually executes.
*/
virtual void setPendingCommand(
debugger::Command command,
std::shared_ptr<folly::Promise<folly::Unit>> promise) {
promise->setException(
InvalidStateException("cmd", description(), "paused"));
}
/**
* pause requests an async pause from the VM.
*/
virtual bool pause() {
return false;
}
/*
* Convenience functions for determining the concrete type and description
* for a state instance without RTTI.
*/
virtual bool isRunningDetached() const {
return false;
}
virtual bool isRunningWaitEnable() const {
return false;
}
virtual bool isRunningWaitPause() const {
return false;
}
virtual bool isPausedWaitEnable() const {
return false;
}
virtual bool isRunning() const {
return false;
}
virtual bool isPaused() const {
return false;
}
virtual const char *description() const = 0;
friend std::ostream &operator<<(
std::ostream &os,
const InspectorState &state);
class RunningDetached;
class RunningWaitEnable;
class RunningWaitPause;
class PausedWaitEnable;
class Running;
class Paused;
protected:
debugger::PauseReason getPauseReason() {
return inspector_.debugger_.getProgramState().getPauseReason();
}
private:
Inspector &inspector_;
};
extern std::ostream &operator<<(std::ostream &os, const InspectorState &state);
/**
* RunningDetached is the initial state when we're associated with a VM that
* initially has no breakpoints.
*/
class InspectorState::RunningDetached : public InspectorState {
public:
static std::unique_ptr<InspectorState> make(Inspector &inspector) {
return std::make_unique<RunningDetached>(inspector);
}
RunningDetached(Inspector &inspector) : InspectorState(inspector) {}
~RunningDetached() {}
std::pair<NextStatePtr, CommandPtr> didPause(MonitorLock &lock) override;
std::pair<NextStatePtr, bool> enable() override;
bool isRunningDetached() const override {
return true;
}
const char *description() const override {
return "RunningDetached";
}
};
/**
* RunningWaitEnable is the initial state when we're associated with a VM that
* has a breakpoint on the first statement.
*/
class InspectorState::RunningWaitEnable : public InspectorState {
public:
static std::unique_ptr<InspectorState> make(Inspector &inspector) {
return std::make_unique<RunningWaitEnable>(inspector);
}
RunningWaitEnable(Inspector &inspector) : InspectorState(inspector) {}
~RunningWaitEnable() {}
std::pair<NextStatePtr, CommandPtr> didPause(MonitorLock &lock) override;
std::pair<NextStatePtr, bool> enable() override;
bool isRunningWaitEnable() const override {
return true;
}
const char *description() const override {
return "RunningWaitEnable";
}
};
/**
* RunningWaitPause is the state when we've received enable call, but
* waiting for didPause because we need to pause on the first statement.
*/
class InspectorState::RunningWaitPause : public InspectorState {
public:
static std::unique_ptr<InspectorState> make(Inspector &inspector) {
return std::make_unique<RunningWaitPause>(inspector);
}
std::pair<NextStatePtr, CommandPtr> didPause(MonitorLock &lock) override;
RunningWaitPause(Inspector &inspector) : InspectorState(inspector) {}
~RunningWaitPause() {}
bool isRunningWaitPause() const override {
return true;
}
const char *description() const override {
return "RunningWaitPause";
}
};
/**
* PausedWaitEnable is the state when we're in a didPause callback and we're
* waiting for the client to call enable.
*/
class InspectorState::PausedWaitEnable : public InspectorState {
public:
static std::unique_ptr<InspectorState> make(Inspector &inspector) {
return std::make_unique<PausedWaitEnable>(inspector);
}
PausedWaitEnable(Inspector &inspector) : InspectorState(inspector) {}
~PausedWaitEnable() {}
std::pair<NextStatePtr, CommandPtr> didPause(MonitorLock &lock) override;
std::pair<NextStatePtr, bool> enable() override;
bool isPausedWaitEnable() const override {
return true;
}
const char *description() const override {
return "PausedWaitEnable";
}
private:
bool enabled_ = false;
std::condition_variable enabledCondition_;
};
/**
* PendingEval holds an eval command and a promise that is fulfilled with the
* eval result.
*/
struct PendingEval {
debugger::Command command;
std::shared_ptr<folly::Promise<facebook::hermes::debugger::EvalResult>>
promise;
folly::Function<void(const facebook::hermes::debugger::EvalResult &)>
resultTransformer;
};
/**
* Running is the state when we're enabled and not currently paused, e.g. when
* we're actively executing JS.
*
* Note that we can be in the running state even if we're not actively running
* JS. For instance, React Native could be blocked in a native message queue
* waiting for the next message to process outside of the call in to Hermes.
* That still counts as Running in this FSM.
*/
class InspectorState::Running : public InspectorState {
public:
static std::unique_ptr<InspectorState> make(Inspector &inspector) {
return std::make_unique<Running>(inspector);
}
Running(Inspector &inspector) : InspectorState(inspector) {}
~Running() {}
void onEnter(InspectorState *prevState) override;
void detach(std::shared_ptr<folly::Promise<folly::Unit>> promise) override;
std::pair<NextStatePtr, CommandPtr> didPause(MonitorLock &lock) override;
bool pushPendingFunc(folly::Func func) override;
void pushPendingEval(
uint32_t frameIndex,
const std::string &src,
std::shared_ptr<folly::Promise<facebook::hermes::debugger::EvalResult>>
promise,
folly::Function<void(const facebook::hermes::debugger::EvalResult &)>
resultTransformer) override;
bool pause() override;
bool isRunning() const override {
return true;
}
const char *description() const override {
return "Running";
}
private:
std::vector<folly::Func> pendingFuncs_;
std::queue<PendingEval> pendingEvals_;
std::shared_ptr<folly::Promise<facebook::hermes::debugger::EvalResult>>
pendingEvalPromise_;
folly::Function<void(const facebook::hermes::debugger::EvalResult &)>
pendingEvalResultTransformer_;
std::shared_ptr<folly::Promise<folly::Unit>> pendingDetach_;
};
/**
* PendingCommand holds a resume or step command and a promise that is fulfilled
* just before the debugger resumes or steps.
*/
struct PendingCommand {
PendingCommand(
debugger::Command command,
std::shared_ptr<folly::Promise<folly::Unit>> promise)
: command(std::move(command)), promise(promise) {}
debugger::Command command;
std::shared_ptr<folly::Promise<folly::Unit>> promise;
};
/**
* Paused is the state when we're enabled and and currently in a didPause
* callback.
*/
class InspectorState::Paused : public InspectorState {
public:
static std::unique_ptr<InspectorState> make(Inspector &inspector) {
return std::make_unique<Paused>(inspector);
}
Paused(Inspector &inspector) : InspectorState(inspector) {}
~Paused() {}
void onEnter(InspectorState *prevState) override;
void detach(std::shared_ptr<folly::Promise<folly::Unit>> promise) override;
std::pair<NextStatePtr, CommandPtr> didPause(MonitorLock &lock) override;
bool pushPendingFunc(folly::Func func) override;
void pushPendingEval(
uint32_t frameIndex,
const std::string &src,
std::shared_ptr<folly::Promise<facebook::hermes::debugger::EvalResult>>
promise,
folly::Function<void(const facebook::hermes::debugger::EvalResult &)>
resultTransformer) override;
void setPendingCommand(
debugger::Command command,
std::shared_ptr<folly::Promise<folly::Unit>> promise) override;
bool isPaused() const override {
return true;
}
const char *description() const override {
return "Paused";
}
private:
std::condition_variable hasPendingWork_;
std::vector<folly::Func> pendingFuncs_;
std::queue<PendingEval> pendingEvals_;
std::shared_ptr<folly::Promise<facebook::hermes::debugger::EvalResult>>
pendingEvalPromise_;
folly::Function<void(const facebook::hermes::debugger::EvalResult &)>
pendingEvalResultTransformer_;
std::unique_ptr<PendingCommand> pendingCommand_;
std::shared_ptr<folly::Promise<folly::Unit>> pendingDetach_;
};
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,113 @@
hermes-inspector provides a bridge between the low-level debugging API exposed
by Hermes and higher-level debugging protocols such as the Chrome DevTools
protocol.
# Targets
- chrome: classes that implement the Chrome DevTools Protocol adapter. Sits on
top of classes provided by the inspector target.
- detail: utility classes and functions
- inspector: protocol-independent classes that sit on top of the low-level
Hermes debugging API.
# Testing
Tests are implemented using gtest. Debug logging is enabled for tests, and you
can get debug logs to show even when tests are passing by running the test
executable directly:
```
$ buck build //xplat/js/react-native-github/ReactCommon/hermes/inspector:chrome-tests
$ buck-out/gen/js/react-native-github/ReactCommon/hermes/inspector/chrome-tests
[...]
```
You can use standard gtest filters to only execute a particular set of tests:
```
$ buck-out/gen/js/react-native-github/ReactCommon/hermes/inspector/chrome-tests \
--gtest_filter='ConnectionTests.testSetBreakpoint'
```
You can debug the tests using lldb or gdb:
```
$ lldb buck-out/gen/js/react-native-github/ReactCommon/hermes/inspector/chrome-tests
$ gdb buck-out/gen/js/react-native-github/ReactCommon/hermes/inspector/chrome-tests
```
# Formatting
Make sure the code is formatted using the hermes clang-format rules before
committing:
```
$ xplat/js/react-native-github/ReactCommon/hermes/inspector/tools/format
```
We follow the clang format rules used by the rest of the Hermes project.
# Adding Support For New Message Types
To add support for a new Chrome DevTools protocol message, add the message you
want to add to tools/message_types.txt, and re-run the message types generator:
```
$ xplat/js/react-native-github/ReactCommon/hermes/inspector/tools/run_msggen
```
This will generate C++ structs for the new message type in
`chrome/MessageTypes.{h,cpp}`.
You'll then need to:
1. Implement a message handler for the new message type in `chrome::Connection`.
2. Implement a public API for the new message type in `Inspector`. This will
most likely return a `folly::Future` that the message handler in (1) can use
for chaining.
3. Implement a private API for the new message type in `Inspector` that performs
the logic in Inspector's executor. (Inspector.cpp contains a comment
explaining why the executor is necessary.)
4. Optionally, implement a method for the new message type in `InspectorState`.
In most cases this is probably not necessary--one of the existing methods in
`InspectorState` will work.
For a diff that illustrates these steps, take a look at D6601459.
# Testing Integration With Nuclide and Apps
For now, the quickest way to use hermes-inspector in an app is with Eats. First,
make sure the packager is running:
```
$ js1 run
```
Then, on Android, build the fbeats target:
```
$ buck install --run fbeats
```
On iOS, build the `//Apps/Internal/Eats:Eats` target:
```
$ buck install --run //Apps/Internal/Eats:Eats
```
You can also build `Eats` in Xcode using `arc focus` if you prefer an
IDE:
```
$ arc focus --force-build \
-b //Apps/Internal/Eats:Eats \
cxxreact //xplat/hermes/API:HermesAPI //xplat/hermes/lib/VM:VM jsi \
jsinspector hermes-inspector FBReactKit FBReactModule FBCatalystWrapper \
//xplat/js:React //xplat/js/react-native-github:ReactInternal
```
For all the above commands, if you want to build the inspector `-O0` for better
debug info, add the argument `--config hermes.build_mode=dbg`.
You should then be able to launch the app and see it listed in the list of
Mobile JS contexts in the Nuclide debugger.

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "RuntimeAdapter.h"
namespace facebook {
namespace hermes {
namespace inspector {
RuntimeAdapter::~RuntimeAdapter() = default;
void RuntimeAdapter::tickleJs() {}
SharedRuntimeAdapter::SharedRuntimeAdapter(
std::shared_ptr<jsi::Runtime> runtime,
debugger::Debugger &debugger)
: runtime_(std::move(runtime)), debugger_(debugger) {}
SharedRuntimeAdapter::~SharedRuntimeAdapter() = default;
jsi::Runtime &SharedRuntimeAdapter::getRuntime() {
return *runtime_;
}
debugger::Debugger &SharedRuntimeAdapter::getDebugger() {
return debugger_;
}
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <memory>
#include <hermes/hermes.h>
namespace facebook {
namespace hermes {
namespace inspector {
/**
* RuntimeAdapter encapsulates a HermesRuntime object. The underlying Hermes
* runtime object should stay alive for at least as long as the RuntimeAdapter
* is alive.
*/
class RuntimeAdapter {
public:
virtual ~RuntimeAdapter() = 0;
/// getRuntime should return the runtime encapsulated by this adapter.
virtual jsi::Runtime &getRuntime() = 0;
virtual debugger::Debugger &getDebugger() = 0;
/// tickleJs is a method that subclasses can choose to override to make the
/// inspector more responsive. If overridden, it should call the "__tickleJs"
/// function. The call should occur with appropriate locking (e.g. via a
/// thread-safe runtime instance, or by enqueuing the call on to a dedicated
/// JS thread).
///
/// This makes the inspector more responsive because it gives the inspector
/// the ability to force the process to enter the Hermes interpreter loop
/// soon. This is important because the inspector can only do a number of
/// important operations (like manipulating breakpoints) within the context of
/// a Hermes interperter loop.
///
/// The default implementation does nothing.
virtual void tickleJs();
};
/**
* SharedRuntimeAdapter is a simple implementation of RuntimeAdapter that
* uses shared_ptr to hold on to the runtime. It's generally only used in tests,
* since it does not implement tickleJs.
*/
class SharedRuntimeAdapter : public RuntimeAdapter {
public:
SharedRuntimeAdapter(
std::shared_ptr<jsi::Runtime> runtime,
debugger::Debugger &debugger);
virtual ~SharedRuntimeAdapter();
jsi::Runtime &getRuntime() override;
debugger::Debugger &getDebugger() override;
private:
std::shared_ptr<jsi::Runtime> runtime_;
debugger::Debugger &debugger_;
};
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,151 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "AutoAttachUtils.h"
#ifdef _WINDOWS
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
bool isNetworkInspected(
const std::string &,
const std::string &,
const std::string &) {
return false;
}
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook
#else
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <folly/String.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
// The following code is copied from
// https://phabricator.intern.facebook.com/diffusion/FBS/browse/master/xplat/js/react-native-github/ReactCommon/cxxreact/JSCExecutor.cpp;431c4d01b7072d9a1a52f8bd6c6ba2ff3e47e25d$250
bool isNetworkInspected(
const std::string &owner,
const std::string &app,
const std::string &device) {
auto connect_socket = [](int socket_desc, std::string address, int port) {
if (socket_desc < 0) {
close(socket_desc);
return false;
}
struct timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 0;
auto sock_opt_rcv_resp = setsockopt(
socket_desc,
SOL_SOCKET,
SO_RCVTIMEO,
(const char *)&tv,
sizeof(struct timeval));
if (sock_opt_rcv_resp < 0) {
close(socket_desc);
return false;
}
auto sock_opt_snd_resp = setsockopt(
socket_desc,
SOL_SOCKET,
SO_SNDTIMEO,
(const char *)&tv,
sizeof(struct timeval));
if (sock_opt_snd_resp < 0) {
close(socket_desc);
return false;
}
struct sockaddr_in server;
server.sin_addr.s_addr = inet_addr(address.c_str());
server.sin_family = AF_INET;
server.sin_port = htons(port);
auto connect_resp =
::connect(socket_desc, (struct sockaddr *)&server, sizeof(server));
if (connect_resp < 0) {
::close(socket_desc);
return false;
}
return true;
};
int socket_desc = socket(AF_INET, SOCK_STREAM, 0);
if (!connect_socket(socket_desc, "127.0.0.1", 8082)) {
#if defined(__ANDROID__)
socket_desc = socket(AF_INET, SOCK_STREAM, 0);
if (!connect_socket(socket_desc, "10.0.2.2", 8082) /* emulator */) {
socket_desc = socket(AF_INET, SOCK_STREAM, 0);
if (!connect_socket(socket_desc, "10.0.3.2", 8082) /* genymotion */) {
return false;
}
}
#else //! defined(__ANDROID__)
return false;
#endif // defined(__ANDROID__)
}
std::string escapedOwner =
folly::uriEscape<std::string>(owner, folly::UriEscapeMode::QUERY);
std::string escapedApp =
folly::uriEscape<std::string>(app, folly::UriEscapeMode::QUERY);
std::string escapedDevice =
folly::uriEscape<std::string>(device, folly::UriEscapeMode::QUERY);
std::string msg = folly::to<std::string>(
"GET /autoattach?title=",
escapedOwner,
"&app=",
escapedApp,
"&device=",
escapedDevice,
" HTTP/1.1\r\n\r\n");
auto send_resp = ::send(socket_desc, msg.c_str(), msg.length(), 0);
if (send_resp < 0) {
close(socket_desc);
return false;
}
char server_reply[200];
server_reply[199] = '\0';
auto recv_resp =
::recv(socket_desc, server_reply, sizeof(server_reply) - 1, 0);
if (recv_resp < 0) {
close(socket_desc);
return false;
}
std::string response(server_reply);
if (response.size() < 25) {
close(socket_desc);
return false;
}
auto responseCandidate = response.substr(response.size() - 25);
auto found =
responseCandidate.find("{\"autoattach\":true}") != std::string::npos;
close(socket_desc);
return found;
}
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook
#endif

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <string>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
bool isNetworkInspected(
const std::string &owner,
const std::string &app,
const std::string &device);
}
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,852 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "Connection.h"
#include <cstdlib>
#include <mutex>
#include <folly/Conv.h>
#include <folly/Executor.h>
#include <folly/Function.h>
#include <glog/logging.h>
#include <hermes/inspector/Inspector.h>
#include <hermes/inspector/chrome/MessageConverters.h>
#include <hermes/inspector/chrome/RemoteObjectsTable.h>
#include <hermes/inspector/detail/CallbackOStream.h>
#include <hermes/inspector/detail/SerialExecutor.h>
#include <hermes/inspector/detail/Thread.h>
#include <jsi/instrumentation.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
using ::facebook::react::ILocalConnection;
using ::facebook::react::IRemoteConnection;
using ::folly::Unit;
namespace debugger = ::facebook::hermes::debugger;
namespace inspector = ::facebook::hermes::inspector;
namespace m = ::facebook::hermes::inspector::chrome::message;
/*
* Connection::Impl
*/
class Connection::Impl : public inspector::InspectorObserver,
public message::RequestHandler {
public:
Impl(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string &title,
bool waitForDebugger);
~Impl();
jsi::Runtime &getRuntime();
std::string getTitle() const;
bool connect(std::unique_ptr<IRemoteConnection> remoteConn);
bool disconnect();
void sendMessage(std::string str);
/* InspectorObserver overrides */
void onBreakpointResolved(
Inspector &inspector,
const debugger::BreakpointInfo &info) override;
void onContextCreated(Inspector &inspector) override;
void onPause(Inspector &inspector, const debugger::ProgramState &state)
override;
void onResume(Inspector &inspector) override;
void onScriptParsed(Inspector &inspector, const ScriptInfo &info) override;
void onMessageAdded(Inspector &inspector, const ConsoleMessageInfo &info)
override;
/* RequestHandler overrides */
void handle(const m::UnknownRequest &req) override;
void handle(const m::debugger::DisableRequest &req) override;
void handle(const m::debugger::EnableRequest &req) override;
void handle(const m::debugger::EvaluateOnCallFrameRequest &req) override;
void handle(const m::debugger::PauseRequest &req) override;
void handle(const m::debugger::RemoveBreakpointRequest &req) override;
void handle(const m::debugger::ResumeRequest &req) override;
void handle(const m::debugger::SetBreakpointRequest &req) override;
void handle(const m::debugger::SetBreakpointByUrlRequest &req) override;
void handle(const m::debugger::SetPauseOnExceptionsRequest &req) override;
void handle(const m::debugger::StepIntoRequest &req) override;
void handle(const m::debugger::StepOutRequest &req) override;
void handle(const m::debugger::StepOverRequest &req) override;
void handle(const m::heapProfiler::TakeHeapSnapshotRequest &req) override;
void handle(
const m::heapProfiler::StartTrackingHeapObjectsRequest &req) override;
void handle(
const m::heapProfiler::StopTrackingHeapObjectsRequest &req) override;
void handle(const m::runtime::EvaluateRequest &req) override;
void handle(const m::runtime::GetPropertiesRequest &req) override;
private:
std::vector<m::runtime::PropertyDescriptor> makePropsFromScope(
std::pair<uint32_t, uint32_t> frameAndScopeIndex,
const std::string &objectGroup,
const debugger::ProgramState &state);
std::vector<m::runtime::PropertyDescriptor> makePropsFromValue(
const jsi::Value &value,
const std::string &objectGroup,
bool onlyOwnProperties);
void sendSnapshot(
int reqId,
std::string message,
bool reportProgress,
bool stopStackTraceCapture);
void sendToClient(const std::string &str);
void sendResponseToClient(const m::Response &resp);
void sendNotificationToClient(const m::Notification &resp);
folly::Function<void(const std::exception &)> sendErrorToClient(int id);
void sendResponseToClientViaExecutor(int id);
void sendResponseToClientViaExecutor(folly::Future<Unit> future, int id);
void sendNotificationToClientViaExecutor(const m::Notification &note);
void sendErrorToClientViaExecutor(int id, const std::string &error);
std::shared_ptr<RuntimeAdapter> runtimeAdapter_;
std::string title_;
// connected_ is protected by connectionMutex_.
std::mutex connectionMutex_;
bool connected_;
// parsedScripts_ list stores file names of all scripts that have been
// parsed so that we could find script's file name by regex.
// This is similar to Inspector's loadedScripts_ map but we want to
// store this info here because searching file name that matches
// given regex (on setBreakpointByUrl command) is more related to Chrome
// protocol than to Hermes inspector.
// Access is protected by parsedScriptsMutex_.
std::mutex parsedScriptsMutex_;
std::vector<std::string> parsedScripts_;
// The rest of these member variables are only accessed via executor_.
std::unique_ptr<folly::Executor> executor_;
std::unique_ptr<IRemoteConnection> remoteConn_;
std::shared_ptr<inspector::Inspector> inspector_;
// objTable_ is protected by the inspector lock. It should only be accessed
// when the VM is paused, e.g. in an InspectorObserver callback or in an
// executeIfEnabled callback.
RemoteObjectsTable objTable_;
};
Connection::Impl::Impl(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string &title,
bool waitForDebugger)
: runtimeAdapter_(std::move(adapter)),
title_(title),
connected_(false),
executor_(std::make_unique<inspector::detail::SerialExecutor>(
"hermes-chrome-inspector-conn")),
remoteConn_(nullptr),
inspector_(std::make_shared<inspector::Inspector>(
runtimeAdapter_,
*this,
waitForDebugger)) {
inspector_->installLogHandler();
}
Connection::Impl::~Impl() = default;
jsi::Runtime &Connection::Impl::getRuntime() {
return runtimeAdapter_->getRuntime();
}
std::string Connection::Impl::getTitle() const {
return title_;
}
bool Connection::Impl::connect(std::unique_ptr<IRemoteConnection> remoteConn) {
assert(remoteConn);
std::lock_guard<std::mutex> lock(connectionMutex_);
if (connected_) {
return false;
}
connected_ = true;
executor_->add([this, remoteConn = std::move(remoteConn)]() mutable {
remoteConn_ = std::move(remoteConn);
});
return true;
}
bool Connection::Impl::disconnect() {
std::lock_guard<std::mutex> lock(connectionMutex_);
if (!connected_) {
return false;
}
connected_ = false;
inspector_->disable().via(executor_.get()).thenValue([this](auto &&) {
// HACK: We purposely call RemoteConnection::onDisconnect on a *different*
// rather than on this thread (the executor thread). This is to prevent this
// scenario:
//
// 1. RemoteConnection::onDisconnect runs on the executor thread
// 2. onDisconnect through a long chain of calls causes the Connection
// destructor to run
// 3. The Connection destructor causes the SerialExecutor destructor to run.
// 4. The SerialExecutor destructor waits for all outstanding work items to
// finish via a call to join().
// 5. join() fails, since the executor thread is trying to join against
// itself.
//
// To prevent this chain of events, we always call onDisconnect on a
// different thread.
//
// See P59135203 for an example stack trace.
//
// One more hack: we use release() and delete instead of unique_ptr because
// detail::Thread expects a std::function, and std::function cannot capture
// move-only types like unique_ptr.
auto conn = remoteConn_.release();
inspector::detail::Thread disconnectLaterThread{
"hermes-chrome-inspector-conn-disconnect", [conn] {
conn->onDisconnect();
delete conn;
}};
disconnectLaterThread.detach();
});
return true;
}
void Connection::Impl::sendMessage(std::string str) {
executor_->add([this, str = std::move(str)]() mutable {
folly::Try<std::unique_ptr<m::Request>> maybeReq =
m::Request::fromJson(str);
if (maybeReq.hasException()) {
LOG(ERROR) << "Invalid request `" << str
<< "`: " << maybeReq.exception().what();
return;
}
auto &req = maybeReq.value();
if (req) {
req->accept(*this);
}
});
}
/*
* InspectorObserver overrides
*/
void Connection::Impl::onBreakpointResolved(
Inspector &inspector,
const debugger::BreakpointInfo &info) {
m::debugger::BreakpointResolvedNotification note;
note.breakpointId = folly::to<std::string>(info.id);
note.location = m::debugger::makeLocation(info.resolvedLocation);
sendNotificationToClientViaExecutor(note);
}
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.name = "hermes";
sendNotificationToClientViaExecutor(note);
}
void Connection::Impl::onPause(
Inspector &inspector,
const debugger::ProgramState &state) {
m::debugger::PausedNotification note;
note.callFrames = m::debugger::makeCallFrames(state, objTable_, getRuntime());
switch (state.getPauseReason()) {
case debugger::PauseReason::Breakpoint:
// use other, chrome protocol has no reason specifically for breakpoints
note.reason = "other";
// TODO: hermes hasn't implemented ProgramState::getBreakpoint yet
#if HERMES_SUPPORTS_STATE_GET_BREAKPOINT
note.hitBreakpoints = std::vector<m::debugger::BreakpointId>();
note.hitBreakpoints->emplace_back(
folly::to<std::string>(state.getBreakpoint()));
#endif
break;
case debugger::PauseReason::Exception:
note.reason = "exception";
break;
default:
note.reason = "other";
break;
}
sendNotificationToClientViaExecutor(note);
}
void Connection::Impl::onResume(Inspector &inspector) {
objTable_.releaseObjectGroup(BacktraceObjectGroup);
m::debugger::ResumedNotification note;
sendNotificationToClientViaExecutor(note);
}
void Connection::Impl::onScriptParsed(
Inspector &inspector,
const ScriptInfo &info) {
m::debugger::ScriptParsedNotification note;
note.scriptId = folly::to<std::string>(info.fileId);
note.url = info.fileName;
if (!info.sourceMappingUrl.empty()) {
note.sourceMapURL = info.sourceMappingUrl;
}
{
std::lock_guard<std::mutex> lock(parsedScriptsMutex_);
parsedScripts_.push_back(info.fileName);
}
sendNotificationToClientViaExecutor(note);
}
void Connection::Impl::onMessageAdded(
facebook::hermes::inspector::Inspector &inspector,
const ConsoleMessageInfo &info) {
m::runtime::ConsoleAPICalledNotification apiCalledNote;
apiCalledNote.type = info.level;
size_t argsSize = info.args.size(getRuntime());
for (size_t index = 0; index < argsSize; ++index) {
apiCalledNote.args.push_back(m::runtime::makeRemoteObject(
getRuntime(),
info.args.getValueAtIndex(getRuntime(), index),
objTable_,
"ConsoleObjectGroup"));
}
sendNotificationToClientViaExecutor(apiCalledNote);
}
/*
* RequestHandler overrides
*/
void Connection::Impl::handle(const m::UnknownRequest &req) {
LOG(INFO) << "responding ok to unknown request: " << req.toDynamic();
sendResponseToClientViaExecutor(req.id);
}
void Connection::Impl::handle(const m::debugger::DisableRequest &req) {
sendResponseToClientViaExecutor(inspector_->disable(), req.id);
}
void Connection::Impl::handle(const m::debugger::EnableRequest &req) {
sendResponseToClientViaExecutor(inspector_->enable(), req.id);
}
void Connection::Impl::handle(
const m::debugger::EvaluateOnCallFrameRequest &req) {
auto remoteObjPtr = std::make_shared<m::runtime::RemoteObject>();
inspector_
->evaluate(
atoi(req.callFrameId.c_str()),
req.expression,
[this,
remoteObjPtr,
objectGroup = req.objectGroup,
byValue = req.returnByValue.value_or(false)](
const facebook::hermes::debugger::EvalResult
&evalResult) mutable {
*remoteObjPtr = m::runtime::makeRemoteObject(
getRuntime(),
evalResult.value,
objTable_,
objectGroup.value_or(""),
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::sendSnapshot(
int reqId,
std::string message,
bool reportProgress,
bool stopStackTraceCapture) {
inspector_
->executeIfEnabled(
message,
[this, reportProgress, stopStackTraceCapture](
const debugger::ProgramState &) {
if (reportProgress) {
// A progress notification with finished = true indicates the
// snapshot has been captured and is ready to be sent. Our
// implementation streams the snapshot as it is being captured, so
// we must send this notification first.
m::heapProfiler::ReportHeapSnapshotProgressNotification note;
note.done = 1;
note.total = 1;
note.finished = true;
sendNotificationToClient(note);
}
// Size picked to conform to Chrome's own implementation, at the
// time of writing.
inspector::detail::CallbackOStream cos(
/* sz */ 100 << 10, [this](std::string s) {
m::heapProfiler::AddHeapSnapshotChunkNotification note;
note.chunk = std::move(s);
sendNotificationToClient(note);
return true;
});
getRuntime().instrumentation().createSnapshotToStream(cos);
if (stopStackTraceCapture) {
getRuntime()
.instrumentation()
.stopTrackingHeapObjectStackTraces();
}
})
.via(executor_.get())
.thenValue([this, reqId](auto &&) {
sendResponseToClient(m::makeOkResponse(reqId));
})
.thenError<std::exception>(sendErrorToClient(reqId));
}
void Connection::Impl::handle(
const m::heapProfiler::TakeHeapSnapshotRequest &req) {
sendSnapshot(
req.id,
"HeapSnapshot.takeHeapSnapshot",
req.reportProgress && *req.reportProgress,
/* stopStackTraceCapture */ false);
}
void Connection::Impl::handle(
const m::heapProfiler::StartTrackingHeapObjectsRequest &req) {
const auto id = req.id;
inspector_
->executeIfEnabled(
"HeapProfiler.startTrackingHeapObjects",
[this](const debugger::ProgramState &) {
getRuntime().instrumentation().startTrackingHeapObjectStackTraces();
})
.via(executor_.get())
.thenValue(
[this, id](auto &&) { sendResponseToClient(m::makeOkResponse(id)); })
.thenError<std::exception>(sendErrorToClient(req.id));
}
void Connection::Impl::handle(
const m::heapProfiler::StopTrackingHeapObjectsRequest &req) {
sendSnapshot(
req.id,
"HeapSnapshot.takeHeapSnapshot",
req.reportProgress && *req.reportProgress,
/* stopStackTraceCapture */ true);
}
void Connection::Impl::handle(const m::runtime::EvaluateRequest &req) {
auto remoteObjPtr = std::make_shared<m::runtime::RemoteObject>();
inspector_
->evaluate(
0, // Top of the stackframe
req.expression,
[this,
remoteObjPtr,
objectGroup = req.objectGroup,
byValue = req.returnByValue.value_or(false)](
const facebook::hermes::debugger::EvalResult
&evalResult) mutable {
*remoteObjPtr = m::runtime::makeRemoteObject(
getRuntime(),
evalResult.value,
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::debugger::PauseRequest &req) {
sendResponseToClientViaExecutor(inspector_->pause(), req.id);
}
void Connection::Impl::handle(const m::debugger::RemoveBreakpointRequest &req) {
auto breakpointId = folly::to<debugger::BreakpointID>(req.breakpointId);
sendResponseToClientViaExecutor(
inspector_->removeBreakpoint(breakpointId), req.id);
}
void Connection::Impl::handle(const m::debugger::ResumeRequest &req) {
sendResponseToClientViaExecutor(inspector_->resume(), req.id);
}
void Connection::Impl::handle(const m::debugger::SetBreakpointRequest &req) {
debugger::SourceLocation loc;
auto scriptId = folly::tryTo<unsigned int>(req.location.scriptId);
if (!scriptId) {
sendErrorToClientViaExecutor(
req.id, "Expected integer scriptId: " + req.location.scriptId);
return;
}
loc.fileId = scriptId.value();
// CDP Locations are 0-based, Hermes lines/columns are 1-based
loc.line = req.location.lineNumber + 1;
if (req.location.columnNumber) {
loc.column = req.location.columnNumber.value() + 1;
}
inspector_->setBreakpoint(loc, req.condition)
.via(executor_.get())
.thenValue([this, id = req.id](debugger::BreakpointInfo info) {
m::debugger::SetBreakpointResponse resp;
resp.id = id;
resp.breakpointId = folly::to<std::string>(info.id);
if (info.resolved) {
resp.actualLocation =
m::debugger::makeLocation(info.resolvedLocation);
}
sendResponseToClient(resp);
})
.thenError<std::exception>(sendErrorToClient(req.id));
}
void Connection::Impl::handle(
const m::debugger::SetBreakpointByUrlRequest &req) {
debugger::SourceLocation loc;
{
std::lock_guard<std::mutex> lock(parsedScriptsMutex_);
setHermesLocation(loc, req, parsedScripts_);
}
inspector_->setBreakpoint(loc, req.condition)
.via(executor_.get())
.thenValue([this, id = req.id](debugger::BreakpointInfo info) {
m::debugger::SetBreakpointByUrlResponse resp;
resp.id = id;
resp.breakpointId = folly::to<std::string>(info.id);
if (info.resolved) {
resp.locations.emplace_back(
m::debugger::makeLocation(info.resolvedLocation));
}
sendResponseToClient(resp);
})
.thenError<std::exception>(sendErrorToClient(req.id));
}
void Connection::Impl::handle(
const m::debugger::SetPauseOnExceptionsRequest &req) {
debugger::PauseOnThrowMode mode = debugger::PauseOnThrowMode::None;
if (req.state == "none") {
mode = debugger::PauseOnThrowMode::None;
} else if (req.state == "all") {
mode = debugger::PauseOnThrowMode::All;
} else if (req.state == "uncaught") {
mode = debugger::PauseOnThrowMode::Uncaught;
} else {
sendErrorToClientViaExecutor(
req.id, "Unknown pause-on-exception state: " + req.state);
return;
}
sendResponseToClientViaExecutor(
inspector_->setPauseOnExceptions(mode), req.id);
}
void Connection::Impl::handle(const m::debugger::StepIntoRequest &req) {
sendResponseToClientViaExecutor(inspector_->stepIn(), req.id);
}
void Connection::Impl::handle(const m::debugger::StepOutRequest &req) {
sendResponseToClientViaExecutor(inspector_->stepOut(), req.id);
}
void Connection::Impl::handle(const m::debugger::StepOverRequest &req) {
sendResponseToClientViaExecutor(inspector_->stepOver(), req.id);
}
std::vector<m::runtime::PropertyDescriptor>
Connection::Impl::makePropsFromScope(
std::pair<uint32_t, uint32_t> frameAndScopeIndex,
const std::string &objectGroup,
const debugger::ProgramState &state) {
// Chrome represents variables in a scope as properties on a dummy object.
// We don't instantiate such dummy objects, we just pretended to have one.
// Chrome has now asked for its properties, so it's time to synthesize
// descriptions of the properties that the dummy object would have had.
std::vector<m::runtime::PropertyDescriptor> result;
uint32_t frameIndex = frameAndScopeIndex.first;
uint32_t scopeIndex = frameAndScopeIndex.second;
debugger::LexicalInfo lexicalInfo = state.getLexicalInfo(frameIndex);
uint32_t varCount = lexicalInfo.getVariablesCountInScope(scopeIndex);
// If this is the frame's local scope, include 'this'.
if (scopeIndex == 0) {
auto varInfo = state.getVariableInfoForThis(frameIndex);
m::runtime::PropertyDescriptor desc;
desc.name = varInfo.name;
desc.value = m::runtime::makeRemoteObject(
getRuntime(), varInfo.value, objTable_, objectGroup);
// Chrome only shows enumerable properties.
desc.enumerable = true;
result.emplace_back(std::move(desc));
}
// Then add each of the variables in this lexical scope.
for (uint32_t varIndex = 0; varIndex < varCount; varIndex++) {
debugger::VariableInfo varInfo =
state.getVariableInfo(frameIndex, scopeIndex, varIndex);
m::runtime::PropertyDescriptor desc;
desc.name = varInfo.name;
desc.value = m::runtime::makeRemoteObject(
getRuntime(), varInfo.value, objTable_, objectGroup);
desc.enumerable = true;
result.emplace_back(std::move(desc));
}
return result;
}
std::vector<m::runtime::PropertyDescriptor>
Connection::Impl::makePropsFromValue(
const jsi::Value &value,
const std::string &objectGroup,
bool onlyOwnProperties) {
std::vector<m::runtime::PropertyDescriptor> result;
if (value.isObject()) {
jsi::Runtime &runtime = getRuntime();
jsi::Object obj = value.getObject(runtime);
// TODO(hypuk): obj.getPropertyNames only returns enumerable properties.
jsi::Array propNames = onlyOwnProperties
? runtime.global()
.getPropertyAsObject(runtime, "Object")
.getPropertyAsFunction(runtime, "getOwnPropertyNames")
.call(runtime, obj)
.getObject(runtime)
.getArray(runtime)
: obj.getPropertyNames(runtime);
size_t propCount = propNames.length(runtime);
for (size_t i = 0; i < propCount; i++) {
jsi::String propName =
propNames.getValueAtIndex(runtime, i).getString(runtime);
m::runtime::PropertyDescriptor desc;
desc.name = propName.utf8(runtime);
try {
// Currently, we fetch the property even if it runs code.
// Chrome instead detects getters and makes you click to invoke.
jsi::Value propValue = obj.getProperty(runtime, propName);
desc.value = m::runtime::makeRemoteObject(
runtime, propValue, objTable_, objectGroup);
} catch (const jsi::JSError &err) {
// We fetched a property with a getter that threw. Show a placeholder.
// We could have added additional info, but the UI quickly gets messy.
desc.value = m::runtime::makeRemoteObject(
runtime,
jsi::String::createFromUtf8(runtime, "(Exception)"),
objTable_,
objectGroup);
}
result.emplace_back(std::move(desc));
}
if (onlyOwnProperties) {
jsi::Value proto = runtime.global()
.getPropertyAsObject(runtime, "Object")
.getPropertyAsFunction(runtime, "getPrototypeOf")
.call(runtime, obj);
if (!proto.isNull()) {
m::runtime::PropertyDescriptor desc;
desc.name = "__proto__";
desc.value = m::runtime::makeRemoteObject(
runtime, proto, objTable_, objectGroup);
result.emplace_back(std::move(desc));
}
}
}
return result;
}
void Connection::Impl::handle(const m::runtime::GetPropertiesRequest &req) {
auto resp = std::make_shared<m::runtime::GetPropertiesResponse>();
resp->id = req.id;
inspector_
->executeIfEnabled(
"Runtime.getProperties",
[this, req, resp](const debugger::ProgramState &state) {
std::string objGroup = objTable_.getObjectGroup(req.objectId);
auto scopePtr = objTable_.getScope(req.objectId);
auto valuePtr = objTable_.getValue(req.objectId);
if (scopePtr != nullptr) {
resp->result = makePropsFromScope(*scopePtr, objGroup, state);
} else if (valuePtr != nullptr) {
resp->result = makePropsFromValue(
*valuePtr, objGroup, req.ownProperties.value_or(true));
}
})
.via(executor_.get())
.thenValue([this, resp](auto &&) { sendResponseToClient(*resp); })
.thenError<std::exception>(sendErrorToClient(req.id));
}
/*
* Send-to-client methods
*/
void Connection::Impl::sendToClient(const std::string &str) {
if (remoteConn_) {
remoteConn_->onMessage(str);
}
}
void Connection::Impl::sendResponseToClient(const m::Response &resp) {
sendToClient(resp.toJson());
}
void Connection::Impl::sendNotificationToClient(const m::Notification &note) {
sendToClient(note.toJson());
}
folly::Function<void(const std::exception &)>
Connection::Impl::sendErrorToClient(int id) {
return [this, id](const std::exception &e) {
sendResponseToClient(
m::makeErrorResponse(id, m::ErrorCode::ServerError, e.what()));
};
}
void Connection::Impl::sendResponseToClientViaExecutor(int id) {
sendResponseToClientViaExecutor(folly::makeFuture(), id);
}
void Connection::Impl::sendResponseToClientViaExecutor(
folly::Future<Unit> future,
int id) {
future.via(executor_.get())
.thenValue([this, id](const Unit &unit) {
sendResponseToClient(m::makeOkResponse(id));
})
.thenError<std::exception>(sendErrorToClient(id));
}
void Connection::Impl::sendErrorToClientViaExecutor(
int id,
const std::string &error) {
folly::makeFuture()
.via(executor_.get())
.thenValue([this, id, error](const Unit &unit) {
sendResponseToClient(
makeErrorResponse(id, m::ErrorCode::ServerError, error));
});
}
void Connection::Impl::sendNotificationToClientViaExecutor(
const m::Notification &note) {
executor_->add(
[this, noteJson = note.toJson()]() { sendToClient(noteJson); });
}
/*
* Connection
*/
Connection::Connection(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string &title,
bool waitForDebugger)
: impl_(
std::make_unique<Impl>(std::move(adapter), title, waitForDebugger)) {}
Connection::~Connection() = default;
jsi::Runtime &Connection::getRuntime() {
return impl_->getRuntime();
}
std::string Connection::getTitle() const {
return impl_->getTitle();
}
bool Connection::connect(std::unique_ptr<IRemoteConnection> remoteConn) {
return impl_->connect(std::move(remoteConn));
}
bool Connection::disconnect() {
return impl_->disconnect();
}
void Connection::sendMessage(std::string str) {
impl_->sendMessage(std::move(str));
}
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <hermes/hermes.h>
#include <hermes/inspector/RuntimeAdapter.h>
#include <jsinspector/InspectorInterfaces.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
/// Connection is a duplex connection between the client and the debugger.
class Connection {
public:
/// Connection constructor enables the debugger on the provided runtime. This
/// should generally called before you start running any JS in the runtime.
Connection(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string &title,
bool waitForDebugger = false);
~Connection();
/// getRuntime returns the underlying runtime being debugged.
jsi::Runtime &getRuntime();
/// getTitle returns the name of the friendly name of the runtime that's shown
/// to users in Nuclide.
std::string getTitle() const;
/// connect attaches this connection to the runtime's debugger. Requests to
/// the debugger sent via send(). Replies and notifications from the debugger
/// are sent back to the client via IRemoteConnection::onMessage.
bool connect(
std::unique_ptr<::facebook::react::IRemoteConnection> remoteConn);
/// disconnect disconnects this connection from the runtime's debugger
bool disconnect();
/// sendMessage delivers a JSON-encoded Chrome DevTools Protocol request to
/// the debugger.
void sendMessage(std::string str);
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,142 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "ConnectionDemux.h"
#include "AutoAttachUtils.h"
#include "Connection.h"
#include <jsinspector/InspectorInterfaces.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
using ::facebook::react::IInspector;
using ::facebook::react::ILocalConnection;
using ::facebook::react::IRemoteConnection;
namespace {
class LocalConnection : public ILocalConnection {
public:
LocalConnection(
std::shared_ptr<Connection> conn,
std::shared_ptr<std::unordered_set<std::string>> inspectedContexts);
~LocalConnection();
void sendMessage(std::string message) override;
void disconnect() override;
private:
std::shared_ptr<Connection> conn_;
std::shared_ptr<std::unordered_set<std::string>> inspectedContexts_;
};
LocalConnection::LocalConnection(
std::shared_ptr<Connection> conn,
std::shared_ptr<std::unordered_set<std::string>> inspectedContexts)
: conn_(conn), inspectedContexts_(inspectedContexts) {
inspectedContexts_->insert(conn->getTitle());
}
LocalConnection::~LocalConnection() = default;
void LocalConnection::sendMessage(std::string str) {
conn_->sendMessage(std::move(str));
}
void LocalConnection::disconnect() {
inspectedContexts_->erase(conn_->getTitle());
conn_->disconnect();
}
} // namespace
ConnectionDemux::ConnectionDemux(facebook::react::IInspector &inspector)
: globalInspector_(inspector),
inspectedContexts_(std::make_shared<std::unordered_set<std::string>>()) {}
ConnectionDemux::~ConnectionDemux() = default;
int ConnectionDemux::enableDebugging(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string &title) {
std::lock_guard<std::mutex> lock(mutex_);
// TODO(#22976087): workaround for ComponentScript contexts never being
// destroyed.
//
// After a reload, the old ComponentScript VM instance stays alive. When we
// register the new CS VM instance, check for any previous CS VM (via strcmp
// of title) and remove them.
std::vector<int> pagesToDelete;
for (auto it = conns_.begin(); it != conns_.end(); ++it) {
if (it->second->getTitle() == title) {
pagesToDelete.push_back(it->first);
}
}
for (auto pageId : pagesToDelete) {
removePage(pageId);
}
// TODO(hypuk): Provide real app and device names.
auto waitForDebugger =
(inspectedContexts_->find(title) != inspectedContexts_->end()) ||
isNetworkInspected(title, "app_name", "device_name");
return addPage(
std::make_shared<Connection>(std::move(adapter), title, waitForDebugger));
}
void ConnectionDemux::disableDebugging(HermesRuntime &runtime) {
std::lock_guard<std::mutex> lock(mutex_);
for (auto &it : conns_) {
int pageId = it.first;
auto &conn = it.second;
if (&(conn->getRuntime()) == &runtime) {
removePage(pageId);
// must break here. removePage mutates conns_, so range-for iterator is
// now invalid.
break;
}
}
}
int ConnectionDemux::addPage(std::shared_ptr<Connection> conn) {
auto connectFunc = [conn, this](std::unique_ptr<IRemoteConnection> remoteConn)
-> std::unique_ptr<ILocalConnection> {
if (!conn->connect(std::move(remoteConn))) {
return nullptr;
}
return std::make_unique<LocalConnection>(conn, inspectedContexts_);
};
int pageId = globalInspector_.addPage(
conn->getTitle(), "Hermes", std::move(connectFunc));
conns_[pageId] = std::move(conn);
return pageId;
}
void ConnectionDemux::removePage(int pageId) {
globalInspector_.removePage(pageId);
auto conn = conns_.at(pageId);
conn->disconnect();
conns_.erase(pageId);
}
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <hermes/hermes.h>
#include <hermes/inspector/RuntimeAdapter.h>
#include <hermes/inspector/chrome/Connection.h>
#include <jsinspector/InspectorInterfaces.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
/*
* ConnectionDemux keeps track of all debuggable Hermes runtimes (called
* "pages" in the higher-level React Native API) in this process. See
* Registration.h for documentation of the public API.
*/
class ConnectionDemux {
public:
explicit ConnectionDemux(facebook::react::IInspector &inspector);
~ConnectionDemux();
ConnectionDemux(const ConnectionDemux &) = delete;
ConnectionDemux &operator=(const ConnectionDemux &) = delete;
int enableDebugging(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string &title);
void disableDebugging(HermesRuntime &runtime);
private:
int addPage(std::shared_ptr<Connection> conn);
void removePage(int pageId);
facebook::react::IInspector &globalInspector_;
std::mutex mutex_;
std::unordered_map<int, std::shared_ptr<Connection>> conns_;
std::shared_ptr<std::unordered_set<std::string>> inspectedContexts_;
};
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,248 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "MessageConverters.h"
#include <cmath>
#include <limits>
#include <folly/Conv.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
namespace h = ::facebook::hermes;
namespace m = ::facebook::hermes::inspector::chrome::message;
m::ErrorResponse
m::makeErrorResponse(int id, m::ErrorCode code, const std::string &message) {
m::ErrorResponse resp;
resp.id = id;
resp.code = static_cast<int>(code);
resp.message = message;
return resp;
}
m::OkResponse m::makeOkResponse(int id) {
m::OkResponse resp;
resp.id = id;
return resp;
}
std::string m::stripCachePrevention(const std::string &url) {
std::regex regex("&?cachePrevention=[0-9]*");
return std::regex_replace(url, regex, "");
}
/*
* debugger message conversion helpers
*/
m::debugger::Location m::debugger::makeLocation(
const h::debugger::SourceLocation &loc) {
m::debugger::Location result;
result.scriptId = folly::to<std::string>(loc.fileId);
m::setChromeLocation(result, loc);
return result;
}
m::debugger::CallFrame m::debugger::makeCallFrame(
uint32_t callFrameIndex,
const h::debugger::CallFrameInfo &callFrameInfo,
const h::debugger::LexicalInfo &lexicalInfo,
RemoteObjectsTable &objTable,
jsi::Runtime &runtime,
const facebook::hermes::debugger::ProgramState &state) {
m::debugger::CallFrame result;
result.callFrameId = folly::to<std::string>(callFrameIndex);
result.functionName = callFrameInfo.functionName;
result.location = makeLocation(callFrameInfo.location);
uint32_t scopeCount = lexicalInfo.getScopesCount();
// First we have our local scope (unless we're in the global function)
if (scopeCount > 1) {
m::debugger::Scope scope;
scope.type = "local";
scope.object.objectId = objTable.addScope(
std::make_pair(callFrameIndex, 0), BacktraceObjectGroup);
scope.object.type = "object";
scope.object.className = "Object";
result.scopeChain.emplace_back(std::move(scope));
}
// Then we have zero or more parent closure scopes
for (uint32_t scopeIndex = 1; scopeIndex < scopeCount - 1; scopeIndex++) {
m::debugger::Scope scope;
scope.type = "closure";
// TODO: Get the parent closure's name
scope.name = folly::to<std::string>(scopeIndex);
scope.object.objectId = objTable.addScope(
std::make_pair(callFrameIndex, scopeIndex), BacktraceObjectGroup);
scope.object.type = "object";
scope.object.className = "Object";
result.scopeChain.emplace_back(std::move(scope));
}
// Finally, we always have the global scope
{
m::debugger::Scope scope;
scope.type = "global";
scope.object.objectId =
objTable.addValue(runtime.global(), BacktraceObjectGroup);
scope.object.type = "object";
scope.object.className = "Object";
result.scopeChain.emplace_back(std::move(scope));
}
result.thisObj.type = "object";
result.thisObj.objectId = objTable.addValue(
state.getVariableInfoForThis(callFrameIndex).value, BacktraceObjectGroup);
return result;
}
std::vector<m::debugger::CallFrame> m::debugger::makeCallFrames(
const h::debugger::ProgramState &state,
RemoteObjectsTable &objTable,
jsi::Runtime &runtime) {
const h::debugger::StackTrace &stackTrace = state.getStackTrace();
uint32_t count = stackTrace.callFrameCount();
std::vector<m::debugger::CallFrame> result;
result.reserve(count);
for (uint32_t i = 0; i < count; i++) {
h::debugger::CallFrameInfo callFrameInfo = stackTrace.callFrameForIndex(i);
h::debugger::LexicalInfo lexicalInfo = state.getLexicalInfo(i);
result.emplace_back(
makeCallFrame(i, callFrameInfo, lexicalInfo, objTable, runtime, state));
}
return result;
}
/*
* runtime message conversion helpers
*/
m::runtime::CallFrame m::runtime::makeCallFrame(
const h::debugger::CallFrameInfo &info) {
m::runtime::CallFrame result;
result.functionName = info.functionName;
result.scriptId = folly::to<std::string>(info.location.fileId);
result.url = info.location.fileName;
m::setChromeLocation(result, info.location);
return result;
}
std::vector<m::runtime::CallFrame> m::runtime::makeCallFrames(
const facebook::hermes::debugger::StackTrace &stackTrace) {
std::vector<m::runtime::CallFrame> result;
result.reserve(stackTrace.callFrameCount());
for (size_t i = 0; i < stackTrace.callFrameCount(); i++) {
h::debugger::CallFrameInfo info = stackTrace.callFrameForIndex(i);
result.emplace_back(makeCallFrame(info));
}
return result;
}
m::runtime::ExceptionDetails m::runtime::makeExceptionDetails(
const h::debugger::ExceptionDetails &details) {
m::runtime::ExceptionDetails result;
result.text = details.text;
result.scriptId = folly::to<std::string>(details.location.fileId);
result.url = details.location.fileName;
result.stackTrace = m::runtime::StackTrace();
result.stackTrace->callFrames = makeCallFrames(details.getStackTrace());
m::setChromeLocation(result, details.location);
return result;
}
m::runtime::RemoteObject m::runtime::makeRemoteObject(
facebook::jsi::Runtime &runtime,
const facebook::jsi::Value &value,
RemoteObjectsTable &objTable,
const std::string &objectGroup,
bool byValue) {
m::runtime::RemoteObject result;
if (value.isUndefined()) {
result.type = "undefined";
} else if (value.isNull()) {
result.type = "object";
result.subtype = "null";
result.value = "null";
} else if (value.isBool()) {
result.type = "boolean";
result.value = value.getBool();
} else if (value.isNumber()) {
double numberValue = value.getNumber();
result.type = "number";
if (std::isnan(numberValue)) {
result.description = result.unserializableValue = "NaN";
} else if (numberValue == -std::numeric_limits<double>::infinity()) {
result.description = result.unserializableValue = "-Infinity";
} else if (numberValue == std::numeric_limits<double>::infinity()) {
result.description = result.unserializableValue = "Infinity";
} else if (numberValue == 0.0 && std::signbit(numberValue)) {
result.description = result.unserializableValue = "-0";
} else {
result.value = numberValue;
}
} else if (value.isString()) {
result.type = "string";
result.value = value.getString(runtime).utf8(runtime);
} else if (value.isObject()) {
jsi::Object obj = value.getObject(runtime);
if (obj.isFunction(runtime)) {
result.type = "function";
result.value = "";
} else if (obj.isArray(runtime)) {
auto array = obj.getArray(runtime);
size_t arrayCount = array.length(runtime);
result.type = "object";
result.subtype = "array";
result.className = "Array";
result.description = "Array(" + folly::to<std::string>(arrayCount) + ")";
} else {
result.type = "object";
result.description = result.className = "Object";
}
if (byValue) {
// FIXME: JSI currently does not handle cycles and functions well here
result.value = jsi::dynamicFromValue(runtime, value);
} else {
result.objectId =
objTable.addValue(jsi::Value(std::move(obj)), objectGroup);
}
}
return result;
}
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,135 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <regex>
#include <string>
#include <vector>
#include <hermes/DebuggerAPI.h>
#include <hermes/hermes.h>
#include <hermes/inspector/chrome/MessageTypes.h>
#include <hermes/inspector/chrome/RemoteObjectsTable.h>
#include <jsi/JSIDynamic.h>
#include <jsi/jsi.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
namespace message {
std::string stripCachePrevention(const std::string &url);
template <typename T>
void setHermesLocation(
facebook::hermes::debugger::SourceLocation &hermesLoc,
const T &chromeLoc,
const std::vector<std::string> &parsedScripts) {
hermesLoc.line = chromeLoc.lineNumber + 1;
if (chromeLoc.columnNumber.hasValue()) {
if (chromeLoc.columnNumber.value() == 0) {
// TODO: When CDTP sends a column number of 0, we send Hermes a column
// number of 1. For some reason, this causes Hermes to not be
// able to resolve breakpoints.
hermesLoc.column = ::facebook::hermes::debugger::kInvalidLocation;
} else {
hermesLoc.column = chromeLoc.columnNumber.value() + 1;
}
}
if (chromeLoc.url.hasValue()) {
hermesLoc.fileName = stripCachePrevention(chromeLoc.url.value());
} else if (chromeLoc.urlRegex.hasValue()) {
const std::regex regex(stripCachePrevention(chromeLoc.urlRegex.value()));
auto it = parsedScripts.rbegin();
// We currently only support one physical breakpoint per location, so
// search backwards so that we find the latest matching file.
while (it != parsedScripts.rend()) {
if (std::regex_match(*it, regex)) {
hermesLoc.fileName = *it;
break;
}
it++;
}
}
}
template <typename T>
void setChromeLocation(
T &chromeLoc,
const facebook::hermes::debugger::SourceLocation &hermesLoc) {
if (hermesLoc.line != facebook::hermes::debugger::kInvalidLocation) {
chromeLoc.lineNumber = hermesLoc.line - 1;
}
if (hermesLoc.column != facebook::hermes::debugger::kInvalidLocation) {
chromeLoc.columnNumber = hermesLoc.column - 1;
}
}
/// ErrorCode magic numbers match JSC's (see InspectorBackendDispatcher.cpp)
enum class ErrorCode {
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
ServerError = -32000
};
ErrorResponse
makeErrorResponse(int id, ErrorCode code, const std::string &message);
OkResponse makeOkResponse(int id);
namespace debugger {
Location makeLocation(const facebook::hermes::debugger::SourceLocation &loc);
CallFrame makeCallFrame(
uint32_t callFrameIndex,
const facebook::hermes::debugger::CallFrameInfo &callFrameInfo,
const facebook::hermes::debugger::LexicalInfo &lexicalInfo,
facebook::hermes::inspector::chrome::RemoteObjectsTable &objTable,
jsi::Runtime &runtime,
const facebook::hermes::debugger::ProgramState &state);
std::vector<CallFrame> makeCallFrames(
const facebook::hermes::debugger::ProgramState &state,
facebook::hermes::inspector::chrome::RemoteObjectsTable &objTable,
jsi::Runtime &runtime);
} // namespace debugger
namespace runtime {
CallFrame makeCallFrame(const facebook::hermes::debugger::CallFrameInfo &info);
std::vector<CallFrame> makeCallFrames(
const facebook::hermes::debugger::StackTrace &stackTrace);
ExceptionDetails makeExceptionDetails(
const facebook::hermes::debugger::ExceptionDetails &details);
RemoteObject makeRemoteObject(
facebook::jsi::Runtime &runtime,
const facebook::jsi::Value &value,
facebook::hermes::inspector::chrome::RemoteObjectsTable &objTable,
const std::string &objectGroup,
bool byValue = false);
} // namespace runtime
} // namespace message
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include <folly/Try.h>
#include <folly/dynamic.h>
#include <folly/json.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
namespace message {
struct RequestHandler;
/// Serializable is an interface for objects that can be serialized to and from
/// JSON.
struct Serializable {
virtual ~Serializable() = default;
virtual folly::dynamic toDynamic() const = 0;
std::string toJson() const {
return folly::toJson(toDynamic());
}
};
/// Requests are sent from the debugger to the target.
struct Request : public Serializable {
static std::unique_ptr<Request> fromJsonThrowOnError(const std::string &str);
static folly::Try<std::unique_ptr<Request>> fromJson(const std::string &str);
Request() = default;
explicit Request(std::string method) : method(method) {}
// accept dispatches to the appropriate handler method in RequestHandler based
// on the type of the request.
virtual void accept(RequestHandler &handler) const = 0;
int id = 0;
std::string method;
};
/// Responses are sent from the target to the debugger in response to a Request.
struct Response : public Serializable {
Response() = default;
int id = 0;
};
/// Notifications are sent from the target to the debugger. This is used to
/// notify the debugger about events that occur in the target, e.g. stopping
/// at a breakpoint.
struct Notification : public Serializable {
Notification() = default;
explicit Notification(std::string method) : method(method) {}
std::string method;
};
} // namespace message
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,597 @@
// Copyright 2004-present Facebook. All Rights Reserved.
// @generated SignedSource<<0a1a011902fd18d4eebd2fe12fafb8b1>>
#pragma once
#include <hermes/inspector/chrome/MessageInterfaces.h>
#include <vector>
#include <folly/Optional.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
namespace message {
struct UnknownRequest;
namespace debugger {
using BreakpointId = std::string;
struct BreakpointResolvedNotification;
struct CallFrame;
using CallFrameId = std::string;
struct DisableRequest;
struct EnableRequest;
struct EvaluateOnCallFrameRequest;
struct EvaluateOnCallFrameResponse;
struct Location;
struct PauseRequest;
struct PausedNotification;
struct RemoveBreakpointRequest;
struct ResumeRequest;
struct ResumedNotification;
struct Scope;
struct ScriptParsedNotification;
struct SetBreakpointByUrlRequest;
struct SetBreakpointByUrlResponse;
struct SetBreakpointRequest;
struct SetBreakpointResponse;
struct SetPauseOnExceptionsRequest;
struct StepIntoRequest;
struct StepOutRequest;
struct StepOverRequest;
} // namespace debugger
namespace runtime {
struct CallFrame;
struct ConsoleAPICalledNotification;
struct EvaluateRequest;
struct EvaluateResponse;
struct ExceptionDetails;
struct ExecutionContextCreatedNotification;
struct ExecutionContextDescription;
using ExecutionContextId = int;
struct GetPropertiesRequest;
struct GetPropertiesResponse;
struct InternalPropertyDescriptor;
struct PropertyDescriptor;
struct RemoteObject;
using RemoteObjectId = std::string;
using ScriptId = std::string;
struct StackTrace;
using Timestamp = double;
using UnserializableValue = std::string;
} // namespace runtime
namespace heapProfiler {
struct AddHeapSnapshotChunkNotification;
struct ReportHeapSnapshotProgressNotification;
struct StartTrackingHeapObjectsRequest;
struct StopTrackingHeapObjectsRequest;
struct TakeHeapSnapshotRequest;
} // namespace heapProfiler
/// RequestHandler handles requests via the visitor pattern.
struct RequestHandler {
virtual ~RequestHandler() = default;
virtual void handle(const UnknownRequest &req) = 0;
virtual void handle(const debugger::DisableRequest &req) = 0;
virtual void handle(const debugger::EnableRequest &req) = 0;
virtual void handle(const debugger::EvaluateOnCallFrameRequest &req) = 0;
virtual void handle(const debugger::PauseRequest &req) = 0;
virtual void handle(const debugger::RemoveBreakpointRequest &req) = 0;
virtual void handle(const debugger::ResumeRequest &req) = 0;
virtual void handle(const debugger::SetBreakpointRequest &req) = 0;
virtual void handle(const debugger::SetBreakpointByUrlRequest &req) = 0;
virtual void handle(const debugger::SetPauseOnExceptionsRequest &req) = 0;
virtual void handle(const debugger::StepIntoRequest &req) = 0;
virtual void handle(const debugger::StepOutRequest &req) = 0;
virtual void handle(const debugger::StepOverRequest &req) = 0;
virtual void handle(
const heapProfiler::StartTrackingHeapObjectsRequest &req) = 0;
virtual void handle(
const heapProfiler::StopTrackingHeapObjectsRequest &req) = 0;
virtual void handle(const heapProfiler::TakeHeapSnapshotRequest &req) = 0;
virtual void handle(const runtime::EvaluateRequest &req) = 0;
virtual void handle(const runtime::GetPropertiesRequest &req) = 0;
};
/// NoopRequestHandler can be subclassed to only handle some requests.
struct NoopRequestHandler : public RequestHandler {
void handle(const UnknownRequest &req) override {}
void handle(const debugger::DisableRequest &req) override {}
void handle(const debugger::EnableRequest &req) override {}
void handle(const debugger::EvaluateOnCallFrameRequest &req) override {}
void handle(const debugger::PauseRequest &req) override {}
void handle(const debugger::RemoveBreakpointRequest &req) override {}
void handle(const debugger::ResumeRequest &req) override {}
void handle(const debugger::SetBreakpointRequest &req) override {}
void handle(const debugger::SetBreakpointByUrlRequest &req) override {}
void handle(const debugger::SetPauseOnExceptionsRequest &req) override {}
void handle(const debugger::StepIntoRequest &req) override {}
void handle(const debugger::StepOutRequest &req) override {}
void handle(const debugger::StepOverRequest &req) override {}
void handle(
const heapProfiler::StartTrackingHeapObjectsRequest &req) override {}
void handle(
const heapProfiler::StopTrackingHeapObjectsRequest &req) override {}
void handle(const heapProfiler::TakeHeapSnapshotRequest &req) override {}
void handle(const runtime::EvaluateRequest &req) override {}
void handle(const runtime::GetPropertiesRequest &req) override {}
};
/// Types
struct debugger::Location : public Serializable {
Location() = default;
explicit Location(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
runtime::ScriptId scriptId{};
int lineNumber{};
folly::Optional<int> columnNumber;
};
struct runtime::RemoteObject : public Serializable {
RemoteObject() = default;
explicit RemoteObject(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
std::string type;
folly::Optional<std::string> subtype;
folly::Optional<std::string> className;
folly::Optional<folly::dynamic> value;
folly::Optional<runtime::UnserializableValue> unserializableValue;
folly::Optional<std::string> description;
folly::Optional<runtime::RemoteObjectId> objectId;
};
struct runtime::CallFrame : public Serializable {
CallFrame() = default;
explicit CallFrame(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
std::string functionName;
runtime::ScriptId scriptId{};
std::string url;
int lineNumber{};
int columnNumber{};
};
struct runtime::StackTrace : public Serializable {
StackTrace() = default;
explicit StackTrace(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
folly::Optional<std::string> description;
std::vector<runtime::CallFrame> callFrames;
std::unique_ptr<runtime::StackTrace> parent;
};
struct runtime::ExceptionDetails : public Serializable {
ExceptionDetails() = default;
explicit ExceptionDetails(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
int exceptionId{};
std::string text;
int lineNumber{};
int columnNumber{};
folly::Optional<runtime::ScriptId> scriptId;
folly::Optional<std::string> url;
folly::Optional<runtime::StackTrace> stackTrace;
folly::Optional<runtime::RemoteObject> exception;
folly::Optional<runtime::ExecutionContextId> executionContextId;
};
struct debugger::Scope : public Serializable {
Scope() = default;
explicit Scope(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
std::string type;
runtime::RemoteObject object{};
folly::Optional<std::string> name;
folly::Optional<debugger::Location> startLocation;
folly::Optional<debugger::Location> endLocation;
};
struct debugger::CallFrame : public Serializable {
CallFrame() = default;
explicit CallFrame(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
debugger::CallFrameId callFrameId{};
std::string functionName;
folly::Optional<debugger::Location> functionLocation;
debugger::Location location{};
std::string url;
std::vector<debugger::Scope> scopeChain;
runtime::RemoteObject thisObj{};
folly::Optional<runtime::RemoteObject> returnValue;
};
struct runtime::ExecutionContextDescription : public Serializable {
ExecutionContextDescription() = default;
explicit ExecutionContextDescription(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
runtime::ExecutionContextId id{};
std::string origin;
std::string name;
folly::Optional<folly::dynamic> auxData;
};
struct runtime::PropertyDescriptor : public Serializable {
PropertyDescriptor() = default;
explicit PropertyDescriptor(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
std::string name;
folly::Optional<runtime::RemoteObject> value;
folly::Optional<bool> writable;
folly::Optional<runtime::RemoteObject> get;
folly::Optional<runtime::RemoteObject> set;
bool configurable{};
bool enumerable{};
folly::Optional<bool> wasThrown;
folly::Optional<bool> isOwn;
folly::Optional<runtime::RemoteObject> symbol;
};
struct runtime::InternalPropertyDescriptor : public Serializable {
InternalPropertyDescriptor() = default;
explicit InternalPropertyDescriptor(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
std::string name;
folly::Optional<runtime::RemoteObject> value;
};
/// Requests
struct UnknownRequest : public Request {
UnknownRequest();
explicit UnknownRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
folly::Optional<folly::dynamic> params;
};
struct debugger::DisableRequest : public Request {
DisableRequest();
explicit DisableRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
};
struct debugger::EnableRequest : public Request {
EnableRequest();
explicit EnableRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
};
struct debugger::EvaluateOnCallFrameRequest : public Request {
EvaluateOnCallFrameRequest();
explicit EvaluateOnCallFrameRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
debugger::CallFrameId callFrameId{};
std::string expression;
folly::Optional<std::string> objectGroup;
folly::Optional<bool> includeCommandLineAPI;
folly::Optional<bool> silent;
folly::Optional<bool> returnByValue;
folly::Optional<bool> throwOnSideEffect;
};
struct debugger::PauseRequest : public Request {
PauseRequest();
explicit PauseRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
};
struct debugger::RemoveBreakpointRequest : public Request {
RemoveBreakpointRequest();
explicit RemoveBreakpointRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
debugger::BreakpointId breakpointId{};
};
struct debugger::ResumeRequest : public Request {
ResumeRequest();
explicit ResumeRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
};
struct debugger::SetBreakpointRequest : public Request {
SetBreakpointRequest();
explicit SetBreakpointRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
debugger::Location location{};
folly::Optional<std::string> condition;
};
struct debugger::SetBreakpointByUrlRequest : public Request {
SetBreakpointByUrlRequest();
explicit SetBreakpointByUrlRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
int lineNumber{};
folly::Optional<std::string> url;
folly::Optional<std::string> urlRegex;
folly::Optional<std::string> scriptHash;
folly::Optional<int> columnNumber;
folly::Optional<std::string> condition;
};
struct debugger::SetPauseOnExceptionsRequest : public Request {
SetPauseOnExceptionsRequest();
explicit SetPauseOnExceptionsRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
std::string state;
};
struct debugger::StepIntoRequest : public Request {
StepIntoRequest();
explicit StepIntoRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
};
struct debugger::StepOutRequest : public Request {
StepOutRequest();
explicit StepOutRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
};
struct debugger::StepOverRequest : public Request {
StepOverRequest();
explicit StepOverRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
};
struct heapProfiler::StartTrackingHeapObjectsRequest : public Request {
StartTrackingHeapObjectsRequest();
explicit StartTrackingHeapObjectsRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
folly::Optional<bool> trackAllocations;
};
struct heapProfiler::StopTrackingHeapObjectsRequest : public Request {
StopTrackingHeapObjectsRequest();
explicit StopTrackingHeapObjectsRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
folly::Optional<bool> reportProgress;
folly::Optional<bool> treatGlobalObjectsAsRoots;
};
struct heapProfiler::TakeHeapSnapshotRequest : public Request {
TakeHeapSnapshotRequest();
explicit TakeHeapSnapshotRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
folly::Optional<bool> reportProgress;
folly::Optional<bool> treatGlobalObjectsAsRoots;
};
struct runtime::EvaluateRequest : public Request {
EvaluateRequest();
explicit EvaluateRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
std::string expression;
folly::Optional<std::string> objectGroup;
folly::Optional<bool> includeCommandLineAPI;
folly::Optional<bool> silent;
folly::Optional<runtime::ExecutionContextId> contextId;
folly::Optional<bool> returnByValue;
folly::Optional<bool> userGesture;
folly::Optional<bool> awaitPromise;
};
struct runtime::GetPropertiesRequest : public Request {
GetPropertiesRequest();
explicit GetPropertiesRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
runtime::RemoteObjectId objectId{};
folly::Optional<bool> ownProperties;
};
/// Responses
struct ErrorResponse : public Response {
ErrorResponse() = default;
explicit ErrorResponse(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
int code;
std::string message;
folly::Optional<folly::dynamic> data;
};
struct OkResponse : public Response {
OkResponse() = default;
explicit OkResponse(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
};
struct debugger::EvaluateOnCallFrameResponse : public Response {
EvaluateOnCallFrameResponse() = default;
explicit EvaluateOnCallFrameResponse(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
runtime::RemoteObject result{};
folly::Optional<runtime::ExceptionDetails> exceptionDetails;
};
struct debugger::SetBreakpointResponse : public Response {
SetBreakpointResponse() = default;
explicit SetBreakpointResponse(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
debugger::BreakpointId breakpointId{};
debugger::Location actualLocation{};
};
struct debugger::SetBreakpointByUrlResponse : public Response {
SetBreakpointByUrlResponse() = default;
explicit SetBreakpointByUrlResponse(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
debugger::BreakpointId breakpointId{};
std::vector<debugger::Location> locations;
};
struct runtime::EvaluateResponse : public Response {
EvaluateResponse() = default;
explicit EvaluateResponse(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
runtime::RemoteObject result{};
folly::Optional<runtime::ExceptionDetails> exceptionDetails;
};
struct runtime::GetPropertiesResponse : public Response {
GetPropertiesResponse() = default;
explicit GetPropertiesResponse(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
std::vector<runtime::PropertyDescriptor> result;
folly::Optional<std::vector<runtime::InternalPropertyDescriptor>>
internalProperties;
folly::Optional<runtime::ExceptionDetails> exceptionDetails;
};
/// Notifications
struct debugger::BreakpointResolvedNotification : public Notification {
BreakpointResolvedNotification();
explicit BreakpointResolvedNotification(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
debugger::BreakpointId breakpointId{};
debugger::Location location{};
};
struct debugger::PausedNotification : public Notification {
PausedNotification();
explicit PausedNotification(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
std::vector<debugger::CallFrame> callFrames;
std::string reason;
folly::Optional<folly::dynamic> data;
folly::Optional<std::vector<std::string>> hitBreakpoints;
folly::Optional<runtime::StackTrace> asyncStackTrace;
};
struct debugger::ResumedNotification : public Notification {
ResumedNotification();
explicit ResumedNotification(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
};
struct debugger::ScriptParsedNotification : public Notification {
ScriptParsedNotification();
explicit ScriptParsedNotification(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
runtime::ScriptId scriptId{};
std::string url;
int startLine{};
int startColumn{};
int endLine{};
int endColumn{};
runtime::ExecutionContextId executionContextId{};
std::string hash;
folly::Optional<folly::dynamic> executionContextAuxData;
folly::Optional<std::string> sourceMapURL;
folly::Optional<bool> hasSourceURL;
folly::Optional<bool> isModule;
folly::Optional<int> length;
};
struct heapProfiler::AddHeapSnapshotChunkNotification : public Notification {
AddHeapSnapshotChunkNotification();
explicit AddHeapSnapshotChunkNotification(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
std::string chunk;
};
struct heapProfiler::ReportHeapSnapshotProgressNotification
: public Notification {
ReportHeapSnapshotProgressNotification();
explicit ReportHeapSnapshotProgressNotification(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
int done{};
int total{};
folly::Optional<bool> finished;
};
struct runtime::ConsoleAPICalledNotification : public Notification {
ConsoleAPICalledNotification();
explicit ConsoleAPICalledNotification(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
std::string type;
std::vector<runtime::RemoteObject> args;
runtime::ExecutionContextId executionContextId{};
runtime::Timestamp timestamp{};
folly::Optional<runtime::StackTrace> stackTrace;
};
struct runtime::ExecutionContextCreatedNotification : public Notification {
ExecutionContextCreatedNotification();
explicit ExecutionContextCreatedNotification(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
runtime::ExecutionContextDescription context{};
};
} // namespace message
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,159 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <hermes/inspector/chrome/MessageInterfaces.h>
#include <memory>
#include <type_traits>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
namespace message {
using dynamic = folly::dynamic;
template <typename T>
using optional = folly::Optional<T>;
template <typename>
struct is_vector : std::false_type {};
template <typename T>
struct is_vector<std::vector<T>> : std::true_type {};
/// valueFromDynamic
template <typename T>
typename std::enable_if<std::is_base_of<Serializable, T>::value, T>::type
valueFromDynamic(const dynamic &obj) {
return T(obj);
}
template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type valueFromDynamic(
const dynamic &obj) {
return obj.asInt();
}
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
valueFromDynamic(const dynamic &obj) {
return obj.asDouble();
}
template <typename T>
typename std::enable_if<std::is_same<T, std::string>::value, T>::type
valueFromDynamic(const dynamic &obj) {
return obj.asString();
}
template <typename T>
typename std::enable_if<std::is_same<T, dynamic>::value, T>::type
valueFromDynamic(const dynamic &obj) {
return obj;
}
template <typename T>
typename std::enable_if<is_vector<T>::value, T>::type valueFromDynamic(
const dynamic &items) {
T result;
result.reserve(items.size());
for (const auto &item : items) {
result.push_back(valueFromDynamic<typename T::value_type>(item));
}
return result;
}
/// assign(lhs, obj, key) is a wrapper for:
///
/// lhs = obj[key]
///
/// It mainly exists so that we can choose the right version of valueFromDynamic
/// based on the type of lhs.
template <typename T, typename U>
void assign(T &lhs, const dynamic &obj, const U &key) {
lhs = valueFromDynamic<T>(obj.at(key));
}
template <typename T, typename U>
void assign(optional<T> &lhs, const dynamic &obj, const U &key) {
auto it = obj.find(key);
if (it != obj.items().end()) {
lhs = valueFromDynamic<T>(it->second);
} else {
lhs.clear();
}
}
template <typename T, typename U>
void assign(std::unique_ptr<T> &lhs, const dynamic &obj, const U &key) {
auto it = obj.find(key);
if (it != obj.items().end()) {
lhs = std::make_unique<T>(valueFromDynamic<T>(it->second));
} else {
lhs.reset();
}
}
/// valueToDynamic
inline dynamic valueToDynamic(const Serializable &value) {
return value.toDynamic();
}
template <typename T>
typename std::enable_if<!std::is_base_of<Serializable, T>::value, dynamic>::type
valueToDynamic(const T &item) {
return dynamic(item);
}
template <typename T>
dynamic valueToDynamic(const std::vector<T> &items) {
dynamic result = dynamic::array;
for (const auto &item : items) {
result.push_back(valueToDynamic(item));
}
return result;
}
/// put(obj, key, value) is a wrapper for:
///
/// obj[key] = valueToDynamic(value);
template <typename K, typename V>
void put(dynamic &obj, const K &key, const V &value) {
obj[key] = valueToDynamic(value);
}
template <typename K, typename V>
void put(dynamic &obj, const K &key, const optional<V> &optValue) {
if (optValue.hasValue()) {
obj[key] = valueToDynamic(optValue.value());
} else {
obj.erase(key);
}
}
template <typename K, typename V>
void put(dynamic &obj, const K &key, const std::unique_ptr<V> &ptr) {
if (ptr.get()) {
obj[key] = valueToDynamic(*ptr);
} else {
obj.erase(key);
}
}
} // namespace message
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "Registration.h"
#include "ConnectionDemux.h"
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
namespace {
ConnectionDemux &demux() {
static ConnectionDemux instance{facebook::react::getInspectorInstance()};
return instance;
}
} // namespace
void enableDebugging(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string &title) {
demux().enableDebugging(std::move(adapter), title);
}
void disableDebugging(HermesRuntime &runtime) {
demux().disableDebugging(runtime);
}
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <memory>
#include <string>
#include <hermes/hermes.h>
#include <hermes/inspector/RuntimeAdapter.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
/*
* enableDebugging adds this runtime to the list of debuggable JS targets
* (called "pages" in the higher-leavel React Native API) in this process. It
* should be called before any JS runs in the runtime.
*/
extern void enableDebugging(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string &title);
/*
* disableDebugging removes this runtime from the list of debuggable JS targets
* in this process.
*/
extern void disableDebugging(HermesRuntime &runtime);
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,146 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "RemoteObjectsTable.h"
#include <cstdlib>
#include <folly/Conv.h>
namespace {
bool isScopeId(int64_t id) {
return id < 0;
}
bool isValueId(int64_t id) {
return id > 0;
}
std::string toObjId(int64_t id) {
return folly::to<std::string>(id);
}
int64_t toId(const std::string &objId) {
return atoll(objId.c_str());
}
} // namespace
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
const char *BacktraceObjectGroup = "backtrace";
const char *ConsoleObjectGroup = "console";
RemoteObjectsTable::RemoteObjectsTable() = default;
RemoteObjectsTable::~RemoteObjectsTable() = default;
std::string RemoteObjectsTable::addScope(
std::pair<uint32_t, uint32_t> frameAndScopeIndex,
const std::string &objectGroup) {
int64_t id = scopeId_--;
scopes_[id] = frameAndScopeIndex;
if (!objectGroup.empty()) {
idToGroup_[id] = objectGroup;
groupToIds_[objectGroup].push_back(id);
}
return toObjId(id);
}
std::string RemoteObjectsTable::addValue(
::facebook::jsi::Value value,
const std::string &objectGroup) {
int64_t id = valueId_++;
values_[id] = std::move(value);
if (!objectGroup.empty()) {
idToGroup_[id] = objectGroup;
groupToIds_[objectGroup].push_back(id);
}
return toObjId(id);
}
const std::pair<uint32_t, uint32_t> *RemoteObjectsTable::getScope(
const std::string &objId) const {
int64_t id = toId(objId);
if (!isScopeId(id)) {
return nullptr;
}
auto it = scopes_.find(id);
if (it == scopes_.end()) {
return nullptr;
}
return &it->second;
}
const ::facebook::jsi::Value *RemoteObjectsTable::getValue(
const std::string &objId) const {
int64_t id = toId(objId);
if (!isValueId(id)) {
return nullptr;
}
auto it = values_.find(id);
if (it == values_.end()) {
return nullptr;
}
return &it->second;
}
std::string RemoteObjectsTable::getObjectGroup(const std::string &objId) const {
int64_t id = toId(objId);
auto it = idToGroup_.find(id);
if (it == idToGroup_.end()) {
return "";
}
return it->second;
}
void RemoteObjectsTable::releaseObject(int64_t id) {
if (isScopeId(id)) {
scopes_.erase(id);
} else if (isValueId(id)) {
values_.erase(id);
}
}
void RemoteObjectsTable::releaseObject(const std::string &objId) {
int64_t id = toId(objId);
releaseObject(id);
}
void RemoteObjectsTable::releaseObjectGroup(const std::string &objectGroup) {
auto it = groupToIds_.find(objectGroup);
if (it == groupToIds_.end()) {
return;
}
const auto &ids = it->second;
for (int64_t id : ids) {
releaseObject(id);
}
groupToIds_.erase(it);
}
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,124 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <cstdint>
#include <unordered_map>
#include <utility>
#include <vector>
#include <jsi/jsi.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
/// Well-known object group names
/**
* Objects created as a result of the Debugger.paused notification (e.g. scope
* objects) are placed in the "backtrace" object group. This object group is
* cleared when the VM resumes.
*/
extern const char *BacktraceObjectGroup;
/**
* Objects that are created as a result of a console evaluation are placed in
* the "console" object group. This object group is cleared when the client
* clears the console.
*/
extern const char *ConsoleObjectGroup;
/**
* RemoteObjectsTable manages the mapping of string object ids to scope metadata
* or actual JSI objects. The debugger vends these ids to the client so that the
* client can perform operations on the ids (e.g. enumerate properties on the
* object backed by the id). See Runtime.RemoteObjectId in the CDT docs for
* more details.
*
* Note that object handles are not ref-counted. Suppose an object foo is mapped
* to object id "objId" and is also in object group "objGroup". Then *either* of
* `releaseObject("objId")` or `releaseObjectGroup("objGroup")` will remove foo
* from the table. This matches the behavior of object groups in CDT.
*/
class RemoteObjectsTable {
public:
RemoteObjectsTable();
~RemoteObjectsTable();
RemoteObjectsTable(const RemoteObjectsTable &) = delete;
RemoteObjectsTable &operator=(const RemoteObjectsTable &) = delete;
/**
* addScope adds the provided (frameIndex, scopeIndex) mapping to the table.
* If objectGroup is non-empty, then the scope object is also added to that
* object group for releasing via releaseObjectGroup. Returns an object id.
*/
std::string addScope(
std::pair<uint32_t, uint32_t> frameAndScopeIndex,
const std::string &objectGroup);
/**
* addValue adds the JSI value to the table. If objectGroup is non-empty, then
* the scope object is also added to that object group for releasing via
* releaseObjectGroup. Returns an object id.
*/
std::string addValue(
::facebook::jsi::Value value,
const std::string &objectGroup);
/**
* Retrieves the (frameIndex, scopeIndex) associated with this object id, or
* nullptr if no mapping exists. The pointer stays valid as long as you only
* call const methods on this class.
*/
const std::pair<uint32_t, uint32_t> *getScope(const std::string &objId) const;
/**
* Retrieves the JSI value associated with this object id, or nullptr if no
* mapping exists. The pointer stays valid as long as you only call const
* methods on this class.
*/
const ::facebook::jsi::Value *getValue(const std::string &objId) const;
/**
* Retrieves the object group that this object id is in, or empty string if it
* isn't in an object group. The returned pointer is only guaranteed to be
* valid until the next call to this class.
*/
std::string getObjectGroup(const std::string &objId) const;
/**
* Removes the scope or JSI value backed by the provided object ID from the
* table.
*/
void releaseObject(const std::string &objId);
/**
* Removes all objects that are part of the provided object group from the
* table.
*/
void releaseObjectGroup(const std::string &objectGroup);
private:
void releaseObject(int64_t id);
int64_t scopeId_ = -1;
int64_t valueId_ = 1;
std::unordered_map<int64_t, std::pair<uint32_t, uint32_t>> scopes_;
std::unordered_map<int64_t, ::facebook::jsi::Value> values_;
std::unordered_map<int64_t, std::string> idToGroup_;
std::unordered_map<std::string, std::vector<int64_t>> groupToIds_;
};
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,271 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <memory>
#include <thread>
#include <utility>
#include <getopt.h>
#include <folly/json.h>
#include <hermes/hermes.h>
#include <hermes/inspector/RuntimeAdapter.h>
#include <hermes/inspector/chrome/Connection.h>
using ::facebook::react::IRemoteConnection;
namespace fbhermes = ::facebook::hermes;
static const char *usageMessage = R"(hermes-chrome-debug-server script.js
Uses Hermes to evaluate script.js within a debugging session. The process will
wait for Chrome DevTools Protocol requests on stdin and writes responses and
events to stdout.
This can be used with a WebSocket bridge to host a Chrome DevTools Protocol
debug server. For instance, running this:
websocketd --port=9999 hermes-chrome-debug-server script.js
will run a WebSocket server on port 9999 that debugs script.js in Hermes. Chrome
can connect to this debugging session using a URL like this:
chrome-devtools://devtools/bundled/inspector.html?experiments=false&v8only=true&ws=127.0.0.1:9999
Options:
-l, --log: path to a file with pretty-printed protocol logs
-h, --help: this message
)";
static void usage() {
fputs(usageMessage, stderr);
exit(1);
}
/// Truncate UTF8 string \p s to be at most \p len bytes long. If a multi-byte
/// sequence crosses the limit (len), it will be wholly omitted from the
/// output. If the output is truncated, it will be suffixed with "...".
///
/// \pre len must be strictly greater than the length of the truncation suffix
/// (three characters), so there is something left after truncation.
static void truncate(std::string &s, size_t len) {
static constexpr char suffix[] = "...";
static const size_t suflen = strlen(suffix);
assert(len > suflen);
if (s.size() <= len) {
return;
}
// Iterate back from the edge to pop off continuation bytes.
ssize_t last = len - suflen;
while (last > 0 && (s[last] & 0xC0) == 0x80)
--last;
// Copy in the suffix.
strncpy(&s[last], suffix, suflen);
// Trim the excess.
s.resize(last + suflen);
}
/// Traverse the structure of \p d, truncating all the strings found,
/// (excluding object keys) to at most \p len bytes long using the definition
/// of truncate above.
static void truncateStringsIn(folly::dynamic &d, size_t len) {
switch (d.type()) {
case folly::dynamic::STRING:
truncate(d.getString(), len);
break;
case folly::dynamic::ARRAY:
for (auto &child : d) {
truncateStringsIn(child, len);
}
break;
case folly::dynamic::OBJECT:
for (auto &kvp : d.items()) {
truncateStringsIn(kvp.second, len);
}
break;
default:
/* nop */
break;
}
}
/// Pretty print the JSON blob contained within \p str, by introducing
/// parsing it and pretty printing it. Large string values (larger than 512
/// characters) are truncated.
///
/// \pre str contains a valid JSON blob.
static std::string prettify(const std::string &str) {
constexpr size_t MAX_LINE_LEN = 512;
try {
folly::dynamic obj = folly::parseJson(str);
truncateStringsIn(obj, MAX_LINE_LEN);
return folly::toPrettyJson(obj);
} catch (...) {
// pass
}
if (str.size() > MAX_LINE_LEN) {
std::string cpy = str;
truncate(cpy, MAX_LINE_LEN);
return cpy;
}
return str;
}
static FILE *logFile = stderr;
static void setLogFilePath(const char *path) {
logFile = fopen(path, "w");
if (logFile == nullptr) {
perror("fopen couldn't open log file");
exit(1);
}
}
static void log(const std::string &str, bool isReq) {
fprintf(logFile, "%s %s\n\n", isReq ? "=>" : "<=", prettify(str).c_str());
}
static void logRequest(const std::string &str) {
log(str, true);
}
static void sendResponse(const std::string &str) {
log(str, false);
printf("%s\n", str.c_str());
}
static std::string readScriptSource(const char *path) {
std::ifstream stream(path);
return std::string{std::istreambuf_iterator<char>(stream),
std::istreambuf_iterator<char>()};
}
static std::string getUrl(const char *path) {
char absPath[PATH_MAX] = {};
realpath(path, absPath);
return std::string("file://") + absPath;
}
static bool handleScriptSourceRequest(
const std::string &reqStr,
const std::string &scriptSource) {
auto req = folly::parseJson(reqStr);
if (req.at("method") == "Debugger.getScriptSource") {
folly::dynamic result = folly::dynamic::object;
result["scriptSource"] = scriptSource;
folly::dynamic resp = folly::dynamic::object;
resp["id"] = req.at("id");
resp["result"] = std::move(result);
sendResponse(folly::toJson(resp));
return true;
}
return false;
}
class RemoteConnection : public IRemoteConnection {
public:
void onMessage(std::string message) override {
sendResponse(message);
}
void onDisconnect() override {}
};
static void runDebuggerLoop(
fbhermes::inspector::chrome::Connection &conn,
std::string scriptSource) {
conn.connect(std::make_unique<RemoteConnection>());
std::string line;
while (std::getline(std::cin, line)) {
logRequest(line);
if (!handleScriptSourceRequest(line, scriptSource)) {
conn.sendMessage(line);
}
}
}
static void runScript(const std::string &scriptSource, const std::string &url) {
std::shared_ptr<fbhermes::HermesRuntime> runtime(
fbhermes::makeHermesRuntime());
auto adapter = std::make_unique<fbhermes::inspector::SharedRuntimeAdapter>(
runtime, runtime->getDebugger());
fbhermes::inspector::chrome::Connection conn(
std::move(adapter), "hermes-chrome-debug-server");
std::thread debuggerLoop(runDebuggerLoop, std::ref(conn), scriptSource);
fbhermes::HermesRuntime::DebugFlags flags{};
runtime->debugJavaScript(scriptSource, url, flags);
debuggerLoop.join();
}
int main(int argc, char **argv) {
const char *shortOpts = "l:h";
const option longOpts[] = {{"log", 1, nullptr, 'l'},
{"help", 0, nullptr, 'h'},
{nullptr, 0, nullptr, 0}};
while (true) {
int opt = getopt_long(argc, argv, shortOpts, longOpts, nullptr);
if (opt == -1) {
break;
}
switch (opt) {
case 'l':
setLogFilePath(optarg);
break;
case 'h':
usage();
break;
default:
fprintf(stderr, "Unrecognized option: %c\n", opt);
usage();
break;
}
}
setbuf(logFile, nullptr);
setbuf(stdout, nullptr);
if (optind + 1 != argc) {
usage();
}
const char *path = argv[optind];
std::string scriptSource = readScriptSource(path);
std::string url = getUrl(path);
runScript(scriptSource, url);
fclose(logFile);
return 0;
}

View File

@ -0,0 +1,136 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "AsyncHermesRuntime.h"
#include <functional>
#include <stdexcept>
#include <thread>
#include <glog/logging.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
namespace detail = facebook::hermes::inspector::detail;
AsyncHermesRuntime::AsyncHermesRuntime()
: runtime_(facebook::hermes::makeHermesRuntime()),
executor_(
std::make_unique<detail::SerialExecutor>("async-hermes-runtime")) {
using namespace std::placeholders;
runtime_->global().setProperty(
*runtime_,
"shouldStop",
jsi::Function::createFromHostFunction(
*runtime_,
jsi::PropNameID::forAscii(*runtime_, "shouldStop"),
0,
std::bind(&AsyncHermesRuntime::shouldStop, this, _1, _2, _3, _4)));
runtime_->global().setProperty(
*runtime_,
"storeValue",
jsi::Function::createFromHostFunction(
*runtime_,
jsi::PropNameID::forAscii(*runtime_, "storeValue"),
0,
std::bind(&AsyncHermesRuntime::storeValue, this, _1, _2, _3, _4)));
}
AsyncHermesRuntime::~AsyncHermesRuntime() {
stop();
wait();
}
void AsyncHermesRuntime::executeScriptAsync(
const std::string &script,
const std::string &url,
HermesRuntime::DebugFlags flags) {
int scriptId = rand();
LOG(INFO) << "AsyncHermesRuntime will execute script with id: " << scriptId
<< ", contents: " << script;
executor_->add([this, script, url, flags, scriptId] {
LOG(INFO) << "AsyncHermesRuntime executing script id " << scriptId
<< " in background";
try {
runtime_->debugJavaScript(script, url, flags);
} catch (jsi::JSError &error) {
LOG(INFO) << "AsyncHermesRuntime JSError " << error.getMessage();
thrownExceptions_.push_back(error.getMessage());
}
LOG(INFO) << "AsyncHermesRuntime finished executing script id " << scriptId;
});
}
void AsyncHermesRuntime::start() {
LOG(INFO) << "AsyncHermesRuntime: set stop flag false";
stopFlag_.store(false);
}
void AsyncHermesRuntime::stop() {
LOG(INFO) << "AsyncHermesRuntime: set stop flag true";
stopFlag_.store(true);
}
folly::Future<jsi::Value> AsyncHermesRuntime::getStoredValue() {
return storedValue_.getFuture();
}
jsi::Value AsyncHermesRuntime::awaitStoredValue(
std::chrono::milliseconds timeout) {
return getStoredValue().get(timeout);
}
void AsyncHermesRuntime::wait(std::chrono::milliseconds timeout) {
LOG(INFO) << "AsyncHermesRuntime wait requested";
auto promise = std::make_shared<folly::Promise<bool>>();
auto future = promise->getFuture();
executor_->add([promise] {
LOG(INFO) << "AsyncHermesRuntime wait resolved";
promise->setValue(true);
});
std::move(future).get(timeout);
}
jsi::Value AsyncHermesRuntime::shouldStop(
jsi::Runtime &runtime,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count) {
return stopFlag_.load() ? jsi::Value(true) : jsi::Value(false);
}
jsi::Value AsyncHermesRuntime::storeValue(
jsi::Runtime &runtime,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count) {
if (count > 0) {
storedValue_.setValue(jsi::Value(runtime, args[0]));
}
return jsi::Value();
}
size_t AsyncHermesRuntime::getNumberOfExceptions() {
return thrownExceptions_.size();
}
std::string AsyncHermesRuntime::getLastThrownExceptionMessage() {
return thrownExceptions_.back();
}
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,114 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <atomic>
#include <chrono>
#include <memory>
#include <mutex>
#include <folly/futures/Future.h>
#include <hermes/hermes.h>
#include <hermes/inspector/detail/SerialExecutor.h>
#include <jsi/jsi.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
/**
* AsyncHermesRuntime is a helper class that runs JS scripts in a Hermes VM on
* a separate thread. This is useful for tests that want to test running JS
* in a multithreaded environment.
*/
class AsyncHermesRuntime {
public:
AsyncHermesRuntime();
~AsyncHermesRuntime();
std::shared_ptr<HermesRuntime> runtime() {
return runtime_;
}
/**
* stop sets the stop flag on this instance. JS scripts can get the current
* value of the stop flag by calling the global shouldStop() function.
*/
void stop();
/**
* start unsets the stop flag on this instance. JS scripts can get the current
* value of the stop flag by calling the global shouldStop() function.
*/
void start();
/**
* getStoredValue returns a future that is fulfilled with the value passed in
* to storeValue() by the JS script.
*/
folly::Future<jsi::Value> getStoredValue();
/**
* awaitStoredValue is a helper for getStoredValue that returns the value
* synchronously rather than in a future.
*/
jsi::Value awaitStoredValue(
std::chrono::milliseconds timeout = std::chrono::milliseconds(2500));
/**
* executeScriptAsync evaluates JS in the underlying Hermes runtime on a
* separate thread.
*
* This method should be called at most once during the lifetime of an
* AsyncHermesRuntime instance.
*/
void executeScriptAsync(
const std::string &str,
const std::string &url = "url",
facebook::hermes::HermesRuntime::DebugFlags flags =
facebook::hermes::HermesRuntime::DebugFlags{});
/**
* wait blocks until all previous executeScriptAsync calls finish.
*/
void wait(
std::chrono::milliseconds timeout = std::chrono::milliseconds(2500));
/**
* returns the number of thrown exceptions.
*/
size_t getNumberOfExceptions();
/**
* returns the message of the last thrown exception.
*/
std::string getLastThrownExceptionMessage();
private:
jsi::Value shouldStop(
jsi::Runtime &runtime,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count);
jsi::Value storeValue(
jsi::Runtime &runtime,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count);
std::shared_ptr<HermesRuntime> runtime_;
std::unique_ptr<folly::Executor> executor_;
std::atomic<bool> stopFlag_{};
folly::Promise<jsi::Value> storedValue_;
std::vector<std::string> thrownExceptions_;
};
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,149 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include <gtest/gtest.h>
#include <hermes/hermes.h>
#include <hermes/inspector/chrome/ConnectionDemux.h>
#include <jsinspector/InspectorInterfaces.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
using ::facebook::react::IInspector;
using ::facebook::react::InspectorPage;
using ::facebook::react::IRemoteConnection;
namespace {
std::unordered_map<int, std::string> makePageMap(
const std::vector<InspectorPage> &pages) {
std::unordered_map<int, std::string> pageMap;
for (auto &page : pages) {
pageMap[page.id] = page.title;
}
return pageMap;
}
void expectPages(
IInspector &inspector,
const std::unordered_map<int, std::string> &expected) {
auto pages = makePageMap(inspector.getPages());
EXPECT_EQ(pages, expected);
}
class TestRemoteConnection : public IRemoteConnection {
public:
class Data {
public:
void expectDisconnected() {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait_for(
lock, std::chrono::milliseconds(2500), [&] { return !connected_; });
EXPECT_FALSE(connected_);
}
void setDisconnected() {
std::lock_guard<std::mutex> lock(mutex_);
connected_ = false;
cv_.notify_one();
}
private:
std::mutex mutex_;
std::condition_variable cv_;
bool connected_{true};
};
TestRemoteConnection() : data_(std::make_shared<Data>()) {}
~TestRemoteConnection() {}
void onMessage(std::string message) override {}
void onDisconnect() override {
data_->setDisconnected();
}
std::shared_ptr<Data> getData() {
return data_;
}
private:
std::shared_ptr<Data> data_;
};
}; // namespace
TEST(ConnectionDemuxTests, TestEnableDisable) {
std::shared_ptr<HermesRuntime> runtime1(
facebook::hermes::makeHermesRuntime());
std::shared_ptr<HermesRuntime> runtime2(
facebook::hermes::makeHermesRuntime());
auto inspector = facebook::react::makeTestInspectorInstance();
ConnectionDemux demux{*inspector};
int id1 = demux.enableDebugging(
std::make_unique<SharedRuntimeAdapter>(runtime1, runtime1->getDebugger()),
"page1");
int id2 = demux.enableDebugging(
std::make_unique<SharedRuntimeAdapter>(runtime2, runtime2->getDebugger()),
"page2");
expectPages(*inspector, {{id1, "page1"}, {id2, "page2"}});
auto remoteConn1 = std::make_unique<TestRemoteConnection>();
auto remoteData1 = remoteConn1->getData();
auto localConn1 = inspector->connect(id1, std::move(remoteConn1));
EXPECT_NE(localConn1.get(), nullptr);
{
// If we connect to the same page id again without disconnecting, we should
// get null
auto remoteConn = std::make_unique<TestRemoteConnection>();
auto localConn = inspector->connect(id1, std::move(remoteConn));
EXPECT_EQ(localConn.get(), nullptr);
}
auto remoteConn2 = std::make_unique<TestRemoteConnection>();
auto remoteData2 = remoteConn2->getData();
auto localConn2 = inspector->connect(id2, std::move(remoteConn2));
EXPECT_NE(localConn2.get(), nullptr);
// Disable debugging on runtime2. This should remove its page from the list
// and call onDisconnect on its remoteConn
demux.disableDebugging(*runtime2);
expectPages(*inspector, {{id1, "page1"}});
remoteData2->expectDisconnected();
// Disconnect conn1. Its page should still be in the page list and
// onDisconnect should be called.
localConn1->disconnect();
remoteData1->expectDisconnected();
{
// Should still be able to reconnect after disconnecting
auto remoteConn = std::make_unique<TestRemoteConnection>();
auto localConn = inspector->connect(id1, std::move(remoteConn));
EXPECT_NE(localConn.get(), nullptr);
}
}
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,462 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <hermes/inspector/chrome/MessageTypes.h>
#include <iostream>
#include <folly/dynamic.h>
#include <folly/json.h>
#include <gtest/gtest.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
namespace message {
using folly::dynamic;
TEST(MessageTests, testSerializeSomeFieldsInRequest) {
debugger::SetBreakpointByUrlRequest req;
// req.id should default to 0
req.lineNumber = 2;
req.url = "http://example.com/example.js";
dynamic result = req.toDynamic();
dynamic expected = folly::parseJson(R"({
"id": 0,
"method": "Debugger.setBreakpointByUrl",
"params": {
"lineNumber": 2,
"url": "http://example.com/example.js"
}
})");
EXPECT_EQ(result, expected);
}
TEST(MessageTests, testDeserializeSomeFieldsInRequest) {
dynamic message = folly::parseJson(R"(
{
"id": 10,
"method": "Debugger.setBreakpointByUrl",
"params": {
"lineNumber": 42,
"url": "http://example.com"
}
}
)");
debugger::SetBreakpointByUrlRequest req(message);
EXPECT_EQ(req.id, 10);
EXPECT_EQ(req.method, "Debugger.setBreakpointByUrl");
EXPECT_EQ(req.lineNumber, 42);
EXPECT_FALSE(req.columnNumber.hasValue());
EXPECT_FALSE(req.condition.hasValue());
EXPECT_EQ(req.url, "http://example.com");
EXPECT_FALSE(req.urlRegex.hasValue());
}
TEST(MessageTests, testSerializeAllFieldsInRequest) {
debugger::SetBreakpointByUrlRequest req;
req.id = 1;
req.lineNumber = 2;
req.columnNumber = 3;
req.condition = "foo == 42";
req.url = "http://example.com/example.js";
req.urlRegex = "http://example.com/.*";
dynamic result = req.toDynamic();
dynamic expected = folly::parseJson(R"({
"id": 1,
"method": "Debugger.setBreakpointByUrl",
"params": {
"lineNumber": 2,
"columnNumber": 3,
"condition": "foo == 42",
"url": "http://example.com/example.js",
"urlRegex": "http://example.com/.*"
}
})");
EXPECT_EQ(result, expected);
}
TEST(MessageTests, testDeserializeAllFieldsInRequest) {
dynamic message = folly::parseJson(R"({
"id": 1,
"method": "Debugger.setBreakpointByUrl",
"params": {
"lineNumber": 2,
"columnNumber": 3,
"condition": "foo == 42",
"url": "http://example.com/example.js",
"urlRegex": "http://example.com/.*"
}
})");
debugger::SetBreakpointByUrlRequest req(message);
EXPECT_EQ(req.id, 1);
EXPECT_EQ(req.method, "Debugger.setBreakpointByUrl");
EXPECT_EQ(req.lineNumber, 2);
EXPECT_EQ(req.columnNumber, 3);
EXPECT_EQ(req.condition, "foo == 42");
EXPECT_EQ(req.url, "http://example.com/example.js");
EXPECT_EQ(req.urlRegex, "http://example.com/.*");
}
TEST(MessageTests, testSerializeResponse) {
debugger::Location location;
location.scriptId = "myScriptId";
location.lineNumber = 2;
location.columnNumber = 3;
debugger::SetBreakpointByUrlResponse resp;
resp.id = 1;
resp.breakpointId = "myBreakpointId";
resp.locations = {location};
dynamic result = resp.toDynamic();
dynamic expected = folly::parseJson(R"({
"id": 1,
"result": {
"breakpointId": "myBreakpointId",
"locations": [
{
"lineNumber": 2,
"columnNumber": 3,
"scriptId": "myScriptId"
}
]
}
})");
EXPECT_EQ(result, expected);
}
TEST(MessageTests, testDeserializeResponse) {
dynamic message = folly::parseJson(R"({
"id": 1,
"result": {
"breakpointId": "myBreakpointId",
"locations": [
{
"lineNumber": 2,
"columnNumber": 3,
"scriptId": "myScriptId"
}
]
}
})");
debugger::SetBreakpointByUrlResponse resp(message);
EXPECT_EQ(resp.id, 1);
EXPECT_EQ(resp.breakpointId, "myBreakpointId");
EXPECT_EQ(resp.locations.size(), 1);
EXPECT_EQ(resp.locations[0].lineNumber, 2);
EXPECT_EQ(resp.locations[0].columnNumber, 3);
EXPECT_EQ(resp.locations[0].scriptId, "myScriptId");
}
TEST(MessageTests, testSerializeNotification) {
debugger::Location startLocation;
startLocation.lineNumber = 1;
startLocation.scriptId = "script1";
debugger::Location endLocation;
endLocation.lineNumber = 2;
endLocation.scriptId = "script2";
debugger::Scope scope;
scope.type = "closure";
scope.object.type = "object";
scope.object.subtype = "regexp";
scope.object.className = "RegExp";
scope.object.value = dynamic::object("foo", "bar");
scope.object.unserializableValue = "nope";
scope.object.description = "myDesc";
scope.object.objectId = "id1";
scope.name = "myScope";
scope.startLocation = startLocation;
scope.endLocation = endLocation;
debugger::CallFrame frame;
frame.callFrameId = "callFrame1";
frame.functionName = "foo1";
frame.location.scriptId = "script1";
frame.location.lineNumber = 3;
frame.location.columnNumber = 4;
frame.url = "foo.js";
frame.scopeChain = std::vector<debugger::Scope>{scope};
frame.thisObj.type = "function";
debugger::PausedNotification note;
note.callFrames = std::vector<debugger::CallFrame>{frame};
note.reason = "debugCommand";
note.data = dynamic::object("foo", "bar");
note.hitBreakpoints = std::vector<std::string>{"a", "b"};
dynamic result = note.toDynamic();
dynamic expected = folly::parseJson(R"({
"method": "Debugger.paused",
"params": {
"callFrames": [
{
"callFrameId": "callFrame1",
"functionName": "foo1",
"location": {
"scriptId": "script1",
"lineNumber": 3,
"columnNumber": 4
},
"url": "foo.js",
"scopeChain": [
{
"type": "closure",
"object": {
"type": "object",
"subtype": "regexp",
"className": "RegExp",
"value": { "foo": "bar" },
"unserializableValue": "nope",
"description": "myDesc",
"objectId": "id1"
},
"name": "myScope",
"startLocation": {
"lineNumber": 1,
"scriptId": "script1"
},
"endLocation": {
"lineNumber": 2,
"scriptId": "script2"
}
}
],
"this": { "type": "function" }
}
],
"reason": "debugCommand",
"data": {
"foo": "bar"
},
"hitBreakpoints": [ "a", "b" ]
}
})");
EXPECT_EQ(result, expected);
}
TEST(MessageTests, testDeserializeNotification) {
dynamic message = folly::parseJson(R"({
"method": "Debugger.paused",
"params": {
"callFrames": [
{
"callFrameId": "callFrame1",
"functionName": "foo1",
"location": {
"scriptId": "script1",
"lineNumber": 3,
"columnNumber": 4
},
"url": "foo.js",
"scopeChain": [
{
"type": "closure",
"object": {
"type": "object",
"subtype": "regexp",
"className": "RegExp",
"value": { "foo": "bar" },
"unserializableValue": "nope",
"description": "myDesc",
"objectId": "id1"
},
"name": "myScope",
"startLocation": {
"lineNumber": 1,
"scriptId": "script1"
},
"endLocation": {
"lineNumber": 2,
"scriptId": "script2"
}
}
],
"this": { "type": "function" }
}
],
"reason": "debugCommand",
"data": {
"foo": "bar"
},
"hitBreakpoints": [ "a", "b" ]
}
})");
debugger::PausedNotification note(message);
EXPECT_EQ(note.method, "Debugger.paused");
EXPECT_EQ(note.callFrames.size(), 1);
EXPECT_EQ(note.reason, "debugCommand");
EXPECT_EQ(note.data, static_cast<dynamic>(dynamic::object("foo", "bar")));
auto expectedHitBreakpoints = std::vector<std::string>{"a", "b"};
EXPECT_EQ(note.hitBreakpoints, expectedHitBreakpoints);
debugger::CallFrame &callFrame = note.callFrames[0];
EXPECT_EQ(callFrame.callFrameId, "callFrame1");
EXPECT_EQ(callFrame.functionName, "foo1");
EXPECT_EQ(callFrame.location.scriptId, "script1");
EXPECT_EQ(callFrame.location.lineNumber, 3);
EXPECT_EQ(callFrame.location.columnNumber, 4);
EXPECT_EQ(callFrame.url, "foo.js");
EXPECT_EQ(callFrame.scopeChain.size(), 1);
EXPECT_EQ(callFrame.thisObj.type, "function");
debugger::Scope &scope = callFrame.scopeChain[0];
EXPECT_EQ(scope.type, "closure");
EXPECT_EQ(scope.object.type, "object");
EXPECT_EQ(scope.object.subtype, "regexp");
EXPECT_EQ(scope.object.className, "RegExp");
EXPECT_EQ(
scope.object.value, static_cast<dynamic>(dynamic::object("foo", "bar")));
EXPECT_EQ(scope.object.unserializableValue, "nope");
EXPECT_EQ(scope.object.description, "myDesc");
EXPECT_EQ(scope.object.objectId, "id1");
EXPECT_EQ(scope.name, "myScope");
debugger::Location &startLocation = scope.startLocation.value();
EXPECT_EQ(startLocation.lineNumber, 1);
EXPECT_FALSE(startLocation.columnNumber.hasValue());
EXPECT_EQ(startLocation.scriptId, "script1");
debugger::Location &endLocation = scope.endLocation.value();
EXPECT_EQ(endLocation.lineNumber, 2);
EXPECT_FALSE(endLocation.columnNumber.hasValue());
EXPECT_EQ(endLocation.scriptId, "script2");
}
TEST(MessageTests, TestSerializeAsyncStackTrace) {
runtime::StackTrace stack;
stack.description = "childStack";
stack.parent = std::make_unique<runtime::StackTrace>();
stack.parent->description = "parentStack";
dynamic result = stack.toDynamic();
dynamic expected = folly::parseJson(R"({
"description": "childStack",
"callFrames": [],
"parent": {
"description": "parentStack",
"callFrames": []
}
})");
EXPECT_EQ(result, expected);
}
TEST(MessageTests, TestDeserializeAsyncStackTrace) {
dynamic message = folly::parseJson(R"({
"description": "childStack",
"callFrames": [],
"parent": {
"description": "parentStack",
"callFrames": []
}
})");
runtime::StackTrace stack(message);
EXPECT_EQ(stack.description, "childStack");
EXPECT_EQ(stack.callFrames.size(), 0);
EXPECT_EQ(stack.parent->description, "parentStack");
EXPECT_EQ(stack.parent->callFrames.size(), 0);
}
TEST(MessageTests, TestRequestFromJson) {
std::unique_ptr<Request> baseReq1 = Request::fromJsonThrowOnError(R"({
"id": 1,
"method": "Debugger.enable"
})");
auto req1 = static_cast<debugger::EnableRequest *>(baseReq1.get());
EXPECT_EQ(req1->id, 1);
EXPECT_EQ(req1->method, "Debugger.enable");
std::unique_ptr<Request> baseReq2 = Request::fromJsonThrowOnError(R"({
"id": 2,
"method": "Debugger.removeBreakpoint",
"params": {
"breakpointId": "foobar"
}
})");
auto req2 = static_cast<debugger::RemoveBreakpointRequest *>(baseReq2.get());
EXPECT_EQ(req2->id, 2);
EXPECT_EQ(req2->method, "Debugger.removeBreakpoint");
EXPECT_EQ(req2->breakpointId, "foobar");
folly::Try<std::unique_ptr<Request>> invalidReq =
Request::fromJson("invalid");
EXPECT_TRUE(invalidReq.hasException());
}
TEST(MessageTests, TestBreakpointRequestFromJSON) {
std::unique_ptr<Request> baseReq = Request::fromJsonThrowOnError(R"({
"id": 1,
"method": "Debugger.setBreakpoint",
"params": {
"location": {
"scriptId": "23",
"lineNumber": 45,
"columnNumber": 67
}
}
})");
auto req = static_cast<debugger::SetBreakpointRequest *>(baseReq.get());
EXPECT_EQ(req->location.scriptId, "23");
EXPECT_EQ(req->location.lineNumber, 45);
EXPECT_EQ(req->location.columnNumber.value(), 67);
}
struct MyHandler : public NoopRequestHandler {
void handle(const debugger::EnableRequest &req) override {
enableReq = req;
}
void handle(const debugger::RemoveBreakpointRequest &req) override {
removeReq = req;
}
debugger::EnableRequest enableReq;
debugger::RemoveBreakpointRequest removeReq;
};
TEST(MessageTests, TestRequestHandler) {
MyHandler handler;
std::unique_ptr<Request> enableReq = Request::fromJsonThrowOnError(R"({
"id": 1,
"method": "Debugger.enable"
})");
enableReq->accept(handler);
EXPECT_EQ(handler.enableReq.id, 1);
EXPECT_EQ(handler.enableReq.method, "Debugger.enable");
std::unique_ptr<Request> removeReq = Request::fromJsonThrowOnError(R"({
"id": 2,
"method": "Debugger.removeBreakpoint",
"params": {
"breakpointId": "foobar"
}
})");
removeReq->accept(handler);
EXPECT_EQ(handler.removeReq.id, 2);
EXPECT_EQ(handler.removeReq.method, "Debugger.removeBreakpoint");
EXPECT_EQ(handler.removeReq.breakpointId, "foobar");
}
} // namespace message
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,111 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <hermes/inspector/chrome/RemoteObjectsTable.h>
#include <gtest/gtest.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
namespace {
struct TestContext {
TestContext() {
scope1 = table.addScope(std::make_pair(1, 1), BacktraceObjectGroup);
scope2 = table.addScope(std::make_pair(2, 1), ConsoleObjectGroup);
scope3 = table.addScope(std::make_pair(3, 1), "");
value1 = table.addValue(jsi::Value(1.5), BacktraceObjectGroup);
value2 = table.addValue(jsi::Value(2.5), BacktraceObjectGroup);
value3 = table.addValue(jsi::Value(3.5), "");
}
RemoteObjectsTable table;
std::string scope1;
std::string scope2;
std::string scope3;
std::string value1;
std::string value2;
std::string value3;
};
} // namespace
TEST(RemoteObjectsTableTest, TestGetScope) {
TestContext ctx;
EXPECT_EQ(ctx.table.getScope(ctx.scope1)->first, 1);
EXPECT_EQ(ctx.table.getScope(ctx.scope2)->first, 2);
EXPECT_EQ(ctx.table.getScope(ctx.scope3)->first, 3);
EXPECT_TRUE(ctx.table.getScope(ctx.value1) == nullptr);
EXPECT_TRUE(ctx.table.getScope(ctx.value2) == nullptr);
EXPECT_TRUE(ctx.table.getScope(ctx.value3) == nullptr);
}
TEST(RemoteObjectsTableTest, TestGetValue) {
TestContext ctx;
EXPECT_TRUE(ctx.table.getValue(ctx.scope1) == nullptr);
EXPECT_TRUE(ctx.table.getValue(ctx.scope2) == nullptr);
EXPECT_TRUE(ctx.table.getValue(ctx.scope3) == nullptr);
EXPECT_EQ(ctx.table.getValue(ctx.value1)->asNumber(), 1.5);
EXPECT_EQ(ctx.table.getValue(ctx.value2)->asNumber(), 2.5);
EXPECT_EQ(ctx.table.getValue(ctx.value3)->asNumber(), 3.5);
}
TEST(RemoteObjectsTableTest, TestGetObjectGroup) {
TestContext ctx;
EXPECT_EQ(ctx.table.getObjectGroup(ctx.scope1), BacktraceObjectGroup);
EXPECT_EQ(ctx.table.getObjectGroup(ctx.scope2), ConsoleObjectGroup);
EXPECT_EQ(ctx.table.getObjectGroup(ctx.scope3), "");
EXPECT_EQ(ctx.table.getObjectGroup(ctx.value1), BacktraceObjectGroup);
EXPECT_EQ(ctx.table.getObjectGroup(ctx.value2), BacktraceObjectGroup);
EXPECT_EQ(ctx.table.getObjectGroup(ctx.value3), "");
}
TEST(RemoteObjectsTableTest, TestReleaseObject) {
TestContext ctx;
ctx.table.releaseObject(ctx.scope1);
ctx.table.releaseObject(ctx.value3);
std::string scope4 = ctx.table.addScope(std::make_pair(4, 1), "");
std::string value4 = ctx.table.addValue(jsi::Value(4.5), "");
EXPECT_EQ(ctx.table.getScope(ctx.scope1), nullptr);
EXPECT_EQ(ctx.table.getScope(ctx.scope2)->first, 2);
EXPECT_EQ(ctx.table.getScope(ctx.scope3)->first, 3);
EXPECT_EQ(ctx.table.getScope(scope4)->first, 4);
EXPECT_EQ(ctx.table.getValue(ctx.value1)->asNumber(), 1.5);
EXPECT_EQ(ctx.table.getValue(ctx.value2)->asNumber(), 2.5);
EXPECT_EQ(ctx.table.getValue(ctx.value3), nullptr);
EXPECT_EQ(ctx.table.getValue(value4)->asNumber(), 4.5);
}
TEST(RemoteObjectsTableTest, TestReleaseObjectGroup) {
TestContext ctx;
ctx.table.releaseObjectGroup(BacktraceObjectGroup);
std::string scope4 = ctx.table.addScope(std::make_pair(4, 1), "");
std::string value4 = ctx.table.addValue(jsi::Value(4.5), "");
EXPECT_EQ(ctx.table.getScope(ctx.scope1), nullptr);
EXPECT_EQ(ctx.table.getScope(ctx.scope2)->first, 2);
EXPECT_EQ(ctx.table.getScope(ctx.scope3)->first, 3);
EXPECT_EQ(ctx.table.getScope(scope4)->first, 4);
EXPECT_EQ(ctx.table.getValue(ctx.value1), nullptr);
EXPECT_EQ(ctx.table.getValue(ctx.value2), nullptr);
EXPECT_EQ(ctx.table.getValue(ctx.value3)->asNumber(), 3.5);
EXPECT_EQ(ctx.table.getValue(value4)->asNumber(), 4.5);
}
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,132 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "SyncConnection.h"
#include <functional>
#include <stdexcept>
#include <folly/json.h>
#include <glog/logging.h>
#include <hermes/inspector/RuntimeAdapter.h>
#include <jsinspector/InspectorInterfaces.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
using namespace std::placeholders;
using ::facebook::react::IRemoteConnection;
namespace {
std::string prettify(const std::string &str) {
try {
folly::dynamic obj = folly::parseJson(str);
return folly::toPrettyJson(obj);
} catch (...) {
// pass
}
return str;
}
} // namespace
class SyncConnection::RemoteConnnection : public IRemoteConnection {
public:
RemoteConnnection(SyncConnection &conn) : conn_(conn) {}
void onMessage(std::string message) override {
conn_.onReply(message);
}
void onDisconnect() override {}
private:
SyncConnection &conn_;
};
SyncConnection::SyncConnection(std::shared_ptr<HermesRuntime> runtime)
: connection_(
std::make_unique<SharedRuntimeAdapter>(
runtime,
runtime->getDebugger()),
"testConn") {
connection_.connect(std::make_unique<RemoteConnnection>(*this));
}
void SyncConnection::send(const std::string &str) {
LOG(INFO) << "SyncConnection::send sending " << str;
connection_.sendMessage(str);
}
void SyncConnection::waitForResponse(
folly::Function<void(const std::string &)> handler,
std::chrono::milliseconds timeout) {
std::string reply;
{
std::unique_lock<std::mutex> lock(mutex_);
bool success = hasReply_.wait_for(
lock, timeout, [this]() -> bool { return !replies_.empty(); });
if (!success) {
throw std::runtime_error("timed out waiting for reply");
}
reply = std::move(replies_.front());
replies_.pop();
}
handler(reply);
}
void SyncConnection::waitForNotification(
folly::Function<void(const std::string &)> handler,
std::chrono::milliseconds timeout) {
std::string notification;
{
std::unique_lock<std::mutex> lock(mutex_);
bool success = hasNotification_.wait_for(
lock, timeout, [this]() -> bool { return !notifications_.empty(); });
if (!success) {
throw std::runtime_error("timed out waiting for notification");
}
notification = std::move(notifications_.front());
notifications_.pop();
}
handler(notification);
}
void SyncConnection::onReply(const std::string &message) {
LOG(INFO) << "SyncConnection::onReply got message: " << prettify(message);
std::lock_guard<std::mutex> lock(mutex_);
folly::dynamic obj = folly::parseJson(message);
if (obj.count("id")) {
replies_.push(message);
hasReply_.notify_one();
} else {
notifications_.push(message);
hasNotification_.notify_one();
}
}
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <queue>
#include <folly/Function.h>
#include <folly/Optional.h>
#include <hermes/hermes.h>
#include <hermes/inspector/chrome/Connection.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
/**
* SyncConnection provides a synchronous interface over Connection that is
* useful in tests.
*/
class SyncConnection {
public:
SyncConnection(std::shared_ptr<HermesRuntime> runtime);
~SyncConnection() = default;
/// sends a message to the debugger
void send(const std::string &str);
/// waits for the next response from the debugger. handler is called with the
/// response. throws on timeout.
void waitForResponse(
folly::Function<void(const std::string &)> handler,
std::chrono::milliseconds timeout = std::chrono::milliseconds(2500));
/// waits for the next notification from the debugger. handler is called with
/// the notification. throws on timeout.
void waitForNotification(
folly::Function<void(const std::string &)> handler,
std::chrono::milliseconds timeout = std::chrono::milliseconds(2500));
private:
class RemoteConnnection;
friend class RemoteConnnection;
void onReply(const std::string &message);
Connection connection_;
std::mutex mutex_;
std::condition_variable hasReply_;
std::queue<std::string> replies_;
std::condition_variable hasNotification_;
std::queue<std::string> notifications_;
};
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "CallbackOStream.h"
#include <algorithm>
#include <cassert>
namespace facebook {
namespace hermes {
namespace inspector {
namespace detail {
CallbackOStream::CallbackOStream(size_t sz, Fn cb)
: std::ostream(&sbuf_), sbuf_(sz, std::move(cb)) {}
CallbackOStream::StreamBuf::StreamBuf(size_t sz, Fn cb)
: sz_(sz), buf_(std::make_unique<char[]>(sz)), cb_(std::move(cb)) {
reset();
}
CallbackOStream::StreamBuf::~StreamBuf() {
sync();
}
std::streambuf::int_type CallbackOStream::StreamBuf::overflow(
std::streambuf::int_type ch) {
assert(pptr() <= epptr() && "overflow expects the buffer not to be overfull");
if (!pptr()) {
return traits_type::eof();
}
*pptr() = ch;
pbump(1);
if (sync() == 0) {
return traits_type::not_eof(ch);
}
// Set to nullptr on failure.
setp(nullptr, nullptr);
return traits_type::eof();
}
int CallbackOStream::StreamBuf::sync() {
try {
return pbase() == pptr() || cb_(take()) ? 0 : -1;
} catch (...) {
return -1;
}
}
void CallbackOStream::StreamBuf::reset() {
assert(sz_ > 0 && "Buffer cannot be empty.");
// std::streambuf::overflow accepts the character that caused the overflow as
// a parameter. Part of handling the overflow is adding this character to the
// stream. We choose to do this by stealing a byte at the end of the "put"
// area where the character can be written, even if the area is otherwise
// full, immediately prior to being flushed.
setp(&buf_[0], &buf_[0] + sz_ - 1);
}
std::string CallbackOStream::StreamBuf::take() {
const size_t strsz = pptr() - pbase();
reset();
return std::string(pbase(), strsz);
}
} // namespace detail
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,89 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <functional>
#include <memory>
#include <ostream>
#include <streambuf>
#include <string>
namespace facebook {
namespace hermes {
namespace inspector {
namespace detail {
/// Subclass of \c std::ostream where flushing is implemented through a
/// callback. Writes are collected in a buffer. When filled, the buffer's
/// contents are emptied out and sent to a callback.
struct CallbackOStream : public std::ostream {
/// Signature of callback called to flush buffer contents. Accepts the buffer
/// as a string. Returns a boolean indicating whether flushing succeeded.
/// Callback failure will be translated to stream failure. If the callback
/// throws an exception it will be swallowed and translated into stream
/// failure.
using Fn = std::function<bool(std::string)>;
/// Construct a new stream.
///
/// \p sz The size of the buffer -- how large it can get before it must be
/// flushed. Must be non-zero.
/// \p cb The callback function.
CallbackOStream(size_t sz, Fn cb);
/// This class is neither movable nor copyable.
CallbackOStream(CallbackOStream &&that) = delete;
CallbackOStream &operator=(CallbackOStream &&that) = delete;
CallbackOStream(const CallbackOStream &that) = delete;
CallbackOStream &operator=(const CallbackOStream &that) = delete;
private:
/// \c std::streambuf sub-class backed by a std::string buffer and
/// implementing overflow by calling a callback.
struct StreamBuf : public std::streambuf {
/// Construct a new streambuf. Parameters are the same as those of
/// \c CallbackOStream .
StreamBuf(size_t sz, Fn cb);
/// Destruction will flush any remaining buffer contents.
~StreamBuf();
/// StreamBufs are not copyable, to avoid the flush callback receiving
/// the contents of multiple streams.
StreamBuf(const StreamBuf &) = delete;
StreamBuf &operator=(const StreamBuf &) = delete;
protected:
/// std::streambuf overrides
int_type overflow(int_type ch) override;
int sync() override;
private:
/// The size of the backing buffer. Fixed for an instance of the streambuf.
size_t sz_;
/// The backing buffer that writes will go to until full.
std::unique_ptr<char[]> buf_;
/// The function called when buf_ has been filled.
Fn cb_;
/// Clears the backing buffer.
void reset();
/// Clears the backing buffer and returns it contents in a string.
std::string take();
};
StreamBuf sbuf_;
};
} // namespace detail
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "SerialExecutor.h"
namespace facebook {
namespace hermes {
namespace inspector {
namespace detail {
SerialExecutor::SerialExecutor(const std::string &name)
: finish_(false), thread_(name, [this]() { runLoop(); }) {}
SerialExecutor::~SerialExecutor() {
{
std::lock_guard<std::mutex> lock(mutex_);
finish_ = true;
wakeup_.notify_one();
}
thread_.join();
}
void SerialExecutor::add(folly::Func func) {
std::lock_guard<std::mutex> lock(mutex_);
funcs_.push(std::move(func));
wakeup_.notify_one();
}
void SerialExecutor::runLoop() {
bool shouldExit = false;
while (!shouldExit) {
folly::Func func;
{
std::unique_lock<std::mutex> lock(mutex_);
wakeup_.wait(lock, [this] { return finish_ || !funcs_.empty(); });
if (!funcs_.empty()) {
func = std::move(funcs_.front());
funcs_.pop();
}
shouldExit = funcs_.empty() && finish_;
}
if (func) {
func();
}
}
}
} // namespace detail
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,57 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <condition_variable>
#include <mutex>
#include <queue>
#include <string>
#include <hermes/inspector/detail/Thread.h>
#include <folly/Executor.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace detail {
/// SerialExecutor is a simple implementation of folly::Executor that processes
/// work items serially on a worker thread. It exists for two reasons:
///
/// 1. Currently Hermes builds for the host as well as in fbandroid and
/// fbobjc, so we need an implementation of a serial executor that doesn't
/// use the SerialAsyncExecutorFactory from fbandroid or fbobjc.
/// 2. None of folly's Executor factories are included in the stripped-down
/// version of folly in xplat.
///
/// TODO: create a factory that uses SerialAsyncExecutorFactory if we're
/// building for fbandroid or fbobjc, and otherwise creates an instance of this
/// class.
class SerialExecutor : public folly::Executor {
public:
SerialExecutor(const std::string &name);
~SerialExecutor();
void add(folly::Func) override;
private:
void runLoop();
std::mutex mutex_;
std::queue<folly::Func> funcs_;
std::condition_variable wakeup_;
bool finish_;
Thread thread_;
};
} // namespace detail
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#ifdef __ANDROID__
#include "Thread.h"
#include <fbjni/JThread.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace detail {
struct Thread::Impl {
facebook::jni::global_ref<facebook::jni::JThread> thread_;
};
Thread::Thread(std::string, std::function<void()> runnable)
: impl_(std::make_unique<Impl>(Impl{facebook::jni::make_global(
facebook::jni::JThread::create(std::move(runnable)))})) {
impl_->thread_->start();
}
Thread::~Thread() {}
void Thread::join() {
impl_->thread_->join();
}
} // namespace detail
} // namespace inspector
} // namespace hermes
} // namespace facebook
#endif

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <functional>
#include <memory>
#ifdef _WINDOWS
#include <thread>
#elif !defined(__ANDROID__)
#include <pthread.h>
#include <thread>
#endif
namespace facebook {
namespace hermes {
namespace inspector {
namespace detail {
#ifdef __ANDROID__
/// Android version of Thread that uses JThread, which is a java.lang.Thread.
/// This is desirable because real Java threads have access to the app's
/// classloader, which allows us to call in to Java from C++.
///
/// The implementation is private to the .cpp file to avoid leaking
/// the fbjni dependencies into code which creates Threads.
class Thread {
public:
Thread(std::string name, std::function<void()> runnable);
~Thread();
void detach() {
// Java threads don't need to be explicitly detached
}
void join();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
#else
class Thread {
public:
Thread(std::string name, std::function<void()> runnable)
: thread_(run, name, runnable) {}
void detach() {
thread_.detach();
}
void join() {
thread_.join();
}
private:
static void run(std::string name, std::function<void()> runnable) {
#if defined(_GNU_SOURCE)
pthread_setname_np(pthread_self(), name.c_str());
#elif defined(__APPLE__)
pthread_setname_np(name.c_str());
#endif
runnable();
}
std::thread thread_;
};
#endif
}; // namespace detail
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,104 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <hermes/inspector/detail/CallbackOStream.h>
#include <memory>
#include <ostream>
#include <type_traits>
#include <vector>
#include <gmock/gmock.h>
namespace {
using namespace ::testing;
using namespace facebook::hermes::inspector::detail;
TEST(CallbackOStreamTests, Chunking) {
std::vector<std::string> recvd;
CallbackOStream cos(/* sz */ 4, [&recvd](std::string s) {
recvd.emplace_back(std::move(s));
return true;
});
cos << "123412341234";
EXPECT_THAT(recvd, ElementsAre("1234", "1234", "1234"));
}
TEST(CallbackOStreamTests, SyncOnDestruction) {
std::vector<std::string> recvd;
{
CallbackOStream cos(/* sz */ 4, [&recvd](std::string s) {
recvd.emplace_back(std::move(s));
return true;
});
cos << "123412341234123";
ASSERT_THAT(recvd, ElementsAre("1234", "1234", "1234"));
}
EXPECT_THAT(recvd, ElementsAre("1234", "1234", "1234", "123"));
}
TEST(CallbackOStreamTests, ExplicitFlush) {
std::vector<std::string> recvd;
CallbackOStream cos(/* sz */ 4, [&recvd](std::string s) {
recvd.emplace_back(std::move(s));
return true;
});
cos << "123412341234123";
EXPECT_THAT(recvd, ElementsAre("1234", "1234", "1234"));
cos << std::flush;
EXPECT_THAT(recvd, ElementsAre("1234", "1234", "1234", "123"));
}
TEST(CallbackOStreamTests, FlushEmpty) {
size_t i = 0;
CallbackOStream cos(/* sz */ 4, [&i](std::string) { return ++i; });
cos << "12341234";
ASSERT_THAT(i, Eq(2));
// If the put area is empty, we will not flush.
cos << std::flush;
EXPECT_THAT(i, Eq(2));
}
TEST(CallbackOStreamTests, FailingCallback) {
size_t i = 0;
std::vector<std::string> recvd;
CallbackOStream cos(/* sz */ 4, [&i, &recvd](std::string s) {
recvd.emplace_back(std::move(s));
return ++i < 2;
});
cos << "123412341234";
EXPECT_THAT(recvd, ElementsAre("1234", "1234"));
EXPECT_THAT(!cos, Eq(true));
}
TEST(CallbackOStreamTests, ThrowingCallback) {
size_t i = 0;
std::vector<std::string> recvd;
CallbackOStream cos(/* sz */ 4, [&i, &recvd](std::string s) {
if (i++ >= 2) {
throw "too big";
}
recvd.emplace_back(std::move(s));
return true;
});
cos << "123412341234";
EXPECT_THAT(recvd, ElementsAre("1234", "1234"));
EXPECT_THAT(!cos, Eq(true));
}
} // namespace

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <hermes/inspector/detail/SerialExecutor.h>
#include <array>
#include <iostream>
#include <gtest/gtest.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace detail {
TEST(SerialExecutorTests, testProcessesItems) {
std::array<int, 1024> values{};
{
SerialExecutor executor("TestExecutor");
for (int i = 0; i < values.size(); i++) {
executor.add([=, &values]() { values[i] = i; });
}
}
// By this time the serial executor destructor should have exited and waited
// for all work items to complete.
for (int i = 0; i < values.size(); i++) {
EXPECT_EQ(values[i], i);
}
}
} // namespace detail
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,7 @@
#!/bin/sh
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
dot -Tpdf InspectorFSM.gv -o InspectorFSM.pdf

View File

@ -0,0 +1,39 @@
digraph InspectorFSM {
node [shape = point];
init1;
init2;
node [shape = circle];
init1 -> RWE [ label = " pauseOnFirstStmt " ]
init2 -> RD [ label = " !pauseOnFirstStmt " ]
RD -> RD [ label = " didPause" ];
RD -> PWE [ label = " debuggerStmt " ];
RWE -> PWE [ label = " didPause " ];
RWP -> P [label = " didPause " ];
RD -> R [ label = " enable " ];
PWE -> P [label = " enable " ];
RWE -> RWP [ label = " enable" ];
R -> P [ label = " !implicitPause " ];
R -> R [ label = " implicitPause "];
P -> R [ label = " receivedCommand " ];
P -> RD [ label = " disable "];
R -> RD [ label = " disable "];
{ rank = same; RD RWE }
{ rank = same; R P }
label = < <table border="0">
<!-- hack: empty row for spacing -->
<tr> <td></td><td> </td></tr>
<tr><td>Abbrev</td><td>State</td></tr>
<hr/>
<tr><td>RWE</td><td>RunningWaitEnable</td></tr>
<tr><td>RWP</td><td>RunningWaitPause</td></tr>
<tr><td>RD</td><td>RunningDetached</td></tr>
<tr><td>PWE</td><td>PausedWaitEnable</td></tr>
<tr><td>P</td><td>Paused</td></tr>
<tr><td>R</td><td>Running</td></tr>
</table> >;
labelloc = "b";
}

Binary file not shown.

View File

@ -0,0 +1,412 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <hermes/inspector/Inspector.h>
#include <atomic>
#include <chrono>
#include <functional>
#include <iostream>
#include <memory>
#include <folly/futures/Future.h>
#include <gtest/gtest.h>
#include <hermes/inspector/RuntimeAdapter.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace debugger = facebook::hermes::debugger;
using namespace std::chrono_literals;
using Unit = folly::Unit;
static auto constexpr kDefaultTimeout = 5000ms;
namespace {
int getCurrentLine(const debugger::ProgramState &state) {
return state.getStackTrace().callFrameForIndex(0).location.line;
}
debugger::SourceLocation locationForLine(int line) {
debugger::SourceLocation loc;
loc.line = line;
return loc;
}
} // namespace
/*
* LambdaInspectorObserver is useful for sequencing calls to the debugger based
* on the number of onPause() callbacks.
*/
using OnPauseFunction =
std::function<void(Inspector &, const debugger::ProgramState &, int)>;
class LambdaInspectorObserver : public InspectorObserver {
public:
LambdaInspectorObserver(OnPauseFunction func)
: onPauseFunc_(func), pauseCount_(0) {}
~LambdaInspectorObserver() = default;
void onBreakpointResolved(
Inspector &inspector,
const debugger::BreakpointInfo &info) override {}
void onContextCreated(Inspector &inspector) override {}
void onPause(Inspector &inspector, const debugger::ProgramState &state)
override {
pauseCount_++;
onPauseFunc_(inspector, state, pauseCount_);
}
void onResume(Inspector &inspector) override {}
void onScriptParsed(Inspector &inspector, const ScriptInfo &info) override {}
void onMessageAdded(Inspector &inspector, const ConsoleMessageInfo &info)
override{};
int getPauseCount() {
return pauseCount_;
}
private:
OnPauseFunction onPauseFunc_;
int pauseCount_;
};
/*
* Helpers for running JS in a separate thread.
*/
struct HermesDebugContext {
HermesDebugContext(
InspectorObserver &observer,
folly::Future<Unit> &&finished)
: runtime(makeHermesRuntime()),
inspector(
std::make_shared<SharedRuntimeAdapter>(
runtime,
runtime->getDebugger()),
observer,
false),
stopFlag(false),
finished(std::move(finished)) {
runtime->global().setProperty(
*runtime,
"shouldStop",
jsi::Function::createFromHostFunction(
*runtime,
jsi::PropNameID::forAscii(*runtime, "shouldStop"),
0,
[this](
jsi::Runtime &,
const jsi::Value &,
const jsi::Value *args,
size_t count) {
return stopFlag.load() ? jsi::Value(true) : jsi::Value(false);
}));
}
~HermesDebugContext() = default;
void setStopFlag() {
stopFlag.store(true);
}
void wait(std::chrono::milliseconds timeout = kDefaultTimeout) {
std::move(finished).get(timeout);
}
std::shared_ptr<HermesRuntime> runtime;
Inspector inspector;
std::atomic<bool> stopFlag{};
folly::Future<Unit> finished;
};
static std::shared_ptr<HermesDebugContext> runScriptAsync(
InspectorObserver &observer,
const std::string &script) {
auto promise = std::make_shared<folly::Promise<Unit>>();
auto future = promise->getFuture();
auto context =
std::make_shared<HermesDebugContext>(observer, std::move(future));
std::thread t([=]() {
HermesRuntime::DebugFlags flags{};
context->runtime->debugJavaScript(script, "url", flags);
promise->setValue();
});
t.detach();
return context;
}
/*
* Tests
*/
TEST(InspectorTests, testStepOver) {
std::string script = R"(
var a = 1 + 2;
debugger;
var b = a / 2;
var c = a + b;
var d = b - c;
var e = c * d;
var f = 10;
)";
// TODO: move this vector into lambdaInspectorObserver
std::vector<folly::Future<Unit>> futures;
OnPauseFunction onPauseFunc = [&futures](
Inspector &inspector,
const debugger::ProgramState &state,
int pauseCount) {
switch (pauseCount) {
case 1: {
EXPECT_EQ(
state.getPauseReason(), debugger::PauseReason::DebuggerStatement);
EXPECT_EQ(getCurrentLine(state), 3);
futures.emplace_back(inspector.stepOver());
break;
}
case 2: {
EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::StepFinish);
EXPECT_EQ(getCurrentLine(state), 4);
futures.emplace_back(inspector.stepOver());
break;
}
case 3: {
EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::StepFinish);
EXPECT_EQ(getCurrentLine(state), 5);
futures.emplace_back(inspector.resume());
break;
}
}
};
LambdaInspectorObserver observer(onPauseFunc);
std::shared_ptr<HermesDebugContext> context =
runScriptAsync(observer, script);
// TODO: temporarily do this to ensure i hit failure case
std::this_thread::sleep_for(1000ms);
futures.emplace_back(context->inspector.enable());
context->wait();
folly::collectAll(futures).get(kDefaultTimeout);
EXPECT_EQ(observer.getPauseCount(), 3);
}
TEST(InspectorTests, testSetBreakpoint) {
std::string script = R"(
var a = 1 + 2;
debugger;
var b = a / 2;
var c = a + b;
var d = b - c;
var e = c * d;
var f = 10;
)";
std::vector<folly::Future<Unit>> futures;
OnPauseFunction onPauseFunc = [&futures](
Inspector &inspector,
const debugger::ProgramState &state,
int pauseCount) {
switch (pauseCount) {
case 1: {
EXPECT_EQ(
state.getPauseReason(), debugger::PauseReason::DebuggerStatement);
EXPECT_EQ(getCurrentLine(state), 3);
auto stepFuture = inspector.stepOver();
futures.emplace_back(std::move(stepFuture));
break;
}
case 2: {
EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::StepFinish);
EXPECT_EQ(getCurrentLine(state), 4);
auto breakpointFuture = inspector.setBreakpoint(locationForLine(6));
futures.emplace_back(std::move(breakpointFuture)
.thenValue([](debugger::BreakpointInfo info) {
EXPECT_EQ(info.resolvedLocation.line, 6);
}));
auto resumeFuture = inspector.resume();
futures.emplace_back(std::move(resumeFuture));
break;
}
case 3: {
EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::Breakpoint);
EXPECT_EQ(getCurrentLine(state), 6);
auto resumeFuture = inspector.resume();
futures.emplace_back(std::move(resumeFuture));
break;
}
}
};
LambdaInspectorObserver observer(onPauseFunc);
std::shared_ptr<HermesDebugContext> context =
runScriptAsync(observer, script);
auto enablePromise = context->inspector.enable();
futures.emplace_back(std::move(enablePromise));
context->wait();
folly::collectAll(futures).get(kDefaultTimeout);
EXPECT_EQ(observer.getPauseCount(), 3);
}
TEST(InspectorTests, testAsyncSetBreakpoint) {
std::string script = R"(
while (!shouldStop()) {
var a = 1;
var b = 2;
var c = a + b;
var d = 10;
}
)";
std::vector<folly::Future<Unit>> futures;
folly::Func stopFunc;
OnPauseFunction onPauseFunc = [&futures, &stopFunc](
Inspector &inspector,
const debugger::ProgramState &state,
int pauseCount) {
switch (pauseCount) {
case 1: {
EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::Breakpoint);
EXPECT_EQ(getCurrentLine(state), 4);
auto stepFuture = inspector.stepOver();
futures.emplace_back(std::move(stepFuture));
break;
}
case 2: {
EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::StepFinish);
EXPECT_EQ(getCurrentLine(state), 5);
stopFunc();
auto resumeFuture = inspector.resume();
futures.emplace_back(std::move(resumeFuture));
break;
}
}
};
LambdaInspectorObserver observer(onPauseFunc);
std::shared_ptr<HermesDebugContext> context =
runScriptAsync(observer, script);
stopFunc = [context]() { context->setStopFlag(); };
context->inspector.enable();
auto breakpointPromise = context->inspector.setBreakpoint(locationForLine(4))
.thenValue([](debugger::BreakpointInfo info) {
EXPECT_EQ(info.resolvedLocation.line, 4);
});
context->wait();
futures.emplace_back(std::move(breakpointPromise));
folly::collectAll(futures).get(kDefaultTimeout);
EXPECT_EQ(observer.getPauseCount(), 2);
}
TEST(InspectorTests, testDisable) {
std::string script = R"(
var a = 1 + 2;
debugger;
var b = a / 2;
var c = a + b;
var d = b - c;
var e = c * d;
var f = 10;
)";
std::vector<folly::Future<Unit>> futures;
OnPauseFunction onPauseFunc = [&futures](
Inspector &inspector,
const debugger::ProgramState &state,
int pauseCount) {
switch (pauseCount) {
case 1: {
EXPECT_EQ(
state.getPauseReason(), debugger::PauseReason::DebuggerStatement);
EXPECT_EQ(getCurrentLine(state), 3);
auto stepFuture = inspector.stepOver();
futures.emplace_back(std::move(stepFuture));
break;
}
case 2: {
EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::StepFinish);
EXPECT_EQ(getCurrentLine(state), 4);
futures.emplace_back(inspector.setBreakpoint(locationForLine(6))
.thenValue([](debugger::BreakpointInfo info) {
EXPECT_EQ(info.resolvedLocation.line, 6);
}));
futures.emplace_back(inspector.setBreakpoint(locationForLine(7))
.thenValue([](debugger::BreakpointInfo info) {
EXPECT_EQ(info.resolvedLocation.line, 7);
}));
auto detachFuture = inspector.disable();
futures.emplace_back(std::move(detachFuture));
break;
}
}
};
LambdaInspectorObserver observer(onPauseFunc);
std::shared_ptr<HermesDebugContext> context =
runScriptAsync(observer, script);
auto enablePromise = context->inspector.enable();
futures.emplace_back(std::move(enablePromise));
context->wait();
folly::collectAll(futures).get(kDefaultTimeout);
EXPECT_EQ(observer.getPauseCount(), 2);
}
} // namespace inspector
} // namespace hermes
} // namespace facebook

View File

@ -0,0 +1,10 @@
#!/bin/sh
DIR="$(dirname "${BASH_SOURCE[0]}")"
cd "$DIR"
FBSOURCE="$(hg root)"
CLANG_FORMAT="$FBSOURCE/tools/third-party/clang-format/clang-format"
SRC="$FBSOURCE/xplat/js/react-native-github/ReactCommon/hermes/inspector"
find "$SRC" '(' -name '*.h' -or -name '*.cpp' ')' -exec "$CLANG_FORMAT" -i -style=file '{}' ';'

View File

@ -0,0 +1,25 @@
Debugger.breakpointResolved
Debugger.disable
Debugger.enable
Debugger.evaluateOnCallFrame
Debugger.pause
Debugger.paused
Debugger.removeBreakpoint
Debugger.resume
Debugger.resumed
Debugger.scriptParsed
Debugger.setBreakpoint
Debugger.setBreakpointByUrl
Debugger.setPauseOnExceptions
Debugger.stepInto
Debugger.stepOut
Debugger.stepOver
HeapProfiler.addHeapSnapshotChunk
HeapProfiler.reportHeapSnapshotProgress
HeapProfiler.takeHeapSnapshot
HeapProfiler.startTrackingHeapObjects
HeapProfiler.stopTrackingHeapObjects
Runtime.consoleAPICalled
Runtime.evaluate
Runtime.executionContextCreated
Runtime.getProperties

View File

@ -0,0 +1,9 @@
{
"presets": [ "@babel/preset-flow", ["@babel/preset-env", {
"targets": {
"node": "current"
}
}]
],
"plugins": ["idx"]
}

View File

@ -0,0 +1,3 @@
.DS_Store
bin/
node_modules/

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
import { Command } from '../src/Command.js';
test('parses simple command', () => {
let obj = {
'name': 'setBreakpointsActive',
'parameters': [
{ 'name': 'active', 'type': 'boolean', 'description': 'New value for breakpoints active state.' },
],
'description': 'Activates / deactivates all breakpoints on the page.',
};
let command = Command.create('Debugger', obj, false);
expect(command.domain).toBe('Debugger');
expect(command.name).toBe('setBreakpointsActive');
expect(command.description).toBe('Activates / deactivates all breakpoints on the page.');
expect(command.parameters.map(p => p.name)).toEqual(['active']);
expect(command.returns.length).toBe(0);
expect(command.getDebuggerName()).toBe('Debugger.setBreakpointsActive');
expect(command.getCppNamespace()).toBe('debugger');
expect(command.getRequestCppType()).toBe('SetBreakpointsActiveRequest');
expect(command.getResponseCppType()).toBeUndefined();
expect(command.getForwardDecls()).toEqual(['struct SetBreakpointsActiveRequest;']);
});
test('parses command with return', () => {
let obj = {
'name': 'setBreakpoint',
'parameters': [
{ 'name': 'location', '$ref': 'Location', 'description': 'Location to set breakpoint in.' },
{ 'name': 'condition', 'type': 'string', 'optional': true, 'description': 'Expression to use as a breakpoint condition. When specified, debugger will only stop on the breakpoint if this expression evaluates to true.' },
],
'returns': [
{ 'name': 'breakpointId', '$ref': 'BreakpointId', 'description': 'Id of the created breakpoint for further reference.' },
{ 'name': 'actualLocation', '$ref': 'Location', 'description': 'Location this breakpoint resolved into.' },
],
'description': 'Sets JavaScript breakpoint at a given location.',
};
let command = Command.create('Debugger', obj, false);
expect(command.domain).toBe('Debugger');
expect(command.name).toBe('setBreakpoint');
expect(command.description).toBe('Sets JavaScript breakpoint at a given location.');
expect(command.parameters.map(p => p.name)).toEqual(['location', 'condition']);
expect(command.returns.map(p => p.name)).toEqual(['breakpointId', 'actualLocation']);
expect(command.getDebuggerName()).toBe('Debugger.setBreakpoint');
expect(command.getCppNamespace()).toBe('debugger');
expect(command.getRequestCppType()).toBe('SetBreakpointRequest');
expect(command.getResponseCppType()).toBe('SetBreakpointResponse');
expect(command.getForwardDecls()).toEqual([
'struct SetBreakpointRequest;',
'struct SetBreakpointResponse;',
]);
});

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
import { Event } from '../src/Event.js';
test('parses simple event', () => {
let obj = {
'name': 'resumed',
'description': 'Fired when the virtual machine resumed execution.',
};
let event = Event.create('Debugger', obj, false);
expect(event.domain).toBe('Debugger');
expect(event.name).toBe('resumed');
expect(event.description).toBe('Fired when the virtual machine resumed execution.');
expect(event.getDebuggerName()).toBe('Debugger.resumed');
expect(event.getCppNamespace()).toBe('debugger');
expect(event.getCppType()).toBe('ResumedNotification');
expect(event.getForwardDecls()).toEqual(['struct ResumedNotification;']);
});
test('parses event with params', () => {
let obj = {
'name': 'breakpointResolved',
'parameters': [
{ 'name': 'breakpointId', '$ref': 'BreakpointId', 'description': 'Breakpoint unique identifier.' },
{ 'name': 'location', '$ref': 'Location', 'description': 'Actual breakpoint location.' },
],
'description': 'Fired when breakpoint is resolved to an actual script and location.',
};
let event = Event.create('Debugger', obj, false);
expect(event.domain).toBe('Debugger');
expect(event.name).toBe('breakpointResolved');
expect(event.description).toBe('Fired when breakpoint is resolved to an actual script and location.');
expect(event.parameters.map(p => p.name)).toEqual(['breakpointId', 'location']);
expect(event.getDebuggerName()).toBe('Debugger.breakpointResolved');
expect(event.getCppNamespace()).toBe('debugger');
expect(event.getCppType()).toBe('BreakpointResolvedNotification');
expect(event.getForwardDecls()).toEqual(['struct BreakpointResolvedNotification;']);
});

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
import { Graph } from '../src/Graph.js';
// graph looks like this before test: https://pxl.cl/9k8t
let graph = null;
beforeEach(() => {
graph = new Graph();
graph.addEdge('A1', 'B1');
graph.addEdge('A2', 'B1');
graph.addEdge('A2', 'B2');
graph.addEdge('A3', 'B2');
graph.addEdge('A3', 'C3');
graph.addEdge('B1', 'C1');
graph.addEdge('B1', 'C2');
graph.addEdge('B1', 'C3');
graph.addEdge('B2', 'C2');
});
// checks id1 occurs after id2 in arr
function expectOccursAfter(arr, id1, id2) {
let idx1 = arr.indexOf(id1);
let idx2 = arr.indexOf(id2);
expect(idx1).not.toBe(-1);
expect(idx2).not.toBe(-1);
expect(idx1).toBeGreaterThan(idx2);
}
test('detects cycle', () => {
graph.addEdge('C2', 'A1');
expect(() => graph.traverse(['A2'])).toThrow(/^Not a DAG/);
});
test('checks for presence of root', () => {
expect(() => graph.traverse(['A1', 'NX'])).toThrow(/^No node/);
});
test('traverses partial graph', () => {
let ids = graph.traverse(['B1', 'A3']);
// Check that expected nodes are there
let sortedIds = ids.slice().sort();
expect(sortedIds).toEqual(['A3', 'B1', 'B2', 'C1', 'C2', 'C3']);
// Check that the result is topologically sorted
expectOccursAfter(ids, 'A3', 'B2');
expectOccursAfter(ids, 'A3', 'C3');
expectOccursAfter(ids, 'B1', 'C1');
expectOccursAfter(ids, 'B1', 'C2');
expectOccursAfter(ids, 'B1', 'C3');
expectOccursAfter(ids, 'B2', 'C2');
});
test('traverses complete graph', () => {
let ids = graph.traverse(['A1', 'A2', 'A3']);
// Check that expected nodes are there
let sortedIds = ids.slice().sort();
expect(sortedIds).toEqual(['A1', 'A2', 'A3', 'B1', 'B2', 'C1', 'C2', 'C3']);
// Check that the result is topologically sorted
expectOccursAfter(ids, 'A1', 'B1');
expectOccursAfter(ids, 'A2', 'B1');
expectOccursAfter(ids, 'A2', 'B2');
expectOccursAfter(ids, 'A3', 'B2');
expectOccursAfter(ids, 'A3', 'C3');
expectOccursAfter(ids, 'B1', 'C1');
expectOccursAfter(ids, 'B1', 'C2');
expectOccursAfter(ids, 'B1', 'C3');
expectOccursAfter(ids, 'B2', 'C2');
});

View File

@ -0,0 +1,130 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
import { expectCodeIsEqual, FakeWritable } from '../src/TestHelpers';
import {
emitNotificationDecl,
emitRequestDecl,
emitResponseDecl,
emitTypeDecl,
} from '../src/HeaderWriter';
import { Event } from '../src/Event';
import { Command } from '../src/Command';
import { Type } from '../src/Type';
let stream = null;
beforeEach(() => {
stream = new FakeWritable();
});
test('emits type decl', () => {
let obj = {
'id': 'Location',
'type': 'object',
'properties': [
{ 'name': 'scriptId', '$ref': 'Runtime.ScriptId', 'description': 'Script identifier as reported in the <code>Debugger.scriptParsed</code>.' },
{ 'name': 'lineNumber', 'type': 'integer', 'description': 'Line number in the script (0-based).' },
{ 'name': 'columnNumber', 'type': 'integer', 'optional': true, 'description': 'Column number in the script (0-based).' },
],
'description': 'Location in the source code.',
};
let type = Type.create('Debugger', obj);
emitTypeDecl(stream, type);
expectCodeIsEqual(stream.get(), `
struct debugger::Location : public Serializable {
Location() = default;
explicit Location(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
runtime::ScriptId scriptId{};
int lineNumber{};
folly::Optional<int> columnNumber;
};
`);
});
test('emits request decl', () => {
let obj = {
'name': 'getScriptSource',
'parameters': [
{ 'name': 'scriptId', '$ref': 'Runtime.ScriptId', 'description': 'Id of the script to get source for.' },
],
'returns': [
{ 'name': 'scriptSource', 'type': 'string', 'description': 'Script source.' },
],
'description': 'Returns source for the script with given id.',
};
let command = Command.create('Debugger', obj);
emitRequestDecl(stream, command);
expectCodeIsEqual(stream.get(), `
struct debugger::GetScriptSourceRequest : public Request {
GetScriptSourceRequest();
explicit GetScriptSourceRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
runtime::ScriptId scriptId{};
};
`);
});
test('emits response decl', () => {
let obj = {
'name': 'getScriptSource',
'parameters': [
{ 'name': 'scriptId', '$ref': 'Runtime.ScriptId', 'description': 'Id of the script to get source for.' },
],
'returns': [
{ 'name': 'scriptSource', 'type': 'string', 'description': 'Script source.' },
],
'description': 'Returns source for the script with given id.',
};
let command = Command.create('Debugger', obj);
emitResponseDecl(stream, command);
expectCodeIsEqual(stream.get(), `
struct debugger::GetScriptSourceResponse : public Response {
GetScriptSourceResponse() = default;
explicit GetScriptSourceResponse(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
std::string scriptSource;
};
`);
});
test('emits notification decl', () => {
let obj = {
'name': 'messageAdded',
'parameters': [
{ 'name': 'message', '$ref': 'ConsoleMessage', 'description': 'Console message that has been added.' },
],
'description': 'Issued when new console message is added.',
};
let event = Event.create('Console', obj);
emitNotificationDecl(stream, event);
expectCodeIsEqual(stream.get(), `
struct console::MessageAddedNotification : public Notification {
MessageAddedNotification();
explicit MessageAddedNotification(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
console::ConsoleMessage message{};
};
`);
});

View File

@ -0,0 +1,173 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
import { expectCodeIsEqual, FakeWritable } from '../src/TestHelpers';
import {
emitNotificationDef,
emitRequestDef,
emitResponseDef,
emitTypeDef,
} from '../src/ImplementationWriter';
import { Event } from '../src/Event';
import { Command } from '../src/Command';
import { Type } from '../src/Type';
let stream = null;
beforeEach(() => {
stream = new FakeWritable();
});
test('emits type def', () => {
let obj = {
'id': 'Location',
'type': 'object',
'properties': [
{ 'name': 'scriptId', '$ref': 'Runtime.ScriptId', 'description': 'Script identifier as reported in the <code>Debugger.scriptParsed</code>.' },
{ 'name': 'lineNumber', 'type': 'integer', 'description': 'Line number in the script (0-based).' },
{ 'name': 'columnNumber', 'type': 'integer', 'optional': true, 'description': 'Column number in the script (0-based).' },
],
'description': 'Location in the source code.',
};
let type = Type.create('Debugger', obj);
emitTypeDef(stream, type);
expectCodeIsEqual(stream.get(), `
debugger::Location::Location(const dynamic &obj) {
assign(scriptId, obj, "scriptId");
assign(lineNumber, obj, "lineNumber");
assign(columnNumber, obj, "columnNumber");
}
dynamic debugger::Location::toDynamic() const {
dynamic obj = dynamic::object;
put(obj, "scriptId", scriptId);
put(obj, "lineNumber", lineNumber);
put(obj, "columnNumber", columnNumber);
return obj;
}
`);
});
test('emits request def', () => {
let obj = {
'name': 'getScriptSource',
'parameters': [
{ 'name': 'scriptId', '$ref': 'Runtime.ScriptId', 'description': 'Id of the script to get source for.' },
],
'returns': [
{ 'name': 'scriptSource', 'type': 'string', 'description': 'Script source.' },
],
'description': 'Returns source for the script with given id.',
};
let command = Command.create('Debugger', obj);
emitRequestDef(stream, command);
expectCodeIsEqual(stream.get(), `
debugger::GetScriptSourceRequest::GetScriptSourceRequest()
: Request("Debugger.getScriptSource") {}
debugger::GetScriptSourceRequest::GetScriptSourceRequest(const dynamic &obj)
: Request("Debugger.getScriptSource") {
assign(id, obj, "id");
assign(method, obj, "method");
dynamic params = obj.at("params");
assign(scriptId, params, "scriptId");
}
dynamic debugger::GetScriptSourceRequest::toDynamic() const {
dynamic params = dynamic::object;
put(params, "scriptId", scriptId);
dynamic obj = dynamic::object;
put(obj, "id", id);
put(obj, "method", method);
put(obj, "params", std::move(params));
return obj;
}
void debugger::GetScriptSourceRequest::accept(RequestHandler &handler) const {
handler.handle(*this);
}
`);
});
test('emits response def', () => {
let obj = {
'name': 'getScriptSource',
'parameters': [
{ 'name': 'scriptId', '$ref': 'Runtime.ScriptId', 'description': 'Id of the script to get source for.' },
],
'returns': [
{ 'name': 'scriptSource', 'type': 'string', 'description': 'Script source.' },
],
'description': 'Returns source for the script with given id.',
};
let command = Command.create('Debugger', obj);
emitResponseDef(stream, command);
expectCodeIsEqual(stream.get(), `
debugger::GetScriptSourceResponse::GetScriptSourceResponse(const dynamic &obj) {
assign(id, obj, "id");
dynamic res = obj.at("result");
assign(scriptSource, res, "scriptSource");
}
dynamic debugger::GetScriptSourceResponse::toDynamic() const {
dynamic res = dynamic::object;
put(res, "scriptSource", scriptSource);
dynamic obj = dynamic::object;
put(obj, "id", id);
put(obj, "result", std::move(res));
return obj;
}
`);
});
test('emits notification def', () => {
let obj = {
'name': 'messageAdded',
'parameters': [
{ 'name': 'message', '$ref': 'ConsoleMessage', 'description': 'Console message that has been added.' },
],
'description': 'Issued when new console message is added.',
};
let event = Event.create('Console', obj);
emitNotificationDef(stream, event);
expectCodeIsEqual(stream.get(), `
console::MessageAddedNotification::MessageAddedNotification()
: Notification("Console.messageAdded") {}
console::MessageAddedNotification::MessageAddedNotification(const dynamic &obj)
: Notification("Console.messageAdded") {
assign(method, obj, "method");
dynamic params = obj.at("params");
assign(message, params, "message");
}
dynamic console::MessageAddedNotification::toDynamic() const {
dynamic params = dynamic::object;
put(params, "message", message);
dynamic obj = dynamic::object;
put(obj, "method", method);
put(obj, "params", std::move(params));
return obj;
}
`);
});

View File

@ -0,0 +1,133 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
import { Property } from '../src/Property.js';
test('parses required primitive prop', () => {
let obj = {
'name': 'lineNumber',
'type': 'integer',
'description': 'Line number in the script (0-based).',
};
let prop = Property.create('Debugger', obj);
expect(prop.domain).toBe('Debugger');
expect(prop.name).toBe('lineNumber');
expect(prop.type).toBe('integer');
expect(prop.optional).toBeUndefined();
expect(prop.description).toBe('Line number in the script (0-based).');
expect(prop.getFullCppType()).toBe('int');
expect(prop.getCppIdentifier()).toBe('lineNumber');
expect(prop.getInitializer()).toBe('{}');
});
test('parses optional primitive prop', () => {
let obj = {
'name': 'samplingInterval',
'type': 'number',
'optional': true,
'description': 'Average sample interval in bytes.',
};
let prop = Property.create('HeapProfiler', obj);
expect(prop.domain).toBe('HeapProfiler');
expect(prop.name).toBe('samplingInterval');
expect(prop.type).toBe('number');
expect(prop.optional).toBe(true);
expect(prop.description).toBe('Average sample interval in bytes.');
expect(prop.getFullCppType()).toBe('folly::Optional<double>');
expect(prop.getCppIdentifier()).toBe('samplingInterval');
expect(prop.getInitializer()).toBe('');
});
test('parses optional ref prop', () => {
let obj = {
'name': 'exceptionDetails',
'optional': true,
'$ref': 'Runtime.ExceptionDetails',
'description': 'Exception details if any.',
};
let prop = Property.create('Debugger', obj);
expect(prop.domain).toBe('Debugger');
expect(prop.name).toBe('exceptionDetails');
expect(prop.optional).toBe(true);
expect(prop.$ref).toBe('Runtime.ExceptionDetails');
expect(prop.description).toBe('Exception details if any.');
expect(prop.getFullCppType()).toBe('folly::Optional<runtime::ExceptionDetails>');
expect(prop.getCppIdentifier()).toBe('exceptionDetails');
expect(prop.getInitializer()).toBe('');
});
test('parses recursive ref prop', () => {
let obj = {
'name': 'parent',
'$ref': 'StackTrace',
'optional': true,
'recursive': true,
'description': 'Asynchronous JavaScript stack trace...',
};
let prop = Property.create('Runtime', obj);
expect(prop.domain).toBe('Runtime');
expect(prop.name).toBe('parent');
expect(prop.optional).toBe(true);
expect(prop.recursive).toBe(true);
expect(prop.$ref).toBe('StackTrace');
expect(prop.description).toBe('Asynchronous JavaScript stack trace...');
expect(prop.getFullCppType()).toBe('std::unique_ptr<runtime::StackTrace>');
expect(prop.getCppIdentifier()).toBe('parent');
expect(prop.getInitializer()).toBe('');
});
test('parses optional array items prop', () => {
let obj = {
'name': 'hitBreakpoints',
'type': 'array',
'optional': true,
'items': { 'type': 'string' },
'description': 'Hit breakpoints IDs',
};
let prop = Property.create('Debugger', obj);
expect(prop.domain).toBe('Debugger');
expect(prop.name).toBe('hitBreakpoints');
expect(prop.type).toBe('array');
expect(prop.optional).toBe(true);
expect(prop.items).toEqual({ 'type': 'string' });
expect(prop.description).toBe('Hit breakpoints IDs');
expect(prop.getFullCppType()).toBe('folly::Optional<std::vector<std::string>>');
expect(prop.getCppIdentifier()).toBe('hitBreakpoints');
expect(prop.getInitializer()).toBe('');
});
test('parses array ref prop', () => {
let obj = {
'name': 'domains',
'type': 'array',
'items': { '$ref': 'Domain' },
'description': 'List of supported domains.',
};
let prop = Property.create('Schema', obj);
expect(prop.domain).toBe('Schema');
expect(prop.name).toBe('domains');
expect(prop.type).toBe('array');
expect(prop.items).toEqual({ $ref: 'Domain' });
expect(prop.description).toBe('List of supported domains.');
expect(prop.getFullCppType()).toBe('std::vector<schema::Domain>');
expect(prop.getCppIdentifier()).toBe('domains');
expect(prop.getInitializer()).toBe('');
});

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
import { Type } from '../src/Type.js';
test('parses primitive type', () => {
let obj = {
'id': 'Timestamp',
'type': 'number',
'description': 'Number of milliseconds since epoch.',
};
let type = Type.create('Runtime', obj, false);
expect(type.domain).toBe('Runtime');
expect(type.id).toBe('Timestamp');
expect(type.type).toBe('number');
expect(type.description).toBe('Number of milliseconds since epoch.');
expect(type.getCppNamespace()).toBe('runtime');
expect(type.getCppType()).toBe('Timestamp');
expect(type.getForwardDecls()).toEqual(['using Timestamp = double;']);
});
test('parses object type', () => {
let obj = {
'id': 'Location',
'type': 'object',
'properties': [
{ 'name': 'scriptId', '$ref': 'Runtime.ScriptId', 'description': 'Script identifier as reported in the <code>Debugger.scriptParsed</code>.' },
{ 'name': 'lineNumber', 'type': 'integer', 'description': 'Line number in the script (0-based).' },
{ 'name': 'columnNumber', 'type': 'integer', 'optional': true, 'description': 'Column number in the script (0-based).' },
],
'description': 'Location in the source code.',
};
let type = Type.create('Debugger', obj, false);
expect(type.domain).toBe('Debugger');
expect(type.id).toBe('Location');
expect(type.type).toBe('object');
expect(type.properties.map(p => p.name)).toEqual(['scriptId', 'lineNumber', 'columnNumber']);
expect(type.description).toBe('Location in the source code.');
expect(type.getCppNamespace()).toBe('debugger');
expect(type.getCppType()).toBe('Location');
expect(type.getForwardDecls()).toEqual(['struct Location;']);
});

View File

@ -0,0 +1,39 @@
{
"name": "hermes-inspector-msggen",
"version": "1.0.0",
"license": "MIT",
"bin": {
"msggen": "./bin/index.js"
},
"scripts": {
"flow": "flow",
"build": "babel src --out-dir bin --source-maps",
"watch": "babel src --out-dir bin --source-maps --watch",
"test": "jest"
},
"devDependencies": {
"@babel/cli": "^7.2.0",
"@babel/core": "^7.2.0",
"@babel/plugin-syntax-flow": "^7.2.0",
"@babel/plugin-transform-flow-strip-types": "^7.2.0",
"@babel/preset-env": "^7.2.0",
"@babel/preset-flow": "^7.2.0",
"diff": "3.5.0",
"extend": "3.0.2",
"jest": "^24.9.0",
"nwmatcher": "1.4.4",
"randomatic": "3.0.0",
"sshpk": "1.16.1",
"webpack": "^4.41.0"
},
"jest": {
"transform": {
".*": "<rootDir>/node_modules/babel-jest"
}
},
"dependencies": {
"devtools-protocol": "0.0.730699",
"idx": "^2.1.0",
"yargs": "^14.2.0"
}
}

View File

@ -0,0 +1,81 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import {Property} from './Property';
import {toCppNamespace, toCppType} from './Converters';
export class Command {
domain: string;
name: string;
description: ?string;
experimental: ?boolean;
parameters: Array<Property>;
returns: Array<Property>;
static create(
domain: string,
obj: any,
ignoreExperimental: boolean,
): ?Command {
return ignoreExperimental && obj.experimental
? null
: new Command(domain, obj, ignoreExperimental);
}
constructor(domain: string, obj: any, ignoreExperimental: boolean) {
this.domain = domain;
this.name = obj.name;
this.description = obj.description;
this.experimental = obj.experimental;
this.parameters = Property.createArray(
domain,
obj.parameters || [],
ignoreExperimental,
);
this.returns = Property.createArray(
domain,
obj.returns || [],
ignoreExperimental,
);
}
getDebuggerName(): string {
return `${this.domain}.${this.name}`;
}
getCppNamespace(): string {
return toCppNamespace(this.domain);
}
getRequestCppType(): string {
return toCppType(this.name + 'Request');
}
getResponseCppType(): ?string {
if (this.returns && this.returns.length > 0) {
return toCppType(this.name + 'Response');
}
}
getForwardDecls(): Array<string> {
const decls = [`struct ${this.getRequestCppType()};`];
const respCppType = this.getResponseCppType();
if (respCppType) {
decls.push(`struct ${respCppType};`);
}
return decls;
}
getForwardDeclSortKey(): string {
return this.getRequestCppType();
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
export function toCppNamespace(domain: string): string {
return domain.substr(0, 1).toLowerCase() + domain.substr(1);
}
export function toCppType(type: string): string {
return type.substr(0, 1).toUpperCase() + type.substr(1);
}
export type JsTypeString =
| 'any'
| 'boolean'
| 'integer'
| 'number'
| 'object'
| 'string';
const jsTypeMappings = {
any: 'folly::dynamic',
array: 'folly::dynamic',
boolean: 'bool',
integer: 'int',
number: 'double',
object: 'folly::dynamic',
string: 'std::string',
};
export function jsTypeToCppType(jsTypeStr: JsTypeString): string {
return jsTypeMappings[jsTypeStr];
}

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import {Property} from './Property';
import {toCppNamespace, toCppType} from './Converters';
export class Event {
domain: string;
name: string;
description: ?string;
experimental: ?boolean;
parameters: Array<Property>;
static create(domain: string, obj: any, ignoreExperimental: boolean): ?Event {
return ignoreExperimental && obj.experimental
? null
: new Event(domain, obj, ignoreExperimental);
}
constructor(domain: string, obj: any, ignoreExperimental: boolean) {
this.domain = domain;
this.name = obj.name;
this.description = obj.description;
this.parameters = Property.createArray(
domain,
obj.parameters || [],
ignoreExperimental,
);
}
getDebuggerName(): string {
return `${this.domain}.${this.name}`;
}
getCppNamespace(): string {
return toCppNamespace(this.domain);
}
getCppType(): string {
return toCppType(this.name + 'Notification');
}
getForwardDecls(): Array<string> {
return [`struct ${this.getCppType()};`];
}
getForwardDeclSortKey(): string {
return this.getCppType();
}
}

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
// placeholder token that will be replaced by signedsource script
export const GeneratedHeader: string =
'// Copyright 2004-present Facebook. All Rights Reserved.\n' +
'// @generated <<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@IsG>>';

View File

@ -0,0 +1,92 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
import invariant from 'assert';
type NodeId = string;
class Node {
id: NodeId;
children: Set<Node>;
state: 'none' | 'visiting' | 'visited';
constructor(id: NodeId) {
this.id = id;
this.children = new Set();
this.state = 'none';
}
}
export class Graph {
nodes: Map<NodeId, Node>;
constructor() {
this.nodes = new Map();
}
addNode(nodeId: NodeId): Node {
let node = this.nodes.get(nodeId);
if (!node) {
node = new Node(nodeId);
this.nodes.set(nodeId, node);
}
return node;
}
addEdge(srcId: NodeId, dstId: NodeId) {
const src = this.addNode(srcId);
const dst = this.addNode(dstId);
src.children.add(dst);
}
// traverse returns all nodes in the graph reachable from the given rootIds.
// the returned nodes are topologically sorted, with the deepest nodes
// returned first.
traverse(rootIds: Array<NodeId>): Array<NodeId> {
// clear marks
for (const node of this.nodes.values()) {
node.state = 'none';
}
// make a fake root node that points to all the provided rootIds
const root = new Node('root');
for (const id of rootIds) {
const node = this.nodes.get(id);
invariant(node != null, `No node ${id} in graph`);
root.children.add(node);
}
const output = [];
postorder(root, output);
// remove fake root node
output.splice(-1);
return output;
}
}
function postorder(node: Node, output: Array<NodeId>) {
if (node.state === 'visited') {
return;
}
invariant(node.state !== 'visiting', `Not a DAG: cycle involving ${node.id}`);
node.state = 'visiting';
for (const child of node.children) {
postorder(child, output);
}
node.state = 'visited';
output.push(node.id);
}

View File

@ -0,0 +1,325 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import {Writable} from 'stream';
import {GeneratedHeader} from './GeneratedHeader';
import {Property} from './Property';
import {PropsType, Type} from './Type';
import {Command} from './Command';
import {Event} from './Event';
import {toCppNamespace} from './Converters';
export class HeaderWriter {
stream: Writable;
types: Array<Type>;
commands: Array<Command>;
events: Array<Event>;
constructor(
stream: Writable,
types: Array<Type>,
commands: Array<Command>,
events: Array<Event>,
) {
this.stream = stream;
this.types = types;
this.commands = commands;
this.events = events;
}
write() {
this.writePrologue();
this.writeForwardDecls();
this.writeRequestHandlerDecls();
this.writeTypeDecls();
this.writeRequestDecls();
this.writeResponseDecls();
this.writeNotificationDecls();
this.writeEpilogue();
}
writePrologue() {
this.stream.write(`${GeneratedHeader}
#pragma once
#include <hermes/inspector/chrome/MessageInterfaces.h>
#include <vector>
#include <folly/Optional.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
namespace message {
`);
}
writeForwardDecls() {
this.stream.write('struct UnknownRequest;\n\n');
const namespaceMap: Map<string, Array<Type | Command | Event>> = new Map();
const addToMap = function(type) {
const domain = type.domain;
let types = namespaceMap.get(domain);
if (!types) {
types = [];
namespaceMap.set(domain, types);
}
types.push(type);
};
this.types.forEach(addToMap);
this.commands.forEach(addToMap);
this.events.forEach(addToMap);
for (const [domain, types] of namespaceMap) {
types.sort((a, b) => {
const nameA = a.getForwardDeclSortKey();
const nameB = b.getForwardDeclSortKey();
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0;
});
const ns = toCppNamespace(domain);
this.stream.write(`namespace ${ns} {\n`);
for (const type of types) {
for (const decl of type.getForwardDecls()) {
this.stream.write(`${decl}\n`);
}
}
this.stream.write(`} // namespace ${ns}\n\n`);
}
}
writeRequestHandlerDecls() {
this.stream.write(
'\n/// RequestHandler handles requests via the visitor pattern.\n',
);
emitRequestHandlerDecl(this.stream, this.commands);
this.stream.write(
'\n/// NoopRequestHandler can be subclassed to only handle some requests.\n',
);
emitNoopRequestHandlerDecl(this.stream, this.commands);
}
writeTypeDecls() {
this.stream.write('\n/// Types\n');
for (const type of this.types) {
if (type instanceof PropsType) {
emitTypeDecl(this.stream, type);
}
}
}
writeRequestDecls() {
this.stream.write('\n/// Requests\n');
emitUnknownRequestDecl(this.stream);
for (const command of this.commands) {
emitRequestDecl(this.stream, command);
}
}
writeResponseDecls() {
this.stream.write('\n/// Responses\n');
emitErrorResponseDecl(this.stream);
emitOkResponseDecl(this.stream);
for (const command of this.commands) {
emitResponseDecl(this.stream, command);
}
}
writeNotificationDecls() {
this.stream.write('\n/// Notifications\n');
for (const event of this.events) {
emitNotificationDecl(this.stream, event);
}
}
writeEpilogue() {
this.stream.write(`
} // namespace message
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook
`);
}
}
function emitRequestHandlerDecl(stream: Writable, commands: Array<Command>) {
stream.write(`struct RequestHandler {
virtual ~RequestHandler() = default;
virtual void handle(const UnknownRequest &req) = 0;
`);
for (const command of commands) {
const cppNs = command.getCppNamespace();
const cppType = command.getRequestCppType();
stream.write(`virtual void handle(const ${cppNs}::${cppType} &req) = 0;`);
}
stream.write('};\n');
}
function emitNoopRequestHandlerDecl(
stream: Writable,
commands: Array<Command>,
) {
stream.write(`struct NoopRequestHandler : public RequestHandler {
void handle(const UnknownRequest &req) override {}
`);
for (const command of commands) {
const cppNs = command.getCppNamespace();
const cppType = command.getRequestCppType();
stream.write(`void handle(const ${cppNs}::${cppType} &req) override {}`);
}
stream.write('};\n');
}
function emitProps(stream: Writable, props: ?Array<Property>) {
if (!props || props.length === 0) {
return;
}
stream.write('\n');
for (const prop of props) {
const fullCppType = prop.getFullCppType();
const cppId = prop.getCppIdentifier();
const init = prop.getInitializer();
stream.write(` ${fullCppType} ${cppId}${init};\n`);
}
}
export function emitTypeDecl(stream: Writable, type: PropsType) {
const cppNs = type.getCppNamespace();
const cppType = type.getCppType();
stream.write(`struct ${cppNs}::${cppType} : public Serializable {
${cppType}() = default;
explicit ${cppType}(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
`);
if (type instanceof PropsType) {
emitProps(stream, type.properties);
}
stream.write('};\n\n');
}
function emitUnknownRequestDecl(stream: Writable) {
stream.write(`struct UnknownRequest : public Request {
UnknownRequest();
explicit UnknownRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
folly::Optional<folly::dynamic> params;
};
`);
}
export function emitRequestDecl(stream: Writable, command: Command) {
const cppNs = command.getCppNamespace();
const cppType = command.getRequestCppType();
stream.write(`struct ${cppNs}::${cppType} : public Request {
${cppType}();
explicit ${cppType}(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
`);
emitProps(stream, command.parameters);
stream.write('};\n\n');
}
function emitErrorResponseDecl(stream: Writable) {
stream.write(`struct ErrorResponse : public Response {
ErrorResponse() = default;
explicit ErrorResponse(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
int code;
std::string message;
folly::Optional<folly::dynamic> data;
};
`);
}
function emitOkResponseDecl(stream: Writable) {
stream.write(`struct OkResponse : public Response {
OkResponse() = default;
explicit OkResponse(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
};
`);
}
export function emitResponseDecl(stream: Writable, command: Command) {
const cppNs = command.getCppNamespace();
const cppType = command.getResponseCppType();
if (!cppType) {
return;
}
stream.write(`struct ${cppNs}::${cppType} : public Response {
${cppType}() = default;
explicit ${cppType}(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
`);
emitProps(stream, command.returns);
stream.write('};\n\n');
}
export function emitNotificationDecl(stream: Writable, event: Event) {
const cppNs = event.getCppNamespace();
const cppType = event.getCppType();
stream.write(`struct ${cppNs}::${cppType} : public Notification {
${cppType}();
explicit ${cppType}(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
`);
emitProps(stream, event.parameters);
stream.write('};\n\n');
}

View File

@ -0,0 +1,410 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import {Writable} from 'stream';
import {GeneratedHeader} from './GeneratedHeader';
import {PropsType, Type} from './Type';
import {Command} from './Command';
import {Event} from './Event';
export class ImplementationWriter {
stream: Writable;
types: Array<Type>;
commands: Array<Command>;
events: Array<Event>;
constructor(
stream: Writable,
types: Array<Type>,
commands: Array<Command>,
events: Array<Event>,
) {
this.stream = stream;
this.types = types;
this.commands = commands;
this.events = events;
}
write() {
this.writePrologue();
this.writeRequestParser();
this.writeTypeDefs();
this.writeRequestDefs();
this.writeResponseDefs();
this.writeNotificationDefs();
this.writeEpilogue();
}
writePrologue() {
this.stream.write(`${GeneratedHeader}
#include "MessageTypes.h"
#include "MessageTypesInlines.h"
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
namespace message {
`);
}
writeRequestParser() {
emitRequestParser(this.stream, this.commands);
}
writeTypeDefs() {
this.stream.write('\n/// Types\n');
for (const type of this.types) {
if (type instanceof PropsType) {
emitTypeDef(this.stream, type);
}
}
}
writeRequestDefs() {
this.stream.write('\n/// Requests\n');
emitUnknownRequestDef(this.stream);
for (const command of this.commands) {
emitRequestDef(this.stream, command);
}
}
writeResponseDefs() {
this.stream.write('\n/// Responses\n');
emitErrorResponseDef(this.stream);
emitOkResponseDef(this.stream);
for (const command of this.commands) {
emitResponseDef(this.stream, command);
}
}
writeNotificationDefs() {
this.stream.write('\n/// Notifications\n');
for (const event of this.events) {
emitNotificationDef(this.stream, event);
}
}
writeEpilogue() {
this.stream.write(`
} // namespace message
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook
`);
}
}
function emitRequestParser(stream: Writable, commands: Array<Command>) {
stream.write(`
using RequestBuilder = std::unique_ptr<Request> (*)(const dynamic &);
namespace {
template <typename T>
std::unique_ptr<Request> makeUnique(const dynamic &obj) {
return std::make_unique<T>(obj);
}
} // namespace
std::unique_ptr<Request> Request::fromJsonThrowOnError(const std::string &str) {
static std::unordered_map<std::string, RequestBuilder> builders = {
`);
for (const command of commands) {
const cppNs = command.getCppNamespace();
const cppType = command.getRequestCppType();
const dbgName = command.getDebuggerName();
stream.write(`{"${dbgName}", makeUnique<${cppNs}::${cppType}>},\n`);
}
stream.write(`};
dynamic obj = folly::parseJson(str);
std::string method = obj.at("method").asString();
auto it = builders.find(method);
if (it == builders.end()) {
return std::make_unique<UnknownRequest>(obj);
}
auto builder = it->second;
return builder(obj);
}
folly::Try<std::unique_ptr<Request>> Request::fromJson(const std::string &str) {
return folly::makeTryWith(
[&str] { return Request::fromJsonThrowOnError(str); });
}\n\n`);
stream.write('\n');
}
export function emitTypeDef(stream: Writable, type: PropsType) {
const cppNs = type.getCppNamespace();
const cppType = type.getCppType();
const props = type.properties || [];
// From-dynamic constructor
stream.write(`${cppNs}::${cppType}::${cppType}(const dynamic &obj) {\n`);
for (const prop of props) {
const id = prop.getCppIdentifier();
const name = prop.name;
stream.write(`assign(${id}, obj, "${name}");\n`);
}
stream.write('}\n\n');
// toDynamic
stream.write(`dynamic ${cppNs}::${cppType}::toDynamic() const {
dynamic obj = dynamic::object;\n\n`);
for (const prop of props) {
const id = prop.getCppIdentifier();
const name = prop.name;
stream.write(`put(obj, "${name}", ${id});\n`);
}
stream.write('return obj;\n}\n\n');
}
function emitErrorResponseDef(stream: Writable) {
stream.write(`ErrorResponse::ErrorResponse(const dynamic &obj) {
assign(id, obj, "id");
dynamic error = obj.at("error");
assign(code, error, "code");
assign(message, error, "message");
assign(data, error, "data");
}
dynamic ErrorResponse::toDynamic() const {
dynamic error = dynamic::object;
put(error, "code", code);
put(error, "message", message);
put(error, "data", data);
dynamic obj = dynamic::object;
put(obj, "id", id);
put(obj, "error", std::move(error));
return obj;
}\n\n`);
}
function emitOkResponseDef(stream: Writable) {
stream.write(`OkResponse::OkResponse(const dynamic &obj) {
assign(id, obj, "id");
}
dynamic OkResponse::toDynamic() const {
dynamic result = dynamic::object;
dynamic obj = dynamic::object;
put(obj, "id", id);
put(obj, "result", std::move(result));
return obj;
}\n\n`);
}
function emitUnknownRequestDef(stream: Writable) {
stream.write(`UnknownRequest::UnknownRequest() {}
UnknownRequest::UnknownRequest(const dynamic &obj) {
assign(id, obj, "id");
assign(method, obj, "method");
assign(params, obj, "params");
}
dynamic UnknownRequest::toDynamic() const {
dynamic obj = dynamic::object;
put(obj, "id", id);
put(obj, "method", method);
put(obj, "params", params);
return obj;
}
void UnknownRequest::accept(RequestHandler &handler) const {
handler.handle(*this);
}\n\n`);
}
export function emitRequestDef(stream: Writable, command: Command) {
const cppNs = command.getCppNamespace();
const cppType = command.getRequestCppType();
const dbgName = command.getDebuggerName();
const props = command.parameters || [];
// Default constructor
stream.write(`${cppNs}::${cppType}::${cppType}()
: Request("${dbgName}") {}\n\n`);
// From-dynamic constructor
stream.write(`${cppNs}::${cppType}::${cppType}(const dynamic &obj)
: Request("${dbgName}") {
assign(id, obj, "id");
assign(method, obj, "method");\n\n`);
if (props.length > 0) {
stream.write('dynamic params = obj.at("params");\n');
for (const prop of props) {
const id = prop.getCppIdentifier();
const name = prop.name;
stream.write(`assign(${id}, params, "${name}");\n`);
}
}
stream.write('}\n\n');
// toDynamic
stream.write(`dynamic ${cppNs}::${cppType}::toDynamic() const {\n`);
if (props.length > 0) {
stream.write('dynamic params = dynamic::object;\n');
for (const prop of props) {
const id = prop.getCppIdentifier();
const name = prop.name;
stream.write(`put(params, "${name}", ${id});\n`);
}
}
stream.write(`
dynamic obj = dynamic::object;
put(obj, "id", id);
put(obj, "method", method);
`);
if (props.length > 0) {
stream.write('put(obj, "params", std::move(params));\n');
}
stream.write(`return obj;
}\n\n`);
// visitor
stream.write(`void ${cppNs}::${cppType}::accept(RequestHandler &handler) const {
handler.handle(*this);
}\n\n`);
}
export function emitResponseDef(stream: Writable, command: Command) {
const cppNs = command.getCppNamespace();
const cppType = command.getResponseCppType();
if (!cppType) {
return;
}
// From-dynamic constructor
stream.write(`${cppNs}::${cppType}::${cppType}(const dynamic &obj) {
assign(id, obj, "id");\n\n`);
const props = command.returns || [];
if (props.length > 0) {
stream.write('dynamic res = obj.at("result");\n');
for (const prop of props) {
const id = prop.getCppIdentifier();
const name = prop.name;
stream.write(`assign(${id}, res, "${name}");\n`);
}
}
stream.write('}\n\n');
// toDynamic
stream.write(`dynamic ${cppNs}::${cppType}::toDynamic() const {\n`);
if (props.length > 0) {
stream.write('dynamic res = dynamic::object;\n');
for (const prop of props) {
const id = prop.getCppIdentifier();
const name = prop.name;
stream.write(`put(res, "${name}", ${id});\n`);
}
}
stream.write(`
dynamic obj = dynamic::object;
put(obj, "id", id);
put(obj, "result", std::move(res));
return obj;
}\n\n`);
}
export function emitNotificationDef(stream: Writable, event: Event) {
const cppNs = event.getCppNamespace();
const cppType = event.getCppType();
const dbgName = event.getDebuggerName();
const props = event.parameters || [];
// Default constructor
stream.write(`${cppNs}::${cppType}::${cppType}()
: Notification("${dbgName}") {}\n\n`);
// From-dynamic constructor
stream.write(`${cppNs}::${cppType}::${cppType}(const dynamic &obj)
: Notification("${dbgName}") {
assign(method, obj, "method");\n\n`);
if (props.length > 0) {
stream.write('dynamic params = obj.at("params");\n');
for (const prop of props) {
const id = prop.getCppIdentifier();
const name = prop.name;
stream.write(`assign(${id}, params, "${name}");\n`);
}
}
stream.write('}\n\n');
// toDynamic
stream.write(`dynamic ${cppNs}::${cppType}::toDynamic() const {\n`);
if (props.length > 0) {
stream.write('dynamic params = dynamic::object;\n');
for (const prop of props) {
const id = prop.getCppIdentifier();
const name = prop.name;
stream.write(`put(params, "${name}", ${id});\n`);
}
}
stream.write(`
dynamic obj = dynamic::object;
put(obj, "method", method);
`);
if (props.length > 0) {
stream.write('put(obj, "params", std::move(params));\n');
}
stream.write(`return obj;
}\n\n`);
}

View File

@ -0,0 +1,220 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import {
jsTypeToCppType,
toCppNamespace,
toCppType,
type JsTypeString,
} from './Converters';
export class Property {
domain: string;
name: string;
description: ?string;
exported: ?boolean;
experimental: ?boolean;
optional: ?boolean;
static create(domain: string, obj: any): Property {
if (obj.$ref) {
return new RefProperty(domain, obj);
} else if (obj.type === 'array') {
return new ArrayProperty(domain, obj);
}
return new PrimitiveProperty(domain, obj);
}
static createArray(
domain: string,
elements: Array<any>,
ignoreExperimental: boolean,
): Array<Property> {
let props = elements.map(elem => Property.create(domain, elem));
if (ignoreExperimental) {
props = props.filter(prop => !prop.experimental);
}
return props;
}
constructor(domain: string, obj: any) {
this.domain = domain;
this.name = obj.name;
this.description = obj.description;
this.exported = obj.exported;
this.experimental = obj.experimental;
this.optional = obj.optional;
}
getRefDebuggerName(): ?string {
throw new Error('subclass must implement');
}
getFullCppType(): string {
throw new Error('subclass must implement');
}
getCppIdentifier(): string {
// need to munge identifier if it matches a C++ keyword like "this"
if (this.name === 'this') {
return 'thisObj';
}
return this.name;
}
getInitializer(): string {
throw new Error('subclass must implement');
}
}
function maybeWrapOptional(
type: string,
optional: ?boolean,
recursive: ?boolean,
) {
if (optional) {
return recursive ? `std::unique_ptr<${type}>` : `folly::Optional<${type}>`;
}
return type;
}
function toDomainAndId(
curDomain: string,
absOrRelRef: string,
): [string, string] {
let [domain, id] = ['', ''];
// absOrRelRef can be:
// 1) absolute ref with a "." referencing a type from another namespace, like
// "Runtime.ExceptionDetails"
// 2) relative ref without a "." referencing a type in current domain, like
// "Domain"
const i = absOrRelRef.indexOf('.');
if (i === -1) {
domain = curDomain;
id = absOrRelRef;
} else {
domain = absOrRelRef.substr(0, i);
id = absOrRelRef.substr(i + 1);
}
return [domain, id];
}
function toFullCppType(curDomain: string, absOrRelRef: string) {
const [domain, id] = toDomainAndId(curDomain, absOrRelRef);
return `${toCppNamespace(domain)}::${toCppType(id)}`;
}
class PrimitiveProperty extends Property {
type: JsTypeString;
constructor(domain: string, obj: any) {
super(domain, obj);
this.type = obj.type;
}
getRefDebuggerName(): ?string {
return undefined;
}
getFullCppType(): string {
return maybeWrapOptional(jsTypeToCppType(this.type), this.optional);
}
getInitializer(): string {
// folly::Optional doesn't need to be explicitly zero-init
if (this.optional) {
return '';
}
// we want to explicitly zero-init bool, int, and double
const type = this.type;
if (type === 'boolean' || type === 'integer' || type === 'number') {
return '{}';
}
// everything else (folly::dynamic and std::string) has sensible default
// constructor, no need to explicitly zero-init
return '';
}
}
class RefProperty extends Property {
$ref: string;
recursive: ?boolean;
constructor(domain: string, obj: any) {
super(domain, obj);
this.$ref = obj.$ref;
}
getRefDebuggerName(): ?string {
const [domain, id] = toDomainAndId(this.domain, this.$ref);
return `${domain}.${id}`;
}
getFullCppType(): string {
const fullCppType = toFullCppType(this.domain, this.$ref);
return maybeWrapOptional(`${fullCppType}`, this.optional, this.recursive);
}
getInitializer(): string {
// must zero-init non-optional ref props since the ref could just be an
// alias to a C++ primitive type like int which we always want to zero-init
return this.optional ? '' : '{}';
}
}
class ArrayProperty extends Property {
type: 'array';
items:
| {|type: JsTypeString, recursive: false|}
| {|$ref: string, recursive: ?boolean|};
constructor(domain: string, obj: any) {
super(domain, obj);
this.type = obj.type;
this.items = obj.items;
}
getRefDebuggerName(): ?string {
if (this.items && this.items.$ref && !this.items.recursive) {
const [domain, id] = toDomainAndId(this.domain, this.items.$ref);
return `${domain}.${id}`;
}
}
getFullCppType(): string {
let elemType: string = 'folly::dynamic';
let recursive = false;
if (this.items) {
if (this.items.type) {
elemType = jsTypeToCppType(this.items.type);
} else if (this.items.$ref) {
elemType = toFullCppType(this.domain, this.items.$ref);
recursive = this.items.recursive;
}
}
return maybeWrapOptional(
`std::vector<${elemType}>`,
this.optional,
recursive,
);
}
getInitializer(): string {
return '';
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/*global expect*/
'use strict';
// munges string so that it's nice to look at in a test diff
function strip(str) {
// Trim leading and trailing WS
str = str.replace(/^\s+/, '');
str = str.replace(/\s+$/, '');
// Collapse all repeating newlines (possibly with spaces in between) into a
// single newline
str = str.replace(/\n(\s*)/g, '\n');
// Collapse all non-newline whitespace into a single space
return str.replace(/[^\S\n]+/g, ' ');
}
export function expectCodeIsEqual(actual, expected) {
expect(strip(actual)).toBe(strip(expected));
}
export class FakeWritable {
constructor() {
this.result = '';
}
write(str) {
this.result += str;
}
get() {
return this.result;
}
}

View File

@ -0,0 +1,100 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import {Property} from './Property';
import {jsTypeToCppType, toCppNamespace, toCppType} from './Converters';
export class Type {
domain: string;
id: string;
description: ?string;
exported: ?boolean;
experimental: ?boolean;
static create(domain: string, obj: any, ignoreExperimental: boolean): ?Type {
let type = null;
if (obj.type === 'object' && obj.properties) {
type = new PropsType(domain, obj, ignoreExperimental);
} else if (obj.type) {
type = new PrimitiveType(domain, obj, ignoreExperimental);
} else {
throw new TypeError('Type requires `type` property.');
}
if (ignoreExperimental && type.experimental) {
type = null;
}
return type;
}
constructor(domain: string, obj: any) {
this.domain = domain;
this.id = obj.id;
this.description = obj.description;
this.exported = obj.exported;
this.experimental = obj.experimental;
}
getDebuggerName(): string {
return `${this.domain}.${this.id}`;
}
getCppNamespace(): string {
return toCppNamespace(this.domain);
}
getCppType(): string {
return toCppType(this.id);
}
getForwardDecls(): Array<string> {
throw new Error('subclass must implement');
}
getForwardDeclSortKey(): string {
return this.getCppType();
}
}
export class PrimitiveType extends Type {
type: 'integer' | 'number' | 'object' | 'string';
constructor(domain: string, obj: any, ignoreExperimental: boolean) {
super(domain, obj);
this.type = obj.type;
}
getForwardDecls(): Array<string> {
return [`using ${this.getCppType()} = ${jsTypeToCppType(this.type)};`];
}
}
export class PropsType extends Type {
type: 'object';
properties: Array<Property>;
constructor(domain: string, obj: any, ignoreExperimental: boolean) {
super(domain, obj);
this.type = obj.type;
this.properties = Property.createArray(
domain,
obj.properties || [],
ignoreExperimental,
);
}
getForwardDecls(): Array<string> {
return [`struct ${this.getCppType()};`];
}
}

View File

@ -0,0 +1,230 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import fs from 'fs';
import yargs from 'yargs';
import {Command} from './Command';
import {Event} from './Event';
import {Graph} from './Graph';
import {Property} from './Property';
import {PropsType, Type} from './Type';
import {HeaderWriter} from './HeaderWriter';
import {ImplementationWriter} from './ImplementationWriter';
// $FlowFixMe: this isn't a module, just a JSON file.
const proto = require('devtools-protocol/json/js_protocol.json');
type Descriptor = {|
types: Array<Type>,
commands: Array<Command>,
events: Array<Event>,
|};
function parseDomains(
domainObjs: Array<any>,
ignoreExperimental: boolean,
): Descriptor {
const desc = {
types: [],
commands: [],
events: [],
};
for (const obj of domainObjs) {
const domain = obj.domain;
for (const typeObj of obj.types || []) {
const type = Type.create(domain, typeObj, ignoreExperimental);
if (type) {
desc.types.push(type);
}
}
for (const commandObj of obj.commands || []) {
const command = Command.create(domain, commandObj, ignoreExperimental);
if (command) {
desc.commands.push(command);
}
}
for (const eventObj of obj.events || []) {
const event = Event.create(domain, eventObj, ignoreExperimental);
if (event) {
desc.events.push(event);
}
}
}
return desc;
}
function buildGraph(desc: Descriptor): Graph {
const graph = new Graph();
const types = desc.types;
const commands = desc.commands;
const events = desc.events;
const maybeAddPropEdges = function(nodeId: string, props: ?Array<Property>) {
if (props) {
for (const prop of props) {
const refId = prop.getRefDebuggerName();
(prop: Object).recursive = refId && refId === nodeId;
if (refId && refId !== nodeId) {
// Don't add edges for recursive properties.
graph.addEdge(nodeId, refId);
}
}
}
};
for (const type of types) {
graph.addNode(type.getDebuggerName());
if (type instanceof PropsType) {
maybeAddPropEdges(type.getDebuggerName(), type.properties);
}
}
for (const command of commands) {
graph.addNode(command.getDebuggerName());
maybeAddPropEdges(command.getDebuggerName(), command.parameters);
maybeAddPropEdges(command.getDebuggerName(), command.returns);
}
for (const event of events) {
graph.addNode(event.getDebuggerName());
maybeAddPropEdges(event.getDebuggerName(), event.parameters);
}
return graph;
}
function parseRoots(desc: Descriptor, rootsPath: ?string): Array<string> {
const roots = [];
if (rootsPath) {
const buf = fs.readFileSync(rootsPath);
for (let line of buf.toString().split('\n')) {
line = line.trim();
// ignore comments and blank lines
if (!line.match(/\s*#/) && line.length > 0) {
roots.push(line);
}
}
} else {
for (const type of desc.types) {
roots.push(type.getDebuggerName());
}
for (const command of desc.commands) {
roots.push(command.getDebuggerName());
}
for (const event of desc.events) {
roots.push(event.getDebuggerName());
}
}
return roots;
}
// only include types, commands, events that can be reached from the given
// root messages
function filterReachableFromRoots(
desc: Descriptor,
graph: Graph,
roots: Array<string>,
): Descriptor {
const topoSortedIds = graph.traverse(roots);
// Types can include other types by value, so they need to be topologically
// sorted in the header.
const typeMap: Map<string, Type> = new Map();
for (const type of desc.types) {
typeMap.set(type.getDebuggerName(), type);
}
const types = [];
for (const id of topoSortedIds) {
const type = typeMap.get(id);
if (type) {
types.push(type);
}
}
// Commands and events don't depend on each other, so just emit them in the
// order we got them from the JSON file.
const ids = new Set(topoSortedIds);
const commands = desc.commands.filter(cmd => ids.has(cmd.getDebuggerName()));
const events = desc.events.filter(event => ids.has(event.getDebuggerName()));
// Sort commands and events so the code is easier to read. Types have to be
// topologically sorted as explained above.
const comparator = (a, b) => {
const id1 = a.getDebuggerName();
const id2 = b.getDebuggerName();
return id1 < id2 ? -1 : id1 > id2 ? 1 : 0;
};
commands.sort(comparator);
events.sort(comparator);
return {types, commands, events};
}
function main() {
const args = yargs
.usage('Usage: msggen <header_path> <cpp_path>')
.alias('h', 'help')
.help('h')
.boolean('e')
.alias('e', 'ignore-experimental')
.describe('e', 'ignore experimental commands, props, and types')
.alias('r', 'roots')
.describe('r', 'path to a file listing root types, events, and commands')
.nargs('r', 1)
.demandCommand(2, 2).argv;
const ignoreExperimental = !!args.e;
const [headerPath, implPath] = args._;
const headerStream = fs.createWriteStream(headerPath);
const implStream = fs.createWriteStream(implPath);
const desc = parseDomains(proto.domains, ignoreExperimental);
const graph = buildGraph(desc);
const roots = parseRoots(desc, String(args.roots));
const reachable = filterReachableFromRoots(desc, graph, roots);
const hw = new HeaderWriter(
headerStream,
reachable.types,
reachable.commands,
reachable.events,
);
hw.write();
const iw = new ImplementationWriter(
implStream,
reachable.types,
reachable.commands,
reachable.events,
);
iw.write();
}
main();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
#!/bin/bash
set -e
DIR=$(dirname "${BASH_SOURCE[0]}")
cd "${DIR}/msggen"
yarn install
yarn build
FBSOURCE=$(hg root)
MSGTYPES_PATH="${FBSOURCE}/xplat/js/react-native-github/ReactCommon/hermes/inspector/tools/message_types.txt"
HEADER_PATH="${FBSOURCE}/xplat/js/react-native-github/ReactCommon/hermes/inspector/chrome/MessageTypes.h"
CPP_PATH="${FBSOURCE}/xplat/js/react-native-github/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp"
node bin/index.js \
--ignore-experimental \
--roots "$MSGTYPES_PATH" \
"$HEADER_PATH" "$CPP_PATH"
clang-format -i --style=file "$HEADER_PATH"
clang-format -i --style=file "$CPP_PATH"
"${FBSOURCE}/tools/signedsource" sign "$HEADER_PATH"
"${FBSOURCE}/tools/signedsource" sign "$CPP_PATH"

View File

@ -0,0 +1,13 @@
#!/bin/bash
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
THIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
source "$THIS_DIR/setup.sh"
buck test //xplat/js/react-native-github/ReactCommon/hermes/inspector:chrome &&
buck test //xplat/js/react-native-github/ReactCommon/hermes/inspector:detail &&
buck test //xplat/js/react-native-github/ReactCommon/hermes/inspector:inspectorlib &&
buck build //xplat/js/react-native-github/ReactCommon/hermes/inspector:hermes-chrome-debug-server

View File

@ -0,0 +1,35 @@
#!/bin/bash
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
#
# Basic setup for xplat testing in sandcastle. Based on
# xplat/hermes/facebook/sandcastle/setup.sh.
set -x
set -e
set -o pipefail
THIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ROOT_DIR=$(cd "$THIS_DIR" && hg root)
# Buck by default uses clang-3.6 from /opt/local/bin.
# Override it to use system clang.
export PATH="/usr/bin:$PATH"
# Enter xplat
cd "$ROOT_DIR"/xplat || exit 1
# Setup env
export TITLE
TITLE=$(hg log -l 1 --template "{desc|strip|firstline}")
export REV
REV=$(hg log -l 1 --template "{node}")
export AUTHOR
AUTHOR=$(hg log -l 1 --template "{author|emailuser}")
if [ -n "$SANDCASTLE" ]; then
source automation/setup_buck.sh
fi