Merge pull request #784 from jodal/feature/http-startup

http: Improve error handling at HTTP server startup
This commit is contained in:
Thomas Adamcik 2014-07-17 23:29:04 +02:00
commit 03f2e0e322
3 changed files with 110 additions and 72 deletions

View File

@ -7,14 +7,16 @@ import threading
import pykka
import tornado.httpserver
import tornado.ioloop
import tornado.netutil
import tornado.web
import tornado.websocket
from mopidy import models, zeroconf
from mopidy import exceptions, models, zeroconf
from mopidy.core import CoreListener
from mopidy.http import handlers
from mopidy.utils import formatting
from mopidy.utils import encoding, formatting, network
logger = logging.getLogger(__name__)
@ -26,20 +28,32 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
def __init__(self, config, core):
super(HttpFrontend, self).__init__()
self.config = config
self.core = core
self.hostname = config['http']['hostname']
self.hostname = network.format_hostname(config['http']['hostname'])
self.port = config['http']['port']
tornado_hostname = config['http']['hostname']
if tornado_hostname == '::':
tornado_hostname = None
try:
logger.debug('Starting HTTP server')
sockets = tornado.netutil.bind_sockets(self.port, tornado_hostname)
self.server = HttpServer(
config=config, core=core, sockets=sockets,
apps=self.apps, statics=self.statics)
except IOError as error:
raise exceptions.FrontendError(
'HTTP server startup failed: %s' %
encoding.locale_decode(error))
self.zeroconf_name = config['http']['zeroconf']
self.zeroconf_http = None
self.zeroconf_mopidy_http = None
self.app = None
def on_start(self):
threading.Thread(target=self._startup).start()
logger.info(
'HTTP server running at [%s]:%s', self.hostname, self.port)
self.server.start()
if self.zeroconf_name:
self.zeroconf_http = zeroconf.Zeroconf(
@ -57,56 +71,59 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
if self.zeroconf_mopidy_http:
self.zeroconf_mopidy_http.unpublish()
tornado.ioloop.IOLoop.instance().add_callback(self._shutdown)
def _startup(self):
logger.debug('Starting HTTP server')
self.app = tornado.web.Application(self._get_request_handlers())
self.app.listen(self.port,
self.hostname if self.hostname != '::' else None)
logger.info(
'HTTP server running at http://%s:%s', self.hostname, self.port)
tornado.ioloop.IOLoop.instance().start()
def _shutdown(self):
logger.debug('Stopping HTTP server')
tornado.ioloop.IOLoop.instance().stop()
logger.debug('Stopped HTTP server')
self.server.stop()
def on_event(self, name, **data):
event = data
event['event'] = name
message = json.dumps(event, cls=models.ModelJSONEncoder)
handlers.WebSocketHandler.broadcast(message)
on_event(name, **data)
def on_event(name, **data):
event = data
event['event'] = name
message = json.dumps(event, cls=models.ModelJSONEncoder)
handlers.WebSocketHandler.broadcast(message)
class HttpServer(threading.Thread):
name = 'HttpServer'
def __init__(self, config, core, sockets, apps, statics):
super(HttpServer, self).__init__()
self.config = config
self.core = core
self.sockets = sockets
self.apps = apps
self.statics = statics
self.app = None
self.server = None
def run(self):
self.app = tornado.web.Application(self._get_request_handlers())
self.server = tornado.httpserver.HTTPServer(self.app)
self.server.add_sockets(self.sockets)
tornado.ioloop.IOLoop.instance().start()
logger.debug('Stopped HTTP server')
def stop(self):
logger.debug('Stopping HTTP server')
tornado.ioloop.IOLoop.instance().add_callback(
tornado.ioloop.IOLoop.instance().stop)
def _get_request_handlers(self):
request_handlers = []
request_handlers.extend(self._get_app_request_handlers())
request_handlers.extend(self._get_static_request_handlers())
# Either default Mopidy or user defined path to files
static_dir = self.config['http']['static_dir']
if static_dir and not os.path.exists(static_dir):
logger.warning(
'Configured http/static_dir %s does not exist. '
'Falling back to default HTTP handler.', static_dir)
static_dir = None
if static_dir:
request_handlers.append((r'/(.*)', handlers.StaticFileHandler, {
'path': self.config['http']['static_dir'],
'default_filename': 'index.html',
}))
else:
request_handlers.append((r'/', tornado.web.RedirectHandler, {
'url': '/mopidy/',
'permanent': False,
}))
request_handlers.extend(self._get_mopidy_request_handlers())
logger.debug(
'HTTP routes from extensions: %s',
formatting.indent('\n'.join(
'%r: %r' % (r[0], r[1]) for r in request_handlers)))
return request_handlers
def _get_app_request_handlers(self):
@ -141,3 +158,25 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
))
logger.debug('Loaded static HTTP extension: %s', static['name'])
return result
def _get_mopidy_request_handlers(self):
# Either default Mopidy or user defined path to files
static_dir = self.config['http']['static_dir']
if static_dir and not os.path.exists(static_dir):
logger.warning(
'Configured http/static_dir %s does not exist. '
'Falling back to default HTTP handler.', static_dir)
static_dir = None
if static_dir:
return [(r'/(.*)', handlers.StaticFileHandler, {
'path': self.config['http']['static_dir'],
'default_filename': 'index.html',
})]
else:
return [(r'/', tornado.web.RedirectHandler, {
'url': '/mopidy/',
'permanent': False,
})]

