| #!/usr/bin/env python |
| |
| r''' |
| Copyright (C) 2010 The Android Open Source Project |
| Copyright (C) 2012 Ray Donnelly <mingw.android@gmail.com> |
| |
| 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. |
| |
| |
| This wrapper script is used to launch a native debugging session |
| on a given NDK application. The application must be debuggable, i.e. |
| its android:debuggable attribute must be set to 'true' in the |
| <application> element of its manifest. |
| |
| See docs/NDK-GDB.TXT for usage description. Essentially, you just |
| need to launch ndk-gdb-py from your application project directory |
| after doing ndk-build && ant debug && \ |
| adb install && <start-application-on-device> |
| ''' |
| |
| import atexit, sys, os, platform, argparse, subprocess, types |
| import xml.etree.cElementTree as ElementTree |
| import shutil, time |
| from threading import Thread |
| try: |
| from Queue import Queue, Empty |
| except ImportError: |
| from queue import Queue, Empty # python 3.x |
| |
| def get_platform_arch(abi): |
| '''Convert sysprop abi to a platform architecture name.''' |
| if abi.startswith("armeabi"): |
| return "arm" |
| elif abi.startswith("arm64"): |
| return "arm64" |
| elif abi.startswith("mips64"): |
| return "mips64" |
| elif abi.startswith("mips"): |
| return "mips" |
| return abi |
| |
| def find_program(program, extra_paths = []): |
| ''' extra_paths are searched before PATH ''' |
| PATHS = extra_paths+os.environ['PATH'].replace('"','').split(os.pathsep) |
| exts = [''] |
| if sys.platform.startswith('win'): |
| exts += ['.exe', '.bat', '.cmd'] |
| for path in PATHS: |
| if os.path.isdir(path): |
| for ext in exts: |
| full = path + os.sep + program + ext |
| if os.path.isfile(full): |
| return True, full |
| return False, None |
| |
| def ndk_bin_path(ndk): |
| ''' |
| Return the prebuilt bin path for the host OS. |
| |
| If Python executable is the NDK-prebuilt one (it should be) |
| then use the location of the executable as the first guess. |
| We take the grand-parent foldername and then ensure that it |
| starts with one of 'linux', 'darwin' or 'windows'. |
| |
| If this is not the case, then we're using some other Python |
| and fall-back to using platform.platform() and sys.maxsize. |
| ''' |
| |
| try: |
| ndk_host = os.path.basename( |
| os.path.dirname( |
| os.path.dirname(sys.executable))) |
| except: |
| ndk_host = '' |
| # NDK-prebuilt Python? |
| if (not ndk_host.startswith('linux') and |
| not ndk_host.startswith('darwin') and |
| not ndk_host.startswith('windows')): |
| is64bit = True if sys.maxsize > 2**32 else False |
| if platform.platform().startswith('Linux'): |
| ndk_host = 'linux%s' % ('-x86_64' if is64bit else '-x86') |
| elif platform.platform().startswith('Darwin'): |
| ndk_host = 'darwin%s' % ('-x86_64' if is64bit else '-x86') |
| elif platform.platform().startswith('Windows'): |
| ndk_host = 'windows%s' % ('-x86_64' if is64bit else '') |
| else: |
| ndk_host = 'UNKNOWN' |
| return ndk+os.sep+'prebuilt'+os.sep+ndk_host+os.sep+'bin' |
| |
| VERBOSE = False |
| PROJECT = None |
| ADB_CMD = None |
| GNUMAKE_CMD = None |
| JDB_CMD = None |
| # Extra arguments passed to the NDK build system when |
| # querying it. |
| GNUMAKE_FLAGS = [] |
| |
| OPTION_FORCE = None |
| OPTION_EXEC = None |
| OPTION_START = None |
| OPTION_LAUNCH = None |
| OPTION_LAUNCH_LIST = None |
| OPTION_TUI = None |
| OPTION_WAIT = ['-D'] |
| OPTION_STDCXXPYPR = None |
| |
| PYPRPR_BASE = sys.prefix + '/share/pretty-printers/' |
| PYPRPR_GNUSTDCXX_BASE = PYPRPR_BASE + 'libstdcxx/' |
| |
| DEBUG_PORT = 5039 |
| JDB_PORT = 65534 |
| |
| # Name of the manifest file |
| MANIFEST = 'AndroidManifest.xml' |
| |
| # Delay in seconds between launching the activity and attaching gdbserver on it. |
| # This is needed because there is no way to know when the activity has really |
| # started, and sometimes this takes a few seconds. |
| # |
| DELAY = 2.0 |
| NDK = os.path.abspath(os.path.dirname(sys.argv[0])).replace('\\','/') |
| ADB_FLAGS = [] |
| |
| def log(string): |
| global VERBOSE |
| if VERBOSE: |
| print(string) |
| |
| def error(string, errcode=1): |
| print('ERROR: %s' % (string)) |
| exit(errcode) |
| |
| def handle_args(): |
| global VERBOSE, DEBUG_PORT, DELAY |
| global GNUMAKE_CMD, GNUMAKE_FLAGS |
| global ADB_CMD, ADB_FLAGS |
| global JDB_CMD |
| global PROJECT, NDK |
| global OPTION_START, OPTION_LAUNCH, OPTION_LAUNCH_LIST |
| global OPTION_FORCE, OPTION_EXEC, OPTION_TUI, OPTION_WAIT |
| global OPTION_STDCXXPYPR |
| global PYPRPR_GNUSTDCXX_BASE |
| |
| parser = argparse.ArgumentParser(description=''' |
| Setup a gdb debugging session for your Android NDK application. |
| Read ''' + NDK + '''/docs/NDK-GDB.html for complete usage instructions.''', |
| formatter_class=argparse.RawTextHelpFormatter) |
| |
| parser.add_argument( '--verbose', |
| help='Enable verbose mode', action='store_true', dest='verbose') |
| |
| parser.add_argument( '--force', |
| help='Kill existing debug session if it exists', |
| action='store_true') |
| |
| app_group = parser.add_argument_group('application debugging') |
| start_group = app_group.add_mutually_exclusive_group() |
| start_group.add_argument( '--start', |
| help='Launch application instead of attaching to existing one', |
| action='store_true') |
| |
| start_group.add_argument( '--launch', |
| help='Same as --start, but specify activity name (see below)', |
| dest='launch_name', nargs=1) |
| |
| start_group.add_argument( '--launch-list', |
| help='List all launchable activity names from manifest', |
| action='store_true') |
| |
| app_group.add_argument( '--delay', |
| help='Delay in seconds between activity start and gdbserver attach', |
| type=float, default=DELAY, |
| dest='delay') |
| |
| app_group.add_argument( '-p', '--project', |
| help='Specify application project path', |
| dest='project') |
| |
| app_group.add_argument( '--nowait', |
| help='Do not wait for debugger to attach (may miss early JNI breakpoints)', |
| action='store_true', dest='nowait') |
| |
| parser.add_argument( '--port', |
| help='Use tcp:localhost:<DEBUG_PORT> to communicate with gdbserver', |
| type=int, default=DEBUG_PORT, |
| dest='debug_port') |
| |
| parser.add_argument( '-x', '--exec', |
| help='Execute gdb initialization commands in <EXEC_FILE> after connection', |
| dest='exec_file') |
| |
| parser.add_argument( '--adb', |
| help='Use specific adb command', |
| dest='adb_cmd') |
| |
| # Unused flag retained for compatibility |
| parser.add_argument( '--awk', |
| help=argparse.SUPPRESS) |
| |
| connect_group = parser.add_argument_group('device selection').add_mutually_exclusive_group() |
| connect_group.add_argument( '-e', |
| help='Connect to single emulator instance', |
| action='store_true', dest='emulator') |
| |
| connect_group.add_argument( '-d', |
| help='Connect to single target device', |
| action='store_true', dest='device') |
| |
| connect_group.add_argument( '-s', |
| help='Connect to specific emulator or device', |
| dest='device_serial') |
| |
| parser.add_argument( '-t','--tui', |
| help='Use tui mode', |
| action='store_true', dest='tui') |
| |
| parser.add_argument( '--gnumake-flag', |
| help='Flag to pass to gnumake, e.g. NDK_TOOLCHAIN_VERSION=4.8', |
| action='append', dest='gnumake_flags') |
| |
| if os.path.isdir(PYPRPR_GNUSTDCXX_BASE): |
| stdcxx_pypr_versions = [ 'gnustdcxx'+d.replace('gcc','') |
| for d in os.listdir(PYPRPR_GNUSTDCXX_BASE) |
| if os.path.isdir(os.path.join(PYPRPR_GNUSTDCXX_BASE, d)) ] |
| else: |
| stdcxx_pypr_versions = [] |
| |
| parser.add_argument( '--stdcxx-py-pr', |
| help='Specify stdcxx python pretty-printer', |
| choices=['auto', 'none', 'gnustdcxx'] + stdcxx_pypr_versions + ['stlport'], |
| default='none', dest='stdcxxpypr') |
| |
| args = parser.parse_args() |
| |
| VERBOSE = args.verbose |
| |
| ndk_bin = ndk_bin_path(NDK) |
| (found_adb, ADB_CMD) = find_program('adb', [ndk_bin]) |
| (found_gnumake, GNUMAKE_CMD) = find_program('make', [ndk_bin]) |
| (found_jdb, JDB_CMD) = find_program('jdb', []) |
| |
| if not found_gnumake: |
| error('Failed to find GNU make') |
| |
| log('Android NDK installation path: %s' % (NDK)) |
| |
| if args.device: |
| ADB_FLAGS = ['-d'] |
| if args.emulator: |
| ADB_FLAGS = ['-e'] |
| if args.device_serial: |
| ADB_FLAGS = ['-s', args.device_serial] |
| if args.adb_cmd != None: |
| log('Using specific adb command: %s' % (args.adb_cmd)) |
| ADB_CMD = args.adb_cmd |
| if ADB_CMD is None: |
| error('''The 'adb' tool is not in your path. |
| You can change your PATH variable, or use |
| --adb=<executable> to point to a valid one.''') |
| if not os.path.isfile(ADB_CMD): |
| error('Could not run ADB with: %s' % (ADB_CMD)) |
| |
| if args.project != None: |
| PROJECT = args.project |
| |
| if args.start != None: |
| OPTION_START = args.start |
| |
| if args.launch_name != None: |
| OPTION_LAUNCH = args.launch_name |
| |
| if args.launch_list != None: |
| OPTION_LAUNCH_LIST = args.launch_list |
| |
| if args.force != None: |
| OPTION_FORCE = args.force |
| |
| if args.exec_file != None: |
| OPTION_EXEC = args.exec_file |
| |
| if args.tui != False: |
| OPTION_TUI = True |
| |
| if args.delay != None: |
| DELAY = args.delay |
| |
| if args.gnumake_flags != None: |
| GNUMAKE_FLAGS = args.gnumake_flags |
| |
| if args.nowait == True: |
| OPTION_WAIT = [] |
| elif not found_jdb: |
| error('Failed to find jdb.\n..you can use --nowait to disable jdb\n..but may miss early breakpoints.') |
| |
| OPTION_STDCXXPYPR = args.stdcxxpypr |
| |
| def get_build_var(var): |
| global GNUMAKE_CMD, GNUMAKE_FLAGS, NDK, PROJECT |
| text = subprocess.check_output([GNUMAKE_CMD, |
| '--no-print-dir', |
| '-f', |
| NDK+'/build/core/build-local.mk', |
| '-C', |
| PROJECT, |
| 'DUMP_'+var] + GNUMAKE_FLAGS |
| ) |
| # replace('\r', '') due to Windows crlf (\r\n) |
| # ...universal_newlines=True causes bytes to be returned |
| # rather than a str |
| return text.decode('ascii').replace('\r', '').splitlines()[0] |
| |
| def get_build_var_for_abi(var, abi): |
| global GNUMAKE_CMD, GNUMAKE_FLAGS, NDK, PROJECT |
| text = subprocess.check_output([GNUMAKE_CMD, |
| '--no-print-dir', |
| '-f', |
| NDK+'/build/core/build-local.mk', |
| '-C', |
| PROJECT, |
| 'DUMP_'+var, |
| 'APP_ABI='+abi] + GNUMAKE_FLAGS, |
| ) |
| return text.decode('ascii').replace('\r', '').splitlines()[0] |
| |
| # Silent if gdb is running in tui mode to keep things tidy. |
| def output_gdbserver(text): |
| if not OPTION_TUI or OPTION_TUI != 'running': |
| print(text) |
| |
| # Likewise, silent in tui mode (also prepends 'JDB :: ') |
| def output_jdb(text): |
| if not OPTION_TUI or OPTION_TUI != 'running': |
| print('JDB :: %s' % text) |
| |
| def input_jdb(inhandle): |
| while True: |
| inhandle.write('\n') |
| time.sleep(1.0) |
| |
| def background_spawn(args, redirect_stderr, output_fn, redirect_stdin = None, input_fn = None): |
| def async_stdout(outhandle, queue, output_fn): |
| for line in iter(outhandle.readline, b''): |
| output_fn(line.replace('\r', '').replace('\n', '')) |
| outhandle.close() |
| |
| def async_stderr(outhandle, queue, output_fn): |
| for line in iter(outhandle.readline, b''): |
| output_fn(line.replace('\r', '').replace('\n', '')) |
| outhandle.close() |
| |
| def async_stdin(inhandle, queue, input_fn): |
| input_fn(inhandle) |
| inhandle.close() |
| |
| if redirect_stderr: |
| used_stderr = subprocess.PIPE |
| else: |
| used_stderr = subprocess.STDOUT |
| if redirect_stdin: |
| used_stdin = subprocess.PIPE |
| else: |
| used_stdin = None |
| |
| windows = os.name == "nt" |
| creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if windows else 0 |
| preexec_fn = None if windows else os.setpgrp |
| p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=used_stderr, stdin=used_stdin, |
| bufsize=1, close_fds='posix' in sys.builtin_module_names, |
| creationflags=creationflags, preexec_fn=preexec_fn) |
| atexit.register(p.kill) |
| |
| qo = Queue() |
| to = Thread(target=async_stdout, args=(p.stdout, qo, output_fn)) |
| to.daemon = True |
| to.start() |
| if redirect_stderr: |
| te = Thread(target=async_stderr, args=(p.stderr, qo, output_fn)) |
| te.daemon = True |
| te.start() |
| if redirect_stdin: |
| ti = Thread(target=async_stdin, args=(p.stdin, qo, input_fn)) |
| ti.daemon = True |
| ti.start() |
| |
| return p |
| |
| def adb_cmd(redirect_stderr, args, log_command=False, adb_trace=False, background=False): |
| global ADB_CMD, ADB_FLAGS |
| fullargs = [ADB_CMD] |
| if ADB_FLAGS: |
| fullargs += ADB_FLAGS |
| if isinstance(args, str): |
| fullargs.append(args) |
| else: |
| fullargs += [arg for arg in args] |
| new_env = os.environ.copy() |
| retval = 0 |
| if adb_trace: |
| new_env["ADB_TRACE"] = "1" |
| if background: |
| if log_command: |
| log('## COMMAND: adb_cmd %s [BACKGROUND]' % (' '.join(args))) |
| background_spawn(fullargs, redirect_stderr, output_gdbserver) |
| return 0, '' |
| else: |
| if log_command: |
| log('## COMMAND: adb_cmd %s' % (' '.join(args))) |
| try: |
| if redirect_stderr: |
| text = subprocess.check_output(fullargs, |
| stderr=subprocess.STDOUT, |
| env=new_env |
| ) |
| else: |
| text = subprocess.check_output(fullargs, |
| env=new_env |
| ) |
| except subprocess.CalledProcessError as e: |
| retval = e.returncode |
| text = e.output |
| # rstrip() because of final newline. |
| return retval, text.decode('ascii').replace('\r', '').rstrip() |
| |
| def _adb_var_shell(args, redirect_stderr=False, log_command=True): |
| if log_command: |
| log('## COMMAND: adb_cmd shell %s' % (' '.join(args))) |
| arg_str = str(' '.join(args)+' ; echo $?') |
| adb_ret,output = adb_cmd(redirect_stderr=redirect_stderr, |
| args=['shell', arg_str], log_command=False) |
| output = output.splitlines() |
| retcode = int(output.pop()) |
| return retcode,'\n'.join(output) |
| |
| def adb_var_shell(args, log_command=False): |
| return _adb_var_shell(args, redirect_stderr=False, log_command=log_command) |
| |
| def adb_var_shell2(args, log_command=False): |
| return _adb_var_shell(args, redirect_stderr=True, log_command=log_command) |
| |
| def get_processes(): |
| '''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()]) |
| |
| retcode, output = adb_cmd(False, ['shell', ps_script]) |
| if retcode != 0: |
| raise RuntimeException("Failed to get ps output") |
| |
| processes = dict() |
| output = output.replace('\r', '').splitlines() |
| columns = output.pop(0).split() |
| try: |
| PID_column = columns.index('PID') |
| except: |
| 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 extract_package_name(xmlfile): |
| ''' |
| The name itself is the value of the 'package' attribute in the |
| 'manifest' element. |
| ''' |
| tree = ElementTree.ElementTree(file=xmlfile) |
| root = tree.getroot() |
| if 'package' in root.attrib: |
| return root.attrib['package'] |
| return None |
| |
| def extract_debuggable(xmlfile): |
| ''' |
| simply extract the 'android:debuggable' attribute value from |
| the first <manifest><application> element we find. |
| ''' |
| tree = ElementTree.ElementTree(file=xmlfile) |
| root = tree.getroot() |
| for application in root.iter('application'): |
| for k in application.attrib.keys(): |
| if str(k).endswith('debuggable'): |
| return application.attrib[k] == 'true' |
| return False |
| |
| def extract_launchable(xmlfile): |
| ''' |
| A given application can have several activities, and each activity |
| can have several intent filters. We want to only list, in the final |
| output, the activities which have a intent-filter that contains the |
| following elements: |
| |
| <action android:name="android.intent.action.MAIN" /> |
| <category android:name="android.intent.category.LAUNCHER" /> |
| ''' |
| tree = ElementTree.ElementTree(file=xmlfile) |
| root = tree.getroot() |
| launchable_activities = [] |
| for application in root.iter('application'): |
| for activity in application.iter('activity'): |
| for intent_filter in activity.iter('intent-filter'): |
| found_action_MAIN = False |
| found_category_LAUNCHER = False |
| for child in intent_filter: |
| if child.tag == 'action': |
| if True in [str(child.attrib[k]).endswith('MAIN') for k in child.attrib.keys()]: |
| found_action_MAIN = True |
| if child.tag == 'category': |
| if True in [str(child.attrib[k]).endswith('LAUNCHER') for k in child.attrib.keys()]: |
| found_category_LAUNCHER = True |
| if found_action_MAIN and found_category_LAUNCHER: |
| names = [str(activity.attrib[k]) for k in activity.attrib.keys() if str(k).endswith('name')] |
| for name in names: |
| if name[0] != '.': |
| name = '.'+name |
| launchable_activities.append(name) |
| return launchable_activities |
| |
| def main(): |
| global ADB_CMD, NDK, PROJECT |
| global JDB_CMD |
| global OPTION_START, OPTION_LAUNCH, OPTION_LAUNCH_LIST |
| global OPTION_FORCE, OPTION_EXEC, OPTION_TUI, OPTION_WAIT |
| global OPTION_STDCXXPYPR |
| global PYPRPR_BASE, PYPRPR_GNUSTDCXX_BASE |
| |
| if NDK.find(' ')!=-1: |
| error('NDK path cannot contain space') |
| handle_args() |
| if OPTION_EXEC: |
| if not os.path.isfile(OPTION_EXEC): |
| error('Invalid initialization file: %s' % (OPTION_EXEC)) |
| ADB_VERSION = subprocess.check_output([ADB_CMD, 'version'], |
| ).decode('ascii').replace('\r', '').splitlines()[0] |
| log('ADB version found: %s' % (ADB_VERSION)) |
| log('Using ADB flags: %s' % (ADB_FLAGS)) |
| if PROJECT != None: |
| PROJECT = os.path.expanduser(PROJECT) |
| log('Using specified project path: %s' % (PROJECT)) |
| if not os.path.isdir(PROJECT): |
| error('Your --project option does not point to a directory!') |
| if not os.path.isfile(PROJECT+os.sep+MANIFEST): |
| error('''Your --project does not point to an Android project path! |
| It is missing a %s file.''' % (MANIFEST)) |
| else: |
| # Assume we are in the project directory |
| if os.path.isfile(MANIFEST): |
| PROJECT = '.' |
| else: |
| PROJECT = '' |
| CURDIR = os.getcwd() |
| |
| while CURDIR != os.path.dirname(CURDIR): |
| if os.path.isfile(CURDIR+os.sep+MANIFEST): |
| PROJECT=CURDIR |
| break |
| CURDIR = os.path.dirname(CURDIR) |
| |
| if not os.path.isdir(PROJECT): |
| error('Launch this script from an application project directory, or use --project=<path>.') |
| log('Using auto-detected project path: %s' % (PROJECT)) |
| |
| PACKAGE_NAME = extract_package_name(PROJECT+os.sep+MANIFEST) |
| if PACKAGE_NAME is None: |
| PACKAGE_NAME = '<none>' |
| log('Found package name: %s' % (PACKAGE_NAME)) |
| if PACKAGE_NAME == '<none>': |
| error('''Could not extract package name from %s. |
| Please check that the file is well-formed!''' % (PROJECT+os.sep+MANIFEST)) |
| if OPTION_LAUNCH_LIST: |
| log('Extracting list of launchable activities from manifest:') |
| print(' '.join(extract_launchable(PROJECT+os.sep+MANIFEST))) |
| exit(0) |
| APP_ABIS = get_build_var('APP_ABI').split(' ') |
| if 'all' in APP_ABIS: |
| ALL_ABIS = get_build_var('NDK_ALL_ABIS').split(' ') |
| APP_ABIS = APP_ABIS[:APP_ABIS.index('all')]+ALL_ABIS+APP_ABIS[APP_ABIS.index('all')+1:] |
| log('ABIs targetted by application: %s' % (' '.join(APP_ABIS))) |
| |
| retcode, ADB_TEST = adb_cmd(True, ['shell', 'echo']) |
| if retcode != 0: |
| print(ADB_TEST) |
| error('''Could not connect to device or emulator! |
| Please check that an emulator is running or a device is connected |
| through USB to this machine. You can use -e, -d and -s <serial> |
| in case of multiple ones.''') |
| |
| retcode,API_LEVEL = adb_var_shell(['getprop', 'ro.build.version.sdk']) |
| if retcode != 0 or API_LEVEL == '': |
| error('''Could not find target device's supported API level! |
| ndk-gdb will only work if your device is running Android 2.2 or higher.''') |
| API_LEVEL = int(API_LEVEL) |
| log('Device API Level: %d' % (API_LEVEL)) |
| if API_LEVEL < 8: |
| error('''ndk-gdb requires a target device running Android 2.2 (API level 8) or higher. |
| The target device is running API level %d!''' % (API_LEVEL)) |
| COMPAT_ABI = [] |
| |
| _,CPU_ABILIST32 = adb_var_shell(['getprop', 'ro.product.cpu.abilist32']) |
| _,CPU_ABILIST64 = adb_var_shell(['getprop', 'ro.product.cpu.abilist64']) |
| # Both CPU_ABILIST32 and CPU_ABILIST64 may contain multiple comma-delimited abis. |
| # Concatanate CPU_ABILIST32 and CPU_ABILIST64. |
| CPU_ABIS = CPU_ABILIST64.split(',')+CPU_ABILIST32.split(',') |
| if not CPU_ABILIST64 and not CPU_ABILIST32: |
| _,CPU_ABI1 = adb_var_shell(['getprop', 'ro.product.cpu.abi']) |
| _,CPU_ABI2 = adb_var_shell(['getprop', 'ro.product.cpu.abi2']) |
| CPU_ABIS = CPU_ABI1.split(',')+CPU_ABI2.split(',') |
| |
| log('Device CPU ABIs: %s' % (' '.join(CPU_ABIS))) |
| |
| device_bits = 32 |
| if len(CPU_ABILIST64): |
| device_bits = 64 |
| |
| app_bits = 32 |
| # First look compatible ABI in the list of 64-bit ABIs. |
| COMPAT_ABI = [ABI for ABI in CPU_ABILIST64.split(',') if ABI in APP_ABIS] |
| if len(COMPAT_ABI): |
| # We found compatible ABI and it is 64-bit. |
| app_bits = 64 |
| else: |
| # If we found nothing - look among 32-bit ABIs. |
| COMPAT_ABI = [ABI for ABI in CPU_ABILIST32.split(',') if ABI in APP_ABIS] |
| if not len(COMPAT_ABI): |
| # Lastly, lets check ro.product.cpu.abi and ro.product.cpu.abi2 |
| COMPAT_ABI = [ABI for ABI in CPU_ABIS if ABI in APP_ABIS] |
| |
| if not len(COMPAT_ABI): |
| error('''The device does not support the application's targetted CPU ABIs! |
| Device supports: %s |
| Package supports: %s''' % (' '.join(CPU_ABIS),' '.join(APP_ABIS))) |
| COMPAT_ABI = COMPAT_ABI[0] |
| log('Compatible device ABI: %s' % (COMPAT_ABI)) |
| GDBSETUP_INIT = get_build_var_for_abi('NDK_APP_GDBSETUP', COMPAT_ABI) |
| GDBSETUP_INIT = os.path.normpath(os.path.join(PROJECT, GDBSETUP_INIT)) |
| log('Using gdb setup init: %s' % (GDBSETUP_INIT)) |
| |
| APP_OUT = get_build_var_for_abi('TARGET_OUT', COMPAT_ABI) |
| APP_OUT = os.path.normpath(os.path.join(PROJECT, APP_OUT)) |
| if os.name == "nt": |
| APP_OUT = APP_OUT.replace("\\", "/") |
| log('Using app out directory: %s' % (APP_OUT)) |
| DEBUGGABLE = extract_debuggable(PROJECT+os.sep+MANIFEST) |
| log('Found debuggable flag: %s' % ('true' if DEBUGGABLE==True else 'false')) |
| |
| # Find the <dataDir> of the package on the device |
| retcode,DATA_DIR = adb_var_shell2(['run-as', PACKAGE_NAME, '/system/bin/sh', '-c', 'pwd']) |
| if retcode or DATA_DIR == '': |
| error('''Could not find package's data directory. Are you sure that |
| the application is installed and debuggable?''') |
| log("Found data directory: '%s'" % (DATA_DIR)) |
| |
| # Let's check that 'gdbserver' is properly installed on the device too. If this |
| # is not the case, push 'gdbserver' found in prebuilt. |
| # |
| LIB_DIR = '%s/lib' % (DATA_DIR) |
| device_gdbserver = '%s/gdbserver' % (LIB_DIR) |
| retcode, LS_GDBSERVER = adb_var_shell2(['run-as', PACKAGE_NAME, 'ls', device_gdbserver]) |
| if not retcode: |
| # TODO: Make sure that the existing gdbserver matches the size of our current one |
| log('Found device gdbserver: %s' % (device_gdbserver)) |
| else: |
| gdbserver_prebuilt_path = '%s/prebuilt/android-%s/gdbserver/gdbserver' |
| prebuilt_arch = "android-%s" % get_platform_arch(COMPAT_ABI) |
| gdbserver_prebuilt_path = os.path.join(NDK, 'prebuilt', prebuilt_arch, |
| 'gdbserver', 'gdbserver') |
| if os.path.isfile(gdbserver_prebuilt_path): |
| log('Found prebuilt gdbserver, copying it to the device') |
| |
| # DATA_DIR/lib is probably read-only |
| device_gdbserver = '/data/local/tmp/gdbserver' |
| push_args = ['push', gdbserver_prebuilt_path, device_gdbserver] |
| retcode, PUSH_OUTPUT = adb_cmd(True, push_args) |
| if retcode: |
| error('Could not copy prebuilt gdbserver to the device') |
| |
| if API_LEVEL >= 23: |
| # Copy gdbserver to the data directory since selinux prevents |
| # executing binaries in /data/local/tmp on M+, and we can't |
| # copy directly into the data directory because of permissions. |
| # |
| # Don't do this pre-M to avoid a ~100ms adb shell roundtrip. |
| temp_gdbserver = device_gdbserver |
| device_gdbserver = '%s/gdbserver' % (DATA_DIR) |
| args = ['cat', temp_gdbserver, '|', 'run-as', PACKAGE_NAME, |
| 'sh', '-c', "'cat > %s'" % device_gdbserver] |
| retcode, _ = adb_var_shell2(args) |
| if retcode: |
| error('Failed to copy gdbserver to application directory') |
| |
| # Check if gdbserver is runnable by the application user |
| retcode, _ = adb_var_shell2(['run-as', PACKAGE_NAME, |
| device_gdbserver, '--version']) |
| if retcode: |
| error('''Could not run the prebuilt gdbserver on the device, |
| please ensure that the installed application is debuggable.''') |
| else: |
| error('Cannot find prebuilt gdbserver for ABI \'%s\'' % COMPAT_ABI) |
| |
| # Launch the activity if needed |
| if OPTION_START: |
| if not OPTION_LAUNCH: |
| OPTION_LAUNCH = extract_launchable(PROJECT+os.sep+MANIFEST) |
| if not len(OPTION_LAUNCH): |
| error('''Could not extract name of launchable activity from manifest! |
| Try to use --launch=<name> directly instead as a work-around.''') |
| log('Found first launchable activity: %s' % (OPTION_LAUNCH[0])) |
| if not len(OPTION_LAUNCH): |
| error('''It seems that your Application does not have any launchable activity! |
| Please fix your manifest file and rebuild/re-install your application.''') |
| |
| if OPTION_LAUNCH: |
| log('Launching activity: %s/%s' % (PACKAGE_NAME,OPTION_LAUNCH[0])) |
| retcode,LAUNCH_OUTPUT=adb_cmd(True, |
| ['shell', 'am', 'start'] + OPTION_WAIT + ['-n', '%s/%s' % (PACKAGE_NAME,OPTION_LAUNCH[0])], |
| log_command=True) |
| if retcode: |
| error('''Could not launch specified activity: %s |
| Use --launch-list to dump a list of valid values.''' % (OPTION_LAUNCH[0])) |
| |
| # Sleep a bit, it sometimes take one second to start properly |
| # Note that we use the 'sleep' command on the device here. |
| # |
| adb_cmd(True, ['shell', 'sleep', '%f' % (DELAY)], log_command=True) |
| |
| processes = get_processes() |
| |
| # Find the PID of the application being run |
| if PACKAGE_NAME not in processes: |
| if OPTION_LAUNCH: |
| error('''Could not extract PID of application on device/emulator. |
| Weird, this probably means one of these: |
| |
| - The installed package does not match your current manifest. |
| - The application process was terminated. |
| |
| Try using the --verbose option and look at its output for details.''') |
| else: |
| error('''Could not extract PID of application on device/emulator. |
| Are you sure the application is already started? |
| Consider using --start or --launch=<name> if not.''') |
| |
| if len(processes[PACKAGE_NAME]) > 1: |
| error('Multiple processes found for %s: %s' % (PACKAGE_NAME, processes[PACKAGE_NAME])) |
| |
| pid = processes[PACKAGE_NAME][0] |
| log('Found running PID: %d' % (pid)) |
| |
| # Check that there is no other instance of gdbserver running |
| if 'lib/gdbserver' in processes: |
| if not OPTION_FORCE: |
| error('Another debug session running, Use --force to kill it.') |
| log('Killing existing debugging session') |
| for gdbserver_pid in processes['lib/gdbserver']: |
| adb_cmd(False, ['shell', 'kill -9 %s' % (gdbserver_pid)]) |
| |
| # Launch gdbserver now |
| DEBUG_SOCKET = 'debug-socket' |
| adb_cmd(False, |
| ['shell', 'run-as', PACKAGE_NAME, device_gdbserver, '+%s' % (DEBUG_SOCKET), '--attach', str(pid)], |
| log_command=True, adb_trace=True, background=True) |
| log('Launched gdbserver succesfully.') |
| |
| # Setup network redirection |
| log('Setup network redirection') |
| retcode,_ = adb_cmd(False, |
| ['forward', 'tcp:%d' % (DEBUG_PORT), 'localfilesystem:%s/%s' % (DATA_DIR,DEBUG_SOCKET)], |
| log_command=True) |
| if retcode: |
| error('''Could not setup network redirection to gdbserver? |
| Maybe using --port=<port> to use a different TCP port might help?''') |
| |
| # If we are debugging 64-bit app, then we need to pull linker64, |
| # app_process64 and libc.so from lib64 directory. |
| linker_name = 'linker' |
| libdir_name = 'lib' |
| app_process_name = 'app_process32' |
| if (app_bits == 64): |
| linker_name = 'linker64' |
| libdir_name = 'lib64' |
| app_process_name = 'app_process64' |
| else: |
| retcode,_ = adb_cmd(False, ['shell', 'test -e /system/bin/%s' % (app_process_name)]) |
| if retcode: |
| # Old 32-bit devices do not have app_process32. Pull |
| # app_process in this case |
| app_process_name = 'app_process' |
| |
| # Get the app_server binary from the device |
| pulled_app_process = '%s/app_process' % (APP_OUT) |
| adb_cmd(False, ['pull', '/system/bin/%s' % (app_process_name), pulled_app_process], log_command=True) |
| log('Pulled %s from device/emulator.' % (app_process_name)) |
| |
| pulled_linker = '%s/%s' % (APP_OUT, linker_name) |
| adb_cmd(False, ['pull', '/system/bin/%s' % (linker_name), pulled_linker], log_command=True) |
| log('Pulled %s from device/emulator.' % (linker_name)) |
| |
| pulled_libc = '%s/libc.so' % (APP_OUT) |
| adb_cmd(False, ['pull', '/system/%s/libc.so' % libdir_name, pulled_libc], log_command=True) |
| log('Pulled /system/%s/libc.so from device/emulator.' % libdir_name) |
| |
| # Setup JDB connection, for --start or --launch |
| if (OPTION_START != None or OPTION_LAUNCH != None) and len(OPTION_WAIT): |
| log('Set up JDB connection, using jdb command: %s' % JDB_CMD) |
| retcode,_ = adb_cmd(False, |
| ['forward', 'tcp:%d' % (JDB_PORT), 'jdwp:%d' % (pid)], |
| log_command=True) |
| if retcode: |
| error('Could not forward JDB port') |
| time.sleep(1.0) |
| background_spawn([JDB_CMD,'-connect','com.sun.jdi.SocketAttach:hostname=localhost,port=%d' % (JDB_PORT)], True, output_jdb, True, input_jdb) |
| time.sleep(1.0) |
| |
| # Work out the python pretty printer details. |
| pypr_folder = None |
| pypr_function = None |
| |
| # Automatic determination of pypr. |
| if OPTION_STDCXXPYPR == 'auto': |
| libdir = os.path.join(PROJECT,'libs',COMPAT_ABI) |
| libs = [ f for f in os.listdir(libdir) |
| if os.path.isfile(os.path.join(libdir, f)) and f.endswith('.so') ] |
| if 'libstlport_shared.so' in libs: |
| OPTION_STDCXXPYPR = 'stlport' |
| elif 'libgnustl_shared.so' in libs: |
| OPTION_STDCXXPYPR = 'gnustdcxx' |
| |
| if OPTION_STDCXXPYPR == 'stlport': |
| pypr_folder = PYPRPR_BASE + 'stlport/gppfs-0.2/stlport' |
| pypr_function = 'register_stlport_printers' |
| elif OPTION_STDCXXPYPR.startswith('gnustdcxx'): |
| if OPTION_STDCXXPYPR == 'gnustdcxx': |
| NDK_TOOLCHAIN_VERSION = get_build_var_for_abi('NDK_TOOLCHAIN_VERSION', COMPAT_ABI) |
| log('Using toolchain version: %s' % (NDK_TOOLCHAIN_VERSION)) |
| pypr_folder = PYPRPR_GNUSTDCXX_BASE + 'gcc-' + NDK_TOOLCHAIN_VERSION |
| else: |
| pypr_folder = PYPRPR_GNUSTDCXX_BASE + OPTION_STDCXXPYPR.replace('gnustdcxx-','gcc-') |
| pypr_function = 'register_libstdcxx_printers' |
| |
| # Now launch the appropriate gdb client with the right init commands |
| # |
| GDBCLIENT = '{}/gdb'.format(ndk_bin_path(NDK)) |
| GDBSETUP = '%s/gdb.setup' % (APP_OUT) |
| shutil.copyfile(GDBSETUP_INIT, GDBSETUP) |
| with open(GDBSETUP, "a") as gdbsetup: |
| #uncomment the following to debug the remote connection only |
| #gdbsetup.write('set debug remote 1\n') |
| gdbsetup.write('file '+pulled_app_process+'\n') |
| gdbsetup.write('target remote :%d\n' % (DEBUG_PORT)) |
| gdbsetup.write('set breakpoint pending on\n') |
| |
| if pypr_function: |
| gdbsetup.write('python\n') |
| gdbsetup.write('import sys\n') |
| gdbsetup.write('sys.path.append("%s")\n' % pypr_folder) |
| gdbsetup.write('from printers import %s\n' % pypr_function) |
| gdbsetup.write('%s(None)\n' % pypr_function) |
| gdbsetup.write('end\n') |
| |
| if OPTION_EXEC: |
| with open(OPTION_EXEC, 'r') as execfile: |
| for line in execfile: |
| gdbsetup.write(line) |
| |
| gdbargs = [GDBCLIENT, '-x', '%s' % (GDBSETUP)] |
| if OPTION_TUI: |
| gdbhelp = subprocess.check_output([GDBCLIENT, '--help']).decode('ascii') |
| try: |
| gdbhelp.index('--tui') |
| gdbargs.append('--tui') |
| OPTION_TUI = 'running' |
| except: |
| print('Warning: Disabled tui mode as %s does not support it' % (os.path.basename(GDBCLIENT))) |
| gdbp = subprocess.Popen(gdbargs) |
| while gdbp.returncode is None: |
| try: |
| gdbp.communicate() |
| except KeyboardInterrupt: |
| pass |
| log("Exited gdb, returncode %d" % gdbp.returncode) |
| |
| if __name__ == '__main__': |
| main() |