| OLD | NEW |
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import atexit | 5 import atexit |
| 6 import datetime | 6 import datetime |
| 7 import email.utils | 7 import email.utils |
| 8 import hashlib | 8 import hashlib |
| 9 import itertools | 9 import itertools |
| 10 import json | 10 import json |
| 11 import logging | 11 import logging |
| 12 import math | 12 import math |
| 13 import os | 13 import os |
| 14 import os.path | 14 import os.path |
| 15 import random | 15 import random |
| 16 import shutil |
| 17 import signal |
| 16 import subprocess | 18 import subprocess |
| 17 import sys | 19 import sys |
| 20 import tempfile |
| 18 import threading | 21 import threading |
| 19 import time | 22 import time |
| 20 import urlparse | 23 import urlparse |
| 21 | 24 |
| 22 import SimpleHTTPServer | 25 import SimpleHTTPServer |
| 23 import SocketServer | 26 import SocketServer |
| 24 | 27 |
| 25 | 28 |
| 26 # Tags used by the mojo shell application logs. | 29 # Tags used by the mojo shell application logs. |
| 27 LOGCAT_TAGS = [ | 30 LOGCAT_TAGS = [ |
| (...skipping 150 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 178 | 181 |
| 179 | 182 |
| 180 class AndroidShell(object): | 183 class AndroidShell(object): |
| 181 """ Allows to set up and run a given mojo shell binary on an Android device. | 184 """ Allows to set up and run a given mojo shell binary on an Android device. |
| 182 | 185 |
| 183 Args: | 186 Args: |
| 184 shell_apk_path: path to the shell Android binary | 187 shell_apk_path: path to the shell Android binary |
| 185 local_dir: directory where locally build Mojo apps will be served, optional | 188 local_dir: directory where locally build Mojo apps will be served, optional |
| 186 adb_path: path to adb, optional if adb is in PATH | 189 adb_path: path to adb, optional if adb is in PATH |
| 187 target_device: device to run on, if multiple devices are connected | 190 target_device: device to run on, if multiple devices are connected |
| 191 src_root: root of the source tree |
| 188 """ | 192 """ |
| 189 def __init__( | 193 def __init__( |
| 190 self, shell_apk_path, local_dir=None, adb_path="adb", target_device=None, | 194 self, shell_apk_path, local_dir=None, adb_path="adb", target_device=None, |
| 191 target_package=MOJO_SHELL_PACKAGE_NAME): | 195 target_package=MOJO_SHELL_PACKAGE_NAME, src_root=None): |
| 192 self.shell_apk_path = shell_apk_path | 196 self.shell_apk_path = shell_apk_path |
| 197 self.src_root = src_root |
| 193 self.adb_path = adb_path | 198 self.adb_path = adb_path |
| 194 self.local_dir = local_dir | 199 self.local_dir = local_dir |
| 195 self.target_device = target_device | 200 self.target_device = target_device |
| 196 self.target_package = target_package | 201 self.target_package = target_package |
| 197 | 202 |
| 198 def _CreateADBCommand(self, args): | 203 def _CreateADBCommand(self, args): |
| 199 adb_command = [self.adb_path] | 204 adb_command = [self.adb_path] |
| 200 if self.target_device: | 205 if self.target_device: |
| 201 adb_command.extend(['-s', self.target_device]) | 206 adb_command.extend(['-s', self.target_device]) |
| 202 adb_command.extend(args) | 207 adb_command.extend(args) |
| (...skipping 100 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 303 return [] | 308 return [] |
| 304 | 309 |
| 305 original_values = list(itertools.chain( | 310 original_values = list(itertools.chain( |
| 306 *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters))) | 311 *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters))) |
| 307 sorted(original_values) | 312 sorted(original_values) |
| 308 result = [] | 313 result = [] |
| 309 for value in original_values: | 314 for value in original_values: |
| 310 result.append(self._StartHttpServerForOriginMapping(value, 0)) | 315 result.append(self._StartHttpServerForOriginMapping(value, 0)) |
| 311 return [MAPPING_PREFIX + ','.join(result)] | 316 return [MAPPING_PREFIX + ','.join(result)] |
| 312 | 317 |
| 313 def PrepareShellRun(self, origin=None): | 318 def PrepareShellRun(self, origin=None, install=True, gdb=False): |
| 314 """ Prepares for StartShell: runs adb as root and installs the apk. If the | 319 """ Prepares for StartShell: runs adb as root and installs the apk. If the |
| 315 origin specified is 'localhost', a local http server will be set up to serve | 320 origin specified is 'localhost', a local http server will be set up to serve |
| 316 files from the build directory along with port forwarding. | 321 files from the build directory along with port forwarding. |
| 317 | 322 |
| 318 Returns arguments that should be appended to shell argument list.""" | 323 Returns arguments that should be appended to shell argument list.""" |
| 319 # TODO(msw): Remove logging after devices are found; http://crbug.com/486220 | 324 # TODO(msw): Remove logging after devices are found; http://crbug.com/486220 |
| 320 logging.getLogger().debug("Path to adb: %s", self.adb_path) | 325 logging.getLogger().debug("Path to adb: %s", self.adb_path) |
| 321 logging.getLogger().debug("adb devices: %s", | 326 logging.getLogger().debug("adb devices: %s", |
| 322 subprocess.check_output(self._CreateADBCommand(['devices']))) | 327 subprocess.check_output(self._CreateADBCommand(['devices']))) |
| 323 | 328 |
| 324 subprocess.check_call(self._CreateADBCommand(['root'])) | 329 subprocess.check_call(self._CreateADBCommand(['root'])) |
| 325 subprocess.check_call( | 330 if install: |
| 326 self._CreateADBCommand(['install', '-r', self.shell_apk_path, '-i', | 331 subprocess.check_call( |
| 327 self.target_package])) | 332 self._CreateADBCommand(['install', '-r', self.shell_apk_path, '-i', |
| 333 self.target_package])) |
| 334 |
| 328 atexit.register(self.StopShell) | 335 atexit.register(self.StopShell) |
| 329 | 336 |
| 330 extra_args = [] | 337 extra_args = [] |
| 331 if origin is 'localhost': | 338 if origin is 'localhost': |
| 332 origin = self._StartHttpServerForDirectory(self.local_dir, 0) | 339 origin = self._StartHttpServerForDirectory(self.local_dir, 0) |
| 333 if origin: | 340 if origin: |
| 334 extra_args.append("--origin=" + origin) | 341 extra_args.append("--origin=" + origin) |
| 342 |
| 343 if gdb: |
| 344 # Remote debugging needs a port forwarded. |
| 345 subprocess.check_call(self._CreateADBCommand(['forward', 'tcp:5039', |
| 346 'tcp:5039'])) |
| 347 |
| 335 return extra_args | 348 return extra_args |
| 336 | 349 |
| 350 def _GetProcessId(self, process): |
| 351 """Returns the process id of the process on the remote device.""" |
| 352 while True: |
| 353 line = process.stdout.readline() |
| 354 pid_command = 'launcher waiting for GDB. pid: ' |
| 355 index = line.find(pid_command) |
| 356 if index != -1: |
| 357 return line[index + len(pid_command):].strip() |
| 358 return 0 |
| 359 |
| 360 def _GetLocalGdbPath(self): |
| 361 """Returns the path to the android gdb.""" |
| 362 return os.path.join(self.src_root, "third_party", "android_tools", "ndk", |
| 363 "toolchains", "arm-linux-androideabi-4.9", "prebuilt", |
| 364 "linux-x86_64", "bin", "arm-linux-androideabi-gdb") |
| 365 |
| 366 def _WaitForProcessIdAndStartGdb(self, process): |
| 367 """Waits until we see the process id from the remote device, starts up |
| 368 gdbserver on the remote device, and gdb on the local device.""" |
| 369 # Wait until we see "PID" |
| 370 pid = self._GetProcessId(process) |
| 371 assert pid != 0 |
| 372 # No longer need the logcat process. |
| 373 process.kill() |
| 374 # Disable python's processing of SIGINT while running gdb. Otherwise |
| 375 # control-c doesn't work well in gdb. |
| 376 signal.signal(signal.SIGINT, signal.SIG_IGN) |
| 377 gdbserver_process = subprocess.Popen(self._CreateADBCommand(['shell', |
| 378 'gdbserver', |
| 379 '--attach', |
| 380 ':5039', |
| 381 pid])) |
| 382 atexit.register(_ExitIfNeeded, gdbserver_process) |
| 383 |
| 384 temp_dir = tempfile.mkdtemp() |
| 385 atexit.register(shutil.rmtree, temp_dir, True) |
| 386 |
| 387 gdbinit_path = os.path.join(temp_dir, 'gdbinit') |
| 388 _CreateGdbInit(temp_dir, gdbinit_path, self.local_dir) |
| 389 |
| 390 _CreateSOLinks(temp_dir, self.local_dir) |
| 391 |
| 392 # Wait a second for gdb to start up on the device. Without this the local |
| 393 # gdb starts before the remote side has registered the port. |
| 394 # TODO(sky): maybe we should try a couple of times and then give up? |
| 395 time.sleep(1) |
| 396 |
| 397 local_gdb_process = subprocess.Popen([self._GetLocalGdbPath(), |
| 398 "-x", |
| 399 gdbinit_path], |
| 400 cwd=temp_dir) |
| 401 atexit.register(_ExitIfNeeded, local_gdb_process) |
| 402 local_gdb_process.wait() |
| 403 signal.signal(signal.SIGINT, signal.SIG_DFL) |
| 404 |
| 337 def StartShell(self, | 405 def StartShell(self, |
| 338 arguments, | 406 arguments, |
| 339 stdout=None, | 407 stdout=None, |
| 340 on_application_stop=None): | 408 on_application_stop=None, |
| 409 gdb=False): |
| 341 """ | 410 """ |
| 342 Starts the mojo shell, passing it the given arguments. | 411 Starts the mojo shell, passing it the given arguments. |
| 343 | 412 |
| 344 The |arguments| list must contain the "--origin=" arg from PrepareShellRun. | 413 The |arguments| list must contain the "--origin=" arg from PrepareShellRun. |
| 345 If |stdout| is not None, it should be a valid argument for subprocess.Popen. | 414 If |stdout| is not None, it should be a valid argument for subprocess.Popen. |
| 346 """ | 415 """ |
| 416 |
| 347 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % self.target_package | 417 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % self.target_package |
| 348 | 418 |
| 349 cmd = self._CreateADBCommand([ | 419 cmd = self._CreateADBCommand([ |
| 350 'shell', | 420 'shell', |
| 351 'am', | 421 'am', |
| 352 'start', | 422 'start', |
| 353 '-S', | 423 '-S', |
| 354 '-a', 'android.intent.action.VIEW', | 424 '-a', 'android.intent.action.VIEW', |
| 355 '-n', '%s/%s.MojoShellActivity' % (self.target_package, | 425 '-n', '%s/%s.MojoShellActivity' % (self.target_package, |
| 356 MOJO_SHELL_PACKAGE_NAME)]) | 426 MOJO_SHELL_PACKAGE_NAME)]) |
| 357 | 427 |
| 428 logcat_process = None |
| 429 |
| 430 if gdb: |
| 431 arguments += ['--wait-for-debugger'] |
| 432 logcat_process = self.ShowLogs(stdout=subprocess.PIPE) |
| 433 |
| 358 parameters = [] | 434 parameters = [] |
| 359 if stdout or on_application_stop: | 435 if stdout or on_application_stop: |
| 360 subprocess.check_call(self._CreateADBCommand( | 436 subprocess.check_call(self._CreateADBCommand( |
| 361 ['shell', 'rm', '-f', STDOUT_PIPE])) | 437 ['shell', 'rm', '-f', STDOUT_PIPE])) |
| 362 parameters.append('--fifo-path=%s' % STDOUT_PIPE) | 438 parameters.append('--fifo-path=%s' % STDOUT_PIPE) |
| 363 max_attempts = 5 | 439 max_attempts = 5 |
| 364 if '--wait-for-debugger' in arguments: | 440 if '--wait-for-debugger' in arguments: |
| 365 max_attempts = 200 | 441 max_attempts = 200 |
| 366 self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop, | 442 self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop, |
| 367 max_attempts=max_attempts) | 443 max_attempts=max_attempts) |
| 368 | 444 |
| 369 # Extract map-origin arguments. | 445 # Extract map-origin arguments. |
| 370 map_parameters, other_parameters = _Split(arguments, _IsMapOrigin) | 446 map_parameters, other_parameters = _Split(arguments, _IsMapOrigin) |
| 371 parameters += other_parameters | 447 parameters += other_parameters |
| 372 parameters += self._StartHttpServerForOriginMappings(map_parameters) | 448 parameters += self._StartHttpServerForOriginMappings(map_parameters) |
| 373 | 449 |
| 374 if parameters: | 450 if parameters: |
| 375 encodedParameters = json.dumps(parameters) | 451 encodedParameters = json.dumps(parameters) |
| 376 cmd += ['--es', 'encodedParameters', encodedParameters] | 452 cmd += ['--es', 'encodedParameters', encodedParameters] |
| 377 | 453 |
| 378 with open(os.devnull, 'w') as devnull: | 454 with open(os.devnull, 'w') as devnull: |
| 379 subprocess.Popen(cmd, stdout=devnull).wait() | 455 cmd_process = subprocess.Popen(cmd, stdout=devnull) |
| 456 if logcat_process: |
| 457 self._WaitForProcessIdAndStartGdb(logcat_process) |
| 458 cmd_process.wait() |
| 380 | 459 |
| 381 def StopShell(self): | 460 def StopShell(self): |
| 382 """ | 461 """ |
| 383 Stops the mojo shell. | 462 Stops the mojo shell. |
| 384 """ | 463 """ |
| 385 subprocess.check_call(self._CreateADBCommand(['shell', | 464 subprocess.check_call(self._CreateADBCommand(['shell', |
| 386 'am', | 465 'am', |
| 387 'force-stop', | 466 'force-stop', |
| 388 self.target_package])) | 467 self.target_package])) |
| 389 | 468 |
| 390 def CleanLogs(self): | 469 def CleanLogs(self): |
| 391 """ | 470 """ |
| 392 Cleans the logs on the device. | 471 Cleans the logs on the device. |
| 393 """ | 472 """ |
| 394 subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) | 473 subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) |
| 395 | 474 |
| 396 def ShowLogs(self): | 475 def ShowLogs(self, stdout=sys.stdout): |
| 397 """ | 476 """ |
| 398 Displays the log for the mojo shell. | 477 Displays the log for the mojo shell. |
| 399 | 478 |
| 400 Returns the process responsible for reading the logs. | 479 Returns the process responsible for reading the logs. |
| 401 """ | 480 """ |
| 402 logcat = subprocess.Popen(self._CreateADBCommand([ | 481 logcat = subprocess.Popen(self._CreateADBCommand([ |
| 403 'logcat', | 482 'logcat', |
| 404 '-s', | 483 '-s', |
| 405 ' '.join(LOGCAT_TAGS)]), | 484 ' '.join(LOGCAT_TAGS)]), |
| 406 stdout=sys.stdout) | 485 stdout=stdout) |
| 407 atexit.register(_ExitIfNeeded, logcat) | 486 atexit.register(_ExitIfNeeded, logcat) |
| 408 return logcat | 487 return logcat |
| 488 |
| 489 |
| 490 def _CreateGdbInit(tmp_dir, gdb_init_path, build_dir): |
| 491 """ |
| 492 Creates the gdbinit file. |
| 493 Args: |
| 494 tmp_dir: the directory where the gdbinit and other files lives. |
| 495 gdb_init_path: path to gdbinit |
| 496 build_dir: path where build files are located. |
| 497 """ |
| 498 gdbinit = ('target remote localhost:5039\n' |
| 499 'def reload-symbols\n' |
| 500 ' set solib-search-path %s:%s\n' |
| 501 'end\n' |
| 502 'def info-symbols\n' |
| 503 ' info sharedlibrary\n' |
| 504 'end\n' |
| 505 'reload-symbols\n' |
| 506 'echo \\n\\n' |
| 507 'You are now in gdb and need to type continue (or c) to continue ' |
| 508 'execution.\\n' |
| 509 'gdb is in the directory %s\\n' |
| 510 'The following functions have been defined:\\n' |
| 511 'reload-symbols: forces reloading symbols. If after a crash you\\n' |
| 512 'still do not see symbols you likely need to create a link in\\n' |
| 513 'the directory you are in.\\n' |
| 514 'info-symbols: shows status of current shared libraries.\\n' |
| 515 'NOTE: you may need to type reload-symbols again after a ' |
| 516 'crash.\\n\\n' % (tmp_dir, build_dir, tmp_dir)) |
| 517 with open(gdb_init_path, 'w') as f: |
| 518 f.write(gdbinit) |
| 519 |
| 520 |
| 521 def _CreateSOLinks(dest_dir, build_dir): |
| 522 """ |
| 523 Creates links from files (such as mojo files) to the real .so so that gdb can |
| 524 find them. |
| 525 """ |
| 526 # The files to create links for. The key is the name as seen on the device, |
| 527 # and the target an array of path elements as to where the .so lives (relative |
| 528 # to the output directory). |
| 529 # TODO(sky): come up with some way to automate this. |
| 530 files_to_link = { |
| 531 'html_viewer.mojo': ['libhtml_viewer', 'html_viewer_library.so'], |
| 532 'libmandoline_runner.so': ['mandoline_runner'], |
| 533 } |
| 534 for android_name, so_path in files_to_link.iteritems(): |
| 535 src = os.path.join(build_dir, *so_path) |
| 536 if not os.path.isfile(src): |
| 537 print 'Expected file not found', src |
| 538 sys.exit(-1) |
| 539 os.symlink(src, os.path.join(dest_dir, android_name)) |
| OLD | NEW |