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