amis-rpc-design/node_modules/react-native/ReactCommon/hermes/inspector/Inspector.cpp

745 lines
24 KiB
C++
Raw Normal View History

2023-10-07 19:42:30 +08:00
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "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 = false;
// 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->getRuntime().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) {
awaitingDebuggerOnStart_ = true;
TRANSITION(std::make_unique<InspectorState::RunningWaitEnable>(*this));
} else {
TRANSITION(std::make_unique<InspectorState::RunningDetached>(*this));
}
}
debugger_.setShouldPauseOnScriptLoad(true);
debugger_.setEventObserver(this);
}
Inspector::~Inspector() {
debugger_.setEventObserver(nullptr);
}
static bool toBoolean(jsi::Runtime &runtime, const jsi::Value &val) {
// Based on Operations.cpp:toBoolean in the Hermes VM.
if (val.isUndefined() || val.isNull()) {
return false;
}
if (val.isBool()) {
return val.getBool();
}
if (val.isNumber()) {
double m = val.getNumber();
return m != 0 && !std::isnan(m);
}
if (val.isSymbol() || val.isObject()) {
return true;
}
if (val.isString()) {
std::string s = val.getString(runtime).utf8(runtime);
return !s.empty();
}
assert(false && "All cases should be covered");
return false;
}
void Inspector::installConsoleFunction(
jsi::Object &console,
std::shared_ptr<jsi::Object> &originalConsole,
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, originalConsole, name, chromeType](
jsi::Runtime &runtime,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count) {
if (originalConsole) {
auto val = originalConsole->getProperty(runtime, name.c_str());
if (val.isObject()) {
auto obj = val.getObject(runtime);
if (obj.isFunction(runtime)) {
auto func = obj.getFunction(runtime);
func.callWithThis(runtime, *originalConsole, args, count);
}
}
}
if (auto inspector = weakInspector.lock()) {
if (name != "assert") {
// All cases other than assert just log a simple message.
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();
}
// console.assert needs to check the first parameter before
// logging.
if (count == 0) {
// No parameters, throw a blank assertion failed message.
inspector->logMessage(
ConsoleMessageInfo{chromeType, jsi::Array(runtime, 0)});
} else if (!toBoolean(runtime, args[0])) {
// Shift the message array down by one to not include the
// condition.
jsi::Array argsArray(runtime, count - 1);
for (size_t index = 1; 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);
auto val = rt.global().getProperty(rt, "console");
std::shared_ptr<jsi::Object> originalConsole;
if (val.isObject()) {
originalConsole = std::make_shared<jsi::Object>(val.getObject(rt));
}
installConsoleFunction(console, originalConsole, "assert");
installConsoleFunction(console, originalConsole, "clear");
installConsoleFunction(console, originalConsole, "debug");
installConsoleFunction(console, originalConsole, "dir");
installConsoleFunction(console, originalConsole, "dirxml");
installConsoleFunction(console, originalConsole, "error");
installConsoleFunction(console, originalConsole, "group", "startGroup");
installConsoleFunction(
console, originalConsole, "groupCollapsed", "startGroupCollapsed");
installConsoleFunction(console, originalConsole, "groupEnd", "endGroup");
installConsoleFunction(console, originalConsole, "info");
installConsoleFunction(console, originalConsole, "log");
installConsoleFunction(console, originalConsole, "profile");
installConsoleFunction(console, originalConsole, "profileEnd");
installConsoleFunction(console, originalConsole, "table");
installConsoleFunction(console, originalConsole, "trace");
installConsoleFunction(console, originalConsole, "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,
std::optional<std::string> condition) {
auto promise = std::make_shared<folly::Promise<debugger::BreakpointInfo>>();
// Automatically re-enable breakpoints since the user presumably wants this
// to start triggering.
breakpointsActive_ = true;
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();
};
folly::Future<folly::Unit> Inspector::setPauseOnLoads(
const PauseOnLoadMode mode) {
// This flag does not touch the runtime, so it doesn't need the executor.
// Return a future anyways for consistency.
auto promise = std::make_shared<folly::Promise<Unit>>();
pauseOnLoadMode_ = mode;
promise->setValue();
return promise->getFuture();
};
folly::Future<folly::Unit> Inspector::setBreakpointsActive(bool active) {
// Same logic as setPauseOnLoads.
auto promise = std::make_shared<folly::Promise<Unit>>();
breakpointsActive_ = active;
promise->setValue();
return promise->getFuture();
};
bool Inspector::shouldPauseOnThisScriptLoad() {
switch (pauseOnLoadMode_) {
case None:
return false;
case All:
return true;
case Smart:
// If we don't have active breakpoints, there's nothing to set or update.
if (debugger_.getBreakpoints().size() == 0) {
return false;
}
// If there's no source map URL, it's probably not a file we care about.
if (getScriptInfoFromTopCallFrame().sourceMappingUrl.size() == 0) {
return false;
}
return true;
}
};
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 {
if (auto userCallbackException = runUserCallback(wrappedFunc)) {
promise->setException(*userCallbackException);
} else {
promise->setValue();
}
});
}
void Inspector::setBreakpointOnExecutor(
debugger::SourceLocation loc,
std::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;
}
bool Inspector::isAwaitingDebuggerOnStart() {
return awaitingDebuggerOnStart_;
}
} // namespace inspector
} // namespace hermes
} // namespace facebook