blob: a15675b80bba22f1944260efdf2854e01304041f [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):
104 def __init__(self, serial, product=None):
105 self.serial = serial
106 self.product = product
107 self.adb_cmd = ['adb']
108 if self.serial is not None:
109 self.adb_cmd.extend(['-s', serial])
110 if self.product is not None:
111 self.adb_cmd.extend(['-p', product])
112 self._linesep = None
113 self._shell_result_pattern = None
114
115 @property
116 def linesep(self):
117 if self._linesep is None:
118 self._linesep = subprocess.check_output(['adb', 'shell', 'echo'])
119 return self._linesep
120
121 def _make_shell_cmd(self, user_cmd):
122 # Follow any shell command with `; echo; echo $?` to get the exit
123 # status of a program since this isn't propagated by adb.
124 #
125 # The leading newline is needed because `printf 1; echo $?` would print
126 # "10", and we wouldn't be able to distinguish the exit code.
127 rc_probe = '; echo "\n$?"'
128 return self.adb_cmd + ['shell'] + user_cmd + [rc_probe]
129
130 def _parse_shell_output(self, out): # pylint: disable=no-self-use
131 search_text = out
132 max_result_len = len('{0}255{0}'.format(self.linesep))
133 if len(search_text) > max_result_len:
134 # We don't want to regex match over massive amounts of data when we
135 # know the part we want is right at the end.
136 search_text = search_text[-max_result_len:]
137 if self._shell_result_pattern is None:
138 self._shell_result_pattern = re.compile(
139 r'({0}\d+{0})$'.format(self.linesep), re.MULTILINE)
140 m = self._shell_result_pattern.search(search_text)
141 if m is None:
142 raise RuntimeError('Could not find exit status in shell output.')
143
144 result_text = m.group(1)
145 result = int(result_text.strip())
146 out = out[:-len(result_text)] # Trim the result text from the output.
147 return result, out
148
149 def _simple_call(self, cmd):
Yasuhiro Matsudaab379832015-07-03 02:08:55 +0900150 logging.info(' '.join(self.adb_cmd + cmd))
Dan Albert8e1fdd72015-07-24 17:08:33 -0700151 return subprocess.check_output(
152 self.adb_cmd + cmd, stderr=subprocess.STDOUT)
153
154 def shell(self, cmd):
Yasuhiro Matsudaab379832015-07-03 02:08:55 +0900155 logging.info(' '.join(self.adb_cmd + ['shell'] + cmd))
Dan Albert8e1fdd72015-07-24 17:08:33 -0700156 cmd = self._make_shell_cmd(cmd)
157 out = subprocess.check_output(cmd)
158 rc, out = self._parse_shell_output(out)
159 if rc != 0:
160 error = subprocess.CalledProcessError(rc, cmd)
161 error.out = out
162 raise error
163 return out
164
165 def shell_nocheck(self, cmd):
166 cmd = self._make_shell_cmd(cmd)
Yasuhiro Matsudaab379832015-07-03 02:08:55 +0900167 logging.info(' '.join(cmd))
Dan Albert8e1fdd72015-07-24 17:08:33 -0700168 p = subprocess.Popen(
169 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
170 out, _ = p.communicate()
171 return self._parse_shell_output(out)
172
Yasuhiro Matsudac0822e82015-08-05 19:49:03 +0900173 def install(self, filename, replace=False):
174 cmd = ['install']
175 if replace:
176 cmd.append('-r')
177 cmd.append(filename)
178 return self._simple_call(cmd)
Dan Albert8e1fdd72015-07-24 17:08:33 -0700179
180 def push(self, local, remote):
181 return self._simple_call(['push', local, remote])
182
183 def pull(self, remote, local):
184 return self._simple_call(['pull', remote, local])
185
186 def sync(self, directory=None):
187 cmd = ['sync']
188 if directory is not None:
189 cmd.append(directory)
190 return self._simple_call(cmd)
191
192 def forward(self, local, remote):
193 return self._simple_call(['forward', local, remote])
194
195 def tcpip(self, port):
196 return self._simple_call(['tcpip', port])
197
198 def usb(self):
199 return self._simple_call(['usb'])
200
Yasuhiro Matsudaab379832015-07-03 02:08:55 +0900201 def reboot(self):
202 return self._simple_call(['reboot'])
203
Dan Albert8e1fdd72015-07-24 17:08:33 -0700204 def root(self):
205 return self._simple_call(['root'])
206
207 def unroot(self):
208 return self._simple_call(['unroot'])
209
210 def forward_remove(self, local):
211 return self._simple_call(['forward', '--remove', local])
212
213 def forward_remove_all(self):
214 return self._simple_call(['forward', '--remove-all'])
215
216 def connect(self, host):
217 return self._simple_call(['connect', host])
218
219 def disconnect(self, host):
220 return self._simple_call(['disconnect', host])
221
222 def reverse(self, remote, local):
223 return self._simple_call(['reverse', remote, local])
224
225 def reverse_remove_all(self):
226 return self._simple_call(['reverse', '--remove-all'])
227
228 def reverse_remove(self, remote):
229 return self._simple_call(['reverse', '--remove', remote])
230
231 def wait(self):
232 return self._simple_call(['wait-for-device'])
233
234 def get_prop(self, prop_name):
Dan Alberte2b4a5f2015-07-28 14:53:03 -0700235 output = self.shell(['getprop', prop_name]).splitlines()
Dan Albert8e1fdd72015-07-24 17:08:33 -0700236 if len(output) != 1:
237 raise RuntimeError('Too many lines in getprop output:\n' +
238 '\n'.join(output))
239 value = output[0]
240 if not value.strip():
241 return None
242 return value
243
244 def set_prop(self, prop_name, value):
245 self.shell(['setprop', prop_name, value])