2015-07-22 18:04:26 +00:00
|
|
|
"""
|
|
|
|
Run a Tornado HTTP service.
|
|
|
|
|
|
|
|
- :class:`.Runner`: encapsulates the running of the application
|
2016-06-14 03:23:17 +00:00
|
|
|
- :class:`.RunCommand`: distutils command to runs an application
|
2015-07-22 18:04:26 +00:00
|
|
|
|
|
|
|
"""
|
2016-06-14 03:23:17 +00:00
|
|
|
from distutils import cmd, errors, log
|
2015-12-10 15:44:52 +00:00
|
|
|
import logging
|
2016-06-14 03:23:17 +00:00
|
|
|
import os.path
|
2015-07-22 18:04:26 +00:00
|
|
|
import signal
|
2016-01-28 12:09:32 +00:00
|
|
|
import sys
|
2015-07-22 18:04:26 +00:00
|
|
|
|
2016-07-31 13:30:06 +00:00
|
|
|
from tornado import httpserver, ioloop
|
2015-07-22 18:04:26 +00:00
|
|
|
|
2016-07-31 13:30:06 +00:00
|
|
|
import sprockets.http.app
|
2015-08-28 14:57:06 +00:00
|
|
|
|
2015-07-22 18:04:26 +00:00
|
|
|
|
|
|
|
class Runner(object):
|
|
|
|
"""
|
|
|
|
HTTP service runner.
|
|
|
|
|
2016-07-31 13:30:06 +00:00
|
|
|
:param tornado.web.Application app: the application to serve
|
2015-07-22 18:04:26 +00:00
|
|
|
|
|
|
|
This class implements the logic necessary to safely run a
|
|
|
|
Tornado HTTP service inside of a docker container.
|
|
|
|
|
|
|
|
.. rubric:: Usage Example
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
def make_app():
|
|
|
|
return web.Application(...)
|
|
|
|
|
|
|
|
def run():
|
|
|
|
server = runner.Runner(make_app())
|
|
|
|
server.start_server()
|
|
|
|
ioloop.IOLoop.instance().start()
|
|
|
|
|
|
|
|
The :meth:`.start_server` method sets up the necessary signal handling
|
|
|
|
to ensure that we have a clean shutdown in the face of signals.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2016-07-31 13:30:06 +00:00
|
|
|
def __init__(self, app, before_run=None, on_start=None, shutdown=None):
|
2016-03-10 22:10:31 +00:00
|
|
|
"""Create a new instance of the runner.
|
|
|
|
|
2016-07-31 13:30:06 +00:00
|
|
|
:param tornado.web.Application app: The application instance to run
|
2016-03-10 22:10:31 +00:00
|
|
|
:param list before_run: Callbacks to invoke before starting
|
|
|
|
:param list on_start: Callbacks to invoke after starting the IOLoop
|
|
|
|
:param list shutdown: Callbacks to invoke on shutdown
|
|
|
|
|
|
|
|
"""
|
2016-07-31 13:30:06 +00:00
|
|
|
self.application = sprockets.http.app.wrap_application(
|
|
|
|
app, before_run, on_start, shutdown)
|
2015-07-22 18:04:26 +00:00
|
|
|
self.logger = logging.getLogger('Runner')
|
|
|
|
self.server = None
|
|
|
|
|
2015-09-24 18:57:03 +00:00
|
|
|
def start_server(self, port_number, number_of_procs=0):
|
2015-07-22 18:04:26 +00:00
|
|
|
"""
|
|
|
|
Create a HTTP server and start it.
|
|
|
|
|
|
|
|
:param int port_number: the port number to bind the server to
|
2015-09-24 18:57:03 +00:00
|
|
|
:param int number_of_procs: number of processes to pass to
|
|
|
|
Tornado's ``httpserver.HTTPServer.start``.
|
2015-07-22 18:04:26 +00:00
|
|
|
|
|
|
|
If the application's ``debug`` setting is ``True``, then we are
|
|
|
|
going to run in a single-process mode; otherwise, we'll let
|
|
|
|
tornado decide how many sub-processes to spawn.
|
|
|
|
|
|
|
|
"""
|
|
|
|
signal.signal(signal.SIGTERM, self._on_signal)
|
|
|
|
signal.signal(signal.SIGINT, self._on_signal)
|
2016-02-23 16:39:52 +00:00
|
|
|
xheaders = self.application.settings.get('xheaders', False)
|
2015-07-22 18:04:26 +00:00
|
|
|
|
2016-06-14 03:23:46 +00:00
|
|
|
self.server = httpserver.HTTPServer(self.application,
|
|
|
|
xheaders=xheaders)
|
2015-07-22 18:04:26 +00:00
|
|
|
if self.application.settings.get('debug', False):
|
|
|
|
self.logger.info('starting 1 process on port %d', port_number)
|
|
|
|
self.server.listen(port_number)
|
|
|
|
else:
|
|
|
|
self.logger.info('starting processes on port %d', port_number)
|
|
|
|
self.server.bind(port_number)
|
2015-09-24 18:57:03 +00:00
|
|
|
self.server.start(number_of_procs)
|
2015-07-22 18:04:26 +00:00
|
|
|
|
2016-03-10 22:10:31 +00:00
|
|
|
def stop_server(self):
|
|
|
|
"""Stop the HTTP Server"""
|
|
|
|
self.server.stop()
|
|
|
|
|
2015-09-24 18:57:03 +00:00
|
|
|
def run(self, port_number, number_of_procs=0):
|
2015-07-22 18:04:26 +00:00
|
|
|
"""
|
|
|
|
Create the server and run the IOLoop.
|
|
|
|
|
|
|
|
:param int port_number: the port number to bind the server to
|
2015-09-24 18:57:03 +00:00
|
|
|
:param int number_of_procs: number of processes to pass to
|
|
|
|
Tornado's ``httpserver.HTTPServer.start``.
|
2015-07-22 18:04:26 +00:00
|
|
|
|
|
|
|
If the application's ``debug`` setting is ``True``, then we are
|
|
|
|
going to run in a single-process mode; otherwise, we'll let
|
2016-01-28 12:09:32 +00:00
|
|
|
tornado decide how many sub-processes to spawn. In any case, the
|
|
|
|
applications *before_run* callbacks are invoked. If a callback
|
|
|
|
raises an exception, then the application is terminated by calling
|
|
|
|
:func:`sys.exit`.
|
2015-07-22 18:04:26 +00:00
|
|
|
|
2016-03-10 22:10:31 +00:00
|
|
|
If any ``on_start`` callbacks are registered, they will be added to
|
|
|
|
the Tornado IOLoop for execution after the IOLoop is started.
|
|
|
|
|
2015-07-22 18:04:26 +00:00
|
|
|
"""
|
2015-09-24 18:57:03 +00:00
|
|
|
self.start_server(port_number, number_of_procs)
|
2016-02-15 18:01:16 +00:00
|
|
|
iol = ioloop.IOLoop.instance()
|
2016-03-10 22:10:31 +00:00
|
|
|
|
2016-07-31 13:30:06 +00:00
|
|
|
try:
|
|
|
|
self.application.run(iol)
|
|
|
|
except:
|
|
|
|
self.logger.exception('application terminated during start, '
|
|
|
|
'exiting')
|
|
|
|
sys.exit(70)
|
2016-03-10 22:10:31 +00:00
|
|
|
|
2016-01-27 23:24:02 +00:00
|
|
|
iol.start()
|
2015-07-22 18:04:26 +00:00
|
|
|
|
|
|
|
def _on_signal(self, signo, frame):
|
|
|
|
self.logger.info('signal %s received, stopping', signo)
|
|
|
|
ioloop.IOLoop.instance().add_callback_from_signal(self._shutdown)
|
|
|
|
|
2016-03-10 22:10:31 +00:00
|
|
|
def _shutdown(self):
|
|
|
|
self.logger.debug('Shutting down')
|
|
|
|
|
|
|
|
# Ensure the HTTP server is stopped
|
|
|
|
self.stop_server()
|
|
|
|
|
2016-07-31 13:30:06 +00:00
|
|
|
# Start the application shutdown process
|
|
|
|
self.application.stop(ioloop.IOLoop.instance())
|
2016-06-14 03:23:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
class RunCommand(cmd.Command):
|
|
|
|
"""
|
|
|
|
Simple distutils.Command that calls :func:`sprockets.http.run`
|
|
|
|
|
|
|
|
This is installed as the httprun distutils command when you
|
|
|
|
install the ``sprockets.http`` module.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
description = 'Run a sprockets.http application.'
|
|
|
|
user_options = [
|
|
|
|
('application=', 'a',
|
|
|
|
'application callable in `pkg.mod:func` syntax'),
|
|
|
|
('env-file=', 'e', 'environment file to import'),
|
|
|
|
('port=', 'p', 'port for the application to listen on'),
|
|
|
|
]
|
|
|
|
|
|
|
|
def initialize_options(self):
|
|
|
|
self.application = None
|
|
|
|
self.env_file = None
|
|
|
|
self.port = None
|
|
|
|
|
|
|
|
def finalize_options(self):
|
|
|
|
if not self.application:
|
|
|
|
raise errors.DistutilsArgError('application is required')
|
|
|
|
if self.env_file and not os.path.exists(self.env_file):
|
|
|
|
raise errors.DistutilsArgError(
|
|
|
|
'environment file "{}" does not exist'.format(
|
|
|
|
self.env_file))
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
self._read_environment()
|
|
|
|
if self.port:
|
|
|
|
log.info('overriding port to %s', self.port)
|
|
|
|
os.environ['PORT'] = self.port
|
|
|
|
app_factory = self._find_callable()
|
|
|
|
if self.dry_run:
|
|
|
|
log.info('would run %r', app_factory)
|
|
|
|
else:
|
|
|
|
log.info('running %r', app_factory)
|
|
|
|
sprockets.http.run(app_factory)
|
|
|
|
|
|
|
|
def _read_environment(self):
|
|
|
|
if not self.env_file:
|
|
|
|
return
|
|
|
|
|
|
|
|
with open(self.env_file) as env_file:
|
|
|
|
for line in env_file.readlines():
|
|
|
|
orig_line = line.strip()
|
|
|
|
if '#' in line:
|
|
|
|
line = line[:line.index('#')]
|
|
|
|
if line.startswith('export '):
|
|
|
|
line = line[7:]
|
|
|
|
|
|
|
|
name, sep, value = line.strip().partition('=')
|
|
|
|
if sep == '=':
|
|
|
|
if (value.startswith(('"', "'")) and
|
|
|
|
value.endswith(value[0])):
|
|
|
|
value = value[1:-1]
|
|
|
|
if value:
|
|
|
|
log.info('setting environment %s=%s', name, value)
|
|
|
|
os.environ[name] = value
|
|
|
|
else:
|
|
|
|
log.info('removing %s from environment', name)
|
|
|
|
os.environ.pop(name, None)
|
|
|
|
elif line:
|
|
|
|
log.info('malformed environment line %r ignored',
|
|
|
|
orig_line)
|
|
|
|
|
|
|
|
def _find_callable(self):
|
|
|
|
app_module, callable_name = self.application.split(':')
|
|
|
|
mod = __import__(app_module)
|
|
|
|
for next_mod in app_module.split('.')[1:]:
|
|
|
|
mod = getattr(mod, next_mod)
|
|
|
|
return getattr(mod, callable_name)
|