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.
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.
31 'MojoShellApplication',
35 MAPPING_PREFIX
= '--map-origin='
38 def _ExitIfNeeded(process
):
39 '''Exits |process| if it is still alive.'''
40 if process
.poll() is None:
44 class AndroidShell(object):
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.
49 def __init__(self
, config
):
50 self
.adb_path
= constants
.GetAdbPath()
52 self
.paths
= Paths(config
)
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
))
67 def _ReadFifo(self
, path
, pipe
, on_fifo_closed
, max_attempts
=5):
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.
76 for _
in xrange(max_attempts
):
77 if self
.device
.FileExists(path
):
81 raise Exception('Unable to find fifo: %s' % path
)
83 stdout_cat
= subprocess
.Popen(self
._CreateADBCommand
([
88 atexit
.register(_ExitIfNeeded
, stdout_cat
)
92 thread
= threading
.Thread(target
=Run
, name
='StdoutRedirector')
96 def InitShell(self
, device
=None):
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.
103 devices
= device_utils
.DeviceUtils
.HealthyDevices()
105 self
.device
= next((d
for d
in devices
if d
== device
), None)
107 raise device_errors
.DeviceUnreachableError(device
)
109 self
.device
= devices
[0]
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
127 def _GetProcessId(self
, process
):
128 '''Returns the process id of the process on the remote device.'''
130 line
= process
.stdout
.readline()
131 pid_command
= 'launcher waiting for GDB. pid: '
132 index
= line
.find(pid_command
)
134 return line
[index
+ len(pid_command
):].strip()
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')
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
)
162 # No longer need the logcat process.
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',
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?
182 local_gdb_process
= subprocess
.Popen([self
._GetLocalGdbPath
(),
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
,
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.
203 arguments
+= self
.shell_args
205 cmd
= self
._CreateADBCommand
([
210 '-a', 'android.intent.action.VIEW',
211 '-n', '%s/%s.%s' % (self
.target_package
,
215 logcat_process
= None
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
)
239 self
._WaitForProcessIdAndStartGdb
(logcat_process
)
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
([
251 ' '.join(LOGCAT_TAGS
)]),
253 atexit
.register(_ExitIfNeeded
, logcat
)
257 def _CreateGdbInit(tmp_dir
, gdb_init_path
, build_dir
):
259 Creates the gdbinit file.
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'
271 ' info sharedlibrary\n'
275 'You are now in gdb and need to type continue (or c) to continue '
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
: