[tor-commits] r24402: {arm} Presenting full circuit paths in the connection panel. For a (in arm/trunk: . src/interface/connections src/util)

Damian Johnson atagar1 at gmail.com
Mon Mar 21 16:39:27 UTC 2011


Author: atagar
Date: 2011-03-21 16:39:27 +0000 (Mon, 21 Mar 2011)
New Revision: 24402

Added:
   arm/trunk/src/interface/connections/clientEntry.py
Modified:
   arm/trunk/armrc.sample
   arm/trunk/src/interface/connections/connEntry.py
   arm/trunk/src/interface/connections/connPanel.py
   arm/trunk/src/util/torTools.py
   arm/trunk/src/util/uiTools.py
Log:
Presenting full circuit paths in the connection panel. For a somewhat outdated screenshot of what this includes see 'http://www.atagar.com/transfer/tmp/connPanelWithCircs.png'.



Modified: arm/trunk/armrc.sample
===================================================================
--- arm/trunk/armrc.sample	2011-03-21 15:14:17 UTC (rev 24401)
+++ arm/trunk/armrc.sample	2011-03-21 16:39:27 UTC (rev 24402)
@@ -182,7 +182,7 @@
 features.connection.showColumn.fingerprint true
 features.connection.showColumn.nickname true
 features.connection.showColumn.destination true
-features.connection.showColumn.expanedIp true
+features.connection.showColumn.expandedIp true
 
 # Thread pool size for hostname resolutions
 # Determines the maximum number of concurrent requests. Upping this to around

