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