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

[or-cvs] r20354: {arm} Several fixes and changes, mostly concerning the graph panel (in arm/trunk: . interface)



Author: atagar
Date: 2009-08-23 02:44:45 -0400 (Sun, 23 Aug 2009)
New Revision: 20354

Modified:
   arm/trunk/interface/bandwidthMonitor.py
   arm/trunk/interface/confPanel.py
   arm/trunk/interface/connCountMonitor.py
   arm/trunk/interface/connPanel.py
   arm/trunk/interface/controller.py
   arm/trunk/interface/descriptorPopup.py
   arm/trunk/interface/graphPanel.py
   arm/trunk/interface/headerPanel.py
   arm/trunk/interface/util.py
   arm/trunk/readme.txt
Log:
Several fixes and changes, mostly concerning the graph panel and making better use of screen real estate.
added: labeled the graph's x-axis and reordered the information with changes omitted for small (tty sized) terminals (feature request by StrangeCharm)
added: doubling up contents of header panel in case of wide screens to take advantage of added space
added: exit policy to header if a wide display
change: added precision for bandwidth measurements
change: using "orconn-status" info to eliminated ambiguity in identifying inbound connection fingerprints (clever idea, but had very little impact)
fix: when sighup signal is received reloads torrc and internal state (caught by StrangeCharm)
fix: probable resolution of nasty concurrent bug concerning access to connection cache
fix: minor issues concerning connection panel including graph widths and miscalculating local maxima
fix: short circuits fingerprint cache when looking up localhost descriptor (preventing lookup failures)
fix: minor issues with connection panel and description popups when no connections are available
fix: descriptor popup wasn't determining if the first visible line belonged to an encryption block
fix: made interface more resilient against arbitrary resizing (such as during popups)



Modified: arm/trunk/interface/bandwidthMonitor.py
===================================================================
--- arm/trunk/interface/bandwidthMonitor.py	2009-08-22 01:49:25 UTC (rev 20353)
+++ arm/trunk/interface/bandwidthMonitor.py	2009-08-23 06:44:45 UTC (rev 20354)
@@ -11,6 +11,10 @@
 DL_COLOR = "green"  # download section color
 UL_COLOR = "cyan"   # upload section color
 
+# width at which panel abandons placing optional stats (avg and total) with
+# header in favor of replacing the x-axis label
+COLLAPSE_WIDTH = 120
+
 class BandwidthMonitor(graphPanel.GraphStats, TorCtl.PostEventListener):
   """
   Tor event listener, taking bandwidth sampling to draw a bar graph. This is
@@ -42,6 +46,18 @@
     self._processEvent(event.read / 1024.0, event.written / 1024.0)
   
   def redraw(self, panel):
+    # if display is narrow, overwrites x-axis labels with avg / total stats
+    if panel.maxX <= COLLAPSE_WIDTH:
+      # clears line
+      panel.addstr(8, 0, " " * 200)
+      graphCol = min((panel.maxX - 10) / 2, graphPanel.MAX_GRAPH_COL)
+      
+      primaryFooter = "%s, %s" % (self._getAvgLabel(True), self._getTotalLabel(True))
+      secondaryFooter = "%s, %s" % (self._getAvgLabel(False), self._getTotalLabel(False))
+      
+      panel.addstr(8, 1, primaryFooter, util.getColor(self.primaryColor))
+      panel.addstr(8, graphCol + 6, secondaryFooter, util.getColor(self.secondaryColor))
+    
     # provides accounting stats if enabled
     if self.isAccounting:
       if not self.isPaused: self._updateAccountingInfo()
@@ -68,15 +84,38 @@
     
     return labelContents
   
-  def getHeaderLabel(self, isPrimary):
-    if isPrimary: return "Downloaded (%s/sec):" % util.getSizeLabel(self.lastPrimary * 1024)
-    else: return "Uploaded (%s/sec):" % util.getSizeLabel(self.lastSecondary * 1024)
+  def getHeaderLabel(self, width, isPrimary):
+    graphType = "Downloaded" if isPrimary else "Uploaded"
+    total = self.primaryTotal if isPrimary else self.secondaryTotal
+    stats = [""]
+    
+    # conditional is to avoid flickering as stats change size for tty terminals
+    if width * 2 > COLLAPSE_WIDTH:
+      stats = [""] * 3
+      stats[1] = "- %s" % self._getAvgLabel(isPrimary)
+      stats[2] = ", %s" % self._getTotalLabel(isPrimary)
+    
+    stats[0] = "%-14s" % ("%s/sec" % util.getSizeLabel((self.lastPrimary if isPrimary else self.lastSecondary) * 1024, 1))
+    
+    labeling = graphType + " (" + "".join(stats).strip() + "):"
+    while (len(labeling) >= width):
+      if len(stats) > 1:
+        del stats[-1]
+        labeling = graphType + " (" + "".join(stats).strip() + "):"
+      else:
+        labeling = graphType + ":"
+        break
+    
+    return labeling
   
-  def getFooterLabel(self, isPrimary):
+  def _getAvgLabel(self, isPrimary):
     total = self.primaryTotal if isPrimary else self.secondaryTotal
-    avg = total / max(1, self.tick)
-    return "avg: %s/sec, total: %s" % (util.getSizeLabel(avg * 1024), util.getSizeLabel(total * 1024))
+    return "avg: %s/sec" % util.getSizeLabel((total / max(1, self.tick)) * 1024, 1)
   
+  def _getTotalLabel(self, isPrimary):
+    total = self.primaryTotal if isPrimary else self.secondaryTotal
+    return "total: %s" % util.getSizeLabel(total * 1024, 1)
+  
   def _updateAccountingInfo(self):
     """
     Updates mapping used for accounting info. This includes the following keys:

Modified: arm/trunk/interface/confPanel.py
===================================================================
--- arm/trunk/interface/confPanel.py	2009-08-22 01:49:25 UTC (rev 20353)
+++ arm/trunk/interface/confPanel.py	2009-08-23 06:44:45 UTC (rev 20354)
@@ -76,7 +76,7 @@
             numOffset = numFieldWidth + 1
           
           command, argument, comment = "", "", ""
-          if not lineText: continue # no text
+          if not lineText: pass # no text
           elif lineText[0] == "#":
             # whole line is commented out
             comment = lineText

Modified: arm/trunk/interface/connCountMonitor.py
===================================================================
--- arm/trunk/interface/connCountMonitor.py	2009-08-22 01:49:25 UTC (rev 20353)
+++ arm/trunk/interface/connCountMonitor.py	2009-08-23 06:44:45 UTC (rev 20354)
@@ -18,7 +18,7 @@
   def __init__(self, connectionPanel):
     graphPanel.GraphStats.__init__(self)
     TorCtl.PostEventListener.__init__(self)