Added: arm/trunk/src/interface/connections/clientEntry.py
===================================================================
--- arm/trunk/src/interface/connections/clientEntry.py	                        (rev 0)
+++ arm/trunk/src/interface/connections/clientEntry.py	2011-03-21 16:39:27 UTC (rev 24402)
@@ -0,0 +1,215 @@
+"""
+Connection panel entries for client circuits. This includes a header entry
+followed by an entry for each hop in the circuit. For instance:
+
+89.188.20.246:42667    -->  217.172.182.26 (de)       General / Built     8.6m (CLIENT)
+|  85.8.28.4 (se)               98FBC3B2B93897A78CDD797EF549E6B62C9A8523    1 / Guard
+|  91.121.204.76 (fr)           546387D93F8D40CFF8842BB9D3A8EC477CEDA984    2 / Middle
++- 217.172.182.26 (de)          5CFA9EA136C0EA0AC096E5CEA7EB674F1207CF86    3 / Exit
+"""
+
+import curses
+
+from interface.connections import entries, connEntry
+from util import torTools, uiTools
+
+# cached fingerprint -> (IP Address, ORPort) results
+RELAY_INFO = {}
+
+def getRelayInfo(fingerprint):
+  """
+  Provides the (IP Address, ORPort) tuple for the given relay. If the lookup
+  fails then this returns ("192.168.0.1", "0").
+  
+  Arguments:
+    fingerprint - relay to look up
+  """
+  
+  if not fingerprint in RELAY_INFO:
+    conn = torTools.getConn()
+    failureResult = ("192.168.0.1", "0")
+    
+    nsEntry = conn.getConsensusEntry(fingerprint)
+    if not nsEntry: return failureResult
+    
+    nsLineComp = nsEntry.split("\n")[0].split(" ")
+    if len(nsLineComp) < 8: return failureResult
+    
+    RELAY_INFO[fingerprint] = (nsLineComp[6], nsLineComp[7])
+  
+  return RELAY_INFO[fingerprint]
+
+class ClientEntry(connEntry.ConnectionEntry):
+  def __init__(self, circuitID, status, purpose, path):
+    connEntry.ConnectionEntry.__init__(self, "127.0.0.1", "0", "127.0.0.1", "0")
+    
+    self.circuitID = circuitID
+    self.status = status
+    
+    # drops to lowercase except the first letter
+    if len(purpose) >= 2:
+      purpose = purpose[0].upper() + purpose[1:].lower()
+    
+    self.lines = [ClientHeaderLine(self.circuitID, purpose)]
+    
+    # Overwrites attributes of the initial line to make it more fitting as the
+    # header for our listing.
+    
+    self.lines[0].baseType = connEntry.Category.CLIENT
+    
+    self.update(status, path)
+  
+  def update(self, status, path):
+    """
+    Our status and path can change over time if the circuit is still in the
+    process of being built. Updates these attributs of our relay.
+    
+    Arguments:
+      status - new status of the circuit
+      path   - list of fingerprints for the series of relays involved in the
+               circuit
+    """
+    
+    self.status = status
+    self.lines = [self.lines[0]]
+    
+    if status == "BUILT" and not self.lines[0].isBuilt:
+      exitIp, exitORPort = getRelayInfo(path[-1])
+      self.lines[0].setExit(exitIp, exitORPort, path[-1])
+    
+    for i in range(len(path)):
+      relayFingerprint = path[i]
+      relayIp, relayOrPort = getRelayInfo(relayFingerprint)
+      
+      if i == len(path) - 1:
+        if status == "BUILT": placementType = "Exit"
+        else: placementType = "Extending"
+      elif i == 0: placementType = "Guard"
+      else: placementType = "Middle"
+      
+      placementLabel = "%i / %s" % (i + 1, placementType)
+      
+      self.lines.append(ClientLine(relayIp, relayOrPort, relayFingerprint, placementLabel))
+    
+    self.lines[-1].isLast = True
+
+class ClientHeaderLine(connEntry.ConnectionLine):
+  """
+  Initial line of a client entry. This has the same basic format as connection
+  lines except that its etc field has circuit attributes.
+  """
+  
+  def __init__(self, circuitID, purpose):
+    connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", "0.0.0.0", "0", False, False)
+    self.circuitID = circuitID
+    self.purpose = purpose
+    self.isBuilt = False
+  
+  def setExit(self, exitIpAddr, exitPort, exitFingerprint):
+    connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", exitIpAddr, exitPort, False, False)
+    self.isBuilt = True
+    self.foreign.fingerprintOverwrite = exitFingerprint
+  
+  def getType(self):
+    return connEntry.Category.CLIENT
+  
+  def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False):
+    if not self.isBuilt: return "Building..."
+    return connEntry.ConnectionLine.getDestinationLabel(self, maxLength, includeLocale, includeHostname)
+  
+  def getEtcContent(self, width, listingType):
+    """
+    Attempts to provide all circuit related stats. Anything that can't be
+    shown completely (not enough room) is dropped.
+    """
+    
+    etcAttr = ["Purpose: %s" % self.purpose, "Circuit ID: %i" % self.circuitID]
+    
+    for i in range(len(etcAttr), -1, -1):
+      etcLabel = ", ".join(etcAttr[:i])
+      if len(etcLabel) <= width: return etcLabel
+    
+    return ""
+  
+  def getDetails(self, width):
+    if not self.isBuilt:
+      detailFormat = curses.A_BOLD | uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()])
+      return [uiTools.DrawEntry("Building Circuit...", detailFormat)]
+    else: return connEntry.ConnectionLine.getDetails(self, width)
+
+class ClientLine(connEntry.ConnectionLine):
+  """
+  An inidividual hop in a circuit. This overwrites the displayed listing, but
+  otherwise makes use of the ConnectionLine attributes (for the detail display,
+  caching, etc).
+  """
+  
+  def __init__(self, fIpAddr, fPort, fFingerprint, placementLabel):
+    connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", fIpAddr, fPort)
+    self.foreign.fingerprintOverwrite = fFingerprint
+    self.placementLabel = placementLabel
+    self.includePort = False
+    
+    # determines the sort of left hand bracketing we use
+    self.isLast = False
+  
+  def getType(self):
+    return connEntry.Category.CLIENT
+  
+  def getListingEntry(self, width, currentTime, listingType):
+    """
+    Provides the DrawEntry for this relay in the circuilt listing. Lines are
+    composed of the following components:
+      <bracket> <dst> <etc> <placement label>
+    
+    The dst and etc entries largely match their ConnectionEntry counterparts.
+    
+    Arguments:
+      width       - maximum length of the line
+      currentTime - the current unix time (ignored)
+      listingType - primary attribute we're listing connections by
+    """
+    
+    return entries.ConnectionPanelLine.getListingEntry(self, width, currentTime, listingType)
+  
+  def _getListingEntry(self, width, currentTime, listingType):
+    lineFormat = uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()])
+    
+    # The required widths are the sum of the following:
+    # bracketing (3 characters)
+    # placementLabel (14 characters)
+    # gap between etc and placement label (5 characters)
+    
+    if self.isLast: bracket = (curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' '))
+    else: bracket = (curses.ACS_VLINE, ord(' '), ord(' '))
+    baselineSpace = len(bracket) + 14 + 5
+    
+    dst, etc = "", ""
+    if listingType == entries.ListingType.IP_ADDRESS:
+      # TODO: include hostname when that's available
+      # dst width is derived as:
+      # src (21) + dst (26) + divider (7) + right gap (2) - bracket (3) = 53 char
+      dst = "%-53s" % self.getDestinationLabel(53, includeLocale = True)
+      etc = self.getEtcContent(width - baselineSpace - len(dst), listingType)
+    elif listingType == entries.ListingType.HOSTNAME:
+      # min space for the hostname is 40 characters
+      etc = self.getEtcContent(width - baselineSpace - 40, listingType)
+      dstLayout = "%%-%is" % (width - baselineSpace - len(etc))
+      dst = dstLayout % self.foreign.getHostname(self.foreign.getIpAddr())
+    elif listingType == entries.ListingType.FINGERPRINT:
+      # dst width is derived as:
+      # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char
+      dst = "%-55s" % self.foreign.getFingerprint()
+      etc = self.getEtcContent(width - baselineSpace - len(dst), listingType)
+    else:
+      # min space for the nickname is 50 characters
+      etc = self.getEtcContent(width - baselineSpace - 50, listingType)
+      dstLayout = "%%-%is" % (width - baselineSpace - len(etc))
+      dst = dstLayout % self.foreign.getNickname()
+    
+    drawEntry = uiTools.DrawEntry("%-14s" % self.placementLabel, lineFormat)
+    drawEntry = uiTools.DrawEntry(" " * (width - baselineSpace - len(dst) - len(etc) + 5), lineFormat, drawEntry)
+    drawEntry = uiTools.DrawEntry(dst + etc, lineFormat, drawEntry)
+    drawEntry = uiTools.DrawEntry(bracket, curses.A_NORMAL, drawEntry, lockFormat = True)
+    return drawEntry
+

