| 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 json | 7 import json |
| 8 import logging | 8 import logging |
| 9 import os | 9 import os |
| 10 import os.path | 10 import os.path |
| (...skipping 87 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 98 process.kill() | 98 process.kill() |
| 99 | 99 |
| 100 | 100 |
| 101 class AndroidShell(object): | 101 class AndroidShell(object): |
| 102 """ Allows to set up and run a given mojo shell binary on an Android device. | 102 """ Allows to set up and run a given mojo shell binary on an Android device. |
| 103 | 103 |
| 104 Args: | 104 Args: |
| 105 shell_apk_path: path to the shell Android binary | 105 shell_apk_path: path to the shell Android binary |
| 106 local_dir: directory where locally build Mojo apps will be served, optional | 106 local_dir: directory where locally build Mojo apps will be served, optional |
| 107 adb_path: path to adb, optional if adb is in PATH | 107 adb_path: path to adb, optional if adb is in PATH |
| 108 target_device: device to run on, if multiple devices are connected |
| 108 """ | 109 """ |
| 109 def __init__(self, shell_apk_path, local_dir=None, adb_path="adb"): | 110 def __init__( |
| 111 self, shell_apk_path, local_dir=None, adb_path="adb", target_device=None): |
| 110 self.shell_apk_path = shell_apk_path | 112 self.shell_apk_path = shell_apk_path |
| 111 self.adb_path = adb_path | 113 self.adb_path = adb_path |
| 112 self.local_dir = local_dir | 114 self.local_dir = local_dir |
| 115 self.target_device = target_device |
| 116 |
| 117 def _CreateADBCommand(self, args): |
| 118 adb_command = [self.adb_path] |
| 119 if self.target_device: |
| 120 adb_command.extend(['-s', self.target_device]) |
| 121 adb_command.extend(args) |
| 122 return adb_command |
| 113 | 123 |
| 114 def _ReadFifo(self, fifo_path, pipe, on_fifo_closed, max_attempts=5): | 124 def _ReadFifo(self, fifo_path, pipe, on_fifo_closed, max_attempts=5): |
| 115 """ | 125 """ |
| 116 Reads |fifo_path| on the device and write the contents to |pipe|. Calls | 126 Reads |fifo_path| on the device and write the contents to |pipe|. Calls |
| 117 |on_fifo_closed| when the fifo is closed. This method will try to find the | 127 |on_fifo_closed| when the fifo is closed. This method will try to find the |
| 118 path up to |max_attempts|, waiting 1 second between each attempt. If it | 128 path up to |max_attempts|, waiting 1 second between each attempt. If it |
| 119 cannot find |fifo_path|, a exception will be raised. | 129 cannot find |fifo_path|, a exception will be raised. |
| 120 """ | 130 """ |
| 121 fifo_command = [self.adb_path, 'shell', 'test -e "%s"; echo $?' % fifo_path] | 131 fifo_command = self._CreateADBCommand( |
| 132 ['shell', 'test -e "%s"; echo $?' % fifo_path]) |
| 122 | 133 |
| 123 def Run(): | 134 def Run(): |
| 124 def _WaitForFifo(): | 135 def _WaitForFifo(): |
| 125 for _ in xrange(max_attempts): | 136 for _ in xrange(max_attempts): |
| 126 if subprocess.check_output(fifo_command)[0] == '0': | 137 if subprocess.check_output(fifo_command)[0] == '0': |
| 127 return | 138 return |
| 128 time.sleep(1) | 139 time.sleep(1) |
| 129 if on_fifo_closed: | 140 if on_fifo_closed: |
| 130 on_fifo_closed() | 141 on_fifo_closed() |
| 131 raise Exception("Unable to find fifo.") | 142 raise Exception("Unable to find fifo.") |
| 132 _WaitForFifo() | 143 _WaitForFifo() |
| 133 stdout_cat = subprocess.Popen([self.adb_path, | 144 stdout_cat = subprocess.Popen(self._CreateADBCommand([ |
| 134 'shell', | 145 'shell', |
| 135 'cat', | 146 'cat', |
| 136 fifo_path], | 147 fifo_path]), |
| 137 stdout=pipe) | 148 stdout=pipe) |
| 138 atexit.register(_ExitIfNeeded, stdout_cat) | 149 atexit.register(_ExitIfNeeded, stdout_cat) |
| 139 stdout_cat.wait() | 150 stdout_cat.wait() |
| 140 if on_fifo_closed: | 151 if on_fifo_closed: |
| 141 on_fifo_closed() | 152 on_fifo_closed() |
| 142 | 153 |
| 143 thread = threading.Thread(target=Run, name="StdoutRedirector") | 154 thread = threading.Thread(target=Run, name="StdoutRedirector") |
| 144 thread.start() | 155 thread.start() |
| 145 | 156 |
| 146 def _MapPort(self, device_port, host_port): | 157 def _MapPort(self, device_port, host_port): |
| 147 """ | 158 """ |
| 148 Maps the device port to the host port. If |device_port| is 0, a random | 159 Maps the device port to the host port. If |device_port| is 0, a random |
| 149 available port is chosen. Returns the device port. | 160 available port is chosen. Returns the device port. |
| 150 """ | 161 """ |
| 151 def _FindAvailablePortOnDevice(): | 162 def _FindAvailablePortOnDevice(): |
| 152 opened = subprocess.check_output([self.adb_path, 'shell', 'netstat']) | 163 opened = subprocess.check_output( |
| 164 self._CreateADBCommand(['shell', 'netstat'])) |
| 153 opened = [int(x.strip().split()[3].split(':')[1]) | 165 opened = [int(x.strip().split()[3].split(':')[1]) |
| 154 for x in opened if x.startswith(' tcp')] | 166 for x in opened if x.startswith(' tcp')] |
| 155 while True: | 167 while True: |
| 156 port = random.randint(4096, 16384) | 168 port = random.randint(4096, 16384) |
| 157 if port not in opened: | 169 if port not in opened: |
| 158 return port | 170 return port |
| 159 if device_port == 0: | 171 if device_port == 0: |
| 160 device_port = _FindAvailablePortOnDevice() | 172 device_port = _FindAvailablePortOnDevice() |
| 161 subprocess.Popen([self.adb_path, | 173 subprocess.Popen(self._CreateADBCommand([ |
| 162 "reverse", | 174 "reverse", |
| 163 "tcp:%d" % device_port, | 175 "tcp:%d" % device_port, |
| 164 "tcp:%d" % host_port]).wait() | 176 "tcp:%d" % host_port])).wait() |
| 165 | 177 |
| 166 unmap_command = [self.adb_path, "reverse", "--remove", | 178 unmap_command = self._CreateADBCommand(["reverse", "--remove", |
| 167 "tcp:%d" % device_port] | 179 "tcp:%d" % device_port]) |
| 168 | 180 |
| 169 def _UnmapPort(): | 181 def _UnmapPort(): |
| 170 subprocess.Popen(unmap_command) | 182 subprocess.Popen(unmap_command) |
| 171 atexit.register(_UnmapPort) | 183 atexit.register(_UnmapPort) |
| 172 return device_port | 184 return device_port |
| 173 | 185 |
| 174 def _StartHttpServerForDirectory(self, path, port=0): | 186 def _StartHttpServerForDirectory(self, path, port=0): |
| 175 """Starts an http server serving files from |path|. Returns the local | 187 """Starts an http server serving files from |path|. Returns the local |
| 176 url.""" | 188 url.""" |
| 177 assert path | 189 assert path |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 218 result.append(self._StartHttpServerForOriginMapping( | 230 result.append(self._StartHttpServerForOriginMapping( |
| 219 value, DEFAULT_BASE_PORT + 1 + i if fixed_port else 0)) | 231 value, DEFAULT_BASE_PORT + 1 + i if fixed_port else 0)) |
| 220 return [MAPPING_PREFIX + ','.join(result)] | 232 return [MAPPING_PREFIX + ','.join(result)] |
| 221 | 233 |
| 222 def PrepareShellRun(self, origin=None, fixed_port=True): | 234 def PrepareShellRun(self, origin=None, fixed_port=True): |
| 223 """ Prepares for StartShell: runs adb as root and installs the apk. If no | 235 """ Prepares for StartShell: runs adb as root and installs the apk. If no |
| 224 --origin is specified, local http server will be set up to serve files from | 236 --origin is specified, local http server will be set up to serve files from |
| 225 the build directory along with port forwarding. | 237 the build directory along with port forwarding. |
| 226 | 238 |
| 227 Returns arguments that should be appended to shell argument list.""" | 239 Returns arguments that should be appended to shell argument list.""" |
| 228 if 'cannot run as root' in subprocess.check_output([self.adb_path, 'root']): | 240 if 'cannot run as root' in subprocess.check_output( |
| 241 self._CreateADBCommand(['root'])): |
| 229 raise Exception("Unable to run adb as root.") | 242 raise Exception("Unable to run adb as root.") |
| 230 subprocess.check_call( | 243 subprocess.check_call( |
| 231 [self.adb_path, 'install', '-r', self.shell_apk_path, '-i', | 244 self._CreateADBCommand(['install', '-r', self.shell_apk_path, '-i', |
| 232 MOJO_SHELL_PACKAGE_NAME]) | 245 MOJO_SHELL_PACKAGE_NAME])) |
| 233 atexit.register(self.StopShell) | 246 atexit.register(self.StopShell) |
| 234 | 247 |
| 235 extra_shell_args = [] | 248 extra_shell_args = [] |
| 236 origin_url = origin if origin else self._StartHttpServerForDirectory( | 249 origin_url = origin if origin else self._StartHttpServerForDirectory( |
| 237 self.local_dir, DEFAULT_BASE_PORT if fixed_port else 0) | 250 self.local_dir, DEFAULT_BASE_PORT if fixed_port else 0) |
| 238 extra_shell_args.append("--origin=" + origin_url) | 251 extra_shell_args.append("--origin=" + origin_url) |
| 239 | 252 |
| 240 return extra_shell_args | 253 return extra_shell_args |
| 241 | 254 |
| 242 def StartShell(self, | 255 def StartShell(self, |
| 243 arguments, | 256 arguments, |
| 244 stdout=None, | 257 stdout=None, |
| 245 on_application_stop=None, | 258 on_application_stop=None, |
| 246 fixed_port=True): | 259 fixed_port=True): |
| 247 """ | 260 """ |
| 248 Starts the mojo shell, passing it the given arguments. | 261 Starts the mojo shell, passing it the given arguments. |
| 249 | 262 |
| 250 The |arguments| list must contain the "--origin=" arg from PrepareShellRun. | 263 The |arguments| list must contain the "--origin=" arg from PrepareShellRun. |
| 251 If |stdout| is not None, it should be a valid argument for subprocess.Popen. | 264 If |stdout| is not None, it should be a valid argument for subprocess.Popen. |
| 252 """ | 265 """ |
| 253 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % MOJO_SHELL_PACKAGE_NAME | 266 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % MOJO_SHELL_PACKAGE_NAME |
| 254 | 267 |
| 255 cmd = [self.adb_path, | 268 cmd = self._CreateADBCommand([ |
| 256 'shell', | 269 'shell', |
| 257 'am', | 270 'am', |
| 258 'start', | 271 'start', |
| 259 '-S', | 272 '-S', |
| 260 '-a', 'android.intent.action.VIEW', | 273 '-a', 'android.intent.action.VIEW', |
| 261 '-n', '%s/.MojoShellActivity' % MOJO_SHELL_PACKAGE_NAME] | 274 '-n', '%s/.MojoShellActivity' % MOJO_SHELL_PACKAGE_NAME]) |
| 262 | 275 |
| 263 parameters = [] | 276 parameters = [] |
| 264 if stdout or on_application_stop: | 277 if stdout or on_application_stop: |
| 265 subprocess.check_call([self.adb_path, 'shell', 'rm', STDOUT_PIPE]) | 278 subprocess.check_call(self._CreateADBCommand( |
| 279 ['shell', 'rm', STDOUT_PIPE])) |
| 266 parameters.append('--fifo-path=%s' % STDOUT_PIPE) | 280 parameters.append('--fifo-path=%s' % STDOUT_PIPE) |
| 267 self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop) | 281 self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop) |
| 268 # The origin has to be specified whether it's local or external. | 282 # The origin has to be specified whether it's local or external. |
| 269 assert any("--origin=" in arg for arg in arguments) | 283 assert any("--origin=" in arg for arg in arguments) |
| 270 | 284 |
| 271 # Extract map-origin arguments. | 285 # Extract map-origin arguments. |
| 272 map_parameters, other_parameters = _Split(arguments, _IsMapOrigin) | 286 map_parameters, other_parameters = _Split(arguments, _IsMapOrigin) |
| 273 parameters += other_parameters | 287 parameters += other_parameters |
| 274 parameters += self._StartHttpServerForOriginMappings(map_parameters, | 288 parameters += self._StartHttpServerForOriginMappings(map_parameters, |
| 275 fixed_port) | 289 fixed_port) |
| 276 | 290 |
| 277 if parameters: | 291 if parameters: |
| 278 encodedParameters = json.dumps(parameters) | 292 encodedParameters = json.dumps(parameters) |
| 279 cmd += ['--es', 'encodedParameters', encodedParameters] | 293 cmd += ['--es', 'encodedParameters', encodedParameters] |
| 280 | 294 |
| 281 with open(os.devnull, 'w') as devnull: | 295 with open(os.devnull, 'w') as devnull: |
| 282 subprocess.Popen(cmd, stdout=devnull).wait() | 296 subprocess.Popen(cmd, stdout=devnull).wait() |
| 283 | 297 |
| 284 def StopShell(self): | 298 def StopShell(self): |
| 285 """ | 299 """ |
| 286 Stops the mojo shell. | 300 Stops the mojo shell. |
| 287 """ | 301 """ |
| 288 subprocess.check_call( | 302 subprocess.check_call(self._CreateADBCommand(['shell', |
| 289 [self.adb_path, 'shell', 'am', 'force-stop', MOJO_SHELL_PACKAGE_NAME]) | 303 'am', |
| 304 'force-stop', |
| 305 MOJO_SHELL_PACKAGE_NAME])) |
| 290 | 306 |
| 291 def CleanLogs(self): | 307 def CleanLogs(self): |
| 292 """ | 308 """ |
| 293 Cleans the logs on the device. | 309 Cleans the logs on the device. |
| 294 """ | 310 """ |
| 295 subprocess.check_call([self.adb_path, 'logcat', '-c']) | 311 subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) |
| 296 | 312 |
| 297 def ShowLogs(self): | 313 def ShowLogs(self): |
| 298 """ | 314 """ |
| 299 Displays the log for the mojo shell. | 315 Displays the log for the mojo shell. |
| 300 | 316 |
| 301 Returns the process responsible for reading the logs. | 317 Returns the process responsible for reading the logs. |
| 302 """ | 318 """ |
| 303 logcat = subprocess.Popen([self.adb_path, | 319 logcat = subprocess.Popen(self._CreateADBCommand([ |
| 304 'logcat', | 320 'logcat', |
| 305 '-s', | 321 '-s', |
| 306 ' '.join(LOGCAT_TAGS)], | 322 ' '.join(LOGCAT_TAGS)]), |
| 307 stdout=sys.stdout) | 323 stdout=sys.stdout) |
| 308 atexit.register(_ExitIfNeeded, logcat) | 324 atexit.register(_ExitIfNeeded, logcat) |
| 309 return logcat | 325 return logcat |
| OLD | NEW |