xref: /aosp_15_r20/external/autotest/client/cros/xmlrpc_server.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
6# contextlib.nested is deprecated and removed in python3
7# Since the apis are quite different, keep track of whether to use nested or not
8# based on the availability of contextlib.nested and take different code paths.
9try:
10    from contextlib import nested
11    use_nested = True
12except ImportError:
13    import contextlib
14    use_nested = False
15
16import dbus
17import errno
18import functools
19import logging
20import os
21import select
22import signal
23import six.moves.xmlrpc_server
24import threading
25
26
27def terminate_old(script_name, sigterm_timeout=5, sigkill_timeout=3):
28    """
29    Avoid "address already in use" errors by killing any leftover RPC server
30    processes, possibly from previous runs.
31
32    A process is a match if it's Python and has the given script in the command
33    line.  This should avoid including processes such as editors and 'tail' of
34    logs, which might match a simple pkill.
35
36    exe=/usr/local/bin/python3
37    cmdline=['/usr/bin/python3', '-u', '/usr/local/autotest/.../rpc_server.py']
38
39    @param script_name: The filename of the main script, used to match processes
40    @param sigterm_timeout: Wait N seconds after SIGTERM before trying SIGKILL.
41    @param sigkill_timeout: Wait N seconds after SIGKILL before complaining.
42    """
43    # import late, to avoid affecting servers that don't call the method
44    import psutil
45
46    script_name_abs = os.path.abspath(script_name)
47    script_name_base = os.path.basename(script_name)
48    me = psutil.Process()
49
50    logging.debug('This process:  %s: %s, %s', me, me.exe(), me.cmdline())
51    logging.debug('Checking for leftover processes...')
52
53    running = []
54    for proc in psutil.process_iter(attrs=['name', 'exe', 'cmdline']):
55        if proc == me:
56            continue
57        try:
58            name = proc.name()
59            if not name or 'py' not in name:
60                continue
61            exe = proc.exe()
62            args = proc.cmdline()
63            # Note: If we ever need multiple instances on different ports,
64            # add a check for listener ports, likely via proc.connections()
65            if '/python' in exe and (script_name in args
66                                     or script_name_abs in args
67                                     or script_name_base in args):
68                logging.debug('Found process: %s: %s', proc, args)
69                running.append(proc)
70        except psutil.Error as e:
71            logging.debug('%s: %s', e, proc)
72            continue
73
74    if not running:
75        return
76
77    logging.info('Trying SIGTERM: pids=%s', [p.pid for p in running])
78    for proc in running:
79        try:
80            proc.send_signal(0)
81            proc.terminate()
82        except psutil.NoSuchProcess as e:
83            logging.debug('%s: %s', e, proc)
84        except psutil.Error as e:
85            logging.warning('%s: %s', e, proc)
86
87    (terminated, running) = psutil.wait_procs(running, sigterm_timeout)
88    if not running:
89        return
90
91    running.sort(key=lambda p: p.pid)
92    logging.info('Trying SIGKILL: pids=%s', [p.pid for p in running])
93    for proc in running:
94        try:
95            proc.kill()
96        except psutil.NoSuchProcess as e:
97            logging.debug('%s: %s', e, proc)
98        except psutil.Error as e:
99            logging.warning('%s: %s', e, proc)
100
101    (sigkilled, running) = psutil.wait_procs(running, sigkill_timeout)
102    if running:
103        running.sort(key=lambda p: p.pid)
104        logging.warning('Found leftover processes %s; address may be in use!',
105                     [p.pid for p in running])
106    else:
107        logging.debug('Leftover processes have exited.')
108
109
110class XmlRpcServer(threading.Thread):
111    """Simple XMLRPC server implementation.
112
113    In theory, Python should provide a valid XMLRPC server implementation as
114    part of its standard library.  In practice the provided implementation
115    doesn't handle signals, not even EINTR.  As a result, we have this class.
116
117    Usage:
118
119    server = XmlRpcServer(('localhost', 43212))
120    server.register_delegate(my_delegate_instance)
121    server.run()
122
123    """
124
125    def __init__(self, host, port):
126        """Construct an XmlRpcServer.
127
128        @param host string hostname to bind to.
129        @param port int port number to bind to.
130
131        """
132        super(XmlRpcServer, self).__init__()
133        logging.info('Binding server to %s:%d', host, port)
134        self._server = six.moves.xmlrpc_server.SimpleXMLRPCServer(
135                (host, port), allow_none=True)
136        self._server.register_introspection_functions()
137        # After python 2.7.10, BaseServer.handle_request automatically retries
138        # on EINTR, so handle_request will be blocked at select.select forever
139        # if timeout is None. Set a timeout so server can be shut down
140        # gracefully. Check issue crbug.com/571737 and
141        # https://bugs.python.org/issue7978 for the explanation.
142        self._server.timeout = 0.5
143        self._keep_running = True
144        self._delegates = []
145        # Gracefully shut down on signals.  This is how we expect to be shut
146        # down by autotest.
147        signal.signal(signal.SIGTERM, self._handle_signal)
148        signal.signal(signal.SIGINT, self._handle_signal)
149
150
151    def register_delegate(self, delegate):
152        """Register delegate objects with the server.
153
154        The server will automagically look up all methods not prefixed with an
155        underscore and treat them as potential RPC calls.  These methods may
156        only take basic Python objects as parameters, as noted by the
157        SimpleXMLRPCServer documentation.  The state of the delegate is
158        persisted across calls.
159
160        @param delegate object Python object to be exposed via RPC.
161
162        """
163        self._server.register_instance(delegate)
164        self._delegates.append(delegate)
165
166    def run(self):
167        """Block and handle many XmlRpc requests."""
168        logging.info('XmlRpcServer starting...')
169
170        def stack_inner():
171            """Handle requests to server until asked to stop running."""
172            while self._keep_running:
173                try:
174                    self._server.handle_request()
175                except select.error as v:
176                    # In a cruel twist of fate, the python library doesn't
177                    # handle this kind of error.
178                    if v[0] != errno.EINTR:
179                        raise
180
181        if use_nested:
182            with nested(*self._delegates):
183                stack_inner()
184        else:
185            with contextlib.ExitStack() as stack:
186                delegates = [stack.enter_context(d) for d in self._delegates]
187                stack_inner()
188
189        for delegate in self._delegates:
190            if hasattr(delegate, 'cleanup'):
191                delegate.cleanup()
192
193        logging.info('XmlRpcServer exited.')
194
195
196    def _handle_signal(self, _signum, _frame):
197        """Handle a process signal by gracefully quitting.
198
199        SimpleXMLRPCServer helpfully exposes a method called shutdown() which
200        clears a flag similar to _keep_running, and then blocks until it sees
201        the server shut down.  Unfortunately, if you call that function from
202        a signal handler, the server will just hang, since the process is
203        paused for the signal, causing a deadlock.  Thus we are reinventing the
204        wheel with our own event loop.
205
206        """
207        self._keep_running = False
208
209
210def dbus_safe(default_return_value):
211    """Catch all DBus exceptions and return a default value instead.
212
213    Wrap a function with a try block that catches DBus exceptions and
214    returns default values instead.  This is convenient for simple error
215    handling since XMLRPC doesn't understand DBus exceptions.
216
217    @param wrapped_function function to wrap.
218    @param default_return_value value to return on exception (usually False).
219
220    """
221    def decorator(wrapped_function):
222        """Call a function and catch DBus errors.
223
224        @param wrapped_function function to call in dbus safe context.
225        @return function return value or default_return_value on failure.
226
227        """
228        @functools.wraps(wrapped_function)
229        def wrapper(*args, **kwargs):
230            """Pass args and kwargs to a dbus safe function.
231
232            @param args formal python arguments.
233            @param kwargs keyword python arguments.
234            @return function return value or default_return_value on failure.
235
236            """
237            logging.debug('%s()', wrapped_function.__name__)
238            try:
239                return wrapped_function(*args, **kwargs)
240
241            except dbus.exceptions.DBusException as e:
242                logging.error('Exception while performing operation %s: %s: %s',
243                              wrapped_function.__name__,
244                              e.get_dbus_name(),
245                              e.get_dbus_message())
246                return default_return_value
247
248        return wrapper
249
250    return decorator
251
252
253class XmlRpcDelegate(object):
254    """A super class for XmlRPC delegates used with XmlRpcServer.
255
256    This doesn't add much helpful functionality except to implement the trivial
257    status check method expected by autotest's host.xmlrpc_connect() method.
258    Subclass this class to add more functionality.
259
260    """
261
262
263    def __enter__(self):
264        logging.debug('Bringing up XmlRpcDelegate: %r.', self)
265        pass
266
267
268    def __exit__(self, exception, value, traceback):
269        logging.debug('Tearing down XmlRpcDelegate: %r.', self)
270        pass
271
272
273    def ready(self):
274        """Confirm that the XMLRPC server is up and ready to serve.
275
276        @return True (always).
277
278        """
279        logging.debug('ready()')
280        return True
281