[LLDB] Add module hook implementation (#185465)

Introduce a unified target hook command system for LLDB that handles
multiple target lifecycle events (module load, module unload, process
stop) through a single Hook class.

The existing target stop-hook commands remain unchanged for now, meaning
stop-hooks can now be created through either target stop-hook add or
target hook add -S.
A future follow-up can alias target stop-hook to the unified system to
consolidate the two paths.

 New commands:
- target hook add [-o cmd] [-P class] [-u] [-S] - create a hook (fires
on module load by default, optionally on unload/stop)
- target hook add-filter [filter-opts] <hook-id> - attach stop-event
filters (shlib, function, file/line, thread) to an existing hook
- target hook list / delete / enable / disable - manage hooks
- target hook {enable,disable} {module-loaded|module-unloaded|stop-hook}
<id> - per-event toggling

top-event filter options live on a separate add-filter command rather
than on target hook add, keeping the add command clean and making the
system safe for filters on other event types.

Ran the tests + Made sure stop tests are passing as well.

---------

Co-authored-by: Bar Soloveychik <barsolo@fb.com>
This commit is contained in:
Bar Soloveychik 2026-04-28 14:07:44 -07:00 committed by GitHub
parent f70ed1f758
commit b1c027b3aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 2000 additions and 1 deletions

View file

@ -0,0 +1,53 @@
//===-- ScriptedHookInterface.h ---------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#ifndef LLDB_INTERPRETER_INTERFACES_SCRIPTEDHOOKINTERFACE_H
#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDHOOKINTERFACE_H
#include "lldb/lldb-private.h"
#include "ScriptedInterface.h"
namespace lldb_private {
class ScriptedHookInterface : public ScriptedInterface {
public:
/// Describes which hook callback methods the Python class implements.
struct SupportedHookMethods {
bool handle_module_loaded = false;
bool handle_module_unloaded = false;
bool handle_stop = false;
bool any() const {
return handle_module_loaded || handle_module_unloaded || handle_stop;
}
};
virtual llvm::Expected<StructuredData::GenericSP>
CreatePluginObject(llvm::StringRef class_name, lldb::TargetSP target_sp,
const StructuredDataImpl &args_sp) = 0;
/// Check which hook callback methods the Python class implements.
/// Called after CreatePluginObject to determine the trigger mask.
virtual SupportedHookMethods GetSupportedMethods() { return {}; }
/// Called when modules are loaded into the target.
virtual void HandleModuleLoaded(lldb::StreamSP &output_sp) {}
/// Called when modules are unloaded from the target. Optional.
virtual void HandleModuleUnloaded(lldb::StreamSP &output_sp) {}
/// Called when the process stops. Returns "should_stop" if false, the
/// process will continue. Defaults to true (stop on unimplemented).
virtual llvm::Expected<bool> HandleStop(ExecutionContext &exe_ctx,
lldb::StreamSP &output_sp) {
return true;
}
};
} // namespace lldb_private
#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDHOOKINTERFACE_H

View file

@ -562,6 +562,10 @@ public:
return {};
}
virtual lldb::ScriptedHookInterfaceSP CreateScriptedHookInterface() {
return {};
}
virtual lldb::ScriptedBreakpointInterfaceSP
CreateScriptedBreakpointInterface() {
return {};

View file

@ -1666,6 +1666,201 @@ public:
typedef std::shared_ptr<StopHook> StopHookSP;
// Target Hooks
//
// Hooks fire on target lifecycle events. There are two flows:
//
// Command-based hooks: the user specifies which triggers the hook responds
// to (--on-load, --on-unload, --on-stop) and provides a list of commands.
// All commands run for every trigger the hook is signed up for.
//
// Python class hooks: the user provides a Python class name and optional
// extra_args that will be passed to the hook init method (-k key -v value).
// The class controls which events it handles by implementing the
// corresponding callback methods (handle_module_loaded,
// handle_module_unloaded, handle_stop). Triggers are set automatically
// based on which methods exist.
class Hook : public UserID {
public:
Hook(const Hook &rhs);
virtual ~Hook() = default;
enum class HookKind : uint32_t { CommandBased = 0, ScriptBased };
HookKind GetHookKind() const { return m_kind; }
/// Individual trigger bits. Combine with bitwise OR to form a trigger mask.
// FIXME: Add kProcessExit, kProcessDetach, etc. as needed.
enum TriggerBit : uint32_t {
kModulesLoaded = (1u << 0),
kModulesUnloaded = (1u << 1),
kProcessStop = (1u << 2),
};
lldb::TargetSP &GetTarget() { return m_target_sp; }
bool IsEnabled() { return m_enabled; }
void SetIsEnabled(bool enabled) { m_enabled = enabled; }
/// Return the bitmask of triggers this hook responds to.
/// Each bit corresponds to a TriggerBit value.
uint32_t GetTriggerMask() const { return m_trigger_mask; }
/// Return true if this hook fires on the given trigger.
bool FiresOn(uint32_t trigger) const { return m_trigger_mask & trigger; }
// Filter fields
/// Set the symbol context specifier. The hook takes ownership.
void SetSCSpecifier(SymbolContextSpecifier *specifier);
SymbolContextSpecifier *GetSCSpecifier() { return m_sc_specifier_sp.get(); }
/// Check if the execution context passes the specifier and thread spec
/// filters. Always returns true if no filters are set.
bool ExecutionContextPasses(const ExecutionContext &exe_ctx);
/// Set the thread specifier. The hook takes ownership.
void SetThreadSpecifier(ThreadSpec *specifier);
ThreadSpec *GetThreadSpecifier() { return m_thread_spec_up.get(); }
void SetRunAtInitialStop(bool at_initial_stop) {
m_at_initial_stop = at_initial_stop;
}
bool GetRunAtInitialStop() const { return m_at_initial_stop; }
// Reaction settings
void SetAutoContinue(bool auto_continue) {
m_auto_continue = auto_continue;
}
bool GetAutoContinue() const { return m_auto_continue; }
void SetSuppressOutput(bool suppress_output) {
m_suppress_output = suppress_output;
}
bool GetSuppressOutput() const { return m_suppress_output; }
// Event handler methods (default no-ops)
virtual void HandleModuleLoaded(lldb::StreamSP output) {}
virtual void HandleModuleUnloaded(lldb::StreamSP output) {}
/// Called when the process stops. Returns a StopHookResult indicating
/// whether the process should remain stopped or continue.
virtual StopHook::StopHookResult HandleStop(ExecutionContext &exe_ctx,
lldb::StreamSP output) {
return StopHook::StopHookResult::NoPreference;
}
virtual void GetDescription(Stream &s, lldb::DescriptionLevel level) const;
protected:
/// Print the filter portion of the description (AutoContinue, Specifier,
/// ThreadSpec). Called by subclass GetDescription after printing the
/// hook-specific content (commands or class).
void GetFilterDescription(Stream &s, lldb::DescriptionLevel level) const;
lldb::TargetSP m_target_sp;
HookKind m_kind;
bool m_enabled = true;
uint32_t m_trigger_mask = 0; // No default, triggers must be explicit.
// Filters
lldb::SymbolContextSpecifierSP m_sc_specifier_sp;
std::unique_ptr<ThreadSpec> m_thread_spec_up;
bool m_at_initial_stop = true;
// Reaction settings
bool m_auto_continue = false;
bool m_suppress_output = false;
Hook(lldb::TargetSP target_sp, lldb::user_id_t uid, HookKind kind);
};
class HookCommandLine : public Hook {
public:
~HookCommandLine() override = default;
/// Replace the trigger mask. \a mask is a bitwise OR of TriggerBit values.
void SetTriggerMask(uint32_t mask) { m_trigger_mask = mask; }
/// Add a trigger to the mask. \a trigger is a single TriggerBit value.
void AddTrigger(uint32_t trigger) { m_trigger_mask |= trigger; }
/// Remove a trigger from the mask. \a trigger is a single TriggerBit value.
void RemoveTrigger(uint32_t trigger) { m_trigger_mask &= ~trigger; }
/// Return the list of commands that this hook runs.
StringList &GetCommands() { return m_commands; }
/// Populate the command list by splitting a single string on newlines.
void SetActionFromString(const std::string &string);
/// Populate the command list from a vector of individual command strings.
void SetActionFromStrings(const std::vector<std::string> &strings);
void GetDescription(Stream &s, lldb::DescriptionLevel level) const override;
void HandleModuleLoaded(lldb::StreamSP output) override;
void HandleModuleUnloaded(lldb::StreamSP output) override;
StopHook::StopHookResult HandleStop(ExecutionContext &exe_ctx,
lldb::StreamSP output) override;
private:
StringList m_commands;
HookCommandLine(lldb::TargetSP target_sp, lldb::user_id_t uid)
: Hook(target_sp, uid, HookKind::CommandBased) {}
friend class Target;
};
class HookScripted : public Hook {
public:
~HookScripted() override = default;
void GetDescription(Stream &s, lldb::DescriptionLevel level) const override;
void HandleModuleLoaded(lldb::StreamSP output) override;
void HandleModuleUnloaded(lldb::StreamSP output) override;
StopHook::StopHookResult HandleStop(ExecutionContext &exe_ctx,
lldb::StreamSP output) override;
Status SetScriptCallback(std::string class_name,
StructuredData::ObjectSP extra_args_sp);
private:
std::string m_class_name;
StructuredDataImpl m_extra_args;
lldb::ScriptedHookInterfaceSP m_interface_sp;
HookScripted(lldb::TargetSP target_sp, lldb::user_id_t uid)
: Hook(target_sp, uid, HookKind::ScriptBased) {}
friend class Target;
};
typedef std::shared_ptr<Hook> HookSP;
HookSP CreateHook(Hook::HookKind kind);
/// Removes the most recently created hook. Used to roll back a
/// hook creation when an error occurs (e.g., invalid script class name
/// or empty interactive input).
void UndoCreateHook(lldb::user_id_t uid);
bool RemoveHookByID(lldb::user_id_t uid);
void RemoveAllHooks();
HookSP GetHookByID(lldb::user_id_t uid);
bool SetHookEnabledStateByID(lldb::user_id_t uid, bool enabled);
void SetAllHooksEnabledState(bool enabled);
size_t GetNumHooks() const { return m_hooks.size(); }
HookSP GetHookAtIndex(size_t index);
void RunModuleHooks(bool is_load);
/// Add an empty stop hook to the Target's stop hook list, and returns a
/// shared pointer to the new hook.
StopHookSP CreateStopHook(StopHook::StopHookKind kind, bool internal = false);
@ -1859,6 +2054,10 @@ protected:
bool m_valid;
bool m_suppress_stop_hooks; /// Used to not run stop hooks for expressions
bool m_is_dummy_target;
typedef std::map<lldb::user_id_t, HookSP> HookCollection;
HookCollection m_hooks;
lldb::user_id_t m_hook_next_id = 0;
unsigned m_next_persistent_variable_index = 0;
lldb::user_id_t m_target_unique_id =
LLDB_INVALID_GLOBALLY_UNIQUE_TARGET_ID; ///< The globally unique ID

View file

@ -191,6 +191,7 @@ class ScriptedFrameInterface;
class ScriptedFrameProviderInterface;
class ScriptedMetadata;
class ScriptedBreakpointInterface;
class ScriptedHookInterface;
class ScriptedPlatformInterface;
class ScriptedProcessInterface;
class ScriptedStopHookInterface;
@ -425,6 +426,8 @@ typedef std::unique_ptr<lldb_private::ScriptedProcessInterface>
ScriptedProcessInterfaceUP;
typedef std::shared_ptr<lldb_private::ScriptedStopHookInterface>
ScriptedStopHookInterfaceSP;
typedef std::shared_ptr<lldb_private::ScriptedHookInterface>
ScriptedHookInterfaceSP;
typedef std::shared_ptr<lldb_private::ScriptedThreadInterface>
ScriptedThreadInterfaceSP;
typedef std::shared_ptr<lldb_private::ScriptedThreadPlanInterface>

View file

@ -5290,6 +5290,708 @@ public:
~CommandObjectMultiwordTargetStopHooks() override = default;
};
#pragma mark CommandObjectTargetHookAdd
#define LLDB_OPTIONS_target_hook_add
#include "CommandOptions.inc"
class CommandObjectTargetHookAdd : public CommandObjectParsed,
public IOHandlerDelegateMultiline {
public:
class CommandOptions : public OptionGroup {
public:
CommandOptions() = default;
~CommandOptions() override = default;
llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
return llvm::ArrayRef(g_target_hook_add_options);
}
Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
ExecutionContext *execution_context) override {
Status error;
const int short_option =
g_target_hook_add_options[option_idx].short_option;
switch (short_option) {
case 'o':
m_use_one_liner = true;
m_one_liner.push_back(std::string(option_arg));
break;
case 'L':
m_on_load = true;
break;
case 'u':
m_on_unload = true;
break;
case 'S':
m_on_stop = true;
break;
case 's':
m_module_name = std::string(option_arg);
m_sym_ctx_specified = true;
break;
case 'x': {
uint32_t thread_index;
if (option_arg.getAsInteger(0, thread_index))
error = Status::FromErrorStringWithFormat("invalid thread index '%s'",
option_arg.str().c_str());
else
m_thread_index = thread_index;
m_thread_specified = true;
break;
}
case 't': {
lldb::tid_t thread_id;
if (option_arg.getAsInteger(0, thread_id))
error = Status::FromErrorStringWithFormat("invalid thread id '%s'",
option_arg.str().c_str());
else
m_thread_id = thread_id;
m_thread_specified = true;
break;
}
case 'T':
m_thread_name = std::string(option_arg);
m_thread_specified = true;
break;
case 'q':
m_queue_name = std::string(option_arg);
m_thread_specified = true;
break;
case 'f':
m_file_name = std::string(option_arg);
m_sym_ctx_specified = true;
break;
case 'l': {
uint32_t line;
if (option_arg.getAsInteger(0, line))
error = Status::FromErrorStringWithFormat(
"invalid start line number '%s'", option_arg.str().c_str());
else
m_line_start = line;
m_sym_ctx_specified = true;
break;
}
case 'e': {
uint32_t line;
if (option_arg.getAsInteger(0, line))
error = Status::FromErrorStringWithFormat(
"invalid end line number '%s'", option_arg.str().c_str());
else
m_line_end = line;
m_sym_ctx_specified = true;
break;
}
case 'c':
m_class_name = std::string(option_arg);
m_sym_ctx_specified = true;
break;
case 'n':
m_function_name = std::string(option_arg);
m_sym_ctx_specified = true;
break;
case 'G': {
bool value, success;
value = OptionArgParser::ToBoolean(option_arg, false, &success);
if (success)
m_auto_continue = value;
else
error = Status::FromErrorStringWithFormat(
"invalid boolean value '%s' passed for -G option",
option_arg.str().c_str());
break;
}
case 'I': {
bool value, success;
value = OptionArgParser::ToBoolean(option_arg, true, &success);
if (success)
m_at_initial_stop = value;
else
error = Status::FromErrorStringWithFormat(
"invalid boolean value '%s' passed for -I option",
option_arg.str().c_str());
break;
}
default:
llvm_unreachable("unhandled option");
}
return error;
}
void OptionParsingStarting(ExecutionContext *execution_context) override {
m_use_one_liner = false;
m_one_liner.clear();
m_on_load = false;
m_on_unload = false;
m_on_stop = false;
m_sym_ctx_specified = false;
m_thread_specified = false;
m_module_name.clear();
m_file_name.clear();
m_class_name.clear();
m_function_name.clear();
m_line_start = 0;
m_line_end = UINT_MAX;
m_thread_id = LLDB_INVALID_THREAD_ID;
m_thread_index = UINT32_MAX;
m_thread_name.clear();
m_queue_name.clear();
m_auto_continue = false;
m_at_initial_stop = true;
}
std::vector<std::string> m_one_liner;
bool m_use_one_liner = false;
bool m_on_load = false;
bool m_on_unload = false;
bool m_on_stop = false;
// Filter options (for stop trigger).
bool m_sym_ctx_specified = false;
bool m_thread_specified = false;
std::string m_module_name;
std::string m_file_name;
std::string m_class_name;
std::string m_function_name;
uint32_t m_line_start = 0;
uint32_t m_line_end = UINT_MAX;
lldb::tid_t m_thread_id = LLDB_INVALID_THREAD_ID;
uint32_t m_thread_index = UINT32_MAX;
std::string m_thread_name;
std::string m_queue_name;
bool m_auto_continue = false;
bool m_at_initial_stop = true;
};
CommandObjectTargetHookAdd(CommandInterpreter &interpreter)
: CommandObjectParsed(
interpreter, "target hook add",
"Add a hook to be executed on target lifecycle events.",
"target hook add"),
IOHandlerDelegateMultiline("DONE",
IOHandlerDelegate::Completion::LLDBCommand),
m_python_class_options("scripted hook", false, 'P') {
SetHelpLong(R"help(
Command-based hooks:
--------------------
Specify which triggers the hook responds to with --on-load (-L),
--on-unload (-u), and/or --on-stop (-S). At least one trigger is required.
Provide commands with --one-liner (-o), or omit -o to enter an interactive
command editor. All commands run for every trigger the hook is signed up
for; there is no per-trigger command list.
Examples:
target hook add -L -o "script print('module loaded')"
target hook add -L -u -o "script print('module event')"
target hook add -S -o "bt"
target hook add -L -u -S -o "script print('all events')"
target hook add -S -s mylib.so -n main -o "bt"
target hook add -S -G true -o "thread info"
Python class hooks:
-------------------
Provide a Python class with --python-class (-P). The class controls which
events it handles by implementing the corresponding methods; you do not
specify triggers on the command line. Use -k <key> -v <value> to pass
extra_args to the class constructor.
Examples:
target hook add -P mymodule.MyHook
target hook add -P mymodule.MyHook -k verbose -v true
The Python class should implement at least one of these methods:
class MyHook:
def __init__(self, target, extra_args, internal_dict):
self.target = target
def handle_module_loaded(self, stream):
pass
def handle_module_unloaded(self, stream):
pass
def handle_stop(self, exe_ctx, stream):
return True # True = should_stop, False = continue
Filter options:
---------------
Filters (-s, -f, -l, -e, -c, -n, -x, -t, -T, -q) restrict when the hook
fires. They apply to both command-based and Python class hooks.
)help");
// Python class options (-P, -k, -v) are placed in Set 2 (dst_mask).
// src_mask must cover Set 1 | Set 2 to match the internal usage masks of
// OptionGroupPythonClassWithDict (class=Set1, key/value=Set2).
// Since -o, -L, -u, -S are Group<1> only, the parser prevents mixing.
m_all_options.Append(&m_python_class_options,
LLDB_OPT_SET_1 | LLDB_OPT_SET_2, LLDB_OPT_SET_2);
m_all_options.Append(&m_options);
m_all_options.Finalize();
}
~CommandObjectTargetHookAdd() override = default;
Options *GetOptions() override { return &m_all_options; }
protected:
void IOHandlerActivated(IOHandler &io_handler, bool interactive) override {
if (interactive) {
if (lldb::LockableStreamFileSP output_sp =
io_handler.GetOutputStreamFileSP()) {
LockedStreamFile locked_stream = output_sp->Lock();
locked_stream.PutCString(
"Enter your hook command(s). Type 'DONE' to end.\n");
}
}
}
void IOHandlerInputComplete(IOHandler &io_handler,
std::string &line) override {
if (m_hook_sp) {
if (line.empty()) {
if (lldb::LockableStreamFileSP error_sp =
io_handler.GetErrorStreamFileSP()) {
LockedStreamFile locked_stream = error_sp->Lock();
locked_stream.Printf("error: hook #%" PRIu64
" aborted, no commands.\n",
m_hook_sp->GetID());
}
GetTarget().UndoCreateHook(m_hook_sp->GetID());
} else {
auto *hook = static_cast<Target::HookCommandLine *>(m_hook_sp.get());
hook->SetActionFromString(line);
if (lldb::LockableStreamFileSP output_sp =
io_handler.GetOutputStreamFileSP()) {
LockedStreamFile locked_stream = output_sp->Lock();
locked_stream.Printf("Hook #%" PRIu64 " added.\n",
m_hook_sp->GetID());
}
}
m_hook_sp.reset();
}
io_handler.SetIsDone(true);
}
void DoExecute(Args &command, CommandReturnObject &result) override {
m_hook_sp.reset();
Target &target = GetTarget();
bool is_python_class = !m_python_class_options.GetName().empty();
// Command-based hooks require at least one explicit trigger.
if (!is_python_class && !m_options.m_on_load && !m_options.m_on_unload &&
!m_options.m_on_stop) {
result.AppendError("at least one trigger must be specified: "
"--on-load (-L), --on-unload (-u), or --on-stop (-S)");
return;
}
Target::Hook::HookKind hook_kind =
is_python_class ? Target::Hook::HookKind::ScriptBased
: Target::Hook::HookKind::CommandBased;
Target::HookSP new_hook_sp = target.CreateHook(hook_kind);
if (!is_python_class) {
// Build trigger mask from explicit command-line flags.
auto *cmd_hook =
static_cast<Target::HookCommandLine *>(new_hook_sp.get());
uint32_t trigger_mask = 0;
if (m_options.m_on_load)
trigger_mask |= Target::Hook::kModulesLoaded;
if (m_options.m_on_unload)
trigger_mask |= Target::Hook::kModulesUnloaded;
if (m_options.m_on_stop)
trigger_mask |= Target::Hook::kProcessStop;
cmd_hook->SetTriggerMask(trigger_mask);
}
// Python class hooks: triggers are computed in SetScriptCallback based
// on which callback methods the class implements.
// Set up symbol context specifier if filter options were provided.
if (m_options.m_sym_ctx_specified) {
auto specifier_up = std::make_unique<SymbolContextSpecifier>(
GetDebugger().GetSelectedTarget());
if (!m_options.m_module_name.empty())
specifier_up->AddSpecification(
m_options.m_module_name.c_str(),
SymbolContextSpecifier::eModuleSpecified);
if (!m_options.m_class_name.empty())
specifier_up->AddSpecification(
m_options.m_class_name.c_str(),
SymbolContextSpecifier::eClassOrNamespaceSpecified);
if (!m_options.m_file_name.empty())
specifier_up->AddSpecification(m_options.m_file_name.c_str(),
SymbolContextSpecifier::eFileSpecified);
if (m_options.m_line_start != 0)
specifier_up->AddLineSpecification(
m_options.m_line_start,
SymbolContextSpecifier::eLineStartSpecified);
if (m_options.m_line_end != UINT_MAX)
specifier_up->AddLineSpecification(
m_options.m_line_end, SymbolContextSpecifier::eLineEndSpecified);
if (!m_options.m_function_name.empty())
specifier_up->AddSpecification(
m_options.m_function_name.c_str(),
SymbolContextSpecifier::eFunctionSpecified);
new_hook_sp->SetSCSpecifier(specifier_up.release());
}
// Set up thread specifier.
if (m_options.m_thread_specified) {
ThreadSpec *thread_spec = new ThreadSpec();
if (m_options.m_thread_id != LLDB_INVALID_THREAD_ID)
thread_spec->SetTID(m_options.m_thread_id);
if (m_options.m_thread_index != UINT32_MAX)
thread_spec->SetIndex(m_options.m_thread_index);
if (!m_options.m_thread_name.empty())
thread_spec->SetName(m_options.m_thread_name.c_str());
if (!m_options.m_queue_name.empty())
thread_spec->SetQueueName(m_options.m_queue_name.c_str());
new_hook_sp->SetThreadSpecifier(thread_spec);
}
new_hook_sp->SetAutoContinue(m_options.m_auto_continue);
new_hook_sp->SetRunAtInitialStop(m_options.m_at_initial_stop);
if (m_options.m_use_one_liner) {
auto *hook = static_cast<Target::HookCommandLine *>(new_hook_sp.get());
hook->SetActionFromStrings(m_options.m_one_liner);
result.AppendMessageWithFormatv("Hook #{0} added.\n",
new_hook_sp->GetID());
} else if (!m_python_class_options.GetName().empty()) {
auto *hook = static_cast<Target::HookScripted *>(new_hook_sp.get());
Status callback_error =
hook->SetScriptCallback(m_python_class_options.GetName(),
m_python_class_options.GetStructuredData());
if (callback_error.Fail()) {
result.AppendErrorWithFormat("couldn't add hook: %s",
callback_error.AsCString());
target.UndoCreateHook(new_hook_sp->GetID());
return;
}
result.AppendMessageWithFormatv("Hook #{0} added.\n",
new_hook_sp->GetID());
} else {
m_hook_sp = new_hook_sp;
m_interpreter.GetLLDBCommandsFromIOHandler("> ", // prompt
*this); // delegate
}
result.SetStatus(eReturnStatusSuccessFinishNoResult);
}
private:
CommandOptions m_options;
OptionGroupPythonClassWithDict m_python_class_options;
OptionGroupOptions m_all_options;
Target::HookSP m_hook_sp;
};
#pragma mark CommandObjectTargetHookDelete
class CommandObjectTargetHookDelete : public CommandObjectParsed {
public:
CommandObjectTargetHookDelete(CommandInterpreter &interpreter)
: CommandObjectParsed(interpreter, "target hook delete", "Delete a hook.",
"target hook delete [<id>]") {
AddSimpleArgumentList(eArgTypeStopHookID, eArgRepeatStar);
}
~CommandObjectTargetHookDelete() override = default;
protected:
void DoExecute(Args &command, CommandReturnObject &result) override {
Target &target = GetTarget();
if (command.GetArgumentCount() == 0) {
if (!m_interpreter.Confirm("Delete all hooks?", true)) {
result.SetStatus(eReturnStatusFailed);
return;
}
target.RemoveAllHooks();
result.SetStatus(eReturnStatusSuccessFinishNoResult);
return;
}
for (size_t i = 0; i < command.GetArgumentCount(); i++) {
lldb::user_id_t user_id;
if (!llvm::to_integer(command.GetArgumentAtIndex(i), user_id)) {
result.AppendErrorWithFormat("invalid hook id: \"%s\"",
command.GetArgumentAtIndex(i));
return;
}
if (!target.RemoveHookByID(user_id)) {
result.AppendErrorWithFormat("unknown hook id: \"%s\"",
command.GetArgumentAtIndex(i));
return;
}
}
result.SetStatus(eReturnStatusSuccessFinishNoResult);
}
};
#pragma mark CommandObjectTargetHookEnableDisable
class CommandObjectTargetHookEnableDisable : public CommandObjectParsed {
public:
CommandObjectTargetHookEnableDisable(CommandInterpreter &interpreter,
bool enable, const char *name,
const char *help, const char *syntax)
: CommandObjectParsed(interpreter, name, help, syntax), m_enable(enable) {
AddSimpleArgumentList(eArgTypeStopHookID, eArgRepeatStar);
}
~CommandObjectTargetHookEnableDisable() override = default;
protected:
void DoExecute(Args &command, CommandReturnObject &result) override {
Target &target = GetTarget();
// No IDs = apply to all hooks.
if (command.GetArgumentCount() == 0) {
target.SetAllHooksEnabledState(m_enable);
result.SetStatus(eReturnStatusSuccessFinishNoResult);
return;
}
for (size_t i = 0; i < command.GetArgumentCount(); i++) {
lldb::user_id_t user_id;
if (!llvm::to_integer(command.GetArgumentAtIndex(i), user_id)) {
result.AppendErrorWithFormat("invalid hook id: \"%s\"",
command.GetArgumentAtIndex(i));
return;
}
if (!target.SetHookEnabledStateByID(user_id, m_enable)) {
result.AppendErrorWithFormat("unknown hook id: \"%s\"",
command.GetArgumentAtIndex(i));
return;
}
}
result.SetStatus(eReturnStatusSuccessFinishNoResult);
}
private:
bool m_enable;
};
#pragma mark CommandObjectTargetHookModify
#define LLDB_OPTIONS_target_hook_modify
#include "CommandOptions.inc"
/// Modify trigger settings on a hook. Only valid for command-based hooks;
/// scripted hooks derive their triggers from the class methods.
class CommandObjectTargetHookModify : public CommandObjectParsed {
public:
class CommandOptions : public Options {
public:
CommandOptions() = default;
~CommandOptions() override = default;
llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
return llvm::ArrayRef(g_target_hook_modify_options);
}
Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
ExecutionContext *execution_context) override {
Status error;
const int short_option =
g_target_hook_modify_options[option_idx].short_option;
switch (short_option) {
case 'e':
m_enable_trigger = option_arg.str();
break;
case 'd':
m_disable_trigger = option_arg.str();
break;
default:
llvm_unreachable("unhandled option");
}
return error;
}
void OptionParsingStarting(ExecutionContext *execution_context) override {
m_enable_trigger.clear();
m_disable_trigger.clear();
}
std::string m_enable_trigger;
std::string m_disable_trigger;
};
CommandObjectTargetHookModify(CommandInterpreter &interpreter)
: CommandObjectParsed(interpreter, "target hook modify",
"Modify trigger settings on a hook.",
"target hook modify [--enable-trigger <name>] "
"[--disable-trigger <name>] [<id>]") {
AddSimpleArgumentList(eArgTypeStopHookID, eArgRepeatOptional);
SetHelpLong(R"help(
Modify trigger settings on command-based hooks. Scripted hooks derive their
triggers from the class methods and cannot be modified.
If no hook ID is given, the last added hook is modified.
Valid trigger names: load, unload, stop.
Examples:
target hook modify --enable-trigger stop 1
target hook modify --disable-trigger load 1
target hook modify --enable-trigger stop (modifies last added hook)
)help");
}
~CommandObjectTargetHookModify() override = default;
Options *GetOptions() override { return &m_options; }
protected:
static uint32_t ParseTriggerName(llvm::StringRef name) {
if (name == "load")
return Target::Hook::kModulesLoaded;
if (name == "unload")
return Target::Hook::kModulesUnloaded;
if (name == "stop")
return Target::Hook::kProcessStop;
return 0;
}
void DoExecute(Args &command, CommandReturnObject &result) override {
Target &target = GetTarget();
if (m_options.m_enable_trigger.empty() &&
m_options.m_disable_trigger.empty()) {
result.AppendError("at least one of --enable-trigger or "
"--disable-trigger must be specified");
return;
}
// Resolve the hook ID. Default to last added if not specified.
Target::HookSP hook_sp;
if (command.GetArgumentCount() == 0) {
size_t num_hooks = target.GetNumHooks();
if (num_hooks == 0) {
result.AppendError("no hooks exist");
return;
}
hook_sp = target.GetHookAtIndex(num_hooks - 1);
} else {
lldb::user_id_t user_id;
if (!llvm::to_integer(command.GetArgumentAtIndex(0), user_id)) {
result.AppendErrorWithFormat("invalid hook id: \"%s\"",
command.GetArgumentAtIndex(0));
return;
}
hook_sp = target.GetHookByID(user_id);
if (!hook_sp) {
result.AppendErrorWithFormat("unknown hook id: \"%s\"",
command.GetArgumentAtIndex(0));
return;
}
}
// Reject trigger modification on scripted hooks.
if (hook_sp->GetHookKind() != Target::Hook::HookKind::CommandBased) {
result.AppendError("cannot modify triggers on a scripted hook; "
"triggers are determined by the class methods");
return;
}
auto *cmd_hook = static_cast<Target::HookCommandLine *>(hook_sp.get());
if (!m_options.m_enable_trigger.empty()) {
uint32_t trigger = ParseTriggerName(m_options.m_enable_trigger);
if (!trigger) {
result.AppendErrorWithFormat("unknown trigger name: \"%s\". "
"Valid names: load, unload, stop",
m_options.m_enable_trigger.c_str());
return;
}
cmd_hook->AddTrigger(trigger);
}
if (!m_options.m_disable_trigger.empty()) {
uint32_t trigger = ParseTriggerName(m_options.m_disable_trigger);
if (!trigger) {
result.AppendErrorWithFormat("unknown trigger name: \"%s\". "
"Valid names: load, unload, stop",
m_options.m_disable_trigger.c_str());
return;
}
cmd_hook->RemoveTrigger(trigger);
}
result.SetStatus(eReturnStatusSuccessFinishNoResult);
}
private:
CommandOptions m_options;
};
#pragma mark CommandObjectTargetHookList
class CommandObjectTargetHookList : public CommandObjectParsed {
public:
CommandObjectTargetHookList(CommandInterpreter &interpreter)
: CommandObjectParsed(interpreter, "target hook list", "List all hooks.",
"target hook list") {}
~CommandObjectTargetHookList() override = default;
protected:
void DoExecute(Args &command, CommandReturnObject &result) override {
Target &target = GetTarget();
size_t num_hooks = target.GetNumHooks();
if (num_hooks == 0) {
result.GetOutputStream().PutCString("No hooks.\n");
} else {
for (size_t i = 0; i < num_hooks; i++) {
Target::HookSP hook_sp = target.GetHookAtIndex(i);
if (hook_sp)
hook_sp->GetDescription(result.GetOutputStream(),
eDescriptionLevelFull);
}
}
result.SetStatus(eReturnStatusSuccessFinishResult);
}
};
#pragma mark CommandObjectMultiwordTargetHooks
class CommandObjectMultiwordTargetHooks : public CommandObjectMultiword {
public:
CommandObjectMultiwordTargetHooks(CommandInterpreter &interpreter)
: CommandObjectMultiword(
interpreter, "target hook",
"Commands for operating on target hooks.",
"target hook <subcommand> [<subcommand-options>]") {
LoadSubCommand(
"add", CommandObjectSP(new CommandObjectTargetHookAdd(interpreter)));
LoadSubCommand("delete", CommandObjectSP(new CommandObjectTargetHookDelete(
interpreter)));
LoadSubCommand("disable",
CommandObjectSP(new CommandObjectTargetHookEnableDisable(
interpreter, false, "target hook disable",
"Disable a hook.", "target hook disable [<id> ...]")));
LoadSubCommand("enable",
CommandObjectSP(new CommandObjectTargetHookEnableDisable(
interpreter, true, "target hook enable",
"Enable a hook.", "target hook enable [<id> ...]")));
LoadSubCommand(
"list", CommandObjectSP(new CommandObjectTargetHookList(interpreter)));
LoadSubCommand("modify", CommandObjectSP(new CommandObjectTargetHookModify(
interpreter)));
}
~CommandObjectMultiwordTargetHooks() override = default;
};
#pragma mark CommandObjectTargetDumpTypesystem
/// Dumps the TypeSystem of the selected Target.
@ -5583,6 +6285,8 @@ CommandObjectMultiwordTarget::CommandObjectMultiwordTarget(
LoadSubCommand(
"stop-hook",
CommandObjectSP(new CommandObjectMultiwordTargetStopHooks(interpreter)));
LoadSubCommand("hook", CommandObjectSP(new CommandObjectMultiwordTargetHooks(
interpreter)));
LoadSubCommand("modules",
CommandObjectSP(new CommandObjectTargetModules(interpreter)));
LoadSubCommand("symbols",

View file

@ -1901,6 +1901,103 @@ let Command = "target stop_hook list" in {
Desc<"Show debugger ${i}nternal stop hooks.">;
}
let Command = "target hook add" in {
// Command-based options (Group 1 only -- mutually exclusive with -P).
def target_hook_add_one_liner
: Option<"one-liner", "o">,
Group<1>,
Arg<"OneLiner">,
Desc<"Add a command for the hook. Can be specified more than "
"once, and commands will be run in the order they appear.">;
def target_hook_add_on_load
: Option<"on-load", "L">,
Group<1>,
Desc<"Fire the hook when modules are ${L}oaded.">;
def target_hook_add_on_unload
: Option<"on-unload", "u">,
Group<1>,
Desc<"Fire the hook when modules are ${u}nloaded.">;
def target_hook_add_on_stop
: Option<"on-stop", "S">,
Group<1>,
Desc<"Fire the hook when the process ${S}tops.">;
// Shared options (both command-based and Python class hooks).
def target_hook_add_shlib
: Option<"shlib", "s">,
Arg<"ShlibName">,
Completion<"Module">,
Desc<"Set the module within which the stop handler is to be run.">;
def target_hook_add_thread_index
: Option<"thread-index", "x">,
Arg<"ThreadIndex">,
Desc<"The stop handler is run only for the thread whose inde${x} "
"matches this argument.">;
def target_hook_add_thread_id
: Option<"thread-id", "t">,
Arg<"ThreadID">,
Desc<"The stop handler is run only for the ${t}hread whose TID "
"matches this argument.">;
def target_hook_add_thread_name
: Option<"thread-name", "T">,
Arg<"ThreadName">,
Desc<"The stop handler is run only for the ${T}hread whose thread "
"name matches this argument.">;
def target_hook_add_queue_name
: Option<"queue-name", "q">,
Arg<"QueueName">,
Desc<"The stop handler is run only for threads in the ${q}ueue whose "
"name is given by this argument.">;
def target_hook_add_file
: Option<"file", "f">,
Arg<"Filename">,
Desc<"Specify the source ${f}ile within which the stop handler is "
"to be run.">,
Completion<"SourceFile">;
def target_hook_add_start_line
: Option<"start-line", "l">,
Arg<"LineNum">,
Desc<"Set the start of the ${l}ine range for which the stop handler "
"is to be run.">;
def target_hook_add_end_line
: Option<"end-line", "e">,
Arg<"LineNum">,
Desc<"Set the ${e}nd of the line range for which the stop handler "
"is to be run.">;
def target_hook_add_classname
: Option<"classname", "c">,
Arg<"ClassName">,
Desc<"Specify the ${c}lass within which the stop handler is to "
"be run.">;
def target_hook_add_name
: Option<"name", "n">,
Arg<"FunctionName">,
Desc<"Set the function ${n}ame within which the stop handler will "
"be run.">,
Completion<"Symbol">;
def target_hook_add_auto_continue
: Option<"auto-continue", "G">,
Arg<"Boolean">,
Desc<"The hook will auto-continue after running its stop handler.">;
def target_hook_add_at_initial_stop
: Option<"at-initial-stop", "I">,
Arg<"Boolean">,
Desc<"Whether the stop handler will trigger when lldb ${I}nitially "
"gains control of the process. Defaults to true.">;
}
let Command = "target hook modify" in {
def target_hook_modify_enable_trigger
: Option<"enable-trigger", "e">,
Arg<"Name">,
Desc<"Enable a trigger on the hook. Valid trigger names: load, "
"unload, stop.">;
def target_hook_modify_disable_trigger
: Option<"disable-trigger", "d">,
Arg<"Name">,
Desc<"Disable a trigger on the hook. Valid trigger names: load, "
"unload, stop.">;
}
let Command = "thread backtrace" in {
def thread_backtrace_count : Option<"count", "c">,
Group<1>,

View file

@ -31,6 +31,7 @@ add_lldb_library(lldbPluginScriptInterpreterPython PLUGIN
Interfaces/ScriptedProcessPythonInterface.cpp
Interfaces/ScriptedPythonInterface.cpp
Interfaces/ScriptedStopHookPythonInterface.cpp
Interfaces/ScriptedHookPythonInterface.cpp
Interfaces/ScriptedBreakpointPythonInterface.cpp
Interfaces/ScriptedThreadPlanPythonInterface.cpp
Interfaces/ScriptedThreadPythonInterface.cpp

View file

@ -24,6 +24,7 @@ void ScriptInterpreterPythonInterfaces::Initialize() {
ScriptedPlatformPythonInterface::Initialize();
ScriptedProcessPythonInterface::Initialize();
ScriptedStopHookPythonInterface::Initialize();
ScriptedHookPythonInterface::Initialize();
ScriptedBreakpointPythonInterface::Initialize();
ScriptedThreadPlanPythonInterface::Initialize();
ScriptedFrameProviderPythonInterface::Initialize();
@ -34,6 +35,7 @@ void ScriptInterpreterPythonInterfaces::Terminate() {
ScriptedPlatformPythonInterface::Terminate();
ScriptedProcessPythonInterface::Terminate();
ScriptedStopHookPythonInterface::Terminate();
ScriptedHookPythonInterface::Terminate();
ScriptedBreakpointPythonInterface::Terminate();
ScriptedThreadPlanPythonInterface::Terminate();
ScriptedFrameProviderPythonInterface::Terminate();

View file

@ -16,6 +16,7 @@
#include "ScriptedBreakpointPythonInterface.h"
#include "ScriptedFrameProviderPythonInterface.h"
#include "ScriptedFramePythonInterface.h"
#include "ScriptedHookPythonInterface.h"
#include "ScriptedPlatformPythonInterface.h"
#include "ScriptedProcessPythonInterface.h"
#include "ScriptedStopHookPythonInterface.h"

View file

@ -0,0 +1,105 @@
//===-- ScriptedHookPythonInterface.cpp -----------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "lldb/Core/PluginManager.h"
#include "lldb/Target/ExecutionContext.h"
#include "lldb/Utility/Log.h"
#include "lldb/lldb-enumerations.h"
#include "../SWIGPythonBridge.h"
#include "../ScriptInterpreterPythonImpl.h"
#include "../lldb-python.h"
#include "ScriptedHookPythonInterface.h"
using namespace lldb;
using namespace lldb_private;
using namespace lldb_private::python;
ScriptedHookPythonInterface::ScriptedHookPythonInterface(
ScriptInterpreterPythonImpl &interpreter)
: ScriptedHookInterface(), ScriptedPythonInterface(interpreter) {}
ScriptedHookInterface::SupportedHookMethods
ScriptedHookPythonInterface::GetSupportedMethods() {
SupportedHookMethods methods;
// Qualify through ScriptedPythonInterface to resolve the diamond
// inheritance (both ScriptedHookInterface and ScriptedPythonInterface
// inherit ScriptedInterface which owns m_object_instance_sp).
auto &obj_sp = ScriptedPythonInterface::m_object_instance_sp;
if (!obj_sp)
return methods;
using Locker = ScriptInterpreterPythonImpl::Locker;
Locker py_lock(&m_interpreter, Locker::AcquireLock | Locker::NoSTDIN,
Locker::FreeLock);
PythonObject implementor(PyRefType::Borrowed, (PyObject *)obj_sp->GetValue());
if (!implementor.IsValid())
return methods;
methods.handle_module_loaded =
implementor.HasAttribute("handle_module_loaded");
methods.handle_module_unloaded =
implementor.HasAttribute("handle_module_unloaded");
methods.handle_stop = implementor.HasAttribute("handle_stop");
return methods;
}
llvm::Expected<StructuredData::GenericSP>
ScriptedHookPythonInterface::CreatePluginObject(
llvm::StringRef class_name, lldb::TargetSP target_sp,
const StructuredDataImpl &args_sp) {
return ScriptedPythonInterface::CreatePluginObject(class_name, nullptr,
target_sp, args_sp);
}
void ScriptedHookPythonInterface::HandleModuleLoaded(
lldb::StreamSP &output_sp) {
Status error;
Dispatch("handle_module_loaded", error, output_sp);
}
void ScriptedHookPythonInterface::HandleModuleUnloaded(
lldb::StreamSP &output_sp) {
Status error;
Dispatch("handle_module_unloaded", error, output_sp);
}
llvm::Expected<bool>
ScriptedHookPythonInterface::HandleStop(ExecutionContext &exe_ctx,
lldb::StreamSP &output_sp) {
ExecutionContextRefSP exe_ctx_ref_sp =
std::make_shared<ExecutionContextRef>(exe_ctx);
Status error;
StructuredData::ObjectSP obj =
Dispatch("handle_stop", error, exe_ctx_ref_sp, output_sp);
if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
error)) {
if (!obj)
return true;
return error.ToError();
}
return obj->GetBooleanValue();
}
void ScriptedHookPythonInterface::Initialize() {
const std::vector<llvm::StringRef> ci_usages = {
"target hook add -P <script-name> [-k key -v value ...]"};
const std::vector<llvm::StringRef> api_usages = {};
PluginManager::RegisterPlugin(
GetPluginNameStatic(),
llvm::StringRef("Perform actions on target lifecycle events (module "
"load/unload, process stop)."),
CreateInstance, eScriptLanguagePython, {ci_usages, api_usages});
}
void ScriptedHookPythonInterface::Terminate() {
PluginManager::UnregisterPlugin(CreateInstance);
}

View file

@ -0,0 +1,54 @@
//===-- ScriptedHookPythonInterface.h ---------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#ifndef LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDHOOKPYTHONINTERFACE_H
#define LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDHOOKPYTHONINTERFACE_H
#include "lldb/Interpreter/Interfaces/ScriptedHookInterface.h"
#include "ScriptedPythonInterface.h"
namespace lldb_private {
class ScriptedHookPythonInterface : public ScriptedHookInterface,
public ScriptedPythonInterface,
public PluginInterface {
public:
ScriptedHookPythonInterface(ScriptInterpreterPythonImpl &interpreter);
llvm::Expected<StructuredData::GenericSP>
CreatePluginObject(llvm::StringRef class_name, lldb::TargetSP target_sp,
const StructuredDataImpl &args_sp) override;
/// A hook class must implement at least one callback. All three are
/// individually optional; hooks that implement none will be rejected
/// at creation time.
llvm::SmallVector<AbstractMethodRequirement>
GetAbstractMethodRequirements() const override {
return {};
}
/// Check which of the three hook methods the Python class implements.
SupportedHookMethods GetSupportedMethods() override;
void HandleModuleLoaded(lldb::StreamSP &output_sp) override;
void HandleModuleUnloaded(lldb::StreamSP &output_sp) override;
llvm::Expected<bool> HandleStop(ExecutionContext &exe_ctx,
lldb::StreamSP &output_sp) override;
static void Initialize();
static void Terminate();
static llvm::StringRef GetPluginNameStatic() {
return "ScriptedHookPythonInterface";
}
llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
};
} // namespace lldb_private
#endif // LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDHOOKPYTHONINTERFACE_H

View file

@ -1525,6 +1525,11 @@ ScriptInterpreterPythonImpl::CreateScriptedStopHookInterface() {
return std::make_shared<ScriptedStopHookPythonInterface>(*this);
}
ScriptedHookInterfaceSP
ScriptInterpreterPythonImpl::CreateScriptedHookInterface() {
return std::make_shared<ScriptedHookPythonInterface>(*this);
}
ScriptedBreakpointInterfaceSP
ScriptInterpreterPythonImpl::CreateScriptedBreakpointInterface() {
return std::make_shared<ScriptedBreakpointPythonInterface>(*this);

View file

@ -90,6 +90,8 @@ public:
lldb::ScriptedStopHookInterfaceSP CreateScriptedStopHookInterface() override;
lldb::ScriptedHookInterfaceSP CreateScriptedHookInterface() override;
lldb::ScriptedBreakpointInterfaceSP
CreateScriptedBreakpointInterface() override;

View file

@ -38,6 +38,7 @@
#include "lldb/Interpreter/CommandInterpreter.h"
#include "lldb/Interpreter/CommandReturnObject.h"
#include "lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h"
#include "lldb/Interpreter/Interfaces/ScriptedHookInterface.h"
#include "lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h"
#include "lldb/Interpreter/OptionGroupWatchpoint.h"
#include "lldb/Interpreter/OptionValueEnumeration.h"
@ -222,6 +223,8 @@ void Target::PrimeFromDummyTarget(Target &target) {
m_stop_hooks = target.m_stop_hooks;
m_stop_hook_next_id = target.m_stop_hook_next_id;
m_internal_stop_hooks = target.m_internal_stop_hooks;
m_hooks = target.m_hooks;
m_hook_next_id = target.m_hook_next_id;
for (const auto &breakpoint_sp : target.m_breakpoint_list.Breakpoints()) {
if (breakpoint_sp->IsInternal())
@ -1859,6 +1862,7 @@ void Target::ModulesDidLoad(ModuleList &module_list) {
if (m_process_sp) {
m_process_sp->ModulesDidLoad(module_list);
}
RunModuleHooks(/*is_load=*/true);
auto data_sp =
std::make_shared<TargetEventData>(shared_from_this(), module_list);
BroadcastEvent(eBroadcastBitModulesLoaded, data_sp);
@ -1918,6 +1922,8 @@ void Target::ModulesDidUnload(ModuleList &module_list, bool delete_locations) {
if (should_flush_type_systems)
m_scratch_type_system_map.Clear();
RunModuleHooks(/*is_load=*/false);
}
}
@ -3149,7 +3155,16 @@ bool Target::RunStopHooks(bool at_initial_stop) {
if (is_active(hook))
active_hooks.push_back(hook);
}
if (active_hooks.empty())
// Also collect unified hooks that fire on process stop.
std::vector<HookSP> active_unified_hooks;
for (auto &[_, hook] : m_hooks) {
if (hook->IsEnabled() && hook->FiresOn(Hook::kProcessStop) &&
(!at_initial_stop || hook->GetRunAtInitialStop()))
active_unified_hooks.push_back(hook);
}
if (active_hooks.empty() && active_unified_hooks.empty())
return false;
// Make sure we check that we are not stopped because of us running a user
@ -3200,6 +3215,8 @@ bool Target::RunStopHooks(bool at_initial_stop) {
size_t num_hooks_with_output = llvm::count_if(
active_hooks, [](auto h) { return !h->GetSuppressOutput(); });
num_hooks_with_output += llvm::count_if(
active_unified_hooks, [](auto h) { return !h->GetSuppressOutput(); });
bool print_hook_header = (num_hooks_with_output > 1);
bool print_thread_header = (num_exe_ctx > 1);
bool should_stop = false;
@ -3263,6 +3280,53 @@ bool Target::RunStopHooks(bool at_initial_stop) {
}
}
// Run unified hooks that fire on process stop.
for (auto cur_hook_sp : active_unified_hooks) {
bool any_thread_matched = false;
for (auto exc_ctx : exc_ctx_with_reasons) {
if (!cur_hook_sp->ExecutionContextPasses(exc_ctx))
continue;
bool suppress_output = cur_hook_sp->GetSuppressOutput();
if (print_hook_header && !any_thread_matched && !suppress_output) {
StreamString s;
cur_hook_sp->GetDescription(s, eDescriptionLevelBrief);
if (s.GetSize() != 0)
output_sp->Printf("\n- Hook %" PRIu64 " (%s)\n", cur_hook_sp->GetID(),
s.GetData());
else
output_sp->Printf("\n- Hook %" PRIu64 "\n", cur_hook_sp->GetID());
any_thread_matched = true;
}
if (print_thread_header && !suppress_output)
output_sp->Printf("-- Thread %d\n",
exc_ctx.GetThreadPtr()->GetIndexID());
auto result = cur_hook_sp->HandleStop(exc_ctx, output_sp);
switch (result) {
case StopHook::StopHookResult::KeepStopped:
if (cur_hook_sp->GetAutoContinue())
requested_continue = true;
else
should_stop = true;
break;
case StopHook::StopHookResult::RequestContinue:
requested_continue = true;
break;
case StopHook::StopHookResult::NoPreference:
break;
case StopHook::StopHookResult::AlreadyContinued:
output_sp->Printf("\nAborting stop hooks, hook %" PRIu64
" set the program running.\n"
" Consider using '-G true' to make "
"stop hooks auto-continue.\n",
cur_hook_sp->GetID());
return true;
}
}
}
// Resume iff at least one hook requested to continue and no hook asked to
// stop.
if (requested_continue && !should_stop) {
@ -4249,6 +4313,427 @@ void Target::StopHookScripted::GetSubclassDescription(
as_dict->ForEach(print_one_element);
}
// Hook
Target::Hook::Hook(lldb::TargetSP target_sp, lldb::user_id_t uid, HookKind kind)
: UserID(uid), m_target_sp(std::move(target_sp)), m_kind(kind) {}
Target::Hook::Hook(const Hook &rhs)
: UserID(rhs.GetID()), m_target_sp(rhs.m_target_sp), m_kind(rhs.m_kind),
m_enabled(rhs.m_enabled), m_trigger_mask(rhs.m_trigger_mask),
m_sc_specifier_sp(rhs.m_sc_specifier_sp),
m_at_initial_stop(rhs.m_at_initial_stop),
m_auto_continue(rhs.m_auto_continue),
m_suppress_output(rhs.m_suppress_output) {
if (rhs.m_thread_spec_up)
m_thread_spec_up = std::make_unique<ThreadSpec>(*rhs.m_thread_spec_up);
}
void Target::Hook::SetSCSpecifier(SymbolContextSpecifier *specifier) {
m_sc_specifier_sp.reset(specifier);
}
void Target::Hook::SetThreadSpecifier(ThreadSpec *specifier) {
m_thread_spec_up.reset(specifier);
}
bool Target::Hook::ExecutionContextPasses(const ExecutionContext &exc_ctx) {
SymbolContextSpecifier *specifier = GetSCSpecifier();
if (!specifier)
return true;
bool will_run = true;
if (exc_ctx.GetFramePtr())
will_run = specifier->SymbolContextMatches(
exc_ctx.GetFramePtr()->GetSymbolContext(eSymbolContextEverything));
if (will_run && GetThreadSpecifier() != nullptr)
will_run =
GetThreadSpecifier()->ThreadPassesBasicTests(exc_ctx.GetThreadRef());
return will_run;
}
void Target::Hook::GetDescription(Stream &s,
lldb::DescriptionLevel level) const {
s.Printf("Hook: %" PRIu64 "\n", GetID());
if (level == eDescriptionLevelBrief)
return;
s.IndentMore();
s.Indent();
s.Printf("State: %s\n", m_enabled ? "enabled" : "disabled");
{
std::string fires_on;
if (m_trigger_mask & kModulesLoaded)
fires_on += "load";
if (m_trigger_mask & kModulesUnloaded) {
if (!fires_on.empty())
fires_on += ", ";
fires_on += "unload";
}
if (m_trigger_mask & kProcessStop) {
if (!fires_on.empty())
fires_on += ", ";
fires_on += "stop";
}
if (!fires_on.empty()) {
s.Indent();
s.Printf("Triggers: %s\n", fires_on.c_str());
}
}
// Subclasses add their content (commands or class) then call
// GetFilterDescription to print filters.
s.IndentLess();
}
void Target::Hook::GetFilterDescription(Stream &s,
lldb::DescriptionLevel level) const {
s.IndentMore();
if (m_auto_continue)
s.Indent("AutoContinue on\n");
if (m_sc_specifier_sp) {
s.Indent();
s.PutCString("Specifier:\n");
s.IndentMore();
m_sc_specifier_sp->GetDescription(&s, level);
s.IndentLess();
}
if (m_thread_spec_up) {
StreamString tmp;
s.Indent("Thread:\n");
m_thread_spec_up->GetDescription(&tmp, level);
s.IndentMore();
s.Indent(tmp.GetString());
s.PutCString("\n");
s.IndentLess();
}
s.IndentLess();
}
void Target::HookCommandLine::GetDescription(
Stream &s, lldb::DescriptionLevel level) const {
Hook::GetDescription(s, level);
if (level == eDescriptionLevelBrief) {
if (m_commands.GetSize() == 1)
s.PutCString(m_commands.GetStringAtIndex(0));
else
s.Printf("%" PRIu64 " commands", (uint64_t)m_commands.GetSize());
return;
}
// Commands come after the header (ID, State, Triggers) but before filters.
s.IndentMore();
s.Indent("Commands: \n");
s.IndentMore();
for (uint32_t i = 0; i < m_commands.GetSize(); i++) {
s.Indent(m_commands.GetStringAtIndex(i));
s.PutCString("\n");
}
s.IndentLess();
s.IndentLess();
GetFilterDescription(s, level);
}
// HookCommandLine
void Target::HookCommandLine::SetActionFromString(const std::string &string) {
GetCommands().SplitIntoLines(string);
}
void Target::HookCommandLine::SetActionFromStrings(
const std::vector<std::string> &strings) {
for (const auto &string : strings)
GetCommands().AppendString(string.c_str());
}
void Target::HookCommandLine::HandleModuleLoaded(StreamSP output_sp) {
if (!m_commands.GetSize())
return;
TargetSP target_sp = GetTarget();
if (!target_sp)
return;
CommandReturnObject result(false);
result.SetImmediateOutputStream(output_sp);
result.SetInteractive(false);
Debugger &debugger = target_sp->GetDebugger();
ExecutionContext exe_ctx;
if (target_sp->GetProcessSP())
exe_ctx.SetContext(target_sp->GetProcessSP());
else
exe_ctx.SetContext(target_sp, false);
CommandInterpreterRunOptions options;
options.SetStopOnContinue(true);
options.SetStopOnError(true);
options.SetEchoCommands(false);
options.SetPrintResults(true);
options.SetPrintErrors(true);
options.SetAddToHistory(false);
bool old_async = debugger.GetAsyncExecution();
debugger.SetAsyncExecution(true);
debugger.GetCommandInterpreter().HandleCommands(GetCommands(), exe_ctx,
options, result);
debugger.SetAsyncExecution(old_async);
}
void Target::HookCommandLine::HandleModuleUnloaded(StreamSP output_sp) {
// Command-based hooks run the same commands on unload as on load.
HandleModuleLoaded(output_sp);
}
Target::StopHook::StopHookResult
Target::HookCommandLine::HandleStop(ExecutionContext &exc_ctx,
StreamSP output_sp) {
assert(exc_ctx.GetTargetPtr() && "Can't call HandleStop on a context "
"with no target");
if (!m_commands.GetSize())
return StopHook::StopHookResult::KeepStopped;
CommandReturnObject result(false);
result.SetImmediateOutputStream(output_sp);
result.SetInteractive(false);
Debugger &debugger = exc_ctx.GetTargetPtr()->GetDebugger();
CommandInterpreterRunOptions options;
options.SetStopOnContinue(true);
options.SetStopOnError(true);
options.SetEchoCommands(false);
options.SetPrintResults(true);
options.SetPrintErrors(true);
options.SetAddToHistory(false);
bool old_async = debugger.GetAsyncExecution();
debugger.SetAsyncExecution(true);
debugger.GetCommandInterpreter().HandleCommands(GetCommands(), exc_ctx,
options, result);
debugger.SetAsyncExecution(old_async);
lldb::ReturnStatus status = result.GetStatus();
if (status == eReturnStatusSuccessContinuingNoResult ||
status == eReturnStatusSuccessContinuingResult)
return StopHook::StopHookResult::AlreadyContinued;
return StopHook::StopHookResult::KeepStopped;
}
// HookScripted
Status Target::HookScripted::SetScriptCallback(
std::string class_name, StructuredData::ObjectSP extra_args_sp) {
ScriptInterpreter *script_interp =
GetTarget()->GetDebugger().GetScriptInterpreter();
if (!script_interp)
return Status::FromErrorString("No script interpreter installed.");
m_interface_sp = script_interp->CreateScriptedHookInterface();
if (!m_interface_sp)
return Status::FromErrorStringWithFormat(
"ScriptedHook::%s () - ERROR: %s", __FUNCTION__,
"Script interpreter couldn't create Scripted Hook Interface");
m_class_name = std::move(class_name);
m_extra_args.SetObjectSP(extra_args_sp);
auto obj_or_err = m_interface_sp->CreatePluginObject(
m_class_name, GetTarget(), m_extra_args);
if (!obj_or_err)
return Status::FromError(obj_or_err.takeError());
StructuredData::ObjectSP object_sp = *obj_or_err;
if (!object_sp || !object_sp->IsValid())
return Status::FromErrorStringWithFormat(
"ScriptedHook::%s () - ERROR: %s", __FUNCTION__,
"Failed to create valid script object");
// Determine which triggers the class supports by checking which callback
// methods it implements.
auto methods = m_interface_sp->GetSupportedMethods();
if (!methods.any())
return Status::FromErrorString(
"hook class implements none of the expected methods "
"(handle_module_loaded, handle_module_unloaded, handle_stop)");
if (methods.handle_module_loaded)
m_trigger_mask |= kModulesLoaded;
if (methods.handle_module_unloaded)
m_trigger_mask |= kModulesUnloaded;
if (methods.handle_stop)
m_trigger_mask |= kProcessStop;
return {};
}
void Target::HookScripted::HandleModuleLoaded(StreamSP output_sp) {
if (!m_interface_sp)
return;
StreamSP stream = std::make_shared<StreamString>();
m_interface_sp->HandleModuleLoaded(stream);
output_sp->PutCString(static_cast<StreamString *>(stream.get())->GetData());
}
void Target::HookScripted::HandleModuleUnloaded(StreamSP output_sp) {
if (!m_interface_sp)
return;
StreamSP stream = std::make_shared<StreamString>();
m_interface_sp->HandleModuleUnloaded(stream);
output_sp->PutCString(static_cast<StreamString *>(stream.get())->GetData());
}
Target::StopHook::StopHookResult
Target::HookScripted::HandleStop(ExecutionContext &exc_ctx,
StreamSP output_sp) {
assert(exc_ctx.GetTargetPtr() && "Can't call HandleStop on a context "
"with no target");
if (!m_interface_sp)
return StopHook::StopHookResult::KeepStopped;
lldb::StreamSP stream = std::make_shared<lldb_private::StreamString>();
auto should_stop_or_err = m_interface_sp->HandleStop(exc_ctx, stream);
output_sp->PutCString(static_cast<StreamString *>(stream.get())->GetData());
if (!should_stop_or_err)
return StopHook::StopHookResult::KeepStopped;
return *should_stop_or_err ? StopHook::StopHookResult::KeepStopped
: StopHook::StopHookResult::RequestContinue;
}
void Target::HookScripted::GetDescription(Stream &s,
lldb::DescriptionLevel level) const {
Hook::GetDescription(s, level);
if (level == eDescriptionLevelBrief) {
s.PutCString(m_class_name);
return;
}
// Class and args come after the header (ID, State, Triggers) but before
// filters.
s.IndentMore();
s.Indent("Class: ");
s.Printf("%s\n", m_class_name.c_str());
if (m_extra_args.IsValid()) {
StructuredData::ObjectSP object_sp = m_extra_args.GetObjectSP();
if (object_sp && object_sp->IsValid()) {
StructuredData::Dictionary *as_dict = object_sp->GetAsDictionary();
if (as_dict && as_dict->IsValid() && as_dict->GetSize() > 0) {
s.Indent("Args:\n");
s.IndentMore();
auto print_one_element = [&s](llvm::StringRef key,
StructuredData::Object *object) {
s.Indent();
s.Format("{0} : {1}\n", key, object->GetStringValue());
return true;
};
as_dict->ForEach(print_one_element);
s.IndentLess();
}
}
}
s.IndentLess();
GetFilterDescription(s, level);
}
// Hook management methods
Target::HookSP Target::CreateHook(Hook::HookKind kind) {
lldb::user_id_t new_uid = ++m_hook_next_id;
HookSP hook_sp;
switch (kind) {
case Hook::HookKind::CommandBased:
hook_sp.reset(new HookCommandLine(shared_from_this(), new_uid));
break;
case Hook::HookKind::ScriptBased:
hook_sp.reset(new HookScripted(shared_from_this(), new_uid));
break;
}
m_hooks[new_uid] = hook_sp;
return hook_sp;
}
void Target::UndoCreateHook(lldb::user_id_t uid) {
if (!RemoveHookByID(uid))
return;
if (uid > 0)
--m_hook_next_id;
}
bool Target::RemoveHookByID(lldb::user_id_t uid) {
size_t num_removed = m_hooks.erase(uid);
return (num_removed != 0);
}
void Target::RemoveAllHooks() { m_hooks.clear(); }
Target::HookSP Target::GetHookByID(lldb::user_id_t uid) {
auto iter = m_hooks.find(uid);
if (iter == m_hooks.end())
return {};
return iter->second;
}
Target::HookSP Target::GetHookAtIndex(size_t index) {
if (index >= m_hooks.size())
return {};
auto iter = m_hooks.begin();
std::advance(iter, index);
return iter->second;
}
bool Target::SetHookEnabledStateByID(lldb::user_id_t uid, bool enabled) {
auto iter = m_hooks.find(uid);
if (iter == m_hooks.end())
return false;
iter->second->SetIsEnabled(enabled);
return true;
}
void Target::SetAllHooksEnabledState(bool enabled) {
for (auto &[_, hook] : m_hooks)
hook->SetIsEnabled(enabled);
}
void Target::RunModuleHooks(bool is_load) {
if (m_hooks.empty())
return;
uint32_t trigger = is_load ? Hook::kModulesLoaded : Hook::kModulesUnloaded;
// Copy active hooks into a local vector before iterating, in case a
// callback modifies m_hooks (same pattern as RunStopHooks).
std::vector<HookSP> active_hooks;
for (auto &[_, hook_sp] : m_hooks) {
if (hook_sp->IsEnabled() && hook_sp->FiresOn(trigger))
active_hooks.push_back(hook_sp);
}
if (active_hooks.empty())
return;
StreamSP output_sp = m_debugger.GetAsyncOutputStream();
for (auto &hook_sp : active_hooks) {
if (is_load)
hook_sp->HandleModuleLoaded(output_sp);
else
hook_sp->HandleModuleUnloaded(output_sp);
}
output_sp->Flush();
}
static constexpr OptionEnumValueElement g_dynamic_value_types[] = {
{
eNoDynamicValues,

View file

@ -0,0 +1,19 @@
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
class TestCase(TestBase):
@no_debug_info_test
def test_invalid_arg(self):
self.expect(
"target hook delete -1",
error=True,
startstr='error: invalid hook id: "-1"',
)
self.expect(
"target hook delete abcdfx",
error=True,
startstr='error: invalid hook id: "abcdfx"',
)

View file

@ -0,0 +1,19 @@
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
class TestCase(TestBase):
@no_debug_info_test
def test_invalid_arg(self):
self.expect(
"target hook disable -1",
error=True,
startstr='error: invalid hook id: "-1"',
)
self.expect(
"target hook disable abcdfx",
error=True,
startstr='error: invalid hook id: "abcdfx"',
)

View file

@ -0,0 +1,19 @@
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
class TestCase(TestBase):
@no_debug_info_test
def test_invalid_arg(self):
self.expect(
"target hook enable -1",
error=True,
startstr='error: invalid hook id: "-1"',
)
self.expect(
"target hook enable abcdfx",
error=True,
startstr='error: invalid hook id: "abcdfx"',
)

View file

@ -0,0 +1,10 @@
# Test that module hooks fire when modules are loaded.
#
# RUN: %clang_host -g %S/Inputs/main.c -o %t
# RUN: %lldb -b -o 'file %t' \
# RUN: -o 'target hook add -L -o "script print(\"HOOK_FIRED\")"' \
# RUN: -o 'target modules add %t' \
# RUN: 2>&1 | FileCheck %s
# CHECK: Hook #1 added.
# CHECK: HOOK_FIRED

View file

@ -0,0 +1,168 @@
# Test hook add/list/delete/enable/disable commands.
#
# RUN: %lldb -b -s %s 2>&1 | FileCheck %s
# Adding hooks assigns incrementing IDs.
target hook add -L -o "script print('hook1')"
# CHECK: Hook #1 added.
target hook add -L -o "script print('hook2')"
# CHECK: Hook #2 added.
target hook add -L -o "script print('hook3')"
# CHECK: Hook #3 added.
# List all hooks.
target hook list
# CHECK: (lldb) target hook list
# CHECK: Hook: 1
# CHECK: State: enabled
# CHECK: Triggers: load
# CHECK: Commands:
# CHECK: script print('hook1')
# CHECK: Hook: 2
# CHECK: State: enabled
# CHECK: Triggers: load
# CHECK: Commands:
# CHECK: script print('hook2')
# CHECK: Hook: 3
# CHECK: State: enabled
# CHECK: Triggers: load
# CHECK: Commands:
# CHECK: script print('hook3')
# Disable a hook.
target hook disable 2
target hook list
# CHECK: (lldb) target hook list
# CHECK: Hook: 1
# CHECK: State: enabled
# CHECK: Hook: 2
# CHECK: State: disabled
# CHECK: Hook: 3
# CHECK: State: enabled
# Re-enable the hook.
target hook enable 2
target hook list
# CHECK: (lldb) target hook list
# CHECK: Hook: 1
# CHECK: State: enabled
# CHECK: Hook: 2
# CHECK: State: enabled
# CHECK: Hook: 3
# CHECK: State: enabled
# Delete a hook in the middle.
target hook delete 2
target hook list
# CHECK: (lldb) target hook list
# CHECK: Hook: 1
# CHECK-NOT: Hook: 2
# CHECK: Hook: 3
# New hook gets a new ID (not reused).
target hook add -L -o "script print('hook4')"
# CHECK: Hook #4 added.
target hook list
# CHECK: (lldb) target hook list
# CHECK: Hook: 1
# CHECK-NOT: Hook: 2
# CHECK: Hook: 3
# CHECK: Hook: 4
# Delete all hooks.
settings set auto-confirm true
target hook delete
target hook list
# CHECK: (lldb) target hook list
# CHECK: No hooks.
# Add a hook with --on-load and --on-unload.
target hook add -L -u -o "script print('load+unload')"
# CHECK: Hook #5 added.
target hook list
# CHECK: (lldb) target hook list
# CHECK: Hook: 5
# CHECK: State: enabled
# CHECK: Triggers: load, unload
# CHECK: Commands:
# CHECK: script print('load+unload')
# Add a hook with multiple one-liner commands.
target hook add -L -o "script print('first')" -o "script print('second')"
# CHECK: Hook #6 added.
target hook list
# CHECK: Hook: 6
# CHECK: State: enabled
# CHECK: Triggers: load
# CHECK: Commands:
# CHECK: script print('first')
# CHECK: script print('second')
# Disable all hooks.
target hook disable
target hook list
# CHECK: (lldb) target hook list
# CHECK: Hook: 5
# CHECK: State: disabled
# CHECK: Hook: 6
# CHECK: State: disabled
# Enable all hooks.
target hook enable
target hook list
# CHECK: (lldb) target hook list
# CHECK: Hook: 5
# CHECK: State: enabled
# CHECK: Hook: 6
# CHECK: State: enabled
# Add a hook with --on-stop only.
target hook delete
target hook add -S -o "bt"
# CHECK: Hook #7 added.
target hook list
# CHECK: (lldb) target hook list
# CHECK: Hook: 7
# CHECK: State: enabled
# CHECK: Triggers: stop
# CHECK: Commands:
# CHECK: bt
# Add a hook with all triggers.
target hook add -L -u -S -o "script print('all events')"
# CHECK: Hook #8 added.
target hook list
# CHECK: Hook: 8
# CHECK: State: enabled
# CHECK: Triggers: load, unload, stop
# Add a hook with --on-stop and inline filter.
target hook delete
target hook add -S -s mylib.so -o "bt"
# CHECK: Hook #9 added.
target hook list
# CHECK: (lldb) target hook list
# CHECK: Hook: 9
# CHECK: State: enabled
# CHECK: Triggers: stop
# CHECK: Commands:
# CHECK: bt
# CHECK: Specifier:
# CHECK: Module: mylib.so
# Per-trigger disable: remove stop from hook #9 using modify.
target hook modify --disable-trigger stop 9
target hook list
# CHECK: (lldb) target hook list
# CHECK: Hook: 9
# CHECK: State: enabled
# CHECK-NOT: Triggers: stop
# CHECK: Commands:
# Per-trigger enable: re-add stop to hook #9 using modify.
target hook modify --enable-trigger stop 9
target hook list
# CHECK: (lldb) target hook list
# CHECK: Hook: 9
# CHECK: State: enabled
# CHECK: Triggers: stop

View file

@ -0,0 +1 @@
target hook add -S -n b -o "expr ptr"

View file

@ -0,0 +1,48 @@
# Test that stop-hooks added via "target hook add -S" work the same as
# "target stop-hook add".
#
# RUN: %clang_host %p/Inputs/stop-hook.c -g -o %t
# RUN: %lldb -b -s %p/Inputs/stop-hook-unified-1.lldbinit -s %s -f %t \
# RUN: | FileCheck %s
# UNSUPPORTED: system-windows
break set -f stop-hook.c -p "// Set breakpoint here to test target stop-hook"
break set -f stop-hook.c -p "// Another breakpoint which is outside of the stop-hook range"
target hook list
# CHECK: Hook: 1
# CHECK-NEXT: State: enabled
# CHECK-NEXT: Triggers: stop
# CHECK-NEXT: Commands:
# CHECK-NEXT: expr ptr
# CHECK-NEXT: Specifier:
# CHECK-NEXT: Function: b.
run
# Stopping inside of the stop hook range
# CHECK: (lldb) run
# CHECK-NEXT: (void *) ${{.*}} = 0x
thread step-over
# Stepping inside of the stop hook range
# CHECK: (lldb) thread step-over
# CHECK-NEXT: (void *) ${{.*}} = 0x
# CHECK: ->{{.*}} // We should stop here after stepping.
process continue
# Stopping outside of the stop hook range
# CHECK: (lldb) process continue
# CHECK-NOT: (void *)
# CHECK: ->{{.*}} // Another breakpoint which is outside of the stop-hook range.
thread step-over
# Stepping inside of the stop hook range
# CHECK: (lldb) thread step-over
# CHECK-NOT: (void *)
settings set auto-confirm true
target hook delete
target hook list
# CHECK: (lldb) target hook list
# CHECK: No hooks.