/lib/server_base.py
Python | 128 lines | 64 code | 20 blank | 44 comment | 8 complexity | 341d90b31a4326281e12356ef7ffd43a MD5 | raw file
- # Copyright (c) 2017, Neil Booth
- #
- # All rights reserved.
- #
- # See the file "LICENCE" for information about the copyright
- # and warranty status of this software.
- import asyncio
- import os
- import signal
- import sys
- import time
- from functools import partial
- import lib.util as util
- class ServerBase(util.LoggedClass):
- '''Base class server implementation.
- Derived classes are expected to:
- - set PYTHON_MIN_VERSION and SUPPRESS_MESSAGES as appropriate
- - implement the start_servers() coroutine, called from the run() method.
- Upon return the event loop runs until the shutdown signal is received.
- - implement the shutdown() coroutine
- '''
- SUPPRESS_MESSAGES = [
- 'Fatal read error on socket transport',
- 'Fatal write error on socket transport',
- ]
- PYTHON_MIN_VERSION = (3, 6)
- def __init__(self, env):
- '''Save the environment, perform basic sanity checks, and set the
- event loop policy.
- '''
- super().__init__()
- self.env = env
- # Sanity checks
- if sys.version_info < self.PYTHON_MIN_VERSION:
- mvs = '.'.join(str(part) for part in self.PYTHON_MIN_VERSION)
- raise RuntimeError('Python version >= {} is required'.format(mvs))
- if os.geteuid() == 0 and not env.allow_root:
- raise RuntimeError('RUNNING AS ROOT IS STRONGLY DISCOURAGED!\n'
- 'You shoud create an unprivileged user account '
- 'and use that.\n'
- 'To continue as root anyway, restart with '
- 'environment variable ALLOW_ROOT non-empty')
- # First asyncio operation must be to set the event loop policy
- # as this replaces the event loop
- self.logger.info('event loop policy: {}'.format(self.env.loop_policy))
- asyncio.set_event_loop_policy(self.env.loop_policy)
- # Trigger this event to cleanly shutdown
- self.shutdown_event = asyncio.Event()
- async def start_servers(self):
- '''Override to perform initialization that requires the event loop,
- and start servers.'''
- pass
- async def shutdown(self):
- '''Override to perform the shutdown sequence, if any.'''
- pass
- async def _wait_for_shutdown_event(self):
- '''Wait for shutdown to be signalled, and log it.
- Derived classes may want to provide a shutdown() coroutine.'''
- # Shut down cleanly after waiting for shutdown to be signalled
- await self.shutdown_event.wait()
- self.logger.info('shutting down')
- # Wait for the shutdown sequence
- await self.shutdown()
- # Finally, work around an apparent asyncio bug that causes log
- # spew on shutdown for partially opened SSL sockets
- try:
- del asyncio.sslproto._SSLProtocolTransport.__del__
- except Exception:
- pass
- self.logger.info('shutdown complete')
- def on_signal(self, signame):
- '''Call on receipt of a signal to cleanly shutdown.'''
- self.logger.warning('received {} signal, initiating shutdown'
- .format(signame))
- self.shutdown_event.set()
- def on_exception(self, loop, context):
- '''Suppress spurious messages it appears we cannot control.'''
- message = context.get('message')
- if message in self.SUPPRESS_MESSAGES:
- return
- if 'accept_connection2()' in repr(context.get('task')):
- return
- loop.default_exception_handler(context)
- def run(self):
- '''Run the server application:
- - record start time
- - set the event loop policy as specified by the environment
- - install SIGINT and SIGKILL handlers to trigger shutdown_event
- - set loop's exception handler to suppress unwanted messages
- - run the event loop until start_servers() completes
- - run the event loop until shutdown is signalled
- '''
- self.start_time = time.time()
- loop = asyncio.get_event_loop()
- for signame in ('SIGINT', 'SIGTERM'):
- loop.add_signal_handler(getattr(signal, signame),
- partial(self.on_signal, signame))
- loop.set_exception_handler(self.on_exception)
- loop.run_until_complete(self.start_servers())
- loop.run_until_complete(self._wait_for_shutdown_event())
- loop.close()