Make --gdb work for android

This mostly works.  I haven't yet set up pulling down the
system binaries from the device to the host so that symbols
appear correctly, but I'll do that in the next patch.

One of the crazy things this patch adds it a script
to watch for loads on adb logcat and set up mappings
from the cache library names to the symboled binaries
in the out directory.  Presumably other scripts may
want to share this functionality so I've made it its
own script.

Better would be to have mojo_shell spit out a file
including the cache mapping information and we could
watch that file instead of logcat, but this works
for now.

R=qsr@chromium.org
BUG=

Review URL: https://codereview.chromium.org/848013004
This commit is contained in:
Eric Seidel 2015-01-16 11:28:31 -08:00
parent 0545f369f0
commit 74a45026ac
2 changed files with 185 additions and 22 deletions

64
tools/mojo_cache_linker.py Executable file
View File

@ -0,0 +1,64 @@
#!/usr/bin/env python
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import logging
import os
import re
import sys
# TODO(eseidel): This should be shared with tools/android_stack_parser/stack
# TODO(eseidel): This could be replaced by using build-ids on Android
# TODO(eseidel): mojo_shell should write out a cache mapping file.
def main():
logging.basicConfig(level=logging.INFO)
parser = argparse.ArgumentParser(
description='Watches mojo_shell logcat output and builds a directory '
'of symlinks to symboled binaries for seen cache names.')
parser.add_argument('links_dir', type=str)
parser.add_argument('symbols_dir', type=str)
parser.add_argument('base_url', type=str)
args = parser.parse_args()
regex = re.compile('Caching mojo app (?P<url>\S+) at (?P<path>\S+)')
if not os.path.isdir(args.links_dir):
logging.fatal('links_dir: %s is not a directory' % args.links_dir)
sys.exit(1)
for line in sys.stdin:
result = regex.search(line)
if not result:
continue
url = result.group('url')
if not url.startswith(args.base_url):
logging.debug('%s does not match base %s' % (url, args.base_url))
continue
full_name = os.path.basename(url)
name, ext = os.path.splitext(full_name)
if ext != '.mojo':
logging.debug('%s is not a .mojo library' % url)
continue
symboled_name = 'lib%s_library.so' % name
cache_link_path = os.path.join(args.links_dir,
os.path.basename(result.group('path')))
symboled_path = os.path.realpath(
os.path.join(args.symbols_dir, symboled_name))
if not os.path.isfile(symboled_path):
logging.warn('symboled path %s does not exist' % symboled_path)
continue
print "%s -> %s" % (cache_link_path, symboled_path)
if os.path.lexists(cache_link_path):
logging.debug('link already exists %s, replacing' % symboled_path)
os.unlink(cache_link_path)
os.symlink(symboled_path, cache_link_path)
if __name__ == '__main__':
main()

View File

