1# Copyright 2014 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import httplib 6import logging 7import os 8import re 9import socket 10import xmlrpclib 11import pprint 12import sys 13 14from autotest_lib.client.bin import utils 15from autotest_lib.client.common_lib import logging_manager 16from autotest_lib.client.common_lib import error 17from autotest_lib.client.common_lib.cros import retry 18from autotest_lib.client.cros import constants 19from autotest_lib.server import autotest 20from autotest_lib.server.cros.multimedia import audio_facade_adapter 21from autotest_lib.server.cros.multimedia import bluetooth_hid_facade_adapter 22from autotest_lib.server.cros.multimedia import browser_facade_adapter 23from autotest_lib.server.cros.multimedia import cfm_facade_adapter 24from autotest_lib.server.cros.multimedia import display_facade_adapter 25from autotest_lib.server.cros.multimedia import input_facade_adapter 26from autotest_lib.server.cros.multimedia import kiosk_facade_adapter 27from autotest_lib.server.cros.multimedia import system_facade_adapter 28from autotest_lib.server.cros.multimedia import usb_facade_adapter 29from autotest_lib.server.cros.multimedia import video_facade_adapter 30 31 32# Log the client messages in the DEBUG level, with the prefix [client]. 33CLIENT_LOG_STREAM = logging_manager.LoggingFile( 34 level=logging.DEBUG, 35 prefix='[client] ') 36 37 38class _Method: 39 """Class to save the name of the RPC method instead of the real object. 40 41 It keeps the name of the RPC method locally first such that the RPC method 42 can be evalulated to a real object while it is called. Its purpose is to 43 refer to the latest RPC proxy as the original previous-saved RPC proxy may 44 be lost due to reboot. 45 46 The call_method is the method which does refer to the latest RPC proxy. 47 """ 48 49 def __init__(self, call_method, name): 50 self.__call_method = call_method 51 self.__name = name 52 53 54 def __getattr__(self, name): 55 # Support a nested method. 56 return _Method(self.__call_method, "%s.%s" % (self.__name, name)) 57 58 59 def __call__(self, *args, **dargs): 60 return self.__call_method(self.__name, *args, **dargs) 61 62 63class RemoteFacadeProxy(object): 64 """An abstraction of XML RPC proxy to the DUT multimedia server. 65 66 The traditional XML RPC server proxy is static. It is lost when DUT 67 reboots. This class reconnects the server again when it finds the 68 connection is lost. 69 70 """ 71 72 XMLRPC_CONNECT_TIMEOUT = 90 73 XMLRPC_RETRY_TIMEOUT = 180 74 XMLRPC_RETRY_DELAY = 10 75 REBOOT_TIMEOUT = 60 76 77 def __init__(self, host, no_chrome, extra_browser_args=None): 78 """Construct a RemoteFacadeProxy. 79 80 @param host: Host object representing a remote host. 81 @param no_chrome: Don't start Chrome by default. 82 @param extra_browser_args: A list containing extra browser args passed 83 to Chrome in addition to default ones. 84 85 """ 86 self._client = host 87 self._xmlrpc_proxy = None 88 self._log_saving_job = None 89 self._no_chrome = no_chrome 90 self._extra_browser_args = extra_browser_args 91 self.connect() 92 if not no_chrome: 93 self._start_chrome(reconnect=False, retry=True, 94 extra_browser_args=self._extra_browser_args) 95 96 97 def __getattr__(self, name): 98 """Return a _Method object only, not its real object.""" 99 return _Method(self.__call_proxy, name) 100 101 102 def __call_proxy(self, name, *args, **dargs): 103 """Make the call on the latest RPC proxy object. 104 105 This method gets the internal method of the RPC proxy and calls it. 106 107 @param name: Name of the RPC method, a nested method supported. 108 @param args: The rest of arguments. 109 @param dargs: The rest of dict-type arguments. 110 @return: The return value of the RPC method. 111 """ 112 def process_log(): 113 """Process the log from client, i.e. showing the log messages.""" 114 if self._log_saving_job: 115 # final_read=True to process all data until the end 116 self._log_saving_job.process_output( 117 stdout=True, final_read=True) 118 self._log_saving_job.process_output( 119 stdout=False, final_read=True) 120 121 def parse_exception(message): 122 """Parse the given message and extract the exception line. 123 124 @return: A tuple of (keyword, reason); or None if not found. 125 """ 126 EXCEPTION_PATTERN = r'(\w+): (.+)' 127 # Search the line containing the exception keyword, like: 128 # "TestFail: Not able to start session." 129 for line in reversed(message.split('\n')): 130 m = re.match(EXCEPTION_PATTERN, line) 131 if m: 132 return (m.group(1), m.group(2)) 133 return None 134 135 def call_rpc_with_log(): 136 """Call the RPC with log.""" 137 value = getattr(self._xmlrpc_proxy, name)(*args, **dargs) 138 process_log() 139 140 # For debug, print the return value. 141 logging.debug('RPC %s returns %s.', rpc, pprint.pformat(value)) 142 143 # Raise some well-known client exceptions, like TestFail. 144 if type(value) is str and value.startswith('Traceback'): 145 exception_tuple = parse_exception(value) 146 if exception_tuple: 147 keyword, reason = exception_tuple 148 reason = reason + ' (RPC: %s)' % name 149 if keyword == 'TestFail': 150 raise error.TestFail(reason) 151 elif keyword == 'TestError': 152 raise error.TestError(reason) 153 154 # Raise the exception with the original exception keyword. 155 raise Exception('%s: %s' % (keyword, reason)) 156 157 # Raise the default exception with the original message. 158 raise Exception('Exception from client (RPC: %s)\n%s' % 159 (name, value)) 160 161 return value 162 163 try: 164 # TODO(ihf): This logs all traffic from server to client. Make 165 # the spew optional. 166 rpc = ( 167 '%s(%s, %s)' % 168 (pprint.pformat(name), pprint.pformat(args), 169 pprint.pformat(dargs))) 170 try: 171 return call_rpc_with_log() 172 except (socket.error, 173 xmlrpclib.ProtocolError, 174 httplib.BadStatusLine): 175 # Reconnect the RPC server in case connection lost, e.g. reboot. 176 self.connect() 177 if not self._no_chrome: 178 self._start_chrome( 179 reconnect=True, retry=False, 180 extra_browser_args=self._extra_browser_args) 181 # Try again. 182 logging.warning('Retrying RPC %s.', rpc) 183 return call_rpc_with_log() 184 except: 185 # Process the log if any. It is helpful for debug. 186 process_log() 187 logging.error( 188 'Failed RPC %s with status [%s].', rpc, sys.exc_info()[0]) 189 raise 190 191 192 def save_log_bg(self): 193 """Save the log from client in background.""" 194 # Run a tail command in background that keeps all the log messages from 195 # client. 196 command = 'tail -n0 -f %s' % constants.MULTIMEDIA_XMLRPC_SERVER_LOG_FILE 197 full_command = '%s "%s"' % (self._client.ssh_command(), command) 198 199 if self._log_saving_job: 200 # Kill and join the previous job, probably due to a DUT reboot. 201 # In this case, a new job will be recreated. 202 logging.info('Kill and join the previous log job.') 203 utils.nuke_subprocess(self._log_saving_job.sp) 204 utils.join_bg_jobs([self._log_saving_job]) 205 206 # Create the background job and pipe its stdout and stderr to the 207 # Autotest logging. 208 self._log_saving_job = utils.BgJob(full_command, 209 stdout_tee=CLIENT_LOG_STREAM, 210 stderr_tee=CLIENT_LOG_STREAM) 211 212 213 def connect(self): 214 """Connects the XML-RPC proxy on the client. 215 216 @return: True on success. Note that if autotest server fails to 217 connect to XMLRPC server on Cros host after timeout, 218 error.TimeoutException will be raised by retry.retry 219 decorator. 220 221 """ 222 @retry.retry((socket.error, 223 xmlrpclib.ProtocolError, 224 httplib.BadStatusLine), 225 timeout_min=self.XMLRPC_RETRY_TIMEOUT / 60.0, 226 delay_sec=self.XMLRPC_RETRY_DELAY) 227 def connect_with_retries(): 228 """Connects the XML-RPC proxy with retries.""" 229 self._xmlrpc_proxy = self._client.rpc_server_tracker.xmlrpc_connect( 230 constants.MULTIMEDIA_XMLRPC_SERVER_COMMAND, 231 constants.MULTIMEDIA_XMLRPC_SERVER_PORT, 232 command_name=( 233 constants.MULTIMEDIA_XMLRPC_SERVER_CLEANUP_PATTERN 234 ), 235 ready_test_name=( 236 constants.MULTIMEDIA_XMLRPC_SERVER_READY_METHOD), 237 timeout_seconds=self.XMLRPC_CONNECT_TIMEOUT, 238 logfile=constants.MULTIMEDIA_XMLRPC_SERVER_LOG_FILE, 239 request_timeout_seconds= 240 constants.MULTIMEDIA_XMLRPC_SERVER_REQUEST_TIMEOUT) 241 242 logging.info('Setup the connection to RPC server, with retries...') 243 connect_with_retries() 244 245 logging.info('Start a job to save the log from the client.') 246 self.save_log_bg() 247 248 return True 249 250 251 def _start_chrome(self, reconnect, retry=False, extra_browser_args=None): 252 """Starts Chrome using browser facade on Cros host. 253 254 @param reconnect: True for reconnection, False for the first-time. 255 @param retry: True to retry using a reboot on host. 256 @param extra_browser_args: A list containing extra browser args passed 257 to Chrome in addition to default ones. 258 259 @raise: error.TestError: if fail to start Chrome after retry. 260 261 """ 262 logging.info( 263 'Start Chrome with default arguments and extra browser args %s...', 264 extra_browser_args) 265 success = self._xmlrpc_proxy.browser.start_default_chrome( 266 reconnect, extra_browser_args) 267 if not success and retry: 268 logging.warning('Can not start Chrome. Reboot host and try again') 269 # Reboot host and try again. 270 self._client.reboot() 271 # Wait until XMLRPC server can be reconnected. 272 utils.poll_for_condition(condition=self.connect, 273 timeout=self.REBOOT_TIMEOUT) 274 logging.info( 275 'Retry starting Chrome with default arguments and ' 276 'extra browser args %s...', extra_browser_args) 277 success = self._xmlrpc_proxy.browser.start_default_chrome( 278 reconnect, extra_browser_args) 279 280 if not success: 281 raise error.TestError( 282 'Failed to start Chrome on DUT. ' 283 'Check multimedia_xmlrpc_server.log in result folder.') 284 285 286 def __del__(self): 287 """Destructor of RemoteFacadeFactory.""" 288 self._client.rpc_server_tracker.disconnect( 289 constants.MULTIMEDIA_XMLRPC_SERVER_PORT) 290 291 292class RemoteFacadeFactory(object): 293 """A factory to generate remote multimedia facades. 294 295 The facade objects are remote-wrappers to access the DUT multimedia 296 functionality, like display, video, and audio. 297 298 """ 299 300 def __init__(self, host, no_chrome=False, install_autotest=True, 301 results_dir=None, extra_browser_args=None): 302 """Construct a RemoteFacadeFactory. 303 304 @param host: Host object representing a remote host. 305 @param no_chrome: Don't start Chrome by default. 306 @param install_autotest: Install autotest on host. 307 @param results_dir: A directory to store multimedia server init log. 308 @param extra_browser_args: A list containing extra browser args passed 309 to Chrome in addition to default ones. 310 If it is not None, we will get multimedia init log to the results_dir. 311 312 """ 313 self._client = host 314 if install_autotest: 315 # Make sure the client library is on the device so that 316 # the proxy code is there when we try to call it. 317 client_at = autotest.Autotest(self._client) 318 client_at.install() 319 try: 320 self._proxy = RemoteFacadeProxy( 321 host=self._client, 322 no_chrome=no_chrome, 323 extra_browser_args=extra_browser_args) 324 finally: 325 if results_dir: 326 host.get_file(constants.MULTIMEDIA_XMLRPC_SERVER_LOG_FILE, 327 os.path.join(results_dir, 328 'multimedia_xmlrpc_server.log.init')) 329 330 331 def ready(self): 332 """Returns the proxy ready status""" 333 return self._proxy.ready() 334 335 336 def create_audio_facade(self): 337 """Creates an audio facade object.""" 338 return audio_facade_adapter.AudioFacadeRemoteAdapter( 339 self._client, self._proxy) 340 341 342 def create_video_facade(self): 343 """Creates a video facade object.""" 344 return video_facade_adapter.VideoFacadeRemoteAdapter( 345 self._client, self._proxy) 346 347 348 def create_display_facade(self): 349 """Creates a display facade object.""" 350 return display_facade_adapter.DisplayFacadeRemoteAdapter( 351 self._client, self._proxy) 352 353 354 def create_system_facade(self): 355 """Creates a system facade object.""" 356 return system_facade_adapter.SystemFacadeRemoteAdapter( 357 self._client, self._proxy) 358 359 360 def create_usb_facade(self): 361 """"Creates a USB facade object.""" 362 return usb_facade_adapter.USBFacadeRemoteAdapter(self._proxy) 363 364 365 def create_browser_facade(self): 366 """"Creates a browser facade object.""" 367 return browser_facade_adapter.BrowserFacadeRemoteAdapter(self._proxy) 368 369 370 def create_bluetooth_hid_facade(self): 371 """"Creates a bluetooth hid facade object.""" 372 return bluetooth_hid_facade_adapter.BluetoothHIDFacadeRemoteAdapter( 373 self._client, self._proxy) 374 375 376 def create_input_facade(self): 377 """"Creates an input facade object.""" 378 return input_facade_adapter.InputFacadeRemoteAdapter(self._proxy) 379 380 381 def create_cfm_facade(self): 382 """"Creates a cfm facade object.""" 383 return cfm_facade_adapter.CFMFacadeRemoteAdapter( 384 self._client, self._proxy) 385 386 387 def create_kiosk_facade(self): 388 """"Creates a kiosk facade object.""" 389 return kiosk_facade_adapter.KioskFacadeRemoteAdapter( 390 self._client, self._proxy) 391