def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, daemon=None) -> None: super().__init__(group=group, target=target, name=name, args=args, kwargs=kwargs, daemon=daemon) self.args = args self.kwargs = kwargs def joined(session, details): print("session ready") self.mySession = session comp = Component(transports="ws://host.docker.internal:8090/ws", realm=u"racelog.state") comp.on_join(joined) comp.start(loop=self)
class Publisher: def __init__(self): self.component = None async def connect(self, crossbar_url): """Method used to initialize a session for the publisher :param crossbar_url: url hosting the crossbar instance """ log.debug(f"Connecting to {crossbar_url}") self.component = Component(transports=crossbar_url, realm='realm1') loop = asyncio.get_event_loop() txaio.config.loop = loop session_ready = asyncio.Event() async def setup_session(created_session, _details): """Callback method used to retrieve a session and notify interested parties with an event""" self.session = created_session nonlocal session_ready session_ready.set() self.component.start(loop=loop) self.component.on_join(setup_session) await session_ready.wait() def publish(self, topic, payload): """Publish the payload on the specified topic :param topic: target topic :param payload: payload to be published """ self.session.publish(topic, payload) log.debug(f"data is published to : {topic}")
def test_asyncio_component(event_loop): orig_loop = txaio.config.loop txaio.config.loop = event_loop comp = Component( transports=[ { "url": "ws://localhost:12/bogus", "max_retries": 1, "max_retry_delay": 0.1, } ] ) # if having trouble, try starting some logging (and use # "py.test -s" to get real-time output) # txaio.start_logging(level="debug") f = comp.start(loop=event_loop) txaio.config.loop = event_loop finished = txaio.create_future() def fail(): finished.set_exception(AssertionError("timed out")) txaio.config.loop = orig_loop txaio.call_later(4.0, fail) def done(f): try: f.result() finished.set_exception(AssertionError("should get an error")) except RuntimeError as e: if 'Exhausted all transport connect attempts' not in str(e): finished.set_exception(AssertionError("wrong exception caught")) finished.set_result(None) txaio.config.loop = orig_loop assert comp._done_f is None f.add_done_callback(done) return finished
def test_asyncio_component_404(event_loop): """ If something connects but then gets aborted, it should still try to re-connect (in real cases this could be e.g. wrong path, TLS failure, WebSocket handshake failure, etc) """ orig_loop = txaio.config.loop txaio.config.loop = event_loop class FakeTransport(object): def close(self): pass def write(self, data): pass fake_transport = FakeTransport() actual_protocol = [None] # set in a closure below def create_connection(protocol_factory=None, server_hostname=None, host=None, port=None, ssl=False): if actual_protocol[0] is None: protocol = protocol_factory() actual_protocol[0] = protocol protocol.connection_made(fake_transport) return txaio.create_future_success((fake_transport, protocol)) else: return txaio.create_future_error(RuntimeError("second connection fails completely")) with mock.patch.object(event_loop, 'create_connection', create_connection): event_loop.create_connection = create_connection comp = Component( transports=[ { "url": "ws://localhost:12/bogus", "max_retries": 1, "max_retry_delay": 0.1, } ] ) # if having trouble, try starting some logging (and use # "py.test -s" to get real-time output) # txaio.start_logging(level="debug") f = comp.start(loop=event_loop) txaio.config.loop = event_loop # now that we've started connecting, we *should* be able # to connetion_lost our transport .. but we do a # call-later to ensure we're after the setup stuff in the # event-loop (because asyncio doesn't synchronously # process already-completed Futures like Twisted does) def nuke_transport(): actual_protocol[0].connection_lost(None) # asyncio can call this with None txaio.call_later(0.1, nuke_transport) finished = txaio.create_future() def fail(): finished.set_exception(AssertionError("timed out")) txaio.config.loop = orig_loop txaio.call_later(1.0, fail) def done(f): try: f.result() finished.set_exception(AssertionError("should get an error")) except RuntimeError as e: if 'Exhausted all transport connect attempts' not in str(e): finished.set_exception(AssertionError("wrong exception caught")) finished.set_result(None) txaio.config.loop = orig_loop f.add_done_callback(done) return finished
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 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()
def wampConnect(wamp_conf): """WAMP connection procedures. :param wamp_conf: WAMP configuration from settings.json file """ LOG.info("WAMP connection precedures:") try: LOG.info("WAMP status @ boot:" + "\n- board = " + str(board.status) + "\n- reconnection = " + str(reconnection) + "\n- connected = " + str(connected)) # LR creates the Autobahn Asyncio Component that points to the # WAMP Agent (main/registration agent) global component component = Component(transports=wamp_conf['url'], realm=wamp_conf['realm']) # To manage the registration stage: we got the info for the main # WAMP agent and LR is going to connect to it starting the Component # with the new WAMP configuration. if connected == False and board.status == "registered" \ and reconnection == False: component.start(loop) @component.on_join async def join(session, details): """Execute the following procedures when the board connects to Crossbar. :param details: WAMP session details """ global connected connected = True # LIGHTNING-ROD STATES: # - REGISTRATION STATE: the first connection to Iotronic # - FIRST CONNECTION: the board become operative after registration # - LIGHTNING-ROD BOOT: the first connection to WAMP # after Lightning-rod starting # - WAMP RECOVERY: when the established WAMP connection fails global reconnection # reconnection flag is False when the board is: # - LIGHTNING-ROD BOOT # - REGISTRATION STATE # - FIRST CONNECTION # # reconnection flag is True when the board is: # - WAMP RECOVERY global SESSION SESSION = session # LOG.debug(" - session: " + str(details)) board.session_id = details.session LOG.info(" - Joined in realm " + board.wamp_config['realm'] + ":") LOG.info(" - WAMP Agent: " + str(board.agent)) LOG.info(" - Session ID: " + str(board.session_id)) LOG.info(" - Board status: " + str(board.status)) if reconnection is False: if board.uuid is None: ###################### # REGISTRATION STATE # ###################### # If in the LR configuration file there is not the # Board UUID specified it means the board is a new one # and it has to call IoTronic in order to complete # the registration. try: LOG.info(" - Board needs to be registered.") rpc = u'stack4things.register' with timeoutRPC(seconds=3, action=rpc): res = await session.call(rpc, code=board.code, session=board.session_id) w_msg = WM.deserialize(res) # LOG.info(" - Board registration result: \n" + # json.loads(w_msg.message, indent=4)) if w_msg.result == WM.SUCCESS: LOG.info("Registration authorized by IoTronic:\n" + str(w_msg.message)) # the 'message' field contains # the board configuration to load board.setConf(w_msg.message) # We need to disconnect the client from the # registration-agent in order to reconnect # to the WAMP agent assigned by Iotronic # at the provisioning stage LOG.info( "\n\nDisconnecting from Registration Agent " "to load new settings...\n\n") # We stop the Component in order to trigger the # onDisconnect event component.stop() else: LOG.error("Registration denied by Iotronic: " + str(w_msg.message)) Bye() except exception.ApplicationError as e: LOG.error("IoTronic registration error: " + str(e)) # Iotronic is offline the board can not call the # "stack4things.connection" RPC. The board will # disconnect from WAMP agent and retry later. # TO ACTIVE BOOT CONNECTION RECOVERY MODE reconnection = True # We stop the Component in order to trigger the # onDisconnect event component.stop() except Exception as e: LOG.warning(" - Board registration call error: " + str(e)) Bye() else: if board.status == "registered": #################### # FIRST CONNECTION # #################### # In this case we manage the first connection # after the registration stage: # Lightining-rod sets its status to "operative" # completing the provisioning and configuration stage. LOG.info("\n\n\nBoard is becoming operative...\n\n\n") board.updateStatus("operative") board.loadSettings() LOG.info("WAMP status @ firt connection:" + "\n- board = " + str(board.status) + "\n- reconnection = " + str(reconnection) + "\n- connected = " + str(connected)) await IotronicLogin(board, session, details) elif board.status == "operative": ###################### # LIGHTNING-ROD BOOT # ###################### # After join to WAMP agent, Lightning-rod will: # - authenticate to Iotronic # - load the enabled modules # The board will keep at this stage until # it will succeed to connect to Iotronic. await IotronicLogin(board, session, details) else: LOG.error("Wrong board status '" + board.status + "'.") Bye() else: ################# # WAMP RECOVERY # ################# LOG.info("IoTronic connection recovery:") try: rpc = str(board.agent) + u'.stack4things.connection' with timeoutRPC(seconds=3, action=rpc): res = await session.call(rpc, uuid=board.uuid, session=details.session) w_msg = WM.deserialize(res) if w_msg.result == WM.SUCCESS: LOG.info(" - Access granted to Iotronic.") # LOADING BOARD MODULES # If the board is in WAMP connection recovery state # we need to register again the RPCs of each module try: moduleReloadInfo(session) # Reset flag to False reconnection = False LOG.info("WAMP Session Recovered!") LOG.info("\n\nListening...\n\n") except Exception as e: LOG.warning( "WARNING - Could not reload modules: " + str(e)) Bye() else: LOG.error("Access to IoTronic denied: " + str(w_msg.message)) Bye() except exception.ApplicationError as e: LOG.error("IoTronic connection error:\n" + str(e)) # Iotronic is offline the board can not call # the "stack4things.connection" RPC. # The board will disconnect from WAMP agent and retry later # TO ACTIVE WAMP CONNECTION RECOVERY MODE reconnection = False # We stop the Component in order to trigger the # onDisconnect event component.stop() except Exception as e: LOG.warning( "Board connection error after WAMP recovery: " + str(e)) Bye() @component.on_leave async def onLeave(session, details): LOG.warning('WAMP Session Left: ' + str(details)) @component.on_disconnect async def onDisconnect(session, was_clean): """Procedure triggered on WAMP connection lost, for istance when we call component.stop(). :param connector: WAMP connector object :param reason: WAMP connection failure reason """ LOG.warning('WAMP Transport Left: was_clean = ' + str(was_clean)) global connected connected = False global reconnection LOG.info("WAMP status on disconnect:" + "\n- board = " + str(board.status) + "\n- reconnection = " + str(reconnection) + "\n- connected = " + str(connected)) if board.status == "operative" and reconnection is False: ################# # WAMP RECOVERY # ################# # we need to recover wamp session and # we set reconnection flag to True in order to activate # the RPCs module registration procedure for each module reconnection = True # LR needs to reconncet to WAMP if not connected: component.start(loop) elif board.status == "operative" and reconnection is True: ###################### # LIGHTNING-ROD BOOT # ###################### # At this stage if the reconnection flag was set to True # it means that we forced the reconnection procedure # because of the board is not able to connect to IoTronic # calling "stack4things.connection" RPC... # it means IoTronic is offline! # We need to reset the recconnection flag to False in order to # do not enter in RPCs module registration procedure... # At this stage the board tries to reconnect to # IoTronic until it will come online again. reconnection = False # LR needs to reconncet to WAMP if not connected: component.start(loop) elif (board.status == "registered"): ###################### # REGISTRATION STATE # ###################### # LR was disconnected from Registration Agent # in order to connect it to the assigned WAMP Agent. LOG.debug("\n\nReconnecting after registration...\n\n") # LR load the new configuration and gets the new WAMP Agent board.loadSettings() # LR has to connect to the assigned WAMP Agent wampConnect(board.wamp_config) else: LOG.error("Reconnection wrong status!") except Exception as err: LOG.error(" - WAMP connection error: " + str(err)) Bye()