Modified: arm/trunk/src/interface/connections/connEntry.py
===================================================================
--- arm/trunk/src/interface/connections/connEntry.py	2011-03-21 15:14:17 UTC (rev 24401)
+++ arm/trunk/src/interface/connections/connEntry.py	2011-03-21 16:39:27 UTC (rev 24402)
@@ -32,7 +32,7 @@
 CONFIG = {"features.connection.showColumn.fingerprint": True,
           "features.connection.showColumn.nickname": True,
           "features.connection.showColumn.destination": True,
-          "features.connection.showColumn.expanedIp": True}
+          "features.connection.showColumn.expandedIp": True}
 
 def loadConfig(config):
   config.update(CONFIG)
@@ -51,6 +51,9 @@
     # if true, we treat the port as an ORPort when searching for matching
     # fingerprints (otherwise the ORPort is assumed to be unknown)
     self.isORPort = False
+    
+    # if set then this overwrites fingerprint lookups
+    self.fingerprintOverwrite = None
   
   def getIpAddr(self):
     """
@@ -88,14 +91,16 @@
     
     return default
   
-  def getLocale(self):
+  def getLocale(self, default=None):
     """
-    Provides the two letter country code for the IP address' locale. This
-    proivdes None if it can't be determined.
+    Provides the two letter country code for the IP address' locale.
+    
+    Arguments:
+      default - return value if no locale information is available
     """
     
     conn = torTools.getConn()
-    return conn.getInfo("ip-to-country/%s" % self.ipAddr)
+    return conn.getInfo("ip-to-country/%s" % self.ipAddr, default)
   
   def getFingerprint(self):
     """
@@ -103,6 +108,9 @@
     determined.
     """
     
+    if self.fingerprintOverwrite:
+      return self.fingerprintOverwrite
+    
     conn = torTools.getConn()
     orPort = self.port if self.isORPort else None
     myFingerprint = conn.getRelayFingerprint(self.ipAddr, orPort)
@@ -157,7 +165,7 @@
       return self.lines[0].startTime
     elif attr == entries.SortAttr.COUNTRY:
       if connections.isIpAddressPrivate(self.lines[0].foreign.getIpAddr()): return ""
-      else: return self.lines[0].foreign.getLocale()
+      else: return self.lines[0].foreign.getLocale("")
     else:
       return entries.ConnectionPanelEntry.getSortValue(self, attr, listingType)
 
@@ -166,7 +174,7 @@
   Display component of the ConnectionEntry.
   """
   
-  def __init__(self, lIpAddr, lPort, fIpAddr, fPort):
+  def __init__(self, lIpAddr, lPort, fIpAddr, fPort, includePort=True, includeExpandedIpAddr=True):
     entries.ConnectionPanelLine.__init__(self)
     
     self.local = Endpoint(lIpAddr, lPort)
@@ -206,14 +214,19 @@
     
     self.cachedType = None
     
