diff --git a/locales/en.json b/locales/en.json index d8639548..9228cf0b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -25,5 +25,6 @@ "logged_in" : "Logged in", "logged_out" : "Logged out", "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" } diff --git a/locales/fr.json b/locales/fr.json index b446a52d..9e5dba5f 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -25,5 +25,6 @@ "logged_in" : "Connecté", "logged_out" : "Dé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" } diff --git a/moulinette/__init__.py b/moulinette/__init__.py index f236f083..2c6fd08a 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -61,16 +61,19 @@ def init(**kwargs): ## 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 Run a HTTP server with the moulinette for an API usage. Keyword arguments: - 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 {(method, uri): callback} + - use_websocket -- Serve via WSGI to handle asynchronous responses - use_cache -- False if it should parse the actions map file instead of using the cached one @@ -79,7 +82,7 @@ def api(namespaces, port, routes={}, use_cache=True): kwargs={'routes': routes}, actionsmap={'namespaces': namespaces, 'use_cache': use_cache}) - moulinette.run(port) + moulinette.run(host, port, use_websocket) def cli(namespaces, args, print_json=False, use_cache=True): """Command line interface diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py index 5453b477..91e5c893 100644 --- a/moulinette/interfaces/api.py +++ b/moulinette/interfaces/api.py @@ -7,7 +7,10 @@ import logging import binascii import argparse from json import dumps as json_encode + 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.interfaces import (BaseActionsMapParser, BaseInterface) @@ -105,10 +108,12 @@ class _ActionsMapPlugin(object): def __init__(self, actionsmap): # Connect signals to handlers msignals.set_handler('authenticate', self._do_authenticate) + msignals.set_handler('display', self._do_display) self.actionsmap = actionsmap # TODO: Save and load secrets? self.secrets = {} + self.queues = {} def setup(self, app): """Setup plugin on the application @@ -151,6 +156,10 @@ class _ActionsMapPlugin(object): app.route('/logout', name='logout', method='GET', 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 for (m, p) in self.actionsmap.parser.routes: app.route(p, method=m, callback=self.process) @@ -247,6 +256,33 @@ class _ActionsMapPlugin(object): clean_session(s_id) 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={}): """Process the relevant action for the route @@ -289,6 +325,21 @@ class _ActionsMapPlugin(object): else: 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 ------------------------------------------------------- @@ -498,18 +549,28 @@ class Interface(BaseInterface): self._app = app - def run(self, _port): + def run(self, host='localhost', port=80, use_websocket=True): """Run the moulinette Start a server instance on the given port to serve moulinette actions. 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: - 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: if e.args[0] == errno.EADDRINUSE: raise MoulinetteError(errno.EADDRINUSE,