Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(73)

Side by Side Diff: appengine/swarming/swarming_bot/platforms/android.py

Issue 1306633002: Overhaul Android support and make Swarming bot use python-adb (Closed) Base URL: git@github.com:luci/luci-py.git@master
Patch Set: Packaged libusb1 as a relative package to fix import paths Created 5 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2015 The Swarming Authors. All rights reserved.
2 # Use of this source code is governed by the Apache v2.0 license that can be
3 # found in the LICENSE file.
4
5 """Android specific utility functions.
6
7 This file serves as an API to bot_config.py. bot_config.py can be replaced on
8 the server to allow additional server-specific functionality.
9 """
10
11 import logging
12 import os
13 import re
14 import subprocess
15 import sys
16
17 # This file must be imported from swarming_bot.zip (or with '..' in sys.path).
18 # libusb1 must have been put in the path already.
19
20 import adb
21 import adb.adb_commands
22
23 try:
24 from M2Crypto import RSA
25 except ImportError:
26 # In this case, adb support is disabled until maruel stops being dumb and
27 # figure out how to use pycrypto or rsa, both already available.
28 RSA = None
29
30
31 ### Private stuff.
32
33
34 # Set when ADB is initialized. It contains one or multiple key used to
35 # authenticate to Android debug protocol (adb).
36 _ADB_KEYS = None
37
38
39 # Cache of /system/build.prop on Android devices connected to this host.
40 _BUILD_PROP_ANDROID = {}
41
42
43 def _dumpsys(cmd, arg):
44 out = cmd.Shell('dumpsys ' + arg).decode('utf-8', 'replace')
45 if out.startswith('Can\'t find service: '):
46 return None
47 return out.splitlines()
48
49
50 def initialize(pub_key, priv_key):
51 """Initialize Android support through adb.
52
53 You can steal pub_key, priv_key pair from ~/.android/adbkey and
54 ~/.android/adbkey.pub.
55 """
56 global _ADB_KEYS
57 assert bool(pub_key) == bool(priv_key)
58 if _ADB_KEYS is None:
59 _ADB_KEYS = []
60 if not RSA:
61 logging.error('M2Crypto is missing, run: pip install --user M2Crypto')
62 return False
63
64 if pub_key:
65 _ADB_KEYS.append(M2CryptoSigner(pub_key, priv_key))
66
67 # Try to add local adb keys if available.
68 path = os.path.expanduser('~/.android/adbkey')
69 if os.path.isfile(path) and os.path.isfile(path+'.pub'):
70 with open(path + '.pub', 'rb') as f:
71 pub = f.read()
72 with open(path, 'rb') as f:
73 priv = f.read()
74 _ADB_KEYS.append(M2CryptoSigner(pub, priv))
75
76 if not _ADB_KEYS:
77 return False
78 else:
79 if pub_key:
80 logging.warning('initialize() was called repeatedly: ignoring keys')
81 return bool(_ADB_KEYS)
82
83
84 class M2CryptoSigner(object):
85 """Implements adb_protocol.AuthSigner using
86 https://github.com/martinpaljak/M2Crypto.
87 """
88 def __init__(self, pub, priv):
89 self.priv_key = RSA.load_key_string(priv)
90 self.pub_key = pub
91
92 def Sign(self, data):
93 return self.priv_key.sign(data, 'sha1')
94
95 def GetPublicKey(self):
96 return self.pub_key
97
98
99 # TODO(maruel): M2Crypto is not included by default on Ubuntu.
100 # rsa is included in client/third_party/rsa/rsa/ and
101 # pycrypto is normally installed on Ubuntu. It would be preferable to use one of
102 # these 2 but my skills failed up to now, authentication consistently fails.
103 # Revisit later or delete the code.
104 #
105 #
106 #sys.path.insert(0, os.path.join(THIS_FILE, 'third_party', 'rsa'))
107 #import rsa
108 #
109 #class RSASigner(object):
110 # """Implements adb_protocol.AuthSigner using http://stuvel.eu/rsa."""
111 # def __init__(self):
112 # self.privkey = rsa.PrivateKey.load_pkcs1(PRIV_CONVERTED_KEY)
113 #
114 # def Sign(self, data):
115 # return rsa.sign(data, self.privkey, 'SHA-1')
116 #
117 # def GetPublicKey(self):
118 # return PUB_KEY
119 #
120 #
121 #try:
122 # from Crypto.Hash import SHA
123 # from Crypto.PublicKey import RSA
124 # from Crypto.Signature import PKCS1_v1_5
125 # from Crypto.Signature import PKCS1_PSS
126 #except ImportError:
127 # SHA = None
128 #
129 #
130 #class CryptoSigner(object):
131 # """Implements adb_protocol.AuthSigner using
132 # https://www.dlitz.net/software/pycrypto/.
133 # """
134 # def __init__(self):
135 # self.private_key = RSA.importKey(PRIV_KEY, None)
136 # self._signer = PKCS1_v1_5.new(self.private_key)
137 # #self.private_key = RSA.importKey(PRIV_CONVERTED_KEY, None)
138 # #self._signer = PKCS1_PSS.new(self.private_key)
139 #
140 # def Sign(self, data):
141 # h = SHA.new(data)
142 # return self._signer.sign(h)
143 #
144 # def GetPublicKey(self):
145 # return PUB_KEY
146
147
148 def kill_adb():
149 """adb sucks. Kill it with fire."""
150 if not adb:
151 return
152 try:
153 subprocess.call(['adb', 'kill-server'])
154 except OSError:
155 pass
156 subprocess.call(['killall', '--exact', 'adb'])
157
158
159 def get_devices():
160 """Returns the list of devices available.
161
162 Caller MUST call close_devices(cmds) on the return value.
163
164 Returns one of:
165 - dict of {serial_number: adb.adb_commands.AdbCommands}. The value may be
166 None if there was an Auth failure.
167 - None if adb is unavailable.
168 """
169 if not adb:
170 return None
171
172 cmds = {}
173 for handle in adb.adb_commands.AdbCommands.Devices():
174 try:
175 handle.Open()
176 except adb.common.usb1.USBErrorBusy:
177 logging.warning(
178 'Got USBErrorBusy for %s. Killing adb', handle.serial_number)
179 kill_adb()
180 try:
181 # If it throws again, it probably means another process
182 # holds a handle to the USB ports or group acl (plugdev) hasn't been
183 # setup properly.
184 handle.Open()
185 except adb.common.usb1.USBErrorBusy as e:
186 logging.warning(
187 'USB port for %s is already open (and failed to kill ADB) '
188 'Try rebooting the host: %s', handle.serial_number, e)
189 cmds[handle.serial_number] = None
190 continue
191 except adb.common.usb1.USBErrorAccess as e:
192 # Do not try to use serial_number, since we can't even access the port.
193 logging.warning(
194 'Try rebooting the host: %s: %s', handle.port_path, e)
195 cmds['/'.join(map(str, handle.port_path))] = None
196 continue
197
198 try:
199 cmd = adb.adb_commands.AdbCommands.Connect(
200 handle, banner='swarming', rsa_keys=_ADB_KEYS, auth_timeout_ms=100)
201 except adb.usb_exceptions.DeviceAuthError as e:
202 logging.warning('AUTH FAILURE: %s: %s', handle.serial_number, e)
203 cmd = None
204 except adb.usb_exceptions.ReadFailedError as e:
205 logging.warning('READ FAILURE: %s: %s', handle.serial_number, e)
206 cmd = None
207 except ValueError as e:
208 logging.warning(
209 'Trying unpluging and pluging it back: %s: %s',
210 handle.serial_number, e)
211 cmd = None
212 cmds[handle.serial_number] = cmd
213
214 # Remove any /system/build.prop cache so if a device is disconnect, reflashed
215 # then reconnected, it will likely be refresh properly. The main concern is
216 # that the bot didn't have the time to loop once while this is being done.
217 # Restarting the bot works fine too.
218 for key in _BUILD_PROP_ANDROID.keys():
219 if key not in cmds:
220 _BUILD_PROP_ANDROID.pop(key)
221 return cmds
222
223
224 def close_devices(devices):
225 """Closes all devices opened by get_devices()."""
226 for device in (devices or {}).itervalues():
227 if device:
228 device.Close()
229
230
231 def get_build_prop(cmd):
232 """Returns the system properties for a device.
233
234 This isn't really changing through the lifetime of a bot. One corner case is
235 when the device is flashed or disconnected.
236 """
237 if cmd.handle.serial_number not in _BUILD_PROP_ANDROID:
238 properties = {}
239 try:
240 out = cmd.Shell('cat /system/build.prop').decode('utf-8')
241 except adb.usb_exceptions.ReadFailedError:
242 # It's a bit annoying because it means timeout_ms was wasted. Blacklist
243 # the device until it is disconnected and reconnected.
244 properties = None
245 else:
246 for line in out.splitlines():
247 if line.startswith(u'#') or not line:
248 continue
249 key, value = line.split(u'=', 1)
250 properties[key] = value
251 _BUILD_PROP_ANDROID[cmd.handle.serial_number] = properties
252 return _BUILD_PROP_ANDROID[cmd.handle.serial_number]
253
254
255 def get_temp(cmd):
256 """Returns the device's 2 temperatures."""
257 temps = []
258 for i in xrange(2):
259 try:
260 temps.append(
261 int(cmd.Shell('cat /sys/class/thermal/thermal_zone%d/temp' % i)))
262 except ValueError:
263 pass
264 return temps
265
266
267 def get_battery(cmd):
268 """Returns details about the battery's state."""
269 props = {}
270 out = _dumpsys(cmd, 'battery')
271 if not out:
272 return props
273 for line in out:
274 if line.endswith(u':'):
275 continue
276 key, value = line.split(u': ', 2)
277 props[key.lstrip()] = value
278 out = {u'power': []}
279 if props[u'AC powered'] == u'true':
280 out[u'power'].append(u'AC')
281 if props[u'USB powered'] == u'true':
282 out[u'power'].append(u'USB')
283 if props[u'Wireless powered'] == u'true':
284 out[u'power'].append(u'Wireless')
285 for key in (u'health', u'level', u'status', u'temperature', u'voltage'):
286 out[key] = props[key]
287 return out
288
289
290 def get_disk(cmd):
291 """Returns details about the battery's state."""
292 props = {}
293 out = _dumpsys(cmd, 'diskstats')
294 if not out:
295 return props
296 for line in out:
297 if line.endswith(u':'):
298 continue
299 key, value = line.split(u': ', 2)
300 match = re.match(u'^(\d+)K / (\d+)K.*', value)
301 if match:
302 props[key.lstrip()] = {
303 'free_mb': round(float(match.group(1)) / 1024., 1),
304 'size_mb': round(float(match.group(2)) / 1024., 1),
305 }
306 return {
307 u'cache': props[u'Cache-Free'],
308 u'data': props[u'Data-Free'],
309 u'system': props[u'System-Free'],
310 }
OLDNEW
« no previous file with comments | « appengine/swarming/swarming_bot/platforms/__init__.py ('k') | appengine/swarming/swarming_bot/python_libusb1 » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698