[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[tor-commits] r24249: {arm} Connection details popup for the rewritten page. Changes fro (in arm/trunk/src: interface interface/connections util)



Author: atagar
Date: 2011-02-21 06:00:17 +0000 (Mon, 21 Feb 2011)
New Revision: 24249

Modified:
   arm/trunk/src/interface/configPanel.py
   arm/trunk/src/interface/connections/connPanel.py
   arm/trunk/src/interface/connections/listings.py
   arm/trunk/src/interface/controller.py
   arm/trunk/src/util/torTools.py
   arm/trunk/src/util/uiTools.py
Log:
Connection details popup for the rewritten page. Changes from the original version (besides being a sane, maintainable implementation) is:
- using the consensus exit policies rather than the longer descriptor versions (which never fit anyway...)
- displaying connection details no longer freezes the rest of the display
- detail panel is dynamically resizable
- more resilient to missing descriptors



Modified: arm/trunk/src/interface/configPanel.py
===================================================================
--- arm/trunk/src/interface/configPanel.py	2011-02-20 21:31:49 UTC (rev 24248)
+++ arm/trunk/src/interface/configPanel.py	2011-02-21 06:00:17 UTC (rev 24249)
@@ -305,40 +305,40 @@
   def _getConfigOptions(self):
     return self.confContents if self.showAll else self.confImportantContents
   
-  def _drawSelectionPanel(self, cursorSelection, width, detailPanelHeight, isScrollbarVisible):
+  def _drawSelectionPanel(self, selection, width, detailPanelHeight, isScrollbarVisible):
     """
     Renders a panel for the selected configuration option.
     """
     
     # This is a solid border unless the scrollbar is visible, in which case a
     # 'T' pipe connects the border to the bar.
-    uiTools.drawBox(self, 0, 0, width, detailPanelHeight)
+    uiTools.drawBox(self, 0, 0, width, detailPanelHeight + 1)
     if isScrollbarVisible: self.addch(detailPanelHeight, 1, curses.ACS_TTEE)
     
-    selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[cursorSelection.get(Field.CATEGORY)])
+    selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[selection.get(Field.CATEGORY)])
     
     # first entry:
     # <option> (<category> Option)
-    optionLabel =" (%s Option)" % cursorSelection.get(Field.CATEGORY)
-    self.addstr(1, 2, cursorSelection.get(Field.OPTION) + optionLabel, selectionFormat)
+    optionLabel =" (%s Option)" % selection.get(Field.CATEGORY)
+    self.addstr(1, 2, selection.get(Field.OPTION) + optionLabel, selectionFormat)
     
     # second entry:
     # Value: <value> ([default|custom], <type>, usage: <argument usage>)
     if detailPanelHeight >= 3:
       valueAttr = []
-      valueAttr.append("default" if cursorSelection.get(Field.IS_DEFAULT) else "custom")
-      valueAttr.append(cursorSelection.get(Field.TYPE))
-      valueAttr.append("usage: %s" % (cursorSelection.get(Field.ARG_USAGE)))
+      valueAttr.append("default" if selection.get(Field.IS_DEFAULT) else "custom")
+      valueAttr.append(selection.get(Field.TYPE))
+      valueAttr.append("usage: %s" % (selection.get(Field.ARG_USAGE)))
       valueAttrLabel = ", ".join(valueAttr)
       
       valueLabelWidth = width - 12 - len(valueAttrLabel)
-      valueLabel = uiTools.cropStr(cursorSelection.get(Field.VALUE), valueLabelWidth)
+      valueLabel = uiTools.cropStr(selection.get(Field.VALUE), valueLabelWidth)
       
       self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat)
     
     # remainder is filled with the man page description
     descriptionHeight = max(0, detailPanelHeight - 3)
-    descriptionContent = "Description: " + cursorSelection.get(Field.DESCRIPTION)
+    descriptionContent = "Description: " + selection.get(Field.DESCRIPTION)
     
     for i in range(descriptionHeight):
       # checks if we're done writing the description

Modified: arm/trunk/src/interface/connections/connPanel.py
===================================================================
--- arm/trunk/src/interface/connections/connPanel.py	2011-02-20 21:31:49 UTC (rev 24248)
+++ arm/trunk/src/interface/connections/connPanel.py	2011-02-21 06:00:17 UTC (rev 24249)
@@ -7,10 +7,15 @@
 import threading
 
 from interface.connections import listings
-from util import connections, enum, log, panel, uiTools
+from util import connections, enum, log, panel, torTools, uiTools
 
