WIP POC imlementation of Websocket Json RPC server

Includes two testing clients

Requires:
- adding wsrpc-aiohttp==3.0.1 to pypeapp/requirements.txt

- adding code to \pype-config\presets\tray\menu_items.json
, {
            "title": "Websocket Server",
            "type": "module",
            "import_path": "pype.modules.websocket_server",
            "fromlist": ["pype","modules"]
        }
This commit is contained in:
petr.kalis 2020-07-03 17:16:26 +02:00
parent 6f6bd5819b
commit 5df2c32614
5 changed files with 393 additions and 0 deletions

View file

@ -0,0 +1,5 @@
from .websocket_server import WebSocketServer
def tray_init(tray_widget, main_widget):
return WebSocketServer()

View file

@ -0,0 +1,46 @@
import asyncio
from pype.api import Logger
from wsrpc_aiohttp import WebSocketRoute
log = Logger().get_logger("WebsocketServer")
class ExternalApp1(WebSocketRoute):
"""
One route, mimicking external application (like Harmony, etc).
All functions could be called from client.
'do_notify' function calls function on the client - mimicking
notification after long running job on the server or similar
"""
def init(self, **kwargs):
# Python __init__ must be return "self".
# This method might return anything.
log.debug("someone called ExternalApp1 route")
return kwargs
async def server_function_one(self):
log.info('In function one')
async def server_function_two(self):
log.info('In function two')
return 'function two'
async def server_function_three(self):
log.info('In function three')
asyncio.ensure_future(self.do_notify())
return '{"message":"function tree"}'
async def server_function_four(self, *args, **kwargs):
log.info('In function four args {} kwargs {}'.format(args, kwargs))
ret = dict(**kwargs)
ret["message"] = "function four received arguments"
return str(ret)
# This method calls function on the client side
async def do_notify(self):
import time
time.sleep(5)
log.info('Calling function on server after delay')
awesome = 'Somebody server_function_three method!'
await self.socket.call('notify', result=awesome)

View file

@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- CSS only -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
<script type="text/javascript" src="//unpkg.com/@wsrpc/client"></script>
<script>
WSRPC.DEBUG = true;
WSRPC.TRACE = true;
var url = (window.location.protocol==="https):"?"wss://":"ws://") + window.location.host + '/ws/';
url = 'ws://localhost:8099/ws/';
RPC = new WSRPC(url, 5000);
console.log(RPC.state());
// Configure client API, that can be called from server
RPC.addRoute('notify', function (data) {
console.log('Server called client route "notify":', data);
alert('Server called client route "notify":', data)
return data.result;
});
RPC.connect();
console.log(RPC.state());
$(document).ready(function() {
function NoReturn(){
// Call stateful route
// After you call that route, server would execute 'notify' route on the
// client, that is registered above.
RPC.call('ExternalApp1.server_function_one').then(function (data) {
console.log('Result for calling server route "server_function_one": ', data);
alert('Function "server_function_two" returned: '+data);
}, function (error) {
alert(error);
});
}
function ReturnValue(){
// Call stateful route
// After you call that route, server would execute 'notify' route on the
// client, that is registered above.
RPC.call('ExternalApp1.server_function_two').then(function (data) {
console.log('Result for calling server route "server_function_two": ', data);
alert('Function "server_function_two" returned: '+data);
}, function (error) {
alert(error);
});
}
function ValueAndNotify(){
// After you call that route, server would execute 'notify' route on the
// client, that is registered above.
RPC.call('ExternalApp1.server_function_three').then(function (data) {
console.log('Result for calling server route "server_function_three": ', data);
alert('Function "server_function_three" returned: '+data);
}, function (error) {
alert(error);
});
}
function SendValue(){
// After you call that route, server would execute 'notify' route on the
// client, that is registered above.
RPC.call('ExternalApp1.server_function_four', {foo: 'one', bar:'two'}).then(function (data) {
console.log('Result for calling server route "server_function_four": ', data);
alert('Function "server_function_four" returned: '+data);
}, function (error) {
alert(error);
});
}
$('#noReturn').click(function() {
NoReturn();
})
$('#returnValue').click(function() {
ReturnValue();
})
$('#valueAndNotify').click(function() {
ValueAndNotify();
})
$('#sendValue').click(function() {
SendValue();
})
})
<!-- // Call stateless method-->
<!-- RPC.call('test2').then(function (data) {-->
<!-- console.log('Result for calling server route "test2"', data);-->
<!-- });-->
</script>
</head>
<body>
<div class="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm">
<h5 class="my-0 mr-md-auto font-weight-normal">Test of wsrpc javascript client</h5>
</div>
<div class="container">
<div class="card-deck mb-3 text-center">
<div class="card mb-4 shadow-sm">
<div class="card-header">
<h4 class="my-0 font-weight-normal">No return value</h4>
</div>
<div class="card-body">
<ul class="list-unstyled mt-3 mb-4">
<li>Calls server_function_one</li>
<li>Function only logs on server</li>
<li>No return value</li>
<li>&nbsp;</li>
<li>&nbsp;</li>
<li>&nbsp;</li>
</ul>
<button type="button" id="noReturn" class="btn btn-lg btn-block btn-outline-primary">Call server</button>
</div>
</div>
<div class="card mb-4 shadow-sm">
<div class="card-header">
<h4 class="my-0 font-weight-normal">Return value</h4>
</div>
<div class="card-body">
<ul class="list-unstyled mt-3 mb-4">
<li>Calls server_function_two</li>
<li>Function logs on server</li>
<li>Returns simple text value</li>
<li>&nbsp;</li>
<li>&nbsp;</li>
<li>&nbsp;</li>
</ul>
<button type="button" id="returnValue" class="btn btn-lg btn-block btn-outline-primary">Call server</button>
</div>
</div>
<div class="card mb-4 shadow-sm">
<div class="card-header">
<h4 class="my-0 font-weight-normal">Notify</h4>
</div>
<div class="card-body">
<ul class="list-unstyled mt-3 mb-4">
<li>Calls server_function_three</li>
<li>Function logs on server</li>
<li>Returns json payload </li>
<li>Server then calls function ON the client after delay</li>
<li>&nbsp;</li>
</ul>
<button type="button" id="valueAndNotify" class="btn btn-lg btn-block btn-outline-primary">Call server</button>
</div>
</div>
<div class="card mb-4 shadow-sm">
<div class="card-header">
<h4 class="my-0 font-weight-normal">Send value</h4>
</div>
<div class="card-body">
<ul class="list-unstyled mt-3 mb-4">
<li>Calls server_function_four</li>
<li>Function logs on server</li>
<li>Returns modified sent values</li>
<li>&nbsp;</li>
<li>&nbsp;</li>
<li>&nbsp;</li>
</ul>
<button type="button" id="sendValue" class="btn btn-lg btn-block btn-outline-primary">Call server</button>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,34 @@
import asyncio
from wsrpc_aiohttp import WSRPCClient
"""
Simple testing Python client for wsrpc_aiohttp
Calls sequentially multiple methods on server
"""
loop = asyncio.get_event_loop()
async def main():
print("main")
client = WSRPCClient("ws://127.0.0.1:8099/ws/",
loop=asyncio.get_event_loop())
client.add_route('notify', notify)
await client.connect()
print("connected")
print(await client.proxy.ExternalApp1.server_function_one())
print(await client.proxy.ExternalApp1.server_function_two())
print(await client.proxy.ExternalApp1.server_function_three())
print(await client.proxy.ExternalApp1.server_function_four(foo="one"))
await client.close()
def notify(socket, *args, **kwargs):
print("called from server")
if __name__ == "__main__":
# loop.run_until_complete(main())
asyncio.run(main())