+    # includes the port or expanded ip address field when displaying listing
+    # information if true
+    self.includePort = includePort
+    self.includeExpandedIpAddr = includeExpandedIpAddr
+    
     # cached immutable values used for sorting
     self.sortIpAddr = connections.ipToInt(self.foreign.getIpAddr())
     self.sortPort = int(self.foreign.getPort())
   
   def getListingEntry(self, width, currentTime, listingType):
     """
-    Provides the DrawEntry for this connection's listing. The line is made up
-    of six components:
+    Provides the DrawEntry for this connection's listing. Lines are composed
+    of the following components:
       <src>  -->  <dst>     <etc>     <uptime> (<type>)
     
     ListingType.IP_ADDRESS:
@@ -355,7 +368,7 @@
             # a built 1-hop connection since those are most likely a directory
             # mirror).
             
-            for status, _, path in myCircuits:
+            for _, status, _, path in myCircuits:
               if path[0] == destFingerprint and (status != "BUILT" or len(path) > 1):
                 self.cachedType = Category.CLIENT # matched a probable guard connection
             
@@ -366,7 +379,7 @@
           if self._possibleDirectory:
             # Checks if we match a built, single hop circuit.
             
-            for status, _, path in myCircuits:
+            for _, status, _, path in myCircuits:
               if path[0] == destFingerprint and status == "BUILT" and len(path) == 1:
                 self.cachedType = Category.DIRECTORY
             
@@ -379,6 +392,78 @@
     
     return self.cachedType
   
+  def getEtcContent(self, width, listingType):
+    """
+    Provides the optional content for the connection.
+    
+    Arguments:
+      width       - maximum length of the line
+      listingType - primary attribute we're listing connections by
+    """
+    
+    dstAddress = self.getDestinationLabel(26, includeLocale = True)
+    etc, usedSpace = "", 0
+    if listingType == entries.ListingType.IP_ADDRESS:
+      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
+        # show fingerprint (column width: 42 characters)
+        etc += "%-40s  " % self.foreign.getFingerprint()
+        usedSpace += 42
+      
+      if width > usedSpace + 10 and CONFIG["features.connection.showColumn.nickname"]:
+        # show nickname (column width: remainder)
+        nicknameSpace = width - usedSpace
+        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
+        usedSpace += nicknameSpace + 2
+    elif listingType == entries.ListingType.HOSTNAME:
+      if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
+        # show destination ip/port/locale (column width: 28 characters)
+        etc += "%-26s  " % dstAddress
+        usedSpace += 28
+      
+      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
+        # show fingerprint (column width: 42 characters)
+        etc += "%-40s  " % self.foreign.getFingerprint()
+        usedSpace += 42
+      
+      if width > usedSpace + 17 and CONFIG["features.connection.showColumn.nickname"]:
+        # show nickname (column width: min 17 characters, uses half of the remainder)
+        nicknameSpace = 15 + (width - (usedSpace + 17)) / 2
+        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
+        usedSpace += (nicknameSpace + 2)
+    elif listingType == entries.ListingType.FINGERPRINT:
+      if width > usedSpace + 17:
+        # show nickname (column width: min 17 characters, consumes any remaining space)
+        nicknameSpace = width - usedSpace - 2
+        
+        # if there's room then also show a column with the destination
+        # ip/port/locale (column width: 28 characters)
+        isIpLocaleIncluded = width > usedSpace + 45
+        isIpLocaleIncluded &= CONFIG["features.connection.showColumn.destination"]
+        if isIpLocaleIncluded: nicknameSpace -= 28
+        
+        if CONFIG["features.connection.showColumn.nickname"]:
+          nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+          etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
+          usedSpace += nicknameSpace + 2
+        
+        if isIpLocaleIncluded:
+          etc += "%-26s  " % dstAddress
+          usedSpace += 28
+    else:
+      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
+        # show fingerprint (column width: 42 characters)
+        etc += "%-40s  " % self.foreign.getFingerprint()
+        usedSpace += 42
+      
+      if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
+        # show destination ip/port/locale (column width: 28 characters)
+        etc += "%-26s  " % dstAddress
+        usedSpace += 28
+    
+    return etc
+  
   def _getListingContent(self, width, listingType):
     """
     Provides the source, destination, and extra info for our listing.
@@ -390,7 +475,7 @@
     
     conn = torTools.getConn()
     myType = self.getType()
-    dstAddress = self._getDestinationLabel(26, includeLocale = True)
+    dstAddress = self.getDestinationLabel(26, includeLocale = True)
     
     # The required widths are the sum of the following:
     # - room for LABEL_FORMAT and LABEL_MIN_PADDING (11 characters)
@@ -398,70 +483,55 @@
     # - that extra field plus any previous
     
     usedSpace = len(LABEL_FORMAT % tuple([""] * 4)) + LABEL_MIN_PADDING
+    localPort = ":%s" % self.local.getPort() if self.includePort else ""
     
     src, dst, etc = "", "", ""
     if listingType == entries.ListingType.IP_ADDRESS:
       myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr())
       addrDiffer = myExternalIpAddr != self.local.getIpAddr()
       