+REDRAW_RATE = 10 # TODO: make a config option
+
 DEFAULT_CONFIG = {}
 
+# 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")
 
@@ -36,6 +41,7 @@
     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
     
     self._lastUpdate = -1       # time the content was last revised
     self._isPaused = True       # prevents updates if true
@@ -52,6 +58,8 @@
     self._update() # populates initial entries
     
     # TODO: should listen for tor shutdown
+    # TODO: hasn't yet had its pausing functionality tested (for instance, the
+    # key handler still accepts events when paused)
   
   def setPaused(self, isPause):
     """
@@ -73,8 +81,12 @@
     
     if uiTools.isScrollKey(key):
       pageHeight = self.getPreferredSize()[0] - 1
+      if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1)
       isChanged = self.scroller.handleKey(key, self._connections, pageHeight)
       if isChanged: self.redraw(True)
+    elif uiTools.isSelectionKey(key):
+      self._showDetails = not self._showDetails
+      self.redraw(True)
     
     self.valsLock.release()
   
@@ -87,7 +99,7 @@
     while not self._halt:
       currentTime = time.time()
       
-      if self._isPaused or currentTime - lastDraw < 1:
+      if self._isPaused or currentTime - lastDraw < REDRAW_RATE:
         self._cond.acquire()
         if not self._halt: self._cond.wait(0.2)
         self._cond.release()
@@ -95,26 +107,35 @@
         # updates content if their's new results, otherwise just redraws
         self._update()
         self.redraw(True)
-        lastDraw += 1
+        lastDraw += REDRAW_RATE
   
   def draw(self, width, height):
     self.valsLock.acquire()
     
-    # title label with connection counts
-    self.addstr(0, 0, self._title, curses.A_STANDOUT)
+    # extra line when showing the detail panel is for the bottom border
+    detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0
+    isScrollbarVisible = len(self._connections) > height - detailPanelOffset - 1
     
-    scrollLoc = self.scroller.getScrollLoc(self._connections, height - 1)
+    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:
+      self._drawSelectionPanel(cursorSelection, width, isScrollbarVisible)
+    
+    # title label with connection counts
+    title = "Connection Details:" if self._showDetails else self._title
+    self.addstr(0, 0, title, curses.A_STANDOUT)
+    
     scrollOffset = 0
-    if len(self._connections) > height - 1:
+    if isScrollbarVisible:
       scrollOffset = 3
-      self.addScrollBar(scrollLoc, scrollLoc + height - 1, len(self._connections), 1)
+      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._connections), 1 + detailPanelOffset)
     
     currentTime = self._pauseTime if self._pauseTime else time.time()
     for lineNum in range(scrollLoc, len(self._connections)):
       entry = self._connections[lineNum]
-      drawLine = lineNum + 1 - scrollLoc
+      drawLine = lineNum + detailPanelOffset + 1 - scrollLoc
       
       entryType = entry.getType()
       lineFormat = uiTools.getColor(listings.CATEGORY_COLOR[entryType])
@@ -203,4 +224,113 @@
       self._connections = newConnections
       self._lastResourceFetch = currentResolutionCount
       self.valsLock.release()
+  
+  def _drawSelectionPanel(self, selection, width, isScrollbarVisible):
+    """
+    Renders a panel for details on the selected connnection.
+    """
+    
+    # This is a solid border unless the scrollbar is visible, in which case a
+    # 'T' pipe connects the border to the bar.
+    uiTools.drawBox(self, 0, 0, width, DETAILS_HEIGHT + 2)
+    if isScrollbarVisible: self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
+    
+    selectionFormat = curses.A_BOLD | uiTools.getColor(listings.CATEGORY_COLOR[selection.getType()])
+    lines = [""] * 7
+    
+    lines[0] = "address: %s" % selection.getDestinationLabel(width - 11, listings.DestAttr.NONE)
+    lines[1] = "locale: %s" % ("??" if selection.isPrivate() else selection.foreign.getLocale())
+    
+    # Remaining data concerns the consensus results, with three possible cases:
+    # - if there's a single match then display its details
+    # - if there's multiple potenial relays then list all of the combinations
+    #   of ORPorts / Fingerprints
+    # - if no consensus data is available then say so (probably a client or
+    #   exit connection)
+    
+    fingerprint = selection.foreign.getFingerprint()
+    conn = torTools.getConn()
+    
+    if fingerprint != "UNKNOWN":
+      # single match - display information available about it
+      nsEntry = conn.getConsensusEntry(fingerprint)
+      descEntry = conn.getDescriptorEntry(fingerprint)
+      
+      # append the fingerprint to the second line
+      lines[1] = "%-13sfingerprint: %s" % (lines[1], fingerprint)
+      
+      if nsEntry:
+        # example consensus entry:
+        # r murble R8sCM1ar1sS2GulQYFVmvN95xsk RJr6q+wkTFG+ng5v2bdCbVVFfA4 2011-02-21 00:25:32 195.43.157.85 443 0
+        # s Exit Fast Guard Named Running Stable Valid
+        # w Bandwidth=2540
+        # p accept 20-23,43,53,79-81,88,110,143,194,443
+        
+        nsLines = nsEntry.split("\n")
+        
+        firstLineComp = nsLines[0].split(" ")
+        if len(firstLineComp) >= 9:
+          _, nickname, _, _, pubDate, pubTime, _, orPort, dirPort = firstLineComp[:9]
+        else: nickname, pubDate, pubTime, orPort, dirPort = "", "", "", "", ""
+        
+        flags = nsLines[1][2:]
+        microExit = nsLines[3][2:]
+        
+        dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort
+        lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel)
+        lines[3] = "published: %s %s" % (pubDate, pubTime)
+        lines[4] = "flags: %s" % flags.replace(" ", ", ")
+        lines[5] = "exit policy: %s" % microExit.replace(",", ", ")
+      
+      if descEntry:
+        torVersion, patform, contact = "", "", ""
+        
+        for descLine in descEntry.split("\n"):
+          if descLine.startswith("platform"):
+            # has the tor version and platform, ex:
+            # platform Tor 0.2.1.29 (r318f470bc5f2ad43) on Linux x86_64
+            
+            torVersion = descLine[13:descLine.find(" ", 13)]
+            platform = descLine[descLine.rfind(" on ") + 4:]
+          elif descLine.startswith("contact"):
+            contact = descLine[8:]
+            
+            # clears up some highly common obscuring
+            for alias in (" at ", " AT "): contact = contact.replace(alias, "@")
+            for alias in (" dot ", " DOT "): contact = contact.replace(alias, ".")
+            
+            break # contact lines come after the platform
+        
+        lines[3] = "%-36s os: %-14s version: %s" % (lines[3], platform, torVersion)
+        
+        # contact information is an optional field
+        if contact: lines[6] = "contact: %s" % contact
+    else:
+      allMatches = conn.getRelayFingerprint(selection.foreign.getIpAddr(), getAllMatches = True)
+      
+      if allMatches:
+        # multiple matches
+        lines[2] = "Muliple matches, possible fingerprints are:"
+        
+        for i in range(len(allMatches)):
+          isLastLine = i == 3
+          
+          relayPort, relayFingerprint = allMatches[i]
+          lineText = "%i. or port: %-5s fingerprint: %s" % (i, relayPort, relayFingerprint)
+          
+          # if there's multiple lines remaining at the end then give a count
+          remainingRelays = len(allMatches) - i
+          if isLastLine and remainingRelays > 1:
+            lineText = "... %i more" % remainingRelays
+          
+          lines[3 + i] = lineText
+          
+          if isLastLine: break
+      else:
+        # no consensus entry for this ip address
+        lines[2] = "No consensus data found"
+    
+    for i in range(len(lines)):
+      lineText = uiTools.cropStr(lines[i], width - 2)
+      self.addstr(1 + i, 2, lineText, selectionFormat)
 

Modified: arm/trunk/src/interface/connections/listings.py
===================================================================
--- arm/trunk/src/interface/connections/listings.py	2011-02-20 21:31:49 UTC (rev 24248)
+++ arm/trunk/src/interface/connections/listings.py	2011-02-21 06:00:17 UTC (rev 24249)
@@ -16,6 +16,7 @@
 #   Control     Tor controller (arm, vidalia, etc).
 
 # TODO: add recognizing of CLIENT connection type
+DestAttr = enum.Enum("NONE", "LOCALE", "HOSTNAME")
 Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "SOCKS", "CLIENT", "DIRECTORY", "CONTROL")
 CATEGORY_COLOR = {Category.INBOUND: "green", Category.OUTBOUND: "blue",
                   Category.EXIT: "red",      Category.SOCKS: "cyan",
@@ -160,6 +161,56 @@
       return Category.EXIT if isExitConnection else Category.OUTBOUND
     else: return self.baseType
   
+  def getDestinationLabel(self, maxLength, extraAttr=DestAttr.NONE):
+    """
+    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
+    parentheses. The IP address is scrubbed from private connections.
+    
+    Extra information is...
+    - the port's purpose for exit connections
+    - the extraAttr if the address isn't private and isn't on the local network
+    - nothing otherwise
+    
+    Arguments:
+      maxLength - maximum length of the string returned
+    """
+    
+    # destination of the connection
+    if self.isPrivate():
+      dstAddress = "<scrubbed>:%s" % self.foreign.getPort()
+    else:
+      dstAddress = "%s:%s" % (self.foreign.getIpAddr(), self.foreign.getPort())
+    
+    # Only append the extra info if there's at least a couple characters of
+    # space (this is what's needed for the country codes).
+    if len(dstAddress) + 5 <= maxLength:
+      spaceAvailable = maxLength - len(dstAddress) - 3
+      
+      if self.getType() == Category.EXIT:
+        purpose = connections.getPortUsage(self.foreign.getPort())
+        
+        if purpose:
+          # BitTorrent is a common protocol to truncate, so just use "Torrent"
+          # if there's not enough room.
+          if len(purpose) > spaceAvailable and purpose == "BitTorrent":
+            purpose = "Torrent"
+          
+          # crops with a hyphen if too long
+          purpose = uiTools.cropStr(purpose, spaceAvailable, endType = uiTools.Ending.HYPHEN)
+          
+          dstAddress += " (%s)" % purpose
+      elif not connections.isIpAddressPrivate(self.foreign.getIpAddr()):
+        if extraAttr == DestAttr.LOCALE:
+          dstAddress += " (%s)" % self.foreign.getLocale()
+        elif extraAttr == DestAttr.HOSTNAME:
+          dstHostname = self.foreign.getHostname()
+          
+          if dstHostname:
+            dstAddress += " (%s)" % uiTools.cropStr(dstHostname, spaceAvailable)
+    
+    return dstAddress[:maxLength]
+  
   def isPrivate(self):
     """
     Returns true if the endpoint is private, possibly belonging to a client
