/* * 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. */ // using include guards instead of #pragma once due to compile issues // with MSVC and BUCK #ifndef HERMES_INSPECTOR_INSPECTOR_H #define HERMES_INSPECTOR_INSPECTOR_H #include #include #include #include #include #include #include #include #include #include #include #include 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)) {} }; enum PauseOnLoadMode { None, Smart, All }; /** * 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 { 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 adapter, InspectorObserver &observer, bool pauseOnFirstStatement); ~Inspector() override; /** * disable turns off the inspector. All of the subsequent methods will not do * anything unless the inspector is enabled. */ folly::Future 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 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 executeIfEnabled( const std::string &description, folly::Function 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 setBreakpoint( facebook::hermes::debugger::SourceLocation loc, std::optional condition = std::nullopt); folly::Future removeBreakpoint( facebook::hermes::debugger::BreakpointID loc); /** * logs console message. */ folly::Future logMessage(ConsoleMessageInfo info); /** * resume and step methods are only valid when the VM is currently paused. The * returned future succeeds when the VM resumes execution, or fails with an * InvalidStateException otherwise. */ folly::Future resume(); folly::Future stepIn(); folly::Future stepOver(); folly::Future stepOut(); /** * pause can be issued at any time while the inspector is enabled. It requests * the VM to asynchronously break execution. The returned future succeeds if * the VM can be paused in this state and fails with InvalidStateException if * otherwise. */ folly::Future 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 evaluate( uint32_t frameIndex, const std::string &src, folly::Function resultTransformer); folly::Future setPauseOnExceptions( const facebook::hermes::debugger::PauseOnThrowMode &mode); /** * Set whether to pause on loads. This does not require runtime modifications, * but returns a future for consistency. */ folly::Future setPauseOnLoads(const PauseOnLoadMode mode); /** * Set whether breakpoints are active (pause when hit). This does not require * runtime modifications, but returns a future for consistency. */ folly::Future setBreakpointsActive(bool active); /** * If called during a script load event, return true if we should pause. * Assumed to be called from a script load event where we already hold * `mutex_`. */ bool shouldPauseOnThisScriptLoad(); /** * 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; /** * Get whether we started with pauseOnFirstStatement, and have not yet had a * debugger attach and ask to resume from that point. This matches the * semantics of when CDP Debugger.runIfWaitingForDebugger should resume. * * It's not named "isPausedOnStart" because the VM and inspector is not * necessarily paused; we could be in a RunningWaitPause state. */ bool isAwaitingDebuggerOnStart(); private: friend class InspectorState; void triggerAsyncPause(bool andTickle); void notifyContextCreated(); ScriptInfo getScriptInfoFromTopCallFrame(); void addCurrentScriptToLoadedScripts(); void removeAllBreakpoints(); void resetScriptsLoaded(); void notifyScriptsLoaded(); folly::Future setPendingCommand(debugger::Command command); void transition(std::unique_ptr nextState); // All methods that end with OnExecutor run on executor_. void disableOnExecutor(std::shared_ptr> promise); void enableOnExecutor(std::shared_ptr> promise); void executeIfEnabledOnExecutor( const std::string &description, folly::Function func, std::shared_ptr> promise); void setBreakpointOnExecutor( debugger::SourceLocation loc, std::optional condition, std::shared_ptr< folly::Promise> promise); void removeBreakpointOnExecutor( debugger::BreakpointID breakpointId, std::shared_ptr> promise); void logOnExecutor( ConsoleMessageInfo info, std::shared_ptr> promise); void setPendingCommandOnExecutor( facebook::hermes::debugger::Command command, std::shared_ptr> promise); void pauseOnExecutor(std::shared_ptr> promise); void evaluateOnExecutor( uint32_t frameIndex, const std::string &src, std::shared_ptr> promise, folly::Function resultTransformer); void setPauseOnExceptionsOnExecutor( const facebook::hermes::debugger::PauseOnThrowMode &mode, std::shared_ptr> promise); void installConsoleFunction( jsi::Object &console, std::shared_ptr &originalConsole, const std::string &name, const std::string &chromeType); std::shared_ptr adapter_; facebook::hermes::debugger::Debugger &debugger_; InspectorObserver &observer_; // All of the following member variables are guarded by mutex_. std::mutex mutex_; std::unique_ptr 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; // Whether we should enter a paused state when a script loads. PauseOnLoadMode pauseOnLoadMode_ = PauseOnLoadMode::None; // Whether or not we should pause on breakpoints. bool breakpointsActive_ = true; // 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 loadedScripts_; std::unordered_map 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(); // Are we currently waiting for a debugger to attach, because we // requested 'pauseOnFirstStatement'? bool awaitingDebuggerOnStart_; // 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. // NOTE: This needs to be declared LAST because it should be destroyed FIRST. std::unique_ptr executor_; }; /// Helper function that guards user code execution in a try-catch block. template std::optional runUserCallback(C &cb, A &&...arg) { try { cb(std::forward(arg)...); } catch (const std::exception &e) { return UserCallbackException(e); } return {}; } } // namespace inspector } // namespace hermes } // namespace facebook #endif // HERMES_INSPECTOR_INSPECTOR_H