-      srcAddress = "%s:%s" % (myExternalIpAddr, self.local.getPort())
+      srcAddress = myExternalIpAddr + localPort
       src = "%-21s" % srcAddress # ip:port = max of 21 characters
       dst = "%-26s" % dstAddress # ip:port (xx) = max of 26 characters
       
       usedSpace += len(src) + len(dst) # base data requires 47 characters
       
-      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
-        # show fingerprint (column width: 42 characters)
-        etc += "%-40s  " % self.foreign.getFingerprint()
-        usedSpace += 42
+      # Showing the fingerprint (which has the width of 42) has priority over
+      # an expanded address field. Hence check if we either have space for
+      # both or wouldn't be showing the fingerprint reguardless.
       
-      if addrDiffer and width > usedSpace + 28 and CONFIG["features.connection.showColumn.expanedIp"]:
+      isExpandedAddrVisible = width > usedSpace + 28
+      if isExpandedAddrVisible and CONFIG["features.connection.showColumn.fingerprint"]:
+        isExpandedAddrVisible = width < usedSpace + 42 or width > usedSpace + 70
+      
+      if addrDiffer and isExpandedAddrVisible and self.includeExpandedIpAddr and CONFIG["features.connection.showColumn.expandedIp"]:
         # include the internal address in the src (extra 28 characters)
-        internalAddress = "%s:%s" % (self.local.getIpAddr(), self.local.getPort())
+        internalAddress = self.local.getIpAddr() + localPort
         src = "%-21s  -->  %s" % (internalAddress, src)
         usedSpace += 28
       
-      if width > usedSpace + 10 and CONFIG["features.connection.showColumn.nickname"]:
-        # show nickname (column width: remainder)
-        nicknameSpace = width - usedSpace
-        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
-        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
-        usedSpace += nicknameSpace + 2
+      etc = self.getEtcContent(width - usedSpace, listingType)
+      usedSpace += len(etc)
     elif listingType == entries.ListingType.HOSTNAME:
       # 15 characters for source, and a min of 40 reserved for the destination
-      src = "localhost:%-5s" % self.local.getPort()
+      src = "localhost%-6s" % localPort
       usedSpace += len(src)
       minHostnameSpace = 40
       
-      if width > usedSpace + minHostnameSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
-        # show destination ip/port/locale (column width: 28 characters)
-        etc += "%-26s  " % dstAddress
-        usedSpace += 28
+      etc = self.getEtcContent(width - usedSpace - minHostnameSpace, listingType)
+      usedSpace += len(etc)
       
-      if width > usedSpace + minHostnameSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
-        # show fingerprint (column width: 42 characters)
-        etc += "%-40s  " % self.foreign.getFingerprint()
-        usedSpace += 42
-      
-      if width > usedSpace + minHostnameSpace + 17 and CONFIG["features.connection.showColumn.nickname"]:
-        # show nickname (column width: min 17 characters, uses half of the remainder)
-        nicknameSpace = 15 + (width - (usedSpace + minHostnameSpace + 17)) / 2
-        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
-        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
-        usedSpace += (nicknameSpace + 2)
-      
       hostnameSpace = width - usedSpace
       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()
+        portLabel = ":%-5s" % self.foreign.getPort() if self.includePort else ""
         
         # truncates long hostnames and sets dst to <hostname>:<port>
         hostname = uiTools.cropStr(hostname, hostnameSpace, 0)
-        dst = "%s:%-5s" % (hostname, port)
-        dst = ("%%-%is" % hostnameSpace) % dst
+        dst = ("%%-%is" % hostnameSpace) % (hostname + portLabel)
     elif listingType == entries.ListingType.FINGERPRINT:
       src = "localhost"
       if myType == Category.CONTROL: dst = "localhost"
@@ -470,25 +540,8 @@
       
       usedSpace += len(src) + len(dst) # base data requires 49 characters
       
-      if width > usedSpace + 17:
-        # show nickname (column width: min 17 characters, consumes any remaining space)
-        nicknameSpace = width - usedSpace
-        
-        # if there's room then also show a column with the destination
-        # ip/port/locale (column width: 28 characters)
-        isIpLocaleIncluded = width > usedSpace + 45
-        isIpLocaleIncluded &= CONFIG["features.connection.showColumn.destination"]
-        if isIpLocaleIncluded: nicknameSpace -= 28
-        
-        if CONFIG["features.connection.showColumn.nickname"]:
-          nicknameSpace = width - usedSpace - 28 if isIpLocaleIncluded else width - usedSpace
-          nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
-          etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
-          usedSpace += nicknameSpace + 2
-        
-        if isIpLocaleIncluded:
-          etc += "%-26s  " % dstAddress
-          usedSpace += 28
+      etc = self.getEtcContent(width - usedSpace, listingType)
+      usedSpace += len(etc)
     else:
       # base data requires 50 min characters
       src = self.local.getNickname()