-    graphPanel.GraphStats.initialize(self, connPanel.TYPE_COLORS["inbound"], connPanel.TYPE_COLORS["outbound"], 9)
+    graphPanel.GraphStats.initialize(self, connPanel.TYPE_COLORS["inbound"], connPanel.TYPE_COLORS["outbound"], 10)
     self.connectionPanel = connectionPanel  # connection panel, used to limit netstat calls
   
   def bandwidth_event(self, event):
@@ -54,7 +54,7 @@
   def getTitle(self, width):
     return "Connection Count:"
   
-  def getHeaderLabel(self, isPrimary):
+  def getHeaderLabel(self, width, isPrimary):
     avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
     if isPrimary: return "Inbound (%s, avg: %s):" % (self.lastPrimary, avg)
     else: return "Outbound (%s, avg: %s):" % (self.lastSecondary, avg)

Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py	2009-08-22 01:49:25 UTC (rev 20353)
+++ arm/trunk/interface/connPanel.py	2009-08-23 06:44:45 UTC (rev 20354)
@@ -6,6 +6,7 @@
 import time
 import socket
 import curses
+from threading import RLock
 from TorCtl import TorCtl
 
 import hostnameResolver
@@ -16,7 +17,7 @@
 LIST_LABEL = {LIST_IP: "IP Address", LIST_HOSTNAME: "Hostname", LIST_FINGERPRINT: "Fingerprint", LIST_NICKNAME: "Nickname"}
 
 # attributes for connection types
-TYPE_COLORS = {"inbound": "green", "outbound": "blue", "control": "red", "localhost": "cyan"}
+TYPE_COLORS = {"inbound": "green", "outbound": "blue", "control": "red", "localhost": "yellow"}
 TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "control": 2, "localhost": 3} # defines ordering
 
 # enums for indexes of ConnPanel 'connections' fields
@@ -94,14 +95,17 @@
     self.showLabel = True           # shows top label if true, hides otherwise
     self.showingDetails = False     # augments display to accomidate details window if true
     self.lastUpdate = -1            # time last stats was retrived
+    self.localhostEntry = None      # special connection - tuple with (entry for this node, fingerprint)
     self.sortOrdering = [ORD_TYPE, ORD_FOREIGN_LISTING, ORD_FOREIGN_PORT]
     self.isPaused = False
     self.resolver = hostnameResolver.HostnameResolver()
     self.fingerprintLookupCache = {}                              # chache of (ip, port) -> fingerprint
     self.nicknameLookupCache = {}                                 # chache of (ip, port) -> nickname
-    self.fingerprintMappings = _getFingerprintMappings(self.conn) # mappings of ip -> [(port, OR identity), ...]
+    self.fingerprintMappings = _getFingerprintMappings(self.conn) # mappings of ip -> [(port, fingerprint, nickname), ...]
     self.nickname = self.conn.get_option("Nickname")[0][1]
     self.providedGeoipWarning = False
+    self.orconnStatusCache = []           # cache for 'orconn-status' calls
+    self.orconnStatusCacheValid = False   # indicates if cache has been invalidated
     
     self.isCursorEnabled = True
     self.cursorSelection = None
@@ -115,6 +119,7 @@
     # netstat results are tuples of the form:
     # (type, local IP, local port, foreign IP, foreign port, country code)
     self.connections = []
+    self.connectionsLock = RLock()    # limits modifications of connections
     
     # count of total inbound, outbound, and control connections
     self.connectionCount = [0, 0, 0]
@@ -123,12 +128,15 @@
   
   # when consensus changes update fingerprint mappings
   def new_consensus_event(self, event):
+    self.orconnStatusCacheValid = False
     self.fingerprintLookupCache.clear()
     self.nicknameLookupCache.clear()
     self.fingerprintMappings = _getFingerprintMappings(self.conn, event.nslist)
     if self.listingType != LIST_HOSTNAME: self.sortConnections()
   
   def new_desc_event(self, event):
+    self.orconnStatusCacheValid = False
+    
     for fingerprint in event.idlist:
       # clears entries with this fingerprint from the cache
       if fingerprint in self.fingerprintLookupCache.values():
@@ -150,17 +158,17 @@
       if nsEntry.ip in self.fingerprintMappings.keys():
         # if entry already exists with the same orport, remove it
         orportMatch = None
-        for entryPort, entryFingerprint in self.fingerprintMappings[nsEntry.ip]:
+        for entryPort, entryFingerprint, entryNickname in self.fingerprintMappings[nsEntry.ip]:
           if entryPort == nsEntry.orport:
-            orportMatch = (entryPort, entryFingerprint)
+            orportMatch = (entryPort, entryFingerprint, entryNickname)
             break
         
         if orportMatch: self.fingerprintMappings[nsEntry.ip].remove(orportMatch)
         
         # add new entry
-        self.fingerprintMappings[nsEntry.ip].append((nsEntry.orport, nsEntry.idhex))
+        self.fingerprintMappings[nsEntry.ip].append((nsEntry.orport, nsEntry.idhex, nsEntry.nickname))
       else:
-        self.fingerprintMappings[nsEntry.ip] = [(nsEntry.orport, nsEntry.idhex)]
+        self.fingerprintMappings[nsEntry.ip] = [(nsEntry.orport, nsEntry.idhex, nsEntry.nickname)]
     if self.listingType != LIST_HOSTNAME: self.sortConnections()
   
   def reset(self):
