From 5df2c326142b5434339e3b56d9887cc8816bee2f Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Fri, 3 Jul 2020 17:16:26 +0200 Subject: [PATCH] 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"] } --- pype/modules/websocket_server/__init__.py | 5 + .../websocket_server/external_app_1.py | 46 +++++ .../test_client/wsrpc_client.html | 179 ++++++++++++++++++ .../test_client/wsrpc_client.py | 34 ++++ .../websocket_server/websocket_server.py | 129 +++++++++++++ 5 files changed, 393 insertions(+) create mode 100644 pype/modules/websocket_server/__init__.py create mode 100644 pype/modules/websocket_server/external_app_1.py create mode 100644 pype/modules/websocket_server/test_client/wsrpc_client.html create mode 100644 pype/modules/websocket_server/test_client/wsrpc_client.py create mode 100644 pype/modules/websocket_server/websocket_server.py diff --git a/pype/modules/websocket_server/__init__.py b/pype/modules/websocket_server/__init__.py new file mode 100644 index 0000000000..eb5a0d9f27 --- /dev/null +++ b/pype/modules/websocket_server/__init__.py @@ -0,0 +1,5 @@ +from .websocket_server import WebSocketServer + + +def tray_init(tray_widget, main_widget): + return WebSocketServer() diff --git a/pype/modules/websocket_server/external_app_1.py b/pype/modules/websocket_server/external_app_1.py new file mode 100644 index 0000000000..34a43a4d23 --- /dev/null +++ b/pype/modules/websocket_server/external_app_1.py @@ -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) diff --git a/pype/modules/websocket_server/test_client/wsrpc_client.html b/pype/modules/websocket_server/test_client/wsrpc_client.html new file mode 100644 index 0000000000..9c3f469aca --- /dev/null +++ b/pype/modules/websocket_server/test_client/wsrpc_client.html @@ -0,0 +1,179 @@ + + + + + Title + + + + + + + + + + + + + +
+
Test of wsrpc javascript client
+ +
+ +
+
+
+
+

No return value

+
+
+
    +
  • Calls server_function_one
  • +
  • Function only logs on server
  • +
  • No return value
  • +
  •  
  • +
  •  
  • +
  •  
  • +
+ +
+
+
+
+

Return value

+
+
+
    +
  • Calls server_function_two
  • +
  • Function logs on server
  • +
  • Returns simple text value
  • +
  •  
  • +
  •  
  • +
  •  
  • +
+ +
+
+
+
+

Notify

+
+
+
    +
  • Calls server_function_three
  • +
  • Function logs on server
  • +
  • Returns json payload
  • +
  • Server then calls function ON the client after delay
  • +
  •  
  • +
+ +
+
+
+
+

Send value

+
+
+
    +
  • Calls server_function_four
  • +
  • Function logs on server
  • +
  • Returns modified sent values
  • +
  •  
  • +
  •  
  • +
  •  
  • +
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/pype/modules/websocket_server/test_client/wsrpc_client.py b/pype/modules/websocket_server/test_client/wsrpc_client.py new file mode 100644 index 0000000000..ef861513ae --- /dev/null +++ b/pype/modules/websocket_server/test_client/wsrpc_client.py @@ -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()) diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py new file mode 100644 index 0000000000..ce5f23180a --- /dev/null +++ b/pype/modules/websocket_server/websocket_server.py @@ -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()