838 lines
33 KiB
Python
Executable File
838 lines
33 KiB
Python
Executable File
#!/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 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 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('\\','/')
|
|
DEVICE_SERIAL = ''
|
|
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, DEVICE_SERIAL
|
|
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')
|
|
|
|
parser.add_argument( '--start',
|
|
help='Launch application instead of attaching to existing one',
|
|
action='store_true')
|
|
|
|
parser.add_argument( '--launch',
|
|
help='Same as --start, but specify activity name (see below)',
|
|
dest='launch_name', nargs=1)
|
|
|
|
parser.add_argument( '--launch-list',
|
|
help='List all launchable activity names from manifest',
|
|
action='store_true')
|
|
|
|
parser.add_argument( '--delay',
|
|
help='Delay in seconds between activity start and gdbserver attach',
|
|
type=float, default=DELAY,
|
|
dest='delay')
|
|
|
|
parser.add_argument( '-p', '--project',
|
|
help='Specify application project path',
|
|
dest='project')
|
|
|
|
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')
|
|
|
|
parser.add_argument( '--awk',
|
|
help='Use specific awk command (unused flag retained for compatability)')
|
|
|
|
parser.add_argument( '-e',
|
|
help='Connect to single emulator instance....(either this,)',
|
|
action='store_true', dest='emulator')
|
|
|
|
parser.add_argument( '-d',
|
|
help='Connect to single target device........(this,)',
|
|
action='store_true', dest='device')
|
|
|
|
parser.add_argument( '-s',
|
|
help='Connect to specific emulator or device.(or this)',
|
|
default=DEVICE_SERIAL,
|
|
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')
|
|
|
|
parser.add_argument( '--nowait',
|
|
help='Do not wait for debugger to attach (may miss early JNI breakpoints)',
|
|
action='store_true', dest='nowait')
|
|
|
|
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:
|
|
if ADB_FLAGS != '':
|
|
parser.print_help()
|
|
exit(1)
|
|
ADB_FLAGS = '-e'
|
|
if args.device_serial != '':
|
|
DEVICE_SERIAL = args.device_serial
|
|
if ADB_FLAGS != '':
|
|
parser.print_help()
|
|
exit(1)
|
|
ADB_FLAGS = '-s'
|
|
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
|
|
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=used_stderr, stdin=used_stdin,
|
|
bufsize=1, close_fds='posix' in sys.builtin_module_names)
|
|
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()
|
|
|
|
def adb_cmd(redirect_stderr, args, log_command=False, adb_trace=False, background=False):
|
|
global ADB_CMD, ADB_FLAGS, DEVICE_SERIAL
|
|
fullargs = [ADB_CMD]
|
|
if ADB_FLAGS != '':
|
|
fullargs += [ADB_FLAGS]
|
|
if DEVICE_SERIAL != '':
|
|
fullargs += [DEVICE_SERIAL]
|
|
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)
|
|
|
|
# Return the PID of a given package or program, or 0 if it doesn't run
|
|
# $1: Package name ("com.example.hellojni") or program name ("/lib/gdbserver")
|
|
# Out: PID number, or 0 if not running
|
|
#
|
|
def get_pid_of(package_name):
|
|
'''
|
|
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.
|
|
'''
|
|
ps_command = 'ps'
|
|
retcode,output = adb_cmd(False, ['shell', 'readlink $(which ps)'])
|
|
if output:
|
|
output = output.replace('\r', '').splitlines()[0]
|
|
if output == 'busybox':
|
|
ps_command = 'ps -w'
|
|
retcode,output = adb_cmd(False,['shell', ps_command])
|
|
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()
|
|
if columns.pop() == package_name:
|
|
return 0,int(columns[PID_column])
|
|
return 1,0
|
|
|
|
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))
|
|
if DEVICE_SERIAL == '':
|
|
log('Using ADB flags: %s' % (ADB_FLAGS))
|
|
else:
|
|
log('Using ADB flags: %s "%s"' % (ADB_FLAGS,DEVICE_SERIAL))
|
|
if PROJECT != None:
|
|
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', 'ls'])
|
|
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_ABI1 = adb_var_shell(['getprop', 'ro.product.cpu.abi'])
|
|
_,CPU_ABI2 = adb_var_shell(['getprop', 'ro.product.cpu.abi2'])
|
|
# Both CPU_ABI1 and CPU_ABI2 may contain multiple comma-delimited abis.
|
|
# Concatanate CPU_ABI1 and CPU_ABI2.
|
|
CPU_ABIS = CPU_ABI1.split(',')+CPU_ABI2.split(',')
|
|
log('Device CPU ABIs: %s' % (' '.join(CPU_ABIS)))
|
|
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)
|
|
log('Using gdb setup init: %s' % (GDBSETUP_INIT))
|
|
|
|
TOOLCHAIN_PREFIX = get_build_var_for_abi('TOOLCHAIN_PREFIX', COMPAT_ABI)
|
|
log('Using toolchain prefix: %s' % (TOOLCHAIN_PREFIX))
|
|
|
|
APP_OUT = get_build_var_for_abi('TARGET_OUT', COMPAT_ABI)
|
|
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'))
|
|
# If gdbserver exists, then we built with 'ndk-build NDK_DEBUG=1' and it's
|
|
# ok to not have android:debuggable set to true in the original manifest.
|
|
# However, if this is not the case, then complain!!
|
|
#
|
|
gdbserver_path = os.path.join(PROJECT,'libs',COMPAT_ABI,'gdbserver')
|
|
if not DEBUGGABLE:
|
|
if os.path.isfile(gdbserver_path):
|
|
log('Found gdbserver under libs/%s, assuming app was built with NDK_DEBUG=1' % (COMPAT_ABI))
|
|
else:
|
|
error('''Package %s is not debuggable ! You can fix that in two ways:
|
|
|
|
- Rebuilt with the NDK_DEBUG=1 option when calling 'ndk-build'.
|
|
|
|
- Modify your manifest to set android:debuggable attribute to "true",
|
|
then rebuild normally.
|
|
|
|
After one of these, re-install to the device!''' % (PACKAGE_NAME))
|
|
elif not os.path.isfile(gdbserver_path):
|
|
error('''Could not find gdbserver binary under %s/libs/%s
|
|
This usually means you modified your AndroidManifest.xml to set
|
|
the android:debuggable flag to 'true' but did not rebuild the
|
|
native binaries. Please call 'ndk-build' to do so,
|
|
*then* re-install to the device!''' % (PROJECT,COMPAT_ABI))
|
|
|
|
# Let's check that 'gdbserver' is properly installed on the device too. If this
|
|
# is not the case, the user didn't install the proper package after rebuilding.
|
|
#
|
|
retcode,DEVICE_GDBSERVER = adb_var_shell2(['ls', '/data/data/%s/lib/gdbserver' % (PACKAGE_NAME)])
|
|
if retcode:
|
|
error('''Non-debuggable application installed on the target device.
|
|
Please re-install the debuggable version!''')
|
|
log('Found device gdbserver: %s' % (DEVICE_GDBSERVER))
|
|
|
|
# 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 extract package's data directory. Are you sure that
|
|
your installed application is debuggable?''')
|
|
log("Found data directory: '%s'" % (DATA_DIR))
|
|
|
|
# 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)
|
|
|
|
# Find the PID of the application being run
|
|
retcode,PID = get_pid_of(PACKAGE_NAME)
|
|
log('Found running PID: %d' % (PID))
|
|
if retcode or PID == 0:
|
|
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.''')
|
|
|
|
# Check that there is no other instance of gdbserver running
|
|
retcode,GDBSERVER_PID = get_pid_of('lib/gdbserver')
|
|
if not retcode and not GDBSERVER_PID == 0:
|
|
if not OPTION_FORCE:
|
|
error('Another debug session running, Use --force to kill it.')
|
|
log('Killing existing debugging session')
|
|
adb_cmd(False, ['shell', 'kill -9 %s' % (GDBSERVER_PID)])
|
|
|
|
# Launch gdbserver now
|
|
DEBUG_SOCKET = 'debug-socket'
|
|
adb_cmd(False,
|
|
['shell', 'run-as', PACKAGE_NAME, 'lib/gdbserver', '+%s' % (DEBUG_SOCKET), '--attach', str(PID)],
|
|
log_command=True, adb_trace=True, background=True)
|
|
log('Launched gdbserver succesfully.')
|
|
|
|
# Make sure gdbserver was launched - debug check.
|
|
# adb_var_shell(['sleep', '0.1'], log_command=False)
|
|
# retcode,GDBSERVER_PID = get_pid_of('lib/gdbserver')
|
|
# if retcode or GDBSERVER_PID == 0:
|
|
# error('Could not launch gdbserver on the device?')
|
|
# log('Launched gdbserver succesfully (PID=%s)' % (GDBSERVER_PID))
|
|
|
|
# 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?''')
|
|
|
|
# Get the app_server binary from the device
|
|
APP_PROCESS = '%s/app_process' % (APP_OUT)
|
|
adb_cmd(False, ['pull', '/system/bin/app_process', APP_PROCESS], log_command=True)
|
|
log('Pulled app_process from device/emulator.')
|
|
|
|
adb_cmd(False, ['pull', '/system/bin/linker', '%s/linker' % (APP_OUT)], log_command=True)
|
|
log('Pulled linker from device/emulator.')
|
|
|
|
adb_cmd(False, ['pull', '/system/lib/libc.so', '%s/libc.so' % (APP_OUT)], log_command=True)
|
|
log('Pulled libc.so from device/emulator.')
|
|
|
|
# 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)
|
|
time.sleep(1.0)
|
|
if retcode:
|
|
error('Could not forward JDB port')
|
|
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 = '%sgdb' % (TOOLCHAIN_PREFIX)
|
|
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 '+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)
|
|
gdbsetup.close()
|
|
|
|
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()
|