View File

@ -10,20 +10,12 @@ from mopidy.http import actor
@mock.patch('mopidy.http.handlers.WebSocketHandler.broadcast')
class HttpEventsTest(unittest.TestCase):
def setUp(self):
config = {
'http': {
'hostname': '127.0.0.1',
'port': 6680,
'static_dir': None,
'zeroconf': '',
}
}
self.http = actor.HttpFrontend(config=config, core=mock.Mock())
def test_track_playback_paused_is_broadcasted(self, broadcast):
broadcast.reset_mock()
self.http.on_event('track_playback_paused', foo='bar')
actor.on_event('track_playback_paused', foo='bar')
self.assertDictEqual(
json.loads(str(broadcast.call_args[0][0])), {
'event': 'track_playback_paused',
@ -32,7 +24,9 @@ class HttpEventsTest(unittest.TestCase):
def test_track_playback_resumed_is_broadcasted(self, broadcast):
broadcast.reset_mock()
self.http.on_event('track_playback_resumed', foo='bar')
actor.on_event('track_playback_resumed', foo='bar')
self.assertDictEqual(
json.loads(str(broadcast.call_args[0][0])), {
'event': 'track_playback_resumed',

View File

@ -27,16 +27,19 @@ class HttpServerTest(tornado.testing.AsyncHTTPTestCase):
core.get_version = mock.MagicMock(name='get_version')
core.get_version.return_value = mopidy.__version__
apps = [dict(name='testapp')]
statics = [dict(name='teststatic')]
testapps = [dict(name='testapp')]
teststatics = [dict(name='teststatic')]
http_frontend = actor.HttpFrontend(config=self.get_config(), core=core)
http_frontend.apps = [{
apps = [{
'name': 'mopidy',
'factory': handlers.make_mopidy_app_factory(apps, statics),
'factory': handlers.make_mopidy_app_factory(testapps, teststatics),
}]
return tornado.web.Application(http_frontend._get_request_handlers())
http_server = actor.HttpServer(
config=self.get_config(), core=core, sockets=[],
apps=apps, statics=[])
return tornado.web.Application(http_server._get_request_handlers())
class RootRedirectTest(HttpServerTest):
@ -172,12 +175,12 @@ class HttpServerWithStaticFilesTest(tornado.testing.AsyncHTTPTestCase):
}
core = mock.Mock()
http_frontend = actor.HttpFrontend(config=config, core=core)
http_frontend.statics = [
dict(name='static', path=os.path.dirname(__file__)),
]
statics = [dict(name='static', path=os.path.dirname(__file__))]
return tornado.web.Application(http_frontend._get_request_handlers())
http_server = actor.HttpServer(
config=config, core=core, sockets=[], apps=[], statics=statics)
return tornado.web.Application(http_server._get_request_handlers())
def test_without_slash_should_redirect(self):
response = self.fetch('/static', method='GET', follow_redirects=False)
@ -222,13 +225,15 @@ class HttpServerWithWsgiAppTest(tornado.testing.AsyncHTTPTestCase):
}
core = mock.Mock()
http_frontend = actor.HttpFrontend(config=config, core=core)
http_frontend.apps = [{
apps = [{
'name': 'wsgi',
'factory': wsgi_app_factory,
}]
return tornado.web.Application(http_frontend._get_request_handlers())
http_server = actor.HttpServer(
config=config, core=core, sockets=[], apps=apps, statics=[])
return tornado.web.Application(http_server._get_request_handlers())
def test_without_slash_should_redirect(self):
response = self.fetch('/wsgi', method='GET', follow_redirects=False)