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)