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

[or-cvs] r20927: {arm} This will be the last update for a while since I'm about to (in arm/trunk: . interface)



Author: atagar
Date: 2009-11-08 22:51:58 -0500 (Sun, 08 Nov 2009)
New Revision: 20927

Added:
   arm/trunk/interface/fileDescriptorPopup.py
Modified:
   arm/trunk/ChangeLog
   arm/trunk/README
   arm/trunk/TODO
   arm/trunk/arm.py
   arm/trunk/interface/confPanel.py
   arm/trunk/interface/connPanel.py
   arm/trunk/interface/controller.py
   arm/trunk/interface/hostnameResolver.py
Log:
This will be the last update for a while since I'm about to start a new job.
added: including family relays on connections listing
added: file descriptors dialog (stats and scrollable listing)
change: logs warning if torrc fails to load
fix: size and time labels used in torrc are expanded for validation
fix: duplicate torrc entries weren't being detected if not erroneous
fix: crashing issue when cleaning up hostname cache
fix: stretching connection lines to fill full screen



Modified: arm/trunk/ChangeLog
===================================================================
--- arm/trunk/ChangeLog	2009-11-09 02:52:44 UTC (rev 20926)
+++ arm/trunk/ChangeLog	2009-11-09 03:51:58 UTC (rev 20927)
@@ -1,6 +1,17 @@
 CHANGE LOG
 
-10/21/09 - version 1.2.1
+11/8/09 - version 1.2.2
+This will be the last update for a while since I'm about to start a new job.
+
+    * added: including family relays on connections listing
+    * added: file descriptors dialog (stats and scrollable listing)
+    * change: logs warning if torrc fails to load
+    * fix: size and time labels used in torrc are expanded for validation
+    * fix: duplicate torrc entries weren't being detected if not erroneous
+    * fix: crashing issue when cleaning up hostname cache
+    * fix: stretching connection lines to fill full screen
+
+10/21/09 - version 1.2.1 (r20814)
 Substantial bundle of changes including torrc validation, improved arm event logging, and numerous bug fixes.
 
     * added: verifies loaded torrc consistency against tor's actual state (gives warning and providing corrections)

Modified: arm/trunk/README
===================================================================
--- arm/trunk/README	2009-11-09 02:52:44 UTC (rev 20926)
+++ arm/trunk/README	2009-11-09 03:51:58 UTC (rev 20927)
@@ -21,10 +21,11 @@
 Requirements:
 Python 2.5
 TorCtl (retrieved in svn checkout)
-Common *nix commands including: ps, pidof, tail, host, and netstat
 Tor is running with an available control port. This means either...
   ... starting Tor with '--controlport <PORT>'
   ... or including 'ControlPort <PORT>' in your torrc
+For full functionality this requires common *nix commands including: ps, pidof,
+  tail, pwdx, host, netstat, lsof, and ulimit
 
 This is started via 'arm' (use the '--help' argument for usage).
 

Modified: arm/trunk/TODO
===================================================================
--- arm/trunk/TODO	2009-11-09 02:52:44 UTC (rev 20926)
+++ arm/trunk/TODO	2009-11-09 03:51:58 UTC (rev 20927)
@@ -38,6 +38,7 @@
 			if set and there's extra room available show 'MaxAdvertisedBandwidth'
 	* when help popup is showing options let them be directly opened
 			requested by arma
+	* check family connections to see if they're alive (VERSION cell handshake?)
 	* update site's screenshots (pretty out of date...)
 
 - Ideas (low priority)

Modified: arm/trunk/arm.py
===================================================================
--- arm/trunk/arm.py	2009-11-09 02:52:44 UTC (rev 20926)
+++ arm/trunk/arm.py	2009-11-09 03:51:58 UTC (rev 20927)
@@ -19,8 +19,8 @@
 from interface import controller
 from interface import logPanel
 
-VERSION = "1.2.1"
-LAST_MODIFIED = "Oct 21, 2009"
+VERSION = "1.2.2"
+LAST_MODIFIED = "Nov 8, 2009"
 
 DEFAULT_CONTROL_ADDR = "127.0.0.1"
 DEFAULT_CONTROL_PORT = 9051

