blob: cd0879f63cee6df5b46462f5617ed4a758db068f [file]
# This script allows to use LLDB in a way similar to GDB's batch mode. That is, given a text file
# containing LLDB commands (one command per line), this script will execute the commands one after
# the other.
# LLDB also has the -s and -S commandline options which also execute a list of commands from a text
# file. However, this command are execute `immediately`: the command of a `run` or `continue`
# command will be executed immediately after the `run` or `continue`, without waiting for the next
# breakpoint to be hit. This a command sequence like the following will not yield reliable results:
#
# break 11
# run
# print x
#
# Most of the time the `print` command will be executed while the program is still running will thus
# fail. Using this Python script, the above will work as expected.
from __future__ import print_function
import lldb
import os
import sys
import threading
import re
import time
import traceback
try:
import thread
except ModuleNotFoundError:
# The `thread` module was renamed to `_thread` in Python 3.
import _thread as thread
# Set this to True for additional output
DEBUG_OUTPUT = True
def print_debug(s):
"""Print something if DEBUG_OUTPUT is True"""
global DEBUG_OUTPUT
if DEBUG_OUTPUT:
print("DEBUG: " + str(s))
def normalize_whitespace(s):
"""Replace newlines, tabs, multiple spaces, etc with exactly one space"""
return re.sub(r"\s+", " ", s)
def breakpoint_callback(frame, bp_loc, dict):
"""This callback is registered with every breakpoint and makes sure that the
frame containing the breakpoint location is selected"""
# HACK(eddyb) print a newline to avoid continuing an unfinished line.
print("")
print("Hit breakpoint " + str(bp_loc))
# Select the frame and the thread containing it
frame.thread.process.SetSelectedThread(frame.thread)
frame.thread.SetSelectedFrame(frame.idx)
# Returning True means that we actually want to stop at this breakpoint
return True
# This is a list of breakpoints that are not registered with the breakpoint callback. The list is
# populated by the breakpoint listener and checked/emptied whenever a command has been executed
new_breakpoints = []
# This set contains all breakpoint ids that have already been registered with a callback, and is
# used to avoid hooking callbacks into breakpoints more than once
registered_breakpoints = set()
def execute_command(command_interpreter: lldb.SBCommandInterpreter, command: str):
"""Executes a single CLI command"""
global new_breakpoints
global registered_breakpoints
res = lldb.SBCommandReturnObject()
print(f"(lldb) {command}")
command_interpreter.HandleCommand(command, res)
if res.Succeeded():
if res.HasResult():
print(normalize_whitespace(res.GetOutput() or ""), end="\n")
# If the command introduced any breakpoints, make sure to register
# them with the breakpoint
# callback
while len(new_breakpoints) > 0:
res.Clear()
breakpoint_id = new_breakpoints.pop()
if breakpoint_id in registered_breakpoints:
print_debug(
"breakpoint with id %s is already registered. Ignoring."
% str(breakpoint_id)
)
else:
print_debug(
"registering breakpoint callback, id = " + str(breakpoint_id)
)
callback_command = f"breakpoint command add -s python {str(breakpoint_id)} -o \
'import lldb_batchmode; lldb_batchmode.runner.breakpoint_callback'"
command_interpreter.HandleCommand(callback_command, res)
if res.Succeeded():
print_debug(
"successfully registered breakpoint callback, id = "
+ str(breakpoint_id)
)
registered_breakpoints.add(breakpoint_id)
else:
print(
"Error while trying to register breakpoint callback, id = "
+ str(breakpoint_id)
+ ", message = "
+ str(res.GetError())
)
else:
print(res.GetError())
def start_breakpoint_listener(target):
"""Listens for breakpoints being added and adds new ones to the callback
registration list"""
listener = lldb.SBListener("breakpoint listener")
def listen():
event = lldb.SBEvent()
try:
while True:
if listener.WaitForEvent(120, event):
if (
lldb.SBBreakpoint.EventIsBreakpointEvent(event)
and lldb.SBBreakpoint.GetBreakpointEventTypeFromEvent(event)
== lldb.eBreakpointEventTypeAdded
):
global new_breakpoints
breakpoint = lldb.SBBreakpoint.GetBreakpointFromEvent(event)
print_debug("breakpoint added, id = " + str(breakpoint.id))
new_breakpoints.append(breakpoint.id)
except BaseException: # explicitly catch ctrl+c/sysexit
print_debug("breakpoint listener shutting down")
# Start the listener and let it run as a daemon
listener_thread = threading.Thread(target=listen)
listener_thread.daemon = True
listener_thread.start()
# Register the listener with the target
target.GetBroadcaster().AddListener(
listener, lldb.SBTarget.eBroadcastBitBreakpointChanged
)
def start_watchdog():
"""Starts a watchdog thread that will terminate the process after a certain
period of time"""
try:
from time import clock
except ImportError:
from time import perf_counter as clock
watchdog_start_time = clock()
watchdog_max_time = watchdog_start_time + 30
def watchdog():
while clock() < watchdog_max_time:
time.sleep(1)
print("TIMEOUT: lldb_batchmode has been running for too long. Aborting!")
thread.interrupt_main()
# Start the listener and let it run as a daemon
watchdog_thread = threading.Thread(target=watchdog)
watchdog_thread.daemon = True
watchdog_thread.start()
def get_env_arg(name):
value = os.environ.get(name)
if value is None:
print("must set %s" % name)
sys.exit(1)
return value
def dispatch_repr(var_name: str, breakpoint_index: int, frame: lldb.SBFrame) -> bool:
# We save importing the check until we actually see a repr command. This prevents us from trying
# to load input data from tests that don't use `repr` commands.
from .check_lldb import check
from .common import Result
return check(var_name, breakpoint_index, frame) == Result.Ok
####################################################################################################
# ~main
####################################################################################################
def main():
target_path = get_env_arg("LLDB_BATCHMODE_TARGET_PATH")
script_path = get_env_arg("LLDB_BATCHMODE_SCRIPT_PATH")
print("LLDB batch-mode script")
print("----------------------")
print(f"Python version: {sys.version}")
print("Debugger commands script is '%s'." % script_path)
print("Target executable is '%s'." % target_path)
print("Current working directory is '%s'" % os.getcwd())
# 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
# 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.
debugger.SetAsync(False)
# Create a target from a file and arch
print("Creating a target for '%s'" % target_path)
target: lldb.SBTarget = debugger.CreateTargetWithFileAndTargetTriple(
target_path, lldb.SBPlatform.GetHostPlatform().GetTriple()
)
if not target or not target.IsValid():
print(
"Could not create debugging target '" + target_path + ". Aborting.",
)
sys.exit(1)
# Register the breakpoint callback for every breakpoint
start_breakpoint_listener(target)
command_interpreter = debugger.GetCommandInterpreter()
repr_cmd_run = False
breakpoint_index = 0
all_ok = True
try:
script_file = open(script_path, "r")
for line in script_file:
command = line.strip()
if (
command == "run"
or command == "r"
or re.match(r"^process\s+launch.*", command)
):
print(f"(lldb) {command}")
process: lldb.SBProcess = target.LaunchSimple(None, None, None)
if (
process.GetSelectedThread().GetStopReason()
== lldb.eStopReasonBreakpoint
and breakpoint_index is None
):
breakpoint_index = 0
continue
if command == "continue" or command == "c":
print(f"(lldb) {command}")
process.Continue()
if (
process.GetSelectedThread().GetStopReason()
== lldb.eStopReasonBreakpoint
):
breakpoint_index += 1
continue
if command == "quit" or command == "exit":
print(f"(lldb) {command}")
break
if command.startswith("repr "):
repr_cmd_run = True
var_name = command.split(" ", 1)[1]
p = target.GetProcess()
frame = p.GetSelectedThread().GetSelectedFrame()
print(command)
all_ok &= dispatch_repr(var_name, breakpoint_index, frame)
elif command != "":
execute_command(command_interpreter, command)
except IOError as e:
print("Could not read debugging script '%s'." % 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")
except Exception as e:
traceback.print_exception(e, file=sys.stdout)
debugger.HandleCommand("quit 1")
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
# from trying to load input data from tests that don't use `repr` commands.
from .check_lldb import tested_all_types, tested_all_variables
from .common import BLESS, BlessMetadata, INPUT_DATA
# `bless` should resolve any errors from mismatched test data, so any errors that reach
# this point are either from the `bless` not working properly, or some other issue with
# the test itself. In either case, we probably don't want to update the test data until
# those are resolved.
# Only runs if the test contains a repr command, as we don't want to create an input
# file for a test that won't ever use it.
if not tested_all_types() or not tested_all_variables():
debugger.HandleCommand("quit 1")
elif BLESS:
from lldb_providers import FEATURE_FLAGS
INPUT_DATA.save_blessing(
BlessMetadata(
sys.version, debugger.GetVersionString(), str(FEATURE_FLAGS)
)
)
finally:
script_file.close()