android_xmlrpc_server.py revision 594b9ffb680d59eb33265e8fe023dbdbc4211c94
1#!/usr/bin/python2.7 2 3# Copyright (c) 2015 The Chromium OS Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7import argparse 8import contextlib2 9import errno 10import logging 11import Queue 12import select 13import shutil 14import signal 15import threading 16import time 17 18from SimpleXMLRPCServer import SimpleXMLRPCServer 19 20from acts import logger 21from acts import utils 22from acts.controllers import android_device 23from acts.test_utils.wifi import wifi_test_utils as wutils 24 25 26class Map(dict): 27 """A convenience class that makes dictionary values accessible via dot 28 operator. 29 30 Example: 31 >> m = Map({"SSID": "GoogleGuest"}) 32 >> m.SSID 33 GoogleGuest 34 """ 35 def __init__(self, *args, **kwargs): 36 super(Map, self).__init__(*args, **kwargs) 37 for arg in args: 38 if isinstance(arg, dict): 39 for k, v in arg.items(): 40 self[k] = v 41 if kwargs: 42 for k, v in kwargs.items(): 43 self[k] = v 44 45 46 def __getattr__(self, attr): 47 return self.get(attr) 48 49 50 def __setattr__(self, key, value): 51 self.__setitem__(key, value) 52 53 54# This is copied over from client/cros/xmlrpc_server.py so that this 55# daemon has no autotest dependencies. 56class XmlRpcServer(threading.Thread): 57 """Simple XMLRPC server implementation. 58 59 In theory, Python should provide a sane XMLRPC server implementation as 60 part of its standard library. In practice the provided implementation 61 doesn't handle signals, not even EINTR. As a result, we have this class. 62 63 Usage: 64 65 server = XmlRpcServer(('localhost', 43212)) 66 server.register_delegate(my_delegate_instance) 67 server.run() 68 69 """ 70 71 def __init__(self, host, port): 72 """Construct an XmlRpcServer. 73 74 @param host string hostname to bind to. 75 @param port int port number to bind to. 76 77 """ 78 super(XmlRpcServer, self).__init__() 79 logging.info('Binding server to %s:%d', host, port) 80 self._server = SimpleXMLRPCServer((host, port), allow_none=True) 81 self._server.register_introspection_functions() 82 self._keep_running = True 83 self._delegates = [] 84 # Gracefully shut down on signals. This is how we expect to be shut 85 # down by autotest. 86 signal.signal(signal.SIGTERM, self._handle_signal) 87 signal.signal(signal.SIGINT, self._handle_signal) 88 89 90 def register_delegate(self, delegate): 91 """Register delegate objects with the server. 92 93 The server will automagically look up all methods not prefixed with an 94 underscore and treat them as potential RPC calls. These methods may 95 only take basic Python objects as parameters, as noted by the 96 SimpleXMLRPCServer documentation. The state of the delegate is 97 persisted across calls. 98 99 @param delegate object Python object to be exposed via RPC. 100 101 """ 102 self._server.register_instance(delegate) 103 self._delegates.append(delegate) 104 105 106 def run(self): 107 """Block and handle many XmlRpc requests.""" 108 logging.info('XmlRpcServer starting...') 109 with contextlib2.ExitStack() as stack: 110 for delegate in self._delegates: 111 stack.enter_context(delegate) 112 while self._keep_running: 113 try: 114 self._server.handle_request() 115 except select.error as v: 116 # In a cruel twist of fate, the python library doesn't 117 # handle this kind of error. 118 if v[0] != errno.EINTR: 119 raise 120 except Exception as e: 121 logging.error("Error in handle request: %s" % str(e)) 122 logging.info('XmlRpcServer exited.') 123 124 125 def _handle_signal(self, _signum, _frame): 126 """Handle a process signal by gracefully quitting. 127 128 SimpleXMLRPCServer helpfully exposes a method called shutdown() which 129 clears a flag similar to _keep_running, and then blocks until it sees 130 the server shut down. Unfortunately, if you call that function from 131 a signal handler, the server will just hang, since the process is 132 paused for the signal, causing a deadlock. Thus we are reinventing the 133 wheel with our own event loop. 134 135 """ 136 self._server.server_close() 137 self._keep_running = False 138 139 140class XmlRpcServerError(Exception): 141 """Raised when an error is encountered in the XmlRpcServer.""" 142 143 144class AndroidXmlRpcDelegate(object): 145 """Exposes methods called remotely during WiFi autotests. 146 147 All instance methods of this object without a preceding '_' are exposed via 148 an XMLRPC server. 149 """ 150 151 WEP40_HEX_KEY_LEN = 10 152 WEP104_HEX_KEY_LEN = 26 153 SHILL_DISCONNECTED_STATES = ['idle'] 154 SHILL_CONNECTED_STATES = ['portal', 'online', 'ready'] 155 DISCONNECTED_SSID = '0x' 156 DISCOVERY_POLLING_INTERVAL = 1 157 158 159 def __init__(self, serial_number, log_dir): 160 """Initializes the ACTS library components. 161 162 @param serial_number Serial number of the android device to be tested, 163 None if there is only one device connected to the host. 164 @param log_dir Path to store output logs of this run. 165 166 """ 167 # Cleanup all existing logs for this device when starting. 168 shutil.rmtree(log_dir, ignore_errors=True) 169 logger.setup_test_logger(log_path=log_dir, prefix="ANDROID_XMLRPC") 170 if not serial_number: 171 ads = android_device.get_all_instances() 172 if not ads: 173 msg = "No android device found, abort!" 174 logging.error(msg) 175 raise XmlRpcServerError(msg) 176 self.ad = ads[0] 177 elif serial_number in android_device.list_adb_devices(): 178 self.ad = android_device.AndroidDevice(serial_number) 179 else: 180 msg = ("Specified Android device %s can't be found, abort!" 181 ) % serial_number 182 logging.error(msg) 183 raise XmlRpcServerError(msg) 184 185 186 def __enter__(self): 187 logging.debug('Bringing up AndroidXmlRpcDelegate.') 188 self.ad.get_droid() 189 self.ad.ed.start() 190 self.ad.start_adb_logcat() 191 return self 192 193 194 def __exit__(self, exception, value, traceback): 195 logging.debug('Tearing down AndroidXmlRpcDelegate.') 196 self.ad.terminate_all_sessions() 197 self.ad.stop_adb_logcat() 198 199 200 # Commands start. 201 def ready(self): 202 """Confirm that the XMLRPC server is up and ready to serve. 203 204 @return True (always). 205 206 """ 207 logging.debug('ready()') 208 return True 209 210 211 def collect_debug_info(self, test_name): 212 """Collects appropriate debug information on DUT. 213 214 @param test_name: string name of the test to collect debug information 215 for. 216 """ 217 self.ad.cat_adb_log(test_name, self.test_begin_time) 218 self.ad.take_bug_report(test_name, self.test_begin_time) 219 220 221 def list_controlled_wifi_interfaces(self): 222 return ['wlan0'] 223 224 225 def set_device_enabled(self, wifi_interface, enabled): 226 """Enable or disable the WiFi device. 227 228 @param wifi_interface: string name of interface being modified. 229 @param enabled: boolean; true if this device should be enabled, 230 false if this device should be disabled. 231 @return True if it worked; false, otherwise 232 233 """ 234 return wutils.wifi_toggle_state(self.ad, enabled) 235 236 237 def sync_time_to(self, epoch_seconds): 238 """Sync time on the DUT to |epoch_seconds| from the epoch. 239 240 @param epoch_seconds: float number of seconds from the epoch. 241 242 """ 243 # The adb_host is already doing this; just return True. 244 return True 245 246 247 def clean_profiles(self): 248 return True 249 250 251 def create_profile(self, profile_name): 252 return True 253 254 255 def push_profile(self, profile_name): 256 return True 257 258 259 def remove_profile(self, profile_name): 260 return True 261 262 263 def pop_profile(self, profile_name): 264 return True 265 266 267 def disconnect(self, ssid): 268 """Attempt to disconnect from the given ssid. 269 270 Blocks until disconnected or operation has timed out. Returns True iff 271 disconnect was successful. 272 273 @param ssid string network to disconnect from. 274 @return bool True on success, False otherwise. 275 276 """ 277 # Android had no explicit disconnect, so let's just forget the network. 278 return self.delete_entries_for_ssid(ssid) 279 280 281 def get_active_wifi_SSIDs(self): 282 """Get the list of all SSIDs in the current scan results. 283 284 @return list of string SSIDs with at least one BSS we've scanned. 285 286 """ 287 ssids = [] 288 try: 289 self.ad.droid.wifiStartScan() 290 self.ad.ed.pop_event('WifiManagerScanResultsAvailable') 291 scan_results = self.ad.droid.wifiGetScanResults() 292 for result in scan_results: 293 if wutils.WifiEnums.SSID_KEY in result: 294 ssids.append(result[wutils.WifiEnums.SSID_KEY]) 295 except Queue.Empty: 296 logging.error("Scan results available event timed out!") 297 except Exception as e: 298 logging.error("Scan results error: %s" % str(e)) 299 finally: 300 logging.debug(ssids) 301 return ssids 302 303 304 def wait_for_service_states(self, ssid, states, timeout_seconds): 305 """Wait for SSID to reach one state out of a list of states. 306 307 @param ssid string the network to connect to (e.g. 'GoogleGuest'). 308 @param states tuple the states for which to wait 309 @param timeout_seconds int seconds to wait for a state 310 311 @return (result, final_state, wait_time) tuple of the result for the 312 wait. 313 """ 314 current_con = self.ad.droid.wifiGetConnectionInfo() 315 # Check the current state to see if we're connected/disconnected. 316 if set(states).intersection(set(self.SHILL_CONNECTED_STATES)): 317 if current_con[wutils.WifiEnums.SSID_KEY] == ssid: 318 return True, '', 0 319 wait_event = 'WifiNetworkConnected' 320 elif set(states).intersection(set(self.SHILL_DISCONNECTED_STATES)): 321 if current_con[wutils.WifiEnums.SSID_KEY] == self.DISCONNECTED_SSID: 322 return True, '', 0 323 wait_event = 'WifiNetworkDisconnected' 324 else: 325 assert 0, "Unhandled wait states received: %r" % states 326 final_state = "" 327 wait_time = -1 328 result = False 329 logging.debug(current_con) 330 try: 331 self.ad.droid.wifiStartTrackingStateChange() 332 start_time = utils.get_current_epoch_time() 333 wait_result = self.ad.ed.pop_event(wait_event, timeout_seconds) 334 end_time = utils.get_current_epoch_time() 335 wait_time = (end_time - start_time) / 1000 336 if wait_event == 'WifiNetworkConnected': 337 actual_ssid = wait_result['data'][wutils.WifiEnums.SSID_KEY] 338 assert actual_ssid == ssid, ("Expected to connect to %s, but " 339 "connected to %s") % (ssid, actual_ssid) 340 result = True 341 except Queue.Empty: 342 logging.error("No state change available yet!") 343 except Exception as e: 344 logging.error("State change error: %s" % str(e)) 345 finally: 346 logging.debug((result, final_state, wait_time)) 347 self.ad.droid.wifiStopTrackingStateChange() 348 return result, final_state, wait_time 349 350 351 def delete_entries_for_ssid(self, ssid): 352 """Delete all saved entries for an SSID. 353 354 @param ssid string of SSID for which to delete entries. 355 @return True on success, False otherwise. 356 357 """ 358 try: 359 wutils.wifi_forget_network(self.ad, ssid) 360 except Exception as e: 361 logging.error(str(e)) 362 return False 363 return True 364 365 366 def connect_wifi(self, raw_params): 367 """Block and attempt to connect to wifi network. 368 369 @param raw_params serialized AssociationParameters. 370 @return serialized AssociationResult 371 372 """ 373 # Prepare data objects. 374 params = Map(raw_params) 375 params.security_config = Map(raw_params['security_config']) 376 params.bgscan_config = Map(raw_params['bgscan_config']) 377 logging.debug('connect_wifi(). Params: %r' % params) 378 network_config = { 379 "SSID": params.ssid, 380 "hiddenSSID": True if params.is_hidden else False 381 } 382 assoc_result = { 383 "discovery_time" : 0, 384 "association_time" : 0, 385 "configuration_time" : 0, 386 "failure_reason" : "Oops!", 387 "xmlrpc_struct_type_key" : "AssociationResult" 388 } 389 duration = lambda: (utils.get_current_epoch_time() - start_time) / 1000 390 try: 391 # Verify that the network was found, if the SSID is not hidden. 392 if not params.is_hidden: 393 start_time = utils.get_current_epoch_time() 394 found = False 395 while duration() < params.discovery_timeout and not found: 396 active_ssids = self.get_active_wifi_SSIDs() 397 found = params.ssid in active_ssids 398 if not found: 399 time.sleep(self.DISCOVERY_POLLING_INTERVAL) 400 assoc_result["discovery_time"] = duration() 401 assert found, ("Could not find %s in scan results: %r") % ( 402 params.ssid, active_ssids) 403 result = False 404 if params.security_config.security == "psk": 405 network_config["password"] = params.security_config.psk 406 elif params.security_config.security == "wep": 407 network_config["wepTxKeyIndex"] = params.security_config.wep_default_key 408 # Convert all ASCII keys to Hex 409 wep_hex_keys = [] 410 for key in params.security_config.wep_keys: 411 if len(key) == self.WEP40_HEX_KEY_LEN or \ 412 len(key) == self.WEP104_HEX_KEY_LEN: 413 wep_hex_keys.append(key) 414 else: 415 hex_key = "" 416 for byte in bytearray(key, 'utf-8'): 417 hex_key += '%x' % byte 418 wep_hex_keys.append(hex_key) 419 network_config["wepKeys"] = wep_hex_keys 420 # Associate to the network. 421 self.ad.droid.wifiStartTrackingStateChange() 422 start_time = utils.get_current_epoch_time() 423 result = self.ad.droid.wifiConnect(network_config) 424 assert result, "wifiConnect call failed." 425 # Verify connection successful and correct. 426 logging.debug('wifiConnect result: %s. Waiting for connection' % result); 427 timeout = params.association_timeout + params.configuration_timeout 428 connect_result = self.ad.ed.pop_event( 429 wutils.WifiEventNames.WIFI_CONNECTED, timeout) 430 assoc_result["association_time"] = duration() 431 actual_ssid = connect_result['data'][wutils.WifiEnums.SSID_KEY] 432 logging.debug('Connected to SSID: %s' % params.ssid); 433 assert actual_ssid == params.ssid, ("Expected to connect to %s, " 434 "connected to %s") % (params.ssid, actual_ssid) 435 result = True 436 except Queue.Empty: 437 msg = "Failed to connect to %s with %s" % (params.ssid, 438 params.security_config.security) 439 logging.error(msg) 440 assoc_result["failure_reason"] = msg 441 result = False 442 except Exception as e: 443 msg = str(e) 444 logging.error(msg) 445 assoc_result["failure_reason"] = msg 446 result = False 447 finally: 448 assoc_result["success"] = result 449 logging.debug(assoc_result) 450 self.ad.droid.wifiStopTrackingStateChange() 451 return assoc_result 452 453 454 def init_test_network_state(self): 455 """Create a clean slate for tests with respect to remembered networks. 456 457 @return True iff operation succeeded, False otherwise. 458 """ 459 self.test_begin_time = logger.get_log_line_timestamp() 460 try: 461 wutils.wifi_test_device_init(self.ad) 462 except AssertionError as e: 463 logging.error(str(e)) 464 return False 465 return True 466 467 468if __name__ == '__main__': 469 parser = argparse.ArgumentParser(description='Cros Wifi Xml RPC server.') 470 parser.add_argument('-s', '--serial-number', action='store', default=None, 471 help='Serial Number of the device to test.') 472 parser.add_argument('-l', '--log-dir', action='store', default=None, 473 help='Path to store output logs.') 474 args = parser.parse_args() 475 logging.basicConfig(level=logging.DEBUG) 476 logging.debug("android_xmlrpc_server main...") 477 server = XmlRpcServer('localhost', 9989) 478 server.register_delegate( 479 AndroidXmlRpcDelegate(args.serial_number, args.log_dir)) 480 server.run() 481