Modified: arm/trunk/interface/confPanel.py
===================================================================
--- arm/trunk/interface/confPanel.py	2009-11-09 02:52:44 UTC (rev 20926)
+++ arm/trunk/interface/confPanel.py	2009-11-09 03:51:58 UTC (rev 20927)
@@ -12,6 +12,18 @@
 # last updated for tor version 0.2.1.19
 MULTI_LINE_PARAM = ["AlternateBridgeAuthority", "AlternateDirAuthority", "AlternateHSAuthority", "AuthDirBadDir", "AuthDirBadExit", "AuthDirInvalid", "AuthDirReject", "Bridge", "ControlListenAddress", "ControlSocket", "DirListenAddress", "DirPolicy", "DirServer", "DNSListenAddress", "ExitPolicy", "HashedControlPassword", "HiddenServiceDir", "HiddenServiceOptions", "HiddenServicePort", "HiddenServiceVersion", "HiddenServiceAuthorizeClient", "HidServAuth", "Log", "MapAddress", "NatdListenAddress", "NodeFamily", "ORListenAddress", "ReachableAddresses", "ReachableDirAddresses", "ReachableORAddresses", "RecommendedVersions", "RecommendedClientVersions", "RecommendedServerVersions", "SocksListenAddress", "SocksPolicy", "TransListenAddress", "__HashedControlSessionPassword"]
 
