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