@@ -228,36 +279,8 @@
     
     conn = torTools.getConn()
     myType = self.getType()
+    dstAddress = self.getDestinationLabel(26, DestAttr.LOCALE)
     
-    # destination of the connection
-    if self.isPrivate():
-      dstAddress = "<scrubbed>:%s" % self.foreign.getPort()
-    else:
-      dstAddress = "%s:%s" % (self.foreign.getIpAddr(), self.foreign.getPort())
-    
-    # Appends an extra field which could be...
-    # - the port's purpose for exits
-    # - locale for most other connections
-    # - blank if it's on the local network
-    
-    if myType == Category.EXIT:
-      purpose = connections.getPortUsage(self.foreign.getPort())
-      
-      if purpose:
-        spaceAvailable = 26 - len(dstAddress) - 3
-        
-        # BitTorrent is a common protocol to truncate, so just use "Torrent"
-        # if there's not enough room.
-        if len(purpose) > spaceAvailable and purpose == "BitTorrent":
-          purpose = "Torrent"
-        
-        # crops with a hyphen if too long
-        purpose = uiTools.cropStr(purpose, spaceAvailable, endType = uiTools.Ending.HYPHEN)
-        
-        dstAddress += " (%s)" % purpose
-    elif not connections.isIpAddressPrivate(self.foreign.getIpAddr()):
-      dstAddress += " (%s)" % self.foreign.getLocale()
-    
     src, dst, etc = "", "", ""
     if listingType == connPanel.Listing.IP:
       # base data requires 73 characters

