blob: 57f17fc78fbd87f9f76321b13ec3614d87432efa [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#
16import os
17import re
18import subprocess
19
20
21class FindDeviceError(RuntimeError):
22 pass
23
24
25class DeviceNotFoundError(FindDeviceError):
26 def __init__(self, serial):
27 self.serial = serial
28 super(DeviceNotFoundError, self).__init__(
29 'No device with serial {}'.format(serial))
30
31
32class NoUniqueDeviceError(FindDeviceError):
33 def __init__(self):
34 super(NoUniqueDeviceError, self).__init__('No unique device')
35
36
37def get_devices():
38 with open(os.devnull, 'wb') as devnull:
39 subprocess.check_call(['adb', 'start-server'], stdout=devnull,
40 stderr=devnull)
41 out = subprocess.check_output(['adb', 'devices']).splitlines()
42
43 # The first line of `adb devices` just says "List of attached devices", so
44 # skip that.
45 devices = []
46 for line in out[1:]:
47 if not line.strip():
48 continue
49 if 'offline' in line:
50 continue
51
52 serial, _ = re.split(r'\s+', line, maxsplit=1)
53 devices.append(serial)
54 return devices
55
56
57def _get_unique_device(product=None):
58 devices = get_devices()
59 if len(devices) != 1:
60 raise NoUniqueDeviceError()
61 return AndroidDevice(devices[0], product)
62
Dan Alberte2b4a5f2015-07-28 14:53:03 -070063
Dan Albert8e1fdd72015-07-24 17:08:33 -070064def _get_device_by_serial(serial, product=None):
65 for device in get_devices():
66 if device == serial:
67 return AndroidDevice(serial, product)
68 raise DeviceNotFoundError(serial)
69
70
71def get_device(serial=None, product=None):
72 """Get a uniquely identified AndroidDevice if one is available.
73
74 Raises:
75 DeviceNotFoundError:
76 The serial specified by `serial` or $ANDROID_SERIAL is not
77 connected.
78
79 NoUniqueDeviceError:
80 Neither `serial` nor $ANDROID_SERIAL was set, and the number of
81 devices connected to the system is not 1. Having 0 connected
82 devices will also result in this error.
83
84 Returns:
85 An AndroidDevice associated with the first non-None identifier in the
86 following order of preference:
87
88 1) The `serial` argument.
89 2) The environment variable $ANDROID_SERIAL.
90 3) The single device connnected to the system.
91 """
92 if serial is not None:
93 return _get_device_by_serial(serial, product)
94
95 android_serial = os.getenv('ANDROID_SERIAL')
96 if android_serial is not None:
97 return _get_device_by_serial(android_serial, product)
98
99 return _get_unique_device(product)
100
101
102class AndroidDevice(object):
103 def __init__(self, serial, product=None):
104 self.serial = serial
105 self.product = product
106 self.adb_cmd = ['adb']
107 if self.serial is not None:
108 self.adb_cmd.extend(['-s', serial])
109 if self.product is not None:
110 self.adb_cmd.extend(['-p', product])
111 self._linesep = None
112 self._shell_result_pattern = None
113
114 @property
115 def linesep(self):
116 if self._linesep is None:
117 self._linesep = subprocess.check_output(['adb', 'shell', 'echo'])
118 return self._linesep
119
120 def _make_shell_cmd(self, user_cmd):
121 # Follow any shell command with `; echo; echo $?` to get the exit
122 # status of a program since this isn't propagated by adb.
123 #
124 # The leading newline is needed because `printf 1; echo $?` would print
125 # "10", and we wouldn't be able to distinguish the exit code.
126 rc_probe = '; echo "\n$?"'
127 return self.adb_cmd + ['shell'] + user_cmd + [rc_probe]
128
129 def _parse_shell_output(self, out): # pylint: disable=no-self-use
130 search_text = out
131 max_result_len = len('{0}255{0}'.format(self.linesep))
132 if len(search_text) > max_result_len:
133 # We don't want to regex match over massive amounts of data when we
134 # know the part we want is right at the end.
135 search_text = search_text[-max_result_len:]
136 if self._shell_result_pattern is None:
137 self._shell_result_pattern = re.compile(
138 r'({0}\d+{0})$'.format(self.linesep), re.MULTILINE)
139 m = self._shell_result_pattern.search(search_text)
140 if m is None:
141 raise RuntimeError('Could not find exit status in shell output.')
142
143 result_text = m.group(1)
144 result = int(result_text.strip())
145 out = out[:-len(result_text)] # Trim the result text from the output.
146 return result, out
147
148 def _simple_call(self, cmd):
149 return subprocess.check_output(
150 self.adb_cmd + cmd, stderr=subprocess.STDOUT)
151
152 def shell(self, cmd):
153 cmd = self._make_shell_cmd(cmd)
154 out = subprocess.check_output(cmd)
155 rc, out = self._parse_shell_output(out)
156 if rc != 0:
157 error = subprocess.CalledProcessError(rc, cmd)
158 error.out = out
159 raise error
160 return out
161
162 def shell_nocheck(self, cmd):
163 cmd = self._make_shell_cmd(cmd)
164 p = subprocess.Popen(
165 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
166 out, _ = p.communicate()
167 return self._parse_shell_output(out)
168
169 def install(self, filename):
170 return self._simple_call(['install', filename])
171
172 def push(self, local, remote):
173 return self._simple_call(['push', local, remote])
174
175 def pull(self, remote, local):
176 return self._simple_call(['pull', remote, local])
177
178 def sync(self, directory=None):
179 cmd = ['sync']
180 if directory is not None:
181 cmd.append(directory)
182 return self._simple_call(cmd)
183
184 def forward(self, local, remote):
185 return self._simple_call(['forward', local, remote])
186
187 def tcpip(self, port):
188 return self._simple_call(['tcpip', port])
189
190 def usb(self):
191 return self._simple_call(['usb'])
192
193 def root(self):
194 return self._simple_call(['root'])
195
196 def unroot(self):
197 return self._simple_call(['unroot'])
198
199 def forward_remove(self, local):
200 return self._simple_call(['forward', '--remove', local])
201
202 def forward_remove_all(self):
203 return self._simple_call(['forward', '--remove-all'])
204
205 def connect(self, host):
206 return self._simple_call(['connect', host])
207
208 def disconnect(self, host):
209 return self._simple_call(['disconnect', host])
210
211 def reverse(self, remote, local):
212 return self._simple_call(['reverse', remote, local])
213
214 def reverse_remove_all(self):
215 return self._simple_call(['reverse', '--remove-all'])
216
217 def reverse_remove(self, remote):
218 return self._simple_call(['reverse', '--remove', remote])
219
220 def wait(self):
221 return self._simple_call(['wait-for-device'])
222
223 def get_prop(self, prop_name):
Dan Alberte2b4a5f2015-07-28 14:53:03 -0700224 output = self.shell(['getprop', prop_name]).splitlines()
Dan Albert8e1fdd72015-07-24 17:08:33 -0700225 if len(output) != 1:
226 raise RuntimeError('Too many lines in getprop output:\n' +
227 '\n'.join(output))
228 value = output[0]
229 if not value.strip():
230 return None
231 return value
232
233 def set_prop(self, prop_name, value):
234 self.shell(['setprop', prop_name, value])