示例#1
0
def peer(rolename, partnum):
    peername = "%s-%02d" % (rolename, partnum)
    portnum = 5670 + partnum  # fixme: risking collission!
    print(peername)
    ret = Pyre(peername)
    ret.set_port(bytes(str(portnum).encode("ascii")))
    ret.set_header("role", rolename)
    return ret
class Transport():
    '''Message transport mechanisms for LCAS'''

    def send(self, dest, ntuple):
        '''Send given ntuple to Transport named dest. If dest isn't listening for messages from this Transport, the message will (currently) be silently ignored.'''
        if self._prefix is not None:
            dest = self._prefix + dest
        self._pyre.shout(dest, json.dumps(ntuple).encode('utf-8'))
    # send()

    # Notes on subscribe
    #
    # The callback is called in the same thread that listens for pyre
    # messages, so the callback should start a new thread if it's
    # going to block or take a long time to run.
    #
    # The callback must take one positional argument, the tuple, and
    # can OPTIONALLY take a keyword argument (e.g. **kw). I use the
    # inspect module to detect this. May be too clever for my own
    # good.
    #
    # There can be only one callback for a given remote. If you call
    # subscribe again with the same remote, it raises an error.

    def subscribe(self, remote, callback):
        '''When a message is sent from a Transport named remote to this transport, call the passed callback with the ntuple as the first argument. If the callback takes **kw, it will also pass additional metadata such as the Transport name, UUID, and IP of the sender.'''
        if self._prefix is not None:
            remote = self._prefix + remote
        if remote in self._subscribers:
            raise TransportError(self, 'Transport.subscribe() was called a second time with the same remote (\"%s\"). You must call Transport.unsubscribe() before setting a new callback.'%(remote))
        self._subscribers[remote] = callback
    # subscribe()

    def unsubscribe(self, remote):
        '''Stop listening for messages from remote.'''
        if self._prefix is not None:
            remote = self._prefix + remote
        if remote in self._subscribers:
            del self._subscribers[remote]
    # unsubscribe()

    def subscribe_all(self, callback):
        '''Call callback every time a message is sent from any remote Transport to this Transport.'''
        if self._subscribe_all is not None:
            raise TransportError(self, 'Transport.subscribe_all() was called a second time. You must call Transport.unsubscribe_all() before setting a new callback.')
        self._subscribe_all = callback
    # subscribe_all()

    def unsubscribe_all(self):
        self._subscribe_all = None
    # unsubscribe_all()

    # Notes on get()
    #    
    # If you already subscribe to remote, temporarly overrides
    # the subscribe. The subscribed callback will NOT be called.
    # The subscription is replaced after get() returns.

    def get(self, remote):
        '''Block waiting for a message from a Transport named remote. Returns python namedtuple containing fields object, uuid, name, ip, datetime.'''

        if self._prefix is not None:
            remote = self._prefix + remote

        # The final python namedtuple to be returned needs to be shared
        # between get_callback() and get(). In python3, you can use
        # nonlocal, but in python2 you need a trick (storing in a
        # data structure). The actual value to be returned will
        # be ret[0].
        ret = [ None ]

        # The event e will get set when a message is read by the
        # readthread.
        e = threading.Event()

        # This function is a callback used to detect the next message.
        # It stores the message in a Python namedtuple and sets the
        # event.

        def get_callback(tup, **kw):
            ret[0] = collections.namedtuple('TransportEnvelope', ['object', 'uuid', 'name', 'ip', 'datetime'])(tup, kw['uuid'], kw['name'], kw['ip'], kw['datetime'])
            # Inform get() that ret is ready to be returned.
            e.set()
        # get_callback()

        # Store the old callback, if any
        oldcb = self._subscribers.get(remote, None)

        # Set the subscription
        self._subscribers[remote] = get_callback
        
        # Wait for the callback to be called.
        e.wait()

        # Restore the old subscription, if any.
        if oldcb is not None:
            self._subscribers[remote] = oldcb
        else:
            del self._subscribers[remote]

        # Return the namedtuple.
        return ret[0]
    # get()

    def quit_federation(self):
        '''Send a quit message to all agents in this federation, and then close down the Transport.'''
        if self._run:
            self._pyre.shouts(self._globalchannel, u"QUIT")
            self._run = False
            # Wait for the readthread to finish
            self._readthread.join()
            # Tell Pyre to shut down
            self._pyre.stop()

    def is_running(self):
        '''Return the status of this Transport. If the Transport isn't running, you should not send it messages and the callbacks will not be called.'''
        return self._run

    ######################################################################
    # All private methods below here

    def __init__(self, myname, port=None, prefix=None):
        # NOTE: Seems to be a bug in Pyre where you can't set the port.
        if port is not None:
            raise NotImplementedError('There is a bug in Pyre that prevents setting of the discovery port. If you require multiple federations of Pyre components, use prefix instead of port in Transport constructor.')

        # dict of remote name to callback. See subscribe method above.
        self._subscribers = {}
        
        # Callback for all message (or None if none registered)
        self._subscribe_all = None

        self._prefix = prefix

        # Attach the federation name as a prefix to both this channel
        # and the global channel. The global channel is currently
        # just used for QUIT messages.

        if prefix is not None:
            myname = prefix + myname
            self._globalchannel = prefix + "GLOBAL"
        else:
            self._globalchannel = "GLOBAL"

        self._pyre = Pyre(myname)
        if port is not None:
            self._pyre.set_port(port)

        self._pyre.join(myname)
        self._pyre.join(self._globalchannel)
        self._pyre.start()

        # Dict of (UUIDs => IP addresses) that have sent a valid ENTER message
        self._uuid2ip = {}

        self._run = True

        self._readthread = threading.Thread(target=self._readworker)
        self._readthread.start()
    # __init__()

    # Handle pyre messages. Run in self._readthread
    def _readworker(self):
        '''This method is called in a separate thread to handle messages sent over pyre. It dispataches to methods named for the pyre events (e.g. _ENTER).'''

        # Set up a poller so recv doesn't block. Possibly not needed
        # since we'll always get an event when the other agents quit,
        # but just in case something goes wrong, we want to be sure to
        # close down.

        poller = zmq.Poller()
        sock = self._pyre.socket()
        poller.register(sock, zmq.POLLIN)

        while self._run:
            # Wait until a message is received OR one second timeout.
            items = dict(poller.poll(1000))
            if not (sock in items and items[sock] == zmq.POLLIN):
                # This should only happen if we time out.
                continue
            # There's an event waiting. Read and process it.
            event = self._pyre.recv()
            logger.debug('Transport %s-%s received event %s'%(self._pyre.uuid(), self._pyre.name(), event))
            eventtype = event[0].decode('utf-8')
            # Sender's uuid and name
            sid = uuid.UUID(bytes=event[1])
            name = event[2].decode('utf-8')
            # Make sure we've seen matching ENTER for all events
            if eventtype != 'ENTER' and sid not in self._uuid2ip:
                raise TransportProtocolError(self, 'Received event %s with no matching ENTER.'%(event))
                continue

            if eventtype == 'ENTER':
                # Changed
                url = event[4].decode('utf-8')
                self._ENTER(sid, name, url)
            elif eventtype == 'JOIN':
                channel = event[3].decode('utf-8')
                self._JOIN(sid, name, channel)
            elif eventtype == 'SHOUT':
                channel = event[3].decode('utf-8')
                message = event[4].decode('utf-8')
                if channel == self._globalchannel and message == "QUIT":
                    # Set ourself to stop running, close down pyre, exit
                    # worker thread.
                    self._run = False
                    self._pyre.stop()
                    break
                else:
                    self._SHOUT(sid, name, channel, message)
            elif eventtype == 'WHISPER':
                message = event[3].decode('utf-8')
                self._WHISPER(sid, name, message)
            elif eventtype == 'LEAVE':
                channel = event[3].decode('utf-8')
                self._LEAVE(sid, name, channel)
            elif eventtype == 'EXIT':
                self._EXIT(sid, name)
            else:
                raise TransportProtocolError(self, 'Illegal event type in event %s'%(event))
    # _readworker()

    # The following methods are named for the pyre event that this
    # instance has received. They are called automatically from the
    # worker thread that's listening for events.

    def _ENTER(self, sid, name, url):
        # We expect all connections to be tcp on some port. This regular
        # expression is used to extract the ip part.
        urlmatch = re.match('tcp://([0-9.]+):[0-9]+$', url)
        if urlmatch:
            ip = urlmatch.group(1)
            if is_valid_ip(ip):
                # Everything looks good. Add to list of valid uuids.
                self._uuid2ip[sid] = ip
            else:
                raise TransportSecurityError(self, 'Message from invalid IP address %s in ENTER %s %s %s. Check the function is_valid_ip() in Transport.py.'%(ip, sid, name, url))
        else:
            raise TransportProtocolError(self, 'Malformed URL in ENTER %s %s %s'%(sid, name, url))
    # _ENTER()

    def _JOIN(self, sid, name, channel):
        pass
    # _JOIN()

    def _SHOUT(self, sid, name, channel, message):
        now = datetime.datetime.now()
        logger.debug('In _SHOUT with %s %s %s %s'%(sid, name, channel, message)) #???
        if name in self._subscribers:
            logger.debug('got a subscription')
            cb = self._subscribers[name]
            self._call_callback(cb, sid, name, channel, message, now)
        if self._subscribe_all is not None:
            cb = self._subscribe_all
            self._call_callback(cb, sid, name, channel, message, now)
    # _SHOUT()

    def _call_callback(self, cb, sid, name, channel, message, now):
        if inspect.getargspec(cb).keywords is None:
            cb(json.loads(message))
        else:
            cb(message, uuid=sid, name=name, ip=self._uuid2ip[sid], datetime=now)
    # _call_callback


    def _WHISPER(self, sid, name, message):
        raise TransportProtocolError(self, 'Unexpected WHISPER from %s %s'%(sid, name))
    # _WHISPER()

    def _LEAVE(self, sid, name, channel):
        pass
    # _LEAVE()

    def _EXIT(self, sid, name):
        # Remove sid from list of valid uuids. This should
        # never be an error since we check in _readworker().
        del self._uuid2ip[sid]
