Add gdbrunner package for shared functionality.

This deduplicates code between gdbclient.py and ndk-gdb.py.

Bug: http://b/23715403
Change-Id: I6ee61b466aaf3cde8f6b26b11bfa95761821cb6d
diff --git a/python-packages/gdbrunner/__init__.py b/python-packages/gdbrunner/__init__.py
new file mode 100644
index 0000000..0ab1641
--- /dev/null
+++ b/python-packages/gdbrunner/__init__.py
@@ -0,0 +1,244 @@
+#
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Helpers used by both gdbclient.py and ndk-gdb.py."""
+
+import adb
+import argparse
+import atexit
+import os
+import subprocess
+import tempfile
+
+class ArgumentParser(argparse.ArgumentParser):
+    """ArgumentParser subclass that provides adb device selection."""
+
+    class DeviceAction(argparse.Action):
+        def __call__(self, parser, namespace, values, option_string=None):
+            if option_string is None:
+                raise RuntimeError("DeviceAction called without option_string")
+            elif option_string == "-a":
+                # Handled in parse_args
+                return
+            elif option_string == "-d":
+                namespace.device = adb.get_usb_device()
+            elif option_string == "-e":
+                namespace.device = adb.get_emulator_device()
+            elif option_string == "-s":
+                namespace.device = adb.get_device(values[0])
+            else:
+                raise RuntimeError("Unexpected flag {}".format(option_string))
+
+    def __init__(self):
+        super(ArgumentParser, self).__init__()
+        group = self.add_argument_group(title="device selection")
+        group = group.add_mutually_exclusive_group()
+        group.add_argument(
+            "-a", nargs=0, action=self.DeviceAction,
+            help="directs commands to all interfaces")
+        group.add_argument(
+            "-d", nargs=0, action=self.DeviceAction,
+            help="directs commands to the only connected USB device")
+        group.add_argument(
+            "-e", nargs=0, action=self.DeviceAction,
+            help="directs commands to the only connected emulator")
+        group.add_argument(
+            "-s", nargs=1, metavar="SERIAL", action=self.DeviceAction,
+            help="directs commands to device/emulator with the given serial")
+
+    def parse_args(self, args=None, namespace=None):
+        result = super(ArgumentParser, self).parse_args(args, namespace)
+        # Default to -a behavior if no flags are given.
+        if "device" not in result:
+            result.device = adb.get_device()
+        return result
+
+
+def get_run_as_cmd(user, cmd):
+    """Generate a run-as or su command depending on user."""
+
+    if user is None:
+        return cmd
+    elif user == "root":
+        return ["su", "0"] + cmd
+    else:
+        return ["run-as", user] + cmd
+
+
+def get_processes(device):
+    """Return a dict from process name to list of running PIDs on the device."""
+
+    # Some custom ROMs use busybox instead of toolbox for ps. Without -w,
+    # busybox truncates the output, and very long package names like
+    # com.exampleisverylongtoolongbyfar.plasma exceed the limit.
+    #
+    # Perform the check for this on the device to avoid an adb roundtrip
+    # Some devices might not have readlink or which, so we need to handle
+    # this as well.
+
+    ps_script = """
+        if [ ! -x /system/bin/readlink -o ! -x /system/bin/which ]; then
+            ps;
+        elif [ $(readlink $(which ps)) == "toolbox" ]; then
+            ps;
+        else
+            ps -w;
+        fi
+    """
+    ps_script = " ".join([line.strip() for line in ps_script.splitlines()])
+
+    output, _ = device.shell([ps_script])
+
+    processes = dict()
+    output = output.replace("\r", "").splitlines()
+    columns = output.pop(0).split()
+    try:
+        pid_column = columns.index("PID")
+    except ValueError:
+        pid_column = 1
+    while output:
+        columns = output.pop().split()
+        process_name = columns[-1]
+        pid = int(columns[pid_column])
+        if process_name in processes:
+            processes[process_name].append(pid)
+        else:
+            processes[process_name] = [pid]
+
+    return processes
+
+
+def start_gdbserver(device, gdbserver_local_path, gdbserver_remote_path,
+                    target_pid, run_cmd, debug_socket, port, user=None):
+    """Start gdbserver in the background and forward necessary ports.
+
+    Args:
+        device: ADB device to start gdbserver on.
+        gdbserver_local_path: Host path to push gdbserver from.
+        gdbserver_remote_path: Device path to push gdbserver to.
+        target_pid: PID of device process to attach to.
+        run_cmd: Command to run on the device.
+        debug_socket: Device path to place gdbserver unix domain socket.
+        port: Host port to forward the debug_socket to.
+        user: Device user to run gdbserver as.
+
+    Returns:
+        Popen handle to the `adb shell` process gdbserver was started with.
+    """
+
+    assert target_pid is None or run_cmd is None
+
+    # Push gdbserver to the target.
+    device.push(gdbserver_local_path, gdbserver_remote_path)
+
+    # Run gdbserver.
+    gdbserver_cmd = [gdbserver_remote_path, "--once",
+                     "+{}".format(debug_socket)]
+
+    if target_pid is not None:
+        gdbserver_cmd += ["--attach", str(target_pid)]
+    else:
+        gdbserver_cmd += run_cmd
+
+    device.forward("tcp:{}".format(port),
+                   "localfilesystem:{}".format(debug_socket))
+    atexit.register(lambda: device.forward_remove("tcp:{}".format(port)))
+    gdbserver_cmd = get_run_as_cmd(user, gdbserver_cmd)
+
+    # Use ppid so that the file path stays the same.
+    gdbclient_output_path = os.path.join(tempfile.gettempdir(),
+                                         "gdbclient-{}".format(os.getppid()))
+    print "Redirecting gdbclient output to {}".format(gdbclient_output_path)
+    gdbclient_output = file(gdbclient_output_path, 'w')
+    return device.shell_popen(gdbserver_cmd, stdout=gdbclient_output,
+                              stderr=gdbclient_output)
+
+
+def pull_file(device, path, user=None):
+    """Pull a file from a device as a user."""
+
+    file_name = "gdbclient-binary-{}".format(os.getppid())
+    remote_temp_path = "/data/local/tmp/{}".format(file_name)
+    local_temp_path = os.path.join(tempfile.gettempdir(), file_name)
+    cmd = get_run_as_cmd(user, ["cat", path, ">", remote_temp_path])
+    try:
+        device.shell(cmd)
+    except adb.ShellError:
+        raise RuntimeError("Failed to copy file to temporary folder on device")
+    device.pull(remote_temp_path, local_temp_path)
+    return open(local_temp_path, "r")
+
+
+def pull_binary(device, pid, user=None):
+    """Pull a running process's binary from a device as a user"""
+    return pull_file(device, "/proc/{}/exe".format(pid), user)
+
+
+def get_binary_arch(binary_file):
+    """Parse a binary's ELF header for arch."""
+    try:
+        binary_file.seek(0)
+        binary = binary_file.read(0x14)
+    except IOError:
+        raise RuntimeError("failed to read binary file")
+    ei_class = ord(binary[0x4]) # 1 = 32-bit, 2 = 64-bit
+    ei_data = ord(binary[0x5]) # Endianness
+
+    assert ei_class == 1 or ei_class == 2
+    if ei_data != 1:
+        raise RuntimeError("binary isn't little-endian?")
+
+    e_machine = ord(binary[0x13]) << 8 | ord(binary[0x12])
+    if e_machine == 0x28:
+        assert ei_class == 1
+        return "arm"
+    elif e_machine == 0xB7:
+        assert ei_class == 2
+        return "arm64"
+    elif e_machine == 0x03:
+        assert ei_class == 1
+        return "x86"
+    elif e_machine == 0x3E:
+        assert ei_class == 2
+        return "x86_64"
+    elif e_machine == 0x08:
+        if ei_class == 1:
+            return "mips"
+        else:
+            return "mips64"
+    else:
+        raise RuntimeError("unknown architecture: 0x{:x}".format(e_machine))
+
+
+def start_gdb(gdb_path, gdb_commands):
+    """Start gdb in the background and block until it finishes.
+
+    Args:
+        gdb_path: Path of the gdb binary.
+        gdb_commands: Contents of GDB script to run.
+    """
+
+    with tempfile.NamedTemporaryFile() as gdb_script:
+        gdb_script.write(gdb_commands)
+        gdb_script.flush()
+        gdb_args = [gdb_path, "-x", gdb_script.name]
+        gdb_process = subprocess.Popen(gdb_args)
+        while gdb_process.returncode is None:
+            try:
+                gdb_process.communicate()
+            except KeyboardInterrupt:
+                pass
+