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 |