[tor-commits] r24314: {arm} Sorting and custom list type funcitonality for the new conne (in arm/trunk: . src/interface src/interface/connections src/util)

Damian Johnson atagar1 at gmail.com
Wed Mar 9 03:53:11 UTC 2011


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



More information about the tor-commits mailing list