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

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

Powered by Google App Engine
This is Rietveld 408576698