| 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 itertools | 6 import itertools |
| 7 import logging | 7 import logging |
| 8 import os | 8 import os |
| 9 import shutil | 9 import shutil |
| 10 import signal | 10 import signal |
| 11 import subprocess | 11 import subprocess |
| 12 import sys | 12 import sys |
| 13 import tempfile | 13 import tempfile |
| 14 import threading | 14 import threading |
| 15 import time | 15 import time |
| 16 import urlparse | 16 import urlparse |
| 17 | 17 |
| 18 from .paths import Paths | 18 from .paths import Paths |
| 19 | 19 |
| 20 sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, | 20 sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, |
| 21 os.pardir, 'build', 'android')) | 21 os.pardir, 'build', 'android')) |
| 22 from pylib import constants | 22 from pylib import constants |
| 23 from pylib.base import base_test_runner | 23 from pylib.base import base_test_runner |
| 24 from pylib.device import device_errors | 24 from pylib.device import device_errors |
| 25 from pylib.device import device_utils | 25 from pylib.device import device_utils |
| 26 from pylib.utils import base_error |
| 26 from pylib.utils import apk_helper | 27 from pylib.utils import apk_helper |
| 27 | 28 |
| 28 | 29 |
| 29 # Tags used by the mojo shell application logs. | 30 # Tags used by the mojo shell application logs. |
| 30 LOGCAT_TAGS = [ | 31 LOGCAT_TAGS = [ |
| 31 'AndroidHandler', | 32 'AndroidHandler', |
| 32 'MojoFileHelper', | 33 'MojoFileHelper', |
| 33 'MojoMain', | 34 'MojoMain', |
| 34 'MojoShellActivity', | 35 'MojoShellActivity', |
| 35 'MojoShellApplication', | 36 'MojoShellApplication', |
| (...skipping 11 matching lines...) Expand all Loading... |
| 47 | 48 |
| 48 class AndroidShell(object): | 49 class AndroidShell(object): |
| 49 """ | 50 """ |
| 50 Used to set up and run a given mojo shell binary on an Android device. | 51 Used to set up and run a given mojo shell binary on an Android device. |
| 51 |config| is the mopy.config.Config for the build. | 52 |config| is the mopy.config.Config for the build. |
| 52 """ | 53 """ |
| 53 def __init__(self, config): | 54 def __init__(self, config): |
| 54 self.adb_path = constants.GetAdbPath() | 55 self.adb_path = constants.GetAdbPath() |
| 55 self.paths = Paths(config) | 56 self.paths = Paths(config) |
| 56 self.device = None | 57 self.device = None |
| 58 self.shell_args = [] |
| 57 self.target_package = apk_helper.GetPackageName(self.paths.apk_path) | 59 self.target_package = apk_helper.GetPackageName(self.paths.apk_path) |
| 58 # This is used by decive_utils.Install to check if the apk needs updating. | 60 # This is used by decive_utils.Install to check if the apk needs updating. |
| 59 constants.SetOutputDirectory(self.paths.build_dir) | 61 constants.SetOutputDirectory(self.paths.build_dir) |
| 60 | 62 |
| 61 # TODO(msw): Use pylib's adb_wrapper and device_utils instead. | 63 # TODO(msw): Use pylib's adb_wrapper and device_utils instead. |
| 62 def _CreateADBCommand(self, args): | 64 def _CreateADBCommand(self, args): |
| 63 adb_command = [self.adb_path, '-s', self.device.adb.GetDeviceSerial()] | 65 adb_command = [self.adb_path, '-s', self.device.adb.GetDeviceSerial()] |
| 64 adb_command.extend(args) | 66 adb_command.extend(args) |
| 65 logging.getLogger().debug("Command: %s", " ".join(adb_command)) | 67 logging.getLogger().debug("Command: %s", " ".join(adb_command)) |
| 66 return adb_command | 68 return adb_command |
| (...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 124 return [] | 126 return [] |
| 125 | 127 |
| 126 original_values = list(itertools.chain( | 128 original_values = list(itertools.chain( |
| 127 *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters))) | 129 *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters))) |
| 128 sorted(original_values) | 130 sorted(original_values) |
| 129 result = [] | 131 result = [] |
| 130 for value in original_values: | 132 for value in original_values: |
| 131 result.append(self._StartHttpServerForOriginMapping(value)) | 133 result.append(self._StartHttpServerForOriginMapping(value)) |
| 132 return [MAPPING_PREFIX + ','.join(result)] | 134 return [MAPPING_PREFIX + ','.join(result)] |
| 133 | 135 |
| 134 def PrepareShellRun(self, origin=None, device=None, gdb=False): | 136 def InitShell(self, origin='localhost', device=None): |
| 135 """ | 137 """ |
| 136 Prepares for StartShell: runs adb as root and installs the apk as needed. | 138 Runs adb as root, starts an origin server, and installs the apk as needed. |
| 137 If the origin specified is 'localhost', a local http server will be set up | 139 |origin| is the origin for mojo: URLs; if its value is 'localhost', a local |
| 138 to serve files from the build directory along with port forwarding. | 140 http server will be set up to serve files from the build directory. |
| 139 |device| is the device to run on, if multiple devices are connected. | 141 |device| is the target device to run on, if multiple devices are connected. |
| 140 Returns arguments that should be appended to shell argument list. | 142 Returns 0 on success or a non-zero exit code on a terminal failure. |
| 141 """ | 143 """ |
| 142 devices = device_utils.DeviceUtils.HealthyDevices() | 144 try: |
| 143 if device: | 145 devices = device_utils.DeviceUtils.HealthyDevices() |
| 144 self.device = next((d for d in devices if d == device), None) | 146 if device: |
| 145 if not self.device: | 147 self.device = next((d for d in devices if d == device), None) |
| 146 raise device_errors.DeviceUnreachableError(device) | 148 if not self.device: |
| 147 elif devices: | 149 raise device_errors.DeviceUnreachableError(device) |
| 148 self.device = devices[0] | 150 elif devices: |
| 149 else: | 151 self.device = devices[0] |
| 150 raise device_errors.NoDevicesError() | 152 else: |
| 153 raise device_errors.NoDevicesError() |
| 151 | 154 |
| 152 logging.getLogger().debug("Using device: %s", self.device) | 155 logging.getLogger().debug("Using device: %s", self.device) |
| 153 self.device.EnableRoot() | 156 # Clean the logs on the device to avoid displaying prior activity. |
| 157 subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) |
| 158 self.device.EnableRoot() |
| 159 self.device.Install(self.paths.apk_path) |
| 160 except base_error.BaseError as e: |
| 161 # Report "device not found" as infra failures. See http://crbug.com/493900 |
| 162 print "Exception in AndroidShell.InitShell:\n%s" % str(e) |
| 163 if e.is_infra_error or "error: device not found" in str(e): |
| 164 return constants.INFRA_EXIT_CODE |
| 165 return constants.ERROR_EXIT_CODE |
| 154 | 166 |
| 155 # TODO(msw): Install fails often, retry as needed; http://crbug.com/493900 | |
| 156 try: | |
| 157 self.device.Install(self.paths.apk_path) | |
| 158 except device_errors.CommandFailedError as e: | |
| 159 logging.getLogger().error("APK install failed:\n%s", str(e)) | |
| 160 self.device.Install(self.paths.apk_path) | |
| 161 | |
| 162 extra_args = [] | |
| 163 if origin is 'localhost': | 167 if origin is 'localhost': |
| 164 origin = self._StartHttpServerForDirectory(self.paths.build_dir) | 168 origin = self._StartHttpServerForDirectory(self.paths.build_dir) |
| 165 if origin: | 169 if origin: |
| 166 extra_args.append("--origin=" + origin) | 170 self.shell_args.append("--origin=" + origin) |
| 167 | 171 return 0 |
| 168 if gdb: | |
| 169 # Remote debugging needs a port forwarded. | |
| 170 self.device.adb.Forward('tcp:5039', 'tcp:5039') | |
| 171 | |
| 172 return extra_args | |
| 173 | 172 |
| 174 def _GetProcessId(self, process): | 173 def _GetProcessId(self, process): |
| 175 """Returns the process id of the process on the remote device.""" | 174 """Returns the process id of the process on the remote device.""" |
| 176 while True: | 175 while True: |
| 177 line = process.stdout.readline() | 176 line = process.stdout.readline() |
| 178 pid_command = 'launcher waiting for GDB. pid: ' | 177 pid_command = 'launcher waiting for GDB. pid: ' |
| 179 index = line.find(pid_command) | 178 index = line.find(pid_command) |
| 180 if index != -1: | 179 if index != -1: |
| 181 return line[index + len(pid_command):].strip() | 180 return line[index + len(pid_command):].strip() |
| 182 return 0 | 181 return 0 |
| (...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 222 time.sleep(1) | 221 time.sleep(1) |
| 223 | 222 |
| 224 local_gdb_process = subprocess.Popen([self._GetLocalGdbPath(), | 223 local_gdb_process = subprocess.Popen([self._GetLocalGdbPath(), |
| 225 "-x", | 224 "-x", |
| 226 gdbinit_path], | 225 gdbinit_path], |
| 227 cwd=temp_dir) | 226 cwd=temp_dir) |
| 228 atexit.register(_ExitIfNeeded, local_gdb_process) | 227 atexit.register(_ExitIfNeeded, local_gdb_process) |
| 229 local_gdb_process.wait() | 228 local_gdb_process.wait() |
| 230 signal.signal(signal.SIGINT, signal.SIG_DFL) | 229 signal.signal(signal.SIGINT, signal.SIG_DFL) |
| 231 | 230 |
| 232 def StartShell(self, arguments, stdout, on_application_stop, gdb=False): | 231 def StartShell(self, arguments, stdout, on_fifo_closed, gdb=False): |
| 233 """ | 232 """ |
| 234 Starts the shell with the given arguments, directing output to |stdout|. | 233 Starts the shell with the given |arguments|, directing output to |stdout|. |
| 235 The |arguments| list must contain the "--origin=" arg from PrepareShellRun. | 234 |on_fifo_closed| will be run if the FIFO can't be found or when it's closed. |
| 235 |gdb| is a flag that attaches gdb to the device's remote process on startup. |
| 236 """ | 236 """ |
| 237 assert self.device |
| 238 arguments += self.shell_args |
| 239 |
| 237 cmd = self._CreateADBCommand([ | 240 cmd = self._CreateADBCommand([ |
| 238 'shell', | 241 'shell', |
| 239 'am', | 242 'am', |
| 240 'start', | 243 'start', |
| 241 '-S', | 244 '-S', |
| 242 '-a', 'android.intent.action.VIEW', | 245 '-a', 'android.intent.action.VIEW', |
| 243 '-n', '%s/%s.MojoShellActivity' % (self.target_package, | 246 '-n', '%s/%s.MojoShellActivity' % (self.target_package, |
| 244 'org.chromium.mojo.shell')]) | 247 'org.chromium.mojo.shell')]) |
| 245 | 248 |
| 246 logcat_process = None | 249 logcat_process = None |
| 247 if gdb: | 250 if gdb: |
| 248 arguments += ['--wait-for-debugger'] | 251 arguments.append('--wait-for-debugger') |
| 252 # Remote debugging needs a port forwarded. |
| 253 self.device.adb.Forward('tcp:5039', 'tcp:5039') |
| 249 logcat_process = self.ShowLogs(stdout=subprocess.PIPE) | 254 logcat_process = self.ShowLogs(stdout=subprocess.PIPE) |
| 250 | 255 |
| 251 fifo_path = "/data/data/%s/stdout.fifo" % self.target_package | 256 fifo_path = "/data/data/%s/stdout.fifo" % self.target_package |
| 252 subprocess.check_call(self._CreateADBCommand( | 257 subprocess.check_call(self._CreateADBCommand( |
| 253 ['shell', 'rm', '-f', fifo_path])) | 258 ['shell', 'rm', '-f', fifo_path])) |
| 254 arguments.append('--fifo-path=%s' % fifo_path) | 259 arguments.append('--fifo-path=%s' % fifo_path) |
| 255 max_attempts = 200 if '--wait-for-debugger' in arguments else 5 | 260 max_attempts = 200 if '--wait-for-debugger' in arguments else 5 |
| 256 self._ReadFifo(fifo_path, stdout, on_application_stop, max_attempts) | 261 self._ReadFifo(fifo_path, stdout, on_fifo_closed, max_attempts) |
| 257 | 262 |
| 258 # Extract map-origin args and add the extras array with commas escaped. | 263 # Extract map-origin args and add the extras array with commas escaped. |
| 259 parameters = [a for a in arguments if not a.startswith(MAPPING_PREFIX)] | 264 parameters = [a for a in arguments if not a.startswith(MAPPING_PREFIX)] |
| 260 map_parameters = [a for a in arguments if a.startswith(MAPPING_PREFIX)] | 265 map_parameters = [a for a in arguments if a.startswith(MAPPING_PREFIX)] |
| 261 parameters += self._StartHttpServerForOriginMappings(map_parameters) | 266 parameters += self._StartHttpServerForOriginMappings(map_parameters) |
| 262 parameters = [p.replace(',', '\,') for p in parameters] | 267 parameters = [p.replace(',', '\,') for p in parameters] |
| 263 if parameters: | 268 cmd += ['--esa', 'org.chromium.mojo.shell.extras', ','.join(parameters)] |
| 264 cmd += ['--esa', 'org.chromium.mojo.shell.extras', ','.join(parameters)] | |
| 265 | 269 |
| 266 atexit.register(self.StopShell) | 270 atexit.register(self.StopShell) |
| 267 with open(os.devnull, 'w') as devnull: | 271 with open(os.devnull, 'w') as devnull: |
| 268 cmd_process = subprocess.Popen(cmd, stdout=devnull) | 272 cmd_process = subprocess.Popen(cmd, stdout=devnull) |
| 269 if logcat_process: | 273 if logcat_process: |
| 270 self._WaitForProcessIdAndStartGdb(logcat_process) | 274 self._WaitForProcessIdAndStartGdb(logcat_process) |
| 271 cmd_process.wait() | 275 cmd_process.wait() |
| 272 | 276 |
| 273 def StopShell(self): | 277 def StopShell(self): |
| 274 """Stops the mojo shell.""" | 278 """Stops the mojo shell.""" |
| 275 self.device.ForceStop(self.target_package) | 279 self.device.ForceStop(self.target_package) |
| 276 | 280 |
| 277 def CleanLogs(self): | |
| 278 """Cleans the logs on the device.""" | |
| 279 subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) | |
| 280 | |
| 281 def ShowLogs(self, stdout=sys.stdout): | 281 def ShowLogs(self, stdout=sys.stdout): |
| 282 """Displays the mojo shell logs and returns the process reading the logs.""" | 282 """Displays the mojo shell logs and returns the process reading the logs.""" |
| 283 logcat = subprocess.Popen(self._CreateADBCommand([ | 283 logcat = subprocess.Popen(self._CreateADBCommand([ |
| 284 'logcat', | 284 'logcat', |
| 285 '-s', | 285 '-s', |
| 286 ' '.join(LOGCAT_TAGS)]), | 286 ' '.join(LOGCAT_TAGS)]), |
| 287 stdout=stdout) | 287 stdout=stdout) |
| 288 atexit.register(_ExitIfNeeded, logcat) | 288 atexit.register(_ExitIfNeeded, logcat) |
| 289 return logcat | 289 return logcat |
| 290 | 290 |
| (...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 330 files_to_link = { | 330 files_to_link = { |
| 331 'html_viewer.mojo': ['libhtml_viewer', 'html_viewer_library.so'], | 331 'html_viewer.mojo': ['libhtml_viewer', 'html_viewer_library.so'], |
| 332 'libmandoline_runner.so': ['mandoline_runner'], | 332 'libmandoline_runner.so': ['mandoline_runner'], |
| 333 } | 333 } |
| 334 for android_name, so_path in files_to_link.iteritems(): | 334 for android_name, so_path in files_to_link.iteritems(): |
| 335 src = os.path.join(build_dir, *so_path) | 335 src = os.path.join(build_dir, *so_path) |
| 336 if not os.path.isfile(src): | 336 if not os.path.isfile(src): |
| 337 print 'Expected file not found', src | 337 print 'Expected file not found', src |
| 338 sys.exit(-1) | 338 sys.exit(-1) |
| 339 os.symlink(src, os.path.join(dest_dir, android_name)) | 339 os.symlink(src, os.path.join(dest_dir, android_name)) |
| OLD | NEW |