1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import contextlib
6import dbus
7import errno
8import functools
9import logging
10import select
11import signal
12import threading
13import SimpleXMLRPCServer
14
15
16class XmlRpcServer(threading.Thread):
17    """Simple XMLRPC server implementation.
18
19    In theory, Python should provide a sane XMLRPC server implementation as
20    part of its standard library.  In practice the provided implementation
21    doesn't handle signals, not even EINTR.  As a result, we have this class.
22
23    Usage:
24
25    server = XmlRpcServer(('localhost', 43212))
26    server.register_delegate(my_delegate_instance)
27    server.run()
28
29    """
30
31    def __init__(self, host, port):
32        """Construct an XmlRpcServer.
33
34        @param host string hostname to bind to.
35        @param port int port number to bind to.
36
37        """
38        super(XmlRpcServer, self).__init__()
39        logging.info('Binding server to %s:%d', host, port)
40        self._server = SimpleXMLRPCServer.SimpleXMLRPCServer((host, port),
41                                                             allow_none=True)
42        self._server.register_introspection_functions()
43        # After python 2.7.10, BaseServer.handle_request automatically retries
44        # on EINTR, so handle_request will be blocked at select.select forever
45        # if timeout is None. Set a timeout so server can be shut down
46        # gracefully. Check issue crbug.com/571737 and
47        # https://bugs.python.org/issue7978 for the explanation.
48        self._server.timeout = 0.5
49        self._keep_running = True
50        self._delegates = []
51        # Gracefully shut down on signals.  This is how we expect to be shut
52        # down by autotest.
53        signal.signal(signal.SIGTERM, self._handle_signal)
54        signal.signal(signal.SIGINT, self._handle_signal)
55
56
57    def register_delegate(self, delegate):
58        """Register delegate objects with the server.
59
60        The server will automagically look up all methods not prefixed with an
61        underscore and treat them as potential RPC calls.  These methods may
62        only take basic Python objects as parameters, as noted by the
63        SimpleXMLRPCServer documentation.  The state of the delegate is
64        persisted across calls.
65
66        @param delegate object Python object to be exposed via RPC.
67
68        """
69        self._server.register_instance(delegate)
70        self._delegates.append(delegate)
71
72
73    def run(self):
74        """Block and handle many XmlRpc requests."""
75        logging.info('XmlRpcServer starting...')
76        # TODO(wiley) nested is deprecated, but we can't use the replacement
77        #       until we move to Python 3.0.
78        with contextlib.nested(*self._delegates):
79            while self._keep_running:
80                try:
81                    self._server.handle_request()
82                except select.error as v:
83                    # In a cruel twist of fate, the python library doesn't
84                    # handle this kind of error.
85                    if v[0] != errno.EINTR:
86                        raise
87        logging.info('XmlRpcServer exited.')
88
89
90    def _handle_signal(self, _signum, _frame):
91        """Handle a process signal by gracefully quitting.
92
93        SimpleXMLRPCServer helpfully exposes a method called shutdown() which
94        clears a flag similar to _keep_running, and then blocks until it sees
95        the server shut down.  Unfortunately, if you call that function from
96        a signal handler, the server will just hang, since the process is
97        paused for the signal, causing a deadlock.  Thus we are reinventing the
98        wheel with our own event loop.
99
100        """
101        self._keep_running = False
102
103
104def dbus_safe(default_return_value):
105    """Catch all DBus exceptions and return a default value instead.
106
107    Wrap a function with a try block that catches DBus exceptions and
108    returns default values instead.  This is convenient for simple error
109    handling since XMLRPC doesn't understand DBus exceptions.
110
111    @param wrapped_function function to wrap.
112    @param default_return_value value to return on exception (usually False).
113
114    """
115    def decorator(wrapped_function):
116        """Call a function and catch DBus errors.
117
118        @param wrapped_function function to call in dbus safe context.
119        @return function return value or default_return_value on failure.
120
121        """
122        @functools.wraps(wrapped_function)
123        def wrapper(*args, **kwargs):
124            """Pass args and kwargs to a dbus safe function.
125
126            @param args formal python arguments.
127            @param kwargs keyword python arguments.
128            @return function return value or default_return_value on failure.
129
130            """
131            logging.debug('%s()', wrapped_function.__name__)
132            try:
133                return wrapped_function(*args, **kwargs)
134
135            except dbus.exceptions.DBusException as e:
136                logging.error('Exception while performing operation %s: %s: %s',
137                              wrapped_function.__name__,
138                              e.get_dbus_name(),
139                              e.get_dbus_message())
140                return default_return_value
141
142        return wrapper
143
144    return decorator
145
146
147class XmlRpcDelegate(object):
148    """A super class for XmlRPC delegates used with XmlRpcServer.
149
150    This doesn't add much helpful functionality except to implement the trivial
151    status check method expected by autotest's host.xmlrpc_connect() method.
152    Subclass this class to add more functionality.
153
154    """
155
156
157    def __enter__(self):
158        logging.debug('Bringing up XmlRpcDelegate: %r.', self)
159        pass
160
161
162    def __exit__(self, exception, value, traceback):
163        logging.debug('Tearing down XmlRpcDelegate: %r.', self)
164        pass
165
166
167    def ready(self):
168        """Confirm that the XMLRPC server is up and ready to serve.
169
170        @return True (always).
171
172        """
173        logging.debug('ready()')
174        return True
175