| #!/usr/bin/env python | 
 | # Copyright 2014 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 codecs | 
 | import logging | 
 | import os.path | 
 | import requests | 
 | import signal | 
 | import subprocess | 
 | import sys | 
 | import tempfile | 
 |  | 
 |  | 
 | from android_gdb.install_remote_file_reader import install | 
 | from devtoolslib import paths | 
 |  | 
 |  | 
 | _MOJO_DEBUGGER_PORT = 7777 | 
 | _DEFAULT_PACKAGE_NAME = 'org.chromium.mojo.shell' | 
 |  | 
 |  | 
 | # TODO(etiennej): Refactor with similar methods in subdirectories | 
 | class DirectoryNotFoundException(Exception): | 
 |   """Directory has not been found.""" | 
 |   pass | 
 |  | 
 |  | 
 | def _get_dir_above(dirname): | 
 |   """Returns the directory "above" this file containing |dirname|.""" | 
 |   path = paths.find_ancestor_with(dirname) | 
 |   if not path: | 
 |     raise DirectoryNotFoundException(dirname) | 
 |   return path | 
 |  | 
 |  | 
 | def _send_request(request, payload=None): | 
 |   """Sends a request to mojo:debugger.""" | 
 |   try: | 
 |     url = 'http://localhost:%s/%s' % (_MOJO_DEBUGGER_PORT, request) | 
 |     if payload: | 
 |       return requests.post(url, payload) | 
 |     else: | 
 |       return requests.get(url) | 
 |   except requests.exceptions.ConnectionError: | 
 |     print ('Failed to connect to debugger.mojo. Make sure the shell is running ' | 
 |            'and the app was started with debugger, ie. through ' | 
 |            '`mojo_run --debugger APP_URL`') | 
 |  | 
 |     return None | 
 |  | 
 |  | 
 | def _tracing_start(_): | 
 |   """Starts tracing.""" | 
 |   if not _send_request('start_tracing'): | 
 |     return 1 | 
 |   print "Started tracing." | 
 |   return 0 | 
 |  | 
 |  | 
 | def _tracing_stop(args): | 
 |   """Stops tracing and writes trace to file.""" | 
 |   if args.file_name: | 
 |     file_name = args.file_name | 
 |   else: | 
 |     for i in xrange(1000): | 
 |       candidate_file_name = 'mojo_trace_%03d.json' % i | 
 |       if not os.path.exists(candidate_file_name): | 
 |         file_name = candidate_file_name | 
 |         break | 
 |     else: | 
 |       print 'Failed to pick a name for the trace output file.' | 
 |       return 1 | 
 |  | 
 |   response = _send_request('stop_tracing') | 
 |   if not response: | 
 |     return 1 | 
 |  | 
 |   # https://github.com/domokit/mojo/issues/253 | 
 |   if int(response.headers['content-length']) != len(response.content): | 
 |     print 'Response is truncated.' | 
 |     return 1 | 
 |  | 
 |   with open(file_name, "wb") as trace_file: | 
 |     trace_file.write('{"traceEvents":[') | 
 |     trace_file.write(response.content) | 
 |     trace_file.write(']}') | 
 |   print "Trace saved in %s" % file_name | 
 |   return 0 | 
 |  | 
 |  | 
 | def _add_tracing_command(subparsers): | 
 |   """Sets up the command line parser to manage tracing.""" | 
 |   tracing_parser = subparsers.add_parser('tracing', | 
 |       help='tracer (requires debugger.mojo)') | 
 |   tracing_subparser = tracing_parser.add_subparsers( | 
 |       help='the command to run') | 
 |  | 
 |   start_tracing_parser = tracing_subparser.add_parser('start', | 
 |       help='start tracing') | 
 |   start_tracing_parser.set_defaults(func=_tracing_start) | 
 |  | 
 |   stop_tracing_parser = tracing_subparser.add_parser('stop', | 
 |       help='stop tracing and retrieve the result') | 
 |   stop_tracing_parser.add_argument('file_name', type=str, nargs='?', | 
 |       help='name of the output file (optional)') | 
 |   stop_tracing_parser.set_defaults(func=_tracing_stop) | 
 |  | 
 |  | 
 | def _device_stack(args): | 
 |   """Runs the device logcat through android_stack_parser.""" | 
 |   adb_path = args.adb_path if args.adb_path else 'adb' | 
 |   logcat_cmd = [adb_path, 'logcat', '-d'] | 
 |   try: | 
 |     logcat = subprocess.Popen(logcat_cmd, stdout=subprocess.PIPE) | 
 |   except OSError: | 
 |     print 'failed to call adb, make sure it is in PATH or pass --adb-path' | 
 |     return 1 | 
 |  | 
 |   devtools_dir = os.path.dirname(os.path.abspath(__file__)) | 
 |   stack_command = [os.path.join(devtools_dir, 'android_stack_parser', 'stack')] | 
 |   if args.build_dir: | 
 |     stack_command.append('--build-dir=' + | 
 |             os.path.abspath(','.join(args.build_dir))) | 
 |   if args.ndk_dir: | 
 |     stack_command.append('--ndk-dir=' + os.path.abspath(args.ndk_dir)) | 
 |   stack_command.append('-') | 
 |   stack = subprocess.Popen(stack_command, stdin=logcat.stdout) | 
 |  | 
 |   logcat.wait() | 
 |   stack.wait() | 
 |  | 
 |   if logcat.returncode: | 
 |     print 'adb logcat failed, make sure the device is connected and available' | 
 |     return logcat.returncode | 
 |   if stack.returncode: | 
 |     return stack.returncode | 
 |   return 0 | 
 |  | 
 |  | 
 | def _gdb_attach(args): | 
 |   """Run GDB on an instance of Mojo Shell on an android device.""" | 
 |   if args.ndk_dir: | 
 |     ndk_dir = args.ndk_dir | 
 |   else: | 
 |     try: | 
 |       ndk_dir = os.path.join(_get_dir_above('third_party'), 'third_party', | 
 |                              'android_tools', 'ndk') | 
 |       if not os.path.exists(ndk_dir): | 
 |         raise DirectoryNotFoundException() | 
 |     except DirectoryNotFoundException: | 
 |       logging.fatal("Unable to find the Android NDK, please specify its path " | 
 |           "with --ndk-dir.") | 
 |       return | 
 |  | 
 |   install_args = {} | 
 |   if args.gsutil_dir: | 
 |     install_args['gsutil'] = os.path.join(args.gsutil_dir, 'gsutil') | 
 |   else: | 
 |     try: | 
 |       depot_tools_path = paths.find_depot_tools() | 
 |       if not depot_tools_path: | 
 |         raise DirectoryNotFoundException() | 
 |       install_args['gsutil'] = os.path.join(depot_tools_path, 'third_party', | 
 |                                             'gsutil', 'gsutil') | 
 |       if not os.path.exists(install_args['gsutil']): | 
 |         raise DirectoryNotFoundException() | 
 |     except DirectoryNotFoundException: | 
 |       logging.fatal("Unable to find gsutil, please specify its path with " | 
 |                     "--gsutil-dir.") | 
 |       return | 
 |  | 
 |   if args.adb_path: | 
 |     install_args['adb'] = args.adb_path | 
 |   else: | 
 |     install_args['adb'] = 'adb' | 
 |  | 
 |   try: | 
 |     install(**install_args) | 
 |   except OSError as e: | 
 |     if e.errno == 2: | 
 |       # ADB not found in path, print an error message | 
 |       logging.fatal("Unable to find ADB, please specify its path with " | 
 |                     "--adb-path.") | 
 |       return | 
 |     else: | 
 |       raise | 
 |  | 
 |   gdb_path = os.path.join( | 
 |       ndk_dir, | 
 |       'toolchains', | 
 |       # TODO(etiennej): Always select the most recent toolchain? | 
 |       'arm-linux-androideabi-4.9', | 
 |       'prebuilt', | 
 |       # TODO(etiennej): DEPS mac NDK and use it on macs. | 
 |       'linux-x86_64', | 
 |       'bin', | 
 |       'arm-linux-androideabi-gdb') | 
 |   python_gdb_script_path = os.path.join(os.path.dirname(__file__), | 
 |                                         'android_gdb', 'session.py') | 
 |   debug_session_arguments = {} | 
 |   if args.build_dir: | 
 |     debug_session_arguments["build_directory_list"] = ','.join(args.build_dir) | 
 |   else: | 
 |     try: | 
 |       debug_session_arguments["build_directory_list"] = os.path.join( | 
 |           _get_dir_above('out'), 'out', 'android_Debug') | 
 |       if not os.path.exists(debug_session_arguments["build_directory_list"]): | 
 |         raise DirectoryNotFoundException() | 
 |     except DirectoryNotFoundException: | 
 |       logging.fatal("Unable to find the build directory, please specify it " | 
 |                     "using --build-dir.") | 
 |       return | 
 |  | 
 |   if args.package_name: | 
 |     debug_session_arguments["package_name"] = args.package_name | 
 |   else: | 
 |     debug_session_arguments["package_name"] = _DEFAULT_PACKAGE_NAME | 
 |   debug_session_arguments['adb'] = install_args['adb'] | 
 |   if args.pyelftools_dir: | 
 |     debug_session_arguments["pyelftools_dir"] = args.pyelftools_dir | 
 |   else: | 
 |     try: | 
 |       debug_session_arguments["pyelftools_dir"] = os.path.join( | 
 |           _get_dir_above('third_party'), 'third_party', 'pyelftools') | 
 |       if not os.path.exists(debug_session_arguments["pyelftools_dir"]): | 
 |         raise DirectoryNotFoundException() | 
 |     except DirectoryNotFoundException: | 
 |       logging.fatal("Unable to find pyelftools python module, please specify " | 
 |           "its path using --pyelftools-dir.") | 
 |       return | 
 |  | 
 |   debug_session_arguments_str = ', '.join( | 
 |       [k + '="' + codecs.encode(v, 'string_escape') + '"' | 
 |        for k, v in debug_session_arguments.items()]) | 
 |  | 
 |   # We need to pass some commands to GDB at startup. | 
 |   gdb_commands_file = tempfile.NamedTemporaryFile() | 
 |   gdb_commands_file.write('source ' + python_gdb_script_path + '\n') | 
 |   gdb_commands_file.write('py d = DebugSession(' + debug_session_arguments_str | 
 |                           + ')\n') | 
 |   gdb_commands_file.write('py d.start()\n') | 
 |   gdb_commands_file.flush() | 
 |  | 
 |   gdb_proc = subprocess.Popen([gdb_path, '-x', gdb_commands_file.name], | 
 |                               stdin=sys.stdin, | 
 |                               stdout=sys.stdout, | 
 |                               stderr=sys.stderr) | 
 |  | 
 |   # We don't want SIGINT to stop this program. It is automatically propagated by | 
 |   # the system to gdb. | 
 |   signal.signal(signal.SIGINT, signal.SIG_IGN) | 
 |   gdb_proc.wait() | 
 |   signal.signal(signal.SIGINT, signal.SIG_DFL) | 
 |  | 
 |  | 
 | def _add_device_command(subparsers): | 
 |   """Sets up the parser for the 'device' command.""" | 
 |   device_parser = subparsers.add_parser('device', | 
 |       help='interact with the Android device (requires adb in PATH or passing ' | 
 |            '--adb-path)') | 
 |   device_parser.add_argument('--adb-path', type=str, | 
 |       help='path to the adb tool from the Android SDK (optional)') | 
 |   device_subparser = device_parser.add_subparsers( | 
 |       help='the command to run') | 
 |  | 
 |   device_stack_parser = device_subparser.add_parser('stack', | 
 |       help='symbolize the crash stacktraces from the device log') | 
 |   device_stack_parser.add_argument('--ndk-dir', type=str, | 
 |       help='path to the directory containing the Android NDK') | 
 |   device_stack_parser.add_argument('--build-dir', type=str, action='append', | 
 |       help='paths to the build directory, may be repeated') | 
 |   device_stack_parser.set_defaults(func=_device_stack) | 
 |  | 
 |  | 
 | def _add_gdb_command(subparsers): | 
 |   gdb_parser = subparsers.add_parser( | 
 |       'gdb', help='Debug Mojo Shell and its apps using GDB') | 
 |   gdb_subparser = gdb_parser.add_subparsers( | 
 |       help='Commands to GDB') | 
 |  | 
 |   gdb_attach_parser = gdb_subparser.add_parser( | 
 |       'attach', help='Attach GDB to a running Mojo Shell process') | 
 |   gdb_attach_parser.add_argument('--adb-path', type=str, | 
 |       help='path to the adb tool from the Android SDK (optional)') | 
 |   gdb_attach_parser.add_argument('--ndk-dir', type=str, | 
 |       help='path to the directory containing the Android NDK') | 
 |   gdb_attach_parser.add_argument('--build-dir', type=str, action='append', | 
 |       help='Paths to the build directory, may be repeated') | 
 |   gdb_attach_parser.add_argument('--pyelftools-dir', type=str, | 
 |       help='Path to a directory containing third party libraries') | 
 |   gdb_attach_parser.add_argument('--gsutil-dir', type=str, | 
 |       help='Path to a directory containing gsutil') | 
 |   gdb_attach_parser.add_argument('--package-name', type=str, | 
 |       help='Name of the Mojo Shell android package to debug') | 
 |   gdb_attach_parser.set_defaults(func=_gdb_attach) | 
 |  | 
 |  | 
 | def main(): | 
 |   parser = argparse.ArgumentParser(description='Command-line interface for ' | 
 |                                                 'mojo:debugger') | 
 |   subparsers = parser.add_subparsers(help='the tool to run') | 
 |   _add_device_command(subparsers) | 
 |   _add_tracing_command(subparsers) | 
 |   _add_gdb_command(subparsers) | 
 |  | 
 |   args = parser.parse_args() | 
 |   return args.func(args) | 
 |  | 
 | if __name__ == '__main__': | 
 |   sys.exit(main()) |