385 lines
13 KiB
C
385 lines
13 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.
|
||
|
*/
|
||
|
|
||
|
// 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 <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/Exceptions.h>
|
||
|
#include <hermes/inspector/RuntimeAdapter.h>
|
||
|
#include <optional>
|
||
|
|
||
|
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<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() override;
|
||
|
|
||
|
/**
|
||
|
* 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,
|
||
|
std::optional<std::string> condition = std::nullopt);
|
||
|
|
||
|
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 succeeds 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 succeeds 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);
|
||
|
|
||
|
/**
|
||
|
* Set whether to pause on loads. This does not require runtime modifications,
|
||
|
* but returns a future for consistency.
|
||
|
*/
|
||
|
folly::Future<folly::Unit> 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<folly::Unit> 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<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,
|
||
|
std::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,
|
||
|
std::shared_ptr<jsi::Object> &originalConsole,
|
||
|
const std::string &name,
|
||
|
const std::string &chromeType);
|
||
|
|
||
|
std::shared_ptr<RuntimeAdapter> adapter_;
|
||
|
facebook::hermes::debugger::Debugger &debugger_;
|
||
|
InspectorObserver &observer_;
|
||
|
|
||
|
// 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;
|
||
|
|
||
|
// 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<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();
|
||
|
|
||
|
// 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<folly::Executor> executor_;
|
||
|
};
|
||
|
|
||
|
/// Helper function that guards user code execution in a try-catch block.
|
||
|
template <typename C, typename... A>
|
||
|
std::optional<UserCallbackException> runUserCallback(C &cb, A &&...arg) {
|
||
|
try {
|
||
|
cb(std::forward<A>(arg)...);
|
||
|
} catch (const std::exception &e) {
|
||
|
return UserCallbackException(e);
|
||
|
}
|
||
|
|
||
|
return {};
|
||
|
}
|
||
|
|
||
|
} // namespace inspector
|
||
|
} // namespace hermes
|
||
|
} // namespace facebook
|
||
|
|
||
|
#endif // HERMES_INSPECTOR_INSPECTOR_H
|