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