Modified: arm/trunk/src/interface/controller.py
===================================================================
--- arm/trunk/src/interface/controller.py	2011-02-20 21:31:49 UTC (rev 24248)
+++ arm/trunk/src/interface/controller.py	2011-02-21 06:00:17 UTC (rev 24249)
@@ -745,7 +745,7 @@
           isResize = lastSize != newSize
           lastSize = newSize
           
-          if panelKey in ("header", "graph", "log", "config", "torrc"):
+          if panelKey in ("header", "graph", "log", "config", "torrc", "conn2"):
             # revised panel (manages its own content refreshing)
             panels[panelKey].redraw(isResize)
           else:
@@ -1280,7 +1280,9 @@
       hostnames.setPaused(True)
       panels["conn"].sortConnections()
     elif page == 1 and panels["conn"].isCursorEnabled and uiTools.isSelectionKey(key):
-      # TODO: deprecated when migrated to the new connection panel
+      # TODO: deprecated when migrated to the new connection panel, thought as
+      # well keep around until there's a counterpart for hostname fetching
+      
       # provides details on selected connection
       panel.CURSES_LOCK.acquire()
       try:

Modified: arm/trunk/src/util/torTools.py
===================================================================
--- arm/trunk/src/util/torTools.py	2011-02-20 21:31:49 UTC (rev 24248)
+++ arm/trunk/src/util/torTools.py	2011-02-21 06:00:17 UTC (rev 24249)
@@ -246,6 +246,8 @@
     self._fingerprintLookupCache = {}   # lookup cache with (ip, port) -> fingerprint mappings
     self._fingerprintsAttachedCache = None # cache of relays we're connected to
     self._nicknameLookupCache = {}      # lookup cache with fingerprint -> nickname mappings
