| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright 2014 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 import argparse | |
| 7 import codecs | |
| 8 import logging | |
| 9 import os.path | |
| 10 import requests | |
| 11 import signal | |
| 12 import subprocess | |
| 13 import sys | |
| 14 import tempfile | |
| 15 | |
| 16 | |
| 17 from android_gdb.install_remote_file_reader import install | |
| 18 from devtoolslib import paths | |
| 19 | |
| 20 | |
| 21 _MOJO_DEBUGGER_PORT = 7777 | |
| 22 _DEFAULT_PACKAGE_NAME = 'org.chromium.mojo.shell' | |
| 23 | |
| 24 | |
| 25 # TODO(etiennej): Refactor with similar methods in subdirectories | |
| 26 class DirectoryNotFoundException(Exception): | |
| 27 """Directory has not been found.""" | |
| 28 pass | |
| 29 | |
| 30 | |
| 31 def _get_dir_above(dirname): | |
| 32 """Returns the directory "above" this file containing |dirname|.""" | |
| 33 path = paths.find_ancestor_with(dirname) | |
| 34 if not path: | |
| 35 raise DirectoryNotFoundException(dirname) | |
| 36 return path | |
| 37 | |
| 38 | |
| 39 def _send_request(request, payload=None): | |
| 40 """Sends a request to mojo:debugger.""" | |
| 41 try: | |
| 42 url = 'http://localhost:%s/%s' % (_MOJO_DEBUGGER_PORT, request) | |
| 43 if payload: | |
| 44 return requests.post(url, payload) | |
| 45 else: | |
| 46 return requests.get(url) | |
| 47 except requests.exceptions.ConnectionError: | |
| 48 print 'Failed to connect to mojo:debugger, make sure the shell is running.' | |
| 49 return None | |
| 50 | |
| 51 | |
| 52 def _tracing_start(_): | |
| 53 """Starts tracing.""" | |
| 54 if not _send_request('start_tracing'): | |
| 55 return 1 | |
| 56 print "Started tracing." | |
| 57 return 0 | |
| 58 | |
| 59 | |
| 60 def _tracing_stop(args): | |
| 61 """Stops tracing and writes trace to file.""" | |
| 62 if args.file_name: | |
| 63 file_name = args.file_name | |
| 64 else: | |
| 65 for i in xrange(1000): | |
| 66 candidate_file_name = 'mojo_trace_%03d.json' % i | |
| 67 if not os.path.exists(candidate_file_name): | |
| 68 file_name = candidate_file_name | |
| 69 break | |
| 70 else: | |
| 71 print 'Failed to pick a name for the trace output file.' | |
| 72 return 1 | |
| 73 | |
| 74 response = _send_request('stop_tracing') | |
| 75 if not response: | |
| 76 return 1 | |
| 77 | |
| 78 # https://github.com/domokit/mojo/issues/253 | |
| 79 if int(response.headers['content-length']) != len(response.content): | |
| 80 print 'Response is truncated.' | |
| 81 return 1 | |
| 82 | |
| 83 with open(file_name, "wb") as trace_file: | |
| 84 trace_file.write('{"traceEvents":[') | |
| 85 trace_file.write(response.content) | |
| 86 trace_file.write(']}') | |
| 87 print "Trace saved in %s" % file_name | |
| 88 return 0 | |
| 89 | |
| 90 | |
| 91 def _add_tracing_command(subparsers): | |
| 92 """Sets up the command line parser to manage tracing.""" | |
| 93 tracing_parser = subparsers.add_parser('tracing', | |
| 94 help='trace event profiler') | |
| 95 tracing_subparser = tracing_parser.add_subparsers( | |
| 96 help='the command to run') | |
| 97 | |
| 98 start_tracing_parser = tracing_subparser.add_parser('start', | |
| 99 help='start tracing') | |
| 100 start_tracing_parser.set_defaults(func=_tracing_start) | |
| 101 | |
| 102 stop_tracing_parser = tracing_subparser.add_parser('stop', | |
| 103 help='stop tracing and retrieve the result') | |
| 104 stop_tracing_parser.add_argument('file_name', type=str, nargs='?', | |
| 105 help='name of the output file (optional)') | |
| 106 stop_tracing_parser.set_defaults(func=_tracing_stop) | |
| 107 | |
| 108 | |
| 109 def _wm_load(args): | |
| 110 """Loads (embeds) the given url in the window manager.""" | |
| 111 if not _send_request('load', args.url): | |
| 112 return 1 | |
| 113 return 0 | |
| 114 | |
| 115 | |
| 116 def _add_wm_command(subparsers): | |
| 117 """Sets up the parser for the 'wm' command.""" | |
| 118 wm_parser = subparsers.add_parser('wm', help='window manager') | |
| 119 wm_subparser = wm_parser.add_subparsers( | |
| 120 help='the command to run') | |
| 121 | |
| 122 wm_load_parser = wm_subparser.add_parser('load', | |
| 123 help='load (embed) the given url') | |
| 124 wm_load_parser.add_argument('url', type=str, | |
| 125 help='the url to load') | |
| 126 wm_load_parser.set_defaults(func=_wm_load) | |
| 127 | |
| 128 | |
| 129 def _device_stack(args): | |
| 130 """Runs the device logcat through android_stack_parser.""" | |
| 131 adb_path = args.adb_path if args.adb_path else 'adb' | |
| 132 logcat_cmd = [adb_path, 'logcat', '-d'] | |
| 133 try: | |
| 134 logcat = subprocess.Popen(logcat_cmd, stdout=subprocess.PIPE) | |
| 135 except OSError: | |
| 136 print 'failed to call adb, make sure it is in PATH or pass --adb-path' | |
| 137 return 1 | |
| 138 | |
| 139 devtools_dir = os.path.dirname(os.path.abspath(__file__)) | |
| 140 stack_command = [os.path.join(devtools_dir, 'android_stack_parser', 'stack')] | |
| 141 if args.build_dir: | |
| 142 stack_command.append('--build-dir=' + os.path.abspath(args.build_dir)) | |
| 143 if args.ndk_dir: | |
| 144 stack_command.append('--ndk-dir=' + os.path.abspath(args.ndk_dir)) | |
| 145 stack_command.append('-') | |
| 146 stack = subprocess.Popen(stack_command, stdin=logcat.stdout) | |
| 147 | |
| 148 logcat.wait() | |
| 149 stack.wait() | |
| 150 | |
| 151 if logcat.returncode: | |
| 152 print 'adb logcat failed, make sure the device is connected and available' | |
| 153 return logcat.returncode | |
| 154 if stack.returncode: | |
| 155 return stack.returncode | |
| 156 return 0 | |
| 157 | |
| 158 | |
| 159 def _gdb_attach(args): | |
| 160 """Run GDB on an instance of Mojo Shell on an android device.""" | |
| 161 if args.ndk_dir: | |
| 162 ndk_dir = args.ndk_dir | |
| 163 else: | |
| 164 try: | |
| 165 ndk_dir = os.path.join(_get_dir_above('third_party'), 'third_party', | |
| 166 'android_tools', 'ndk') | |
| 167 if not os.path.exists(ndk_dir): | |
| 168 raise DirectoryNotFoundException() | |
| 169 except DirectoryNotFoundException: | |
| 170 logging.fatal("Unable to find the Android NDK, please specify its path " | |
| 171 "with --ndk-dir.") | |
| 172 return | |
| 173 | |
| 174 install_args = {} | |
| 175 if args.gsutil_dir: | |
| 176 install_args['gsutil'] = os.path.join(args.gsutil_dir, 'gsutil') | |
| 177 else: | |
| 178 try: | |
| 179 depot_tools_path = paths.find_depot_tools() | |
| 180 if not depot_tools_path: | |
| 181 raise DirectoryNotFoundException() | |
| 182 install_args['gsutil'] = os.path.join(depot_tools_path, 'third_party', | |
| 183 'gsutil', 'gsutil') | |
| 184 if not os.path.exists(install_args['gsutil']): | |
| 185 raise DirectoryNotFoundException() | |
| 186 except DirectoryNotFoundException: | |
| 187 logging.fatal("Unable to find gsutil, please specify its path with " | |
| 188 "--gsutil-dir.") | |
| 189 return | |
| 190 | |
| 191 if args.adb_path: | |
| 192 install_args['adb'] = args.adb_path | |
| 193 install(**install_args) | |
| 194 | |
| 195 gdb_path = os.path.join( | |
| 196 ndk_dir, | |
| 197 'toolchains', | |
| 198 # TODO(etiennej): Always select the most recent toolchain? | |
| 199 'arm-linux-androideabi-4.9', | |
| 200 'prebuilt', | |
| 201 # TODO(etiennej): DEPS mac NDK and use it on macs. | |
| 202 'linux-x86_64', | |
| 203 'bin', | |
| 204 'arm-linux-androideabi-gdb') | |
| 205 python_gdb_script_path = os.path.join(os.path.dirname(__file__), | |
| 206 'android_gdb', 'session.py') | |
| 207 debug_session_arguments = {} | |
| 208 if args.build_dir: | |
| 209 debug_session_arguments["build_directory"] = args.build_dir | |
| 210 else: | |
| 211 try: | |
| 212 debug_session_arguments["build_directory"] = os.path.join( | |
| 213 _get_dir_above('out'), 'out', 'android_Debug') | |
| 214 if not os.path.exists(debug_session_arguments["build_directory"]): | |
| 215 raise DirectoryNotFoundException() | |
| 216 except DirectoryNotFoundException: | |
| 217 logging.fatal("Unable to find the build directory, please specify it " | |
| 218 "using --build-dir.") | |
| 219 return | |
| 220 | |
| 221 if args.package_name: | |
| 222 debug_session_arguments["package_name"] = args.package_name | |
| 223 else: | |
| 224 debug_session_arguments["package_name"] = _DEFAULT_PACKAGE_NAME | |
| 225 if args.pyelftools_dir: | |
| 226 debug_session_arguments["pyelftools_dir"] = args.pyelftools_dir | |
| 227 else: | |
| 228 debug_session_arguments["pyelftools_dir"] = os.path.join( | |
| 229 _get_dir_above('third_party'), 'third_party', 'pyelftools') | |
| 230 | |
| 231 debug_session_arguments_str = ', '.join( | |
| 232 [k + '="' + codecs.encode(v, 'string_escape') + '"' | |
| 233 for k, v in debug_session_arguments.items()]) | |
| 234 | |
| 235 # We need to pass some commands to GDB at startup. | |
| 236 gdb_commands_file = tempfile.NamedTemporaryFile() | |
| 237 gdb_commands_file.write('source ' + python_gdb_script_path + '\n') | |
| 238 gdb_commands_file.write('py d = DebugSession(' + debug_session_arguments_str | |
| 239 + ')\n') | |
| 240 gdb_commands_file.write('py d.start()\n') | |
| 241 gdb_commands_file.flush() | |
| 242 | |
| 243 gdb_proc = subprocess.Popen([gdb_path, '-x', gdb_commands_file.name], | |
| 244 stdin=sys.stdin, | |
| 245 stdout=sys.stdout, | |
| 246 stderr=sys.stderr) | |
| 247 | |
| 248 # We don't want SIGINT to stop this program. It is automatically propagated by | |
| 249 # the system to gdb. | |
| 250 signal.signal(signal.SIGINT, signal.SIG_IGN) | |
| 251 gdb_proc.wait() | |
| 252 signal.signal(signal.SIGINT, signal.SIG_DFL) | |
| 253 | |
| 254 | |
| 255 def _add_device_command(subparsers): | |
| 256 """Sets up the parser for the 'device' command.""" | |
| 257 device_parser = subparsers.add_parser('device', | |
| 258 help='interact with the Android device (requires adb in PATH or passing ' | |
| 259 '--adb-path)') | |
| 260 device_parser.add_argument('--adb-path', type=str, | |
| 261 help='path to the adb tool from the Android SDK (optional)') | |
| 262 device_subparser = device_parser.add_subparsers( | |
| 263 help='the command to run') | |
| 264 | |
| 265 device_stack_parser = device_subparser.add_parser('stack', | |
| 266 help='symbolize the crash stacktraces from the device log') | |
| 267 device_stack_parser.add_argument('--ndk-dir', type=str, | |
| 268 help='path to the directory containing the Android NDK') | |
| 269 device_stack_parser.add_argument('--build-dir', type=str, | |
| 270 help='path to the build directory') | |
| 271 device_stack_parser.set_defaults(func=_device_stack) | |
| 272 | |
| 273 | |
| 274 def _add_gdb_command(subparsers): | |
| 275 gdb_parser = subparsers.add_parser( | |
| 276 'gdb', help='Debug Mojo Shell and its apps using GDB') | |
| 277 gdb_subparser = gdb_parser.add_subparsers( | |
| 278 help='Commands to GDB') | |
| 279 | |
| 280 gdb_attach_parser = gdb_subparser.add_parser( | |
| 281 'attach', help='Attach GDB to a running Mojo Shell process') | |
| 282 gdb_attach_parser.add_argument('--adb-path', type=str, | |
| 283 help='path to the adb tool from the Android SDK (optional)') | |
| 284 gdb_attach_parser.add_argument('--ndk-dir', type=str, | |
| 285 help='path to the directory containing the Android NDK') | |
| 286 gdb_attach_parser.add_argument('--build-dir', type=str, | |
| 287 help='path to the build directory') | |
| 288 gdb_attach_parser.add_argument('--pyelftools-dir', type=str, | |
| 289 help='Path to a directory containing third party libraries') | |
| 290 gdb_attach_parser.add_argument('--gsutil-dir', type=str, | |
| 291 help='Path to a directory containing gsutil') | |
| 292 gdb_attach_parser.add_argument('--package-name', type=str, | |
| 293 help='Name of the Mojo Shell android package to debug') | |
| 294 gdb_attach_parser.set_defaults(func=_gdb_attach) | |
| 295 | |
| 296 | |
| 297 def main(): | |
| 298 parser = argparse.ArgumentParser(description='Command-line interface for ' | |
| 299 'mojo:debugger') | |
| 300 subparsers = parser.add_subparsers(help='the tool to run') | |
| 301 _add_device_command(subparsers) | |
| 302 _add_tracing_command(subparsers) | |
| 303 _add_wm_command(subparsers) | |
| 304 _add_gdb_command(subparsers) | |
| 305 | |
| 306 args = parser.parse_args() | |
| 307 return args.func(args) | |
| 308 | |
| 309 if __name__ == '__main__': | |
| 310 sys.exit(main()) | |
| OLD | NEW |