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