1#!/usr/bin/env python
2
3r'''
4 Copyright (C) 2010 The Android Open Source Project
5 Copyright (C) 2012 Ray Donnelly <mingw.android@gmail.com>
6
7 Licensed under the Apache License, Version 2.0 (the "License");
8 you may not use this file except in compliance with the License.
9 You may obtain a copy of the License at
10
11      http://www.apache.org/licenses/LICENSE-2.0
12
13 Unless required by applicable law or agreed to in writing, software
14 distributed under the License is distributed on an "AS IS" BASIS,
15 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 See the License for the specific language governing permissions and
17 limitations under the License.
18
19
20 This wrapper script is used to launch a native debugging session
21 on a given NDK application. The application must be debuggable, i.e.
22 its android:debuggable attribute must be set to 'true' in the
23 <application> element of its manifest.
24
25 See docs/NDK-GDB.TXT for usage description. Essentially, you just
26 need to launch ndk-gdb-py from your application project directory
27 after doing ndk-build && ant debug && \
28  adb install && <start-application-on-device>
29'''
30
31import sys, os, platform, argparse, subprocess, types
32import xml.etree.cElementTree as ElementTree
33import shutil, time
34from threading import Thread
35try:
36    from Queue import Queue, Empty
37except ImportError:
38    from queue import Queue, Empty  # python 3.x
39
40def find_program(program, extra_paths = []):
41    ''' extra_paths are searched before PATH '''
42    PATHS = extra_paths+os.environ['PATH'].replace('"','').split(os.pathsep)
43    exts = ['']
44    if sys.platform.startswith('win'):
45        exts += ['.exe', '.bat', '.cmd']
46    for path in PATHS:
47        if os.path.isdir(path):
48            for ext in exts:
49                full = path + os.sep + program + ext
50                if os.path.isfile(full):
51                    return True, full
52    return False, None
53
54def ndk_bin_path(ndk):
55    '''
56    Return the prebuilt bin path for the host OS.
57
58    If Python executable is the NDK-prebuilt one (it should be)
59    then use the location of the executable as the first guess.
60    We take the grand-parent foldername and then ensure that it
61    starts with one of 'linux', 'darwin' or 'windows'.
62
63    If this is not the case, then we're using some other Python
64    and fall-back to using platform.platform() and sys.maxsize.
65    '''
66
67    try:
68        ndk_host = os.path.basename(
69                    os.path.dirname(
70                     os.path.dirname(sys.executable)))
71    except:
72        ndk_host = ''
73    # NDK-prebuilt Python?
74    if (not ndk_host.startswith('linux') and
75        not ndk_host.startswith('darwin') and
76        not ndk_host.startswith('windows')):
77        is64bit = True if sys.maxsize > 2**32 else False
78        if platform.platform().startswith('Linux'):
79            ndk_host = 'linux%s' % ('-x86_64' if is64bit else '-x86')
80        elif platform.platform().startswith('Darwin'):
81            ndk_host = 'darwin%s' % ('-x86_64' if is64bit else '-x86')
82        elif platform.platform().startswith('Windows'):
83            ndk_host = 'windows%s' % ('-x86_64' if is64bit else '')
84        else:
85            ndk_host = 'UNKNOWN'
86    return ndk+os.sep+'prebuilt'+os.sep+ndk_host+os.sep+'bin'
87
88VERBOSE = False
89PROJECT = None
90ADB_CMD = None
91GNUMAKE_CMD = None
92JDB_CMD = None
93# Extra arguments passed to the NDK build system when
94# querying it.
95GNUMAKE_FLAGS = []
96
97OPTION_FORCE = None
98OPTION_EXEC = None
99OPTION_START = None
100OPTION_LAUNCH = None
101OPTION_LAUNCH_LIST = None
102OPTION_TUI = None
103OPTION_WAIT = ['-D']
104OPTION_STDCXXPYPR = None
105
106PYPRPR_BASE = sys.prefix + '/share/pretty-printers/'
107PYPRPR_GNUSTDCXX_BASE = PYPRPR_BASE + 'libstdcxx/'
108
109DEBUG_PORT = 5039
110JDB_PORT = 65534
111
112# Name of the manifest file
113MANIFEST = 'AndroidManifest.xml'
114
115# Delay in seconds between launching the activity and attaching gdbserver on it.
116# This is needed because there is no way to know when the activity has really
117# started, and sometimes this takes a few seconds.
118#
119DELAY = 2.0
120NDK = os.path.abspath(os.path.dirname(sys.argv[0])).replace('\\','/')
121DEVICE_SERIAL = ''
122ADB_FLAGS = ''
123
124def log(string):
125    global VERBOSE
126    if VERBOSE:
127        print(string)
128
129def error(string, errcode=1):
130    print('ERROR: %s' % (string))
131    exit(errcode)
132
133def handle_args():
134    global VERBOSE, DEBUG_PORT, DELAY, DEVICE_SERIAL
135    global GNUMAKE_CMD, GNUMAKE_FLAGS
136    global ADB_CMD, ADB_FLAGS
137    global JDB_CMD
138    global PROJECT, NDK
139    global OPTION_START, OPTION_LAUNCH, OPTION_LAUNCH_LIST
140    global OPTION_FORCE, OPTION_EXEC, OPTION_TUI, OPTION_WAIT
141    global OPTION_STDCXXPYPR
142    global PYPRPR_GNUSTDCXX_BASE
143
144    parser = argparse.ArgumentParser(description='''
145Setup a gdb debugging session for your Android NDK application.
146Read ''' + NDK + '''/docs/NDK-GDB.html for complete usage instructions.''',
147    formatter_class=argparse.RawTextHelpFormatter)
148
149    parser.add_argument( '--verbose',
150                         help='Enable verbose mode', action='store_true', dest='verbose')
151
152    parser.add_argument( '--force',
153                         help='Kill existing debug session if it exists',
154                         action='store_true')
155
156    parser.add_argument( '--start',
157                         help='Launch application instead of attaching to existing one',
158                         action='store_true')
159
160    parser.add_argument( '--launch',
161                         help='Same as --start, but specify activity name (see below)',
162                         dest='launch_name', nargs=1)
163
164    parser.add_argument( '--launch-list',
165                         help='List all launchable activity names from manifest',
166                         action='store_true')
167
168    parser.add_argument( '--delay',
169                         help='Delay in seconds between activity start and gdbserver attach',
170                         type=float, default=DELAY,
171                         dest='delay')
172
173    parser.add_argument( '-p', '--project',
174                         help='Specify application project path',
175                         dest='project')
176
177    parser.add_argument( '--port',
178                         help='Use tcp:localhost:<DEBUG_PORT> to communicate with gdbserver',
179                         type=int, default=DEBUG_PORT,
180                         dest='debug_port')
181
182    parser.add_argument( '-x', '--exec',
183                         help='Execute gdb initialization commands in <EXEC_FILE> after connection',
184                         dest='exec_file')
185
186    parser.add_argument( '--adb',
187                         help='Use specific adb command',
188                         dest='adb_cmd')
189
190    parser.add_argument( '--awk',
191                         help='Use specific awk command (unused flag retained for compatability)')
192
193    parser.add_argument( '-e',
194                         help='Connect to single emulator instance....(either this,)',
195                         action='store_true', dest='emulator')
196
197    parser.add_argument( '-d',
198                         help='Connect to single target device........(this,)',
199                         action='store_true', dest='device')
200
201    parser.add_argument( '-s',
202                         help='Connect to specific emulator or device.(or this)',
203                         default=DEVICE_SERIAL,
204                         dest='device_serial')
205
206    parser.add_argument( '-t','--tui',
207                         help='Use tui mode',
208                         action='store_true', dest='tui')
209
210    parser.add_argument( '--gnumake-flag',
211                         help='Flag to pass to gnumake, e.g. NDK_TOOLCHAIN_VERSION=4.8',
212                         action='append', dest='gnumake_flags')
213
214    parser.add_argument( '--nowait',
215                         help='Do not wait for debugger to attach (may miss early JNI breakpoints)',
216                         action='store_true', dest='nowait')
217
218    if os.path.isdir(PYPRPR_GNUSTDCXX_BASE):
219        stdcxx_pypr_versions = [ 'gnustdcxx'+d.replace('gcc','')
220                                 for d in os.listdir(PYPRPR_GNUSTDCXX_BASE)
221                                 if os.path.isdir(os.path.join(PYPRPR_GNUSTDCXX_BASE, d)) ]
222    else:
223        stdcxx_pypr_versions = []
224
225    parser.add_argument( '--stdcxx-py-pr',
226                         help='Specify stdcxx python pretty-printer',
227                         choices=['auto', 'none', 'gnustdcxx'] + stdcxx_pypr_versions + ['stlport'],
228                         default='none', dest='stdcxxpypr')
229
230    args = parser.parse_args()
231
232    VERBOSE = args.verbose
233
234    ndk_bin = ndk_bin_path(NDK)
235    (found_adb,     ADB_CMD)     = find_program('adb',    [ndk_bin])
236    (found_gnumake, GNUMAKE_CMD) = find_program('make',   [ndk_bin])
237    (found_jdb,     JDB_CMD)     = find_program('jdb',    [])
238
239    if not found_gnumake:
240        error('Failed to find GNU make')
241
242    log('Android NDK installation path: %s' % (NDK))
243
244    if args.device:
245        ADB_FLAGS = '-d'
246    if args.emulator:
247        if ADB_FLAGS != '':
248            parser.print_help()
249            exit(1)
250        ADB_FLAGS = '-e'
251    if args.device_serial != '':
252        DEVICE_SERIAL = args.device_serial
253        if ADB_FLAGS != '':
254            parser.print_help()
255            exit(1)
256        ADB_FLAGS = '-s'
257    if args.adb_cmd != None:
258        log('Using specific adb command: %s' % (args.adb_cmd))
259        ADB_CMD = args.adb_cmd
260    if ADB_CMD is None:
261        error('''The 'adb' tool is not in your path.
262       You can change your PATH variable, or use
263       --adb=<executable> to point to a valid one.''')
264    if not os.path.isfile(ADB_CMD):
265        error('Could not run ADB with: %s' % (ADB_CMD))
266
267    if args.project != None:
268        PROJECT = args.project
269
270    if args.start != None:
271        OPTION_START = args.start
272
273    if args.launch_name != None:
274        OPTION_LAUNCH = args.launch_name
275
276    if args.launch_list != None:
277        OPTION_LAUNCH_LIST = args.launch_list
278
279    if args.force != None:
280        OPTION_FORCE = args.force
281
282    if args.exec_file != None:
283        OPTION_EXEC = args.exec_file
284
285    if args.tui != False:
286        OPTION_TUI = True
287
288    if args.delay != None:
289        DELAY = args.delay
290
291    if args.gnumake_flags != None:
292        GNUMAKE_FLAGS = args.gnumake_flags
293
294    if args.nowait == True:
295        OPTION_WAIT = []
296    elif not found_jdb:
297        error('Failed to find jdb.\n..you can use --nowait to disable jdb\n..but may miss early breakpoints.')
298
299    OPTION_STDCXXPYPR = args.stdcxxpypr
300
301def get_build_var(var):
302    global GNUMAKE_CMD, GNUMAKE_FLAGS, NDK, PROJECT
303    text = subprocess.check_output([GNUMAKE_CMD,
304                                  '--no-print-dir',
305                                  '-f',
306                                  NDK+'/build/core/build-local.mk',
307                                  '-C',
308                                  PROJECT,
309                                  'DUMP_'+var] + GNUMAKE_FLAGS
310                                  )
311    # replace('\r', '') due to Windows crlf (\r\n)
312    #  ...universal_newlines=True causes bytes to be returned
313    #     rather than a str
314    return text.decode('ascii').replace('\r', '').splitlines()[0]
315
316def get_build_var_for_abi(var, abi):
317    global GNUMAKE_CMD, GNUMAKE_FLAGS, NDK, PROJECT
318    text = subprocess.check_output([GNUMAKE_CMD,
319                                   '--no-print-dir',
320                                   '-f',
321                                   NDK+'/build/core/build-local.mk',
322                                   '-C',
323                                   PROJECT,
324                                   'DUMP_'+var,
325                                   'APP_ABI='+abi] + GNUMAKE_FLAGS,
326                                   )
327    return text.decode('ascii').replace('\r', '').splitlines()[0]
328
329# Silent if gdb is running in tui mode to keep things tidy.
330def output_gdbserver(text):
331    if not OPTION_TUI or OPTION_TUI != 'running':
332        print(text)
333
334# Likewise, silent in tui mode (also prepends 'JDB :: ')
335def output_jdb(text):
336    if not OPTION_TUI or OPTION_TUI != 'running':
337        print('JDB :: %s' % text)
338
339def input_jdb(inhandle):
340    while True:
341        inhandle.write('\n')
342        time.sleep(1.0)
343
344def background_spawn(args, redirect_stderr, output_fn, redirect_stdin = None, input_fn = None):
345
346    def async_stdout(outhandle, queue, output_fn):
347        for line in iter(outhandle.readline, b''):
348            output_fn(line.replace('\r', '').replace('\n', ''))
349        outhandle.close()
350
351    def async_stderr(outhandle, queue, output_fn):
352        for line in iter(outhandle.readline, b''):
353            output_fn(line.replace('\r', '').replace('\n', ''))
354        outhandle.close()
355
356    def async_stdin(inhandle, queue, input_fn):
357        input_fn(inhandle)
358        inhandle.close()
359
360    if redirect_stderr:
361        used_stderr = subprocess.PIPE
362    else:
363        used_stderr = subprocess.STDOUT
364    if redirect_stdin:
365        used_stdin = subprocess.PIPE
366    else:
367        used_stdin = None
368    p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=used_stderr, stdin=used_stdin,
369                         bufsize=1, close_fds='posix' in sys.builtin_module_names)
370    qo = Queue()
371    to = Thread(target=async_stdout, args=(p.stdout, qo, output_fn))
372    to.daemon = True
373    to.start()
374    if redirect_stderr:
375        te = Thread(target=async_stderr, args=(p.stderr, qo, output_fn))
376        te.daemon = True
377        te.start()
378    if redirect_stdin:
379        ti = Thread(target=async_stdin, args=(p.stdin, qo, input_fn))
380        ti.daemon = True
381        ti.start()
382
383def adb_cmd(redirect_stderr, args, log_command=False, adb_trace=False, background=False):
384    global ADB_CMD, ADB_FLAGS, DEVICE_SERIAL
385    fullargs = [ADB_CMD]
386    if ADB_FLAGS != '':
387        fullargs += [ADB_FLAGS]
388    if DEVICE_SERIAL != '':
389        fullargs += [DEVICE_SERIAL]
390    if isinstance(args, str):
391        fullargs.append(args)
392    else:
393        fullargs += [arg for arg in args]
394    new_env = os.environ.copy()
395    retval = 0
396    if adb_trace:
397        new_env["ADB_TRACE"] = "1"
398    if background:
399        if log_command:
400            log('## COMMAND: adb_cmd %s [BACKGROUND]' % (' '.join(args)))
401        background_spawn(fullargs, redirect_stderr, output_gdbserver)
402        return 0, ''
403    else:
404        if log_command:
405            log('## COMMAND: adb_cmd %s' % (' '.join(args)))
406        try:
407            if redirect_stderr:
408                text = subprocess.check_output(fullargs,
409                                               stderr=subprocess.STDOUT,
410                                               env=new_env
411                                               )
412            else:
413                text = subprocess.check_output(fullargs,
414                                               env=new_env
415                                               )
416        except subprocess.CalledProcessError as e:
417            retval = e.returncode
418            text = e.output
419        # rstrip() because of final newline.
420        return retval, text.decode('ascii').replace('\r', '').rstrip()
421
422def _adb_var_shell(args, redirect_stderr=False, log_command=True):
423    if log_command:
424        log('## COMMAND: adb_cmd shell %s' % (' '.join(args)))
425    arg_str = str(' '.join(args)+' ; echo $?')
426    adb_ret,output = adb_cmd(redirect_stderr=redirect_stderr,
427                           args=['shell', arg_str], log_command=False)
428    output = output.splitlines()
429    retcode = int(output.pop())
430    return retcode,'\n'.join(output)
431
432def adb_var_shell(args, log_command=False):
433    return _adb_var_shell(args, redirect_stderr=False, log_command=log_command)
434
435def adb_var_shell2(args, log_command=False):
436    return _adb_var_shell(args, redirect_stderr=True, log_command=log_command)
437
438# Return the PID of a given package or program, or 0 if it doesn't run
439# $1: Package name ("com.example.hellojni") or program name ("/lib/gdbserver")
440# Out: PID number, or 0 if not running
441#
442def get_pid_of(package_name):
443    '''
444    Some custom ROMs use busybox instead of toolbox for ps.
445    Without -w, busybox truncates the output, and very long
446    package names like com.exampleisverylongtoolongbyfar.plasma
447    exceed the limit.
448    '''
449    ps_command = 'ps'
450    retcode,output = adb_cmd(False, ['shell', 'readlink $(which ps)'])
451    if output:
452        output = output.replace('\r', '').splitlines()[0]
453    if output == 'busybox':
454        ps_command = 'ps -w'
455    retcode,output = adb_cmd(False,['shell', ps_command])
456    output = output.replace('\r', '').splitlines()
457    columns = output.pop(0).split()
458    try:
459        PID_column = columns.index('PID')
460    except:
461        PID_column = 1
462    while output:
463        columns = output.pop().split()
464        if columns.pop() == package_name:
465            return 0,int(columns[PID_column])
466    return 1,0
467
468def extract_package_name(xmlfile):
469    '''
470    The name itself is the value of the 'package' attribute in the
471    'manifest' element.
472    '''
473    tree = ElementTree.ElementTree(file=xmlfile)
474    root = tree.getroot()
475    if 'package' in root.attrib:
476        return root.attrib['package']
477    return None
478
479def extract_debuggable(xmlfile):
480    '''
481    simply extract the 'android:debuggable' attribute value from
482    the first <manifest><application> element we find.
483    '''
484    tree = ElementTree.ElementTree(file=xmlfile)
485    root = tree.getroot()
486    for application in root.iter('application'):
487        for k in application.attrib.keys():
488            if str(k).endswith('debuggable'):
489                return application.attrib[k] == 'true'
490    return False
491
492def extract_launchable(xmlfile):
493    '''
494    A given application can have several activities, and each activity
495    can have several intent filters. We want to only list, in the final
496    output, the activities which have a intent-filter that contains the
497    following elements:
498
499      <action android:name="android.intent.action.MAIN" />
500      <category android:name="android.intent.category.LAUNCHER" />
501    '''
502    tree = ElementTree.ElementTree(file=xmlfile)
503    root = tree.getroot()
504    launchable_activities = []
505    for application in root.iter('application'):
506        for activity in application.iter('activity'):
507            for intent_filter in activity.iter('intent-filter'):
508                found_action_MAIN = False
509                found_category_LAUNCHER = False
510                for child in intent_filter:
511                    if child.tag == 'action':
512                        if True in [str(child.attrib[k]).endswith('MAIN') for k in child.attrib.keys()]:
513                            found_action_MAIN = True
514                    if child.tag == 'category':
515                        if True in [str(child.attrib[k]).endswith('LAUNCHER') for k in child.attrib.keys()]:
516                            found_category_LAUNCHER = True
517                if found_action_MAIN and found_category_LAUNCHER:
518                    names = [str(activity.attrib[k]) for k in activity.attrib.keys() if str(k).endswith('name')]
519                    for name in names:
520                        if name[0] != '.':
521                            name = '.'+name
522                        launchable_activities.append(name)
523    return launchable_activities
524
525def main():
526    global ADB_CMD, NDK, PROJECT
527    global JDB_CMD
528    global OPTION_START, OPTION_LAUNCH, OPTION_LAUNCH_LIST
529    global OPTION_FORCE, OPTION_EXEC, OPTION_TUI, OPTION_WAIT
530    global OPTION_STDCXXPYPR
531    global PYPRPR_BASE, PYPRPR_GNUSTDCXX_BASE
532
533    if NDK.find(' ')!=-1:
534        error('NDK path cannot contain space')
535    handle_args()
536    if OPTION_EXEC:
537        if not os.path.isfile(OPTION_EXEC):
538            error('Invalid initialization file: %s' % (OPTION_EXEC))
539    ADB_VERSION = subprocess.check_output([ADB_CMD, 'version'],
540                                        ).decode('ascii').replace('\r', '').splitlines()[0]
541    log('ADB version found: %s' % (ADB_VERSION))
542    if DEVICE_SERIAL == '':
543        log('Using ADB flags: %s' % (ADB_FLAGS))
544    else:
545        log('Using ADB flags: %s "%s"' % (ADB_FLAGS,DEVICE_SERIAL))
546    if PROJECT != None:
547        log('Using specified project path: %s' % (PROJECT))
548        if not os.path.isdir(PROJECT):
549            error('Your --project option does not point to a directory!')
550        if not os.path.isfile(PROJECT+os.sep+MANIFEST):
551            error('''Your --project does not point to an Android project path!
552       It is missing a %s file.''' % (MANIFEST))
553    else:
554        # Assume we are in the project directory
555        if os.path.isfile(MANIFEST):
556            PROJECT = '.'
557        else:
558            PROJECT = ''
559            CURDIR = os.getcwd()
560
561            while CURDIR != os.path.dirname(CURDIR):
562                if os.path.isfile(CURDIR+os.sep+MANIFEST):
563                    PROJECT=CURDIR
564                    break
565                CURDIR = os.path.dirname(CURDIR)
566
567            if not os.path.isdir(PROJECT):
568                error('Launch this script from an application project directory, or use --project=<path>.')
569        log('Using auto-detected project path: %s' % (PROJECT))
570
571    PACKAGE_NAME = extract_package_name(PROJECT+os.sep+MANIFEST)
572    if PACKAGE_NAME is None:
573        PACKAGE_NAME = '<none>'
574    log('Found package name: %s' % (PACKAGE_NAME))
575    if PACKAGE_NAME == '<none>':
576        error('''Could not extract package name from %s.
577       Please check that the file is well-formed!''' % (PROJECT+os.sep+MANIFEST))
578    if OPTION_LAUNCH_LIST:
579        log('Extracting list of launchable activities from manifest:')
580        print(' '.join(extract_launchable(PROJECT+os.sep+MANIFEST)))
581        exit(0)
582    APP_ABIS = get_build_var('APP_ABI').split(' ')
583    if 'all' in APP_ABIS:
584        ALL_ABIS = get_build_var('NDK_ALL_ABIS').split(' ')
585        APP_ABIS = APP_ABIS[:APP_ABIS.index('all')]+ALL_ABIS+APP_ABIS[APP_ABIS.index('all')+1:]
586    log('ABIs targetted by application: %s' % (' '.join(APP_ABIS)))
587
588    retcode,ADB_TEST = adb_cmd(True,['shell', 'ls'])
589    if retcode != 0:
590        print(ADB_TEST)
591        error('''Could not connect to device or emulator!
592       Please check that an emulator is running or a device is connected
593       through USB to this machine. You can use -e, -d and -s <serial>
594       in case of multiple ones.''')
595
596    retcode,API_LEVEL = adb_var_shell(['getprop', 'ro.build.version.sdk'])
597    if retcode != 0 or API_LEVEL == '':
598        error('''Could not find target device's supported API level!
599ndk-gdb will only work if your device is running Android 2.2 or higher.''')
600    API_LEVEL = int(API_LEVEL)
601    log('Device API Level: %d' % (API_LEVEL))
602    if API_LEVEL < 8:
603        error('''ndk-gdb requires a target device running Android 2.2 (API level 8) or higher.
604The target device is running API level %d!''' % (API_LEVEL))
605    COMPAT_ABI = []
606
607    _,CPU_ABILIST32 = adb_var_shell(['getprop', 'ro.product.cpu.abilist32'])
608    _,CPU_ABILIST64 = adb_var_shell(['getprop', 'ro.product.cpu.abilist64'])
609    # Both CPU_ABILIST32 and CPU_ABILIST64 may contain multiple comma-delimited abis.
610    # Concatanate CPU_ABILIST32 and CPU_ABILIST64.
611    CPU_ABIS = CPU_ABILIST64.split(',')+CPU_ABILIST32.split(',')
612    if not CPU_ABILIST64 and not CPU_ABILIST32:
613        _,CPU_ABI1 = adb_var_shell(['getprop', 'ro.product.cpu.abi'])
614        _,CPU_ABI2 = adb_var_shell(['getprop', 'ro.product.cpu.abi2'])
615        CPU_ABIS = CPU_ABI1.split(',')+CPU_ABI2.split(',')
616
617    log('Device CPU ABIs: %s' % (' '.join(CPU_ABIS)))
618
619    device_bits = 32
620    if len(CPU_ABILIST64):
621        device_bits = 64
622
623    app_bits = 32
624    # First look compatible ABI in the list of 64-bit ABIs.
625    COMPAT_ABI = [ABI for ABI in CPU_ABILIST64.split(',') if ABI in APP_ABIS]
626    if len(COMPAT_ABI):
627        # We found compatible ABI and it is 64-bit.
628        app_bits = 64
629    else:
630        # If we found nothing - look among 32-bit ABIs.
631        COMPAT_ABI = [ABI for ABI in CPU_ABILIST32.split(',') if ABI in APP_ABIS]
632        if not len(COMPAT_ABI):
633            # Lastly, lets check ro.product.cpu.abi and ro.product.cpu.abi2
634            COMPAT_ABI = [ABI for ABI in CPU_ABIS if ABI in APP_ABIS]
635
636    if not len(COMPAT_ABI):
637        error('''The device does not support the application's targetted CPU ABIs!
638       Device supports:  %s
639       Package supports: %s''' % (' '.join(CPU_ABIS),' '.join(APP_ABIS)))
640    COMPAT_ABI = COMPAT_ABI[0]
641    log('Compatible device ABI: %s' % (COMPAT_ABI))
642    GDBSETUP_INIT = get_build_var_for_abi('NDK_APP_GDBSETUP', COMPAT_ABI)
643    log('Using gdb setup init: %s' % (GDBSETUP_INIT))
644
645    TOOLCHAIN_PREFIX = get_build_var_for_abi('TOOLCHAIN_PREFIX', COMPAT_ABI)
646    log('Using toolchain prefix: %s' % (TOOLCHAIN_PREFIX))
647
648    APP_OUT = get_build_var_for_abi('TARGET_OUT', COMPAT_ABI)
649    log('Using app out directory: %s' % (APP_OUT))
650    DEBUGGABLE = extract_debuggable(PROJECT+os.sep+MANIFEST)
651    log('Found debuggable flag: %s' % ('true' if DEBUGGABLE==True else 'false'))
652    # If gdbserver exists, then we built with 'ndk-build NDK_DEBUG=1' and it's
653    # ok to not have android:debuggable set to true in the original manifest.
654    # However, if this is not the case, then complain!!
655    #
656    gdbserver_path = os.path.join(PROJECT,'libs',COMPAT_ABI,'gdbserver')
657    if not DEBUGGABLE:
658        if os.path.isfile(gdbserver_path):
659            log('Found gdbserver under libs/%s, assuming app was built with NDK_DEBUG=1' % (COMPAT_ABI))
660        else:
661            error('''Package %s is not debuggable ! You can fix that in two ways:
662
663  - Rebuilt with the NDK_DEBUG=1 option when calling 'ndk-build'.
664
665  - Modify your manifest to set android:debuggable attribute to "true",
666    then rebuild normally.
667
668After one of these, re-install to the device!''' % (PACKAGE_NAME))
669    elif not os.path.isfile(gdbserver_path):
670        error('''Could not find gdbserver binary under %s/libs/%s
671       This usually means you modified your AndroidManifest.xml to set
672       the android:debuggable flag to 'true' but did not rebuild the
673       native binaries. Please call 'ndk-build' to do so,
674       *then* re-install to the device!''' % (PROJECT,COMPAT_ABI))
675
676    # Let's check that 'gdbserver' is properly installed on the device too. If this
677    # is not the case, push 'gdbserver' found in prebuilt.
678    #
679    device_gdbserver = '/data/data/%s/lib/gdbserver' % (PACKAGE_NAME)
680    retcode,LS_GDBSERVER = adb_var_shell2(['ls', device_gdbserver])
681    if not retcode:
682        log('Found device gdbserver: %s' % (device_gdbserver))
683    else:
684        gdbserver_prebuilt_path = ('%s/prebuilt/android-%s/gdbserver/gdbserver' % (NDK, COMPAT_ABI))
685        if os.path.isfile(gdbserver_prebuilt_path):
686            log('Found prebuilt gdbserver. Copying it on device')
687            retcode,PUSH_OUTPUT=adb_cmd(True,
688                                       ['shell', 'push', '%s' % gdbserver_prebuilt_path, '/data/local/tmp/gdbserver'])
689            if retcode:
690                error('''Could not copy prebuilt gdberver to the device''')
691            device_gdbserver = '/data/local/tmp/gdbserver'
692        else:
693            error('''Non-debuggable application installed on the target device.
694               Please re-install the debuggable version!''')
695
696    # Find the <dataDir> of the package on the device
697    retcode,DATA_DIR = adb_var_shell2(['run-as', PACKAGE_NAME, '/system/bin/sh', '-c', 'pwd'])
698    if retcode or DATA_DIR == '':
699        error('''Could not extract package's data directory. Are you sure that
700       your installed application is debuggable?''')
701    log("Found data directory: '%s'" % (DATA_DIR))
702
703    # Launch the activity if needed
704    if OPTION_START:
705        if not OPTION_LAUNCH:
706            OPTION_LAUNCH = extract_launchable(PROJECT+os.sep+MANIFEST)
707            if not len(OPTION_LAUNCH):
708                error('''Could not extract name of launchable activity from manifest!
709           Try to use --launch=<name> directly instead as a work-around.''')
710            log('Found first launchable activity: %s' % (OPTION_LAUNCH[0]))
711        if not len(OPTION_LAUNCH):
712            error('''It seems that your Application does not have any launchable activity!
713       Please fix your manifest file and rebuild/re-install your application.''')
714
715    if OPTION_LAUNCH:
716        log('Launching activity: %s/%s' % (PACKAGE_NAME,OPTION_LAUNCH[0]))
717        retcode,LAUNCH_OUTPUT=adb_cmd(True,
718                                      ['shell', 'am', 'start'] + OPTION_WAIT + ['-n', '%s/%s' % (PACKAGE_NAME,OPTION_LAUNCH[0])],
719                                      log_command=True)
720        if retcode:
721            error('''Could not launch specified activity: %s
722       Use --launch-list to dump a list of valid values.''' % (OPTION_LAUNCH[0]))
723
724        # Sleep a bit, it sometimes take one second to start properly
725        # Note that we use the 'sleep' command on the device here.
726        #
727        adb_cmd(True, ['shell', 'sleep', '%f' % (DELAY)], log_command=True)
728
729    # Find the PID of the application being run
730    retcode,PID = get_pid_of(PACKAGE_NAME)
731    log('Found running PID: %d' % (PID))
732    if retcode or PID == 0:
733        if OPTION_LAUNCH:
734            error('''Could not extract PID of application on device/emulator.
735       Weird, this probably means one of these:
736
737         - The installed package does not match your current manifest.
738         - The application process was terminated.
739
740       Try using the --verbose option and look at its output for details.''')
741        else:
742            error('''Could not extract PID of application on device/emulator.
743       Are you sure the application is already started?
744       Consider using --start or --launch=<name> if not.''')
745
746    # Check that there is no other instance of gdbserver running
747    retcode,GDBSERVER_PID = get_pid_of('lib/gdbserver')
748    if not retcode and not GDBSERVER_PID == 0:
749        if not OPTION_FORCE:
750            error('Another debug session running, Use --force to kill it.')
751        log('Killing existing debugging session')
752        adb_cmd(False, ['shell', 'kill -9 %s' % (GDBSERVER_PID)])
753
754    # Launch gdbserver now
755    DEBUG_SOCKET = 'debug-socket'
756    adb_cmd(False,
757            ['shell', 'run-as', PACKAGE_NAME, device_gdbserver, '+%s' % (DEBUG_SOCKET), '--attach', str(PID)],
758            log_command=True, adb_trace=True, background=True)
759    log('Launched gdbserver succesfully.')
760
761# Make sure gdbserver was launched - debug check.
762#    adb_var_shell(['sleep', '0.1'], log_command=False)
763#    retcode,GDBSERVER_PID = get_pid_of('lib/gdbserver')
764#    if retcode or GDBSERVER_PID == 0:
765#        error('Could not launch gdbserver on the device?')
766#    log('Launched gdbserver succesfully (PID=%s)' % (GDBSERVER_PID))
767
768    # Setup network redirection
769    log('Setup network redirection')
770    retcode,_ = adb_cmd(False,
771                        ['forward', 'tcp:%d' % (DEBUG_PORT), 'localfilesystem:%s/%s' % (DATA_DIR,DEBUG_SOCKET)],
772                        log_command=True)
773    if retcode:
774        error('''Could not setup network redirection to gdbserver?
775       Maybe using --port=<port> to use a different TCP port might help?''')
776
777    # If we are debugging 64-bit app, then we need to pull linker64,
778    # app_process64 and libc.so from lib64 directory.
779    linker_name = 'linker'
780    libdir_name = 'lib'
781    app_process_name = 'app_process32'
782    if (app_bits == 64):
783        linker_name = 'linker64'
784        libdir_name = 'lib64'
785        app_process_name = 'app_process64'
786    else:
787        retcode,_ = adb_cmd(False, ['shell', 'test -e /system/bin/%s' % (app_process_name)])
788        if retcode:
789            # Old 32-bit devices do not have app_process32. Pull
790            # app_process in this case
791            app_process_name = 'app_process'
792
793    # Get the app_server binary from the device
794    pulled_app_process = '%s/app_process' % (APP_OUT)
795    adb_cmd(False, ['pull', '/system/bin/%s' % (app_process_name), pulled_app_process], log_command=True)
796    log('Pulled %s from device/emulator.' % (app_process_name))
797
798    pulled_linker = '%s/%s' % (APP_OUT, linker_name)
799    adb_cmd(False, ['pull', '/system/bin/%s' % (linker_name), pulled_linker], log_command=True)
800    log('Pulled %s from device/emulator.' % (linker_name))
801
802    pulled_libc = '%s/libc.so' % (APP_OUT)
803    adb_cmd(False, ['pull', '/system/%s/libc.so' % libdir_name, pulled_libc], log_command=True)
804    log('Pulled /system/%s/libc.so from device/emulator.' % libdir_name)
805
806    # Setup JDB connection, for --start or --launch
807    if (OPTION_START != None or OPTION_LAUNCH != None) and len(OPTION_WAIT):
808        log('Set up JDB connection, using jdb command: %s' % JDB_CMD)
809        retcode,_ = adb_cmd(False,
810                            ['forward', 'tcp:%d' % (JDB_PORT), 'jdwp:%d' % (PID)],
811                            log_command=True)
812        time.sleep(1.0)
813        if retcode:
814            error('Could not forward JDB port')
815        background_spawn([JDB_CMD,'-connect','com.sun.jdi.SocketAttach:hostname=localhost,port=%d' % (JDB_PORT)], True, output_jdb, True, input_jdb)
816        time.sleep(1.0)
817
818    # Work out the python pretty printer details.
819    pypr_folder = None
820    pypr_function = None
821
822    # Automatic determination of pypr.
823    if OPTION_STDCXXPYPR == 'auto':
824        libdir = os.path.join(PROJECT,'libs',COMPAT_ABI)
825        libs = [ f for f in os.listdir(libdir)
826                 if os.path.isfile(os.path.join(libdir, f)) and f.endswith('.so') ]
827        if 'libstlport_shared.so' in libs:
828            OPTION_STDCXXPYPR = 'stlport'
829        elif 'libgnustl_shared.so' in libs:
830            OPTION_STDCXXPYPR = 'gnustdcxx'
831
832    if OPTION_STDCXXPYPR == 'stlport':
833        pypr_folder = PYPRPR_BASE + 'stlport/gppfs-0.2/stlport'
834        pypr_function = 'register_stlport_printers'
835    elif OPTION_STDCXXPYPR.startswith('gnustdcxx'):
836        if OPTION_STDCXXPYPR == 'gnustdcxx':
837            NDK_TOOLCHAIN_VERSION = get_build_var_for_abi('NDK_TOOLCHAIN_VERSION', COMPAT_ABI)
838            log('Using toolchain version: %s' % (NDK_TOOLCHAIN_VERSION))
839            pypr_folder = PYPRPR_GNUSTDCXX_BASE + 'gcc-' + NDK_TOOLCHAIN_VERSION
840        else:
841            pypr_folder = PYPRPR_GNUSTDCXX_BASE + OPTION_STDCXXPYPR.replace('gnustdcxx-','gcc-')
842        pypr_function = 'register_libstdcxx_printers'
843
844    # Now launch the appropriate gdb client with the right init commands
845    #
846    GDBCLIENT = '%sgdb' % (TOOLCHAIN_PREFIX)
847    GDBSETUP = '%s/gdb.setup' % (APP_OUT)
848    shutil.copyfile(GDBSETUP_INIT, GDBSETUP)
849    with open(GDBSETUP, "a") as gdbsetup:
850        #uncomment the following to debug the remote connection only
851        #gdbsetup.write('set debug remote 1\n')
852        gdbsetup.write('file '+pulled_app_process+'\n')
853        gdbsetup.write('target remote :%d\n' % (DEBUG_PORT))
854        gdbsetup.write('set breakpoint pending on\n')
855
856        if pypr_function:
857            gdbsetup.write('python\n')
858            gdbsetup.write('import sys\n')
859            gdbsetup.write('sys.path.append("%s")\n' % pypr_folder)
860            gdbsetup.write('from printers import %s\n' % pypr_function)
861            gdbsetup.write('%s(None)\n' % pypr_function)
862            gdbsetup.write('end\n')
863
864        if OPTION_EXEC:
865            with open(OPTION_EXEC, 'r') as execfile:
866                for line in execfile:
867                    gdbsetup.write(line)
868    gdbsetup.close()
869
870    gdbargs = [GDBCLIENT, '-x', '%s' % (GDBSETUP)]
871    if OPTION_TUI:
872        gdbhelp = subprocess.check_output([GDBCLIENT, '--help']).decode('ascii')
873        try:
874            gdbhelp.index('--tui')
875            gdbargs.append('--tui')
876            OPTION_TUI = 'running'
877        except:
878            print('Warning: Disabled tui mode as %s does not support it' % (os.path.basename(GDBCLIENT)))
879    gdbp = subprocess.Popen(gdbargs)
880    while gdbp.returncode is None:
881        try:
882            gdbp.communicate()
883        except KeyboardInterrupt:
884            pass
885    log("Exited gdb, returncode %d" % gdbp.returncode)
886
887if __name__ == '__main__':
888    main()
889