@@ -169,69 +177,77 @@
     """
     
     if self.isPaused or not self.pid: return
-    self.connections = []
-    self.connectionCount = [0, 0, 0]
-    
-    # looks at netstat for tor with stderr redirected to /dev/null, options are:
-    # n = prevents dns lookups, p = include process (say if it's tor), t = tcp only
-    netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor 2> /dev/null" % self.pid)
+    self.connectionsLock.acquire()
     try:
-      results = netstatCall.readlines()
+      self.connections = []
+      self.connectionCount = [0, 0, 0]
       
-      for line in results:
-        if not line.startswith("tcp"): continue
-        param = line.split()
-        local, foreign = param[3], param[4]
-        localIP, foreignIP = local[:local.find(":")], foreign[:foreign.find(":")]
-        localPort, foreignPort = local[len(localIP) + 1:], foreign[len(foreignIP) + 1:]
+      # looks at netstat for tor with stderr redirected to /dev/null, options are:
+      # n = prevents dns lookups, p = include process (say if it's tor), t = tcp only
+      netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor 2> /dev/null" % self.pid)
+      try:
+        results = netstatCall.readlines()
         
-        if localPort in (self.orPort, self.dirPort):
-          type = "inbound"
-          self.connectionCount[0] += 1
-        elif localPort == self.controlPort:
-          type = "control"
-          self.connectionCount[2] += 1
-        else:
-          type = "outbound"
-          self.connectionCount[1] += 1
-        
+        for line in results:
+          if not line.startswith("tcp"): continue
+          param = line.split()
+          local, foreign = param[3], param[4]
+          localIP, foreignIP = local[:local.find(":")], foreign[:foreign.find(":")]
+          localPort, foreignPort = local[len(localIP) + 1:], foreign[len(foreignIP) + 1:]
+          
+          if localPort in (self.orPort, self.dirPort):
+            type = "inbound"
+            self.connectionCount[0] += 1
+          elif localPort == self.controlPort:
+            type = "control"
+            self.connectionCount[2] += 1
+          else:
+            type = "outbound"
+            self.connectionCount[1] += 1
+          
+          try:
+            countryCodeQuery = "ip-to-country/%s" % foreign[:foreign.find(":")]
+            countryCode = self.conn.get_info(countryCodeQuery)[countryCodeQuery]
+          except socket.error:
+            countryCode = "??"
+            if not self.providedGeoipWarning:
+              self.logger.monitor_event("WARN", "Tor geoip database is unavailable.")
+              self.providedGeoipWarning = True
+          
+          self.connections.append((type, localIP, localPort, foreignIP, foreignPort, countryCode))
+      except IOError:
+        # netstat call failed
+        self.logger.monitor_event("WARN", "Unable to query netstat for new connections")
+      
+      # appends localhost connection to allow user to look up their own consensus entry
+      selfAddress, selfPort, selfFingerprint = None, None, None
+      try:
+        selfAddress = self.conn.get_info("address")["address"]
+        selfPort = self.conn.get_option("ORPort")[0][1]
+        selfFingerprint = self.conn.get_info("fingerprint")["fingerprint"]
+      except TorCtl.ErrorReply: pass
+      except TorCtl.TorCtlClosed: pass
+      except socket.error: pass
+      
+      if selfAddress and selfPort and selfFingerprint:
         try:
-          countryCodeQuery = "ip-to-country/%s" % foreign[:foreign.find(":")]
-          countryCode = self.conn.get_info(countryCodeQuery)[countryCodeQuery]
+          countryCodeQuery = "ip-to-country/%s" % selfAddress
+          selfCountryCode = self.conn.get_info(countryCodeQuery)[countryCodeQuery]
         except socket.error:
-          countryCode = "??"
-          if not self.providedGeoipWarning:
-            self.logger.monitor_event("WARN", "Tor geoip database is unavailable.")
-            self.providedGeoipWarning = True
+          selfCountryCode = "??"
         
-        self.connections.append((type, localIP, localPort, foreignIP, foreignPort, countryCode))
-    except IOError:
-      # netstat call failed
-      self.logger.monitor_event("WARN", "Unable to query netstat for new connections")
-    
-    # appends localhost connection to allow user to look up their own consensus entry
-    selfAddress, selfPort, selfFingerprint = None, None, None
-    try:
-      selfAddress = self.conn.get_info("address")["address"]
-      selfPort = self.conn.get_option("ORPort")[0][1]
-      selfFingerprint = self.conn.get_info("fingerprint")["fingerprint"]
-    except TorCtl.ErrorReply: pass
-    except TorCtl.TorCtlClosed: pass
-    except socket.error: pass
-    
-    if selfAddress and selfPort and selfFingerprint:
-      try:
-        countryCodeQuery = "ip-to-country/%s" % selfAddress
-        selfCountryCode = self.conn.get_info(countryCodeQuery)[countryCodeQuery]
-      except socket.error:
-        selfCountryCode = "??"
-      self.connections.append(("localhost", selfAddress, selfPort, selfAddress, selfPort, selfCountryCode))
-    
-    netstatCall.close()
-    self.lastUpdate = time.time()
-    
-    # hostnames are sorted at redraw - otherwise now's a good time
-    if self.listingType != LIST_HOSTNAME: self.sortConnections()
+        self.localhostEntry = (("localhost", selfAddress, selfPort, selfAddress, selfPort, selfCountryCode), selfFingerprint)
+        self.connections.append(self.localhostEntry[0])
+      else:
+        self.localhostEntry = None
+      
+      netstatCall.close()
+      self.lastUpdate = time.time()
+      
+      # hostnames are sorted at redraw - otherwise now's a good time
+      if self.listingType != LIST_HOSTNAME: self.sortConnections()
+    finally:
+      self.connectionsLock.release()
   
   def handleKey(self, key):
     # cursor or scroll movement
@@ -240,27 +256,31 @@
       pageHeight = self.maxY - 1
       if self.showingDetails: pageHeight -= 8
       
-      # determines location parameter to use
-      if self.isCursorEnabled:
-        try: currentLoc = self.connections.index(self.cursorSelection)
-        except ValueError: currentLoc = self.cursorLoc # fall back to nearby entry
-      else: currentLoc = self.scroll
-      
-      # location offset
-      if key == curses.KEY_UP: shift = -1
-      elif key == curses.KEY_DOWN: shift = 1
-      elif key == curses.KEY_PPAGE: shift = -pageHeight + 1 if self.isCursorEnabled else -pageHeight
-      elif key == curses.KEY_NPAGE: shift = pageHeight - 1 if self.isCursorEnabled else pageHeight
-      newLoc = currentLoc + shift
-      
-      # restricts to valid bounds
-      maxLoc = len(self.connections) - 1 if self.isCursorEnabled else len(self.connections) - pageHeight
-      newLoc = max(0, min(newLoc, maxLoc))
-      
-      # applies to proper parameter
-      if self.isCursorEnabled and self.connections:
-        self.cursorSelection, self.cursorLoc = self.connections[newLoc], newLoc
-      else: self.scroll = newLoc
+      self.connectionsLock.acquire()
+      try:
+        # determines location parameter to use
+        if self.isCursorEnabled:
+          try: currentLoc = self.connections.index(self.cursorSelection)
+          except ValueError: currentLoc = self.cursorLoc # fall back to nearby entry
+        else: currentLoc = self.scroll
+        
+        # location offset
+        if key == curses.KEY_UP: shift = -1
+        elif key == curses.KEY_DOWN: shift = 1
+        elif key == curses.KEY_PPAGE: shift = -pageHeight + 1 if self.isCursorEnabled else -pageHeight
+        elif key == curses.KEY_NPAGE: shift = pageHeight - 1 if self.isCursorEnabled else pageHeight
+        newLoc = currentLoc + shift
+        
+        # restricts to valid bounds
+        maxLoc = len(self.connections) - 1 if self.isCursorEnabled else len(self.connections) - pageHeight
+        newLoc = max(0, min(newLoc, maxLoc))
+        
+        # applies to proper parameter
+        if self.isCursorEnabled and self.connections:
+          self.cursorSelection, self.cursorLoc = self.connections[newLoc], newLoc
+        else: self.scroll = newLoc
+      finally:
+        self.connectionsLock.release()
     elif key == ord('c') or key == ord('C'):
       self.isCursorEnabled = not self.isCursorEnabled
     elif key == ord('r') or key == ord('R'):
@@ -273,6 +293,7 @@
   def redraw(self):
     if self.win:
       if not self.lock.acquire(False): return
+      self.connectionsLock.acquire()
       try:
         # hostnames frequently get updated so frequent sorting needed
         if self.listingType == LIST_HOSTNAME: self.sortConnections()
@@ -341,6 +362,7 @@
         self.refresh()
       finally:
         self.lock.release()
+        self.connectionsLock.release()
   
   def getFingerprint(self, ipAddr, port):
     """
