import os import sys import pyclbr import importlib import urllib import threading import six from pype.lib import PypeLogger from .. import PypeModule, ITrayService if six.PY2: web = asyncio = STATIC_DIR = WebSocketAsync = None else: from aiohttp import web import asyncio from wsrpc_aiohttp import STATIC_DIR, WebSocketAsync log = PypeLogger().get_logger("WebsocketServer") class WebsocketModule(PypeModule, ITrayService): name = "Websocket server" label = "Websocket server" def initialize(self, module_settings): if asyncio is None: raise AssertionError( "WebSocketServer module requires Python 3.5 or higher." ) self.enabled = True self.websocket_server = None def connect_with_modules(self, *_a, **kw): return def tray_init(self): self.websocket_server = WebSocketServer() self.websocket_server.on_stop_callbacks.append( self.set_service_failed_icon ) def tray_start(self): if self.websocket_server: self.websocket_server.module_start() def tray_exit(self): if self.websocket_server: self.websocket_server.module_stop() 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 """ _instance = None def __init__(self): WebSocketServer._instance = self self.client = None self.handlers = {} self.on_stop_callbacks = [] port = None websocket_url = os.getenv("WEBSOCKET_URL") if websocket_url: parsed = urllib.parse.urlparse(websocket_url) port = parsed.port if not port: port = 8098 # fallback self.app = web.Application() 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" directories_with_routes = ['hosts'] self.add_routes_for_directories(directories_with_routes) self.websocket_thread = WebsocketServerThread(self, port) def module_start(self): if self.websocket_thread: self.websocket_thread.start() def module_stop(self): if self.websocket_thread: self.websocket_thread.stop() def add_routes_for_directories(self, directories_with_routes): """ Loops through selected directories to find all modules and in them all classes implementing 'WebSocketRoute' that could be used as route. All methods in these classes are registered automatically. """ for dir_name in directories_with_routes: dir_name = os.path.join(os.path.dirname(__file__), dir_name) for file_name in os.listdir(dir_name): if '.py' in file_name and '__' not in file_name: self.add_routes_for_module(file_name, dir_name) def add_routes_for_module(self, file_name, dir_name): """ Auto routes for all classes implementing 'WebSocketRoute' in 'file_name' in 'dir_name' """ module_name = file_name.replace('.py', '') module_info = pyclbr.readmodule(module_name, [dir_name]) for class_name, cls_object in module_info.items(): sys.path.append(dir_name) if 'WebSocketRoute' in cls_object.super: log.debug('Adding route for {}'.format(class_name)) module = importlib.import_module(module_name) cls = getattr(module, class_name) WebSocketAsync.add_route(class_name, cls) sys.path.pop() def call(self, func): log.debug("websocket.call {}".format(func)) future = asyncio.run_coroutine_threadsafe(func, self.websocket_thread.loop) result = future.result() return result def get_client(self): """ Return first connected client to WebSocket TODO implement selection by Route :return: client """ clients = WebSocketAsync.get_clients() client = None if len(clients) > 0: key = list(clients.keys())[0] client = clients.get(key) return client @staticmethod def get_instance(): if WebSocketServer._instance is None: WebSocketServer() return WebSocketServer._instance def stop_websocket_server(self): self.stop() @property def is_running(self): return self.websocket_thread.is_running def stop(self): if not self.is_running: return try: log.debug("Stopping websocket server") self.websocket_thread.is_running = False self.websocket_thread.stop() except Exception: log.warning( "Error has happened during Killing websocket server", exc_info=True ) def thread_stopped(self): for callback in self.on_stop_callbacks: callback() class WebsocketServerThread(threading.Thread): """ 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): if asyncio is None: raise AssertionError( "WebSocketServer module requires Python 3.5 or higher." ) super(WebsocketServerThread, self).__init__() self.is_running = False self.port = port self.module = module self.loop = None self.runner = None self.site = None self.tasks = [] def run(self): self.is_running = True try: log.info("Starting websocket server") self.loop = asyncio.new_event_loop() # create new loop for thread asyncio.set_event_loop(self.loop) self.loop.run_until_complete(self.start_server()) log.debug( "Running Websocket server on URL:" " \"ws://localhost:{}\"".format(self.port) ) asyncio.ensure_future(self.check_shutdown(), loop=self.loop) self.loop.run_forever() except Exception: log.warning( "Websocket Server service has failed", exc_info=True ) finally: self.loop.close() # optional self.is_running = False self.module.thread_stopped() log.info("Websocket server stopped") async def start_server(self): """ Starts runner and TCPsite """ self.runner = web.AppRunner(self.module.app) await self.runner.setup() self.site = web.TCPSite(self.runner, 'localhost', self.port) await self.site.start() def stop(self): """Sets is_running flag to false, 'check_shutdown' shuts server down""" self.is_running = False async def check_shutdown(self): """ Future that is running and checks if server should be running periodically. """ while self.is_running: while self.tasks: task = self.tasks.pop(0) log.debug("waiting for task {}".format(task)) await task log.debug("returned value {}".format(task.result)) await asyncio.sleep(0.5) log.debug("Starting shutdown") await self.site.stop() log.debug("Site stopped") await self.runner.cleanup() log.debug("Runner stopped") tasks = [task for task in asyncio.all_tasks() if task is not asyncio.current_task()] list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks results = await asyncio.gather(*tasks, return_exceptions=True) log.debug(f'Finished awaiting cancelled tasks, results: {results}...') await self.loop.shutdown_asyncgens() # to really make sure everything else has time to stop await asyncio.sleep(0.07) self.loop.stop()