[Media Router] Add integration tests and e2e tests for media router and presentation...
[chromium-blink-merge.git] / mojo / tools / mopy / android.py
blobfc2f84f557a6e8ed5963b63d1889cf7085d87a72
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.
5 import atexit
6 import itertools
7 import logging
8 import os
9 import shutil
10 import signal
11 import subprocess
12 import sys
13 import tempfile
14 import threading
15 import time
16 import urlparse
18 from .paths import Paths
20 sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir,
21 os.pardir, 'build', 'android'))
22 from pylib import constants
23 from pylib.base import base_test_runner
24 from pylib.device import device_errors
25 from pylib.device import device_utils
26 from pylib.utils import base_error
27 from pylib.utils import apk_helper
30 # Tags used by the mojo shell application logs.
31 LOGCAT_TAGS = [
32 'AndroidHandler',
33 'MojoFileHelper',
34 'MojoMain',
35 'MojoShellActivity',
36 'MojoShellApplication',
37 'chromium',
40 MAPPING_PREFIX = '--map-origin='
43 def _ExitIfNeeded(process):
44 """Exits |process| if it is still alive."""
45 if process.poll() is None:
46 process.kill()
49 class AndroidShell(object):
50 """
51 Used to set up and run a given mojo shell binary on an Android device.
52 |config| is the mopy.config.Config for the build.
53 """
54 def __init__(self, config):
55 self.adb_path = constants.GetAdbPath()
56 self.paths = Paths(config)
57 self.device = None
58 self.shell_args = []
59 self.target_package = apk_helper.GetPackageName(self.paths.apk_path)
60 # This is used by decive_utils.Install to check if the apk needs updating.
61 constants.SetOutputDirectory(self.paths.build_dir)
63 # TODO(msw): Use pylib's adb_wrapper and device_utils instead.
64 def _CreateADBCommand(self, args):
65 adb_command = [self.adb_path, '-s', self.device.adb.GetDeviceSerial()]
66 adb_command.extend(args)
67 logging.getLogger().debug("Command: %s", " ".join(adb_command))
68 return adb_command
70 def _ReadFifo(self, path, pipe, on_fifo_closed, max_attempts=5):
71 """
72 Reads the fifo at |path| on the device and write the contents to |pipe|.
73 Calls |on_fifo_closed| when the fifo is closed. This method will try to find
74 the path up to |max_attempts|, waiting 1 second between each attempt. If it
75 cannot find |path|, a exception will be raised.
76 """
77 def Run():
78 def _WaitForFifo():
79 for _ in xrange(max_attempts):
80 if self.device.FileExists(path):
81 return
82 time.sleep(1)
83 on_fifo_closed()
84 raise Exception("Unable to find fifo: %s" % path)
85 _WaitForFifo()
86 stdout_cat = subprocess.Popen(self._CreateADBCommand([
87 'shell',
88 'cat',
89 path]),
90 stdout=pipe)
91 atexit.register(_ExitIfNeeded, stdout_cat)
92 stdout_cat.wait()
93 on_fifo_closed()
95 thread = threading.Thread(target=Run, name="StdoutRedirector")
96 thread.start()
98 def _StartHttpServerForDirectory(self, path):
99 test_server_helper = base_test_runner.BaseTestRunner(self.device, None)
100 ports = test_server_helper.LaunchTestHttpServer(path)
101 atexit.register(test_server_helper.ShutdownHelperToolsForTestSuite)
102 print 'Hosting %s at http://127.0.0.1:%d' % (path, ports[1])
103 return 'http://127.0.0.1:%d/' % ports[0]
105 def _StartHttpServerForOriginMapping(self, mapping):
107 If |mapping| points at a local file starts an http server to serve files
108 from the directory and returns the new mapping. This is intended to be
109 called for every --map-origin value.
111 parts = mapping.split('=')
112 if len(parts) != 2:
113 return mapping
114 dest = parts[1]
115 # If the destination is a URL, don't map it.
116 if urlparse.urlparse(dest)[0]:
117 return mapping
118 # Assume the destination is a local file. Start a local server that
119 # redirects to it.
120 localUrl = self._StartHttpServerForDirectory(dest)
121 return parts[0] + '=' + localUrl
123 def _StartHttpServerForOriginMappings(self, map_parameters):
124 """Calls _StartHttpServerForOriginMapping for every --map-origin arg."""
125 if not map_parameters:
126 return []
128 original_values = list(itertools.chain(
129 *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters)))
130 sorted(original_values)
131 result = []
132 for value in original_values:
133 result.append(self._StartHttpServerForOriginMapping(value))
134 return [MAPPING_PREFIX + ','.join(result)]
136 def InitShell(self, origin='localhost', device=None):
138 Runs adb as root, starts an origin server, and installs the apk as needed.
139 |origin| is the origin for mojo: URLs; if its value is 'localhost', a local
140 http server will be set up to serve files from the build directory.
141 |device| is the target device to run on, if multiple devices are connected.
142 Returns 0 on success or a non-zero exit code on a terminal failure.
144 try:
145 devices = device_utils.DeviceUtils.HealthyDevices()
146 if device:
147 self.device = next((d for d in devices if d == device), None)
148 if not self.device:
149 raise device_errors.DeviceUnreachableError(device)
150 elif devices:
151 self.device = devices[0]
152 else:
153 raise device_errors.NoDevicesError()
155 logging.getLogger().debug("Using device: %s", self.device)
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
167 if origin is 'localhost':
168 origin = self._StartHttpServerForDirectory(self.paths.build_dir)
169 if origin:
170 self.shell_args.append("--origin=" + origin)
171 return 0
173 def _GetProcessId(self, process):
174 """Returns the process id of the process on the remote device."""
175 while True:
176 line = process.stdout.readline()
177 pid_command = 'launcher waiting for GDB. pid: '
178 index = line.find(pid_command)
179 if index != -1:
180 return line[index + len(pid_command):].strip()
181 return 0
183 def _GetLocalGdbPath(self):
184 """Returns the path to the android gdb."""
185 return os.path.join(self.paths.src_root, "third_party", "android_tools",
186 "ndk", "toolchains", "arm-linux-androideabi-4.9",
187 "prebuilt", "linux-x86_64", "bin",
188 "arm-linux-androideabi-gdb")
190 def _WaitForProcessIdAndStartGdb(self, process):
192 Waits until we see the process id from the remote device, starts up
193 gdbserver on the remote device, and gdb on the local device.
195 # Wait until we see "PID"
196 pid = self._GetProcessId(process)
197 assert pid != 0
198 # No longer need the logcat process.
199 process.kill()
200 # Disable python's processing of SIGINT while running gdb. Otherwise
201 # control-c doesn't work well in gdb.
202 signal.signal(signal.SIGINT, signal.SIG_IGN)
203 gdbserver_process = subprocess.Popen(self._CreateADBCommand(['shell',
204 'gdbserver',
205 '--attach',
206 ':5039',
207 pid]))
208 atexit.register(_ExitIfNeeded, gdbserver_process)
210 temp_dir = tempfile.mkdtemp()
211 atexit.register(shutil.rmtree, temp_dir, True)
213 gdbinit_path = os.path.join(temp_dir, 'gdbinit')
214 _CreateGdbInit(temp_dir, gdbinit_path, self.paths.build_dir)
216 _CreateSOLinks(temp_dir, self.paths.build_dir)
218 # Wait a second for gdb to start up on the device. Without this the local
219 # gdb starts before the remote side has registered the port.
220 # TODO(sky): maybe we should try a couple of times and then give up?
221 time.sleep(1)
223 local_gdb_process = subprocess.Popen([self._GetLocalGdbPath(),
224 "-x",
225 gdbinit_path],
226 cwd=temp_dir)
227 atexit.register(_ExitIfNeeded, local_gdb_process)
228 local_gdb_process.wait()
229 signal.signal(signal.SIGINT, signal.SIG_DFL)
231 def StartActivity(self,
232 activity_name,
233 arguments,
234 stdout,
235 on_fifo_closed,
236 gdb=False):
238 Starts the shell with the given |arguments|, directing output to |stdout|.
239 |on_fifo_closed| will be run if the FIFO can't be found or when it's closed.
240 |gdb| is a flag that attaches gdb to the device's remote process on startup.
242 assert self.device
243 arguments += self.shell_args
245 cmd = self._CreateADBCommand([
246 'shell',
247 'am',
248 'start',
249 '-S',
250 '-a', 'android.intent.action.VIEW',
251 '-n', '%s/%s.%s' % (self.target_package,
252 self.target_package,
253 activity_name)])
255 logcat_process = None
256 if gdb:
257 arguments.append('--wait-for-debugger')
258 # Remote debugging needs a port forwarded.
259 self.device.adb.Forward('tcp:5039', 'tcp:5039')
260 logcat_process = self.ShowLogs(stdout=subprocess.PIPE)
262 fifo_path = "/data/data/%s/stdout.fifo" % self.target_package
263 subprocess.check_call(self._CreateADBCommand(
264 ['shell', 'rm', '-f', fifo_path]))
265 arguments.append('--fifo-path=%s' % fifo_path)
266 max_attempts = 200 if '--wait-for-debugger' in arguments else 5
267 self._ReadFifo(fifo_path, stdout, on_fifo_closed, max_attempts)
269 # Extract map-origin args and add the extras array with commas escaped.
270 parameters = [a for a in arguments if not a.startswith(MAPPING_PREFIX)]
271 map_parameters = [a for a in arguments if a.startswith(MAPPING_PREFIX)]
272 parameters += self._StartHttpServerForOriginMappings(map_parameters)
273 parameters = [p.replace(',', '\,') for p in parameters]
274 cmd += ['--esa', '%s.extras' % self.target_package, ','.join(parameters)]
276 atexit.register(self.StopShell)
277 with open(os.devnull, 'w') as devnull:
278 cmd_process = subprocess.Popen(cmd, stdout=devnull)
279 if logcat_process:
280 self._WaitForProcessIdAndStartGdb(logcat_process)
281 cmd_process.wait()
283 def StopShell(self):
284 """Stops the mojo shell."""
285 self.device.ForceStop(self.target_package)
287 def ShowLogs(self, stdout=sys.stdout):
288 """Displays the mojo shell logs and returns the process reading the logs."""
289 logcat = subprocess.Popen(self._CreateADBCommand([
290 'logcat',
291 '-s',
292 ' '.join(LOGCAT_TAGS)]),
293 stdout=stdout)
294 atexit.register(_ExitIfNeeded, logcat)
295 return logcat
298 def _CreateGdbInit(tmp_dir, gdb_init_path, build_dir):
300 Creates the gdbinit file.
302 Args:
303 tmp_dir: the directory where the gdbinit and other files lives.
304 gdb_init_path: path to gdbinit
305 build_dir: path where build files are located.
307 gdbinit = ('target remote localhost:5039\n'
308 'def reload-symbols\n'
309 ' set solib-search-path %s:%s\n'
310 'end\n'
311 'def info-symbols\n'
312 ' info sharedlibrary\n'
313 'end\n'
314 'reload-symbols\n'
315 'echo \\n\\n'
316 'You are now in gdb and need to type continue (or c) to continue '
317 'execution.\\n'
318 'gdb is in the directory %s\\n'
319 'The following functions have been defined:\\n'
320 'reload-symbols: forces reloading symbols. If after a crash you\\n'
321 'still do not see symbols you likely need to create a link in\\n'
322 'the directory you are in.\\n'
323 'info-symbols: shows status of current shared libraries.\\n'
324 'NOTE: you may need to type reload-symbols again after a '
325 'crash.\\n\\n' % (tmp_dir, build_dir, tmp_dir))
326 with open(gdb_init_path, 'w') as f:
327 f.write(gdbinit)
330 def _CreateSOLinks(dest_dir, build_dir):
331 """Creates links from files (eg. *.mojo) to the real .so for gdb to find."""
332 # The files to create links for. The key is the name as seen on the device,
333 # and the target an array of path elements as to where the .so lives (relative
334 # to the output directory).
335 # TODO(sky): come up with some way to automate this.
336 files_to_link = {
337 'html_viewer.mojo': ['libhtml_viewer', 'html_viewer_library.so'],
338 'libmandoline_runner.so': ['mandoline_runner'],
340 for android_name, so_path in files_to_link.iteritems():
341 src = os.path.join(build_dir, *so_path)
342 if not os.path.isfile(src):
343 print 'Expected file not found', src
344 sys.exit(-1)
345 os.symlink(src, os.path.join(dest_dir, android_name))