Exemplo n.º 1
0
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']()
Exemplo n.º 4
0
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)