| OLD | NEW |
| (Empty) |
| 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 | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 import atexit | |
| 6 import itertools | |
| 7 import json | |
| 8 import logging | |
| 9 import os | |
| 10 import os.path | |
| 11 import random | |
| 12 import subprocess | |
| 13 import sys | |
| 14 import threading | |
| 15 import time | |
| 16 import urlparse | |
| 17 | |
| 18 from pylib.http_server import StartHttpServer | |
| 19 from pylib.shell import Shell | |
| 20 | |
| 21 | |
| 22 # Tags used by the mojo shell application logs. | |
| 23 LOGCAT_TAGS = [ | |
| 24 'AndroidHandler', | |
| 25 'MojoFileHelper', | |
| 26 'MojoMain', | |
| 27 'MojoShellActivity', | |
| 28 'MojoShellApplication', | |
| 29 'chromium', | |
| 30 ] | |
| 31 | |
| 32 MOJO_SHELL_PACKAGE_NAME = 'org.chromium.mojo.shell' | |
| 33 | |
| 34 MAPPING_PREFIX = '--map-origin=' | |
| 35 | |
| 36 DEFAULT_BASE_PORT = 31337 | |
| 37 | |
| 38 _logger = logging.getLogger() | |
| 39 | |
| 40 | |
| 41 def _IsMapOrigin(arg): | |
| 42 """Returns whether arg is a --map-origin argument.""" | |
| 43 return arg.startswith(MAPPING_PREFIX) | |
| 44 | |
| 45 | |
| 46 def _Split(l, pred): | |
| 47 positive = [] | |
| 48 negative = [] | |
| 49 for v in l: | |
| 50 if pred(v): | |
| 51 positive.append(v) | |
| 52 else: | |
| 53 negative.append(v) | |
| 54 return (positive, negative) | |
| 55 | |
| 56 | |
| 57 def _ExitIfNeeded(process): | |
| 58 """Exits |process| if it is still alive.""" | |
| 59 if process.poll() is None: | |
| 60 process.kill() | |
| 61 | |
| 62 | |
| 63 class AndroidShell(Shell): | |
| 64 """Wrapper around Mojo shell running on an Android device. | |
| 65 | |
| 66 Args: | |
| 67 adb_path: Path to adb, optional if adb is in PATH. | |
| 68 target_device: Device to run on, if multiple devices are connected. | |
| 69 """ | |
| 70 | |
| 71 def __init__(self, adb_path="adb", target_device=None, verbose_pipe=None): | |
| 72 self.adb_path = adb_path | |
| 73 self.target_device = target_device | |
| 74 self.stop_shell_registered = False | |
| 75 self.adb_running_as_root = False | |
| 76 self.verbose_pipe = verbose_pipe if verbose_pipe else open(os.devnull, 'w') | |
| 77 | |
| 78 def _CreateADBCommand(self, args): | |
| 79 adb_command = [self.adb_path] | |
| 80 if self.target_device: | |
| 81 adb_command.extend(['-s', self.target_device]) | |
| 82 adb_command.extend(args) | |
| 83 return adb_command | |
| 84 | |
| 85 def _ReadFifo(self, fifo_path, pipe, on_fifo_closed, max_attempts=5): | |
| 86 """Reads |fifo_path| on the device and write the contents to |pipe|. Calls | |
| 87 |on_fifo_closed| when the fifo is closed. This method will try to find the | |
| 88 path up to |max_attempts|, waiting 1 second between each attempt. If it | |
| 89 cannot find |fifo_path|, a exception will be raised. | |
| 90 """ | |
| 91 fifo_command = self._CreateADBCommand( | |
| 92 ['shell', 'test -e "%s"; echo $?' % fifo_path]) | |
| 93 | |
| 94 def Run(): | |
| 95 def _WaitForFifo(): | |
| 96 for _ in xrange(max_attempts): | |
| 97 if subprocess.check_output(fifo_command)[0] == '0': | |
| 98 return | |
| 99 time.sleep(1) | |
| 100 if on_fifo_closed: | |
| 101 on_fifo_closed() | |
| 102 raise Exception("Unable to find fifo.") | |
| 103 _WaitForFifo() | |
| 104 stdout_cat = subprocess.Popen(self._CreateADBCommand([ | |
| 105 'shell', | |
| 106 'cat', | |
| 107 fifo_path]), | |
| 108 stdout=pipe) | |
| 109 atexit.register(_ExitIfNeeded, stdout_cat) | |
| 110 stdout_cat.wait() | |
| 111 if on_fifo_closed: | |
| 112 on_fifo_closed() | |
| 113 | |
| 114 thread = threading.Thread(target=Run, name="StdoutRedirector") | |
| 115 thread.start() | |
| 116 | |
| 117 def _MapPort(self, device_port, host_port): | |
| 118 """Maps the device port to the host port. If |device_port| is 0, a random | |
| 119 available port is chosen. Returns the device port. | |
| 120 """ | |
| 121 def _FindAvailablePortOnDevice(): | |
| 122 opened = subprocess.check_output( | |
| 123 self._CreateADBCommand(['shell', 'netstat'])) | |
| 124 opened = [int(x.strip().split()[3].split(':')[1]) | |
| 125 for x in opened if x.startswith(' tcp')] | |
| 126 while True: | |
| 127 port = random.randint(4096, 16384) | |
| 128 if port not in opened: | |
| 129 return port | |
| 130 if device_port == 0: | |
| 131 device_port = _FindAvailablePortOnDevice() | |
| 132 subprocess.check_call(self._CreateADBCommand([ | |
| 133 "reverse", | |
| 134 "tcp:%d" % device_port, | |
| 135 "tcp:%d" % host_port])) | |
| 136 | |
| 137 unmap_command = self._CreateADBCommand(["reverse", "--remove", | |
| 138 "tcp:%d" % device_port]) | |
| 139 | |
| 140 def _UnmapPort(): | |
| 141 subprocess.Popen(unmap_command) | |
| 142 atexit.register(_UnmapPort) | |
| 143 return device_port | |
| 144 | |
| 145 def _StartHttpServerForDirectory(self, path, port=0): | |
| 146 """Starts an http server serving files from |path|. Returns the local | |
| 147 url. | |
| 148 """ | |
| 149 assert path | |
| 150 print 'starting http for', path | |
| 151 server_address = StartHttpServer(path) | |
| 152 | |
| 153 print 'local port=%d' % server_address[1] | |
| 154 return 'http://127.0.0.1:%d/' % self._MapPort(port, server_address[1]) | |
| 155 | |
| 156 def _StartHttpServerForOriginMapping(self, mapping, port): | |
| 157 """If |mapping| points at a local file starts an http server to serve files | |
| 158 from the directory and returns the new mapping. | |
| 159 | |
| 160 This is intended to be called for every --map-origin value. | |
| 161 """ | |
| 162 parts = mapping.split('=') | |
| 163 if len(parts) != 2: | |
| 164 return mapping | |
| 165 dest = parts[1] | |
| 166 # If the destination is a url, don't map it. | |
| 167 if urlparse.urlparse(dest)[0]: | |
| 168 return mapping | |
| 169 # Assume the destination is a local file. Start a local server that | |
| 170 # redirects to it. | |
| 171 localUrl = self._StartHttpServerForDirectory(dest, port) | |
| 172 print 'started server at %s for %s' % (dest, localUrl) | |
| 173 return parts[0] + '=' + localUrl | |
| 174 | |
| 175 def _StartHttpServerForOriginMappings(self, map_parameters, fixed_port): | |
| 176 """Calls _StartHttpServerForOriginMapping for every --map-origin | |
| 177 argument. | |
| 178 """ | |
| 179 if not map_parameters: | |
| 180 return [] | |
| 181 | |
| 182 original_values = list(itertools.chain( | |
| 183 *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters))) | |
| 184 sorted(original_values) | |
| 185 result = [] | |
| 186 for i, value in enumerate(original_values): | |
| 187 result.append(self._StartHttpServerForOriginMapping( | |
| 188 value, DEFAULT_BASE_PORT + 1 + i if fixed_port else 0)) | |
| 189 return [MAPPING_PREFIX + ','.join(result)] | |
| 190 | |
| 191 def _RunAdbAsRoot(self): | |
| 192 if self.adb_running_as_root: | |
| 193 return | |
| 194 | |
| 195 if 'cannot run as root' in subprocess.check_output( | |
| 196 self._CreateADBCommand(['root'])): | |
| 197 raise Exception("Unable to run adb as root.") | |
| 198 | |
| 199 # Wait for adbd to restart. | |
| 200 subprocess.check_call( | |
| 201 self._CreateADBCommand(['wait-for-device']), | |
| 202 stdout=self.verbose_pipe) | |
| 203 self.adb_running_as_root = True | |
| 204 | |
| 205 def InstallApk(self, shell_apk_path): | |
| 206 """Installs the apk on the device. | |
| 207 | |
| 208 Args: | |
| 209 shell_apk_path: Path to the shell Android binary. | |
| 210 """ | |
| 211 subprocess.check_call( | |
| 212 self._CreateADBCommand(['install', '-r', shell_apk_path, '-i', | |
| 213 MOJO_SHELL_PACKAGE_NAME]), | |
| 214 stdout=self.verbose_pipe) | |
| 215 | |
| 216 def SetUpLocalOrigin(self, local_dir, fixed_port=True): | |
| 217 """Sets up a local http server to serve files in |local_dir| along with | |
| 218 device port forwarding. Returns the origin flag to be set when running the | |
| 219 shell. | |
| 220 """ | |
| 221 | |
| 222 origin_url = self._StartHttpServerForDirectory( | |
| 223 local_dir, DEFAULT_BASE_PORT if fixed_port else 0) | |
| 224 return "--origin=" + origin_url | |
| 225 | |
| 226 def StartShell(self, | |
| 227 arguments, | |
| 228 stdout=None, | |
| 229 on_application_stop=None, | |
| 230 fixed_port=True): | |
| 231 """Starts the mojo shell, passing it the given arguments. | |
| 232 | |
| 233 The |arguments| list must contain the "--origin=" arg. SetUpLocalOrigin() | |
| 234 can be used to set up a local directory on the host machine as origin. | |
| 235 If |stdout| is not None, it should be a valid argument for subprocess.Popen. | |
| 236 """ | |
| 237 if not self.stop_shell_registered: | |
| 238 atexit.register(self.StopShell) | |
| 239 self.stop_shell_registered = True | |
| 240 | |
| 241 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % MOJO_SHELL_PACKAGE_NAME | |
| 242 | |
| 243 cmd = self._CreateADBCommand([ | |
| 244 'shell', | |
| 245 'am', | |
| 246 'start', | |
| 247 '-S', | |
| 248 '-a', 'android.intent.action.VIEW', | |
| 249 '-n', '%s/.MojoShellActivity' % MOJO_SHELL_PACKAGE_NAME]) | |
| 250 | |
| 251 parameters = [] | |
| 252 if stdout or on_application_stop: | |
| 253 # We need to run as root to access the fifo file we use for stdout | |
| 254 # redirection. | |
| 255 self._RunAdbAsRoot() | |
| 256 | |
| 257 # Remove any leftover fifo file after the previous run. | |
| 258 subprocess.check_call(self._CreateADBCommand( | |
| 259 ['shell', 'rm', '-f', STDOUT_PIPE])) | |
| 260 | |
| 261 parameters.append('--fifo-path=%s' % STDOUT_PIPE) | |
| 262 self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop) | |
| 263 # The origin has to be specified whether it's local or external. | |
| 264 assert any("--origin=" in arg for arg in arguments) | |
| 265 | |
| 266 # Extract map-origin arguments. | |
| 267 map_parameters, other_parameters = _Split(arguments, _IsMapOrigin) | |
| 268 parameters += other_parameters | |
| 269 parameters += self._StartHttpServerForOriginMappings(map_parameters, | |
| 270 fixed_port) | |
| 271 | |
| 272 if parameters: | |
| 273 encodedParameters = json.dumps(parameters) | |
| 274 cmd += ['--es', 'encodedParameters', encodedParameters] | |
| 275 | |
| 276 subprocess.check_call(cmd, stdout=self.verbose_pipe) | |
| 277 | |
| 278 def Run(self, arguments): | |
| 279 """Runs the shell with given arguments until shell exits, passing the stdout | |
| 280 mingled with stderr produced by the shell onto the stdout. | |
| 281 | |
| 282 Returns: | |
| 283 Exit code retured by the shell or None if the exit code cannot be | |
| 284 retrieved. | |
| 285 """ | |
| 286 self.CleanLogs() | |
| 287 p = self.ShowLogs() | |
| 288 self.StartShell(arguments, sys.stdout, p.terminate) | |
| 289 p.wait() | |
| 290 return None | |
| 291 | |
| 292 def RunAndGetOutput(self, arguments): | |
| 293 """Runs the shell with given arguments until shell exits. | |
| 294 | |
| 295 Args: | |
| 296 arguments: list of arguments for the shell | |
| 297 | |
| 298 Returns: | |
| 299 A tuple of (return_code, output). |return_code| is the exit code returned | |
| 300 by the shell or None if the exit code cannot be retrieved. |output| is the | |
| 301 stdout mingled with the stderr produced by the shell. | |
| 302 """ | |
| 303 (r, w) = os.pipe() | |
| 304 with os.fdopen(r, "r") as rf: | |
| 305 with os.fdopen(w, "w") as wf: | |
| 306 self.StartShell(arguments, wf, wf.close, False) | |
| 307 output = rf.read() | |
| 308 return None, output | |
| 309 | |
| 310 def StopShell(self): | |
| 311 """Stops the mojo shell.""" | |
| 312 subprocess.check_call(self._CreateADBCommand(['shell', | |
| 313 'am', | |
| 314 'force-stop', | |
| 315 MOJO_SHELL_PACKAGE_NAME])) | |
| 316 | |
| 317 def CleanLogs(self): | |
| 318 """Cleans the logs on the device.""" | |
| 319 subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) | |
| 320 | |
| 321 def ShowLogs(self): | |
| 322 """Displays the log for the mojo shell. | |
| 323 | |
| 324 Returns: | |
| 325 The process responsible for reading the logs. | |
| 326 """ | |
| 327 logcat = subprocess.Popen(self._CreateADBCommand([ | |
| 328 'logcat', | |
| 329 '-s', | |
| 330 ' '.join(LOGCAT_TAGS)]), | |
| 331 stdout=sys.stdout) | |
| 332 atexit.register(_ExitIfNeeded, logcat) | |
| 333 return logcat | |
| OLD | NEW |