示例#3
0
class Transport():
    '''Message transport mechanisms for LCAS'''
    def send(self, dest, ntuple):
        '''Send given ntuple to Transport named dest. If dest isn't listening for messages from this Transport, the message will (currently) be silently ignored.'''
        if self._prefix is not None:
            dest = self._prefix + dest
        self._pyre.shout(dest, json.dumps(ntuple).encode('utf-8'))

    # send()

    # Notes on subscribe
    #
    # The callback is called in the same thread that listens for pyre
    # messages, so the callback should start a new thread if it's
    # going to block or take a long time to run.
    #
    # The callback must take one positional argument, the tuple, and
    # can OPTIONALLY take a keyword argument (e.g. **kw). I use the
    # inspect module to detect this. May be too clever for my own
    # good.
    #
    # There can be only one callback for a given remote. If you call
    # subscribe again with the same remote, it raises an error.

    def subscribe(self, remote, callback):
        '''When a message is sent from a Transport named remote to this transport, call the passed callback with the ntuple as the first argument. If the callback takes **kw, it will also pass additional metadata such as the Transport name, UUID, and IP of the sender.'''
        if self._prefix is not None:
            remote = self._prefix + remote
        if remote in self._subscribers:
            raise TransportError(
                self,
                'Transport.subscribe() was called a second time with the same remote (\"%s\"). You must call Transport.unsubscribe() before setting a new callback.'
                % (remote))
        self._subscribers[remote] = callback

    # subscribe()

    def unsubscribe(self, remote):
        '''Stop listening for messages from remote.'''
        if self._prefix is not None:
            remote = self._prefix + remote
        if remote in self._subscribers:
            del self._subscribers[remote]

    # unsubscribe()

    def subscribe_all(self, callback):
        '''Call callback every time a message is sent from any remote Transport to this Transport.'''
        if self._subscribe_all is not None:
            raise TransportError(
                self,
                'Transport.subscribe_all() was called a second time. You must call Transport.unsubscribe_all() before setting a new callback.'
            )
        self._subscribe_all = callback

    # subscribe_all()

    def unsubscribe_all(self):
        self._subscribe_all = None

    # unsubscribe_all()

    # Notes on get()
    #
    # If you already subscribe to remote, temporarly overrides
    # the subscribe. The subscribed callback will NOT be called.
    # The subscription is replaced after get() returns.

    def get(self, remote):
        '''Block waiting for a message from a Transport named remote. Returns python namedtuple containing fields object, uuid, name, ip, datetime.'''

        if self._prefix is not None:
            remote = self._prefix + remote

        # The final python namedtuple to be returned needs to be shared
        # between get_callback() and get(). In python3, you can use
        # nonlocal, but in python2 you need a trick (storing in a
        # data structure). The actual value to be returned will
        # be ret[0].
        ret = [None]

        # The event e will get set when a message is read by the
        # readthread.
        e = threading.Event()

        # This function is a callback used to detect the next message.
        # It stores the message in a Python namedtuple and sets the
        # event.

        def get_callback(tup, **kw):
            ret[0] = collections.namedtuple(
                'TransportEnvelope',
                ['object', 'uuid', 'name', 'ip', 'datetime'])(tup, kw['uuid'],
                                                              kw['name'],
                                                              kw['ip'],
                                                              kw['datetime'])
            # Inform get() that ret is ready to be returned.
            e.set()

        # get_callback()

        # Store the old callback, if any
        oldcb = self._subscribers.get(remote, None)

        # Set the subscription
        self._subscribers[remote] = get_callback

        # Wait for the callback to be called.
        e.wait()

        # Restore the old subscription, if any.
        if oldcb is not None:
            self._subscribers[remote] = oldcb
        else:
            del self._subscribers[remote]

        # Return the namedtuple.
        return ret[0]

    # get()

    def quit_federation(self):
        '''Send a quit message to all agents in this federation, and then close down the Transport.'''
        if self._run:
            self._pyre.shouts(self._globalchannel, u"QUIT")
            self._run = False
            # Wait for the readthread to finish
            self._readthread.join()
            # Tell Pyre to shut down
            self._pyre.stop()

    def is_running(self):
        '''Return the status of this Transport. If the Transport isn't running, you should not send it messages and the callbacks will not be called.'''
        return self._run

    ######################################################################
    # All private methods below here

    def __init__(self, myname, port=None, prefix=None):
        # NOTE: Seems to be a bug in Pyre where you can't set the port.
        if port is not None:
            raise NotImplementedError(
                'There is a bug in Pyre that prevents setting of the discovery port. If you require multiple federations of Pyre components, use prefix instead of port in Transport constructor.'
            )

        # dict of remote name to callback. See subscribe method above.
        self._subscribers = {}

        # Callback for all message (or None if none registered)
        self._subscribe_all = None

        self._prefix = prefix

        # Attach the federation name as a prefix to both this channel
        # and the global channel. The global channel is currently
        # just used for QUIT messages.

        if prefix is not None:
            myname = prefix + myname
            self._globalchannel = prefix + "GLOBAL"
        else:
            self._globalchannel = "GLOBAL"

        self._pyre = Pyre(myname)
        if port is not None:
            self._pyre.set_port(port)

        self._pyre.join(myname)
        self._pyre.join(self._globalchannel)
        self._pyre.start()

        # Dict of (UUIDs => IP addresses) that have sent a valid ENTER message
        self._uuid2ip = {}

        self._run = True

        self._readthread = threading.Thread(target=self._readworker)
        self._readthread.start()

    # __init__()

    # Handle pyre messages. Run in self._readthread
    def _readworker(self):
        '''This method is called in a separate thread to handle messages sent over pyre. It dispataches to methods named for the pyre events (e.g. _ENTER).'''

        # Set up a poller so recv doesn't block. Possibly not needed
        # since we'll always get an event when the other agents quit,
        # but just in case something goes wrong, we want to be sure to
        # close down.

        poller = zmq.Poller()
        sock = self._pyre.socket()
        poller.register(sock, zmq.POLLIN)

        while self._run:
            # Wait until a message is received OR one second timeout.
            items = dict(poller.poll(1000))
            if not (sock in items and items[sock] == zmq.POLLIN):
                # This should only happen if we time out.
                continue
            # There's an event waiting. Read and process it.
            event = self._pyre.recv()
            logger.debug('Transport %s-%s received event %s' %
                         (self._pyre.uuid(), self._pyre.name(), event))
            eventtype = event[0].decode('utf-8')
            # Sender's uuid and name
            sid = uuid.UUID(bytes=event[1])
            name = event[2].decode('utf-8')
            # Make sure we've seen matching ENTER for all events
            if eventtype != 'ENTER' and sid not in self._uuid2ip:
                raise TransportProtocolError(
                    self,
                    'Received event %s with no matching ENTER.' % (event))
                continue

            if eventtype == 'ENTER':
                # Changed
                url = event[4].decode('utf-8')
                self._ENTER(sid, name, url)
            elif eventtype == 'JOIN':
                channel = event[3].decode('utf-8')
                self._JOIN(sid, name, channel)
            elif eventtype == 'SHOUT':
                channel = event[3].decode('utf-8')
                message = event[4].decode('utf-8')
                if channel == self._globalchannel and message == "QUIT":
                    # Set ourself to stop running, close down pyre, exit
                    # worker thread.
                    self._run = False
                    self._pyre.stop()
                    break
                else:
                    self._SHOUT(sid, name, channel, message)
            elif eventtype == 'WHISPER':
                message = event[3].decode('utf-8')
                self._WHISPER(sid, name, message)
            elif eventtype == 'LEAVE':
                channel = event[3].decode('utf-8')
                self._LEAVE(sid, name, channel)
            elif eventtype == 'EXIT':
                self._EXIT(sid, name)
            else:
                raise TransportProtocolError(
                    self, 'Illegal event type in event %s' % (event))

    # _readworker()

    # The following methods are named for the pyre event that this
    # instance has received. They are called automatically from the
    # worker thread that's listening for events.

    def _ENTER(self, sid, name, url):
        # We expect all connections to be tcp on some port. This regular
        # expression is used to extract the ip part.
        urlmatch = re.match('tcp://([0-9.]+):[0-9]+$', url)
        if urlmatch:
            ip = urlmatch.group(1)
            if is_valid_ip(ip):
                # Everything looks good. Add to list of valid uuids.
                self._uuid2ip[sid] = ip
            else:
                raise TransportSecurityError(
                    self,
                    'Message from invalid IP address %s in ENTER %s %s %s. Check the function is_valid_ip() in Transport.py.'
                    % (ip, sid, name, url))
        else:
            raise TransportProtocolError(
                self, 'Malformed URL in ENTER %s %s %s' % (sid, name, url))

    # _ENTER()

    def _JOIN(self, sid, name, channel):
        pass

    # _JOIN()

    def _SHOUT(self, sid, name, channel, message):
        now = datetime.datetime.now()
        logger.debug('In _SHOUT with %s %s %s %s' %
                     (sid, name, channel, message))  #???
        if name in self._subscribers:
            logger.debug('got a subscription')
            cb = self._subscribers[name]
            self._call_callback(cb, sid, name, channel, message, now)
        if self._subscribe_all is not None:
            cb = self._subscribe_all
            self._call_callback(cb, sid, name, channel, message, now)

    # _SHOUT()

    def _call_callback(self, cb, sid, name, channel, message, now):
        if inspect.getargspec(cb).keywords is None:
            cb(json.loads(message))
        else:
            cb(message,
               uuid=sid,
               name=name,
               ip=self._uuid2ip[sid],
               datetime=now)

    # _call_callback

    def _WHISPER(self, sid, name, message):
        raise TransportProtocolError(
            self, 'Unexpected WHISPER from %s %s' % (sid, name))

    # _WHISPER()

    def _LEAVE(self, sid, name, channel):
        pass

    # _LEAVE()

    def _EXIT(self, sid, name):
        # Remove sid from list of valid uuids. This should
        # never be an error since we check in _readworker().
        del self._uuid2ip[sid]