Author: atagar Date: 2011-03-09 03:53:11 +0000 (Wed, 09 Mar 2011) New Revision: 24314
Modified: arm/trunk/armrc.sample arm/trunk/src/interface/connections/connPanel.py arm/trunk/src/interface/connections/listings.py arm/trunk/src/interface/controller.py arm/trunk/src/util/enum.py Log: Sorting and custom list type funcitonality for the new connection panel. Modified: arm/trunk/armrc.sample =================================================================== --- arm/trunk/armrc.sample 2011-03-09 01:12:30 UTC (rev 24313) +++ arm/trunk/armrc.sample 2011-03-09 03:53:11 UTC (rev 24314) @@ -155,18 +155,29 @@ # --------------------------------- # oldPanel # includes the old connection panel in the interface +# newPanel +# includes the new connection panel in the interface +# listingType +# the primary category of information shown by default, options including: +# 0 -> IP Address / Port 1 -> Hostname +# 2 -> Fingerprint 3 -> Nickname +# order +# three comma separated configuration attributes, options including: +# 0 -> Category, 1 -> Uptime, 2 -> Listing, 3 -> IP Address, +# 4 -> Port, 5 -> Hostname, 6 -> Fingerprint, 7 -> Nickname, +# 8 -> Country # refreshRate # rate at which the connection panel contents is redrawn (if higher than the # connection resolution rate then reducing this won't casue new data to # appear more frequently - just increase the rate at which the uptime field # is updated) -# newPanel -# includes the new connection panel in the interface # showColumn.* # toggles the visability of the connection table columns features.connection.oldPanel true features.connection.newPanel false +features.connection.listingType 0 +features.connection.order 0, 2, 1 features.connection.refreshRate 10 features.connection.showColumn.fingerprint true features.connection.showColumn.nickname true Modified: arm/trunk/src/interface/connections/connPanel.py =================================================================== --- arm/trunk/src/interface/connections/connPanel.py 2011-03-09 01:12:30 UTC (rev 24313) +++ arm/trunk/src/interface/connections/connPanel.py 2011-03-09 03:53:11 UTC (rev 24314) @@ -9,14 +9,17 @@ from interface.connections import listings from util import connections, enum, log, panel, torTools, uiTools -DEFAULT_CONFIG = {"features.connection.refreshRate": 10} +DEFAULT_CONFIG = {"features.connection.listingType": 0, + "features.connection.refreshRate": 10} # height of the detail panel content, not counting top and bottom border DETAILS_HEIGHT = 7 # listing types -Listing = enum.Enum(("IP", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME") +Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME") +DEFAULT_SORT_ORDER = (listings.SortAttr.CATEGORY, listings.SortAttr.LISTING, listings.SortAttr.UPTIME) + class ConnectionPanel(panel.Panel, threading.Thread): """ Listing of connections tor is making, with information correlated against @@ -28,16 +31,21 @@ threading.Thread.__init__(self) self.setDaemon(True) - #self.sortOrdering = DEFAULT_SORT_ORDER + self._sortOrdering = DEFAULT_SORT_ORDER self._config = dict(DEFAULT_CONFIG) if config: config.update(self._config, { + "features.connection.listingType": (0, len(Listing.values()) - 1), "features.connection.refreshRate": 1}) - # TODO: test and add to the sample armrc - #self.sortOrdering = config.getIntCSV("features.connections.order", self.sortOrdering, 3, 0, 6) + sortFields = listings.SortAttr.values() + customOrdering = config.getIntCSV("features.connection.order", None, 3, 0, len(sortFields)) + + if customOrdering: + self._sortOrdering = [sortFields[i] for i in customOrdering] - self.scroller = uiTools.Scroller(True) + self._listingType = Listing.values()[self._config["features.connection.listingType"]] + self._scroller = uiTools.Scroller(True) self._title = "Connections:" # title line of the panel self._connections = [] # last fetched connections self._showDetails = False # presents the details panel if true @@ -75,13 +83,44 @@ # and being paused self.redraw(True) + def setSortOrder(self, ordering = None): + """ + Sets the connection attributes we're sorting by and resorts the contents. + + Arguments: + ordering - new ordering, if undefined then this resorts with the last + set ordering + """ + + self.valsLock.acquire() + if ordering: self._sortOrdering = ordering + self._connections.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType))) + self.valsLock.release() + + def setListingType(self, listingType): + """ + Sets the priority information presented by the panel. + + Arguments: + listingType - Listing instance for the primary information to be shown + """ + + self.valsLock.acquire() + self._listingType = listingType + + # if we're sorting by the listing then we need to resort + if listings.SortAttr.LISTING in self._sortOrdering: + self.setSortOrder() + + self.valsLock.release() + def handleKey(self, key): self.valsLock.acquire() if uiTools.isScrollKey(key): pageHeight = self.getPreferredSize()[0] - 1 if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1) - isChanged = self.scroller.handleKey(key, self._connections, pageHeight) + isChanged = self._scroller.handleKey(key, self._connections, pageHeight) if isChanged: self.redraw(True) elif uiTools.isSelectionKey(key): self._showDetails = not self._showDetails @@ -115,8 +154,8 @@ detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0 isScrollbarVisible = len(self._connections) > height - detailPanelOffset - 1 - scrollLoc = self.scroller.getScrollLoc(self._connections, height - detailPanelOffset - 1) - cursorSelection = self.scroller.getCursorSelection(self._connections) + scrollLoc = self._scroller.getScrollLoc(self._connections, height - detailPanelOffset - 1) + cursorSelection = self._scroller.getCursorSelection(self._connections) # draws the detail panel if currently displaying it if self._showDetails: @@ -147,7 +186,7 @@ # prefix (entry data which is largely static, plus the time label) # the right content (time and type) takes seventeen columns - entryLabel = entry.getLabel(Listing.IP, width - scrollOffset - 17) + entryLabel = entry.getLabel(self._listingType, width - scrollOffset - 17) timeLabel = uiTools.getTimeLabel(currentTime - entry.startTime, 1) prefixLabel = "%s%5s (" % (entryLabel, timeLabel) @@ -205,14 +244,13 @@ for lIp, lPort, fIp, fPort in currentConnections: newConnections.append(listings.ConnectionEntry(lIp, lPort, fIp, fPort)) - # if it's changed then sort the results - #if newConnections != self._connections: - # newConnections.sort(key=lambda i: (i.getAll(self.sortOrdering))) + # Counts the relays in each of the categories. This also flushes the + # type cache for all of the connections (in case its changed since last + # fetched). - # counts the relays in each of the categories categoryTypes = listings.Category.values() typeCounts = dict((type, 0) for type in categoryTypes) - for conn in newConnections: typeCounts[conn.getType()] += 1 + for conn in newConnections: typeCounts[conn.getType(True)] += 1 # makes labels for all the categories with connections (ie, # "21 outbound", "1 control", etc) @@ -226,6 +264,7 @@ else: self._title = "Connections:" self._connections = newConnections + self.setSortOrder() self._lastResourceFetch = currentResolutionCount self.valsLock.release() Modified: arm/trunk/src/interface/connections/listings.py =================================================================== --- arm/trunk/src/interface/connections/listings.py 2011-03-09 01:12:30 UTC (rev 24313) +++ arm/trunk/src/interface/connections/listings.py 2011-03-09 03:53:11 UTC (rev 24314) @@ -22,6 +22,14 @@ Category.APPLICATION: "yellow", Category.DIRECTORY: "magenta", Category.CONTROL: "red"} +SortAttr = enum.Enum("CATEGORY", "UPTIME", "LISTING", "IP_ADDRESS", "PORT", + "HOSTNAME", "FINGERPRINT", "NICKNAME", "COUNTRY") +SORT_COLORS = {SortAttr.CATEGORY: "red", SortAttr.UPTIME: "yellow", + SortAttr.LISTING: "green", SortAttr.IP_ADDRESS: "blue", + SortAttr.PORT: "blue", SortAttr.HOSTNAME: "magenta", + SortAttr.FINGERPRINT: "cyan", SortAttr.NICKNAME: "cyan", + SortAttr.COUNTRY: "blue"} + # static data for listing format # <src> --> <dst> <etc><padding> LABEL_FORMAT = "%s --> %s %s%s" @@ -74,9 +82,17 @@ default - return value if no hostname is available """ - myHostname = hostnames.resolve(self.ipAddr) - if not myHostname: return default - else: return myHostname + # TODO: skipping all hostname resolution to be safe for now + #try: + # myHostname = hostnames.resolve(self.ipAddr) + #except: + # # either a ValueError or IOError depending on the source of the lookup failure + # myHostname = None + # + #if not myHostname: return default + #else: return myHostname + + return default def getLocale(self): """ @@ -161,61 +177,29 @@ else: self.baseType = Category.OUTBOUND self.foreign.isORPort = True + + self.cachedType = None + + # cached immutable values used for sorting + self.sortIpAddr = _ipToInt(self.foreign.getIpAddr()) + self.sortPort = int(self.foreign.getPort()) - def getType(self): + def getType(self, reset=False): """ Provides the category this connection belongs to. This isn't always static since it can rely on dynamic information (like the current consensus). + + Arguments: + reset - determines if the type has changed if true, otherwise this + provides the same result as the last call """ - if self.baseType == Category.OUTBOUND: - # Currently the only non-static categories are OUTBOUND vs... - # - EXIT since this depends on the current consensus - # - CLIENT if this is likely to belong to our guard usage - # - DIRECTORY if this is a single-hop circuit (directory mirror?) - # - # The exitability, circuits, and fingerprints are all cached by the - # torTools util keeping this a quick lookup. - - conn = torTools.getConn() - destFingerprint = self.foreign.getFingerprint() - - if destFingerprint == "UNKNOWN": - # Not a known relay. This might be an exit connection. - - if conn.isExitingAllowed(self.foreign.getIpAddr(), self.foreign.getPort()): - return Category.EXIT - elif self._possibleClient or self._possibleDirectory: - # This belongs to a known relay. If we haven't eliminated ourselves as - # a possible client or directory connection then check if it still - # holds true. - - myCircuits = conn.getCircuits() - - if self._possibleClient: - # Checks that this belongs to the first hop in a circuit that's - # either unestablished or longer than a single hop (ie, anything but - # a built 1-hop connection since those are most likely a directory - # mirror). - - for status, _, path in myCircuits: - if path[0] == destFingerprint and (status != "BUILT" or len(path) > 1): - return Category.CLIENT # matched a probable guard connection - - # fell through, we can eliminate ourselves as a guard in the future - self._possibleClient = False - - if self._possibleDirectory: - # Checks if we match a built, single hop circuit. - - for status, _, path in myCircuits: - if path[0] == destFingerprint and status == "BUILT" and len(path) == 1: - return Category.DIRECTORY - - # fell through, eliminate ourselves as a directory connection - self._possibleDirectory = False + # caches both to simplify the calls and to keep the type consistent until + # we want to reflect changes + if reset or not self.cachedType: + self.cachedType = self._getType() - return self.baseType + return self.cachedType def getDestinationLabel(self, maxLength, extraAttr=DestAttr.NONE): """ @@ -293,6 +277,18 @@ # for everything else this isn't a concern return False + def getSortValues(self, sortAttrs, listingType): + """ + Provides the value used in comparisons to sort based on the given + attribute. + + Arguments: + sortAttrs - list of SortAttr values for the field being sorted on + listingType - primary attribute we're listing connections by + """ + + return [self._getSortValue(attr, listingType) for attr in sortAttrs] + def getLabel(self, listingType, width): """ Provides the formatted display string for this entry in the listing with @@ -300,7 +296,7 @@ <src> --> <dst> <etc> <uptime> (<type>) this provides the first three components padded to fill up to the uptime. - Listing.IP: + Listing.IP_ADDRESS: src - <internal addr:port> --> <external addr:port> dst - <destination addr:port> etc - <fingerprint> <nickname> @@ -345,7 +341,7 @@ usedSpace = len(LABEL_FORMAT % tuple([""] * 4)) + LABEL_MIN_PADDING src, dst, etc = "", "", "" - if listingType == connPanel.Listing.IP: + if listingType == connPanel.Listing.IP_ADDRESS: myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr()) addrDiffer = myExternalIpAddr != self.local.getIpAddr() @@ -375,7 +371,7 @@ elif listingType == connPanel.Listing.HOSTNAME: # 15 characters for source, and a min of 40 reserved for the destination src = "localhost:%-5s" % self.local.getPort() - usedSpace += len(stc) + usedSpace += len(src) minHostnameSpace = 40 if width > usedSpace + minHostnameSpace + 28 and CONFIG["features.connection.showColumn.destination"]: @@ -396,19 +392,17 @@ usedSpace += (nicknameSpace + 2) hostnameSpace = width - usedSpace - usedSpace = width + usedSpace = width # prevents padding at the end if self.isPrivate(): dst = ("%%-%is" % hostnameSpace) % "<scrubbed>" else: hostname = self.foreign.getHostname(self.foreign.getIpAddr()) port = self.foreign.getPort() - # exclude space needed for the ':<port>' - hostnameSpace -= len(port) + 1 - # truncates long hostnames and sets dst to <hostname>:<port> hostname = uiTools.cropStr(hostname, hostnameSpace, 0) - dst = ("%%-%is:%%-5s" % hostnameSpace) % (hostname, port) + dst = "%s:%-5s" % (hostname, port) + dst = ("%%-%is" % hostnameSpace) % dst elif listingType == connPanel.Listing.FINGERPRINT: src = "localhost" if myType == Category.CONTROL: dst = "localhost" @@ -428,7 +422,7 @@ if isIpLocaleIncluded: nicknameSpace -= 28 if CONFIG["features.connection.showColumn.nickname"]: - nicknameSpace = width - usedSpace - 28 if isIpLocaleVisible else width - usedSpace + nicknameSpace = width - usedSpace - 28 if isIpLocaleIncluded else width - usedSpace nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) etc += ("%%-%is " % nicknameSpace) % nicknameLabel usedSpace += nicknameSpace + 2 @@ -454,6 +448,8 @@ usedSpace += 28 baseSpace = width - usedSpace + usedSpace = width # prevents padding at the end + if len(src) + len(dst) > baseSpace: src = uiTools.cropStr(src, baseSpace / 3) dst = uiTools.cropStr(dst, baseSpace - len(src)) @@ -467,4 +463,108 @@ self._labelCacheArgs = (listingType, width) return self._labelCache + + def _getType(self): + """ + Provides our best guess at the current type of the connection. This + depends on consensus results, our current client circuts, etc. + """ + + if self.baseType == Category.OUTBOUND: + # Currently the only non-static categories are OUTBOUND vs... + # - EXIT since this depends on the current consensus + # - CLIENT if this is likely to belong to our guard usage + # - DIRECTORY if this is a single-hop circuit (directory mirror?) + # + # The exitability, circuits, and fingerprints are all cached by the + # torTools util keeping this a quick lookup. + + conn = torTools.getConn() + destFingerprint = self.foreign.getFingerprint() + + if destFingerprint == "UNKNOWN": + # Not a known relay. This might be an exit connection. + + if conn.isExitingAllowed(self.foreign.getIpAddr(), self.foreign.getPort()): + return Category.EXIT + elif self._possibleClient or self._possibleDirectory: + # This belongs to a known relay. If we haven't eliminated ourselves as + # a possible client or directory connection then check if it still + # holds true. + + myCircuits = conn.getCircuits() + + if self._possibleClient: + # Checks that this belongs to the first hop in a circuit that's + # either unestablished or longer than a single hop (ie, anything but + # a built 1-hop connection since those are most likely a directory + # mirror). + + for status, _, path in myCircuits: + if path[0] == destFingerprint and (status != "BUILT" or len(path) > 1): + return Category.CLIENT # matched a probable guard connection + + # fell through, we can eliminate ourselves as a guard in the future + self._possibleClient = False + + if self._possibleDirectory: + # Checks if we match a built, single hop circuit. + + for status, _, path in myCircuits: + if path[0] == destFingerprint and status == "BUILT" and len(path) == 1: + return Category.DIRECTORY + + # fell through, eliminate ourselves as a directory connection + self._possibleDirectory = False + + return self.baseType + + def _getSortValue(self, sortAttr, listingType): + """ + Provides the value of a single attribute used for sorting purposes. + """ + + from interface.connections import connPanel + + if sortAttr == SortAttr.IP_ADDRESS: return self.sortIpAddr + elif sortAttr == SortAttr.PORT: return self.sortPort + elif sortAttr == SortAttr.HOSTNAME: return self.foreign.getHostname("") + elif sortAttr == SortAttr.FINGERPRINT: return self.foreign.getFingerprint() + elif sortAttr == SortAttr.NICKNAME: + myNickname = self.foreign.getNickname() + + if myNickname == "UNKNOWN": return "z" * 20 # orders at the end + else: return myNickname.lower() + elif sortAttr == SortAttr.CATEGORY: return Category.indexOf(self.getType()) + elif sortAttr == SortAttr.UPTIME: return self.startTime + elif sortAttr == SortAttr.COUNTRY: + if connections.isIpAddressPrivate(self.foreign.getIpAddr()): return "" + else: return self.foreign.getLocale() + elif sortAttr == SortAttr.LISTING: + if listingType == connPanel.Listing.IP_ADDRESS: + return self._getSortValue(SortAttr.IP_ADDRESS, listingType) + elif listingType == connPanel.Listing.HOSTNAME: + return self._getSortValue(SortAttr.HOSTNAME, listingType) + elif listingType == connPanel.Listing.FINGERPRINT: + return self._getSortValue(SortAttr.FINGERPRINT, listingType) + elif listingType == connPanel.Listing.NICKNAME: + return self._getSortValue(SortAttr.NICKNAME, listingType) + + return "" +def _ipToInt(ipAddr): + """ + Provides an integer representation of the ip address, suitable for sorting. + + Arguments: + ipAddr - ip address to be converted + """ + + total = 0 + + for comp in ipAddr.split("."): + total *= 255 + total += int(comp) + + return total + Modified: arm/trunk/src/interface/controller.py =================================================================== --- arm/trunk/src/interface/controller.py 2011-03-09 01:12:30 UTC (rev 24313) +++ arm/trunk/src/interface/controller.py 2011-03-09 03:53:11 UTC (rev 24314) @@ -1003,7 +1003,11 @@ popup.addfstr(3, 2, "<b>enter</b>: edit configuration option") popup.addfstr(3, 41, "<b>w</b>: save current configuration") - popup.addfstr(4, 2, "<b>s</b>: sort ordering") + + listingType = panels["conn2"]._listingType.lower() + popup.addfstr(4, 2, "<b>l</b>: listed identity (<b>%s</b>)" % listingType) + + popup.addfstr(4, 41, "<b>s</b>: sort ordering") elif page == 3: popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line") popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line") @@ -1596,6 +1600,39 @@ setPauseState(panels, isPaused, page) finally: panel.CURSES_LOCK.release() + elif page == 2 and (key == ord('l') or key == ord('L')): + # provides a menu to pick the primary information we list connections by + options = interface.connections.connPanel.Listing.values() + initialSelection = options.index(panels["conn2"]._listingType) + + # hides top label of connection panel and pauses the display + panelTitle = panels["conn2"]._title + panels["conn2"]._title = "" + panels["conn2"].redraw(True) + setPauseState(panels, isPaused, page, True) + + selection = showMenu(stdscr, panels["popup"], "List By:", options, initialSelection) + + # reverts changes made for popup + panels["conn2"]._title = panelTitle + setPauseState(panels, isPaused, page) + + # applies new setting + if selection != -1 and options[selection] != panels["conn2"]._listingType: + panels["conn2"].setListingType(options[selection]) + panels["conn2"].redraw(True) + elif page == 2 and (key == ord('s') or key == ord('S')): + # set ordering for connection options + titleLabel = "Connection Ordering:" + options = interface.connections.listings.SortAttr.values() + oldSelection = panels["conn2"]._sortOrdering + optionColors = dict([(attr, interface.connections.listings.SORT_COLORS[attr]) for attr in options]) + results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors) + + if results: + panels["conn2"].setSortOrder(results) + + panels["conn2"].redraw(True) elif page == 3 and (key == ord('c') or key == ord('C')) and False: # TODO: disabled for now (probably gonna be going with separate pages # rather than popup menu) Modified: arm/trunk/src/util/enum.py =================================================================== --- arm/trunk/src/util/enum.py 2011-03-09 01:12:30 UTC (rev 24313) +++ arm/trunk/src/util/enum.py 2011-03-09 03:53:11 UTC (rev 24314) @@ -62,6 +62,17 @@ return list(self.orderedValues) + def indexOf(self, value): + """ + Provides the index of the given value in the collection. This raises a + ValueError if no such element exists. + + Arguments: + value - entry to be looked up + """ + + return self.orderedValues.index(value) + def next(self, value): """ Provides the next enumeration after the given value, raising a ValueError _______________________________________________ tor-commits mailing list tor-commits@lists.torproject.org https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits