| # 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 atexit |
| import logging |
| import os |
| import signal |
| import subprocess |
| import sys |
| import threading |
| import time |
| |
| from .paths import Paths |
| |
| sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), |
| '..', '..', '..', 'build', 'android')) |
| from pylib import constants |
| from pylib.device import device_errors |
| from pylib.device import device_utils |
| from pylib.utils import base_error |
| from pylib.utils import apk_helper |
| |
| |
| # Tags used by the mojo shell application logs. |
| LOGCAT_TAGS = [ |
| 'AndroidHandler', |
| 'MojoFileHelper', |
| 'MojoMain', |
| 'MojoShellActivity', |
| 'MojoShellApplication', |
| 'chromium', |
| ] |
| |
| MAPPING_PREFIX = '--map-origin=' |
| |
| |
| def _ExitIfNeeded(process): |
| '''Exits |process| if it is still alive.''' |
| if process.poll() is None: |
| process.kill() |
| |
| |
| class AndroidShell(object): |
| ''' |
| Used to set up and run a given mojo shell binary on an Android device. |
| |config| is the mopy.config.Config for the build. |
| ''' |
| def __init__(self, config): |
| self.adb_path = constants.GetAdbPath() |
| self.config = config |
| self.paths = Paths(config) |
| self.device = None |
| self.shell_args = [] |
| self.target_package = apk_helper.GetPackageName(self.paths.apk_path) |
| self.temp_gdb_dir = None |
| # This is used by decive_utils.Install to check if the apk needs updating. |
| constants.SetOutputDirectory(self.paths.build_dir) |
| |
| # TODO(msw): Use pylib's adb_wrapper and device_utils instead. |
| def _CreateADBCommand(self, args): |
| adb_command = [self.adb_path, '-s', self.device.adb.GetDeviceSerial()] |
| adb_command.extend(args) |
| logging.getLogger().debug('Command: %s', ' '.join(adb_command)) |
| return adb_command |
| |
| def _ReadFifo(self, path, pipe, on_fifo_closed, max_attempts=5): |
| ''' |
| Reads the fifo at |path| on the device and write the contents to |pipe|. |
| Calls |on_fifo_closed| when the fifo is closed. This method will try to find |
| the path up to |max_attempts|, waiting 1 second between each attempt. If it |
| cannot find |path|, a exception will be raised. |
| ''' |
| def Run(): |
| def _WaitForFifo(): |
| for _ in xrange(max_attempts): |
| if self.device.FileExists(path): |
| return |
| time.sleep(1) |
| on_fifo_closed() |
| raise Exception('Unable to find fifo: %s' % path) |
| _WaitForFifo() |
| stdout_cat = subprocess.Popen(self._CreateADBCommand([ |
| 'shell', |
| 'cat', |
| path]), |
| stdout=pipe) |
| atexit.register(_ExitIfNeeded, stdout_cat) |
| stdout_cat.wait() |
| on_fifo_closed() |
| |
| thread = threading.Thread(target=Run, name='StdoutRedirector') |
| thread.start() |
| |
| |
| def InitShell(self, device=None): |
| ''' |
| Runs adb as root, and installs the apk as needed. |device| is the target |
| device to run on, if multiple devices are connected. Returns 0 on success or |
| a non-zero exit code on a terminal failure. |
| ''' |
| try: |
| devices = device_utils.DeviceUtils.HealthyDevices() |
| if device: |
| self.device = next((d for d in devices if d == device), None) |
| if not self.device: |
| raise device_errors.DeviceUnreachableError(device) |
| elif devices: |
| self.device = devices[0] |
| else: |
| raise device_errors.NoDevicesError() |
| |
| logging.getLogger().debug('Using device: %s', self.device) |
| # Clean the logs on the device to avoid displaying prior activity. |
| subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) |
| self.device.EnableRoot() |
| self.device.Install(self.paths.apk_path) |
| except base_error.BaseError as e: |
| # Report 'device not found' as infra failures. See http://crbug.com/493900 |
| print 'Exception in AndroidShell.InitShell:\n%s' % str(e) |
| if e.is_infra_error or 'error: device not found' in str(e): |
| return constants.INFRA_EXIT_CODE |
| return constants.ERROR_EXIT_CODE |
| |
| return 0 |
| |
| def _GetProcessId(self, process): |
| '''Returns the process id of the process on the remote device.''' |
| while True: |
| line = process.stdout.readline() |
| pid_command = 'launcher waiting for GDB. pid: ' |
| index = line.find(pid_command) |
| if index != -1: |
| return line[index + len(pid_command):].strip() |
| return 0 |
| |
| def _GetLocalGdbPath(self): |
| '''Returns the path to the android gdb.''' |
| if self.config.target_cpu == 'arm': |
| return os.path.join(constants.ANDROID_NDK_ROOT, 'toolchains', |
| 'arm-linux-androideabi-4.9', 'prebuilt', |
| 'linux-x86_64', 'bin', 'arm-linux-androideabi-gdb') |
| elif self.config.target_cpu == 'x86': |
| return os.path.join(constants.ANDROID_NDK_ROOT, 'toolchains', |
| 'x86-4.9', 'prebuilt', 'linux-x86_64', 'bin', |
| 'i686-linux-android-gdb') |
| elif self.config.target_cpu == 'x64': |
| return os.path.join(constants.ANDROID_NDK_ROOT, 'toolchains', |
| 'x86_64-4.9', 'prebuilt', 'linux-x86_64', 'bin', |
| 'x86_64-linux-android-gdb') |
| else: |
| raise Exception('Unknown target_cpu: %s' % self.config.target_cpu) |
| |
| def _WaitForProcessIdAndStartGdb(self, process): |
| ''' |
| Waits until we see the process id from the remote device, starts up |
| gdbserver on the remote device, and gdb on the local device. |
| ''' |
| # Wait until we see 'PID' |
| pid = self._GetProcessId(process) |
| assert pid != 0 |
| # No longer need the logcat process. |
| process.kill() |
| # Disable python's processing of SIGINT while running gdb. Otherwise |
| # control-c doesn't work well in gdb. |
| signal.signal(signal.SIGINT, signal.SIG_IGN) |
| gdbserver_process = subprocess.Popen(self._CreateADBCommand(['shell', |
| 'gdbserver', |
| '--attach', |
| ':5039', |
| pid])) |
| atexit.register(_ExitIfNeeded, gdbserver_process) |
| |
| gdbinit_path = os.path.join(self.temp_gdb_dir, 'gdbinit') |
| _CreateGdbInit(self.temp_gdb_dir, gdbinit_path, self.paths.build_dir) |
| |
| # Wait a second for gdb to start up on the device. Without this the local |
| # gdb starts before the remote side has registered the port. |
| # TODO(sky): maybe we should try a couple of times and then give up? |
| time.sleep(1) |
| |
| local_gdb_process = subprocess.Popen([self._GetLocalGdbPath(), |
| '-x', |
| gdbinit_path], |
| cwd=self.temp_gdb_dir) |
| atexit.register(_ExitIfNeeded, local_gdb_process) |
| local_gdb_process.wait() |
| signal.signal(signal.SIGINT, signal.SIG_DFL) |
| |
| def StartActivity(self, |
| activity_name, |
| arguments, |
| stdout, |
| on_fifo_closed, |
| temp_gdb_dir=None): |
| ''' |
| Starts the shell with the given |arguments|, directing output to |stdout|. |
| |on_fifo_closed| will be run if the FIFO can't be found or when it's closed. |
| |temp_gdb_dir| is set to a location with appropriate symlinks for gdb to |
| find when attached to the device's remote process on startup. |
| ''' |
| assert self.device |
| arguments += self.shell_args |
| |
| cmd = self._CreateADBCommand([ |
| 'shell', |
| 'am', |
| 'start', |
| '-S', |
| '-a', 'android.intent.action.VIEW', |
| '-n', '%s/%s.%s' % (self.target_package, |
| self.target_package, |
| activity_name)]) |
| |
| logcat_process = None |
| if temp_gdb_dir: |
| self.temp_gdb_dir = temp_gdb_dir |
| arguments.append('--wait-for-debugger') |
| # Remote debugging needs a port forwarded. |
| self.device.adb.Forward('tcp:5039', 'tcp:5039') |
| logcat_process = self.ShowLogs(stdout=subprocess.PIPE) |
| |
| fifo_path = '/data/data/%s/stdout.fifo' % self.target_package |
| subprocess.check_call(self._CreateADBCommand( |
| ['shell', 'rm', '-f', fifo_path])) |
| arguments.append('--fifo-path=%s' % fifo_path) |
| max_attempts = 200 if '--wait-for-debugger' in arguments else 5 |
| self._ReadFifo(fifo_path, stdout, on_fifo_closed, max_attempts) |
| |
| # Extract map-origin args and add the extras array with commas escaped. |
| parameters = [a for a in arguments if not a.startswith(MAPPING_PREFIX)] |
| parameters = [p.replace(',', '\,') for p in parameters] |
| cmd += ['--esa', '%s.extras' % self.target_package, ','.join(parameters)] |
| |
| atexit.register(self.kill) |
| with open(os.devnull, 'w') as devnull: |
| cmd_process = subprocess.Popen(cmd, stdout=devnull) |
| if logcat_process: |
| self._WaitForProcessIdAndStartGdb(logcat_process) |
| cmd_process.wait() |
| |
| def kill(self): |
| '''Stops the mojo shell; matches the Popen.kill method signature.''' |
| self.device.ForceStop(self.target_package) |
| |
| def ShowLogs(self, stdout=sys.stdout): |
| '''Displays the mojo shell logs and returns the process reading the logs.''' |
| logcat = subprocess.Popen(self._CreateADBCommand([ |
| 'logcat', |
| '-s', |
| ' '.join(LOGCAT_TAGS)]), |
| stdout=stdout) |
| atexit.register(_ExitIfNeeded, logcat) |
| return logcat |
| |
| |
| def _CreateGdbInit(tmp_dir, gdb_init_path, build_dir): |
| ''' |
| Creates the gdbinit file. |
| |
| Args: |
| tmp_dir: the directory where the gdbinit and other files lives. |
| gdb_init_path: path to gdbinit |
| build_dir: path where build files are located. |
| ''' |
| gdbinit = ('target remote localhost:5039\n' |
| 'def reload-symbols\n' |
| ' set solib-search-path %s:%s\n' |
| 'end\n' |
| 'def info-symbols\n' |
| ' info sharedlibrary\n' |
| 'end\n' |
| 'reload-symbols\n' |
| 'echo \\n\\n' |
| 'You are now in gdb and need to type continue (or c) to continue ' |
| 'execution.\\n' |
| 'gdb is in the directory %s\\n' |
| 'The following functions have been defined:\\n' |
| 'reload-symbols: forces reloading symbols. If after a crash you\\n' |
| 'still do not see symbols you likely need to create a link in\\n' |
| 'the directory you are in.\\n' |
| 'info-symbols: shows status of current shared libraries.\\n' |
| 'NOTE: you may need to type reload-symbols again after a ' |
| 'crash.\\n\\n' % (tmp_dir, build_dir, tmp_dir)) |
| with open(gdb_init_path, 'w') as f: |
| f.write(gdbinit) |