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