+    self._consensusLookupCache = {}     # lookup cache with network status entries
+    self._descriptorLookupCache = {}    # lookup cache with relay descriptors
     self._isReset = False               # internal flag for tracking resets
     self._status = State.CLOSED         # current status of the attached control port
     self._statusTime = 0                # unix time-stamp for the duration of the status
@@ -299,6 +301,8 @@
       self._fingerprintLookupCache = {}
       self._fingerprintsAttachedCache = None
       self._nicknameLookupCache = {}
+      self._consensusLookupCache = {}
+      self._descriptorLookupCache = {}
       
       self._exitPolicyChecker = self.getExitPolicy()
       self._isExitingAllowed = self._exitPolicyChecker.isExitingAllowed()
@@ -805,8 +809,55 @@
     
     return result
   
-  def getRelayFingerprint(self, relayAddress, relayPort = None):
+  def getConsensusEntry(self, relayFingerprint):
     """
+    Provides the most recently available consensus information for the given
+    relay. This is none if no such information exists.
+    
+    Arguments:
+      relayFingerprint - fingerprint of the relay
+    """
+    
+    self.connLock.acquire()
+    
+    result = None
+    if self.isAlive():
+      if not relayFingerprint in self._consensusLookupCache:
+        nsEntry = self.getInfo("ns/id/%s" % relayFingerprint)
+        self._consensusLookupCache[relayFingerprint] = nsEntry
+      
+      result = self._consensusLookupCache[relayFingerprint]
+    
+    self.connLock.release()
+    
+    return result
+  
+  def getDescriptorEntry(self, relayFingerprint):
+    """
+    Provides the most recently available descriptor information for the given
+    relay. Unless FetchUselessDescriptors is set this may frequently be
+    unavailable. If no such descriptor is available then this returns None.
+    
+    Arguments:
+      relayFingerprint - fingerprint of the relay
+    """
+    
+    self.connLock.acquire()
+    
+    result = None
+    if self.isAlive():
+      if not relayFingerprint in self._descriptorLookupCache:
+        descEntry = self.getInfo("desc/id/%s" % relayFingerprint)
+        self._descriptorLookupCache[relayFingerprint] = descEntry
+      
+      result = self._descriptorLookupCache[relayFingerprint]
+    
+    self.connLock.release()
+    
+    return result
+  
+  def getRelayFingerprint(self, relayAddress, relayPort = None, getAllMatches = False):
+    """
     Provides the fingerprint associated with the given address. If there's
     multiple potential matches or the mapping is unknown then this returns
     None. This disambiguates the fingerprint if there's multiple relays on
@@ -814,20 +865,32 @@
     we have a connection with.
     
     Arguments:
-      relayAddress - address of relay to be returned
-      relayPort    - orport of relay (to further narrow the results)
+      relayAddress  - address of relay to be returned
+      relayPort     - orport of relay (to further narrow the results)
+      getAllMatches - ignores the relayPort and provides all of the
+                      (port, fingerprint) tuples matching the given
+                      address
     """
     
     self.connLock.acquire()
     
     result = None
     if self.isAlive():
-      # query the fingerprint if it isn't yet cached
-      if not (relayAddress, relayPort) in self._fingerprintLookupCache:
-        relayFingerprint = self._getRelayFingerprint(relayAddress, relayPort)
-        self._fingerprintLookupCache[(relayAddress, relayPort)] = relayFingerprint
-      
-      result = self._fingerprintLookupCache[(relayAddress, relayPort)]
+      if getAllMatches:
+        # populates the ip -> fingerprint mappings if not yet available
+        if self._fingerprintMappings == None:
+          self._fingerprintMappings = self._getFingerprintMappings()
+        
+        if relayAddress in self._fingerprintMappings:
+          result = self._fingerprintMappings[relayAddress]
+        else: result = []
+      else:
+        # query the fingerprint if it isn't yet cached
+        if not (relayAddress, relayPort) in self._fingerprintLookupCache:
+          relayFingerprint = self._getRelayFingerprint(relayAddress, relayPort)
+          self._fingerprintLookupCache[(relayAddress, relayPort)] = relayFingerprint
+        
+        result = self._fingerprintLookupCache[(relayAddress, relayPort)]
     
     self.connLock.release()
     
