| OLD | NEW |
| (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 } |
| OLD | NEW |