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.
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.
36 'MojoShellApplication',
40 MAPPING_PREFIX
= '--map-origin='
43 def _ExitIfNeeded(process
):
44 """Exits |process| if it is still alive."""
45 if process
.poll() is None:
49 class AndroidShell(object):
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.
54 def __init__(self
, config
):
55 self
.adb_path
= constants
.GetAdbPath()
56 self
.paths
= Paths(config
)
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
))
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 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
)
198 # No longer need the logcat process.
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',
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?
223 local_gdb_process
= subprocess
.Popen([self
._GetLocalGdbPath
(),
227 atexit
.register(_ExitIfNeeded
, local_gdb_process
)
228 local_gdb_process
.wait()
229 signal
.signal(signal
.SIGINT
, signal
.SIG_DFL
)
231 def StartActivity(self
,
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.
243 arguments
+= self
.shell_args
245 cmd
= self
._CreateADBCommand
([
250 '-a', 'android.intent.action.VIEW',
251 '-n', '%s/%s.%s' % (self
.target_package
,
255 logcat_process
= None
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
)
280 self
._WaitForProcessIdAndStartGdb
(logcat_process
)
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
([
292 ' '.join(LOGCAT_TAGS
)]),
294 atexit
.register(_ExitIfNeeded
, logcat
)
298 def _CreateGdbInit(tmp_dir
, gdb_init_path
, build_dir
):
300 Creates the gdbinit file.
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'
312 ' info sharedlibrary\n'
316 'You are now in gdb and need to type continue (or c) to continue '
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
:
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.
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
345 os
.symlink(src
, os
.path
.join(dest_dir
, android_name
))