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