1#
2# Copyright (C) 2015 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17"""Helpers used by both gdbclient.py and ndk-gdb.py."""
18
19import adb
20import argparse
21import atexit
22import os
23import subprocess
24import sys
25import tempfile
26
27class ArgumentParser(argparse.ArgumentParser):
28    """ArgumentParser subclass that provides adb device selection."""
29
30    def __init__(self):
31        super(ArgumentParser, self).__init__()
32        self.add_argument(
33            "--adb", dest="adb_path",
34            help="use specific adb command")
35
36        group = self.add_argument_group(title="device selection")
37        group = group.add_mutually_exclusive_group()
38        group.add_argument(
39            "-a", action="store_const", dest="device", const="-a",
40            help="directs commands to all interfaces")
41        group.add_argument(
42            "-d", action="store_const", dest="device", const="-d",
43            help="directs commands to the only connected USB device")
44        group.add_argument(
45            "-e", action="store_const", dest="device", const="-e",
46            help="directs commands to the only connected emulator")
47        group.add_argument(
48            "-s", metavar="SERIAL", action="store", dest="serial",
49            help="directs commands to device/emulator with the given serial")
50
51    def parse_args(self, args=None, namespace=None):
52        result = super(ArgumentParser, self).parse_args(args, namespace)
53
54        adb_path = result.adb_path or "adb"
55
56        # Try to run the specified adb command
57        try:
58            subprocess.check_output([adb_path, "version"],
59                                    stderr=subprocess.STDOUT)
60        except (OSError, subprocess.CalledProcessError):
61            msg = "ERROR: Unable to run adb executable (tried '{}')."
62            if not result.adb_path:
63                msg += "\n       Try specifying its location with --adb."
64            sys.exit(msg.format(adb_path))
65
66        try:
67            if result.device == "-a":
68                result.device = adb.get_device(adb_path=adb_path)
69            elif result.device == "-d":
70                result.device = adb.get_usb_device(adb_path=adb_path)
71            elif result.device == "-e":
72                result.device = adb.get_emulator_device(adb_path=adb_path)
73            else:
74                result.device = adb.get_device(result.serial, adb_path=adb_path)
75        except (adb.DeviceNotFoundError, adb.NoUniqueDeviceError, RuntimeError):
76            # Don't error out if we can't find a device.
77            result.device = None
78
79        return result
80
81
82def get_run_as_cmd(user, cmd):
83    """Generate a run-as or su command depending on user."""
84
85    if user is None:
86        return cmd
87    elif user == "root":
88        return ["su", "0"] + cmd
89    else:
90        return ["run-as", user] + cmd
91
92
93def get_processes(device):
94    """Return a dict from process name to list of running PIDs on the device."""
95
96    # Some custom ROMs use busybox instead of toolbox for ps. Without -w,
97    # busybox truncates the output, and very long package names like
98    # com.exampleisverylongtoolongbyfar.plasma exceed the limit.
99    #
100    # Perform the check for this on the device to avoid an adb roundtrip
101    # Some devices might not have readlink or which, so we need to handle
102    # this as well.
103    #
104    # Gracefully handle [ or readlink being missing by always using `ps` if
105    # readlink is missing. (API 18 has [, but not readlink).
106
107    ps_script = """
108        if $(ls /system/bin/readlink >/dev/null 2>&1); then
109          if [ $(readlink /system/bin/ps) == "toolbox" ]; then
110            ps;
111          else
112            ps -w;
113          fi
114        else
115          ps;
116        fi
117    """
118    ps_script = " ".join([line.strip() for line in ps_script.splitlines()])
119
120    output, _ = device.shell([ps_script])
121    return parse_ps_output(output)
122
123def parse_ps_output(output):
124    processes = dict()
125    output = adb.split_lines(output.replace("\r", ""))
126    columns = output.pop(0).split()
127    try:
128        pid_column = columns.index("PID")
129    except ValueError:
130        pid_column = 1
131    while output:
132        columns = output.pop().split()
133        process_name = columns[-1]
134        pid = int(columns[pid_column])
135        if process_name in processes:
136            processes[process_name].append(pid)
137        else:
138            processes[process_name] = [pid]
139
140    return processes
141
142
143def get_pids(device, process_name):
144    processes = get_processes(device)
145    return processes.get(process_name, [])
146
147
148def start_gdbserver(device, gdbserver_local_path, gdbserver_remote_path,
149                    target_pid, run_cmd, debug_socket, port, user=None):
150    """Start gdbserver in the background and forward necessary ports.
151
152    Args:
153        device: ADB device to start gdbserver on.
154        gdbserver_local_path: Host path to push gdbserver from, can be None.
155        gdbserver_remote_path: Device path to push gdbserver to.
156        target_pid: PID of device process to attach to.
157        run_cmd: Command to run on the device.
158        debug_socket: Device path to place gdbserver unix domain socket.
159        port: Host port to forward the debug_socket to.
160        user: Device user to run gdbserver as.
161
162    Returns:
163        Popen handle to the `adb shell` process gdbserver was started with.
164    """
165
166    assert target_pid is None or run_cmd is None
167
168    # Push gdbserver to the target.
169    if gdbserver_local_path is not None:
170        device.push(gdbserver_local_path, gdbserver_remote_path)
171
172    # Run gdbserver.
173    gdbserver_cmd = [gdbserver_remote_path, "--once",
174                     "+{}".format(debug_socket)]
175
176    if target_pid is not None:
177        gdbserver_cmd += ["--attach", str(target_pid)]
178    else:
179        gdbserver_cmd += run_cmd
180
181    device.forward("tcp:{}".format(port),
182                   "localfilesystem:{}".format(debug_socket))
183    atexit.register(lambda: device.forward_remove("tcp:{}".format(port)))
184    gdbserver_cmd = get_run_as_cmd(user, gdbserver_cmd)
185
186    gdbserver_output_path = os.path.join(tempfile.gettempdir(),
187                                         "gdbclient.log")
188    print("Redirecting gdbserver output to {}".format(gdbserver_output_path))
189    gdbserver_output = file(gdbserver_output_path, 'w')
190    return device.shell_popen(gdbserver_cmd, stdout=gdbserver_output,
191                              stderr=gdbserver_output)
192
193
194def find_file(device, executable_path, sysroot, user=None):
195    """Finds a device executable file.
196
197    This function first attempts to find the local file which will
198    contain debug symbols. If that fails, it will fall back to
199    downloading the stripped file from the device.
200
201    Args:
202      device: the AndroidDevice object to use.
203      executable_path: absolute path to the executable or symlink.
204      sysroot: absolute path to the built symbol sysroot.
205      user: if necessary, the user to download the file as.
206
207    Returns:
208      A tuple containing (<open file object>, <was found locally>).
209
210    Raises:
211      RuntimeError: could not find the executable binary.
212      ValueError: |executable_path| is not absolute.
213    """
214    if not os.path.isabs(executable_path):
215        raise ValueError("'{}' is not an absolute path".format(executable_path))
216
217    def generate_files():
218        """Yields (<file name>, <found locally>) tuples."""
219        # First look locally to avoid shelling into the device if possible.
220        # os.path.join() doesn't combine absolute paths, use + instead.
221        yield (sysroot + executable_path, True)
222
223        # Next check if the path is a symlink.
224        try:
225            target = device.shell(['readlink', '-e', '-n', executable_path])[0]
226            yield (sysroot + target, True)
227        except adb.ShellError:
228            pass
229
230        # Last, download the stripped executable from the device if necessary.
231        file_name = "gdbclient-binary-{}".format(os.getppid())
232        remote_temp_path = "/data/local/tmp/{}".format(file_name)
233        local_path = os.path.join(tempfile.gettempdir(), file_name)
234        cmd = get_run_as_cmd(user,
235                             ["cat", executable_path, ">", remote_temp_path])
236        try:
237            device.shell(cmd)
238        except adb.ShellError:
239            raise RuntimeError("Failed to copy '{}' to temporary folder on "
240                               "device".format(executable_path))
241        device.pull(remote_temp_path, local_path)
242        yield (local_path, False)
243
244    for path, found_locally in generate_files():
245        if os.path.isfile(path):
246            return (open(path, "r"), found_locally)
247    raise RuntimeError('Could not find executable {}'.format(executable_path))
248
249
250def find_binary(device, pid, sysroot, user=None):
251    """Finds a device executable file corresponding to |pid|."""
252    return find_file(device, "/proc/{}/exe".format(pid), sysroot, user)
253
254
255def get_binary_arch(binary_file):
256    """Parse a binary's ELF header for arch."""
257    try:
258        binary_file.seek(0)
259        binary = binary_file.read(0x14)
260    except IOError:
261        raise RuntimeError("failed to read binary file")
262    ei_class = ord(binary[0x4]) # 1 = 32-bit, 2 = 64-bit
263    ei_data = ord(binary[0x5]) # Endianness
264
265    assert ei_class == 1 or ei_class == 2
266    if ei_data != 1:
267        raise RuntimeError("binary isn't little-endian?")
268
269    e_machine = ord(binary[0x13]) << 8 | ord(binary[0x12])
270    if e_machine == 0x28:
271        assert ei_class == 1
272        return "arm"
273    elif e_machine == 0xB7:
274        assert ei_class == 2
275        return "arm64"
276    elif e_machine == 0x03:
277        assert ei_class == 1
278        return "x86"
279    elif e_machine == 0x3E:
280        assert ei_class == 2
281        return "x86_64"
282    elif e_machine == 0x08:
283        if ei_class == 1:
284            return "mips"
285        else:
286            return "mips64"
287    else:
288        raise RuntimeError("unknown architecture: 0x{:x}".format(e_machine))
289
290
291def start_gdb(gdb_path, gdb_commands, gdb_flags=None):
292    """Start gdb in the background and block until it finishes.
293
294    Args:
295        gdb_path: Path of the gdb binary.
296        gdb_commands: Contents of GDB script to run.
297        gdb_flags: List of flags to append to gdb command.
298    """
299
300    # Windows disallows opening the file while it's open for writing.
301    gdb_script_fd, gdb_script_path = tempfile.mkstemp()
302    os.write(gdb_script_fd, gdb_commands)
303    os.close(gdb_script_fd)
304    gdb_args = [gdb_path, "-x", gdb_script_path] + (gdb_flags or [])
305
306    kwargs = {}
307    if sys.platform.startswith("win"):
308        kwargs["creationflags"] = subprocess.CREATE_NEW_CONSOLE
309
310    gdb_process = subprocess.Popen(gdb_args, **kwargs)
311    while gdb_process.returncode is None:
312        try:
313            gdb_process.communicate()
314        except KeyboardInterrupt:
315            pass
316
317    os.unlink(gdb_script_path)
318
319