blob: bc1364b6ddd75ff9ee2a6cc936efcf88a52e69d8 [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
20
21
22class FindDeviceError(RuntimeError):
23 pass
24
25
26class DeviceNotFoundError(FindDeviceError):
27 def __init__(self, serial):
28 self.serial = serial
29 super(DeviceNotFoundError, self).__init__(
30 'No device with serial {}'.format(serial))
31
32
33class NoUniqueDeviceError(FindDeviceError):
34 def __init__(self):
35 super(NoUniqueDeviceError, self).__init__('No unique device')
36
37
38def get_devices():
39 with open(os.devnull, 'wb') as devnull:
40 subprocess.check_call(['adb', 'start-server'], stdout=devnull,
41 stderr=devnull)
42 out = subprocess.check_output(['adb', 'devices']).splitlines()
43
44 # The first line of `adb devices` just says "List of attached devices", so
45 # skip that.
46 devices = []
47 for line in out[1:]:
48 if not line.strip():
49 continue
50 if 'offline' in line:
51 continue
52
53 serial, _ = re.split(r'\s+', line, maxsplit=1)
54 devices.append(serial)
55 return devices
56
57
58def _get_unique_device(product=None):
59 devices = get_devices()
60 if len(devices) != 1:
61 raise NoUniqueDeviceError()
62 return AndroidDevice(devices[0], product)
63
Dan Alberte2b4a5f2015-07-28 14:53:03 -070064
Dan Albert8e1fdd72015-07-24 17:08:33 -070065def _get_device_by_serial(serial, product=None):
66 for device in get_devices():
67 if device == serial:
68 return AndroidDevice(serial, product)
69 raise DeviceNotFoundError(serial)
70
71
72def get_device(serial=None, product=None):
73 """Get a uniquely identified AndroidDevice if one is available.
74
75 Raises:
76 DeviceNotFoundError:
77 The serial specified by `serial` or $ANDROID_SERIAL is not
78 connected.
79
80 NoUniqueDeviceError:
81 Neither `serial` nor $ANDROID_SERIAL was set, and the number of
82 devices connected to the system is not 1. Having 0 connected
83 devices will also result in this error.
84
85 Returns:
86 An AndroidDevice associated with the first non-None identifier in the
87 following order of preference:
88
89 1) The `serial` argument.
90 2) The environment variable $ANDROID_SERIAL.
91 3) The single device connnected to the system.
92 """
93 if serial is not None:
94 return _get_device_by_serial(serial, product)
95
96 android_serial = os.getenv('ANDROID_SERIAL')
97 if android_serial is not None:
98 return _get_device_by_serial(android_serial, product)
99
100 return _get_unique_device(product)
101
102
103class AndroidDevice(object):
David Purselld4093f12015-08-10 12:52:16 -0700104 # Delimiter string to indicate the start of the exit code.
105 _RETURN_CODE_DELIMITER = 'x'
106
107 # Follow any shell command with this string to get the exit
108 # status of a program since this isn't propagated by adb.
109 #
110 # The delimiter is needed because `printf 1; echo $?` would print
111 # "10", and we wouldn't be able to distinguish the exit code.
112 _RETURN_CODE_PROBE_STRING = 'echo "{0}$?"'.format(_RETURN_CODE_DELIMITER)
113
114 # Maximum search distance from the output end to find the delimiter.
115 _RETURN_CODE_SEARCH_LENGTH = len('{0}255\n'.format(_RETURN_CODE_DELIMITER))
116
Dan Albert8e1fdd72015-07-24 17:08:33 -0700117 def __init__(self, serial, product=None):
118 self.serial = serial
119 self.product = product
120 self.adb_cmd = ['adb']
121 if self.serial is not None:
122 self.adb_cmd.extend(['-s', serial])
123 if self.product is not None:
124 self.adb_cmd.extend(['-p', product])
125 self._linesep = None
Dan Albert8e1fdd72015-07-24 17:08:33 -0700126
127 @property
128 def linesep(self):
129 if self._linesep is None:
David Purselld4093f12015-08-10 12:52:16 -0700130 self._linesep = subprocess.check_output(self.adb_cmd +
131 ['shell', 'echo'])
Dan Albert8e1fdd72015-07-24 17:08:33 -0700132 return self._linesep
133
134 def _make_shell_cmd(self, user_cmd):
David Purselld4093f12015-08-10 12:52:16 -0700135 return (self.adb_cmd + ['shell'] + user_cmd +
136 ['; ' + self._RETURN_CODE_PROBE_STRING])
Dan Albert8e1fdd72015-07-24 17:08:33 -0700137
David Purselld4093f12015-08-10 12:52:16 -0700138 def _parse_shell_output(self, out):
139 """Finds the exit code string from shell output.
140
141 Args:
142 out: Shell output string.
143
144 Returns:
145 An (exit_code, output_string) tuple. The output string is
146 cleaned of any additional stuff we appended to find the
147 exit code.
148
149 Raises:
150 RuntimeError: Could not find the exit code in |out|.
151 """
Dan Albert8e1fdd72015-07-24 17:08:33 -0700152 search_text = out
David Purselld4093f12015-08-10 12:52:16 -0700153 if len(search_text) > self._RETURN_CODE_SEARCH_LENGTH:
154 # We don't want to search over massive amounts of data when we know
155 # the part we want is right at the end.
156 search_text = search_text[-self._RETURN_CODE_SEARCH_LENGTH:]
157 partition = search_text.rpartition(self._RETURN_CODE_DELIMITER)
158 if partition[1] == '':
Dan Albert8e1fdd72015-07-24 17:08:33 -0700159 raise RuntimeError('Could not find exit status in shell output.')
David Purselld4093f12015-08-10 12:52:16 -0700160 result = int(partition[2])
161 # partition[0] won't contain the full text if search_text was truncated,
162 # pull from the original string instead.
163 out = out[:-len(partition[1]) - len(partition[2])]
Dan Albert8e1fdd72015-07-24 17:08:33 -0700164 return result, out
165
166 def _simple_call(self, cmd):
Yasuhiro Matsudaab379832015-07-03 02:08:55 +0900167 logging.info(' '.join(self.adb_cmd + cmd))
Dan Albert8e1fdd72015-07-24 17:08:33 -0700168 return subprocess.check_output(
169 self.adb_cmd + cmd, stderr=subprocess.STDOUT)
170
171 def shell(self, cmd):
Yasuhiro Matsudaab379832015-07-03 02:08:55 +0900172 logging.info(' '.join(self.adb_cmd + ['shell'] + cmd))
Dan Albert8e1fdd72015-07-24 17:08:33 -0700173 cmd = self._make_shell_cmd(cmd)
174 out = subprocess.check_output(cmd)
175 rc, out = self._parse_shell_output(out)
176 if rc != 0:
177 error = subprocess.CalledProcessError(rc, cmd)
178 error.out = out
179 raise error
180 return out
181
182 def shell_nocheck(self, cmd):
183 cmd = self._make_shell_cmd(cmd)
Yasuhiro Matsudaab379832015-07-03 02:08:55 +0900184 logging.info(' '.join(cmd))
Dan Albert8e1fdd72015-07-24 17:08:33 -0700185 p = subprocess.Popen(
186 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
187 out, _ = p.communicate()
188 return self._parse_shell_output(out)
189
Yasuhiro Matsudac0822e82015-08-05 19:49:03 +0900190 def install(self, filename, replace=False):
191 cmd = ['install']
192 if replace:
193 cmd.append('-r')
194 cmd.append(filename)
195 return self._simple_call(cmd)
Dan Albert8e1fdd72015-07-24 17:08:33 -0700196
197 def push(self, local, remote):
198 return self._simple_call(['push', local, remote])
199
200 def pull(self, remote, local):
201 return self._simple_call(['pull', remote, local])
202
203 def sync(self, directory=None):
204 cmd = ['sync']
205 if directory is not None:
206 cmd.append(directory)
207 return self._simple_call(cmd)
208
209 def forward(self, local, remote):
210 return self._simple_call(['forward', local, remote])
211
212 def tcpip(self, port):
213 return self._simple_call(['tcpip', port])
214
215 def usb(self):
216 return self._simple_call(['usb'])
217
Yasuhiro Matsudaab379832015-07-03 02:08:55 +0900218 def reboot(self):
219 return self._simple_call(['reboot'])
220
Dan Albert8e1fdd72015-07-24 17:08:33 -0700221 def root(self):
222 return self._simple_call(['root'])
223
224 def unroot(self):
225 return self._simple_call(['unroot'])
226
227 def forward_remove(self, local):
228 return self._simple_call(['forward', '--remove', local])
229
230 def forward_remove_all(self):
231 return self._simple_call(['forward', '--remove-all'])
232
233 def connect(self, host):
234 return self._simple_call(['connect', host])
235
236 def disconnect(self, host):
237 return self._simple_call(['disconnect', host])
238
239 def reverse(self, remote, local):
240 return self._simple_call(['reverse', remote, local])
241
242 def reverse_remove_all(self):
243 return self._simple_call(['reverse', '--remove-all'])
244
245 def reverse_remove(self, remote):
246 return self._simple_call(['reverse', '--remove', remote])
247
248 def wait(self):
249 return self._simple_call(['wait-for-device'])
250
251 def get_prop(self, prop_name):
Dan Alberte2b4a5f2015-07-28 14:53:03 -0700252 output = self.shell(['getprop', prop_name]).splitlines()
Dan Albert8e1fdd72015-07-24 17:08:33 -0700253 if len(output) != 1:
254 raise RuntimeError('Too many lines in getprop output:\n' +
255 '\n'.join(output))
256 value = output[0]
257 if not value.strip():
258 return None
259 return value
260
261 def set_prop(self, prop_name, value):
262 self.shell(['setprop', prop_name, value])