@@ -349,20 +371,35 @@
     returns "UNKNOWN".
     """
     
+    # checks to see if this matches the localhost entry
+    if self.localhostEntry and ipAddr == self.localhostEntry[0][CONN_L_IP] and port == self.localhostEntry[0][CONN_L_PORT]:
+      return self.localhostEntry[1]
+    
     port = int(port)
     if (ipAddr, port) in self.fingerprintLookupCache:
       return self.fingerprintLookupCache[(ipAddr, port)]
     else:
       match = None
       
+      # orconn-status provides a listing of Tor's current connections - used to
+      # eliminated ambiguity for inbound connections
+      if not self.orconnStatusCacheValid:
+        self.orconnStatusCache, isOdd = [], True
+        self.orconnStatusCacheValid = True
+        try:
+          for entry in self.conn.get_info("orconn-status")["orconn-status"].split():
+            if isOdd: self.orconnStatusCache.append(entry)
+            isOdd = not isOdd
+        except TorCtl.TorCtlClosed: self.orconnStatusCache = None
+        except TorCtl.ErrorReply: self.orconnStatusCache = None
+      
       if ipAddr in self.fingerprintMappings.keys():
         potentialMatches = self.fingerprintMappings[ipAddr]
         
         if len(potentialMatches) == 1: match = potentialMatches[0][1]
-        
-        if not match:
+        else:
           # multiple potential matches - look for exact match with port
-          for (entryPort, entryFingerprint) in potentialMatches:
+          for (entryPort, entryFingerprint, entryNickname) in potentialMatches:
             if entryPort == port:
               match = entryFingerprint
               break
@@ -374,21 +411,29 @@
           # ... list a bandwidth of 0
           # ... have 'opt hibernating' set
           operativeMatches = list(potentialMatches)
-          for (entryPort, entryFingerprint) in potentialMatches:
+          for entryPort, entryFingerprint, entryNickname in potentialMatches:
             # gets router description to see if 'down' is set
+            toRemove = False
             try:
               nsData = self.conn.get_network_status("id/%s" % entryFingerprint)
-              if len(nsData) != 1: continue # ns lookup failed... weird
+              if len(nsData) != 1: raise TorCtl.ErrorReply() # ns lookup failed... weird
               else: nsEntry = nsData[0]
               
               descLookupCmd = "desc/id/%s" % entryFingerprint
               descEntry = TorCtl.Router.build_from_desc(self.conn.get_info(descLookupCmd)[descLookupCmd].split("\n"), nsEntry)
-              if descEntry.down: operativeMatches.remove((entryPort, entryFingerprint))
+              toRemove = descEntry.down
             except TorCtl.ErrorReply: pass # ns or desc lookup fails... also weird
+            
+            # eliminates connections not reported by orconn-status -
+            # this has *very* little impact since few ips have multiple relays
+            if self.orconnStatusCache and not toRemove: toRemove = entryNickname not in self.orconnStatusCache
+            
+            if toRemove: operativeMatches.remove((entryPort, entryFingerprint, entryNickname))
           
           if len(operativeMatches) == 1: match = operativeMatches[0][1]
-          
+      
       if not match: match = "UNKNOWN"
+      
       self.fingerprintLookupCache[(ipAddr, port)] = match
       return match
   
@@ -451,7 +496,9 @@
         sorts.append(lambda x, y: cmp(listingWrapper(x[CONN_L_IP] if x[CONN_TYPE] == "inbound" else x[CONN_F_IP], x[CONN_F_PORT]), listingWrapper(y[CONN_L_IP] if y[CONN_TYPE] == "inbound" else y[CONN_F_IP], y[CONN_F_PORT])))
       else: sorts.append(SORT_TYPES[entry][2])
     
-    self.connections.sort(lambda x, y: _multisort(x, y, sorts))
+    self.connectionsLock.acquire()
+    try: self.connections.sort(lambda x, y: _multisort(x, y, sorts))
+    finally: self.connectionsLock.release()
 
 # recursively checks primary, secondary, and tertiary sorting parameter in ties
 def _multisort(conn1, conn2, sorts):
@@ -476,8 +523,8 @@
     except TorCtl.TorCtlClosed: nsList = []
   
   for entry in nsList:
-    if entry.ip in ipToFingerprint.keys(): ipToFingerprint[entry.ip].append((entry.orport, entry.idhex))
-    else: ipToFingerprint[entry.ip] = [(entry.orport, entry.idhex)]
+    if entry.ip in ipToFingerprint.keys(): ipToFingerprint[entry.ip].append((entry.orport, entry.idhex, entry.nickname))
+    else: ipToFingerprint[entry.ip] = [(entry.orport, entry.idhex, entry.nickname)]
   
   return ipToFingerprint
 

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2009-08-22 01:49:25 UTC (rev 20353)
+++ arm/trunk/interface/controller.py	2009-08-23 06:44:45 UTC (rev 20354)
@@ -94,6 +94,20 @@
       self.addstr(0, 0, msgText, msgAttr)
       self.refresh()
 
+class sighupListener(TorCtl.PostEventListener):
+  """
+  Listens for reload signal (hup), which is produced by:
+  pkill -sighup tor
+  causing the torrc and internal state to be reset.
+  """
+  
+  def __init__(self):
+    TorCtl.PostEventListener.__init__(self)
+    self.isReset = False
+  
+  def msg_event(self, event):
+    self.isReset |= event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)")
+
 def setPauseState(panels, monitorIsPaused, currentPage, overwrite=False):
   """
   Resets the isPaused state of panels. If overwrite is True then this pauses
@@ -255,16 +269,17 @@
   panels["graph"].setStats("bandwidth")
   
   # listeners that update bandwidth and log panels with Tor status
