From 74a45026ac5d421ae439e42e4dfda0de2d4258b1 Mon Sep 17 00:00:00 2001 From: Eric Seidel Date: Fri, 16 Jan 2015 11:28:31 -0800 Subject: [PATCH] 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 --- tools/mojo_cache_linker.py | 64 +++++++++++++++++ tools/skydb | 143 +++++++++++++++++++++++++++++++------ 2 files changed, 185 insertions(+), 22 deletions(-) create mode 100755 tools/mojo_cache_linker.py diff --git a/tools/mojo_cache_linker.py b/tools/mojo_cache_linker.py new file mode 100755 index 00000000000..4592eba39a4 --- /dev/null +++ b/tools/mojo_cache_linker.py @@ -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\S+) at (?P\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() diff --git a/tools/skydb b/tools/skydb index abdbbd4094b..5320e273bd2 100755 --- a/tools/skydb +++ b/tools/skydb @@ -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)