diff --git a/src/etc/debugger_tester/lldb/batchmode.py b/src/etc/debugger_tester/lldb/batchmode.py index fdf6a34c01716..ebc7b7ff91577 100644 --- a/src/etc/debugger_tester/lldb/batchmode.py +++ b/src/etc/debugger_tester/lldb/batchmode.py @@ -16,6 +16,7 @@ import _thread as thread import os import re +import signal import sys import threading import time @@ -184,6 +185,27 @@ def dispatch_repr(var_name: str, breakpoint_index: int, frame: lldb.SBFrame) -> return check(var_name, breakpoint_index, frame) == Result.Ok +def quit_with_error(debugger: lldb.SBDebugger): + """Kills the parent LLDB process with a non-0 exit code""" + + # file handles aren't guaranteed to be flushed when python doesn't return control back to LLDB, + # so we need to do it manually + debugger.GetOutputFile().Flush() + debugger.GetErrorFile().Flush() + sys.stdout.flush() + sys.stderr.flush() + + # When using a debugger created from the python script (e.g. `lldb.SBDebugger.Create()`), this + # doesn't actually work, but it doesn't hurt to try =) + debugger.HandleCommand("quit 1") + + # Returning status codes using `sys.exit` doesn't work since we're in an LLDB managed python + # instance. Instead, we kill the PID, which happens to be the parent LLDB process. + # Note: We use SIGTERM because it works on linux and windows, unlike SIGKILL, and doesn't cause + # LLDB to spit out a backtrace like SIGABRT. + os.kill(os.getpid(), signal.SIGTERM) + + #################################################################################################### # ~main #################################################################################################### @@ -203,12 +225,12 @@ def main(): # Start the timeout watchdog start_watchdog() - # This is the debugger instance of the lldb executable that imported and ran this python script. - # There is some weird behavior around LLDB reassigning, clearing, or not updating their own - # references (like `lldb.debugger`) while a python function is actively running (i.e. if control - # is not given back to the REPL). To prevent LLDB from changing things out from under us, we - # store this reference locally. - debugger = lldb.debugger + # We use a new `SBDebugger` instance, since LLDB often doesn't update internal state/python + # state while a command is being run. Since the entirety of `batchmode` is executed via a + # `script` command, the parent LLDB never gets a chance to update. + # In LLDB <23 this was less of an issue, but a change in LLDB 23 made it difficult to create + # targets from the parent debugger instance. + debugger = lldb.SBDebugger.Create() # When we step or continue, don't return from the function until the process # stops. We do this by setting the async mode to false. @@ -283,13 +305,10 @@ def main(): print(f"Could not read debugging script '{script_path}'.") traceback.print_exception(type(e), e, e.__traceback__, file=sys.stdout) print("Aborting.") - # Returning status codes using `sys.exit` doesn't work since we're in an LLDB managed python - # instance. This command sets the exit code but *does not* kill LLDB, the debugee process, - # or the SBDebugger object. - debugger.HandleCommand("quit 1") + quit_with_error(debugger) except Exception as e: traceback.print_exception(type(e), e, e.__traceback__, file=sys.stdout) - debugger.HandleCommand("quit 1") + quit_with_error(debugger) else: # Executes if the `try` block throws no exceptions. if repr_cmd_run: # We save importing these until we actually see a repr command. This prevents us @@ -329,7 +348,7 @@ def main(): ) if not tested_all_types() or not tested_all_variables(): - debugger.HandleCommand("quit 1") + quit_with_error(debugger) elif BLESS: from lldb_providers import FEATURE_FLAGS