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