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