mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
[enh] Use a WebSocket stream to display messages in the api
This commit is contained in:
parent
abb1517a74
commit
25e1ed86d7
4 changed files with 74 additions and 8 deletions
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue