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.
16 from .paths
import Paths
18 sys
.path
.append(os
.path
.join(os
.path
.abspath(os
.path
.dirname(__file__
)),
19 '..', '..', '..', 'build', 'android'))
20 from pylib
import constants
21 from pylib
.base
import base_test_runner
22 from pylib
.device
import device_errors
23 from pylib
.device
import device_utils
24 from pylib
.utils
import base_error
25 from pylib
.utils
import apk_helper
28 # Tags used by the mojo shell application logs.
34 'MojoShellApplication',
38 MAPPING_PREFIX
= '--map-origin='
41 def _ExitIfNeeded(process
):
42 '''Exits |process| if it is still alive.'''
43 if process
.poll() is None:
47 class AndroidShell(object):
49 Used to set up and run a given mojo shell binary on an Android device.
50 |config| is the mopy.config.Config for the build.
52 def __init__(self
, config
):
53 self
.adb_path
= constants
.GetAdbPath()
55 self
.paths
= Paths(config
)
58 self
.target_package
= apk_helper
.GetPackageName(self
.paths
.apk_path
)
59 self
.temp_gdb_dir
= None
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
))
70 def _ReadFifo(self
, path
, pipe
, on_fifo_closed
, max_attempts
=5):
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.
79 for _
in xrange(max_attempts
):
80 if self
.device
.FileExists(path
):
84 raise Exception('Unable to find fifo: %s' % path
)
86 stdout_cat
= subprocess
.Popen(self
._CreateADBCommand
([
91 atexit
.register(_ExitIfNeeded
, stdout_cat
)
95 thread
= threading
.Thread(target
=Run
, name
='StdoutRedirector')
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('=')
115 # If the destination is a URL, don't map it.
116 if urlparse
.urlparse(dest
)[0]:
118 # Assume the destination is a local file. Start a local server that
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
:
128 original_values
= list(itertools
.chain(
129 *map(lambda x
: x
[len(MAPPING_PREFIX
):].split(','), map_parameters
)))
130 sorted(original_values
)
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.
145 devices
= device_utils
.DeviceUtils
.HealthyDevices()
147 self
.device
= next((d
for d
in devices
if d
== device
), None)
149 raise device_errors
.DeviceUnreachableError(device
)
151 self
.device
= devices
[0]
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
)
170 self
.shell_args
.append('--origin=' + origin
)
173 def _GetProcessId(self
, process
):
174 '''Returns the process id of the process on the remote device.'''
176 line
= process
.stdout
.readline()
177 pid_command
= 'launcher waiting for GDB. pid: '
178 index
= line
.find(pid_command
)
180 return line
[index
+ len(pid_command
):].strip()
183 def _GetLocalGdbPath(self
):
184 '''Returns the path to the android gdb.'''
185 if self
.config
.target_cpu
== 'arm':
186 return os
.path
.join(constants
.ANDROID_NDK_ROOT
, 'toolchains',
187 'arm-linux-androideabi-4.9', 'prebuilt',
188 'linux-x86_64', 'bin', 'arm-linux-androideabi-gdb')
189 elif self
.config
.target_cpu
== 'x86':
190 return os
.path
.join(constants
.ANDROID_NDK_ROOT
, 'toolchains',
191 'x86-4.9', 'prebuilt', 'linux-x86_64', 'bin',
192 'i686-linux-android-gdb')
193 elif self
.config
.target_cpu
== 'x64':
194 return os
.path
.join(constants
.ANDROID_NDK_ROOT
, 'toolchains',
195 'x86_64-4.9', 'prebuilt', 'linux-x86_64', 'bin',
196 'x86_64-linux-android-gdb')
198 raise Exception('Unknown target_cpu: %s' % self
.config
.target_cpu
)
200 def _WaitForProcessIdAndStartGdb(self
, process
):
202 Waits until we see the process id from the remote device, starts up
203 gdbserver on the remote device, and gdb on the local device.
205 # Wait until we see 'PID'
206 pid
= self
._GetProcessId
(process
)
208 # No longer need the logcat process.
210 # Disable python's processing of SIGINT while running gdb. Otherwise
211 # control-c doesn't work well in gdb.
212 signal
.signal(signal
.SIGINT
, signal
.SIG_IGN
)
213 gdbserver_process
= subprocess
.Popen(self
._CreateADBCommand
(['shell',
218 atexit
.register(_ExitIfNeeded
, gdbserver_process
)
220 gdbinit_path
= os
.path
.join(self
.temp_gdb_dir
, 'gdbinit')
221 _CreateGdbInit(self
.temp_gdb_dir
, gdbinit_path
, self
.paths
.build_dir
)
223 # Wait a second for gdb to start up on the device. Without this the local
224 # gdb starts before the remote side has registered the port.
225 # TODO(sky): maybe we should try a couple of times and then give up?
228 local_gdb_process
= subprocess
.Popen([self
._GetLocalGdbPath
(),
231 cwd
=self
.temp_gdb_dir
)
232 atexit
.register(_ExitIfNeeded
, local_gdb_process
)
233 local_gdb_process
.wait()
234 signal
.signal(signal
.SIGINT
, signal
.SIG_DFL
)
236 def StartActivity(self
,
243 Starts the shell with the given |arguments|, directing output to |stdout|.
244 |on_fifo_closed| will be run if the FIFO can't be found or when it's closed.
245 |temp_gdb_dir| is set to a location with appropriate symlinks for gdb to
246 find when attached to the device's remote process on startup.
249 arguments
+= self
.shell_args
251 cmd
= self
._CreateADBCommand
([
256 '-a', 'android.intent.action.VIEW',
257 '-n', '%s/%s.%s' % (self
.target_package
,
261 logcat_process
= None
263 self
.temp_gdb_dir
= temp_gdb_dir
264 arguments
.append('--wait-for-debugger')
265 # Remote debugging needs a port forwarded.
266 self
.device
.adb
.Forward('tcp:5039', 'tcp:5039')
267 logcat_process
= self
.ShowLogs(stdout
=subprocess
.PIPE
)
269 fifo_path
= '/data/data/%s/stdout.fifo' % self
.target_package
270 subprocess
.check_call(self
._CreateADBCommand
(
271 ['shell', 'rm', '-f', fifo_path
]))
272 arguments
.append('--fifo-path=%s' % fifo_path
)
273 max_attempts
= 200 if '--wait-for-debugger' in arguments
else 5
274 self
._ReadFifo
(fifo_path
, stdout
, on_fifo_closed
, max_attempts
)
276 # Extract map-origin args and add the extras array with commas escaped.
277 parameters
= [a
for a
in arguments
if not a
.startswith(MAPPING_PREFIX
)]
278 map_parameters
= [a
for a
in arguments
if a
.startswith(MAPPING_PREFIX
)]
279 parameters
+= self
._StartHttpServerForOriginMappings
(map_parameters
)
280 parameters
= [p
.replace(',', '\,') for p
in parameters
]
281 cmd
+= ['--esa', '%s.extras' % self
.target_package
, ','.join(parameters
)]
283 atexit
.register(self
.kill
)
284 with
open(os
.devnull
, 'w') as devnull
:
285 cmd_process
= subprocess
.Popen(cmd
, stdout
=devnull
)
287 self
._WaitForProcessIdAndStartGdb
(logcat_process
)
291 '''Stops the mojo shell; matches the Popen.kill method signature.'''
292 self
.device
.ForceStop(self
.target_package
)
294 def ShowLogs(self
, stdout
=sys
.stdout
):
295 '''Displays the mojo shell logs and returns the process reading the logs.'''
296 logcat
= subprocess
.Popen(self
._CreateADBCommand
([
299 ' '.join(LOGCAT_TAGS
)]),
301 atexit
.register(_ExitIfNeeded
, logcat
)
305 def _CreateGdbInit(tmp_dir
, gdb_init_path
, build_dir
):
307 Creates the gdbinit file.
310 tmp_dir: the directory where the gdbinit and other files lives.
311 gdb_init_path: path to gdbinit
312 build_dir: path where build files are located.
314 gdbinit
= ('target remote localhost:5039\n'
315 'def reload-symbols\n'
316 ' set solib-search-path %s:%s\n'
319 ' info sharedlibrary\n'
323 'You are now in gdb and need to type continue (or c) to continue '
325 'gdb is in the directory %s\\n'
326 'The following functions have been defined:\\n'
327 'reload-symbols: forces reloading symbols. If after a crash you\\n'
328 'still do not see symbols you likely need to create a link in\\n'
329 'the directory you are in.\\n'
330 'info-symbols: shows status of current shared libraries.\\n'
331 'NOTE: you may need to type reload-symbols again after a '
332 'crash.\\n\\n' % (tmp_dir
, build_dir
, tmp_dir
))
333 with
open(gdb_init_path
, 'w') as f
: