mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
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:
parent
6f6bd5819b
commit
5df2c32614
5 changed files with 393 additions and 0 deletions
5
pype/modules/websocket_server/__init__.py
Normal file
5
pype/modules/websocket_server/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .websocket_server import WebSocketServer
|
||||
|
||||
|
||||
def tray_init(tray_widget, main_widget):
|
||||
return WebSocketServer()
|
||||
46
pype/modules/websocket_server/external_app_1.py
Normal file
46
pype/modules/websocket_server/external_app_1.py
Normal 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)
|
||||
179
pype/modules/websocket_server/test_client/wsrpc_client.html
Normal file
179
pype/modules/websocket_server/test_client/wsrpc_client.html
Normal 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> </li>
|
||||
<li> </li>
|
||||
<li> </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> </li>
|
||||
<li> </li>
|
||||
<li> </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> </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> </li>
|
||||
<li> </li>
|
||||
<li> </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>
|
||||
34
pype/modules/websocket_server/test_client/wsrpc_client.py
Normal file
34
pype/modules/websocket_server/test_client/wsrpc_client.py
Normal 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())
|
||||
129
pype/modules/websocket_server/websocket_server.py
Normal file
129
pype/modules/websocket_server/websocket_server.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue