# 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

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, command):
    """Executes a single CLI command"""
    global new_breakpoints
    global registered_breakpoints

    res = lldb.SBCommandReturnObject()
    print(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 = (
                    "breakpoint command add -F breakpoint_callback "
                    + str(breakpoint_id)
                )
                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.py 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


####################################################################################################
# ~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("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()

    # Create a new 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.
    debugger.SetAsync(False)

    # Create a target from a file and arch
    print("Creating a target for '%s'" % target_path)
    target_error = lldb.SBError()
    target = debugger.CreateTarget(target_path, None, None, True, target_error)

    if not target:
        print(
            "Could not create debugging target '"
            + target_path
            + "': "
            + str(target_error)
            + ". Aborting.",
            file=sys.stderr,
        )
        sys.exit(1)

    # Register the breakpoint callback for every breakpoint
    start_breakpoint_listener(target)

    command_interpreter = debugger.GetCommandInterpreter()

    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)
            ):
                # Before starting to run the program, let the thread sleep a bit, so all
                # breakpoint added events can be processed
                time.sleep(0.5)
            if command != "":
                execute_command(command_interpreter, command)

    except IOError as e:
        print("Could not read debugging script '%s'." % script_path, file=sys.stderr)
        print(e, file=sys.stderr)
        print("Aborting.", file=sys.stderr)
        sys.exit(1)
    finally:
        debugger.Terminate()
        script_file.close()


if __name__ == "__main__":
    main()