+  sighupTracker = sighupListener()
   conn.add_event_listener(panels["log"])
   conn.add_event_listener(panels["graph"].stats["bandwidth"])
   conn.add_event_listener(panels["graph"].stats["connection count"])
   conn.add_event_listener(panels["conn"])
+  conn.add_event_listener(sighupTracker)
   
   # tells Tor to listen to the events we're interested
   loggedEvents = setEventListening(loggedEvents, conn, panels["log"])
   panels["log"].loggedEvents = loggedEvents # strips any that couldn't be set
   
-  oldY, oldX = -1, -1
   isUnresponsive = False    # true if it's been over ten seconds since the last BW event (probably due to Tor closing)
   isPaused = False          # if true updates are frozen
   page = 0
@@ -278,21 +293,30 @@
     cursesLock.acquire()
     try:
       y, x = stdscr.getmaxyx()
-      if x > oldX or y > oldY:
-        # gives panels a chance to take advantage of the maximum bounds
-        startY = 0
-        for panelKey in PAGE_S[:2]:
-          panels[panelKey].recreate(stdscr, startY)
-          startY += panels[panelKey].height
+      
+      # if sighup received then reload related information
+      if sighupTracker.isReset:
+        panels["header"]._updateParams(True)
+        panels["torrc"].reset()
+        sighupTracker.isReset = False
+      
+      # gives panels a chance to take advantage of the maximum bounds
+      # originally this checked in the bounds changed but 'recreate' is a no-op
+      # if panel properties are unchanged and checking every redraw is more
+      # resilient in case of funky changes (such as resizing during popups)
+      startY = 0
+      for panelKey in PAGE_S[:2]:
+        panels[panelKey].recreate(stdscr, startY)
+        startY += panels[panelKey].height
+      
+      panels["popup"].recreate(stdscr, startY, 80)
+      
+      for panelSet in PAGES:
+        tmpStartY = startY
         
-        panels["popup"].recreate(stdscr, startY, 80)
-        
-        for panelSet in PAGES:
-          tmpStartY = startY
-          
-          for panelKey in panelSet:
-            panels[panelKey].recreate(stdscr, tmpStartY)
-            tmpStartY += panels[panelKey].height
+        for panelKey in panelSet:
+          panels[panelKey].recreate(stdscr, tmpStartY)
+          tmpStartY += panels[panelKey].height
       
       # if it's been at least ten seconds since the last BW event Tor's probably done
       if not isUnresponsive and panels["log"].getHeartbeat() >= 10:
@@ -311,13 +335,15 @@
       
       # I haven't the foggiest why, but doesn't work if redrawn out of order...
       for panelKey in (PAGE_S + PAGES[page]): panels[panelKey].redraw()
-      oldY, oldX = y, x
       stdscr.refresh()
     finally:
       cursesLock.release()
     
     key = stdscr.getch()
     if key == ord('q') or key == ord('Q'):
+      # quits arm
+      # very occasionally stderr gets "close failed: [Errno 11] Resource temporarily unavailable"
+      # this appears to be a python bug: http://bugs.python.org/issue3014
       daemonThreads = panels["conn"].resolver.threadPool
       
       # sets halt flags for all worker daemon threads
@@ -435,7 +461,6 @@
       if selection != -1 and selection != initialSelection:
         if selection == 0: panels["graph"].setStats(None)
         else: panels["graph"].setStats(options[selection].lower())
-        oldY = -1 # force resize event
     elif page == 0 and (key == ord('i') or key == ord('I')):
       # provides menu to pick graph panel update interval
       options = [label for (label, intervalTime) in graphPanel.UPDATE_INTERVALS]
@@ -543,7 +568,7 @@
           popup.addstr(0, 0, "Connection Details:", util.LABEL_ATTR)
           
           selection = panels["conn"].cursorSelection
-          if not selection: break
+          if not selection or not panels["conn"].connections: break
           selectionColor = connPanel.TYPE_COLORS[selection[connPanel.CONN_TYPE]]
           format = util.getColor(selectionColor) | curses.A_BOLD
           
@@ -583,7 +608,7 @@
               matchings = panels["conn"].fingerprintMappings[selectedIp]
               
               line = 4
-              for (matchPort, matchFingerprint) in matchings:
+              for (matchPort, matchFingerprint, matchNickname) in matchings:
                 popup.addstr(line, 2, "%i. or port: %-5s fingerprint: %s" % (line - 3, matchPort, matchFingerprint), format)
                 line += 1
                 
@@ -592,46 +617,50 @@
                   break
           else:
             # fingerprint found - retrieve related data
+            lookupErrored = False
             if selection in relayLookupCache.keys(): nsEntry, descEntry = relayLookupCache[selection]
             else:
-              # ns lookup fails... weird
+              # ns lookup fails, can happen with localhost lookups if relay's having problems (orport not reachable)
               try: nsData = conn.get_network_status("id/%s" % fingerprint)
-              except TorCtl.ErrorReply: break
-              except TorCtl.TorCtlClosed: break
+              except TorCtl.ErrorReply: lookupErrored = True
+              except TorCtl.TorCtlClosed: lookupErrored = True
               
-              if len(nsData) > 1:
-                # multiple records for fingerprint (shouldn't happen)
-                panels["log"].monitor_event("WARN", "Multiple consensus entries for fingerprint: %s" % fingerprint)
+              if not lookupErrored:
+                if len(nsData) > 1:
+                  # multiple records for fingerprint (shouldn't happen)
+                  panels["log"].monitor_event("WARN", "Multiple consensus entries for fingerprint: %s" % fingerprint)
+                
+                nsEntry = nsData[0]
+                
+                try:
+                  descLookupCmd = "desc/id/%s" % fingerprint
+                  descEntry = TorCtl.Router.build_from_desc(conn.get_info(descLookupCmd)[descLookupCmd].split("\n"), nsEntry)
+                  relayLookupCache[selection] = (nsEntry, descEntry)
+                except TorCtl.ErrorReply: lookupErrored = True # desc lookup failed
+            
+            if lookupErrored:
+              popup.addstr(3, 2, "Unable to retrieve consensus data", format)
+            else:
+              popup.addstr(2, 15, "fingerprint: %s" % fingerprint, format)
               
-              nsEntry = nsData[0]
+              nickname = panels["conn"].getNickname(selectedIp, selectedPort)
+              dirPortLabel = "dirport: %i" % nsEntry.dirport if nsEntry.dirport else ""
+              popup.addstr(3, 2, "nickname: %-25s orport: %-10i %s" % (nickname, nsEntry.orport, dirPortLabel), format)
               
