[enh] Use a WebSocket stream to display messages in the api

This commit is contained in:
Jérôme Lebleu 2014-05-19 17:21:40 +02:00
parent abb1517a74
commit 25e1ed86d7
4 changed files with 74 additions and 8 deletions

View file

@ -25,5 +25,6 @@
"logged_in" : "Logged in", "logged_in" : "Logged in",
"logged_out" : "Logged out", "logged_out" : "Logged out",
"not_logged_in" : "You are not logged in", "not_logged_in" : "You are not logged in",
"server_already_running" : "A server is already running on that port" "server_already_running" : "A server is already running on that port",
"websocket_request_excepted" : "Excepted a WebSocket request"
} }

View file

@ -25,5 +25,6 @@
"logged_in" : "Connecté", "logged_in" : "Connecté",
"logged_out" : "Déconnecté", "logged_out" : "Déconnecté",
"not_logged_in" : "Vous n'êtes pas connecté", "not_logged_in" : "Vous n'êtes pas connecté",
"server_already_running" : "Un server est déjà en cours d'exécution sur ce port" "server_already_running" : "Un server est déjà en cours d'exécution sur ce port",
"websocket_request_excepted" : "Requête WebSocket attendue"
} }

View file

@ -61,16 +61,19 @@ def init(**kwargs):
## Easy access to interfaces ## Easy access to interfaces
def api(namespaces, port, routes={}, use_cache=True): def api(namespaces, host='localhost', port=80, routes={},
use_websocket=True, use_cache=True):
"""Web server (API) interface """Web server (API) interface
Run a HTTP server with the moulinette for an API usage. Run a HTTP server with the moulinette for an API usage.
Keyword arguments: Keyword arguments:
- namespaces -- The list of namespaces to use - namespaces -- The list of namespaces to use
- port -- Port number to run on - host -- Server address to bind to
- port -- Server port to bind to
- routes -- A dict of additional routes to add in the form of - routes -- A dict of additional routes to add in the form of
{(method, uri): callback} {(method, uri): callback}
- use_websocket -- Serve via WSGI to handle asynchronous responses
- use_cache -- False if it should parse the actions map file - use_cache -- False if it should parse the actions map file
instead of using the cached one instead of using the cached one
@ -79,7 +82,7 @@ def api(namespaces, port, routes={}, use_cache=True):
kwargs={'routes': routes}, kwargs={'routes': routes},
actionsmap={'namespaces': namespaces, actionsmap={'namespaces': namespaces,
'use_cache': use_cache}) 'use_cache': use_cache})
moulinette.run(port) moulinette.run(host, port, use_websocket)
def cli(namespaces, args, print_json=False, use_cache=True): def cli(namespaces, args, print_json=False, use_cache=True):
"""Command line interface """Command line interface

View file

@ -7,7 +7,10 @@ import logging
import binascii import binascii
import argparse import argparse
from json import dumps as json_encode from json import dumps as json_encode
from bottle import run, request, response, Bottle, HTTPResponse from bottle import run, request, response, Bottle, HTTPResponse
from gevent.queue import Queue
from geventwebsocket import WebSocketError
from moulinette.core import MoulinetteError, clean_session from moulinette.core import MoulinetteError, clean_session
from moulinette.interfaces import (BaseActionsMapParser, BaseInterface) from moulinette.interfaces import (BaseActionsMapParser, BaseInterface)
@ -105,10 +108,12 @@ class _ActionsMapPlugin(object):
def __init__(self, actionsmap): def __init__(self, actionsmap):
# Connect signals to handlers # Connect signals to handlers
msignals.set_handler('authenticate', self._do_authenticate) msignals.set_handler('authenticate', self._do_authenticate)
msignals.set_handler('display', self._do_display)
self.actionsmap = actionsmap self.actionsmap = actionsmap
# TODO: Save and load secrets? # TODO: Save and load secrets?
self.secrets = {} self.secrets = {}
self.queues = {}
def setup(self, app): def setup(self, app):
"""Setup plugin on the application """Setup plugin on the application
@ -151,6 +156,10 @@ class _ActionsMapPlugin(object):
app.route('/logout', name='logout', method='GET', app.route('/logout', name='logout', method='GET',
callback=self.logout, skip=['actionsmap'], apply=_logout) callback=self.logout, skip=['actionsmap'], apply=_logout)
# Append messages route
app.route('/messages', name='messages',
callback=self.messages, skip=['actionsmap'])
# Append routes from the actions map # Append routes from the actions map
for (m, p) in self.actionsmap.parser.routes: for (m, p) in self.actionsmap.parser.routes:
app.route(p, method=m, callback=self.process) app.route(p, method=m, callback=self.process)
@ -247,6 +256,33 @@ class _ActionsMapPlugin(object):
clean_session(s_id) clean_session(s_id)
return m18n.g('logged_out') return m18n.g('logged_out')
def messages(self):
"""Listen to the messages WebSocket stream
Retrieve the WebSocket stream and send to it each messages displayed by
the core.MoulinetteSignals.display signal. They are JSON encoded as a
dict { style: message }.
"""
s_id = request.get_cookie('session.id')
try:
queue = self.queues[s_id]
except KeyError:
# Create a new queue for the session
queue = Queue()
self.queues[s_id] = queue
wsock = request.environ.get('wsgi.websocket')
if not wsock:
return HTTPErrorResponse(m18n.g('websocket_request_excepted'))
while True:
style, message = queue.get()
try:
wsock.send(json_encode({ style: message }))
except WebSocketError:
break
def process(self, _route, arguments={}): def process(self, _route, arguments={}):
"""Process the relevant action for the route """Process the relevant action for the route
@ -289,6 +325,21 @@ class _ActionsMapPlugin(object):
else: else:
return authenticator(token=(s_id, s_hash)) return authenticator(token=(s_id, s_hash))
def _do_display(self, message, style):
"""Display a message
Handle the core.MoulinetteSignals.display signal.
"""
s_id = request.get_cookie('session.id')
try:
queue = self.queues[s_id]
except KeyError:
return
# Put the message as a 2-tuple in the queue
queue.put_nowait((style, message))
# HTTP Responses ------------------------------------------------------- # HTTP Responses -------------------------------------------------------
@ -498,18 +549,28 @@ class Interface(BaseInterface):
self._app = app self._app = app
def run(self, _port): def run(self, host='localhost', port=80, use_websocket=True):
"""Run the moulinette """Run the moulinette
Start a server instance on the given port to serve moulinette Start a server instance on the given port to serve moulinette
actions. actions.
Keyword arguments: Keyword arguments:
- _port -- Port number to run on - host -- Server address to bind to
- port -- Server port to bind to
- use_websocket -- Serve via WSGI to handle asynchronous responses
""" """
try: try:
run(self._app, port=_port) if use_websocket:
from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler
server = WSGIServer((host, port), self._app,
handler_class=WebSocketHandler)
server.serve_forever()
else:
run(self._app, host=host, port=port)
except IOError as e: except IOError as e:
if e.args[0] == errno.EADDRINUSE: if e.args[0] == errno.EADDRINUSE:
raise MoulinetteError(errno.EADDRINUSE, raise MoulinetteError(errno.EADDRINUSE,