| #!/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()) |