-              try:
-                descLookupCmd = "desc/id/%s" % fingerprint
-                descEntry = TorCtl.Router.build_from_desc(conn.get_info(descLookupCmd)[descLookupCmd].split("\n"), nsEntry)
-              except TorCtl.ErrorReply: break # desc lookup fails... also weird
+              popup.addstr(4, 2, "published: %-24s os: %-14s version: %s" % (descEntry.published, descEntry.os, descEntry.version), format)
+              popup.addstr(5, 2, "flags: %s" % ", ".join(nsEntry.flags), format)
               
-              relayLookupCache[selection] = (nsEntry, descEntry)
-            
-            popup.addstr(2, 15, "fingerprint: %s" % fingerprint, format)
-            
-            nickname = panels["conn"].getNickname(selectedIp, selectedPort)
-            dirPortLabel = "dirport: %i" % nsEntry.dirport if nsEntry.dirport else ""
-            popup.addstr(3, 2, "nickname: %-25s orport: %-10i %s" % (nickname, nsEntry.orport, dirPortLabel), format)
-            
-            popup.addstr(4, 2, "published: %-24s os: %-14s version: %s" % (descEntry.published, descEntry.os, descEntry.version), format)
-            popup.addstr(5, 2, "flags: %s" % ", ".join(nsEntry.flags), format)
-            
-            exitLine = ", ".join([str(k) for k in descEntry.exitpolicy])
-            if len(exitLine) > 63: exitLine = "%s..." % exitLine[:60]
-            popup.addstr(6, 2, "exit policy: %s" % exitLine, format)
-            
-            if descEntry.contact:
-              # clears up some common obscuring
-              contactAddr = descEntry.contact
-              obscuring = [(" at ", "@"), (" AT ", "@"), ("AT", "@"), (" dot ", "."), (" DOT ", ".")]
-              for match, replace in obscuring: contactAddr = contactAddr.replace(match, replace)
-              if len(contactAddr) > 67: contactAddr = "%s..." % contactAddr[:64]
-              popup.addstr(7, 2, "contact: %s" % contactAddr, format)
+              exitLine = ", ".join([str(k) for k in descEntry.exitpolicy])
+              if len(exitLine) > 63: exitLine = "%s..." % exitLine[:60]
+              popup.addstr(6, 2, "exit policy: %s" % exitLine, format)
+              
+              if descEntry.contact:
+                # clears up some common obscuring
+                contactAddr = descEntry.contact
+                obscuring = [(" at ", "@"), (" AT ", "@"), ("AT", "@"), (" dot ", "."), (" DOT ", ".")]
+                for match, replace in obscuring: contactAddr = contactAddr.replace(match, replace)
+                if len(contactAddr) > 67: contactAddr = "%s..." % contactAddr[:64]
+                popup.addstr(7, 2, "contact: %s" % contactAddr, format)
           
           popup.refresh()
           key = stdscr.getch()

Modified: arm/trunk/interface/descriptorPopup.py
===================================================================
--- arm/trunk/interface/descriptorPopup.py	2009-08-22 01:49:25 UTC (rev 20353)
+++ arm/trunk/interface/descriptorPopup.py	2009-08-23 06:44:45 UTC (rev 20354)
@@ -19,7 +19,7 @@
 SIG_END_KEYS = ["-----END RSA PUBLIC KEY-----", "-----END SIGNATURE-----"]
 
 UNRESOLVED_MSG = "No consensus data available"
-ERROR_MSG = "Unable to retrieve consensus data"
+ERROR_MSG = "Unable to retrieve data"
 
 class PopupProperties:
   """
@@ -50,14 +50,19 @@
         nsCommand = "ns/id/%s" % fingerprint
         self.text.append(nsCommand)
         self.text = self.text + self.conn.get_info(nsCommand)[nsCommand].split("\n")
-        
+      except TorCtl.ErrorReply:
+        self.text = self.text + [ERROR_MSG, ""]
+      except TorCtl.TorCtlClosed:
+        self.text = self.text + [ERROR_MSG, ""]
+      
+      try:
         descCommand = "desc/id/%s" % fingerprint
         self.text.append(descCommand)
         self.text = self.text + self.conn.get_info(descCommand)[descCommand].split("\n")
       except TorCtl.ErrorReply:
-        self.fingerprint = None
-        self.showLineNum = False
-        self.text.append(ERROR_MSG)
+        self.text = self.text + [ERROR_MSG]
+      except TorCtl.TorCtlClosed:
+        self.text = self.text + [ERROR_MSG]
   
   def handleKey(self, key, height):
     if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
@@ -80,7 +85,7 @@
   try:
     while isVisible:
       selection = connectionPanel.cursorSelection
-      if not selection: break
+      if not selection or not connectionPanel.connections: break
       fingerprint = connectionPanel.getFingerprint(selection[connPanel.CONN_F_IP], selection[connPanel.CONN_F_PORT])
       entryColor = connPanel.TYPE_COLORS[selection[connPanel.CONN_TYPE]]
       properties.reset(fingerprint, entryColor)
@@ -128,6 +133,13 @@
     else: popup.addstr(0, 0, "Consensus Descriptor:", util.LABEL_ATTR)
     
     isEncryption = False          # true if line is part of an encryption block
+    
+    # checks if first line is in an encryption block
+    for i in range(0, properties.scroll):
+      lineText = properties.text[i].strip()
+      if lineText in SIG_START_KEYS: isEncryption = True
+      elif lineText in SIG_END_KEYS: isEncryption = False
+    
     pageHeight = popup.maxY - 2
     numFieldWidth = int(math.log10(len(properties.text))) + 1
     lineNum = 1

Modified: arm/trunk/interface/graphPanel.py
===================================================================
--- arm/trunk/interface/graphPanel.py	2009-08-22 01:49:25 UTC (rev 20353)
+++ arm/trunk/interface/graphPanel.py	2009-08-23 06:44:45 UTC (rev 20354)
@@ -8,6 +8,7 @@
 import util
 
 MAX_GRAPH_COL = 150  # max columns of data in graph
+WIDE_LABELING_GRAPH_COL = 50  # minimum graph columns to use wide spacing for x-axis labels
 
 # enums for graph bounds:
 #   BOUNDS_MAX - global maximum (highest value ever seen)
@@ -82,20 +83,13 @@
     
     return ""
   
-  def getHeaderLabel(self, isPrimary):
+  def getHeaderLabel(self, width, isPrimary):
     """
     Provides labeling presented at the top of the graph.
     """
     
     return ""
   
-  def getFooterLabel(self, isPrimary):
-    """
-    Provides labeling present at the bottom of the graph.
-    """
-    
-    return ""
-  
   def redraw(self, panel):
     """
     Allows for any custom redrawing monitor wishes to append.