@ -9,6 +9,7 @@ import json
import logging
import os
import pipes
import re
import requests
import signal
import skypy.paths
@ -40,6 +41,7 @@ DEFAULT_URL = "https://raw.githubusercontent.com/domokit/mojo/master/sky/example
ANDROID_PACKAGE = "org.chromium.mojo.shell"
ANDROID_ACTIVITY = "%s/.MojoShellActivity" % ANDROID_PACKAGE
# FIXME: Move this into mopy.config
def gn_args_from_build_dir(build_dir):
gn_cmd = [
@ -97,7 +99,7 @@ class SkyDebugger(object):
'--esa', 'parameters', ','.join(escaped_args),
]
def _build_mojo_shell_command(self, args):
def _build_mojo_shell_command(self, args, is_android):
content_handlers = ['%s,%s' % (mime_type, 'mojo:sky_viewer')
for mime_type in SUPPORTED_MIME_TYPES]
@ -111,6 +113,14 @@ class SkyDebugger(object):
'mojo:window_manager',
]
# Desktop-only work-around for mojo crashing under chromoting.
if not is_android and args.use_osmesa:
shell_args.append(
'--args-for=mojo:native_viewport_service --use-osmesa')
if is_android and args.gdb:
shell_args.append('--wait_for_debugger')
if 'remote_sky_server_port' in self.pids:
shell_command = self._wrap_for_android(shell_args)
else:
@ -139,6 +149,20 @@ class SkyDebugger(object):
root_relative_build_dir = os.path.relpath(abs_build_dir, SRC_ROOT)
return skypy.paths.Paths(root_relative_build_dir)
def _find_remote_pid_for_package(self, package):
ps_output = subprocess.check_output(['adb', 'shell', 'ps'])
for line in ps_output.split('\n'):
fields = line.split()
if fields and fields[-1] == package:
return fields[1]
return None
def _find_install_location_for_package(self, package):
pm_command = ['adb', 'shell', 'pm', 'path', package]
pm_output = subprocess.check_output(pm_command)
# e.g. package:/data/app/org.chromium.mojo.shell-1/base.apk
return pm_output.split(':')[-1]
def start_command(self, args):
# FIXME: Lame that we use self for a command-specific variable.
self.paths = self._create_paths_for_build_dir(args.build_dir)
@ -181,26 +205,52 @@ class SkyDebugger(object):
])
self.pids['remote_sky_command_port'] = args.command_port
shell_command = self._build_mojo_shell_command(args)
shell_command = self._build_mojo_shell_command(args, is_android)
if not is_android:
# Desktop-only work-around for mojo crashing under chromoting.
if args.use_osmesa:
shell_command.append(
'--args-for=mojo:native_viewport_service --use-osmesa')
# On android we can't launch inside gdb, but rather have to attach.
if args.gdb:
shell_command = ['gdbserver', ':%s' % GDB_PORT] + shell_command
# On android we can't launch inside gdb, but rather have to attach.
if not is_android and args.gdb:
shell_command = ['gdbserver', ':%d' % GDB_PORT] + shell_command
print ' '.join(map(pipes.quote, shell_command))
self.pids['mojo_shell_pid'] = subprocess.Popen(shell_command).pid
# This pid is meaningless on android (it's the adb shell pid)
start_command_pid = subprocess.Popen(shell_command).pid
if is_android:
# TODO(eseidel): am start -W does not seem to work?
pid_tries = 0
while True:
pid = self._find_remote_pid_for_package(ANDROID_PACKAGE)
if pid or pid_tries > 3:
break
logging.debug('No pid for %s yet, waiting' % ANDROID_PACKAGE)
time.sleep(5)
pid_tries += 1
if not pid:
logging.error('Failed to find mojo_shell pid on device!')
return
self.pids['mojo_shell_pid'] = pid
else:
self.pids['mojo_shell_pid'] = start_command_pid
if args.gdb and is_android:
gdbserver_cmd = ['gdbserver', '--attach', ':%s' % GDB_PORT]
self.pids['remote_gdbserver_pid'] = subprocess.Popen(shell_command).pid
# We push our own copy of gdbserver with the package since
# the default gdbserver is a different version from our gdb.
package_path = \
self._find_install_location_for_package(ANDROID_PACKAGE)
gdb_server_path = os.path.join(
os.path.dirname(package_path), 'lib/arm/gdbserver')
gdbserver_cmd = [
'adb', 'shell',
gdb_server_path, '--attach',
':%d' % GDB_PORT,
str(self.pids['mojo_shell_pid'])
]
print ' '.join(map(pipes.quote, gdbserver_cmd))
self.pids['adb_shell_gdbserver_pid'] = \
subprocess.Popen(gdbserver_cmd).pid
port_string = 'tcp:%s' % GDB_PORT
port_string = 'tcp:%d' % GDB_PORT
subprocess.check_call([
'adb', 'forward', port_string, port_string
])
@ -220,18 +270,18 @@ class SkyDebugger(object):
if not pid:
logging.info('No pid for %s, nothing to do.' % name)
return
logging.info('Killing %s (%s).' % (name, pid))
logging.info('Killing %s (%d).' % (name, pid))
try:
os.kill(pid, signal.SIGTERM)
except OSError:
logging.info('%s (%s) already gone.' % (name, pid))
logging.info('%s (%d) already gone.' % (name, pid))
def stop_command(self, args):
# TODO(eseidel): mojo_shell crashes when attempting graceful shutdown.
# self._send_command_to_sky('/quit')
self._kill_if_exists('mojo_shell_pid', 'mojo_shell')
self._kill_if_exists('sky_server_pid', 'sky_server')
# We could be much more surgical here:
if 'remote_sky_server_port' in self.pids:
device = android_commands.AndroidCommands(
@ -245,12 +295,20 @@ class SkyDebugger(object):
subprocess.call([
'adb', 'shell', 'am', 'force-stop', ANDROID_PACKAGE])
else:
# Only try to kill mojo_shell if it's running locally.
self._kill_if_exists('mojo_shell_pid', 'mojo_shell')
if 'remote_gdbserver_port' in self.pids:
self._kill_if_exists('adb_shell_gdbserver_pid',
'adb shell gdbserver')
port_string = 'tcp:%s' % self.pids['remote_gdbserver_port']
subprocess.call(['adb', 'forward', '--remove', port_string])
self.pids = {} # Clear out our pid file.
self._kill_if_exists('mojo_cache_linker_pid', 'mojo cache linker')
def load_command(self, args):
if not urlparse.urlparse(args.url_or_path).scheme:
# The load happens on the remote device, use the remote port.
@ -323,12 +381,53 @@ class SkyDebugger(object):
def gdb_attach_command(self, args):
self.paths = self._create_paths_for_build_dir(self.pids['build_dir'])
self._kill_if_exists('mojo_cache_linker_pid', 'mojo cache linker')
links_path = '/tmp/mojo_cache_links'
if not os.path.exists(links_path):
os.makedirs(links_path)
shell_link_path = os.path.join(links_path, 'libmojo_shell.so')
if os.path.lexists(shell_link_path):
os.unlink(shell_link_path)
os.symlink(self.paths.mojo_shell_path, shell_link_path)
logcat_cmd = ['adb', 'logcat']
logcat = subprocess.Popen(logcat_cmd, stdout=subprocess.PIPE)
mojo_cache_linker_path = os.path.join(
self.paths.sky_tools_directory, 'mojo_cache_linker.py')
cache_linker_cmd = [
mojo_cache_linker_path,
links_path,
self.pids['build_dir'],
'http://localhost:%s' % self.pids['remote_sky_server_port']
]
self.pids['mojo_cache_linker_pid'] = \
subprocess.Popen(cache_linker_cmd, stdin=logcat.stdout).pid
# Write out our pid file before we exec ourselves.
self._write_pid_file(PID_FILE_PATH, self.pids)
# TODO(eseidel): Need to sync down system libraries into a directory.
symbol_search_paths = [
links_path,
self.pids['build_dir'],
]
# TODO(eseidel): We need to look up the toolchain somehow?
gdb_path = os.path.join(SRC_ROOT, 'third_party/android_tools/ndk/'
'toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/'
'bin/arm-linux-androideabi-gdb')
gdb_command = [
'/usr/bin/gdb', self.paths.mojo_shell_path,
'--eval-command', 'target remote localhost:%s' % GDB_PORT
gdb_path,
'--eval-command', 'file %s' % self.paths.mojo_shell_path,
'--eval-command', 'directory %s' % self.paths.src_root,
'--eval-command', 'target remote localhost:%s' % GDB_PORT,
'--eval-command', 'set solib-search-path %s' %
':'.join(symbol_search_paths),
]
print " ".join(gdb_command)
# We don't want python listenting for signals or anything, so exec
# We don't want python listening for signals or anything, so exec
# gdb and let it take the entire process.
os.execv(gdb_command[0], gdb_command)
@ -343,7 +442,7 @@ class SkyDebugger(object):
stack.wait()
def main(self):
logging.basicConfig(level=logging.INFO)
logging.basicConfig(level=logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
self.pids = self._load_pid_file(PID_FILE_PATH)