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