@@ -496,16 +549,9 @@
       else: dst = self.foreign.getNickname()
       minBaseSpace = 50
       
-      if width > usedSpace + minBaseSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
-        # show fingerprint (column width: 42 characters)
-        etc += "%-40s  " % self.foreign.getFingerprint()
-        usedSpace += 42
+      etc = self.getEtcContent(width - usedSpace - minBaseSpace, listingType)
+      usedSpace += len(etc)
       
-      if width > usedSpace + minBaseSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
-        # show destination ip/port/locale (column width: 28 characters)
-        etc += "%-26s  " % dstAddress
-        usedSpace += 28
-      
       baseSpace = width - usedSpace
       usedSpace = width # prevents padding at the end
       
@@ -529,8 +575,8 @@
     """
     
     lines = [""] * 7
-    lines[0] = "address: %s" % self._getDestinationLabel(width - 11)
-    lines[1] = "locale: %s" % ("??" if self.isPrivate() else self.foreign.getLocale())
+    lines[0] = "address: %s" % self.getDestinationLabel(width - 11)
+    lines[1] = "locale: %s" % ("??" if self.isPrivate() else self.foreign.getLocale("??"))
     
     # Remaining data concerns the consensus results, with three possible cases:
     # - if there's a single match then display its details
@@ -627,7 +673,7 @@
     
     return lines
   
-  def _getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False):
+  def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False):
     """
     Provides a short description of the destination. This is made up of two
     components, the base <ip addr>:<port> and an extra piece of information in
