• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2# Copyright 2013 The Flutter Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import argparse
7import os
8import re
9import shutil
10import subprocess
11import sys
12
13"""Tool for starting a GDB client and server to debug a Flutter engine process on an Android device.
14
15Usage:
16  flutter_gdb server com.example.package_name
17  flutter_gdb client com.example.package_name
18
19The Android package must be marked as debuggable in its manifest.
20
21The "client" command will copy system libraries from the device to the host
22in order to provide debug symbols.  If this has already been done on a
23previous run for a given device, then you can skip this step by passing
24--no-pull-libs.
25"""
26
27ADB_LOCAL_PATH = 'third_party/android_tools/sdk/platform-tools/adb'
28
29
30def _get_flutter_root():
31    path = os.path.dirname(os.path.abspath(__file__))
32    while os.path.basename(path) != 'src':
33        path = os.path.dirname(path)
34    return path
35
36
37def _find_package_pid(adb_path, package):
38    """Find the pid of the Flutter application process."""
39    ps_output = subprocess.check_output([adb_path, 'shell', 'ps'])
40    ps_match = re.search('^\S+\s+(\d+).*\s%s' % package, ps_output, re.MULTILINE)
41    if not ps_match:
42        print 'Unable to find pid for package %s on device' % package
43        return None
44    return int(ps_match.group(1))
45
46
47def _get_device_abi(adb_path):
48    abi_output = subprocess.check_output(
49        [adb_path, 'shell', 'getprop', 'ro.product.cpu.abi']).strip()
50    if abi_output.startswith('arm64'):
51        return 'arm64'
52    if abi_output.startswith('arm'):
53        return 'arm'
54    return abi_output
55
56
57def _default_local_engine(abi):
58    """Return the default Flutter build output directory for a given target ABI."""
59    if abi == 'x86':
60        return 'android_debug_unopt_x86'
61    elif abi == 'x86_64':
62        return 'android_debug_unopt_x64'
63    elif abi == 'arm64':
64        return 'android_debug_unopt_arm64'
65    else:
66        return 'android_debug_unopt'
67
68
69class GdbClient(object):
70    SYSTEM_LIBS_PATH = '/tmp/flutter_gdb_device_libs'
71
72    def _gdb_local_path(self):
73        GDB_LOCAL_PATH = ('third_party/android_tools/ndk/prebuilt/%s-x86_64/bin/gdb-orig')
74        if sys.platform.startswith('darwin'):
75            return GDB_LOCAL_PATH % 'darwin'
76        else:
77            return GDB_LOCAL_PATH % 'linux'
78
79    def add_subparser(self, subparsers):
80        parser = subparsers.add_parser('client',
81            help='run a GDB client')
82        parser.add_argument('package', type=str)
83        parser.add_argument('--local-engine', type=str)
84        parser.add_argument('--gdb-port', type=int, default=8888)
85        parser.add_argument('--no-pull-libs', action="store_false",
86            default=True, dest="pull_libs",
87            help="Do not copy system libraries from the device to the host")
88        parser.add_argument('--old-sysroot', action="store_true", default=False,
89            help='Create a sysroot tree suitable for debugging on older (pre-N) versions of Android')
90        parser.set_defaults(func=self.run)
91
92    def _copy_system_libs(self, adb_path, package, old_sysroot):
93        """Copy libraries used by the Flutter process from the device to the host."""
94        package_pid = _find_package_pid(adb_path, package)
95        if package_pid is None:
96            return False
97
98        # Find library files that are mapped into the process.
99        proc_maps = subprocess.check_output(
100            [adb_path, 'shell', 'run-as', package, 'cat', '/proc/%d/maps' % package_pid])
101        proc_libs = re.findall('(/system/.*\.(?:so|oat))\s*$', proc_maps, re.MULTILINE)
102
103        if old_sysroot:
104            device_libs = set((lib, os.path.basename(lib)) for lib in proc_libs)
105            device_libs.add(('/system/bin/linker', 'linker'))
106        else:
107            device_libs = set((lib, lib[1:]) for lib in proc_libs)
108            device_libs.add(('/system/bin/linker', 'system/bin/linker'))
109            device_libs.add(('/system/bin/app_process32', 'system/bin/app_process32'))
110            device_libs.add(('/system/bin/app_process64', 'system/bin/app_process64'))
111
112        if os.path.isdir(GdbClient.SYSTEM_LIBS_PATH):
113            shutil.rmtree(GdbClient.SYSTEM_LIBS_PATH)
114
115        dev_null = open(os.devnull, 'w')
116        for lib, local_path in sorted(device_libs):
117            print 'Copying %s' % lib
118            local_path = os.path.join(GdbClient.SYSTEM_LIBS_PATH, local_path)
119            if not os.path.exists(os.path.dirname(local_path)):
120                os.makedirs(os.path.dirname(local_path))
121            subprocess.call([adb_path, 'pull', lib, local_path], stderr=dev_null)
122
123        return True
124
125    def run(self, args):
126        flutter_root = _get_flutter_root()
127        if args.adb is None:
128            adb_path = os.path.join(flutter_root, ADB_LOCAL_PATH)
129        else:
130            adb_path = args.adb
131
132        if args.pull_libs:
133            if not self._copy_system_libs(adb_path, args.package, args.old_sysroot):
134                return 1
135
136        subprocess.check_call(
137            [adb_path, 'forward', 'tcp:%d' % args.gdb_port, 'tcp:%d' % args.gdb_port])
138
139        if args.local_engine is None:
140            abi = _get_device_abi(adb_path)
141            local_engine = _default_local_engine(abi)
142        else:
143            local_engine = args.local_engine
144
145        debug_out_path = os.path.join(flutter_root, 'out/%s' % local_engine)
146        if not os.path.exists(os.path.join(debug_out_path, 'libflutter.so')):
147            print 'Unable to find libflutter.so. Make sure you have completed a %s build' % local_engine
148            return 1
149
150        eval_commands = []
151        if not args.old_sysroot:
152            eval_commands.append('set sysroot %s' % GdbClient.SYSTEM_LIBS_PATH)
153        eval_commands.append('set solib-search-path %s:%s' %
154                             (debug_out_path, GdbClient.SYSTEM_LIBS_PATH))
155        eval_commands.append('target remote localhost:%d' % args.gdb_port)
156
157        exec_command = [os.path.join(flutter_root, self._gdb_local_path())]
158        for command in eval_commands:
159            exec_command += ['--eval-command', command]
160
161        os.execv(exec_command[0], exec_command)
162
163
164class GdbServer(object):
165    GDB_SERVER_DEVICE_TMP_PATH = '/data/local/tmp/gdbserver'
166
167    def add_subparser(self, subparsers):
168        parser = subparsers.add_parser('server',
169            help='run a GDB server on the device')
170        parser.add_argument('package', type=str)
171        parser.add_argument('--gdb-port', type=int, default=8888)
172        parser.set_defaults(func=self.run)
173
174    def run(self, args):
175        flutter_root = _get_flutter_root()
176        if args.adb is None:
177            adb_path = os.path.join(flutter_root, ADB_LOCAL_PATH)
178        else:
179            adb_path = args.adb
180
181        package_pid = _find_package_pid(adb_path, args.package)
182        if package_pid is None:
183            return 1
184
185        abi = _get_device_abi(adb_path)
186        gdb_server_local_path = 'third_party/android_tools/ndk/prebuilt/android-%s/gdbserver/gdbserver' % abi
187
188        # Copy gdbserver to the package's data directory.
189        subprocess.check_call([adb_path, 'push',
190                               os.path.join(flutter_root, gdb_server_local_path),
191                               GdbServer.GDB_SERVER_DEVICE_TMP_PATH])
192        gdb_server_device_path = '/data/data/%s/gdbserver' % args.package
193        subprocess.check_call([adb_path, 'shell', 'run-as', args.package, 'cp', '-F',
194                               GdbServer.GDB_SERVER_DEVICE_TMP_PATH,
195                               gdb_server_device_path])
196
197
198        subprocess.call([adb_path, 'shell', 'run-as', args.package,
199                         'killall', 'gdbserver'])
200
201        # Run gdbserver.
202        try:
203            subprocess.call([adb_path, 'shell', 'run-as', args.package,
204                             gdb_server_device_path,
205                             '--attach', ':%d' % args.gdb_port, str(package_pid)])
206        except KeyboardInterrupt:
207            pass
208
209
210def main():
211    parser = argparse.ArgumentParser(description='Flutter debugger tool')
212    subparsers = parser.add_subparsers(help='sub-command help')
213    parser.add_argument('--adb', type=str, help='path to ADB tool')
214
215    commands = [
216        GdbClient(),
217        GdbServer(),
218    ]
219    for command in commands:
220        command.add_subparser(subparsers)
221
222    args = parser.parse_args()
223    return args.func(args)
224
225
226if __name__ == '__main__':
227    sys.exit(main())
228