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