blob: 516e88003f1afa344ff1ca125ebd52f4e2aee0d7 [file] [log] [blame]
Dan Albert8e1fdd72015-07-24 17:08:33 -07001#
2# Copyright (C) 2015 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
Yasuhiro Matsudaab379832015-07-03 02:08:55 +090016import logging
Dan Albert8e1fdd72015-07-24 17:08:33 -070017import os
18import re
19import subprocess
Spencer Lowde4505f2015-08-27 20:58:29 -070020import tempfile
Dan Albert8e1fdd72015-07-24 17:08:33 -070021
22
23class FindDeviceError(RuntimeError):
24 pass
25
26
27class DeviceNotFoundError(FindDeviceError):
28 def __init__(self, serial):
29 self.serial = serial
30 super(DeviceNotFoundError, self).__init__(
31 'No device with serial {}'.format(serial))
32
33
34class NoUniqueDeviceError(FindDeviceError):
35 def __init__(self):
36 super(NoUniqueDeviceError, self).__init__('No unique device')
37
38
David Pursell606835a2015-09-08 17:17:02 -070039class ShellError(RuntimeError):
40 def __init__(self, cmd, stdout, stderr, exit_code):
41 super(ShellError, self).__init__(
42 '`{0}` exited with code {1}'.format(cmd, exit_code))
43 self.cmd = cmd
44 self.stdout = stdout
45 self.stderr = stderr
46 self.exit_code = exit_code
47
48
Dan Albert8e1fdd72015-07-24 17:08:33 -070049def get_devices():
50 with open(os.devnull, 'wb') as devnull:
51 subprocess.check_call(['adb', 'start-server'], stdout=devnull,
52 stderr=devnull)
53 out = subprocess.check_output(['adb', 'devices']).splitlines()
54
55 # The first line of `adb devices` just says "List of attached devices", so
56 # skip that.
57 devices = []
58 for line in out[1:]:
59 if not line.strip():
60 continue
61 if 'offline' in line:
62 continue
63
64 serial, _ = re.split(r'\s+', line, maxsplit=1)
65 devices.append(serial)
66 return devices
67
68
69def _get_unique_device(product=None):
70 devices = get_devices()
71 if len(devices) != 1:
72 raise NoUniqueDeviceError()
73 return AndroidDevice(devices[0], product)
74
Dan Alberte2b4a5f2015-07-28 14:53:03 -070075
Dan Albert8e1fdd72015-07-24 17:08:33 -070076def _get_device_by_serial(serial, product=None):
77 for device in get_devices():
78 if device == serial:
79 return AndroidDevice(serial, product)
80 raise DeviceNotFoundError(serial)
81
82
83def get_device(serial=None, product=None):
84 """Get a uniquely identified AndroidDevice if one is available.
85
86 Raises:
87 DeviceNotFoundError:
88 The serial specified by `serial` or $ANDROID_SERIAL is not
89 connected.
90
91 NoUniqueDeviceError:
92 Neither `serial` nor $ANDROID_SERIAL was set, and the number of
93 devices connected to the system is not 1. Having 0 connected
94 devices will also result in this error.
95
96 Returns:
97 An AndroidDevice associated with the first non-None identifier in the
98 following order of preference:
99
100 1) The `serial` argument.
101 2) The environment variable $ANDROID_SERIAL.
102 3) The single device connnected to the system.
103 """
104 if serial is not None:
105 return _get_device_by_serial(serial, product)
106
107 android_serial = os.getenv('ANDROID_SERIAL')
108 if android_serial is not None:
109 return _get_device_by_serial(android_serial, product)
110
111 return _get_unique_device(product)
112
Spencer Lowde4505f2015-08-27 20:58:29 -0700113# Call this instead of subprocess.check_output() to work-around issue in Python
114# 2's subprocess class on Windows where it doesn't support Unicode. This
115# writes the command line to a UTF-8 batch file that is properly interpreted
116# by cmd.exe.
117def _subprocess_check_output(*popenargs, **kwargs):
118 # Only do this slow work-around if Unicode is in the cmd line.
119 if (os.name == 'nt' and
120 any(isinstance(arg, unicode) for arg in popenargs[0])):
121 # cmd.exe requires a suffix to know that it is running a batch file
122 tf = tempfile.NamedTemporaryFile('wb', suffix='.cmd', delete=False)
123 # @ in batch suppresses echo of the current line.
124 # Change the codepage to 65001, the UTF-8 codepage.
125 tf.write('@chcp 65001 > nul\r\n')
126 tf.write('@')
127 # Properly quote all the arguments and encode in UTF-8.
128 tf.write(subprocess.list2cmdline(popenargs[0]).encode('utf-8'))
129 tf.close()
130
131 try:
132 result = subprocess.check_output(['cmd.exe', '/c', tf.name],
133 **kwargs)
134 except subprocess.CalledProcessError as e:
135 # Show real command line instead of the cmd.exe command line.
136 raise subprocess.CalledProcessError(e.returncode, popenargs[0],
137 output=e.output)
138 finally:
139 os.remove(tf.name)
140 return result
141 else:
142 return subprocess.check_output(*popenargs, **kwargs)
Dan Albert8e1fdd72015-07-24 17:08:33 -0700143
144class AndroidDevice(object):
David Purselld4093f12015-08-10 12:52:16 -0700145 # Delimiter string to indicate the start of the exit code.
146 _RETURN_CODE_DELIMITER = 'x'
147
148 # Follow any shell command with this string to get the exit
149 # status of a program since this isn't propagated by adb.
150 #
151 # The delimiter is needed because `printf 1; echo $?` would print
152 # "10", and we wouldn't be able to distinguish the exit code.
153 _RETURN_CODE_PROBE_STRING = 'echo "{0}$?"'.format(_RETURN_CODE_DELIMITER)
154
155 # Maximum search distance from the output end to find the delimiter.
Spencer Lowb7e79af2015-08-16 16:38:47 -0700156 # adb on Windows returns \r\n even if adbd returns \n.
157 _RETURN_CODE_SEARCH_LENGTH = len('{0}255\r\n'.format(_RETURN_CODE_DELIMITER))
David Purselld4093f12015-08-10 12:52:16 -0700158
David Pursell606835a2015-09-08 17:17:02 -0700159 # Shell protocol feature string.
160 SHELL_PROTOCOL_FEATURE = 'shell_2'
161
Dan Albert8e1fdd72015-07-24 17:08:33 -0700162 def __init__(self, serial, product=None):
163 self.serial = serial
164 self.product = product
165 self.adb_cmd = ['adb']
166 if self.serial is not None:
167 self.adb_cmd.extend(['-s', serial])
168 if self.product is not None:
169 self.adb_cmd.extend(['-p', product])
170 self._linesep = None
David Pursell606835a2015-09-08 17:17:02 -0700171 self._features = None
Dan Albert8e1fdd72015-07-24 17:08:33 -0700172
173 @property
174 def linesep(self):
175 if self._linesep is None:
David Purselld4093f12015-08-10 12:52:16 -0700176 self._linesep = subprocess.check_output(self.adb_cmd +
177 ['shell', 'echo'])
Dan Albert8e1fdd72015-07-24 17:08:33 -0700178 return self._linesep
179
David Pursell606835a2015-09-08 17:17:02 -0700180 @property
181 def features(self):
182 if self._features is None:
183 try:
184 self._features = self._simple_call(['features']).splitlines()
185 except subprocess.CalledProcessError:
186 self._features = []
187 return self._features
188
Dan Albert8e1fdd72015-07-24 17:08:33 -0700189 def _make_shell_cmd(self, user_cmd):
David Pursell606835a2015-09-08 17:17:02 -0700190 command = self.adb_cmd + ['shell'] + user_cmd
191 if self.SHELL_PROTOCOL_FEATURE not in self.features:
192 command.append('; ' + self._RETURN_CODE_PROBE_STRING)
193 return command
Dan Albert8e1fdd72015-07-24 17:08:33 -0700194
David Purselld4093f12015-08-10 12:52:16 -0700195 def _parse_shell_output(self, out):
196 """Finds the exit code string from shell output.
197
198 Args:
199 out: Shell output string.
200
201 Returns:
202 An (exit_code, output_string) tuple. The output string is
203 cleaned of any additional stuff we appended to find the
204 exit code.
205
206 Raises:
207 RuntimeError: Could not find the exit code in |out|.
208 """
Dan Albert8e1fdd72015-07-24 17:08:33 -0700209 search_text = out
David Purselld4093f12015-08-10 12:52:16 -0700210 if len(search_text) > self._RETURN_CODE_SEARCH_LENGTH:
211 # We don't want to search over massive amounts of data when we know
212 # the part we want is right at the end.
213 search_text = search_text[-self._RETURN_CODE_SEARCH_LENGTH:]
214 partition = search_text.rpartition(self._RETURN_CODE_DELIMITER)
215 if partition[1] == '':
Dan Albert8e1fdd72015-07-24 17:08:33 -0700216 raise RuntimeError('Could not find exit status in shell output.')
David Purselld4093f12015-08-10 12:52:16 -0700217 result = int(partition[2])
218 # partition[0] won't contain the full text if search_text was truncated,
219 # pull from the original string instead.
220 out = out[:-len(partition[1]) - len(partition[2])]
Dan Albert8e1fdd72015-07-24 17:08:33 -0700221 return result, out
222
223 def _simple_call(self, cmd):
Yasuhiro Matsudaab379832015-07-03 02:08:55 +0900224 logging.info(' '.join(self.adb_cmd + cmd))
Spencer Lowde4505f2015-08-27 20:58:29 -0700225 return _subprocess_check_output(
Dan Albert8e1fdd72015-07-24 17:08:33 -0700226 self.adb_cmd + cmd, stderr=subprocess.STDOUT)
227
228 def shell(self, cmd):
David Pursell606835a2015-09-08 17:17:02 -0700229 """Calls `adb shell`
230
231 Args:
232 cmd: string shell command to execute.
233
234 Returns:
235 A (stdout, stderr) tuple. Stderr may be combined into stdout
236 if the device doesn't support separate streams.
237
238 Raises:
239 ShellError: the exit code was non-zero.
240 """
241 exit_code, stdout, stderr = self.shell_nocheck(cmd)
242 if exit_code != 0:
243 raise ShellError(cmd, stdout, stderr, exit_code)
244 return stdout, stderr
Dan Albert8e1fdd72015-07-24 17:08:33 -0700245
246 def shell_nocheck(self, cmd):
David Pursell606835a2015-09-08 17:17:02 -0700247 """Calls `adb shell`
248
249 Args:
250 cmd: string shell command to execute.
251
252 Returns:
253 An (exit_code, stdout, stderr) tuple. Stderr may be combined
254 into stdout if the device doesn't support separate streams.
255 """
Dan Albert8e1fdd72015-07-24 17:08:33 -0700256 cmd = self._make_shell_cmd(cmd)
Yasuhiro Matsudaab379832015-07-03 02:08:55 +0900257 logging.info(' '.join(cmd))
Dan Albert8e1fdd72015-07-24 17:08:33 -0700258 p = subprocess.Popen(
David Pursell606835a2015-09-08 17:17:02 -0700259 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
260 stdout, stderr = p.communicate()
261 if self.SHELL_PROTOCOL_FEATURE in self.features:
262 exit_code = p.returncode
263 else:
264 exit_code, stdout = self._parse_shell_output(stdout)
265 return exit_code, stdout, stderr
Dan Albert8e1fdd72015-07-24 17:08:33 -0700266
Yasuhiro Matsudac0822e82015-08-05 19:49:03 +0900267 def install(self, filename, replace=False):
268 cmd = ['install']
269 if replace:
270 cmd.append('-r')
271 cmd.append(filename)
272 return self._simple_call(cmd)
Dan Albert8e1fdd72015-07-24 17:08:33 -0700273
274 def push(self, local, remote):
275 return self._simple_call(['push', local, remote])
276
277 def pull(self, remote, local):
278 return self._simple_call(['pull', remote, local])
279
280 def sync(self, directory=None):
281 cmd = ['sync']
282 if directory is not None:
283 cmd.append(directory)
284 return self._simple_call(cmd)
285
286 def forward(self, local, remote):
287 return self._simple_call(['forward', local, remote])
288
289 def tcpip(self, port):
290 return self._simple_call(['tcpip', port])
291
292 def usb(self):
293 return self._simple_call(['usb'])
294
Yasuhiro Matsudaab379832015-07-03 02:08:55 +0900295 def reboot(self):
296 return self._simple_call(['reboot'])
297
Dan Albert8e1fdd72015-07-24 17:08:33 -0700298 def root(self):
299 return self._simple_call(['root'])
300
301 def unroot(self):
302 return self._simple_call(['unroot'])
303
304 def forward_remove(self, local):
305 return self._simple_call(['forward', '--remove', local])
306
307 def forward_remove_all(self):
308 return self._simple_call(['forward', '--remove-all'])
309
310 def connect(self, host):
311 return self._simple_call(['connect', host])
312
313 def disconnect(self, host):
314 return self._simple_call(['disconnect', host])
315
316 def reverse(self, remote, local):
317 return self._simple_call(['reverse', remote, local])
318
319 def reverse_remove_all(self):
320 return self._simple_call(['reverse', '--remove-all'])
321
322 def reverse_remove(self, remote):
323 return self._simple_call(['reverse', '--remove', remote])
324
325 def wait(self):
326 return self._simple_call(['wait-for-device'])
327
328 def get_prop(self, prop_name):
David Pursell606835a2015-09-08 17:17:02 -0700329 output = self.shell(['getprop', prop_name])[0].splitlines()
Dan Albert8e1fdd72015-07-24 17:08:33 -0700330 if len(output) != 1:
331 raise RuntimeError('Too many lines in getprop output:\n' +
332 '\n'.join(output))
333 value = output[0]
334 if not value.strip():
335 return None
336 return value
337
338 def set_prop(self, prop_name, value):
339 self.shell(['setprop', prop_name, value])