@@ -646,10 +692,9 @@
     """
     
     # destination of the connection
-    if self.isPrivate():
-      dstAddress = "<scrubbed>:%s" % self.foreign.getPort()
-    else:
-      dstAddress = "%s:%s" % (self.foreign.getIpAddr(), self.foreign.getPort())
+    ipLabel = "<scrubbed>" if self.isPrivate() else self.foreign.getIpAddr()
+    portLabel = ":%s" % self.foreign.getPort() if self.includePort else ""
+    dstAddress = ipLabel + portLabel
     
     # Only append the extra info if there's at least a couple characters of
     # space (this is what's needed for the country codes).
@@ -673,7 +718,7 @@
         extraInfo = []
         
         if includeLocale:
-          foreignLocale = self.foreign.getLocale()
+          foreignLocale = self.foreign.getLocale("??")
           extraInfo.append(foreignLocale)
           spaceAvailable -= len(foreignLocale) + 2
         

Modified: arm/trunk/src/interface/connections/connPanel.py
===================================================================
--- arm/trunk/src/interface/connections/connPanel.py	2011-03-21 15:14:17 UTC (rev 24401)
+++ arm/trunk/src/interface/connections/connPanel.py	2011-03-21 16:39:27 UTC (rev 24402)
@@ -6,8 +6,8 @@
 import curses
 import threading
 
-from interface.connections import entries, connEntry
-from util import connections, enum, panel, uiTools
+from interface.connections import entries, connEntry, clientEntry
+from util import connections, enum, panel, torTools, uiTools
 
 DEFAULT_CONFIG = {"features.connection.listingType": 0,
                   "features.connection.refreshRate": 10}
@@ -48,8 +48,8 @@
     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._connectionLines = []  # individual lines in the connection listing
+    self._entries = []          # last fetched display entries
+    self._entryLines = []       # individual lines rendered from the entries listing
     self._showDetails = False   # presents the details panel if true
     
     self._lastUpdate = -1       # time the content was last revised
@@ -95,11 +95,11 @@
     
     self.valsLock.acquire()
     if ordering: self._sortOrdering = ordering
-    self._connections.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType)))
+    self._entries.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType)))
     
-    self._connectionLines = []
-    for entry in self._connections:
-      self._connectionLines += entry.getLines()
+    self._entryLines = []
+    for entry in self._entries:
+      self._entryLines += entry.getLines()
     self.valsLock.release()
   
   def setListingType(self, listingType):
@@ -125,7 +125,7 @@
     if uiTools.isScrollKey(key):
       pageHeight = self.getPreferredSize()[0] - 1
       if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1)
-      isChanged = self._scroller.handleKey(key, self._connectionLines, pageHeight)
+      isChanged = self._scroller.handleKey(key, self._entryLines, pageHeight)
       if isChanged: self.redraw(True)
     elif uiTools.isSelectionKey(key):
       self._showDetails = not self._showDetails
@@ -157,10 +157,10 @@
     
     # extra line when showing the detail panel is for the bottom border
     detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0
-    isScrollbarVisible = len(self._connectionLines) > height - detailPanelOffset - 1
+    isScrollbarVisible = len(self._entryLines) > height - detailPanelOffset - 1
     
-    scrollLoc = self._scroller.getScrollLoc(self._connectionLines, height - detailPanelOffset - 1)
-    cursorSelection = self._scroller.getCursorSelection(self._connectionLines)
+    scrollLoc = self._scroller.getScrollLoc(self._entryLines, height - detailPanelOffset - 1)
+    cursorSelection = self._scroller.getCursorSelection(self._entryLines)
     
     # draws the detail panel if currently displaying it
     if self._showDetails:
@@ -177,14 +177,14 @@
     title = "Connection Details:" if self._showDetails else self._title
     self.addstr(0, 0, title, curses.A_STANDOUT)
     
-    scrollOffset = 0
+    scrollOffset = 1
     if isScrollbarVisible:
       scrollOffset = 3
-      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._connectionLines), 1 + detailPanelOffset)
+      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._entryLines), 1 + detailPanelOffset)
     
     currentTime = self._pauseTime if self._pauseTime else time.time()
-    for lineNum in range(scrollLoc, len(self._connectionLines)):
-      entryLine = self._connectionLines[lineNum]
+    for lineNum in range(scrollLoc, len(self._entryLines)):
+      entryLine = self._entryLines[lineNum]
       
       # hilighting if this is the selected line
       extraFormat = curses.A_STANDOUT if entryLine == cursorSelection else curses.A_NORMAL
@@ -216,41 +216,68 @@
     
     if self._lastResourceFetch != currentResolutionCount:
       self.valsLock.acquire()
-      currentConnections = connResolver.getConnections()
       
-      # Replacement listing of connections. We first populate it with any of
-      # our old entries in currentConnections, then add new ConnectionEntries
-      # for whatever remains.
-      newConnections = []
+      newEntries = [] # the new results we'll display
       
-      # preserves any ConnectionEntries they already exist
-      for entry in self._connections:
-        if isinstance(entry, connEntry.ConnectionEntry):
-          connLine = entry.getLines()[0]
+      # Fetches new connections and client circuits...
+      # newConnections  [(local ip, local port, foreign ip, foreign port)...]
+      # newCircuits     {circuitID => (status, purpose, path)...}
+      
+      newConnections = connResolver.getConnections()
+      newCircuits = {}
+      
+      for circuitID, status, purpose, path in torTools.getConn().getCircuits():
+        # Skips established single-hop circuits (these are for directory
+        # fetches, not client circuits)
+        if not (status == "BUILT" and len(path) == 1):
+          newCircuits[circuitID] = (status, purpose, path)
+      
+      # Populates newEntries with any of our old entries that still exist.
+      # This is both for performance and to keep from resetting the uptime
+      # attributes. Note that ClientEntries are a ConnectionEntry subclass so
+      # we need to check for them first.
+      
+      for oldEntry in self._entries:
+        if isinstance(oldEntry, clientEntry.ClientEntry):
+          newEntry = newCircuits.get(oldEntry.circuitID)
+          
+          if newEntry:
+            oldEntry.update(newEntry[0], newEntry[2])
+            newEntries.append(oldEntry)
+            del newCircuits[oldEntry.circuitID]
+        elif isinstance(oldEntry, connEntry.ConnectionEntry):
+          connLine = oldEntry.getLines()[0]
           connAttr = (connLine.local.getIpAddr(), connLine.local.getPort(),
                       connLine.foreign.getIpAddr(), connLine.foreign.getPort())
           
-          if connAttr in currentConnections:
-            newConnections.append(entry)
-            currentConnections.remove(connAttr)
+          if connAttr in newConnections:
+            newEntries.append(oldEntry)
+            newConnections.remove(connAttr)
       
-      # reset any display attributes for the entries we're keeping
-      for entry in newConnections:
-        entry.resetDisplay()
+      # Reset any display attributes for the entries we're keeping
+      for entry in newEntries: entry.resetDisplay()
       
-      # add new entries for any additions
-      for lIp, lPort, fIp, fPort in currentConnections:
-        newConnections.append(connEntry.ConnectionEntry(lIp, lPort, fIp, fPort))
+      # Adds any new connection and circuit entries.
+      for lIp, lPort, fIp, fPort in newConnections:
+        newConnEntry = connEntry.ConnectionEntry(lIp, lPort, fIp, fPort)
+        if newConnEntry.getLines()[0].getType() != connEntry.Category.CLIENT:
+          newEntries.append(newConnEntry)
       
+      for circuitID in newCircuits:
+        status, purpose, path = newCircuits[circuitID]
+        newEntries.append(clientEntry.ClientEntry(circuitID, status, purpose, path))
+      
       # 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).
       
       categoryTypes = connEntry.Category.values()
       typeCounts = dict((type, 0) for type in categoryTypes)
-      for entry in newConnections:
+      for entry in newEntries:
         if isinstance(entry, connEntry.ConnectionEntry):
           typeCounts[entry.getLines()[0].getType()] += 1
+        elif isinstance(entry, clientEntry.ClientEntry):
+          typeCounts[connEntry.Category.CLIENT] += 1
       
       # makes labels for all the categories with connections (ie,
       # "21 outbound", "1 control", etc)
@@ -263,11 +290,11 @@
       if countLabels: self._title = "Connections (%s):" % ", ".join(countLabels)
       else: self._title = "Connections:"
       
-      self._connections = newConnections
+      self._entries = newEntries
       
-      self._connectionLines = []
-      for entry in self._connections:
-        self._connectionLines += entry.getLines()
+      self._entryLines = []
+      for entry in self._entries:
+        self._entryLines += entry.getLines()
       
       self.setSortOrder()
       self._lastResourceFetch = currentResolutionCount

Modified: arm/trunk/src/util/torTools.py
===================================================================
--- arm/trunk/src/util/torTools.py	2011-03-21 15:14:17 UTC (rev 24401)
+++ arm/trunk/src/util/torTools.py	2011-03-21 16:39:27 UTC (rev 24402)
@@ -606,7 +606,7 @@
   def getCircuits(self, default = []):
     """
     This provides a list with tuples of the form:
