xref: /aosp_15_r20/external/autotest/server/cros/faft/rpc_proxy.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright (c) 2013 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
7from six.moves import http_client as httplib
8import socket
9import time
10from six.moves import xmlrpc_client as xmlrpclib
11
12from autotest_lib.client.cros.faft.config import Config as ClientConfig
13from autotest_lib.server import autotest
14
15
16class _Method(object):
17    """Class to save the name of the RPC method instead of the real object.
18
19    It keeps the name of the RPC method locally first such that the RPC method
20    can be evalulated to a real object while it is called. Its purpose is to
21    refer to the latest RPC proxy as the original previous-saved RPC proxy may
22    be lost due to reboot.
23
24    The call_method is the method which does refer to the latest RPC proxy.
25    """
26    def __init__(self, call_method, name):
27        self.__call_method = call_method
28        self.__name = name
29
30    def __getattr__(self, name):
31        # Support a nested method.
32        return _Method(self.__call_method, "%s.%s" % (self.__name, name))
33
34    def __call__(self, *args, **dargs):
35        return self.__call_method(self.__name, *args, **dargs)
36
37    def __repr__(self):
38        """Return a description of the method object"""
39        return "%s('%s')" % (self.__class__.__name__, self.__name)
40
41    def __str__(self):
42        """Return a description of the method object"""
43        return "<%s '%s'>" % (self.__class__.__name__, self.__name)
44
45
46class RPCProxy(object):
47    """Proxy to the FAFTClient RPC server on DUT.
48
49    It acts as a proxy to the FAFTClient on DUT. It is smart enough to:
50     - postpone the RPC connection to the first class method call;
51     - reconnect to the RPC server in case connection lost, e.g. reboot;
52     - always call the latest RPC proxy object.
53
54     @ivar _client: the ssh host object
55     @type host: autotest_lib.server.hosts.abstract_ssh.AbstractSSHHost
56     @ivar _faft_client: the real serverproxy to use for calls
57     @type _faft_client: xmlrpclib.ServerProxy | None
58    """
59    _client_config = ClientConfig()
60
61    def __init__(self, host):
62        """Constructor.
63
64        @param host: The host object, passed via the test control file.
65        """
66        self.host = host
67        self._faft_client = None
68        self.logfiles = []
69
70    def __del__(self):
71        self.disconnect()
72
73    def __getattr__(self, name):
74        """Return a _Method object only, not its real object."""
75        return _Method(self.__call_faft_client, name)
76
77    def __call_faft_client(self, name, *args, **dargs):
78        """Make the call on the latest RPC proxy object.
79
80        This method gets the internal method of the RPC proxy and calls it.
81
82        @param name: Name of the RPC method, a nested method supported.
83        @param args: The rest of arguments.
84        @param dargs: The rest of dict-type arguments.
85        @return: The return value of the FAFTClient RPC method.
86        """
87        if self._faft_client is None:
88            self.connect()
89        try:
90            return getattr(self._faft_client, name)(*args, **dargs)
91        except (socket.error,
92                httplib.BadStatusLine,
93                xmlrpclib.ProtocolError):
94            # Reconnect the RPC server in case connection lost, e.g. reboot.
95            self.connect()
96            # Try again.
97            return getattr(self._faft_client, name)(*args, **dargs)
98
99    def connect(self):
100        """Connect the RPC server."""
101        # Make sure Autotest dependency is there.
102        autotest.Autotest(self.host).install()
103        logfile = "%s.%s" % (self._client_config.rpc_logfile, time.time())
104        self.logfiles.append(logfile)
105        self._faft_client = self.host.rpc_server_tracker.xmlrpc_connect(
106                self._client_config.rpc_command,
107                self._client_config.rpc_port,
108                command_name=self._client_config.rpc_command_short,
109                ready_test_name=self._client_config.rpc_ready_call,
110                timeout_seconds=self._client_config.rpc_timeout,
111                logfile=logfile,
112                server_desc=str(self),
113                request_timeout_seconds=self._client_config.
114                rpc_request_timeout,
115        )
116
117    def disconnect(self):
118        """Disconnect the RPC server."""
119        # The next start of the RPC server will terminate any leftovers,
120        # so no need to pkill upon disconnect.
121        if self._faft_client is not None:
122            logging.debug("Closing FAFT RPC server connection.")
123        self.host.rpc_server_tracker.disconnect(self._client_config.rpc_port,
124                                                pkill=False)
125        self._faft_client = None
126
127    def quit(self):
128        """Tell the RPC server to quit, then disconnect from it."""
129        if self._faft_client is None:
130            return
131        logging.debug("Telling FAFT RPC server to quit.")
132        try:
133            remote_quit = getattr(
134                    self._faft_client, self._client_config.rpc_quit_call)
135            remote_quit()
136            need_pkill = False
137        except Exception as e:
138            logging.warning("Error while telling FAFT RPC server to quit: %s", e)
139            # If we failed to tell the RPC server to quit for some reason,
140            # fall back to SIGTERM, because it may not have exited.
141            need_pkill = True
142
143        self.host.rpc_server_tracker.disconnect(self._client_config.rpc_port,
144                                                pkill=need_pkill)
145        self._faft_client = None
146
147    def collect_logfiles(self, dest):
148        """Download all logfiles from the DUT, then delete them."""
149        if self.logfiles:
150            for logfile in self.logfiles:
151                if self.host.run("test -f", args=[logfile],
152                                 ignore_status=True).exit_status == 0:
153                    self.host.get_file(logfile, dest)
154                    self.host.run("rm -f", ignore_status=True, args=[logfile])
155            self.logfiles.clear()
156
157    def __repr__(self):
158        """Return a description of the proxy object"""
159        return '%s(%s)' % (self.__class__.__name__, self.host)
160
161    def __str__(self):
162        """Return a description of the proxy object"""
163        return "<%s '%s:%s'>" % (self.__class__.__name__, self.host.hostname,
164                                 self._client_config.rpc_port)
165