@@ -184,7 +178,7 @@
       if not self.lock.acquire(False): return
       try:
         self.clear()
-        graphCol = min(self.maxX / 2, MAX_GRAPH_COL)
+        graphCol = min((self.maxX - 10) / 2, MAX_GRAPH_COL)
         
         if self.currentDisplay:
           param = self.stats[self.currentDisplay]
@@ -194,7 +188,7 @@
           if self.showLabel: self.addstr(0, 0, param.getTitle(self.maxX), util.LABEL_ATTR)
           
           # top labels
-          left, right = param.getHeaderLabel(True), param.getHeaderLabel(False)
+          left, right = param.getHeaderLabel(self.maxX / 2, True), param.getHeaderLabel(self.maxX / 2, False)
           if left: self.addstr(1, 0, left, curses.A_BOLD | primaryColor)
           if right: self.addstr(1, graphCol + 5, right, curses.A_BOLD | secondaryColor)
           
@@ -205,8 +199,8 @@
             primaryBound = param.maxPrimary[self.updateInterval]
             secondaryBound = param.maxSecondary[self.updateInterval]
           elif self.bounds == BOUNDS_TIGHT:
-            for value in param.primaryCounts[self.updateInterval][1:]: primaryBound = max(value, primaryBound)
-            for value in param.secondaryCounts[self.updateInterval][1:]: secondaryBound = max(value, secondaryBound)
+            for value in param.primaryCounts[self.updateInterval][1:graphCol + 1]: primaryBound = max(value, primaryBound)
+            for value in param.secondaryCounts[self.updateInterval][1:graphCol + 1]: secondaryBound = max(value, secondaryBound)
           
           # displays bound
           self.addstr(2, 0, "%4s" % str(int(primaryBound)), primaryColor)
@@ -224,11 +218,29 @@
             colHeight = min(5, 5 * param.secondaryCounts[self.updateInterval][col + 1] / max(1, secondaryBound))
             for row in range(colHeight): self.addstr(7 - row, col + graphCol + 10, " ", curses.A_STANDOUT | secondaryColor)
           
-          # bottom labels
-          left, right = param.getFooterLabel(True), param.getFooterLabel(False)
-          if left: self.addstr(8, 1, left, primaryColor)
-          if right: self.addstr(8, graphCol + 6, right, secondaryColor)
+          # bottom labeling of x-axis
+          intervalSec = 1
+          for (label, timescale) in UPDATE_INTERVALS:
+            if label == self.updateInterval: intervalSec = timescale
           
+          intervalSpacing = 10 if graphCol >= WIDE_LABELING_GRAPH_COL else 5
+          unitsLabel, decimalPrecision = None, 0
+          for i in range(1, (graphCol + intervalSpacing - 4) / intervalSpacing):
+            loc = i * intervalSpacing
+            timeLabel = util.getTimeLabel(loc * intervalSec, decimalPrecision)
+            
+            if not unitsLabel: unitsLabel = timeLabel[-1]
+            elif unitsLabel != timeLabel[-1]:
+              # upped scale so also up precision of future measurements
+              unitsLabel = timeLabel[-1]
+              decimalPrecision += 1
+            else:
+              # if constrained on space then strips labeling since already provided
+              timeLabel = timeLabel[:-1]
+            
+            self.addstr(8, 4 + loc, timeLabel, primaryColor)
+            self.addstr(8, graphCol + 10 + loc, timeLabel, secondaryColor)
+            
           # allows for finishing touches by monitor
           param.redraw(self)
           

Modified: arm/trunk/interface/headerPanel.py
===================================================================
--- arm/trunk/interface/headerPanel.py	2009-08-22 01:49:25 UTC (rev 20353)
+++ arm/trunk/interface/headerPanel.py	2009-08-23 06:44:45 UTC (rev 20354)
@@ -8,6 +8,10 @@
 
 import util
 
