2 changed files with 179 additions and 110 deletions
@ -0,0 +1,128 @@ |
|||
# 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() |
Loading…
Reference in new issue