-    (status, purpose, (fingerprint1, fingerprint2...))
+    (circuitID, status, purpose, (fingerprint1, fingerprint2...))
     
     Arguments:
       default - value provided back if unable to query the circuit-status
@@ -1394,15 +1394,9 @@
         for line in orconnResults.split("\n"):
           self._fingerprintsAttachedCache.append(line[1:line.find("=")])
       
-      # circuit-status has entries of the form:
-      # 7 BUILT $33173252B70A50FE3928C7453077936D71E45C52=shiven,...
-      circStatusResults = self.getInfo("circuit-status")
-      if circStatusResults:
-        for line in circStatusResults.split("\n"):
-          clientEntries = line.split(" ")[2].split(",")
-          
-          for entry in clientEntries:
-            self._fingerprintsAttachedCache.append(entry[1:entry.find("=")])
+      # circuit-status results (we only make connections to the first hop)
+      for _, _, _, path in self.getCircuits():
+        self._fingerprintsAttachedCache.append(path[0])
     
     # narrow to only relays we have a connection to
     attachedMatches = []
@@ -1608,7 +1602,7 @@
             if len(lineComp) < 4: continue
             
             path = tuple([hopEntry[1:41] for hopEntry in lineComp[2].split(",")])
-            result.append((lineComp[1], lineComp[3], path))
+            result.append((int(lineComp[0]), lineComp[1], lineComp[3][8:], path))
       
       # cache value
       if result != None: self._cachedParam[key] = result

Modified: arm/trunk/src/util/uiTools.py
===================================================================
--- arm/trunk/src/util/uiTools.py	2011-03-21 15:14:17 UTC (rev 24401)
+++ arm/trunk/src/util/uiTools.py	2011-03-21 16:39:27 UTC (rev 24402)
@@ -415,10 +415,23 @@
   chained together to compose lines with multiple types of formatting.
   """
   
-  def __init__(self, text, format=curses.A_NORMAL, nextEntry=None):
+  def __init__(self, text, format=curses.A_NORMAL, nextEntry=None, lockFormat=False):
+    """
+    Constructor for prepared draw entries.
+    
+    Arguments:
+      text       - content to be drawn, this can either be a string or list of
+                   integer character codes
+      format     - properties to apply when drawing
+      nextEntry  - entry to be drawn after this one
+      lockFormat - prevents extra formatting attributes from being applied
+                   when rendered if true
+    """
+    
     self.text = text
     self.format = format
     self.nextEntry = nextEntry
+    self.lockFormat = lockFormat
   
   def getNext(self):
     """
@@ -449,9 +462,16 @@
       extraFormat - additional formatting
     """
     
-    drawFormat = self.format | extraFormat
-    drawPanel.addstr(y, x, self.text, drawFormat)
+    if self.lockFormat: drawFormat = self.format
+    else: drawFormat = self.format | extraFormat
     
+    if isinstance(self.text, str):
+      drawPanel.addstr(y, x, self.text, drawFormat)
+    else:
+      for i in range(len(self.text)):
+        drawChar = self.text[i]
+        drawPanel.addch(y, x + i, drawChar, drawFormat)
+    
     # if there's additional content to show then render it too
     if self.nextEntry:
       self.nextEntry.render(drawPanel, y, x + len(self.text), extraFormat)



More information about the tor-commits mailing list