/* * 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 #include #include #include #include #ifdef HERMES_INSPECTOR_FOLLY_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; template class folly::Future; namespace folly { namespace futures { SemiFuture sleep(Duration, Timekeeper *) { LOG(FATAL) << "folly::futures::sleep() not implemented"; } } // namespace futures namespace detail { std::shared_ptr getTimekeeperSingleton() { LOG(FATAL) << "folly::detail::getTimekeeperSingleton() not implemented"; } } // namespace detail } // namespace folly // #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 adapter, InspectorObserver &observer, bool pauseOnFirstStatement) : adapter_(adapter), debugger_(adapter->getRuntime().getDebugger()), observer_(observer), executor_(std::make_unique("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(src), "__tickleJsHackUrl"); { std::lock_guard lock(mutex_); if (pauseOnFirstStatement) { awaitingDebuggerOnStart_ = true; TRANSITION(std::make_unique(*this)); } else { TRANSITION(std::make_unique(*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 &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(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 originalConsole; if (val.isObject()) { originalConsole = std::make_shared(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 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 Inspector::disable() { auto promise = std::make_shared>(); executor_->add([this, promise] { disableOnExecutor(promise); }); return promise->getFuture(); } folly::Future Inspector::enable() { auto promise = std::make_shared>(); executor_->add([this, promise] { enableOnExecutor(promise); }); return promise->getFuture(); } folly::Future Inspector::executeIfEnabled( const std::string &description, folly::Function func) { auto promise = std::make_shared>(); executor_->add( [this, description, func = std::move(func), promise]() mutable { executeIfEnabledOnExecutor(description, std::move(func), promise); }); return promise->getFuture(); } folly::Future Inspector::setBreakpoint( debugger::SourceLocation loc, std::optional condition) { auto promise = std::make_shared>(); // 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 Inspector::removeBreakpoint( debugger::BreakpointID breakpointId) { auto promise = std::make_shared>(); executor_->add([this, breakpointId, promise] { removeBreakpointOnExecutor(breakpointId, promise); }); return promise->getFuture(); } folly::Future Inspector::logMessage(ConsoleMessageInfo info) { auto promise = std::make_shared>(); executor_->add([this, pInfo = std::make_unique(std::move(info)), promise] { logOnExecutor(std::move(*pInfo), promise); }); return promise->getFuture(); } folly::Future Inspector::setPendingCommand(debugger::Command command) { auto promise = std::make_shared>(); executor_->add([this, promise, cmd = std::move(command)]() mutable { setPendingCommandOnExecutor(std::move(cmd), promise); }); return promise->getFuture(); } folly::Future Inspector::resume() { return setPendingCommand(debugger::Command::continueExecution()); } folly::Future Inspector::stepIn() { return setPendingCommand(debugger::Command::step(debugger::StepMode::Into)); } folly::Future Inspector::stepOver() { return setPendingCommand(debugger::Command::step(debugger::StepMode::Over)); } folly::Future Inspector::stepOut() { return setPendingCommand(debugger::Command::step(debugger::StepMode::Out)); } folly::Future Inspector::pause() { auto promise = std::make_shared>(); executor_->add([this, promise]() { pauseOnExecutor(promise); }); return promise->getFuture(); } folly::Future Inspector::evaluate( uint32_t frameIndex, const std::string &src, folly::Function resultTransformer) { auto promise = std::make_shared>(); executor_->add([this, frameIndex, src, promise, resultTransformer = std::move(resultTransformer)]() mutable { evaluateOnExecutor(frameIndex, src, promise, std::move(resultTransformer)); }); return promise->getFuture(); } folly::Future Inspector::setPauseOnExceptions( const debugger::PauseOnThrowMode &mode) { auto promise = std::make_shared>(); executor_->add([this, mode, promise]() mutable { setPauseOnExceptionsOnExecutor(mode, promise); }); return promise->getFuture(); }; folly::Future 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>(); pauseOnLoadMode_ = mode; promise->setValue(); return promise->getFuture(); }; folly::Future Inspector::setBreakpointsActive(bool active) { // Same logic as setPauseOnLoads. auto promise = std::make_shared>(); 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 lock(mutex_); if (kShouldLog) { LOG(INFO) << "received didPause for reason: " << static_cast(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 nextState = std::move(result.first); if (nextState) { TRANSITION(std::move(nextState)); } std::unique_ptr command = std::move(result.second); if (command) { return std::move(*command); } } } void Inspector::breakpointResolved( debugger::Debugger &debugger, debugger::BreakpointID breakpointId) { std::unique_lock lock(mutex_); debugger::BreakpointInfo info = debugger.getBreakpointInfo(breakpointId); observer_.onBreakpointResolved(*this, info); } void Inspector::transition(std::unique_ptr nextState) { assert(nextState); assert(state_ != nextState); std::unique_ptr prevState = std::move(state_); state_ = std::move(nextState); state_->onEnter(prevState.get()); } void Inspector::disableOnExecutor( std::shared_ptr> promise) { std::lock_guard lock(mutex_); debugger_.setIsDebuggerAttached(false); state_->detach(promise); } void Inspector::enableOnExecutor( std::shared_ptr> promise) { std::lock_guard 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 nextState = std::move(result.first); if (nextState) { TRANSITION(std::move(nextState)); } } void Inspector::executeIfEnabledOnExecutor( const std::string &description, folly::Function func, std::shared_ptr> promise) { std::lock_guard 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 condition, std::shared_ptr> promise) { std::lock_guard 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> promise) { std::lock_guard 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> promise) { std::lock_guard lock(mutex_); state_->pushPendingFunc([this, info = std::move(info)] { observer_.onMessageAdded(*this, info); }); promise->setValue(); } void Inspector::setPendingCommandOnExecutor( debugger::Command command, std::shared_ptr> promise) { std::lock_guard lock(mutex_); state_->setPendingCommand(std::move(command), promise); } void Inspector::pauseOnExecutor(std::shared_ptr> promise) { std::lock_guard 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> promise, folly::Function resultTransformer) { std::lock_guard lock(mutex_); state_->pushPendingEval( frameIndex, src, promise, std::move(resultTransformer)); } void Inspector::setPauseOnExceptionsOnExecutor( const debugger::PauseOnThrowMode &mode, std::shared_ptr> promise) { std::lock_guard 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