+# size modifiers allowed by config.c
+LABEL_KB = ["kb", "kbyte", "kbytes", "kilobyte", "kilobytes"]
+LABEL_MB = ["m", "mb", "mbyte", "mbytes", "megabyte", "megabytes"]
+LABEL_GB = ["gb", "gbyte", "gbytes", "gigabyte", "gigabytes"]
+LABEL_TB = ["tb", "terabyte", "terabytes"]
+
+# time modifiers allowed by config.c
+LABEL_MIN = ["minute", "minutes"]
+LABEL_HOUR = ["hour", "hours"]
+LABEL_DAY = ["day", "days"]
+LABEL_WEEK = ["week", "weeks"]
+
 class ConfPanel(util.Panel):
   """
   Presents torrc with syntax highlighting in a scroll-able area.
@@ -48,7 +60,7 @@
       # checks if torrc differs from get_option data
       self.irrelevantLines = []
       self.corrections = {}
-      correctedCmd = {}       # mapping of corrected commands to line numbers
+      parsedCommands = {}       # mapping of parsed commands to line numbers
       
       for lineNumber in range(len(self.confContents)):
         lineText = self.confContents[lineNumber].strip()
@@ -60,12 +72,29 @@
           if argEnd == -1: argEnd = len(lineText)
           command, argument = lineText[:ctlEnd], lineText[ctlEnd:argEnd].strip()
           
+          # expands value if it's a size or time
+          comp = argument.strip().lower().split(" ")
+          if len(comp) > 1:
+            size = 0
+            if comp[1] in LABEL_KB: size = int(comp[0]) * 1024
+            elif comp[1] in LABEL_MB: size = int(comp[0]) * 1048576
+            elif comp[1] in LABEL_GB: size = int(comp[0]) * 1073741824
+            elif comp[1] in LABEL_TB: size = int(comp[0]) * 1099511627776
+            elif comp[1] in LABEL_MIN: size = int(comp[0]) * 60
+            elif comp[1] in LABEL_HOUR: size = int(comp[0]) * 3600
+            elif comp[1] in LABEL_DAY: size = int(comp[0]) * 86400
+            elif comp[1] in LABEL_WEEK: size = int(comp[0]) * 604800
+            if size != 0: argument = str(size)
+              
           # most parameters are overwritten if defined multiple times, if so
           # it's erased from corrections and noted as duplicate instead
-          if not command in MULTI_LINE_PARAM and command in correctedCmd.keys():
-            self.irrelevantLines.append(correctedCmd[command])
-            del self.corrections[correctedCmd[command]]
+          if not command in MULTI_LINE_PARAM and command in parsedCommands.keys():
+            previousLineNum = parsedCommands[command]
+            self.irrelevantLines.append(previousLineNum)
+            if previousLineNum in self.corrections.keys(): del self.corrections[previousLineNum]
           
+          parsedCommands[command] = lineNumber + 1
+          
           # check validity against tor's actual state
           try:
             actualValues = []
@@ -73,8 +102,7 @@
               actualValues.append(val)
             
             if not argument in actualValues:
-              self.corrections[lineNumber + 1] = ", ".join(actualValues)
-              correctedCmd[command] = lineNumber + 1
+              self.corrections[lineNumber + 1] = argument + " - " + ", ".join(actualValues)
           except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
             pass # unable to load tor parameter to validate... weird
       
@@ -87,8 +115,9 @@
         self.logger.monitor_event("NOTICE", "%s: %s (highlighted in blue)" % (baseMsg, ", ".join([str(val) for val in self.irrelevantLines])))
       if self.corrections:
         self.logger.monitor_event("WARN", "Tor's state differs from loaded torrc")
-    except IOError:
+    except IOError, exc:
       self.confContents = ["### Unable to load torrc ###"]
+      self.logger.monitor_event("WARN", "Unable to load torrc (%s)" % str(exc))
     self.scroll = 0
   
   def handleKey(self, key):

Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py	2009-11-09 02:52:44 UTC (rev 20926)
+++ arm/trunk/interface/connPanel.py	2009-11-09 03:51:58 UTC (rev 20927)
@@ -25,8 +25,8 @@
 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", "client": "cyan", "directory": "magenta", "control": "red", "localhost": "yellow"}
-TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "client": 2, "directory": 3, "control": 4, "localhost": 5} # defines ordering
+TYPE_COLORS = {"inbound": "green", "outbound": "blue", "client": "cyan", "directory": "magenta", "control": "red", "family": "magenta", "localhost": "yellow"}
+TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "client": 2, "directory": 3, "control": 4, "family": 5, "localhost": 6} # defines ordering
 
 # enums for indexes of ConnPanel 'connections' fields
 CONN_TYPE, CONN_L_IP, CONN_L_PORT, CONN_F_IP, CONN_F_PORT, CONN_COUNTRY, CONN_TIME = range(7)
@@ -112,8 +112,8 @@
     self.localhostEntry = None        # special connection - tuple with (entry for this node, fingerprint)
     self.sortOrdering = [ORD_TYPE, ORD_FOREIGN_LISTING, ORD_FOREIGN_PORT]
     self.resolver = hostnameResolver.HostnameResolver()
-    self.fingerprintLookupCache = {}                              # chache of (ip, port) -> fingerprint
-    self.nicknameLookupCache = {}                                 # chache of (ip, port) -> nickname
+    self.fingerprintLookupCache = {}                              # cache of (ip, port) -> fingerprint
+    self.nicknameLookupCache = {}                                 # cache of (ip, port) -> nickname
     self.fingerprintMappings = _getFingerprintMappings(self.conn) # mappings of ip -> [(port, fingerprint, nickname), ...]
     self.providedGeoipWarning = False
     self.orconnStatusCache = []           # cache for 'orconn-status' calls
@@ -132,11 +132,17 @@
     self.pauseTime = 0              # time when paused
     self.connectionsBuffer = []     # location where connections are stored while paused
     self.connectionCountBuffer = []
+    self.familyResolutionsBuffer = {}
     
+    # mapping of ip/port to fingerprint of family entries, used in hack to short circuit (ip / port) -> fingerprint lookups
+    self.familyResolutions = {}
+    
     self.nickname = ""
     self.orPort = "0"
     self.dirPort = "0"
     self.controlPort = "0"
+    self.family = []                # fingerpints of family entries
+    
     self.resetOptions()
     
     # netstat results are tuples of the form:
@@ -150,6 +156,8 @@
     self.reset()
   
   def resetOptions(self):
+    self.familyResolutions = {}
+    
     try:
       self.nickname = self.conn.get_option("Nickname")[0][1]
       
@@ -157,11 +165,17 @@
       self.orPort = self.conn.get_option("ORPort")[0][1]
       self.dirPort = self.conn.get_option("DirPort")[0][1]
       self.controlPort = self.conn.get_option("ControlPort")[0][1]
+      
+      # entry is None if not set, otherwise of the format "$<fingerprint>,$<fingerprint>"
+      familyEntry = self.conn.get_option("MyFamily")[0][1]
+      if familyEntry: self.family = [entry[1:] for entry in familyEntry.split(",")]
+      else: self.family = []
     except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
       self.nickname = ""
       self.orPort = "0"
       self.dirPort = "0"
       self.controlPort = "0"
+      self.family = []
   
   # change in client circuits
   def circ_status_event(self, event):
@@ -227,6 +241,7 @@
     # temporary variables for connections and count
     connectionsTmp = []
     connectionCountTmp = [0] * 5
+    familyResolutionsTmp = {}
     
     try:
       if self.clientConnectionCache == None:
@@ -309,15 +324,40 @@
       else:
         self.localhostEntry = None
       
+      # appends family connections
+      tmpCounter = 0 # used for unique port of unresolved family entries (funky hack)
+      for fingerprint in self.family:
+        try:
+          nsCommand = "ns/id/%s" % fingerprint
+          familyInfo = self.conn.get_info(nsCommand)[nsCommand].split()
+          familyAddress, familyPort = familyInfo[6], familyInfo[7]
+          
+          countryCodeQuery = "ip-to-country/%s" % familyAddress
+          familyCountryCode = self.conn.get_info(countryCodeQuery)[countryCodeQuery]
+          
+          if (familyAddress, familyPort) in connTimes: connTime = connTimes[(familyAddress, familyPort)]
+          else: connTime = time.time()
+          
+          familyResolutionsTmp[(familyAddress, familyPort)] = fingerprint
+          connectionsTmp.append(("family", familyAddress, familyPort, familyAddress, familyPort, familyCountryCode, connTime))
+        except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
+          # use dummy entry for sorting - the redraw function notes that entries are unknown
+          portIdentifier = str(65536 + tmpCounter)
+          familyResolutionsTmp[("256.255.255.255", portIdentifier)] = fingerprint
+          connectionsTmp.append(("family", "256.255.255.255", portIdentifier, "256.255.255.255", portIdentifier, "??", time.time()))
+          tmpCounter += 1
+      
       self.lastUpdate = time.time()
       
       # assigns results
       if self.isPaused:
         self.connectionsBuffer = connectionsTmp
         self.connectionCountBuffer = connectionCountTmp
+        self.familyResolutionsBuffer = familyResolutionsTmp
       else:
         self.connections = connectionsTmp
         self.connectionCount = connectionCountTmp
+        self.familyResolutions = familyResolutionsTmp
         
         # hostnames are sorted at redraw - otherwise now's a good time
         if self.listingType != LIST_HOSTNAME: self.sortConnections()
@@ -510,8 +550,27 @@
                   etc += "%-26s  " % ("%s:%s %s" % (entry[CONN_F_IP], entry[CONN_F_PORT], "" if type == "control" else "(%s)" % entry[CONN_COUNTRY]))
                 
                 dst = ("%%-%is" % foreignNicknameSpace) % dst
+              
+              timeLabel = util.getTimeLabel(currentTime - entry[CONN_TIME], 1)
               if type == "inbound": src, dst = dst, src
-              lineEntry = "<%s>%s  -->  %s  %s%5s (<b>%s</b>)%s</%s>" % (color, src, dst, etc, util.getTimeLabel(currentTime - entry[CONN_TIME], 1), type.upper(), " " * (9 - len(type)), color)
+              elif type == "family" and int(entry[CONN_L_PORT]) > 65535:
+                # this belongs to an unresolved family entry - replaces invalid data with "UNKNOWN"
+                timeLabel = "---"
+                
+                if self.listingType == LIST_IP:
+                  src = "%-21s" % "UNKNOWN"
+                  dst = "%-26s" % "UNKNOWN"
+                elif self.listingType == LIST_HOSTNAME:
+                  src = "%-15s" % "UNKNOWN"
+                  dst = ("%%-%is" % len(dst)) % "UNKNOWN"
+                  if len(etc) > 0: etc = etc.replace("256.255.255.255 (??)", "UNKNOWN" + " " * 13)
+                else:
+                  ipStart = etc.find("256")
+                  if ipStart > -1: etc = etc[:ipStart] + ("%%-%is" % len(etc[ipStart:])) % "UNKNOWN"
+              
+              padding = self.maxX - (len(src) + len(dst) + len(etc) + 27) # padding needed to fill full line
+              lineEntry = "<%s>%s  -->  %s  %s%s%5s (<b>%s</b>)%s</%s>" % (color, src, dst, etc, " " * padding, timeLabel, type.upper(), " " * (9 - len(type)), color)
+              
               if self.isCursorEnabled and entry == self.cursorSelection:
                 lineEntry = "<h>%s</h>" % lineEntry
               
@@ -540,6 +599,10 @@
     if self.localhostEntry and ipAddr == self.localhostEntry[0][CONN_L_IP] and port == self.localhostEntry[0][CONN_L_PORT]:
       return self.localhostEntry[1]
     
+    # checks if this belongs to a family entry
+    if (ipAddr, port) in self.familyResolutions.keys():
+      return self.familyResolutions[(ipAddr, port)]
+    
     port = int(port)
     if (ipAddr, port) in self.fingerprintLookupCache:
       return self.fingerprintLookupCache[(ipAddr, port)]
@@ -631,9 +694,11 @@
       self.pauseTime = time.time()
       self.connectionsBuffer = list(self.connections)
       self.connectionCountBuffer = list(self.connectionCount)
+      self.familyResolutionsBuffer = dict(self.familyResolutions)
     else:
       self.connections = list(self.connectionsBuffer)
       self.connectionCount = list(self.connectionCountBuffer)
+      self.familyResolutions = dict(self.familyResolutionsBuffer)
       
       # pause buffer connections may be unsorted
       if self.listingType != LIST_HOSTNAME: self.sortConnections()

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2009-11-09 02:52:44 UTC (rev 20926)
+++ arm/trunk/interface/controller.py	2009-11-09 03:51:58 UTC (rev 20927)
@@ -22,6 +22,7 @@
 import connPanel
 import confPanel
 import descriptorPopup
+import fileDescriptorPopup
 
 import util
 import connResolver
@@ -503,15 +504,16 @@
           popup.addfstr(1, 2, "s: graphed stats (<b>%s</b>)" % graphedStats)
           popup.addfstr(1, 41, "i: graph update interval (<b>%s</b>)" % panels["graph"].updateInterval)
           popup.addfstr(2, 2, "b: graph bounds (<b>%s</b>)" % graphPanel.BOUND_LABELS[panels["graph"].bounds])
-          popup.addstr(2, 41, "e: change logged events")
+          popup.addstr(2, 41, "d: file descriptors")
+          popup.addstr(3, 2, "e: change logged events")
           
-          regexLabel = "enabled" if panels["log"].regexFilter else "disabled"
-          popup.addfstr(3, 2, "f: log regex filter (<b>%s</b>)" % regexLabel)
-          
           runlevelEventsLabel = "arm and tor"
           if panels["log"].runlevelTypes == logPanel.RUNLEVEL_TOR_ONLY: runlevelEventsLabel = "tor only"
           elif panels["log"].runlevelTypes == logPanel.RUNLEVEL_ARM_ONLY: runlevelEventsLabel = "arm only"
           popup.addfstr(3, 41, "r: logged runlevels (<b>%s</b>)" % runlevelEventsLabel)
+          
+          regexLabel = "enabled" if panels["log"].regexFilter else "disabled"
+          popup.addfstr(4, 2, "f: log regex filter (<b>%s</b>)" % regexLabel)
         if page == 1:
           popup.addstr(1, 2, "up arrow: scroll up a line")
           popup.addstr(1, 41, "down arrow: scroll down a line")
@@ -608,6 +610,19 @@
     elif page == 0 and (key == ord('b') or key == ord('B')):
       # uses the next boundary type for graph
       panels["graph"].bounds = (panels["graph"].bounds + 1) % 2
+    elif page == 0 and key in (ord('d'), ord('D')):
+      # provides popup with file descriptors
+      cursesLock.acquire()
+      try:
+        setPauseState(panels, isPaused, page, True)
+        curses.cbreak() # wait indefinitely for key presses (no timeout)
+        
+        fileDescriptorPopup.showFileDescriptorPopup(panels["popup"], stdscr, torPid)
+        
+        setPauseState(panels, isPaused, page)
+        curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+      finally:
+        cursesLock.release()
     elif page == 0 and (key == ord('e') or key == ord('E')):
       # allow user to enter new types of events to log - unchanged if left blank
       cursesLock.acquire()
@@ -791,8 +806,13 @@
           
           selectedIp = selection[connPanel.CONN_F_IP]
           selectedPort = selection[connPanel.CONN_F_PORT]
+          
           addrLabel = "address: %s:%s" % (selectedIp, selectedPort)
           
+          if selection[connPanel.CONN_TYPE] == "family" and int(selection[connPanel.CONN_L_PORT]) > 65535:
+            # unresolved family entry - unknown ip/port
+            addrLabel = "address: unknown"
+          
           hostname = resolver.resolve(selectedIp)
           if hostname == None:
             if resolver.isPaused: hostname = "DNS resolution disallowed"

Added: arm/trunk/interface/fileDescriptorPopup.py
===================================================================
--- arm/trunk/interface/fileDescriptorPopup.py	                        (rev 0)
+++ arm/trunk/interface/fileDescriptorPopup.py	2009-11-09 03:51:58 UTC (rev 20927)
@@ -0,0 +1,168 @@
+#!/usr/bin/env python
+# fileDescriptorPopup.py -- provides open file descriptor stats and listing
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import os
+import curses
+
+import util
+
+class PopupProperties:
+  """
+  State attributes of popup window for file descriptors. Any problem in system
+  calls will cause 'errorMsg' to be set (providing the notice rather than
+  displaying data). Under systems other than Solaris there's no way for a
+  process (other than tor itself) to know its file descriptor limit, so this
+  estimates.
+  """
+  
+  def __init__(self, torPid):
+    self.fdFile, self.fdConn, self.fdMisc = [], [], []
+    self.fdLimit = 0
+    self.errorMsg = ""
+    self.scroll = 0
+    
+    try:
+      ulimitCall = None
+      
+      # retrieves list of open files, options are:
+      # n = no dns lookups, p = by pid, -F = show fields (L = login name, n = opened files)
+      lsofCall = os.popen("lsof -np %s -F Ln 2> /dev/null" % torPid)
+      results = lsofCall.readlines()
+      if len(results) == 0: raise Exception("lsof is unavailable")
+      torUser = results[1][1:]
+      results = results[2:] # skip first couple lines (pid listing and user)
+      
+      # splits descriptors into buckets according to their type
+      descriptors = [entry[1:].strip() for entry in results] # strips off first character (always an 'n')
+      
+      for desc in descriptors:
+        if os.path.exists(desc): self.fdFile.append(desc)
+        elif desc[0] != "/" and ":" in desc: self.fdConn.append(desc)
+        else: self.fdMisc.append(desc)
+      
+      self.fdFile.sort()
+      self.fdConn.sort()
+      self.fdMisc.sort()
+      
+      # This is guessing the open file limit. Unfortunately there's no way
+      # (other than "/usr/proc/bin/pfiles pid | grep rlimit" under Solaris) to
+      # get the file descriptor limit for an arbitrary process. What we need is
+      # for the tor process to provide the return value of the "getrlimit"
+      # function via a GET_INFO call.
+      if torUser == "debian-tor":
+        # probably loaded via /etc/init.d/tor which changes descriptor limit
+        self.fdLimit = 8192
+      else:
+        # uses ulimit to estimate (-H is for hard limit, which is what tor uses)
+        ulimitCall = os.popen("ulimit -Hn 2> /dev/null")
+        results = ulimitCall.readlines()
+        if len(results) == 0: raise Exception("ulimit is unavailable")
+        self.fdLimit = int(results[0])
+    except Exception, exc:
+      # problem arose in calling or parsing lsof or ulimit calls
+      self.errorMsg = "error: " + str(exc)
+    finally:
+      lsofCall.close()
+      if ulimitCall: ulimitCall.close()
+  
+  def handleKey(self, key, height):
+    totalEntries = len(self.fdFile) + len(self.fdConn) + len(self.fdMisc)
+    
+    if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
+    elif key == curses.KEY_DOWN: self.scroll = max(0, min(self.scroll + 1, totalEntries - height))
+    elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - height, 0)
+    elif key == curses.KEY_NPAGE: self.scroll = max(0, min(self.scroll + height, totalEntries - height))
+
+def showFileDescriptorPopup(popup, stdscr, torPid):
+  """
+  Presents open file descriptors in popup window with the following controls:
+  Up, Down, Page Up, Page Down - scroll descriptors
+  Any other key - close popup
+  """
+  
+  properties = PopupProperties(torPid)
+  
+  if not popup.lock.acquire(False): return
+  try:
+    if properties.errorMsg:
+      popupWidth = len(properties.errorMsg) + 4
+      popupHeight = 3
+    else:
+      # uses longest entry to determine popup width
+      popupWidth = 40 # minimum width
+      for entry in properties.fdFile + properties.fdConn + properties.fdMisc:
+        popupWidth = max(popupWidth, len(entry) + 4)
+      
+      popupHeight = len(properties.fdFile) + len(properties.fdConn) + len(properties.fdMisc)
+    
+    popup._resetBounds()
+    popup.height = popupHeight
+    popup.recreate(stdscr, popup.startY, popupWidth)
+    
+    while True:
+      draw(popup, properties)
+      key = stdscr.getch()
+      
+      if key in (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE):
+        # navigation - tweak properties and recreate popup
+        properties.handleKey(key, popup.maxY - 4)
+      else:
+        # closes popup
+        break
+    
+    popup.height = 9
+    popup.recreate(stdscr, popup.startY, 80)
+  finally:
+    popup.lock.release()
+
+def draw(popup, properties):
+  popup.clear()
+  popup.win.box()
+  
+  # top label
+  popup.addstr(0, 0, "Open File Descriptors:", curses.A_STANDOUT)
+  
+  if properties.errorMsg:
+    popup.addstr(1, 2, properties.errorMsg, curses.A_BOLD | util.getColor("red"))
+  else:
+    # text with file descriptor count and limit
+    fdCount = len(properties.fdFile) + len(properties.fdConn) + len(properties.fdMisc)
+    fdCountPer = 100 * fdCount / properties.fdLimit
+    
+    statsColor = "green"
+    if fdCountPer >= 90: statsColor = "red"
+    elif fdCountPer >= 50: statsColor = "yellow"
+    
+    countMsg = "%i / %i (%i%%)" % (fdCount, properties.fdLimit, fdCountPer)
+    popup.addstr(1, 2, countMsg, curses.A_BOLD | util.getColor(statsColor))
+    
+    # provides a progress bar reflecting the stats
+    barWidth = popup.maxX - len(countMsg) - 6 # space between "[ ]" in progress bar
+    barProgress = max(1, barWidth * fdCountPer / 100) # filled cells
+    popup.addstr(1, len(countMsg) + 3, "[", curses.A_BOLD)
+    popup.addstr(1, len(countMsg) + 4, " " * barProgress, curses.A_STANDOUT | util.getColor(statsColor))
+    popup.addstr(1, len(countMsg) + 4 + barWidth, "]", curses.A_BOLD)
+    
+    popup.win.hline(2, 1, curses.ACS_HLINE, popup.maxX - 2)
+    
+    # scrollable file descriptor listing
+    lineNum = 3
+    entryNum = properties.scroll
+    while lineNum <= popup.maxY - 2:
+      if entryNum < len(properties.fdFile):
+        line = properties.fdFile[entryNum]
+        color = "green"
+      elif entryNum < len(properties.fdFile) + len(properties.fdMisc):
+        line = properties.fdMisc[entryNum - len(properties.fdFile)]
+        color = "cyan"
+      else:
+        line = properties.fdConn[entryNum - len(properties.fdFile) - len(properties.fdMisc)]
+        color = "blue"
+      
+      popup.addstr(lineNum, 2, line, curses.A_BOLD | util.getColor(color))
+      lineNum += 1
+      entryNum += 1
+  
+  popup.refresh()
+

Modified: arm/trunk/interface/hostnameResolver.py
===================================================================
--- arm/trunk/interface/hostnameResolver.py	2009-11-09 02:52:44 UTC (rev 20926)
+++ arm/trunk/interface/hostnameResolver.py	2009-11-09 03:51:58 UTC (rev 20927)
@@ -69,8 +69,9 @@
         threshold = currentCount - (RESOLVER_MAX_CACHE_SIZE - RESOLVER_CACHE_TRIM_SIZE) # max count of entries being removed
         toDelete = []
         
-        for (entryAddr, (entryHostname, entryAge)) in self.resolvedCache:
-          if entryAge < threshold: toDelete.append(entryAddr)
+        # checks age of each entry, adding to toDelete if too old
+        for ipAddr in self.resolvedCache.keys():
+          if self.resolvedCache[ipAddr][1] < threshold: toDelete.append(ipAddr)
         
         for entryAddr in toDelete: del self.resolvedCache[entryAddr]