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
+
+
+
+
+
+
+
+
+
+ - Calls server_function_one
+ - Function only logs on server
+ - No return value
+ -
+ -
+ -
+
+
+
+
+
+
+
+
+ - Calls server_function_two
+ - Function logs on server
+ - Returns simple text value
+ -
+ -
+ -
+
+
+
+
+
+
+
+
+ - Calls server_function_three
+ - Function logs on server
+ - Returns json payload
+ - Server then calls function ON the client after delay
+ -
+
+
+
+
+
+
+
+
+ - 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()