You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
265 lines
9.1 KiB
265 lines
9.1 KiB
# Lint as: python2, python3
|
|
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
import contextlib
|
|
import dbus
|
|
import errno
|
|
import functools
|
|
import logging
|
|
import os
|
|
import select
|
|
import signal
|
|
import six.moves.xmlrpc_server
|
|
import threading
|
|
|
|
|
|
def terminate_old(script_name, sigterm_timeout=5, sigkill_timeout=3):
|
|
"""
|
|
Avoid "address already in use" errors by killing any leftover RPC server
|
|
processes, possibly from previous runs.
|
|
|
|
A process is a match if it's Python and has the given script in the command
|
|
line. This should avoid including processes such as editors and 'tail' of
|
|
logs, which might match a simple pkill.
|
|
|
|
exe=/usr/local/bin/python2.7
|
|
cmdline=['/usr/bin/python2', '-u', '/usr/local/autotest/.../rpc_server.py']
|
|
|
|
@param script_name: The filename of the main script, used to match processes
|
|
@param sigterm_timeout: Wait N seconds after SIGTERM before trying SIGKILL.
|
|
@param sigkill_timeout: Wait N seconds after SIGKILL before complaining.
|
|
"""
|
|
# import late, to avoid affecting servers that don't call the method
|
|
import psutil
|
|
|
|
script_name_abs = os.path.abspath(script_name)
|
|
script_name_base = os.path.basename(script_name)
|
|
me = psutil.Process()
|
|
|
|
logging.debug('This process: %s: %s, %s', me, me.exe(), me.cmdline())
|
|
logging.debug('Checking for leftover processes...')
|
|
|
|
running = []
|
|
for proc in psutil.process_iter(attrs=['name', 'exe', 'cmdline']):
|
|
if proc == me:
|
|
continue
|
|
try:
|
|
name = proc.name()
|
|
if not name or 'py' not in name:
|
|
continue
|
|
exe = proc.exe()
|
|
args = proc.cmdline()
|
|
# Note: If we ever need multiple instances on different ports,
|
|
# add a check for listener ports, likely via proc.connections()
|
|
if '/python' in exe and (script_name in args
|
|
or script_name_abs in args
|
|
or script_name_base in args):
|
|
logging.debug('Found process: %s: %s', proc, args)
|
|
running.append(proc)
|
|
except psutil.Error as e:
|
|
logging.debug('%s: %s', e, proc)
|
|
continue
|
|
|
|
if not running:
|
|
return
|
|
|
|
logging.info('Trying SIGTERM: pids=%s', [p.pid for p in running])
|
|
for proc in running:
|
|
try:
|
|
proc.send_signal(0)
|
|
proc.terminate()
|
|
except psutil.NoSuchProcess as e:
|
|
logging.debug('%s: %s', e, proc)
|
|
except psutil.Error as e:
|
|
logging.warn('%s: %s', e, proc)
|
|
|
|
(terminated, running) = psutil.wait_procs(running, sigterm_timeout)
|
|
if not running:
|
|
return
|
|
|
|
running.sort()
|
|
logging.info('Trying SIGKILL: pids=%s', [p.pid for p in running])
|
|
for proc in running:
|
|
try:
|
|
proc.kill()
|
|
except psutil.NoSuchProcess as e:
|
|
logging.debug('%s: %s', e, proc)
|
|
except psutil.Error as e:
|
|
logging.warn('%s: %s', e, proc)
|
|
|
|
(sigkilled, running) = psutil.wait_procs(running, sigkill_timeout)
|
|
if running:
|
|
running.sort()
|
|
logging.warn('Found leftover processes %s; address may be in use!',
|
|
[p.pid for p in running])
|
|
else:
|
|
logging.debug('Leftover processes have exited.')
|
|
|
|
|
|
class XmlRpcServer(threading.Thread):
|
|
"""Simple XMLRPC server implementation.
|
|
|
|
In theory, Python should provide a sane XMLRPC server implementation as
|
|
part of its standard library. In practice the provided implementation
|
|
doesn't handle signals, not even EINTR. As a result, we have this class.
|
|
|
|
Usage:
|
|
|
|
server = XmlRpcServer(('localhost', 43212))
|
|
server.register_delegate(my_delegate_instance)
|
|
server.run()
|
|
|
|
"""
|
|
|
|
def __init__(self, host, port):
|
|
"""Construct an XmlRpcServer.
|
|
|
|
@param host string hostname to bind to.
|
|
@param port int port number to bind to.
|
|
|
|
"""
|
|
super(XmlRpcServer, self).__init__()
|
|
logging.info('Binding server to %s:%d', host, port)
|
|
self._server = six.moves.xmlrpc_server.SimpleXMLRPCServer(
|
|
(host, port), allow_none=True)
|
|
self._server.register_introspection_functions()
|
|
# After python 2.7.10, BaseServer.handle_request automatically retries
|
|
# on EINTR, so handle_request will be blocked at select.select forever
|
|
# if timeout is None. Set a timeout so server can be shut down
|
|
# gracefully. Check issue crbug.com/571737 and
|
|
# https://bugs.python.org/issue7978 for the explanation.
|
|
self._server.timeout = 0.5
|
|
self._keep_running = True
|
|
self._delegates = []
|
|
# Gracefully shut down on signals. This is how we expect to be shut
|
|
# down by autotest.
|
|
signal.signal(signal.SIGTERM, self._handle_signal)
|
|
signal.signal(signal.SIGINT, self._handle_signal)
|
|
|
|
|
|
def register_delegate(self, delegate):
|
|
"""Register delegate objects with the server.
|
|
|
|
The server will automagically look up all methods not prefixed with an
|
|
underscore and treat them as potential RPC calls. These methods may
|
|
only take basic Python objects as parameters, as noted by the
|
|
SimpleXMLRPCServer documentation. The state of the delegate is
|
|
persisted across calls.
|
|
|
|
@param delegate object Python object to be exposed via RPC.
|
|
|
|
"""
|
|
self._server.register_instance(delegate)
|
|
self._delegates.append(delegate)
|
|
|
|
|
|
def run(self):
|
|
"""Block and handle many XmlRpc requests."""
|
|
logging.info('XmlRpcServer starting...')
|
|
# TODO(wiley) nested is deprecated, but we can't use the replacement
|
|
# until we move to Python 3.0.
|
|
with contextlib.nested(*self._delegates):
|
|
while self._keep_running:
|
|
try:
|
|
self._server.handle_request()
|
|
except select.error as v:
|
|
# In a cruel twist of fate, the python library doesn't
|
|
# handle this kind of error.
|
|
if v[0] != errno.EINTR:
|
|
raise
|
|
|
|
for delegate in self._delegates:
|
|
if hasattr(delegate, 'cleanup'):
|
|
delegate.cleanup()
|
|
|
|
logging.info('XmlRpcServer exited.')
|
|
|
|
|
|
def _handle_signal(self, _signum, _frame):
|
|
"""Handle a process signal by gracefully quitting.
|
|
|
|
SimpleXMLRPCServer helpfully exposes a method called shutdown() which
|
|
clears a flag similar to _keep_running, and then blocks until it sees
|
|
the server shut down. Unfortunately, if you call that function from
|
|
a signal handler, the server will just hang, since the process is
|
|
paused for the signal, causing a deadlock. Thus we are reinventing the
|
|
wheel with our own event loop.
|
|
|
|
"""
|
|
self._keep_running = False
|
|
|
|
|
|
def dbus_safe(default_return_value):
|
|
"""Catch all DBus exceptions and return a default value instead.
|
|
|
|
Wrap a function with a try block that catches DBus exceptions and
|
|
returns default values instead. This is convenient for simple error
|
|
handling since XMLRPC doesn't understand DBus exceptions.
|
|
|
|
@param wrapped_function function to wrap.
|
|
@param default_return_value value to return on exception (usually False).
|
|
|
|
"""
|
|
def decorator(wrapped_function):
|
|
"""Call a function and catch DBus errors.
|
|
|
|
@param wrapped_function function to call in dbus safe context.
|
|
@return function return value or default_return_value on failure.
|
|
|
|
"""
|
|
@functools.wraps(wrapped_function)
|
|
def wrapper(*args, **kwargs):
|
|
"""Pass args and kwargs to a dbus safe function.
|
|
|
|
@param args formal python arguments.
|
|
@param kwargs keyword python arguments.
|
|
@return function return value or default_return_value on failure.
|
|
|
|
"""
|
|
logging.debug('%s()', wrapped_function.__name__)
|
|
try:
|
|
return wrapped_function(*args, **kwargs)
|
|
|
|
except dbus.exceptions.DBusException as e:
|
|
logging.error('Exception while performing operation %s: %s: %s',
|
|
wrapped_function.__name__,
|
|
e.get_dbus_name(),
|
|
e.get_dbus_message())
|
|
return default_return_value
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
|
|
class XmlRpcDelegate(object):
|
|
"""A super class for XmlRPC delegates used with XmlRpcServer.
|
|
|
|
This doesn't add much helpful functionality except to implement the trivial
|
|
status check method expected by autotest's host.xmlrpc_connect() method.
|
|
Subclass this class to add more functionality.
|
|
|
|
"""
|
|
|
|
|
|
def __enter__(self):
|
|
logging.debug('Bringing up XmlRpcDelegate: %r.', self)
|
|
pass
|
|
|
|
|
|
def __exit__(self, exception, value, traceback):
|
|
logging.debug('Tearing down XmlRpcDelegate: %r.', self)
|
|
pass
|
|
|
|
|
|
def ready(self):
|
|
"""Confirm that the XMLRPC server is up and ready to serve.
|
|
|
|
@return True (always).
|
|
|
|
"""
|
|
logging.debug('ready()')
|
|
return True
|