View file

@ -0,0 +1,129 @@
from pype.api import config, Logger
from Qt import QtCore
from aiohttp import web, WSCloseCode
import asyncio
import weakref
from wsrpc_aiohttp import STATIC_DIR, WebSocketAsync
from . import external_app_1
log = Logger().get_logger("WebsocketServer")
class WebSocketServer():
"""
Basic POC implementation of asychronic websocket RPC server.
Uses class in external_app_1.py to mimic implementation for single
external application.
'test_client' folder contains two test implementations of client
WIP
"""
def __init__(self):
self.qaction = None
self.failed_icon = None
self._is_running = False
default_port = 8099
try:
self.presets = config.get_presets()["services"]["websocket_server"]
except Exception:
self.presets = {"default_port": default_port, "exclude_ports": []}
log.debug((
"There are not set presets for WebsocketServer."
" Using defaults \"{}\""
).format(str(self.presets)))
self.app = web.Application()
self.app["websockets"] = weakref.WeakSet()
self.app.router.add_route("*", "/ws/", WebSocketAsync)
self.app.router.add_static("/js", STATIC_DIR)
self.app.router.add_static("/", ".")
# add route with multiple methods for single "external app"
WebSocketAsync.add_route('ExternalApp1', external_app_1.ExternalApp1)
self.app.on_shutdown.append(self.on_shutdown)
self.websocket_thread = WebsocketServerThread(self, default_port)
def add_routes_for_class(self, cls):
''' Probably obsolete, use classes inheriting from WebSocketRoute '''
methods = [method for method in dir(cls) if '__' not in method]
log.info("added routes for {}".format(methods))
for method in methods:
WebSocketAsync.add_route(method, getattr(cls, method))
def tray_start(self):
self.websocket_thread.start()
# log.info("Starting websocket server")
# loop = asyncio.get_event_loop()
# self.runner = web.AppRunner(self.app)
# loop.run_until_complete(self.runner.setup())
# self.site = web.TCPSite(self.runner, 'localhost', 8044)
# loop.run_until_complete(self.site.start())
# log.info('site {}'.format(self.site._server))
# asyncio.ensure_future()
# #loop.run_forever()
# #web.run_app(self.app, port=8044)
# log.info("Started websocket server")
@property
def is_running(self):
return self.websocket_thread.is_running
def stop(self):
self.websocket_thread.is_running = False
def thread_stopped(self):
self._is_running = False
async def on_shutdown(self):
"""
Gracefully remove all connected websocket connections
:return: None
"""
log.info('Shutting down websocket server')
for ws in set(self.app['websockets']):
await ws.close(code=WSCloseCode.GOING_AWAY,
message='Server shutdown')
class WebsocketServerThread(QtCore.QThread):
""" Listener for websocket rpc requests.
It would be probably better to "attach" this to main thread (as for
example Harmony needs to run something on main thread), but currently
it creates separate thread and separate asyncio event loop
"""
def __init__(self, module, port):
super(WebsocketServerThread, self).__init__()
self.is_running = False
self.port = port
self.module = module
def run(self):
self.is_running = True
try:
log.debug(
"Running Websocket server on URL:"
" \"ws://localhost:{}\"".format(self.port)
)
log.info("Starting websocket server")
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
web.run_app(self.module.app, port=self.port) # blocking
log.info("Started websocket server")
except Exception:
log.warning(
"Websocket Server service has failed", exc_info=True
)
self.is_running = False
self.module.thread_stopped()