[Lldb-commits] [lldb] fe61b38 - Add a Debugger interruption mechanism in conjunction with the
Jim Ingham via lldb-commits
lldb-commits at lists.llvm.org
Wed Mar 15 16:45:24 PDT 2023
Author: Jim Ingham
Date: 2023-03-15T16:45:14-07:00
New Revision: fe61b38258bf4c5f34c32de26f4ed11ef5c32ebc
URL: https://github.com/llvm/llvm-project/commit/fe61b38258bf4c5f34c32de26f4ed11ef5c32ebc
DIFF: https://github.com/llvm/llvm-project/commit/fe61b38258bf4c5f34c32de26f4ed11ef5c32ebc.diff
LOG: Add a Debugger interruption mechanism in conjunction with the
Command Interpreter mechanism.
Differential Revision: https://reviews.llvm.org/D145136
Added:
lldb/test/API/python_api/was_interrupted/Makefile
lldb/test/API/python_api/was_interrupted/TestDebuggerInterruption.py
lldb/test/API/python_api/was_interrupted/interruptible.py
lldb/test/API/python_api/was_interrupted/main.c
Modified:
lldb/include/lldb/API/SBCommandInterpreter.h
lldb/include/lldb/API/SBDebugger.h
lldb/include/lldb/Core/Debugger.h
lldb/include/lldb/Interpreter/CommandInterpreter.h
lldb/source/API/SBCommandInterpreter.cpp
lldb/source/API/SBDebugger.cpp
lldb/source/API/SBFrame.cpp
lldb/source/Commands/CommandObjectTarget.cpp
lldb/source/Commands/CommandObjectThread.cpp
lldb/source/Core/Debugger.cpp
lldb/source/Interpreter/CommandInterpreter.cpp
lldb/source/Target/StackFrameList.cpp
Removed:
################################################################################
diff --git a/lldb/include/lldb/API/SBCommandInterpreter.h b/lldb/include/lldb/API/SBCommandInterpreter.h
index 08ed989714d13..7c3f8275471e8 100644
--- a/lldb/include/lldb/API/SBCommandInterpreter.h
+++ b/lldb/include/lldb/API/SBCommandInterpreter.h
@@ -241,7 +241,20 @@ class SBCommandInterpreter {
lldb::SBStringList &matches,
lldb::SBStringList &descriptions);
+ /// Returns whether an interrupt flag was raised either by the SBDebugger -
+ /// when the function is not running on the RunCommandInterpreter thread, or
+ /// by SBCommandInterpreter::InterruptCommand if it is. If your code is doing
+ /// interruptible work, check this API periodically, and interrupt if it
+ /// returns true.
bool WasInterrupted() const;
+
+ /// Interrupts the command currently executing in the RunCommandInterpreter
+ /// thread.
+ ///
+ /// \return
+ /// \b true if there was a command in progress to recieve the interrupt.
+ /// \b false if there's no command currently in flight.
+ bool InterruptCommand();
// Catch commands before they execute by registering a callback that will get
// called when the command gets executed. This allows GUI or command line
diff --git a/lldb/include/lldb/API/SBDebugger.h b/lldb/include/lldb/API/SBDebugger.h
index 8e8e1b61e08fb..bdcd5eeaaffdb 100644
--- a/lldb/include/lldb/API/SBDebugger.h
+++ b/lldb/include/lldb/API/SBDebugger.h
@@ -197,6 +197,10 @@ class LLDB_API SBDebugger {
lldb::SBCommandInterpreter GetCommandInterpreter();
void HandleCommand(const char *command);
+
+ void RequestInterrupt();
+ void CancelInterruptRequest();
+ bool InterruptRequested();
lldb::SBListener GetListener();
diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index 3d2a37871d794..1d9d691bc2b06 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -371,6 +371,48 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
bool IsHandlingEvents() const { return m_event_handler_thread.IsJoinable(); }
Status RunREPL(lldb::LanguageType language, const char *repl_options);
+
+ /// Interruption in LLDB:
+ ///
+ /// This is a voluntary interruption mechanism, not preemptive. Parts of lldb
+ /// that do work that can be safely interrupted call
+ /// Debugger::InterruptRequested and if that returns true, they should return
+ /// at a safe point, shortcutting the rest of the work they were to do.
+ ///
+ /// lldb clients can both offer a CommandInterpreter (through
+ /// RunCommandInterpreter) and use the SB API's for their own purposes, so it
+ /// is convenient to separate "interrupting the CommandInterpreter execution"
+ /// and interrupting the work it is doing with the SB API's. So there are two
+ /// ways to cause an interrupt:
+ /// * CommandInterpreter::InterruptCommand: Interrupts the command currently
+ /// running in the command interpreter IOHandler thread
+ /// * Debugger::RequestInterrupt: Interrupts are active on anything but the
+ /// CommandInterpreter thread till CancelInterruptRequest is called.
+ ///
+ /// Since the two checks are mutually exclusive, however, it's also convenient
+ /// to have just one function to check the interrupt state.
+
+
+ /// Bump the "interrupt requested" count on the debugger to support
+ /// cooperative interruption. If this is non-zero, InterruptRequested will
+ /// return true. Interruptible operations are expected to query the
+ /// InterruptRequested API periodically, and interrupt what they were doing
+ /// if it returns \b true.
+ ///
+ void RequestInterrupt();
+
+ /// Decrement the "interrupt requested" counter.
+ void CancelInterruptRequest();
+
+ /// This is the correct way to query the state of Interruption.
+ /// If you are on the RunCommandInterpreter thread, it will check the
+ /// command interpreter state, and if it is on another thread it will
+ /// check the debugger Interrupt Request state.
+ ///
+ /// \return
+ /// A boolean value, if \b true an interruptible operation should interrupt
+ /// itself.
+ bool InterruptRequested();
// This is for use in the command interpreter, when you either want the
// selected target, or if no target is present you want to prime the dummy
@@ -512,13 +554,19 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
bool PopIOHandler(const lldb::IOHandlerSP &reader_sp);
- bool HasIOHandlerThread();
+ bool HasIOHandlerThread() const;
bool StartIOHandlerThread();
void StopIOHandlerThread();
+
+ // Sets the IOHandler thread to the new_thread, and returns
+ // the previous IOHandler thread.
+ HostThread SetIOHandlerThread(HostThread &new_thread);
void JoinIOHandlerThread();
+
+ bool IsIOHandlerThreadCurrentThread() const;
lldb::thread_result_t IOHandlerThread();
@@ -602,6 +650,9 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
lldb_private::DebuggerDestroyCallback m_destroy_callback = nullptr;
void *m_destroy_callback_baton = nullptr;
+ uint32_t m_interrupt_requested = 0; ///< Tracks interrupt requests
+ std::mutex m_interrupt_mutex;
+
// Events for m_sync_broadcaster
enum {
eBroadcastBitEventThreadIsListening = (1 << 0),
diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h
index ad6427330dfd4..cf44412b13026 100644
--- a/lldb/include/lldb/Interpreter/CommandInterpreter.h
+++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h
@@ -355,7 +355,7 @@ class CommandInterpreter : public Broadcaster,
CommandReturnObject &result,
bool force_repeat_command = false);
- bool WasInterrupted() const;
+ bool InterruptCommand();
/// Execute a list of commands in sequence.
///
@@ -640,6 +640,10 @@ class CommandInterpreter : public Broadcaster,
protected:
friend class Debugger;
+ // This checks just the RunCommandInterpreter interruption state. It is only
+ // meant to be used in Debugger::InterruptRequested
+ bool WasInterrupted() const;
+
// IOHandlerDelegate functions
void IOHandlerInputComplete(IOHandler &io_handler,
std::string &line) override;
@@ -702,7 +706,6 @@ class CommandInterpreter : public Broadcaster,
void StartHandlingCommand();
void FinishHandlingCommand();
- bool InterruptCommand();
Debugger &m_debugger; // The debugger session that this interpreter is
// associated with
diff --git a/lldb/source/API/SBCommandInterpreter.cpp b/lldb/source/API/SBCommandInterpreter.cpp
index 35c55283855df..36b70bc1e03a7 100644
--- a/lldb/source/API/SBCommandInterpreter.cpp
+++ b/lldb/source/API/SBCommandInterpreter.cpp
@@ -141,7 +141,13 @@ bool SBCommandInterpreter::IsActive() {
bool SBCommandInterpreter::WasInterrupted() const {
LLDB_INSTRUMENT_VA(this);
- return (IsValid() ? m_opaque_ptr->WasInterrupted() : false);
+ return (IsValid() ? m_opaque_ptr->GetDebugger().InterruptRequested() : false);
+}
+
+bool SBCommandInterpreter::InterruptCommand() {
+ LLDB_INSTRUMENT_VA(this);
+
+ return (IsValid() ? m_opaque_ptr->InterruptCommand() : false);
}
const char *SBCommandInterpreter::GetIOHandlerControlSequence(char ch) {
diff --git a/lldb/source/API/SBDebugger.cpp b/lldb/source/API/SBDebugger.cpp
index d90fcbf5031a4..2e7f06c7bb1b7 100644
--- a/lldb/source/API/SBDebugger.cpp
+++ b/lldb/source/API/SBDebugger.cpp
@@ -1699,3 +1699,24 @@ SBDebugger::LoadTraceFromFile(SBError &error,
LLDB_INSTRUMENT_VA(this, error, trace_description_file);
return SBTrace::LoadTraceFromFile(error, *this, trace_description_file);
}
+
+void SBDebugger::RequestInterrupt() {
+ LLDB_INSTRUMENT_VA(this);
+
+ if (m_opaque_sp)
+ m_opaque_sp->RequestInterrupt();
+}
+void SBDebugger::CancelInterruptRequest() {
+ LLDB_INSTRUMENT_VA(this);
+
+ if (m_opaque_sp)
+ m_opaque_sp->CancelInterruptRequest();
+}
+
+bool SBDebugger::InterruptRequested() {
+ LLDB_INSTRUMENT_VA(this);
+
+ if (m_opaque_sp)
+ return m_opaque_sp->InterruptRequested();
+ return false;
+}
diff --git a/lldb/source/API/SBFrame.cpp b/lldb/source/API/SBFrame.cpp
index bf833fbb95615..4cb086ed40ab4 100644
--- a/lldb/source/API/SBFrame.cpp
+++ b/lldb/source/API/SBFrame.cpp
@@ -16,6 +16,7 @@
#include "Utils.h"
#include "lldb/Core/Address.h"
+#include "lldb/Core/Debugger.h"
#include "lldb/Core/StreamFile.h"
#include "lldb/Core/ValueObjectRegister.h"
#include "lldb/Core/ValueObjectVariable.h"
@@ -806,6 +807,7 @@ SBValueList SBFrame::GetVariables(const lldb::SBVariablesOptions &options) {
if (stop_locker.TryLock(&process->GetRunLock())) {
frame = exe_ctx.GetFramePtr();
if (frame) {
+ Debugger &dbg = process->GetTarget().GetDebugger();
VariableList *variable_list = nullptr;
Status var_error;
variable_list = frame->GetVariableList(true, &var_error);
@@ -815,6 +817,11 @@ SBValueList SBFrame::GetVariables(const lldb::SBVariablesOptions &options) {
const size_t num_variables = variable_list->GetSize();
if (num_variables) {
for (const VariableSP &variable_sp : *variable_list) {
+ if (dbg.InterruptRequested()) {
+ Log *log = GetLog(LLDBLog::Host);
+ LLDB_LOG(log, "Interrupted SBFrame::GetVariables");
+ return {};
+ }
if (variable_sp) {
bool add_variable = false;
switch (variable_sp->GetScope()) {
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp
index 490a7e061563a..5874453bca23f 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -2004,7 +2004,7 @@ class CommandObjectTargetModulesDumpSymtab
result.GetOutputStream().EOL();
result.GetOutputStream().EOL();
}
- if (m_interpreter.WasInterrupted())
+ if (GetDebugger().InterruptRequested())
break;
num_dumped++;
DumpModuleSymtab(m_interpreter, result.GetOutputStream(),
@@ -2031,7 +2031,7 @@ class CommandObjectTargetModulesDumpSymtab
result.GetOutputStream().EOL();
result.GetOutputStream().EOL();
}
- if (m_interpreter.WasInterrupted())
+ if (GetDebugger().InterruptRequested())
break;
num_dumped++;
DumpModuleSymtab(m_interpreter, result.GetOutputStream(),
@@ -2092,7 +2092,7 @@ class CommandObjectTargetModulesDumpSections
result.GetOutputStream().Format("Dumping sections for {0} modules.\n",
num_modules);
for (size_t image_idx = 0; image_idx < num_modules; ++image_idx) {
- if (m_interpreter.WasInterrupted())
+ if (GetDebugger().InterruptRequested())
break;
num_dumped++;
DumpModuleSections(
@@ -2110,7 +2110,7 @@ class CommandObjectTargetModulesDumpSections
FindModulesByName(target, arg_cstr, module_list, true);
if (num_matches > 0) {
for (size_t i = 0; i < num_matches; ++i) {
- if (m_interpreter.WasInterrupted())
+ if (GetDebugger().InterruptRequested())
break;
Module *module = module_list.GetModulePointerAtIndex(i);
if (module) {
@@ -2224,7 +2224,7 @@ class CommandObjectTargetModulesDumpClangAST
result.GetOutputStream().Format("Dumping clang ast for {0} modules.\n",
num_modules);
for (ModuleSP module_sp : module_list.ModulesNoLocking()) {
- if (m_interpreter.WasInterrupted())
+ if (GetDebugger().InterruptRequested())
break;
if (SymbolFile *sf = module_sp->GetSymbolFile())
sf->DumpClangAST(result.GetOutputStream());
@@ -2249,7 +2249,7 @@ class CommandObjectTargetModulesDumpClangAST
}
for (size_t i = 0; i < num_matches; ++i) {
- if (m_interpreter.WasInterrupted())
+ if (GetDebugger().InterruptRequested())
break;
Module *m = module_list.GetModulePointerAtIndex(i);
if (SymbolFile *sf = m->GetSymbolFile())
@@ -2298,7 +2298,7 @@ class CommandObjectTargetModulesDumpSymfile
result.GetOutputStream().Format(
"Dumping debug symbols for {0} modules.\n", num_modules);
for (ModuleSP module_sp : target_modules.ModulesNoLocking()) {
- if (m_interpreter.WasInterrupted())
+ if (GetDebugger().InterruptRequested())
break;
if (DumpModuleSymbolFile(result.GetOutputStream(), module_sp.get()))
num_dumped++;
@@ -2314,7 +2314,7 @@ class CommandObjectTargetModulesDumpSymfile
FindModulesByName(target, arg_cstr, module_list, true);
if (num_matches > 0) {
for (size_t i = 0; i < num_matches; ++i) {
- if (m_interpreter.WasInterrupted())
+ if (GetDebugger().InterruptRequested())
break;
Module *module = module_list.GetModulePointerAtIndex(i);
if (module) {
@@ -2381,7 +2381,7 @@ class CommandObjectTargetModulesDumpLineTable
if (target_modules.GetSize() > 0) {
uint32_t num_dumped = 0;
for (ModuleSP module_sp : target_modules.ModulesNoLocking()) {
- if (m_interpreter.WasInterrupted())
+ if (GetDebugger().InterruptRequested())
break;
if (DumpCompileUnitLineTable(
m_interpreter, result.GetOutputStream(), module_sp.get(),
diff --git a/lldb/source/Commands/CommandObjectThread.cpp b/lldb/source/Commands/CommandObjectThread.cpp
index 411f28d84a21f..f06d4bd7937f9 100644
--- a/lldb/source/Commands/CommandObjectThread.cpp
+++ b/lldb/source/Commands/CommandObjectThread.cpp
@@ -228,7 +228,7 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
thread->GetIndexID());
return false;
}
- if (m_options.m_extended_backtrace) {
+ if (m_options.m_extended_backtrace && !GetDebugger().InterruptRequested()) {
DoExtendedBacktrace(thread, result);
}
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index 273660db066ca..2926a29463f59 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -1230,6 +1230,29 @@ StreamSP Debugger::GetAsyncErrorStream() {
return std::make_shared<StreamAsynchronousIO>(*this, false, GetUseColor());
}
+void Debugger::RequestInterrupt() {
+ std::lock_guard<std::mutex> guard(m_interrupt_mutex);
+ m_interrupt_requested++;
+}
+
+void Debugger::CancelInterruptRequest() {
+ std::lock_guard<std::mutex> guard(m_interrupt_mutex);
+ if (m_interrupt_requested > 0)
+ m_interrupt_requested--;
+}
+
+bool Debugger::InterruptRequested() {
+ // This is the one we should call internally. This will return true either
+ // if there's a debugger interrupt and we aren't on the IOHandler thread,
+ // or if we are on the IOHandler thread and there's a CommandInterpreter
+ // interrupt.
+ if (!IsIOHandlerThreadCurrentThread()) {
+ std::lock_guard<std::mutex> guard(m_interrupt_mutex);
+ return m_interrupt_requested != 0;
+ }
+ return GetCommandInterpreter().WasInterrupted();
+}
+
size_t Debugger::GetNumDebuggers() {
if (g_debugger_list_ptr && g_debugger_list_mutex_ptr) {
std::lock_guard<std::recursive_mutex> guard(*g_debugger_list_mutex_ptr);
@@ -1981,7 +2004,15 @@ void Debugger::HandleDiagnosticEvent(const lldb::EventSP &event_sp) {
data->Dump(stream.get());
}
-bool Debugger::HasIOHandlerThread() { return m_io_handler_thread.IsJoinable(); }
+bool Debugger::HasIOHandlerThread() const {
+ return m_io_handler_thread.IsJoinable();
+}
+
+HostThread Debugger::SetIOHandlerThread(HostThread &new_thread) {
+ HostThread old_host = m_io_handler_thread;
+ m_io_handler_thread = new_thread;
+ return old_host;
+}
bool Debugger::StartIOHandlerThread() {
if (!m_io_handler_thread.IsJoinable()) {
@@ -2013,6 +2044,12 @@ void Debugger::JoinIOHandlerThread() {
}
}
+bool Debugger::IsIOHandlerThreadCurrentThread() const {
+ if (!HasIOHandlerThread())
+ return false;
+ return m_io_handler_thread.EqualsThread(Host::GetCurrentThread());
+}
+
Target &Debugger::GetSelectedOrDummyTarget(bool prefer_dummy) {
if (!prefer_dummy) {
if (TargetSP target = m_target_list.GetSelectedTarget())
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index f42dd6cb40794..cf3fff23005bf 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -1886,8 +1886,8 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
LLDB_LOGF(log, "Processing command: %s", command_line);
LLDB_SCOPED_TIMERF("Processing command: %s.", command_line);
- if (WasInterrupted()) {
- result.AppendError("interrupted");
+ if (GetDebugger().InterruptRequested()) {
+ result.AppendError("... Interrupted");
return false;
}
@@ -2555,7 +2555,7 @@ void CommandInterpreter::HandleCommands(const StringList &commands,
m_debugger.SetAsyncExecution(false);
}
- for (size_t idx = 0; idx < num_lines && !WasInterrupted(); idx++) {
+ for (size_t idx = 0; idx < num_lines; idx++) {
const char *cmd = commands.GetStringAtIndex(idx);
if (cmd[0] == '\0')
continue;
@@ -3035,6 +3035,9 @@ bool CommandInterpreter::InterruptCommand() {
}
bool CommandInterpreter::WasInterrupted() const {
+ if (!m_debugger.IsIOHandlerThreadCurrentThread())
+ return false;
+
bool was_interrupted =
(m_command_state == CommandHandlingState::eInterrupted);
lldbassert(!was_interrupted || m_iohandler_nesting_level > 0);
@@ -3048,7 +3051,8 @@ void CommandInterpreter::PrintCommandOutput(IOHandler &io_handler,
lldb::StreamFileSP stream = is_stdout ? io_handler.GetOutputStreamFileSP()
: io_handler.GetErrorStreamFileSP();
// Split the output into lines and poll for interrupt requests
- while (!str.empty() && !WasInterrupted()) {
+ bool had_output = !str.empty();
+ while (!str.empty()) {
llvm::StringRef line;
std::tie(line, str) = str.split('\n');
{
@@ -3059,7 +3063,7 @@ void CommandInterpreter::PrintCommandOutput(IOHandler &io_handler,
}
std::lock_guard<std::recursive_mutex> guard(io_handler.GetOutputMutex());
- if (!str.empty())
+ if (had_output && GetDebugger().InterruptRequested())
stream->Printf("\n... Interrupted.\n");
stream->Flush();
}
@@ -3372,7 +3376,13 @@ CommandInterpreterRunResult CommandInterpreter::RunCommandInterpreter(
if (options.GetSpawnThread()) {
m_debugger.StartIOHandlerThread();
} else {
+ // If the current thread is not managed by a host thread, we won't detect
+ // that this IS the CommandInterpreter IOHandler thread, so make it so:
+ HostThread new_io_handler_thread(Host::GetCurrentThread());
+ HostThread old_io_handler_thread
+ = m_debugger.SetIOHandlerThread(new_io_handler_thread);
m_debugger.RunIOHandlers();
+ m_debugger.SetIOHandlerThread(old_io_handler_thread);
if (options.GetAutoHandleEvents())
m_debugger.StopEventHandlerThread();
diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp
index c782b506a9cb8..2610b1cd6c296 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -9,6 +9,7 @@
#include "lldb/Target/StackFrameList.h"
#include "lldb/Breakpoint/Breakpoint.h"
#include "lldb/Breakpoint/BreakpointLocation.h"
+#include "lldb/Core/Debugger.h"
#include "lldb/Core/SourceManager.h"
#include "lldb/Core/StreamFile.h"
#include "lldb/Symbol/Block.h"
@@ -470,7 +471,15 @@ void StackFrameList::GetFramesUpTo(uint32_t end_idx) {
}
StackFrameSP unwind_frame_sp;
+ Debugger &dbg = m_thread.GetProcess()->GetTarget().GetDebugger();
do {
+ // Check for interruption here when building the frames - this is the
+ // expensive part, Dump later on is cheap.
+ if (dbg.InterruptRequested()) {
+ Log *log = GetLog(LLDBLog::Host);
+ LLDB_LOG(log, "Interrupted %s", __FUNCTION__);
+ break;
+ }
uint32_t idx = m_concrete_frames_fetched++;
lldb::addr_t pc = LLDB_INVALID_ADDRESS;
lldb::addr_t cfa = LLDB_INVALID_ADDRESS;
diff --git a/lldb/test/API/python_api/was_interrupted/Makefile b/lldb/test/API/python_api/was_interrupted/Makefile
new file mode 100644
index 0000000000000..695335e068c0c
--- /dev/null
+++ b/lldb/test/API/python_api/was_interrupted/Makefile
@@ -0,0 +1,4 @@
+C_SOURCES := main.c
+CFLAGS_EXTRAS := -std=c99
+
+include Makefile.rules
diff --git a/lldb/test/API/python_api/was_interrupted/TestDebuggerInterruption.py b/lldb/test/API/python_api/was_interrupted/TestDebuggerInterruption.py
new file mode 100644
index 0000000000000..3c519a65e613b
--- /dev/null
+++ b/lldb/test/API/python_api/was_interrupted/TestDebuggerInterruption.py
@@ -0,0 +1,330 @@
+"""
+Test SBDebugger.InterruptRequested and SBCommandInterpreter.WasInterrupted.
+"""
+
+import lldb
+import lldbsuite.test.lldbutil as lldbutil
+from lldbsuite.test.lldbtest import *
+import threading
+import os
+
+class TestDebuggerInterruption(TestBase):
+ """This test runs a command that starts up, rendevous with the test thread
+ using threading barriers, then checks whether it has been interrupted.
+
+ The command's first argument is either 'interp' or 'debugger', to test
+ InterruptRequested and WasInterrupted respectively.
+
+ The command has two modes, interrupt and check, the former is the one that
+ waits for an interrupt. Then latter just returns whether an interrupt was
+ requested. We use the latter to make sure we took down the flag correctly."""
+
+ NO_DEBUG_INFO_TESTCASE = True
+
+ class CommandRunner(threading.Thread):
+ """This class is for running a command, and for making a thread to run the command on.
+ It gets passed the test it is working on behalf of, and most of the important
+ objects come from the test. """
+ def __init__(self, test):
+ super().__init__()
+ self.test = test
+
+ def rendevous(self):
+ # We smuggle out barriers and event to the runner thread using thread local data:
+ import interruptible
+ interruptible.local_data = interruptible.BarrierContainer(self.test.before_interrupt_barrier,
+ self.test.after_interrupt_barrier,
+ self.test.event)
+
+ class DirectCommandRunner(CommandRunner):
+ """"This version runs a single command using HandleCommand."""
+ def __init__(self, test, command):
+ super().__init__(test)
+ self.command = command
+
+ def run(self):
+ self.rendevous()
+ result = self.test.dbg.GetCommandInterpreter().HandleCommand(self.command, self.test.result)
+ if self.test.result_barrier:
+ self.test.result_barrier.wait()
+
+ class CommandInterpreterRunner(CommandRunner):
+ """This version runs the CommandInterpreter and feeds the command to it."""
+ def __init__(self, test):
+ super().__init__(test)
+
+ def run(self):
+ self.rendevous()
+
+ test = self.test
+
+ # We will use files for debugger input and output:
+
+ # First write down the command:
+ with open(test.getBuildArtifact(test.in_filename), "w") as f:
+ f.write(f"{test.command}\n")
+
+ # Now set the debugger's stdout & stdin to our files, and run
+ # the CommandInterpreter:
+ with open(test.out_filename, "w") as outf, open(test.in_filename, "r") as inf:
+ outsbf = lldb.SBFile(outf.fileno(), "w", False)
+ orig_outf = test.dbg.GetOutputFile()
+ error = test.dbg.SetOutputFile(outsbf)
+ test.assertSuccess(error, "Could not set outfile")
+
+ insbf = lldb.SBFile(inf.fileno(), "r", False)
+ orig_inf = test.dbg.GetOutputFile()
+ error = test.dbg.SetInputFile(insbf)
+ test.assertSuccess(error, "Could not set infile")
+
+ options = lldb.SBCommandInterpreterRunOptions()
+ options.SetPrintResults(True)
+ options.SetEchoCommands(False)
+
+ test.dbg.RunCommandInterpreter(True, False, options, 0, False, False)
+ test.dbg.GetOutputFile().Flush()
+
+ error = test.dbg.SetOutputFile(orig_outf)
+ test.assertSuccess(error, "Restored outfile")
+ test.dbg.SetInputFile(orig_inf)
+ test.assertSuccess(error, "Restored infile")
+
+ def command_setup(self, args):
+ """Insert our command, if needed. Then set up event and barriers if needed.
+ Then return the command to run."""
+
+ self.interp = self.dbg.GetCommandInterpreter()
+ self.command_name = "interruptible_command"
+ self.cmd_result = lldb.SBCommandReturnObject()
+
+ if not "check" in args:
+ self.event = threading.Event()
+ self.result_barrier = threading.Barrier(2, timeout=10)
+ self.before_interrupt_barrier = threading.Barrier(2, timeout=10)
+ self.after_interrupt_barrier = threading.Barrier(2, timeout=10)
+ else:
+ self.event = None
+ self.result_barrier = None
+ self.before_interrupt_barrier = None
+ self.after_interrupt_barrier = None
+
+ if not self.interp.UserCommandExists(self.command_name):
+ # Make the command we're going to use - it spins calling WasInterrupted:
+ cmd_filename = "interruptible"
+ cmd_filename = os.path.join(self.getSourceDir(), "interruptible.py")
+ self.runCmd(f"command script import {cmd_filename}")
+ cmd_string = f"command script add {self.command_name} --class interruptible.WelcomeCommand"
+ self.runCmd(cmd_string)
+
+ if len(args) == 0:
+ command = self.command_name
+ else:
+ command = self.command_name + " " + args
+ return command
+
+ def run_single_command(self, command):
+ # Now start up a thread to run the command:
+ self.result.Clear()
+ self.runner = TestDebuggerInterruption.DirectCommandRunner(self, command)
+ self.runner.start()
+
+ def start_command_interp(self):
+ self.runner = TestDebuggerInterruption.CommandInterpreterRunner(self)
+ self.runner.start()
+
+ def check_text(self, result_text, interrupted):
+ if interrupted:
+ self.assertIn("Command was interrupted", result_text,
+ "Got the interrupted message")
+ else:
+ self.assertIn("Command was not interrupted", result_text,
+ "Got the not interrupted message")
+
+ def gather_output(self):
+ # Now wait for the interrupt to interrupt the command:
+ self.runner.join(10.0)
+ finished = not self.runner.is_alive()
+ # Don't leave the runner thread stranded if the interrupt didn't work.
+ if not finished:
+ self.event.set()
+ self.runner.join(10.0)
+
+ self.assertTrue(finished, "We did finish the command")
+
+ def check_result(self, interrupted = True):
+ self.gather_output()
+ self.check_text(self.result.GetOutput(), interrupted)
+
+ def check_result_output(self, interrupted = True):
+ self.gather_output()
+ buffer = ""
+ # Okay, now open the file for reading, and read.
+ with open(self.out_filename, "r") as f:
+ buffer = f.read()
+
+ self.assertNotEqual(len(buffer), 0, "No command data")
+ self.check_text(buffer, interrupted)
+
+ def debugger_interrupt_test(self, use_interrupt_requested):
+ """Test that debugger interruption interrupts a command
+ running directly through HandleCommand.
+ If use_interrupt_requested is true, we'll check that API,
+ otherwise we'll check WasInterrupted. They should both do
+ the same thing."""
+
+ if use_interrupt_requested:
+ command = self.command_setup("debugger")
+ else:
+ command = self.command_setup("interp")
+
+ self.result = lldb.SBCommandReturnObject()
+ self.run_single_command(command)
+
+ # Okay now wait till the command has gotten started to issue the interrupt:
+ self.before_interrupt_barrier.wait()
+ # I'm going to do it twice here to test that it works as a counter:
+ self.dbg.RequestInterrupt()
+ self.dbg.RequestInterrupt()
+
+ def cleanup():
+ self.dbg.CancelInterruptRequest()
+ self.addTearDownHook(cleanup)
+ # Okay, now set both sides going:
+ self.after_interrupt_barrier.wait()
+
+ # Check that the command was indeed interrupted. First rendevous
+ # after the runner thread had a chance to execute the command:
+ self.result_barrier.wait()
+ self.assertTrue(self.result.Succeeded(), "Our command succeeded")
+ result_output = self.result.GetOutput()
+ self.check_result(True)
+
+ # Do it again to make sure that the counter is counting:
+ self.dbg.CancelInterruptRequest()
+ command = self.command_setup("debugger")
+ self.run_single_command(command)
+
+ # This time we won't even get to run the command, since HandleCommand
+ # checks for the interrupt state on entry, so we don't wait on the command
+ # barriers.
+ self.result_barrier.wait()
+
+ # Again check that we were
+ self.assertFalse(self.result.Succeeded(), "Our command was not allowed to run")
+ error_output = self.result.GetError()
+ self.assertIn("... Interrupted", error_output, "Command was cut short by interrupt")
+
+ # Now take down the flag, and make sure that we aren't interrupted:
+ self.dbg.CancelInterruptRequest()
+
+ # Now make sure that we really did take down the flag:
+ command = self.command_setup("debugger check")
+ self.run_single_command(command)
+ result_output = self.result.GetOutput()
+ self.check_result(False)
+
+ def test_debugger_interrupt_use_dbg(self):
+ self.debugger_interrupt_test(True)
+
+ def test_debugger_interrupt_use_interp(self):
+ self.debugger_interrupt_test(False)
+
+ def test_interp_doesnt_interrupt_debugger(self):
+ """Test that interpreter interruption does not interrupt a command
+ running directly through HandleCommand.
+ If use_interrupt_requested is true, we'll check that API,
+ otherwise we'll check WasInterrupted. They should both do
+ the same thing."""
+
+ command = self.command_setup("debugger poll")
+
+ self.result = lldb.SBCommandReturnObject()
+ self.run_single_command(command)
+
+ # Now raise the debugger interrupt flag. It will also interrupt the command:
+ self.before_interrupt_barrier.wait()
+ self.dbg.GetCommandInterpreter().InterruptCommand()
+ self.after_interrupt_barrier.wait()
+
+ # Check that the command was indeed interrupted:
+ self.result_barrier.wait()
+ self.assertTrue(self.result.Succeeded(), "Our command succeeded")
+ result_output = self.result.GetOutput()
+ self.check_result(False)
+
+
+ def interruptible_command_test(self, use_interrupt_requested):
+ """Test that interpreter interruption interrupts a command
+ running in the RunCommandInterpreter loop.
+ If use_interrupt_requested is true, we'll check that API,
+ otherwise we'll check WasInterrupted. They should both do
+ the same thing."""
+
+ self.out_filename = self.getBuildArtifact("output")
+ self.in_filename = self.getBuildArtifact("input")
+ # We're going to overwrite the input file, but we
+ # don't want data accumulating in the output file.
+
+ if os.path.exists(self.out_filename):
+ os.unlink(self.out_filename)
+
+ # You should be able to use either check method interchangeably:
+ if use_interrupt_requested:
+ self.command = self.command_setup("debugger") + "\n"
+ else:
+ self.command = self.command_setup("interp") + "\n"
+
+ self.start_command_interp()
+
+ # Now give the interpreter a chance to run this command up
+ # to the first barrier
+ self.before_interrupt_barrier.wait()
+ # Then issue the interrupt:
+ sent_interrupt = self.dbg.GetCommandInterpreter().InterruptCommand()
+ self.assertTrue(sent_interrupt, "Did send command interrupt.")
+ # Now give the command a chance to finish:
+ self.after_interrupt_barrier.wait()
+
+ self.check_result_output(True)
+
+ os.unlink(self.out_filename)
+
+ # Now send the check command, and make sure the flag is now down.
+ self.command = self.command_setup("interp check") + "\n"
+ self.start_command_interp()
+
+ self.check_result_output(False)
+
+ def test_interruptible_command_check_dbg(self):
+ self.interruptible_command_test(True)
+
+ def test_interruptible_command_check_interp(self):
+ self.interruptible_command_test(False)
+
+ def test_debugger_doesnt_interrupt_command(self):
+ """Test that debugger interruption doesn't interrupt a command
+ running in the RunCommandInterpreter loop."""
+
+ self.out_filename = self.getBuildArtifact("output")
+ self.in_filename = self.getBuildArtifact("input")
+ # We're going to overwrite the input file, but we
+ # don't want data accumulating in the output file.
+
+ if os.path.exists(self.out_filename):
+ os.unlink(self.out_filename)
+
+ self.command = self.command_setup("interp poll") + "\n"
+
+ self.start_command_interp()
+
+ self.before_interrupt_barrier.wait()
+ self.dbg.RequestInterrupt()
+ def cleanup():
+ self.dbg.CancelInterruptRequest()
+ self.addTearDownHook(cleanup)
+ self.after_interrupt_barrier.wait()
+
+ self.check_result_output(False)
+
+ os.unlink(self.out_filename)
+
diff --git a/lldb/test/API/python_api/was_interrupted/interruptible.py b/lldb/test/API/python_api/was_interrupted/interruptible.py
new file mode 100644
index 0000000000000..8a6b4e96f3eee
--- /dev/null
+++ b/lldb/test/API/python_api/was_interrupted/interruptible.py
@@ -0,0 +1,90 @@
+import lldb
+import threading
+
+local_data = None
+
+class BarrierContainer(threading.local):
+ def __init__(self, before_interrupt_barrier, after_interrupt_barrier, event):
+ self.event = event
+ self.before_interrupt_barrier = before_interrupt_barrier
+ self.after_interrupt_barrier = after_interrupt_barrier
+
+class WelcomeCommand(object):
+
+ def __init__(self, debugger, session_dict):
+ return
+
+ def get_short_help(self):
+ return "A command that waits for an interrupt before returning."
+
+ def check_was_interrupted(self, debugger, use_interpreter):
+ if use_interpreter:
+ self.was_interrupted = debugger.GetCommandInterpreter().WasInterrupted()
+ else:
+ self.was_interrupted = debugger.InterruptRequested()
+ if local_data.event:
+ self.was_canceled = local_data.event.is_set()
+
+ def __call__(self, debugger, args, exe_ctx, result):
+ """Command arguments:
+ {interp/debugger} - Whether to use SBCommandInterpreter::WasInterrupted
+ of SBDebugger::InterruptRequested().
+ check - Don't do the rendevous, just check if an interrupt was requested.
+ If check is not provided, we'll do the lock and then check.
+ poll - Should we poll once after the rendevous or spin waiting for the
+ interruption to happen.
+
+ For the interrupt cases, the command waits serially on the barriers
+ passed to it in local data, giving the test runner a chance to set the
+ interrupt. Once the barriers are passed, it waits for the interrupt
+ or the event.
+ If it finds an interrupt, it returns "Command was interrupted". If it gets an
+ event before seeing the interrupt it returns "Command was not interrupted."
+ For the "poll" case, it waits on the rendevous, then checks once.
+ For the "check" case, it doesn't wait, but just returns whether there was
+ an interrupt in force or not."""
+
+ if local_data == None:
+ result.SetError("local data was not set.")
+ result.SetStatus(lldb.eReturnStatusFailed)
+ return
+
+ use_interpreter = "interp" in args
+ if not use_interpreter:
+ if not "debugger" in args:
+ result.SetError("Must pass either 'interp' or 'debugger'")
+ result.SetStatus(lldb.eReturnStatusFailed)
+ return
+
+ self.was_interrupted = False
+ self.was_canceled = False
+
+ if "check" in args:
+ self.check_was_interrupted(debugger, use_interpreter)
+ if self.was_interrupted:
+ result.Print("Command was interrupted")
+ else:
+ result.Print("Command was not interrupted")
+ else:
+ # Wait here to rendevous in the test before it sets the interrupt.
+ local_data.before_interrupt_barrier.wait()
+ # Now the test will set the interrupt, and we can continue:
+ local_data.after_interrupt_barrier.wait()
+
+ if "poll" in args:
+ self.check_was_interrupted(debugger, use_interpreter)
+ else:
+ while not self.was_interrupted and not self.was_canceled:
+ self.check_was_interrupted(debugger, use_interpreter)
+
+ if self.was_interrupted:
+ result.Print("Command was interrupted")
+ else:
+ result.Print("Command was not interrupted")
+
+ if self.was_canceled:
+ result.Print("Command was canceled")
+ result.SetStatus(lldb.eReturnStatusSuccessFinishResult)
+ return True
+
+
diff --git a/lldb/test/API/python_api/was_interrupted/main.c b/lldb/test/API/python_api/was_interrupted/main.c
new file mode 100644
index 0000000000000..74b8a5995c461
--- /dev/null
+++ b/lldb/test/API/python_api/was_interrupted/main.c
@@ -0,0 +1,11 @@
+#include <stdio.h>
+
+int global_test_var = 10;
+
+int
+main()
+{
+ int test_var = 10;
+ printf ("Set a breakpoint here: %d.\n", test_var);
+ return global_test_var;
+}
More information about the lldb-commits
mailing list