+# minimum width for which panel attempts to double up contents (two columns to
+# better use screen real estate)
+MIN_DUAL_ROW_WIDTH = 140
+
 FLAG_COLORS = {"Authority": "white",  "BadExit": "red",     "BadDirectory": "red",    "Exit": "cyan",
                "Fast": "yellow",      "Guard": "green",     "HSDir": "magenta",       "Named": "blue",
                "Stable": "blue",      "Running": "yellow",  "Unnamed": "magenta",     "Valid": "green",
@@ -37,8 +41,19 @@
     self.vals = {"pid": torPid}     # mapping of information to be presented
     self.conn = conn                # Tor control port connection
     self.isPaused = False
+    self.isWide = False             # doubles up parameters to shorten section if room's available
     self._updateParams()
   
+  def recreate(self, stdscr, startY, maxX=-1):
+    # might need to recreate twice so we have a window to get width
+    if not self.win: util.Panel.recreate(self, stdscr, startY, maxX)
+    
+    self._resetBounds()
+    self.isWide = self.maxX >= MIN_DUAL_ROW_WIDTH
+    self.height = 4 if self.isWide else 6
+    
+    util.Panel.recreate(self, stdscr, startY, maxX)
+  
   def redraw(self):
     if self.win:
       if not self.isPaused: self._updateParams()
@@ -69,24 +84,50 @@
       labelStart = "%s - %s:%s, %sControl Port (" % (self.vals["Nickname"], self.vals["address"], self.vals["ORPort"], dirPortLabel)
       self.addfstr(1, 0, "%s<%s>%s</%s>): %s" % (labelStart, controlPortAuthColor, controlPortAuthLabel, controlPortAuthColor, self.vals["ControlPort"]))
       
-      # Line 3 (system usage info)
-      self.addstr(2, 0, "cpu: %s%%" % self.vals["%cpu"])
-      self.addstr(2, 13, "mem: %s (%s%%)" % (util.getSizeLabel(int(self.vals["rss"]) * 1024), self.vals["%mem"]))
-      self.addstr(2, 34, "pid: %s" % (self.vals["pid"] if self.vals["etime"] else ""))
-      self.addstr(2, 47, "uptime: %s" % self.vals["etime"])
+      # Line 3 (system usage info) - line 1 right if wide
+      y, x = 0 if self.isWide else 2, 75 if self.isWide else 0
+      self.addstr(y, x, "cpu: %s%%" % self.vals["%cpu"])
+      self.addstr(y, x + 13, "mem: %s (%s%%)" % (util.getSizeLabel(int(self.vals["rss"]) * 1024), self.vals["%mem"]))
+      self.addstr(y, x + 34, "pid: %s" % (self.vals["pid"] if self.vals["etime"] else ""))
+      self.addstr(y, x + 47, "uptime: %s" % self.vals["etime"])
       
-      # Line 4 (fingerprint)
-      self.addstr(3, 0, "fingerprint: %s" % self.vals["fingerprint"])
+      # Line 4 (fingerprint) - line 2 right if wide
+      y, x = 1 if self.isWide else 3, 75 if self.isWide else 0
+      self.addstr(y, x, "fingerprint: %s" % self.vals["fingerprint"])
       
-      # Line 5 (flags)
+      # Line 5 (flags) - line 3 left if wide
       flagLine = "flags: "
       for flag in self.vals["flags"]:
         flagColor = FLAG_COLORS[flag] if flag in FLAG_COLORS.keys() else "white"
         flagLine += "<b><%s>%s</%s></b>, " % (flagColor, flag, flagColor)
       
       if len(self.vals["flags"]) > 0: flagLine = flagLine[:-2]
-      self.addfstr(4, 0, flagLine)
+      self.addfstr(2 if self.isWide else 4, 0, flagLine)
       
+      # Line 3 right (exit policy) - not present if not wide
+      if self.isWide:
+        exitPolicy = self.vals["ExitPolicy"]
+        
+        # adds note when default exit policy is appended
+        if exitPolicy == None: exitPolicy = "<default>"
+        elif not exitPolicy.endswith("accept *:*") and not exitPolicy.endswith("reject *:*"):
+          exitPolicy += ", <default>"
+        
+        policies = exitPolicy.split(", ")
+        
+        # color codes accepts to be green, rejects to be red, and default marker to be cyan
+        isSimple = len(policies) <= 2 # if policy is short then it's kept verbose, otherwise 'accept' and 'reject' keywords removed
+        for i in range(len(policies)):
+          policy = policies[i].strip()
+          displayedPolicy = policy if isSimple else policy.replace("accept", "").replace("reject", "").strip()
+          if policy.startswith("accept"): policy = "<green><b>%s</b></green>" % displayedPolicy
+          elif policy.startswith("reject"): policy = "<red><b>%s</b></red>" % displayedPolicy
+          elif policy.startswith("<default>"): policy = "<cyan><b>%s</b></cyan>" % displayedPolicy
+          policies[i] = policy
+        exitPolicy = ", ".join(policies)
+        
+        self.addfstr(2, 75, "exit policy: %s" % exitPolicy)
+      
       self.refresh()
   
   def setPaused(self, isPause):
@@ -96,14 +137,14 @@
     
     self.isPaused = isPause
   
-  def _updateParams(self):
+  def _updateParams(self, forceReload = False):
     """
     Updates mapping of static Tor settings and system information to their
     corresponding string values. Keys include:
     info - version, *address, *fingerprint, *flags, status/version/current
     sys - sys-name, sys-os, sys-version
     ps - *%cpu, *rss, *%mem, *pid, *etime
-    config - Nickname, ORPort, DirPort, ControlPort
+    config - Nickname, ORPort, DirPort, ControlPort, ExitPolicy
     config booleans - IsPasswordAuthSet, IsCookieAuthSet, IsAccountingEnabled
     
     * volatile parameter that'll be reset (otherwise won't be checked if
@@ -111,7 +152,7 @@
     """
     
     infoFields = ["address", "fingerprint"] # keys for which get_info will be called
-    if len(self.vals) <= 1:
+    if len(self.vals) <= 1 or forceReload:
       # first call (only contasns 'pid' mapping) - retrieve static params
       infoFields += ["version", "status/version/current"]
       
@@ -122,7 +163,7 @@
       self.vals["sys-version"] = unameVals[2]
       
       # parameters from the user's torrc
-      configFields = ["Nickname", "ORPort", "DirPort", "ControlPort"]
+      configFields = ["Nickname", "ORPort", "DirPort", "ControlPort", "ExitPolicy"]
       self.vals.update(dict([(key, self.conn.get_option(key)[0][1]) for key in configFields]))
       
       # simply keeps booleans for if authentication info is set

Modified: arm/trunk/interface/util.py
===================================================================
--- arm/trunk/interface/util.py	2009-08-22 01:49:25 UTC (rev 20353)
+++ arm/trunk/interface/util.py	2009-08-23 06:44:45 UTC (rev 20354)
@@ -60,17 +60,30 @@
   
   return COLOR_ATTR[color]
 
-def getSizeLabel(bytes):
+def getSizeLabel(bytes, decimal = 0):
   """
   Converts byte count into label in its most significant units, for instance
   7500 bytes would return "7 KB".
   """
   
-  if bytes >= 1073741824: return "%i GB" % (bytes / 1073741824)
-  elif bytes >= 1048576: return "%i MB" % (bytes / 1048576)
-  elif bytes >= 1024: return "%i KB" % (bytes / 1024)
+  format = "%%.%if" % decimal
+  if bytes >= 1073741824: return (format + " GB") % (bytes / 1073741824.0)
+  elif bytes >= 1048576: return (format + " MB") % (bytes / 1048576.0)
+  elif bytes >= 1024: return (format + " KB") % (bytes / 1024.0)
   else: return "%i bytes" % bytes
 
+def getTimeLabel(seconds, decimal = 0):
+  """
+  Concerts seconds into a time label truncated to its most significant units,
+  for instance 7500 seconds would return "". Units go up through days.
+  """
+  
+  format = "%%.%if" % decimal
+  if seconds >= 86400: return (format + "d") % (seconds / 86400.0)
+  elif seconds >= 3600: return (format + "h") % (seconds / 3600.0)
+  elif seconds >= 60: return (format + "m") % (seconds / 60.0)
+  else: return "%is" % seconds
+
 class Panel():
   """
   Wrapper for curses subwindows. This provides safe proxies to common methods

Modified: arm/trunk/readme.txt
===================================================================
--- arm/trunk/readme.txt	2009-08-22 01:49:25 UTC (rev 20353)
+++ arm/trunk/readme.txt	2009-08-23 06:44:45 UTC (rev 20354)
@@ -38,6 +38,8 @@
 
 Yes - arm is a passive listener with one exception. The second page (connections) provides the hostnames of Tor relays you're connected to. This means reverse DNS lookups which, if monitored, could leak your current connections to an eavesdropper. However, lookups are only made upon request (when showing connection details or listing connections by hostname) and you can disable lookups entirely with 'r' - see the page's help for the current status.
 
+That said, this is a non-issue. ISPs and anyone sniffing your connection already has this data - the only difference is that instead of saying "I am talking to x" you're saying "I'm talking to x. who's x?"
+
 > When arm starts it gives "Unable to resolve tor pid, abandoning connection listing"... why?
 
 If you're running multiple instances of tor then arm needs to figure out which pid belongs to the open control port. If it's running as a different user (such as being in a chroot jail) then it's probably failing due to permission issues. Arm still runs, just no connection listing or ps stats.