class ComponentBase(object): """A base class for WAMP components This class provides a template to turn an asynchronous Modbus client into a WAMP client using the Autobahn package. Provided you have an API client derived from :class:`modbusclient.asyncio.ApiWrapper`, you can use this class as base class for your WAMP client and provice the API client as `client` argument. This avoids the repetition of some boiler plate code. Arguments: transports (list or str): Passed verbatim to :class:`autobahn.asyncio.component.Component` as `transports`. realm (str): WAMP realm. Passed verbatim to :class:`autobahn.asyncio.component.Component` as `realm`. client (:class:`modbusclient.asyncio.ApiWrapper`): An implementation of an API wrapper. Attributes: component (:class:`autobahn.asyncio.component.Component`): Autobahn WAMP component wrapped by this class session (:class:`autobahn.wamp.protocol.ApplicationSession`): Session passed during on_join. ``None`` if client has not joined any session. """ def __init__(self, transports, realm, client): self._component = Component(transports=transports, realm=realm) self._client = client self._component.on('join', self._join) self._component.on('leave', self._leave) self._session = None @property def component(self): """Get component wrapped by this instance Return: :class:`autobahn.asyncio.component.Component`: Autobahn WAMP component wrapped by this class """ return self._component @property def session(self): """Get currently joined session Return: :class:`autobahn.wamp.protocol.ApplicationSession`: Session passed during on_join. ``None`` if client is currently not joined to any session. """ return self._session async def _join(self, session, details): """Call back invoked when joining (on_join) Sets the internal session member variable and prints an info message. Arguments: session (:class:`autobahn.wamp.protocol.ApplicationSession`): Application session. details (dict): Dictionary with details. """ self._session = session self.info('Joined session {session}: {details}', session=session, details=details) async def _leave(self, session, reason): self.info("Disconnecting from session {session}. Reason: {reason}", session=session, reason=reason) self._session = None def debug(self, msg, **kwargs): """Create debug level log message Arguments: msg (str): Log message. **kwargs: Keyword arguments passed to message formatter """ self._component.log.debug(msg, **kwargs) def info(self, msg, **kwargs): """Create info level log message Arguments: msg (str): Log message. **kwargs: Keyword arguments passed to message formatter """ self._component.log.info(msg, **kwargs) def warning(self, msg, **kwargs): """Create warning level log message Arguments: msg (str): Log message. **kwargs: Keyword arguments passed to message formatter """ self._component.log.warning(msg, **kwargs) def error(self, msg, **kwargs): """Create error level log message Arguments: msg (str): Log message. **kwargs: Keyword arguments passed to message formatter """ self._component.log.error(msg, **kwargs)
class API(QtCore.QObject, Utils.AsyncSlotObject): #Class to handle the WAMP connection to the OCP node def __init__(self, node, logger): QtCore.QObject.__init__(self) self.__node = node self.__wamp = None self.__session = None self.__readyEvent = asyncio.Event() self.__registered = {} # key: [(args, kwargs)] self.__registeredSessions = {} # key: [sessions] self.__subscribed = {} self.__subscribedSessions = {} self.__logger = logger self.__settings = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod").GetGroup("Collaboration") # connect to node ready events for auto reconnect self.__node.runningChanged.connect(self.__nodeChange) async def waitTillReady(self): await self.__readyEvent.wait() async def connectToNode(self): self.__logger.debug(f"Try to connect") if not self.__node.running: raise Exception("Cannot connect API if node is not running") if self.connected: return #make the OCP node connection! uri = f"ws://{self.__node.apiUri}:{self.__node.apiPort}/ws" self.__wamp = Component( transports={ "url":uri, "serializers": ['msgpack'], "initial_retry_delay": 10 }, realm = "ocp") self.__wamp.on('join', self.__onJoin) self.__wamp.on('leave', self.__onLeave) self.__wamp.on('disconnect', self.__onDisconnect) self.__wamp.on('ready', self.__onReady) #run the component self.__wamp.start() async def disconnectFromNode(self): # close the connection self.__logger.debug(f"Try to disconnect") if self.__wamp: await self.__wamp.stop() self.__wamp = None async def register(self, key, *args, **kwargs): # Registers an API function. It stays registered over multiple session and reconnects # Can be unregistered with the given key. Note: multiple register and subscribe calls can be # made with a single key self.__registered[key] = self.__registered.get(key, []) + [(args, kwargs)] if self.connected: try: self.__registeredSessions[key] = self.__registeredSessions.get(key, []) + [await self.__session.register(*args, **kwargs)] except: pass async def subscribe(self, key, *args, **kwargs): # Subscribe to API event. It stays subscribed over multiple session and reconnects self.__subscribed[key] = self.__subscribed.get(key, []) + [(args, kwargs)] if self.connected: try: self.__subscribedSessions[key] = self.__subscribedSessions.get(key, []) + [await self.__session.subscribe(*args, **kwargs)] except: pass async def closeKey(self, key): if key in self.__registered: #remove register entries and close sessions del self.__registered[key] for session in self.__registeredSessions.pop(key, []): await session.unregister() if key in self.__subscribed: #remove subscribe entries and close sessions del self.__subscribed[key] for session in self.__subscribedSessions.pop(key, []): await session.unsubscribe() async def call(self, *args, **kwargs): # calls api function if not self.connected: raise Exception("Not connected to Node, cannot call API function") # add a default timeout if the caller did no do already if "options" in kwargs: opts = kwargs["options"] if not opts.timeout: opts.timeout = 5000 else: kwargs["options"] = wamp.CallOptions(timeout=5000) # call return await self.__session.call(*args, **kwargs) # Node callbacks # ******************************************************************************************** @asyncSlot() async def __nodeChange(self): self.__logger.debug(f"Node change callback, node running: {self.__node.running}") if self.reconnect and self.__node.running: await self.connectToNode() if self.__wamp and not self.__node.running: await self.disconnectFromNode() # Wamp callbacks # ******************************************************************************************** async def __onJoin(self, session, details): self.__logger.debug(f"Join WAMP session") self.__session = session # register all functions for key, argsList in self.__registered.items(): sessions = [] for args in argsList: sessions.append(await self.__session.register(*(args[0]), **(args[1]))) self.__registeredSessions[key] = sessions # subscribe to all events for key, argsList in self.__subscribed.items(): sessions = [] for args in argsList: sessions.append(await self.__session.subscribe(*(args[0]), **(args[1]))) self.__subscribedSessions[key] = sessions async def __onLeave(self, session, reason): self.__logger.debug(f"Leave WAMP session: {reason}") # clear all registered and subscribed session objects self.__registeredSessions = {} self.__subscribedSessions = {} self.__readyEvent.clear() self.__session = None self.connectedChanged.emit() async def __onDisconnect(self, *args, **kwargs): self.__logger.info("API closed") async def __onReady(self, *args): self.connectedChanged.emit() self.__readyEvent.set() self.__logger.info("API ready") # Qt Property/Signal API used from the UI # ******************************************************************************************** #signals for property change (needed to have QML update on property change) connectedChanged = QtCore.Signal() __reconnectChanged = QtCore.Signal() @QtCore.Property(bool, notify=connectedChanged) def connected(self): return self.__session != None def getReconnect(self): return self.__settings.GetBool("APIReconnect", True) QtCore.Slot(bool) def setReconnect(self, value): self.__settings.SetBool("APIReconnect", value) self.__reconnectChanged.emit() reconnect = QtCore.Property(bool, getReconnect, setReconnect, notify=__reconnectChanged) @Utils.AsyncSlot() async def toggleConnectedSlot(self): if self.connected: await self.disconnectFromNode() else: await self.connectToNode() await self.waitTillReady()
class Handler(): def __init__(self, connection, manager): if os.getenv('OCP_TEST_RUN', "0") != "1": raise Exception("Test Handler created, but test environment variable not set") self.__manager = manager self.__session = None self.__logger = logging.getLogger("Test handler") #register the Error raise filter to ensure that during testing all error messages lead to test stop #Note: Attach to handler, as adding to logger itself does not propagate to child loggers #logging.getLogger().handlers[0].addFilter(ErrorFilter()) # set the test loglevel loglevel = os.getenv('OCP_TEST_LOG_LEVEL', "Warn") if loglevel == "Warn": loglevel = logging.WARN elif loglevel == "Error": loglevel = logging.ERROR elif loglevel == "Info": loglevel = logging.INFO elif loglevel == "Debug": loglevel = logging.DEBUG elif loglevel == "Trace": loglevel = logging.DEBUG logging.getLogger().setLevel(loglevel) #run the handler asyncio.ensure_future(self._startup(connection)) async def _startup(self, connection): self.__connection = connection await connection.api.waitTillReady() #register ourself to the OCP node await connection.api.subscribe("testhandler", self.__receiveSync, "ocp.documents..content.Document.sync", options=SubscribeOptions(match="wildcard")) #connect to testserver uri = os.getenv('OCP_TEST_SERVER_URI', '') self.test = Component(transports=uri, realm = "ocptest") self.test.on('join', self.__onJoin) self.test.on('leave', self.__onLeave) self.test.start() async def __onJoin(self, session, details): #little remark that we joined (needed for test executable, it waits for this) FreeCAD.Console.PrintMessage("Connection to OCP test server established\n") #store the session for later use self.__session = session #get all the testing functions in this class! methods = [func for func in dir(self) if callable(getattr(self, func))] rpcs = [rpc for rpc in methods if '_rpc' in rpc] #build the correct uri uri = os.getenv('OCP_TEST_RPC_ADDRESS', '') if uri == '': raise ('No rpc uri set for testing') for rpc in rpcs: rpc_uri = uri + "." + rpc[len('_rpc'):] await session.register(getattr(self, rpc), rpc_uri) #inform test framework that we are set up! try: await session.call("ocp.test.triggerEvent", uri, True) except Exception as e: print("Exception in event call: ", str(e)) async def __onLeave(self, session, reason): self.__session = None #inform test framework that we are not here anymore! await session.call("ocp.test.triggerEvent", os.getenv('OCP_TEST_RPC_ADDRESS', ''), False) async def waitTillCloseout(self, docId, timeout=30): # wait till all tasks in the document with given ID are finished try: for entity in self.__manager.getEntities(): if entity.onlinedoc != None and entity.id == docId: await entity.onlinedoc.waitTillCloseout(timeout) self.__logger.debug(f"Closeout finished for {docId}") return True return False except Exception as e: self.__logger.error(f"Trigger synchronize failed, cannot wait for closeout of current actions: {e}") return False async def synchronize(self, docId, numFCs): #Synchronize all other involved FC instances. When this returns one can call waitForSync on the TestServer. The numFCs must not #include the FC instance it is called on, only the remaining ones #Note: # We trigger the FC instances to sync themself via the DML doc. This is to make sure that the sync is called only after all # operations have been received by the FC instance. # 1. Do all known operations # 2. Emit sync event in DML document self.__logger.debug(f"Start syncronisation for: {docId[-5:]}") #we wait till all tasks are finished if not await self.waitTillCloseout(docId): return #register the sync with the testserver await self.__session.call("ocp.test.registerSync", docId, numFCs) #and now issue the event that all FC instances know that they should sync. self.__logger.debug(f"Work done, trigger sync events via dml: {docId[-5:]}") uri = f"ocp.documents.{docId}.content.Document.sync" await self.__connection.api.call(uri, docId) async def __receiveSync(self, docId): #received a sync event from DML document # 1. Wait till all actions are finished # 2. Inform the TestServer, that we are finished self.__logger.debug(f"Request for sync received: {docId[-5:]}") #await asyncio.sleep(0.05) #wait till everything is done! try: entities = self.__manager.getEntities() for entity in entities: if entity.onlinedoc and entity.id == docId: await entity.onlinedoc.waitTillCloseout(30) except Exception as e: print(f"Participation in synchronize failed, cannot wait for closeout of current actions: {e}") return #call testserver that we received and executed the sync! self.__logger.debug("Work done, send sync event") await self.__session.call("ocp.test.sync", docId) async def _rpcShareDocument(self, name): pass async def _rpcUnshareDocument(self, name): pass async def _rpcAddNodeToDocument(self): pass async def _rpcExecuteCode(self, code): code.insert(0, "import FreeCADGui as Gui") code.insert(0, "import FreeCAD as App") exec( f'async def __ex(): ' + ''.join(f'\n {line}' for line in code) ) return await locals()['__ex']()
class App: def __init__(self, config_file): self.config_file = config_file self.config = self.load_config(self.config_file) self.database = DatabaseWorker(filename='db.sqlite') self.messages = MessageService(self.database) self.webapp = create_webapp(self.config['web'], self.messages) # WAMP self.channel_handlers = {} wamp_config = self.config['wamp'] self.wamp_session = None self.wamp_comp = Component( transports=wamp_config['router'], realm=wamp_config['realm'] ) #self.wamp_comp.log = _TxaioLogWrapper(logger.getChild('wamp')) self.wamp_comp.on('join', self.initialize_wamp) self.wamp_comp.on('leave', self.uninitialize_wamp) @staticmethod def load_config(file): if isinstance(file, (str, Path)): file = open(file) with file: config = toml.load(file) return config def run(self): config = self.config logging.config.dictConfig(config['logging']) logging.captureWarnings(True) logger.info('Logging configured!') # Start database worker thread self.database.start() loop = asyncio.get_event_loop() try: return loop.run_until_complete(self.main_loop()) except KeyboardInterrupt: logger.warning("KeyboardInterrupt") return 130 finally: logger.info("Cleaning up") loop.run_until_complete(loop.shutdown_asyncgens()) loop.close() async def main_loop(self): channels = await self.database.get_channels() self.channel_handlers = {c['channel']: c['handler'] for c in channels} logger.info("Channel Handlers: %s", self.channel_handlers) runner = await start_webapp(self.webapp, self.config['web']) await self.wamp_comp.start() # running = True #while running: # logger.info("Running in main loop") # await asyncio.sleep(60) await runner.cleanup() async def initialize_wamp(self, session, details): logger.info("Connected to WAMP router: %s", details) self.wamp_session = session # Setup messages self.messages.publish = self.publish_wamp self.messages.handlers = { name: partial(self.wamp_session.call, handler) for name, handler in self.channel_handlers.items() } self.messages.handlers['echo'] = self.echo async def uninitialize_wamp(self, session, reason): logger.info("%s %s", session, reason) logger.info("Lost WAMP connection") self.wamp_session = None self.messages.publish = None self.messages.handlers = {} async def echo(self, content, channel, time, meta): return dict(content=content, channel=channel, time=time, meta=meta) def publish_wamp(self, channel, content, meta, time): self.wamp_session.publish(f'webhooks.{channel}', content, channel=channel, meta=meta, time=time)