xref: /aosp_15_r20/external/autotest/server/cros/multimedia/remote_facade_factory.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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