Files
react-native/ReactCommon/hermes/inspector/InspectorState.cpp
T
John PortoandFacebook GitHub Bot d2f5044c37 Catch exceptions thrown by user callbacks
Summary:
Calls to Inspector::evaluate() and Inspector::executeIfEnabled()
take a user-provided callback which are not try-guarded. This
means that, should the user code throw, the inspector's process
will likely die due to the unhandled exception. This change
makes sure that, if the user provided callback throws, then
the promise attached to those methods will be fulfilled by
an exception.

Change: [internal]

Reviewed By: avp

Differential Revision: D33852073

fbshipit-source-id: 7fbb6662b28d393a5d5b494c004aa9521e23ebb6
2022-01-28 13:38:27 -08:00

512 lines
16 KiB
C++

/*
* 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 "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());
}
void InspectorState::RunningDetached::onEnter(InspectorState *previous) {
inspector_.awaitingDebuggerOnStart_ = false;
}
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();
}
}
inspector_.awaitingDebuggerOnStart_ = false;
}
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();
if (inspector_.shouldPauseOnThisScriptLoad()) {
return std::make_pair<NextStatePtr, CommandPtr>(
InspectorState::Paused::make(inspector_), nullptr);
}
} else if (reason == debugger::PauseReason::EvalComplete) {
assert(pendingEvalPromise_);
if (auto userCallbackException = runUserCallback(
pendingEvalResultTransformer_,
inspector_.debugger_.getProgramState().getEvalResult())) {
pendingEvalPromise_->setException(*userCallbackException);
} else {
pendingEvalPromise_->setValue(
inspector_.debugger_.getProgramState().getEvalResult());
}
pendingEvalPromise_.reset();
} else if (
reason == debugger::PauseReason::Breakpoint &&
!inspector_.breakpointsActive_) {
// We hit a user defined breakpoint, but breakpoints have been deactivated.
return std::make_pair<NextStatePtr, CommandPtr>(
nullptr, makeContinueCommand());
} 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_);
if (auto userCallbackException = runUserCallback(
pendingEvalResultTransformer_,
inspector_.debugger_.getProgramState().getEvalResult())) {
pendingEvalPromise_->setException(*userCallbackException);
} else {
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