Esempio n. 1
0
    def __init__(self):
        """after initializing all variables, the peer information form the lust run is read from a file"""
        self.logger = newLogger("Peers")
        self._potentialPeers = set()  # set containing data from lunch_peers.cfg
        self._memberIDs = set()  # of PeerIDs members: peers that sent their info, are active and belong to my group
        self._IP_seen = {}  # last seen timestamps by IP
        self._peer_info = {}  # information of every peer by IP
        self._idToIp = {}  # mapping from ID to a set of IPs

        self._groups = set()  # seen group names

        self._lock = loggingMutex("peers", logging=get_settings().get_verbose())

        if get_settings().get_plugins_enabled():
            self._peerNames = PeerNames()
        else:
            self._peerNames = None
Esempio n. 2
0
class LunchPeers(object):
    """This class holds information about all peers known to the lunchinator,
    Terminology:
    * a peer is anyone who sent an info dictionary to this lunchinator.
    a peer can either be identified by its IP and additionally by any ID it told us
    (usually an UUID). This way a peer that changed its IP (e.g. because of switching 
    between LAN and WLAN, or because of DHCP) can be recognised later. If no ID was 
    sent, the ID is the IP. Peers are removed after a defined timeout (default: 300 sec 
    after the last contact or within approx. a minute if they are not reacting to our 
    requests)
    * a candidate is an IP / a hostname that once was a peer. An append-only
    list of candidates is maintained and used on startup to quickly reconnect with 
    all other Lunchinators in the network.
    * a member is a peer that belongs to the same group as this Lunchinator.
    Lunchinators with an empty group form their own group that does not
    communicate with other Lunchinators.
    
    With a few exception most calls to exchange information about the network or the instance
    are sent to all known peers (structural events). 
    Natural language messages are usually sent to members only. Peers that are in another 
    group drop messages by default. 
    
    All public functions (not starting with _) are thread safe unless the documentation 
    says something else ^^.
    
    """

    """Default Info Dict keys"""
    PEER_ID_KEY = u"ID"
    PEER_NAME_KEY = u"name"
    GROUP_KEY = u"group"
    AVATAR_KEY = u"avatar"
    NEXT_LUNCH_BEGIN_KEY = u"next_lunch_begin"
    NEXT_LUNCH_END_KEY = u"next_lunch_end"
    APPLICATION_VERSION_KEY = u"version"
    APPLICATION_COMMIT_COUNT_KEY = u"version_commit_count"
    PLATFORM_KEY = u"platform"

    def __init__(self):
        """after initializing all variables, the peer information form the lust run is read from a file"""
        self.logger = newLogger("Peers")
        self._potentialPeers = set()  # set containing data from lunch_peers.cfg
        self._memberIDs = set()  # of PeerIDs members: peers that sent their info, are active and belong to my group
        self._IP_seen = {}  # last seen timestamps by IP
        self._peer_info = {}  # information of every peer by IP
        self._idToIp = {}  # mapping from ID to a set of IPs

        self._groups = set()  # seen group names

        self._lock = loggingMutex("peers", logging=get_settings().get_verbose())

        if get_settings().get_plugins_enabled():
            self._peerNames = PeerNames()
        else:
            self._peerNames = None

    def finish(self):
        """should be called for a clean shutdown of the program, the peer information will be stored in a file"""
        self._writePeersToFile()

    ################ Group Operations #####################
    # no lock -> groups are not removed
    def getGroups(self):
        """returns a collection of all lunch groups"""
        return self._groups

    def addGroup(self, group_name):
        """adds a new group 
        (done by the lunch server thread)"""
        if group_name not in self._groups:
            self._groups.add(group_name)
            # TODO: what was the second parameter supposed to be?
            get_notification_center().emitGroupAppended(group_name, self._groups)

    ################ IP Timestamp Operations #####################
    # no locks needed: timeouts are not removed
    def seenIP(self, ip):
        """record that there was contact with the given IP just now
        needed for cleanup of peer data
        (done by the lunch server thread)"""
        self._IP_seen[ip] = time()

        # check if ip is the last one in the ip list of the corresponding peer
        with self._lock:
            pID = self.getPeerID(pIP=ip, lock=False)
            if pID in self._idToIp:
                ips = self._idToIp[pID]
                if ips[-1] != ip:
                    # move last seen to the end of the list
                    try:
                        idx = ips.index(ip)
                        ips[idx], ips[-1] = ips[-1], ips[idx]
                    except ValueError:
                        self.logger.error("IP %s of peer %s not in _idToIP", ip, pID)

    def getIPLastSeen(self, ip):
        """returns a timestamp of the last contact with that IP"""
        if ip in self._IP_seen:
            return self._IP_seen[ip]
        return None

    # here we need a lock
    def getIDLastSeen(self, pID):
        """returns a timestamp of the last contact with the peer given by its ID"""
        with self._lock:
            if pID in self._idToIp:
                return self._idToIp[pID][-1]
            else:
                return -1

    ################ Member Operations #####################

    def removeMembersByIP(self, toRemove=None):
        """remove members identified by their IPs, if toRemove is None, all members are removed"""
        with self._lock:
            if toRemove == None:
                mem = deepcopy(self._memberIDs)
                for mID in mem:
                    self._removeMember(mID)
                return

            if type(toRemove) != set:
                toRemove = set(toRemove)

            for ip in toRemove:
                if ip in self._peer_info:
                    self._removeMember(self._peer_info[ip][self.PEER_ID_KEY])

    def getMemberIPs(self):
        """returns the IPs of all members"""
        with self._lock:
            return [self._idToIp[ID] for ID in self._memberIDs]

    def getMembers(self):
        """returns the IDs of all members"""
        return deepcopy(self._memberIDs)

    def getReadyMembers(self):
        """returns a list of IDs of all members that are ready for lunch"""
        with self._lock:
            return set([x for x in self._memberIDs if self._checkInfoForReady(self.getPeerInfo(pID=x, lock=False))])

    def _addMember(self, pID):
        if pID not in self._memberIDs:
            self.logger.debug("Peer %s is a member", pID)
            self._memberIDs.add(pID)
            get_notification_center().emitMemberAppended(pID, deepcopy(self.getPeerInfo(pID=pID, lock=False)))
        else:  # something may have changed for the member data
            get_notification_center().emitMemberUpdated(pID, deepcopy(self.getPeerInfo(pID=pID, lock=False)))

    def _removeMember(self, pID):
        if pID in self._memberIDs:
            self._memberIDs.remove(pID)
            get_notification_center().emitMemberRemoved(pID)

    ################ Peer Operations #####################
    def removePeer(self, pID):
        with self._lock:
            if pID not in self._idToIp:
                return
            pIPs = self._idToIp.pop(pID)
            self._removeMember(pID)
            for pIP in pIPs:
                self._peer_info.pop(pIP)

        get_notification_center().emitPeerRemoved(pID)

    def removePeerIPs(self, toRemove):
        """removes the given IPs and drops information collected about these peers.
        If a peer is registered under multiple IPs and not all are removed its data 
        is not dropped."""
        if type(toRemove) != set:
            toRemove = set(toRemove)

        with self._lock:
            for ip in toRemove:
                if ip in self._peer_info:
                    pID = self._peer_info.pop(ip)[self.PEER_ID_KEY]
                    self._removePeerIPfromID(pID, ip)

    ########### Getters for peer / member information ##############
    # All of the following public methods take keyword arguments:
    #  pID -- Identify peer via peer ID
    #  pIP -- Identify peer via IP
    #  lock -- If false, the getter won't be locked
    # If a single argument is given, it is interpreted as a peer ID.

    @peerGetter(needsID=True)
    def isMember(self, pID):
        """check if the given IP/ID belongs to a member"""
        return pID in self._memberIDs

    @peerGetter(needsID=True)
    def isMe(self, pID):
        """check if the given IP/ID belongs to a member"""
        return pID == get_settings().get_ID()

    @peerGetter()
    def getPeerInfo(self, ip):
        """Returns the info dictionary for a peer
        
        Returns the info dictionary for the peer or None if the IP/ID
        is unknown.
        """
        if ip in self._peer_info:
            return deepcopy(self._peer_info[ip])
        else:
            return None

    @peerGetter()
    def getPeerGroup(self, ip):
        """Returns the group of a peer"""
        if ip in self._peer_info:
            return self._peer_info[ip][u"group"]
        return None

    @peerGetter()
    def getPeerCommitCount(self, ip):
        """Returns the internal version of a peer"""
        if ip in self._peer_info:
            try:
                return int(self._peer_info[ip][u"version_commit_count"])
            except:
                self.logger.debug("Commit Count is not an Integer")
                return None
        return None

    @peerGetter()
    def getRealPeerName(self, ip):
        """Returns the real name of the peer, as provided by its info dict."""
        i = self.getPeerInfo(pIP=ip, lock=False)
        if i:
            return i[u"name"]
        return u"<unknown>"

    @peerGetter(needsID=True)
    def getDisplayedPeerName(self, peerID):
        """Returns the displayed peer name for a given peer ID.
        
        The displayed name is the last known peer name in the
        info dict of the given peer if no custom name was specified.
        Else, the custom name is returned.
        """
        if self._peerNames != None:
            try:
                return self._peerNames.getDisplayedPeerName(peerID)
            except:
                self.logger.exception("Error obtaining displayed peer name")
        # Fall back to peer info dictionary if peer names are not available
        return self.getRealPeerName(pID=peerID, lock=False)

    @peerGetter(needsID=True)
    def hasCustomPeerName(self, peerID):
        """Returns True if the peer has a custom peer name."""
        if self._peerNames == None:
            return False

        return self._peerNames.hasCustomName(peerID)

    @peerGetter()
    def getPeerID(self, ip):
        """Returns the ID of a peer that was sent from the given IP"""
        if ip in self._peer_info:
            return self._peer_info[ip][u"ID"]
        return None

    @peerGetter(needsID=True)
    def isPeerID(self, pID):
        return pID in self._idToIp

    @peerGetter(needsID=True)
    def getPeerIPs(self, pID):
        """returns the IPs of a peer or of all peers if pID==None"""
        if pID == None:
            return self._peer_info.keys()

        if pID in self._idToIp:
            return set(self._idToIp[pID])
        return []

    @peerGetter(needsID=True)
    def getFirstPeerIP(self, pID):
        """returns the first IP of a peer or of all peers if pID==None
        @return: set
        """
        if pID == None:
            return [ips[-1] for ips in self._idToIp.values()]

        if pID in self._idToIp:
            return [self._idToIp[pID][-1]]
        return []

    @peerGetter()
    def isPeerReady(self, ip):
        """returns true if the peer identified by the given IP is ready for lunch"""
        if ip in self._peer_info:
            return self._checkInfoForReady(self._peer_info[ip])
        return False

    @peerGetter()
    def isPeerReadinessKnown(self, ip):
        """returns True if there is a valid lunch time interval for the peer"""
        if ip in self._peer_info:
            p_info = self._peer_info[ip]
            if p_info and p_info.has_key(self.NEXT_LUNCH_BEGIN_KEY) and p_info.has_key(self.NEXT_LUNCH_END_KEY):
                diff = getTimeDifference(
                    p_info[self.NEXT_LUNCH_BEGIN_KEY], p_info[self.NEXT_LUNCH_END_KEY], self.logger
                )
                if diff != None:
                    # valid format
                    return True
        return False

    @peerGetter()
    def getPeerAvatarFile(self, ip):
        """Returns the path to a peer's avatar file, if it exists.

        The method returns None if the peer does not have an avatar or
        the file does not exist.
        """
        peerInfo = self.getPeerInfo(pIP=ip, lock=False)
        if peerInfo != None and self.AVATAR_KEY in peerInfo and peerInfo[self.AVATAR_KEY]:
            avatarFile = os.path.join(get_settings().get_avatar_dir(), peerInfo[self.AVATAR_KEY])
            if os.path.isfile(avatarFile):
                return avatarFile
        return None

    @peerGetter()
    def getAvatarOutdated(self, ip):
        """Returns True if the peer has an avatar but we don't have it"""
        peerInfo = self.getPeerInfo(pIP=ip, lock=False)
        if peerInfo != None and self.AVATAR_KEY in peerInfo and peerInfo[self.AVATAR_KEY]:
            avatarFile = os.path.join(get_settings().get_avatar_dir(), peerInfo[self.AVATAR_KEY])
            return not os.path.exists(avatarFile)
        # doesn't have avatar
        return False

    ############### Additional getters ##################

    def _checkInfoForReady(self, p_info):
        if p_info and p_info.has_key(self.NEXT_LUNCH_BEGIN_KEY) and p_info.has_key(self.NEXT_LUNCH_END_KEY):
            diff = getTimeDifference(p_info[self.NEXT_LUNCH_BEGIN_KEY], p_info[self.NEXT_LUNCH_END_KEY], self.logger)
            if diff == None:
                # illegal format, just assume ready
                return True
            return diff > 0
        else:
            # no lunch time information (only happening with very old lunchinators), assume ready
            return True

    def getPeerIDsByName(self, peerName, sensitive=True):
        """Returns a list of peer IDs of peers with the given name.
        
        The name can either be a peer's real name or a custom name.
        """
        if self._peerNames != None:
            return [peerID for peerID in self._peerNames.iterPeerIDsByName(peerName, sensitive)]
        else:
            if not sensitive:
                peerName = peerName.lower()
            names = []
            with self._lock:
                if sensitive:
                    for anID, aDict in self._peer_info.iteritems():
                        if self.PEER_NAME_KEY in aDict and aDict[self.PEER_NAME_KEY] == peerName:
                            names.append(anID)
                else:
                    for anID, aDict in self._peer_info.iteritems():
                        if self.PEER_NAME_KEY in aDict and aDict[self.PEER_NAME_KEY].lower() == peerName:
                            names.append(anID)
            return names

    def getPeers(self):
        """returns the IDs of all peers"""
        with self._lock:
            return list(self._idToIp.keys())

    def getAllKnownPeerIDs(self):
        """Returns the IDs of all peers ever known."""
        with self._lock:
            return self._peerNames.getAllPeerIDs()

    def getPeerInfoDict(self):
        """Returns all data stored in the peerInfo dict -> all data on all peers"""
        return deepcopy(self._peer_info)

    ############# Setters #################

    def setCustomPeerName(self, peerID, customName):
        """This method might occasionally raise an exception"""
        if self.isMe(pID=peerID):
            # special case: it doesn't make sense to use the custom name for myself
            get_settings().set_user_name(customName)
        else:
            with self._lock:
                infoDict = self.getPeerInfo(pID=peerID, lock=False)
                self._peerNames.setCustomName(peerID, customName, infoDict)

    ############# Methods for initialization and update ################

    def _createPeerByIP(self, ip, info):
        """adds a peer for that IP"""
        self.logger.info("new peer: %s", ip)
        self._peer_info[ip] = info
        pID = self._peer_info[ip][self.PEER_ID_KEY]
        self._addPeerIPtoID(pID, ip)

        if not self._IP_seen.has_key(ip):
            self._IP_seen[ip] = -1

    def updatePeerInfoByIP(self, ip, newInfo):
        """Adds a peer info dict to an IP
        
        The info for the peer that contacted this lunchinator from the given IP 
        will be updated with the data given by newInfo. If the IP is unknown, the
        IP will be added as a new peer. Otherwise, a signal is emitted 
        and the group membership is checked in case this lunchinator is in a group.
        If the peer is in the same group it will be promoted to member, otherwise it 
        will be removed from the list of members. Further signals are emitted if the peer
        is in a group we do not know yet and for member append/remove/update
        
        @type ip: unicode
        @type newInfo: dict 
        """
        # Make sure the required keys are in the dict
        if u"group" not in newInfo:
            newInfo[u"group"] = u""
        if u"name" not in newInfo:
            newInfo[self.PEER_NAME_KEY] = ip
        if self.PEER_ID_KEY not in newInfo:
            newInfo[self.PEER_ID_KEY] = ip

        newPID = newInfo[self.PEER_ID_KEY]

        with self._lock:
            if ip in self._peer_info and self._peer_info[ip][self.PEER_ID_KEY] != newPID:
                # IP has a new ID, assume different peer -> old peer does not use IP any more
                self._removePeerIPfromID(self._peer_info[ip][self.PEER_ID_KEY], ip)
                del self._peer_info[ip]

            if ip not in self._peer_info and newPID not in self._idToIp:
                # this is a new peer
                self._createPeerByIP(ip, newInfo)
            else:
                # this is either an update to an existing IP or a new IP for an existing peer
                if ip in self._peer_info and newPID in self._idToIp:
                    # this is an update
                    existing_info = self._peer_info[ip]
                elif ip not in self._peer_info and newPID in self._idToIp:
                    # this is a new IP for an existing peer
                    self.logger.debug("New IP: %s for ID: %s", ip, newPID)
                    existing_info = self._peer_info[self._idToIp[newPID][-1]]
                    self._peer_info[ip] = existing_info
                    self._addPeerIPtoID(newPID, ip)
                elif ip in self._peer_info and newPID not in self._idToIp:
                    # we already know this IP but it is not this peer - should not happen
                    self.logger.error("Something went wrong - ID %s is missing in _idToIp", newPID)
                    return

                old_info = deepcopy(existing_info)
                existing_info.update(newInfo)

                removedKeys = set(old_info.keys()) - set(newInfo.keys())
                for key in removedKeys:
                    existing_info.pop(key)

                if old_info != existing_info:
                    if self._peerNames is not None:
                        self._peerNames.addPeerName(newPID, existing_info)
                    get_notification_center().emitPeerUpdated(newPID, deepcopy(existing_info))
                    self.logger.debug("%s has new info: %s; \n update was %s", ip, existing_info, newInfo)
                else:
                    self.logger.debug("%s sent info - without new info", ip)

                if (
                    self.AVATAR_KEY in old_info
                    and self.AVATAR_KEY in existing_info
                    and old_info[self.AVATAR_KEY] != existing_info[self.AVATAR_KEY]
                    and not self.getAvatarOutdated(pIP=ip, lock=False)
                ):
                    # avatar changed but we already have the picture
                    get_notification_center().emitAvatarChanged(newPID, deepcopy(existing_info[self.AVATAR_KEY]))

            own_group = get_settings().get_group()

            if self._peer_info[ip][self.GROUP_KEY] == own_group:
                self._addMember(newPID)
                self.addGroup(self._peer_info[ip][self.GROUP_KEY])
            else:
                self._removeMember(newPID)

    def removeInactive(self):
        """
        peers that haven't been seen for <peerTimeout> seconds are removed
        and a PeerRemoved notification is sent. If a removed peer was a
        member, a MemberRemoved notification is sent, too.
        """

        self.logger.debug("Removing inactive peers")
        try:
            with self._lock:
                for ip in self._IP_seen:
                    if ip in self._peer_info and time() - self._IP_seen[ip] > get_settings().get_peer_timeout():
                        pID = self._peer_info[ip][self.PEER_ID_KEY]
                        self._removePeerIPfromID(pID, ip)
                        del self._peer_info[ip]
        except:
            self.logger.exception("Something went wrong while trying to clean up the list of peers")

    def _removePeerIPfromID(self, pID, ip):
        self.logger.debug("Removing %s from ID: %s", ip, pID)
        self._idToIp[pID].remove(ip)

        if 0 == len(self._idToIp[pID]):
            # no IP associated with that ID -> remove peer
            # remove member first
            self._removeMember(pID)
            self._idToIp.pop(pID)
            get_notification_center().emitPeerRemoved(pID)
        else:
            existing_ip = self._idToIp[pID][0]
            get_notification_center().emitPeerUpdated(pID, deepcopy(self._peer_info[existing_ip]))

    def _addPeerIPtoID(self, pID, ip):
        if pID not in self._idToIp:
            self._idToIp[pID] = [ip]
            if self._peerNames is not None:
                self._peerNames.addPeerName(pID, self._peer_info[ip])
            get_notification_center().emitPeerAppended(pID, deepcopy(self._peer_info[ip]))
        else:
            # last one is last seen one
            self._idToIp[pID].append(ip)
            # workaround to get the IP over to the slots
            cp = deepcopy(self._peer_info[ip])
            cp["triggerIP"] = ip
            get_notification_center().emitPeerUpdated(pID, cp)

    def initPeersFromFile(self):
        """Initializes peer IPs from file and returns a list of IPs
        to request info from."""
        p_file = (
            get_settings().get_peers_file()
            if os.path.exists(get_settings().get_peers_file())
            else get_settings().get_members_file()
        )

        peerIPs = []
        # TODO change AF_INET when going to v6
        # TODO for Mac: incorporate search domains somehow?
        ownIPs = set()
        try:
            ownIPs = set(i[4][0] for i in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET))
        except socket.error:
            self.logger.warning("socket error trying to obtain own IP")
        if not ownIPs:
            self.logger.warning("Didn't find IPs for ourselves")
        for ip in ownIPs:
            peerIPs.append(ip)
        if os.path.exists(p_file):
            with codecs.open(p_file, "r", "utf-8") as f:
                for line in f.readlines():
                    line = line.split("\t", 1)
                    hostn = line[0].strip()
                    if not hostn:
                        continue
                    try:
                        self._potentialPeers.add(hostn)
                        # TODO change AF_INET when going to v6
                        for ip in set(i[4][0] for i in socket.getaddrinfo(hostn, None, socket.AF_INET)):
                            peerIPs.append(ip)
                    except socket.error:
                        self.logger.debug(
                            "cannot find host specified in members_file by %s with name %s", p_file, hostn
                        )
        return peerIPs

    def _writePeersToFile(self):
        try:
            with self._lock:
                for ip in self._peer_info:
                    try:
                        hostname = socket.gethostbyaddr(ip)[0]
                    except:
                        hostname = ip
                    self._potentialPeers.add(hostname)

                with codecs.open(get_settings().get_peers_file(), "w", "utf-8") as f:
                    f.truncate()
                    f.write(u"\n".join(sorted(self._potentialPeers)))
        except:
            self.logger.exception("Could not write peers to %s", get_settings().get_peers_file())

    @loggingFunc
    def _alertIfIPnotMyself(self, newPID, peerInfo):
        """ alert if ID is mine but ip is not from my machine
        
        this function has to be called from the main thread
        
        @return: True if that's my ID from another machine
        
        @type newPID: unicode
        @type peerInfo: dict
        @rtype: bool
        """

        if not peerInfo.has_key("triggerIP") or newPID != get_settings().get_ID():
            # that's not me!
            return False

        ip = peerInfo["triggerIP"]
        myname = socket.gethostname()  # socket.getfqdn(socket.gethostname())
        othername = ""
        try:
            othername = socket.gethostbyaddr(ip)[0]
        except:
            self.logger.warning(
                "Another IP (%s) contacted me with my ID, I can't find it's hostname..., won't do anything now." % ip
            )
            return False

        # make sure, we only check the hostname, not the fqdn
        i = myname.find(".")
        if i != -1:
            myname = myname[:i]
        i = othername.find(".")
        if i != -1:
            othername = othername[:i]

        if myname == othername:
            # that seems to be me from another, maybe on a second
            # network interface
            return False

        if othername in get_settings().get_multiple_machines_allowed():
            # he is allowed to do that
            return False

        # that seems to be coming from an unknown machine and has to be reported
        from lunchinator import lunchinator_has_gui

        msg = (
            "Another lunchinator on the network (%s: %s)" % (ip, othername)
            + "is identifying itself with your (%s) ID. " % myname
            + "It will get all messages you get, also private ones!\n"
        )

        if lunchinator_has_gui():
            msg += "If this is not what you want, you should create a new ID immediately."
            from PyQt4.QtGui import QMessageBox, QPushButton

            msgBox = QMessageBox(None)
            #             msgBox.setIcon(QMessageBox.Warning)
            #             msgBox.setWindowTitle("Another Lunchinator with your ID detected")
            msgBox.setText(msg)
            msgBox.addButton(QPushButton("Create New ID"), QMessageBox.AcceptRole)
            msgBox.addButton(QPushButton("Ignore"), QMessageBox.NoRole)
            msgBox.addButton(QPushButton("Allow host to get my messages"), QMessageBox.RejectRole)
            ret = msgBox.exec_()
            if ret == QMessageBox.AcceptRole:
                get_settings().generate_ID()
            elif ret != QMessageBox.NoRole:
                get_settings().add_multiple_machines_allowed(othername)
        else:
            msg += (
                "If you are sure that this is right you can set "
                + "multiple_machines_allowed = %s in your settings.cfg \n" % ip
                + "Otherwise you should create a new ID immediately.\n"
            )
            self.logger.critical(msg)
        return True

    def __len__(self):
        return len(self._idToIp)

    def __enter__(self):
        return self._lock.__enter__()

    def __exit__(self, aType, value, traceback):
        return self._lock.__exit__(aType, value, traceback)

    def __iter__(self):
        return self._idToIp.iterkeys().__iter__()