@@ -854,7 +917,7 @@
           self._nicknameLookupCache[relayFingerprint] = myNickname
         else:
           # check the consensus for the relay
-          nsEntry = self.getInfo("ns/id/%s" % relayFingerprint)
+          nsEntry = self.getConsensusEntry(relayFingerprint)
           
           if nsEntry: relayNickname = nsEntry[2:nsEntry.find(" ", 2)]
           else: relayNickname = None
@@ -1108,6 +1171,7 @@
   
   def ns_event(self, event):
     self._updateHeartbeat()
+    self._consensusLookupCache = {}
     
     myFingerprint = self.getInfo("fingerprint")
     if myFingerprint:
@@ -1135,6 +1199,7 @@
     self._fingerprintLookupCache = {}
     self._fingerprintsAttachedCache = None
     self._nicknameLookupCache = {}
+    self._consensusLookupCache = {}
     
     if self._fingerprintMappings != None:
       self._fingerprintMappings = self._getFingerprintMappings(event.nslist)
@@ -1155,6 +1220,7 @@
     # the new relays.
     self._fingerprintLookupCache = {}
     self._fingerprintsAttachedCache = None
+    self._descriptorLookupCache = {}
     
     if self._fingerprintMappings != None:
       for fingerprint in event.idlist:

Modified: arm/trunk/src/util/uiTools.py
===================================================================
--- arm/trunk/src/util/uiTools.py	2011-02-20 21:31:49 UTC (rev 24248)
+++ arm/trunk/src/util/uiTools.py	2011-02-21 06:00:17 UTC (rev 24249)
@@ -166,6 +166,7 @@
   
   # checks if there isn't the minimum space needed to include anything
   lastWordbreak = msg.rfind(" ", 0, size + 1)
+  lastWordbreak = len(msg[:lastWordbreak].rstrip()) # drops extra ending whitespaces
   if (minWordLen != None and size < minWordLen) or (minWordLen == None and lastWordbreak < 1):
     if getRemainder: return ("", msg)
     else: return ""
@@ -183,13 +184,14 @@
     returnMsg, remainder = msg[:size], msg[size:]
     if endType == Ending.HYPHEN:
       remainder = returnMsg[-1] + remainder
-      returnMsg = returnMsg[:-1] + "-"
+      returnMsg = returnMsg[:-1].rstrip() + "-"
   else: returnMsg, remainder = msg[:lastWordbreak], msg[lastWordbreak:]
   
   # if this is ending with a comma or period then strip it off
   if not getRemainder and returnMsg[-1] in (",", "."): returnMsg = returnMsg[:-1]
   
-  if endType == Ending.ELLIPSE: returnMsg += "..."
+  if endType == Ending.ELLIPSE:
+    returnMsg = returnMsg.rstrip() + "..."
   
   if getRemainder: return (returnMsg, remainder)
   else: return returnMsg
@@ -209,17 +211,17 @@
   
   # draws the top and bottom
   panel.hline(top, left + 1, width - 1, attr)
-  panel.hline(top + height, left + 1, width - 1, attr)
+  panel.hline(top + height - 1, left + 1, width - 1, attr)
   
   # draws the left and right sides
-  panel.vline(top + 1, left, height - 1, attr)
-  panel.vline(top + 1, left + width, height - 1, attr)
+  panel.vline(top + 1, left, height - 2, attr)
+  panel.vline(top + 1, left + width, height - 2, attr)
   
   # draws the corners
   panel.addch(top, left, curses.ACS_ULCORNER, attr)
   panel.addch(top, left + width, curses.ACS_URCORNER, attr)
-  panel.addch(top + height, left, curses.ACS_LLCORNER, attr)
-  panel.addch(top + height, left + width, curses.ACS_LRCORNER, attr)
+  panel.addch(top + height - 1, left, curses.ACS_LLCORNER, attr)
+  panel.addch(top + height - 1, left + width, curses.ACS_LRCORNER, attr)
 
 def isSelectionKey(key):
   """

_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits