Add first experiment with websockets, for datasync status page
This commit is contained in:
parent
065f845707
commit
2375733d0f
9 changed files with 414 additions and 17 deletions
70
tailbone/views/asgi/__init__.py
Normal file
70
tailbone/views/asgi/__init__.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2022 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
ASGI Views
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
import http.cookies
|
||||
|
||||
from beaker.cache import clsmap
|
||||
from beaker.session import SessionObject, SignedCookie
|
||||
|
||||
|
||||
class WebsocketView(object):
|
||||
|
||||
def __init__(self, registry):
|
||||
self.registry = registry
|
||||
|
||||
async def get_user_session(self, scope):
|
||||
settings = self.registry.settings
|
||||
beaker_key = settings['beaker.session.key']
|
||||
beaker_secret = settings['beaker.session.secret']
|
||||
beaker_type = settings['beaker.session.type']
|
||||
beaker_data_dir = settings['beaker.session.data_dir']
|
||||
beaker_lock_dir = settings['beaker.session.lock_dir']
|
||||
|
||||
# get ahold of session identifier cookie
|
||||
headers = dict(scope['headers'])
|
||||
cookie = headers.get(b'cookie')
|
||||
if not cookie:
|
||||
return
|
||||
cookie = cookie.decode('utf_8')
|
||||
cookie = http.cookies.SimpleCookie(cookie)
|
||||
morsel = cookie[beaker_key]
|
||||
|
||||
# simulate pyramid_beaker logic to get at the session
|
||||
cookieheader = morsel.output(header='')
|
||||
cookie = SignedCookie(beaker_secret, input=cookieheader)
|
||||
session_id = cookie[beaker_key].value
|
||||
request = {'cookie': cookieheader}
|
||||
session = SessionObject(
|
||||
request,
|
||||
id=session_id,
|
||||
key=beaker_key,
|
||||
namespace_class=clsmap[beaker_type],
|
||||
data_dir=beaker_data_dir,
|
||||
lock_dir=beaker_lock_dir)
|
||||
|
||||
return session
|
113
tailbone/views/asgi/datasync.py
Normal file
113
tailbone/views/asgi/datasync.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2022 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
DataSync Views
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from tailbone.views.asgi import WebsocketView
|
||||
|
||||
|
||||
class DatasyncWS(WebsocketView):
|
||||
|
||||
async def status(self, scope, receive, send):
|
||||
rattail_config = self.registry['rattail_config']
|
||||
app = rattail_config.get_app()
|
||||
model = app.model
|
||||
auth_handler = app.get_auth_handler()
|
||||
datasync_handler = app.get_datasync_handler()
|
||||
|
||||
authorized = False
|
||||
user_session = await self.get_user_session(scope)
|
||||
if user_session:
|
||||
user_uuid = user_session.get('auth.userid')
|
||||
session = app.make_session()
|
||||
|
||||
user = None
|
||||
if user_uuid:
|
||||
user = session.query(model.User).get(user_uuid)
|
||||
|
||||
# figure out if user is authorized for this websocket
|
||||
permission = 'datasync.status'
|
||||
authorized = auth_handler.has_permission(session, user, permission)
|
||||
session.close()
|
||||
|
||||
# wait for client to connect
|
||||
message = await receive()
|
||||
assert message['type'] == 'websocket.connect'
|
||||
|
||||
# allow or deny access, per authorization
|
||||
if authorized:
|
||||
await send({'type': 'websocket.accept'})
|
||||
else: # forbidden
|
||||
await send({'type': 'websocket.close'})
|
||||
return
|
||||
|
||||
# this tracks when client disconnects
|
||||
state = {'disconnected': False}
|
||||
|
||||
async def wait_for_disconnect():
|
||||
message = await receive()
|
||||
if message['type'] == 'websocket.disconnect':
|
||||
state['disconnected'] = True
|
||||
|
||||
# watch for client disconnect, while we do other things
|
||||
asyncio.create_task(wait_for_disconnect())
|
||||
|
||||
# do the rest forever, until client disconnects
|
||||
while not state['disconnected']:
|
||||
|
||||
# give client latest supervisor process info
|
||||
info = datasync_handler.get_supervisor_process_info()
|
||||
await send({'type': 'websocket.send',
|
||||
'subtype': 'datasync.supervisor_process_info',
|
||||
'text': json.dumps(info)})
|
||||
|
||||
# pause for 1 second
|
||||
await asyncio.sleep(1)
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
cls._defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _defaults(cls, config):
|
||||
|
||||
# status
|
||||
config.add_tailbone_websocket('datasync.status',
|
||||
cls, attr='status')
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
DatasyncWS = kwargs.get('DatasyncWS', base['DatasyncWS'])
|
||||
DatasyncWS.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
|
@ -40,6 +40,7 @@ from rattail.util import simple_error
|
|||
|
||||
from tailbone.views import MasterView
|
||||
from tailbone.util import raw_datetime
|
||||
from tailbone.config import should_expose_websockets
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -400,6 +401,19 @@ class DataSyncChangeView(MasterView):
|
|||
DataSyncChangesView = DataSyncChangeView
|
||||
|
||||
|
||||
def includeme(config):
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
rattail_config = config.registry['rattail_config']
|
||||
|
||||
DataSyncThreadView = kwargs.get('DataSyncThreadView', base['DataSyncThreadView'])
|
||||
DataSyncThreadView.defaults(config)
|
||||
|
||||
DataSyncChangeView = kwargs.get('DataSyncChangeView', base['DataSyncChangeView'])
|
||||
DataSyncChangeView.defaults(config)
|
||||
|
||||
if should_expose_websockets(rattail_config):
|
||||
config.include('tailbone.views.asgi.datasync')
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue