[Android] Implement 3-way sensor fallback for Device Orientation.
[chromium-blink-merge.git] / mojo / tools / mopy / android.py
blob2a886f7c3b604fda88792a45763df69474279739
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 logging
7 import os
8 import signal
9 import subprocess
10 import sys
11 import threading
12 import time
14 from .paths import Paths
16 sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)),
17 '..', '..', '..', 'build', 'android'))
18 from pylib import constants
19 from pylib.device import device_errors
20 from pylib.device import device_utils
21 from pylib.utils import base_error
22 from pylib.utils import apk_helper
25 # Tags used by the mojo shell application logs.
26 LOGCAT_TAGS = [
27 'AndroidHandler',
28 'MojoFileHelper',
29 'MojoMain',
30 'MojoShellActivity',
31 'MojoShellApplication',
32 'chromium',
35 MAPPING_PREFIX = '--map-origin='
38 def _ExitIfNeeded(process):
39 '''Exits |process| if it is still alive.'''
40 if process.poll() is None:
41 process.kill()
44 class AndroidShell(object):
45 '''
46 Used to set up and run a given mojo shell binary on an Android device.
47 |config| is the mopy.config.Config for the build.
48 '''
49 def __init__(self, config):
50 self.adb_path = constants.GetAdbPath()
51 self.config = config
52 self.paths = Paths(config)
53 self.device = None
54 self.shell_args = []
55 self.target_package = apk_helper.GetPackageName(self.paths.apk_path)
56 self.temp_gdb_dir = None
57 # This is used by decive_utils.Install to check if the apk needs updating.
58 constants.SetOutputDirectory(self.paths.build_dir)
60 # TODO(msw): Use pylib's adb_wrapper and device_utils instead.
61 def _CreateADBCommand(self, args):
62 adb_command = [self.adb_path, '-s', self.device.adb.GetDeviceSerial()]
63 adb_command.extend(args)
64 logging.getLogger().debug('Command: %s', ' '.join(adb_command))
65 return adb_command
67 def _ReadFifo(self, path, pipe, on_fifo_closed, max_attempts=5):
68 '''
69 Reads the fifo at |path| on the device and write the contents to |pipe|.
70 Calls |on_fifo_closed| when the fifo is closed. This method will try to find
71 the path up to |max_attempts|, waiting 1 second between each attempt. If it
72 cannot find |path|, a exception will be raised.
73 '''
74 def Run():
75 def _WaitForFifo():
76 for _ in xrange(max_attempts):
77 if self.device.FileExists(path):
78 return
79 time.sleep(1)
80 on_fifo_closed()
81 raise Exception('Unable to find fifo: %s' % path)
82 _WaitForFifo()
83 stdout_cat = subprocess.Popen(self._CreateADBCommand([
84 'shell',
85 'cat',
86 path]),
87 stdout=pipe)
88 atexit.register(_ExitIfNeeded, stdout_cat)
89 stdout_cat.wait()
90 on_fifo_closed()
92 thread = threading.Thread(target=Run, name='StdoutRedirector')
93 thread.start()
96 def InitShell(self, device=None):
97 '''
98 Runs adb as root, and installs the apk as needed. |device| is the target
99 device to run on, if multiple devices are connected. Returns 0 on success or
100 a non-zero exit code on a terminal failure.
102 try:
103 devices = device_utils.DeviceUtils.HealthyDevices()
104 if device:
105 self.device = next((d for d in devices if d == device), None)
106 if not self.device:
107 raise device_errors.DeviceUnreachableError(device)
108 elif devices:
109 self.device = devices[0]
110 else:
111 raise device_errors.NoDevicesError()
113 logging.getLogger().debug('Using device: %s', self.device)
114 # Clean the logs on the device to avoid displaying prior activity.
115 subprocess.check_call(self._CreateADBCommand(['logcat', '-c']))
116 self.device.EnableRoot()
117 self.device.Install(self.paths.apk_path)
118 except base_error.BaseError as e:
119 # Report 'device not found' as infra failures. See http://crbug.com/493900
120 print 'Exception in AndroidShell.InitShell:\n%s' % str(e)
121 if e.is_infra_error or 'error: device not found' in str(e):
122 return constants.INFRA_EXIT_CODE
123 return constants.ERROR_EXIT_CODE
125 return 0
127 def _GetProcessId(self, process):
128 '''Returns the process id of the process on the remote device.'''
129 while True:
130 line = process.stdout.readline()
131 pid_command = 'launcher waiting for GDB. pid: '
132 index = line.find(pid_command)
133 if index != -1:
134 return line[index + len(pid_command):].strip()
135 return 0
137 def _GetLocalGdbPath(self):
138 '''Returns the path to the android gdb.'''
139 if self.config.target_cpu == 'arm':
140 return os.path.join(constants.ANDROID_NDK_ROOT, 'toolchains',
141 'arm-linux-androideabi-4.9', 'prebuilt',
142 'linux-x86_64', 'bin', 'arm-linux-androideabi-gdb')
143 elif self.config.target_cpu == 'x86':
144 return os.path.join(constants.ANDROID_NDK_ROOT, 'toolchains',
145 'x86-4.9', 'prebuilt', 'linux-x86_64', 'bin',
146 'i686-linux-android-gdb')
147 elif self.config.target_cpu == 'x64':
148 return os.path.join(constants.ANDROID_NDK_ROOT, 'toolchains',
149 'x86_64-4.9', 'prebuilt', 'linux-x86_64', 'bin',
150 'x86_64-linux-android-gdb')
151 else:
152 raise Exception('Unknown target_cpu: %s' % self.config.target_cpu)
154 def _WaitForProcessIdAndStartGdb(self, process):
156 Waits until we see the process id from the remote device, starts up
157 gdbserver on the remote device, and gdb on the local device.
159 # Wait until we see 'PID'
160 pid = self._GetProcessId(process)
161 assert pid != 0
162 # No longer need the logcat process.
163 process.kill()
164 # Disable python's processing of SIGINT while running gdb. Otherwise
165 # control-c doesn't work well in gdb.
166 signal.signal(signal.SIGINT, signal.SIG_IGN)
167 gdbserver_process = subprocess.Popen(self._CreateADBCommand(['shell',
168 'gdbserver',
169 '--attach',
170 ':5039',
171 pid]))
172 atexit.register(_ExitIfNeeded, gdbserver_process)
174 gdbinit_path = os.path.join(self.temp_gdb_dir, 'gdbinit')
175 _CreateGdbInit(self.temp_gdb_dir, gdbinit_path, self.paths.build_dir)
177 # Wait a second for gdb to start up on the device. Without this the local
178 # gdb starts before the remote side has registered the port.
179 # TODO(sky): maybe we should try a couple of times and then give up?
180 time.sleep(1)
182 local_gdb_process = subprocess.Popen([self._GetLocalGdbPath(),
183 '-x',
184 gdbinit_path],
185 cwd=self.temp_gdb_dir)
186 atexit.register(_ExitIfNeeded, local_gdb_process)
187 local_gdb_process.wait()
188 signal.signal(signal.SIGINT, signal.SIG_DFL)
190 def StartActivity(self,
191 activity_name,
192 arguments,
193 stdout,
194 on_fifo_closed,
195 temp_gdb_dir=None):
197 Starts the shell with the given |arguments|, directing output to |stdout|.
198 |on_fifo_closed| will be run if the FIFO can't be found or when it's closed.
199 |temp_gdb_dir| is set to a location with appropriate symlinks for gdb to
200 find when attached to the device's remote process on startup.
202 assert self.device
203 arguments += self.shell_args
205 cmd = self._CreateADBCommand([
206 'shell',
207 'am',
208 'start',
209 '-S',
210 '-a', 'android.intent.action.VIEW',
211 '-n', '%s/%s.%s' % (self.target_package,
212 self.target_package,
213 activity_name)])
215 logcat_process = None
216 if temp_gdb_dir:
217 self.temp_gdb_dir = temp_gdb_dir
218 arguments.append('--wait-for-debugger')
219 # Remote debugging needs a port forwarded.
220 self.device.adb.Forward('tcp:5039', 'tcp:5039')
221 logcat_process = self.ShowLogs(stdout=subprocess.PIPE)
223 fifo_path = '/data/data/%s/stdout.fifo' % self.target_package
224 subprocess.check_call(self._CreateADBCommand(
225 ['shell', 'rm', '-f', fifo_path]))
226 arguments.append('--fifo-path=%s' % fifo_path)
227 max_attempts = 200 if '--wait-for-debugger' in arguments else 5
228 self._ReadFifo(fifo_path, stdout, on_fifo_closed, max_attempts)
230 # Extract map-origin args and add the extras array with commas escaped.
231 parameters = [a for a in arguments if not a.startswith(MAPPING_PREFIX)]
232 parameters = [p.replace(',', '\,') for p in parameters]
233 cmd += ['--esa', '%s.extras' % self.target_package, ','.join(parameters)]
235 atexit.register(self.kill)
236 with open(os.devnull, 'w') as devnull:
237 cmd_process = subprocess.Popen(cmd, stdout=devnull)
238 if logcat_process:
239 self._WaitForProcessIdAndStartGdb(logcat_process)
240 cmd_process.wait()
242 def kill(self):
243 '''Stops the mojo shell; matches the Popen.kill method signature.'''
244 self.device.ForceStop(self.target_package)
246 def ShowLogs(self, stdout=sys.stdout):
247 '''Displays the mojo shell logs and returns the process reading the logs.'''
248 logcat = subprocess.Popen(self._CreateADBCommand([
249 'logcat',
250 '-s',
251 ' '.join(LOGCAT_TAGS)]),
252 stdout=stdout)
253 atexit.register(_ExitIfNeeded, logcat)
254 return logcat
257 def _CreateGdbInit(tmp_dir, gdb_init_path, build_dir):
259 Creates the gdbinit file.
261 Args:
262 tmp_dir: the directory where the gdbinit and other files lives.
263 gdb_init_path: path to gdbinit
264 build_dir: path where build files are located.
266 gdbinit = ('target remote localhost:5039\n'
267 'def reload-symbols\n'
268 ' set solib-search-path %s:%s\n'
269 'end\n'
270 'def info-symbols\n'
271 ' info sharedlibrary\n'
272 'end\n'
273 'reload-symbols\n'
274 'echo \\n\\n'
275 'You are now in gdb and need to type continue (or c) to continue '
276 'execution.\\n'
277 'gdb is in the directory %s\\n'
278 'The following functions have been defined:\\n'
279 'reload-symbols: forces reloading symbols. If after a crash you\\n'
280 'still do not see symbols you likely need to create a link in\\n'
281 'the directory you are in.\\n'
282 'info-symbols: shows status of current shared libraries.\\n'
283 'NOTE: you may need to type reload-symbols again after a '
284 'crash.\\n\\n' % (tmp_dir, build_dir, tmp_dir))
285 with open(gdb_init_path, 'w') as f:
286 f.write(gdbinit)