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

[or-cvs] r24187: {arm} Initial checkin for the revised connection panel. It's not y (in arm/trunk: . src src/interface src/interface/connections src/interface/graphing src/util)



Author: atagar
Date: 2011-02-07 03:53:26 +0000 (Mon, 07 Feb 2011)
New Revision: 24187

Added:
   arm/trunk/src/interface/connections/
   arm/trunk/src/interface/connections/__init__.py
   arm/trunk/src/interface/connections/connPanel.py
   arm/trunk/src/interface/connections/listings.py
   arm/trunk/src/util/enum.py
Modified:
   arm/trunk/TODO
   arm/trunk/armrc.sample
   arm/trunk/src/interface/configPanel.py
   arm/trunk/src/interface/controller.py
   arm/trunk/src/interface/graphing/__init__.py
   arm/trunk/src/interface/graphing/bandwidthStats.py
   arm/trunk/src/interface/graphing/connStats.py
   arm/trunk/src/interface/graphing/graphPanel.py
   arm/trunk/src/interface/headerPanel.py
   arm/trunk/src/interface/logPanel.py
   arm/trunk/src/interface/torrcPanel.py
   arm/trunk/src/starter.py
   arm/trunk/src/test.py
   arm/trunk/src/util/__init__.py
   arm/trunk/src/util/conf.py
   arm/trunk/src/util/connections.py
   arm/trunk/src/util/log.py
   arm/trunk/src/util/panel.py
   arm/trunk/src/util/procTools.py
   arm/trunk/src/util/sysTools.py
   arm/trunk/src/util/torConfig.py
   arm/trunk/src/util/torTools.py
   arm/trunk/src/util/uiTools.py
Log:
Initial checkin for the revised connection panel. It's not yet feature complete and currently hidden behind the INCLUDE_CONNPANEL_2 source flag. Missing aspects include sortability, detail popup, and the new features I'm planning. Otherwise it's fully functional and in theory should perform *much* better (in addition to being vastly cleaner code).

This also includes other revisions both related to the new connection panel and unrelated bugfixes I spotted while working on it:
added: identifying relay DNS connections
change: using a dedicated enum class rather than using tuple sets
change: dropping warning suggesting that users set the FetchUselessDescriptors option
fix: concurrency bug in joining on the TorCtl thread when tor shut down
fix: the availability check for bsd resolvers was broken (this was probably causing resolution to fail for a few seconds on that platform before picking a valid resolver)
fix: missing 'is default' option from config sort ordering
fix: the 'startup.dataDirectory' config option was being ignored (due to using a legacy key instead)
fix: dropping the deprecated 'features.config.descriptions.persistPath' config option (it's been replaced by the cached path and persist boolean)
fix: error with graphing package contents listed in __init__



Modified: arm/trunk/TODO
===================================================================
--- arm/trunk/TODO	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/TODO	2011-02-07 03:53:26 UTC (rev 24187)
@@ -61,6 +61,12 @@
 
 - Bugs
   * The default resolver isn't configurable.
+  * Default config value checks don't work with older versions of tor. We need
+    a version check for the feature. From Sjon:
+    > Okay, I found the problem with all values being tagged as default,
+    > 'GETINFO config-text' has been introduced in 0.2.2.7, while I am
+    > running 0.2.1.28. Maybe a check in arm for the version-number for that
+    > feature to be enabled might be an idea?
   * When saving the config the Log entry should be filtered out if unnecessary.
   * The config write dialog (ie, the one for saving the config) has its a
     misaligned border when it's smaller than the top detail section.
@@ -216,9 +222,10 @@
     Take a look at 'linux-tor-prio.sh' to see if any of the stats are 
     available and interesting.
   * escaping function for uiTools' formatted strings
-  * switch check of ip address validity to regex?
-    match = re.match("(\d*)\.(\d*)\.(\d*)\.(\d*)", ip)
-    http://wang.yuxuan.org/blog/2009/4/2/python_script_to_convert_from_ip_range_to_ip_mask
+  * provide an option for showing cpu usage on a per-core basis
+    This would be similar to top when you press 1, ie cpu0 X%, cpu1 Y%, etc.
+    This would help relay operators in checking system load (feature request
+    by Jordan)
   * setup wizard for new relays
     Setting the password and such for torrc generation. Maybe a netinstaller
     that fetches the right package for the plagform, verifies signatures, etc?
@@ -292,7 +299,18 @@
         - copy the results to the webserver, example:
           http://www.atagar.com/transfer/tmp/armBuild_12-7-10/
         - send the dsc link to weasel
+        - sign deb and copy that and sig for the download page
     
+    - Red Hat
+      Contact: None
+      Update Instructions:
+        - update resources/build/redHat/MANIFEST
+        - ./resources/rpm-prep.sh
+        - cd release_rpm
+        - ./debian/make-rpm
+        - copy the results to the webserver
+        - sign rpm and copy that and sig for the download page
+    
     - Gentoo
       Contact: NightMonkey (Jesse Adelman)
       Initial Release: https://bugs.gentoo.org/show_bug.cgi?id=341731

Modified: arm/trunk/armrc.sample
===================================================================
--- arm/trunk/armrc.sample	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/armrc.sample	2011-02-07 03:53:26 UTC (rev 24187)
@@ -62,12 +62,11 @@
 
 # Paremters for the config panel
 # ---------------------------
-# type
-#   0 -> tor state, 1 -> torrc, 2 -> arm state, 3 -> armrc
 # order
 #   three comma separated configuration attributes, options including:
-#   0 -> Category,  1 -> Option Name,   2 -> Value,     3 -> Arg Type,
-#   4 -> Arg Usage, 5 -> Description,   6 -> Man Entry, 7 -> Is Default
+#   0 -> Category,  1 -> Option Name,   2 -> Value,       3 -> Arg Type,
+#   4 -> Arg Usage, 5 -> Summary,       6 -> Description, 7 -> Man Entry,
+#   8 -> Is Default
 # selectionDetails.height
 #   rows of data for the panel showing details on the current selection, this
 #   is disabled entirely if zero
@@ -87,8 +86,7 @@
 # file.maxLinesPerEntry
 #   max number of lines to display for a single entry in the torrc
 
-features.config.type 0
-features.config.order 0, 6, 7
+features.config.order 0, 7, 8
 features.config.selectionDetails.height 6
 features.config.prepopulateEditValues true
 features.config.state.colWidth.option 25
@@ -104,12 +102,11 @@
 # ---------------------------
 # enabled
 #   allows the descriptions to be fetched from the man page if true
-# persistPath
-#   location descriptions should be loaded from and saved to (this feature is
-#   disabled if unset)
+# persist
+#   caches the descriptions (substantially saving on future startup times)
 
 features.config.descriptions.enabled true
-features.config.descriptions.persistPath /tmp/arm/torConfigDescriptions.txt
+features.config.descriptions.persist true
 
 # General graph parameters
 # ------------------------

Modified: arm/trunk/src/interface/configPanel.py
===================================================================
--- arm/trunk/src/interface/configPanel.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/interface/configPanel.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -6,7 +6,7 @@
 import curses
 import threading
 
-from util import conf, panel, torTools, torConfig, uiTools
+from util import conf, enum, panel, torTools, torConfig, uiTools
 
 DEFAULT_CONFIG = {"features.config.selectionDetails.height": 6,
                   "features.config.state.showPrivateOptions": False,
@@ -17,30 +17,31 @@
 # TODO: The arm use cases are incomplete since they currently can't be
 # modified, have their descriptions fetched, or even get a complete listing
 # of what's available.
-TOR_STATE, ARM_STATE = range(1, 3) # state to be presented
+State = enum.Enum("TOR", "ARM") # state to be presented
 
 # mappings of option categories to the color for their entries
-CATEGORY_COLOR = {torConfig.GENERAL: "green",
-                  torConfig.CLIENT: "blue",
-                  torConfig.SERVER: "yellow",
-                  torConfig.DIRECTORY: "magenta",
-                  torConfig.AUTHORITY: "red",
-                  torConfig.HIDDEN_SERVICE: "cyan",
-                  torConfig.TESTING: "white",
-                  torConfig.UNKNOWN: "white"}
+CATEGORY_COLOR = {torConfig.Category.GENERAL: "green",
+                  torConfig.Category.CLIENT: "blue",
+                  torConfig.Category.RELAY: "yellow",
+                  torConfig.Category.DIRECTORY: "magenta",
+                  torConfig.Category.AUTHORITY: "red",
+                  torConfig.Category.HIDDEN_SERVICE: "cyan",
+                  torConfig.Category.TESTING: "white",
+                  torConfig.Category.UNKNOWN: "white"}
 
 # attributes of a ConfigEntry
-FIELD_CATEGORY, FIELD_OPTION, FIELD_VALUE, FIELD_TYPE, FIELD_ARG_USAGE, FIELD_SUMMARY, FIELD_DESCRIPTION, FIELD_MAN_ENTRY, FIELD_IS_DEFAULT = range(9)
-DEFAULT_SORT_ORDER = (FIELD_CATEGORY, FIELD_MAN_ENTRY, FIELD_IS_DEFAULT)
-FIELD_ATTR = {FIELD_CATEGORY: ("Category", "red"),
-              FIELD_OPTION: ("Option Name", "blue"),
-              FIELD_VALUE: ("Value", "cyan"),
-              FIELD_TYPE: ("Arg Type", "green"),
-              FIELD_ARG_USAGE: ("Arg Usage", "yellow"),
-              FIELD_SUMMARY: ("Summary", "green"),
-              FIELD_DESCRIPTION: ("Description", "white"),
-              FIELD_MAN_ENTRY: ("Man Page Entry", "blue"),
-              FIELD_IS_DEFAULT: ("Is Default", "magenta")}
+Field = enum.Enum("CATEGORY", "OPTION", "VALUE", "TYPE", "ARG_USAGE",
+                  "SUMMARY", "DESCRIPTION", "MAN_ENTRY", "IS_DEFAULT")
+DEFAULT_SORT_ORDER = (Field.CATEGORY, Field.MAN_ENTRY, Field.IS_DEFAULT)
+FIELD_ATTR = {Field.CATEGORY: ("Category", "red"),
+              Field.OPTION: ("Option Name", "blue"),
+              Field.VALUE: ("Value", "cyan"),
+              Field.TYPE: ("Arg Type", "green"),
+              Field.ARG_USAGE: ("Arg Usage", "yellow"),
+              Field.SUMMARY: ("Summary", "green"),
+              Field.DESCRIPTION: ("Description", "white"),
+              Field.MAN_ENTRY: ("Man Page Entry", "blue"),
+              Field.IS_DEFAULT: ("Is Default", "magenta")}
 
 class ConfigEntry():
   """
@@ -49,9 +50,9 @@
   
   def __init__(self, option, type, isDefault):
     self.fields = {}
-    self.fields[FIELD_OPTION] = option
-    self.fields[FIELD_TYPE] = type
-    self.fields[FIELD_IS_DEFAULT] = isDefault
+    self.fields[Field.OPTION] = option
+    self.fields[Field.TYPE] = type
+    self.fields[Field.IS_DEFAULT] = isDefault
     
     # Fetches extra infromation from external sources (the arm config and tor
     # man page). These are None if unavailable for this config option.
@@ -59,18 +60,18 @@
     manEntry = torConfig.getConfigDescription(option)
     
     if manEntry:
-      self.fields[FIELD_MAN_ENTRY] = manEntry.index
-      self.fields[FIELD_CATEGORY] = manEntry.category
-      self.fields[FIELD_ARG_USAGE] = manEntry.argUsage
-      self.fields[FIELD_DESCRIPTION] = manEntry.description
+      self.fields[Field.MAN_ENTRY] = manEntry.index
+      self.fields[Field.CATEGORY] = manEntry.category
+      self.fields[Field.ARG_USAGE] = manEntry.argUsage
+      self.fields[Field.DESCRIPTION] = manEntry.description
     else:
-      self.fields[FIELD_MAN_ENTRY] = 99999 # sorts non-man entries last
-      self.fields[FIELD_CATEGORY] = torConfig.UNKNOWN
-      self.fields[FIELD_ARG_USAGE] = ""
-      self.fields[FIELD_DESCRIPTION] = ""
+      self.fields[Field.MAN_ENTRY] = 99999 # sorts non-man entries last
+      self.fields[Field.CATEGORY] = torConfig.Category.UNKNOWN
+      self.fields[Field.ARG_USAGE] = ""
+      self.fields[Field.DESCRIPTION] = ""
     
     # uses the full man page description if a summary is unavailable
-    self.fields[FIELD_SUMMARY] = summary if summary != None else self.fields[FIELD_DESCRIPTION]
+    self.fields[Field.SUMMARY] = summary if summary != None else self.fields[Field.DESCRIPTION]
     
     # cache of what's displayed for this configuration option
     self.labelCache = None
@@ -84,7 +85,7 @@
       field - enum for the field to be provided back
     """
     
-    if field == FIELD_VALUE: return self._getValue()
+    if field == Field.VALUE: return self._getValue()
     else: return self.fields[field]
   
   def getAll(self, fields):
@@ -113,9 +114,9 @@
     
     argSet = (optionWidth, valueWidth, summaryWidth)
     if not self.labelCache or self.labelCacheArgs != argSet:
-      optionLabel = uiTools.cropStr(self.get(FIELD_OPTION), optionWidth)
-      valueLabel = uiTools.cropStr(self.get(FIELD_VALUE), valueWidth)
-      summaryLabel = uiTools.cropStr(self.get(FIELD_SUMMARY), summaryWidth, None)
+      optionLabel = uiTools.cropStr(self.get(Field.OPTION), optionWidth)
+      valueLabel = uiTools.cropStr(self.get(Field.VALUE), valueWidth)
+      summaryLabel = uiTools.cropStr(self.get(Field.SUMMARY), summaryWidth, None)
       lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, summaryWidth)
       self.labelCache = lineTextLayout % (optionLabel, valueLabel, summaryLabel)
       self.labelCacheArgs = argSet
@@ -129,15 +130,15 @@
     value's type to provide a user friendly representation if able.
     """
     
-    confValue = ", ".join(torTools.getConn().getOption(self.get(FIELD_OPTION), [], True))
+    confValue = ", ".join(torTools.getConn().getOption(self.get(Field.OPTION), [], True))
     
     # provides nicer values for recognized types
     if not confValue: confValue = "<none>"
-    elif self.get(FIELD_TYPE) == "Boolean" and confValue in ("0", "1"):
+    elif self.get(Field.TYPE) == "Boolean" and confValue in ("0", "1"):
       confValue = "False" if confValue == "0" else "True"
-    elif self.get(FIELD_TYPE) == "DataSize" and confValue.isdigit():
+    elif self.get(Field.TYPE) == "DataSize" and confValue.isdigit():
       confValue = uiTools.getSizeLabel(int(confValue))
-    elif self.get(FIELD_TYPE) == "TimeInterval" and confValue.isdigit():
+    elif self.get(Field.TYPE) == "TimeInterval" and confValue.isdigit():
       confValue = uiTools.getTimeLabel(int(confValue), isLong = True)
     
     return confValue
@@ -159,7 +160,11 @@
         "features.config.state.colWidth.option": 5,
         "features.config.state.colWidth.value": 5})
       
-      self.sortOrdering = config.getIntCSV("features.config.order", self.sortOrdering, 3, 0, 9)
+      sortFields = Field.values()
+      customOrdering = config.getIntCSV("features.config.order", None, 3, 0, len(sortFields))
+      
+      if customOrdering:
+        self.sortOrdering = [sortFields[i] for i in customOrdering]
     
     self.configType = configType
     self.confContents = []
@@ -170,7 +175,7 @@
     # the 'important' flag are shown
     self.showAll = False
     
-    if self.configType == TOR_STATE:
+    if self.configType == State.TOR:
       conn = torTools.getConn()
       customOptions = torConfig.getCustomOptions()
       configOptionLines = conn.getInfo("config/names", "").strip().split("\n")
@@ -187,7 +192,7 @@
           continue
         
         self.confContents.append(ConfigEntry(confOption, confType, not confOption in customOptions))
-    elif self.configType == ARM_STATE:
+    elif self.configType == State.ARM:
       # loaded via the conf utility
       armConf = conf.getConfig("arm")
       for key in armConf.getKeys():
@@ -196,7 +201,7 @@
     # mirror listing with only the important configuration options
     self.confImportantContents = []
     for entry in self.confContents:
-      if torConfig.isImportant(entry.get(FIELD_OPTION)):
+      if torConfig.isImportant(entry.get(Field.OPTION)):
         self.confImportantContents.append(entry)
     
     # if there aren't any important options then show everything
@@ -247,7 +252,7 @@
     self.valsLock.acquire()
     
     # draws the top label
-    configType = "Tor" if self.configType == TOR_STATE else "Arm"
+    configType = "Tor" if self.configType == State.TOR else "Arm"
     hiddenMsg = "press 'a' to hide most options" if self.showAll else "press 'a' to show all options"
     
     titleLabel = "%s Configuration (%s):" % (configType, hiddenMsg)
@@ -286,8 +291,8 @@
       entry = self._getConfigOptions()[lineNum]
       drawLine = lineNum + detailPanelHeight + 1 - scrollLoc
       
-      lineFormat = curses.A_NORMAL if entry.get(FIELD_IS_DEFAULT) else curses.A_BOLD
-      if entry.get(FIELD_CATEGORY): lineFormat |= uiTools.getColor(CATEGORY_COLOR[entry.get(FIELD_CATEGORY)])
+      lineFormat = curses.A_NORMAL if entry.get(Field.IS_DEFAULT) else curses.A_BOLD
+      if entry.get(Field.CATEGORY): lineFormat |= uiTools.getColor(CATEGORY_COLOR[entry.get(Field.CATEGORY)])
       if entry == cursorSelection: lineFormat |= curses.A_STANDOUT
       
       lineText = entry.getLabel(optionWidth, valueWidth, descriptionWidth)
@@ -322,30 +327,30 @@
     if width >= 2 and isScrollbarVisible: self.win.addch(detailPanelHeight, 1, curses.ACS_TTEE)
     self.win.addch(detailPanelHeight, width, curses.ACS_LRCORNER)
     
-    selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[cursorSelection.get(FIELD_CATEGORY)])
+    selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[cursorSelection.get(Field.CATEGORY)])
     
     # first entry:
     # <option> (<category> Option)
-    optionLabel =" (%s Option)" % torConfig.OPTION_CATEGORY_STR[cursorSelection.get(FIELD_CATEGORY)]
-    self.addstr(1, 2, cursorSelection.get(FIELD_OPTION) + optionLabel, selectionFormat)
+    optionLabel =" (%s Option)" % cursorSelection.get(Field.CATEGORY)
+    self.addstr(1, 2, cursorSelection.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 cursorSelection.get(Field.IS_DEFAULT) else "custom")
+      valueAttr.append(cursorSelection.get(Field.TYPE))
+      valueAttr.append("usage: %s" % (cursorSelection.get(Field.ARG_USAGE)))
       valueAttrLabel = ", ".join(valueAttr)
       
       valueLabelWidth = width - 12 - len(valueAttrLabel)
-      valueLabel = uiTools.cropStr(cursorSelection.get(FIELD_VALUE), valueLabelWidth)
+      valueLabel = uiTools.cropStr(cursorSelection.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: " + cursorSelection.get(Field.DESCRIPTION)
     
     for i in range(descriptionHeight):
       # checks if we're done writing the description
@@ -361,7 +366,7 @@
       
       if i != descriptionHeight - 1:
         # there's more lines to display
-        msg, remainder = uiTools.cropStr(lineContent, width - 2, 4, 4, uiTools.END_WITH_HYPHEN, True)
+        msg, remainder = uiTools.cropStr(lineContent, width - 2, 4, 4, uiTools.Ending.HYPHEN, True)
         descriptionContent = remainder.strip() + descriptionContent
       else:
         # this is the last line, end it with an ellipse

Added: arm/trunk/src/interface/connections/__init__.py
===================================================================
--- arm/trunk/src/interface/connections/__init__.py	                        (rev 0)
+++ arm/trunk/src/interface/connections/__init__.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -0,0 +1,6 @@
+"""
+Panels, popups, and handlers comprising the arm user interface.
+"""
+
+__all__ = ["connPanel", "entry"]
+

Added: arm/trunk/src/interface/connections/connPanel.py
===================================================================
--- arm/trunk/src/interface/connections/connPanel.py	                        (rev 0)
+++ arm/trunk/src/interface/connections/connPanel.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -0,0 +1,205 @@
+"""
+Listing of the currently established connections tor has made.
+"""
+
+import time
+import curses
+import threading
+
+from interface.connections import listings
+from util import connections, enum, log, panel, uiTools
+
+DEFAULT_CONFIG = {}
+
+# listing types
+Listing = enum.Enum(("IP", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
+
+class ConnectionPanel(panel.Panel, threading.Thread):
+  """
+  Listing of connections tor is making, with information correlated against
+  the current consensus and other data sources.
+  """
+  
+  def __init__(self, stdscr, config=None):
+    panel.Panel.__init__(self, stdscr, "connections", 0)
+    threading.Thread.__init__(self)
+    self.setDaemon(True)
+    
+    #self.sortOrdering = DEFAULT_SORT_ORDER
+    self._config = dict(DEFAULT_CONFIG)
+    if config:
+      config.update(self._config)
+      
+      # TODO: test and add to the sample armrc
+      #self.sortOrdering = config.getIntCSV("features.connections.order", self.sortOrdering, 3, 0, 6)
+    
+    self.scroller = uiTools.Scroller(True)
+    self._title = "Connections:" # title line of the panel
+    self._connections = []      # last fetched connections
+    
+    self._lastUpdate = -1       # time the content was last revised
+    self._isPaused = True       # prevents updates if true
+    self._pauseTime = None      # time when the panel was paused
+    self._halt = False          # terminates thread if true
+    self._cond = threading.Condition()  # used for pausing the thread
+    
+    # Last sampling received from the ConnectionResolver, used to detect when
+    # it changes.
+    self._lastResourceFetch = -1
+    
+    self.valsLock = threading.RLock()
+    
+    self._update() # populates initial entries
+    
+    # TODO: should listen for tor shutdown
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents the panel from updating.
+    """
+    
+    if not self._isPaused == isPause:
+      self._isPaused = isPause
+      
+      if isPause: self._pauseTime = time.time()
+      else: self._pauseTime = None
+      
+      # redraws so the display reflects any changes between the last update
+      # and being paused
+      self.redraw(True)
+  
+  def handleKey(self, key):
+    self.valsLock.acquire()
+    
+    if uiTools.isScrollKey(key):
+      pageHeight = self.getPreferredSize()[0] - 1
+      isChanged = self.scroller.handleKey(key, self._connections, pageHeight)
+      if isChanged: self.redraw(True)
+    
+    self.valsLock.release()
+  
+  def run(self):
+    """
+    Keeps connections listing updated, checking for new entries at a set rate.
+    """
+    
+    lastDraw = time.time() - 1
+    while not self._halt:
+      currentTime = time.time()
+      
+      if self._isPaused or currentTime - lastDraw < 1:
+        self._cond.acquire()
+        if not self._halt: self._cond.wait(0.2)
+        self._cond.release()
+      else:
+        # updates content if their's new results, otherwise just redraws
+        self._update()
+        self.redraw(True)
+        lastDraw += 1
+  
+  def draw(self, subwindow, width, height):
+    self.valsLock.acquire()
+    
+    # title label with connection counts
+    self.addstr(0, 0, self._title, curses.A_STANDOUT)
+    
+    scrollLoc = self.scroller.getScrollLoc(self._connections, height - 1)
+    cursorSelection = self.scroller.getCursorSelection(self._connections)
+    
+    scrollOffset = 0
+    if len(self._connections) > height - 1:
+      scrollOffset = 3
+      self.addScrollBar(scrollLoc, scrollLoc + height - 1, len(self._connections), 1)
+    
+    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
+      
+      lineFormat = uiTools.getColor(listings.CATEGORY_COLOR[entry.type])
+      if entry == cursorSelection: lineFormat |= curses.A_STANDOUT
+      
+      # Lines are split into three components (prefix, category, and suffix)
+      # since the category includes the bold attribute (otherwise, all use
+      # lineFormat).
+      xLoc = scrollOffset
+      
+      # prefix (entry data which is largely static, plus the time label)
+      entryLabel = entry.getLabel(Listing.IP, width - scrollOffset)
+      timeLabel = uiTools.getTimeLabel(currentTime - entry.startTime, 1)
+      prefixLabel = "%s%5s (" % (entryLabel, timeLabel)
+      
+      self.addstr(drawLine, xLoc, prefixLabel, lineFormat)
+      xLoc += len(prefixLabel)
+      
+      # category
+      self.addstr(drawLine, xLoc, entry.type.upper(), lineFormat | curses.A_BOLD)
+      xLoc += len(entry.type)
+      
+      # suffix (ending parentheses plus padding so lines are the same length)
+      self.addstr(drawLine, xLoc, ")" + " " * (9 - len(entry.type)), lineFormat)
+      
+      if drawLine >= height: break
+    
+    self.valsLock.release()
+  
+  def stop(self):
+    """
+    Halts further resolutions and terminates the thread.
+    """
+    
+    self._cond.acquire()
+    self._halt = True
+    self._cond.notifyAll()
+    self._cond.release()
+  
+  def _update(self):
+    """
+    Fetches the newest resolved connections.
+    """
+    
+    connResolver = connections.getResolver("tor")
+    currentResolutionCount = connResolver.getResolutionCount()
+    
+    if self._lastResourceFetch != currentResolutionCount:
+      self.valsLock.acquire()
+      currentConnections = connResolver.getConnections()
+      newConnections = []
+      
+      # preserves any ConnectionEntries they already exist
+      for conn in self._connections:
+        connAttr = (conn.local.getIpAddr(), conn.local.getPort(),
+                    conn.foreign.getIpAddr(), conn.foreign.getPort())
+        
+        if connAttr in currentConnections:
+          newConnections.append(conn)
+          currentConnections.remove(connAttr)
+      
+      # add new entries for any additions
+      for lIp, lPort, fIp, fPort in currentConnections:
+        newConnections.append(listings.ConnectionEntry(lIp, lPort, fIp, fPort))
+      
+      # if it's changed then sort the results
+      #if newConnections != self._connections:
+      #  newConnections.sort(key=lambda i: (i.getAll(self.sortOrdering)))
+      
+      # counts the relays in each of the categories
+      categoryTypes = listings.Category.values()
+      typeCounts = dict((type, 0) for type in categoryTypes)
+      for conn in newConnections: typeCounts[conn.type] += 1
+      
+      # makes labels for all the categories with connections (ie,
+      # "21 outbound", "1 control", etc)
+      countLabels = []
+      
+      for category in categoryTypes:
+        if typeCounts[category] > 0:
+          countLabels.append("%i %s" % (typeCounts[category], category.lower()))
+      
+      if countLabels: self._title = "Connections (%s):" % ", ".join(countLabels)
+      else: self._title = "Connections:"
+      
+      self._connections = newConnections
+      self._lastResourceFetch = currentResolutionCount
+      self.valsLock.release()
+

Added: arm/trunk/src/interface/connections/listings.py
===================================================================
--- arm/trunk/src/interface/connections/listings.py	                        (rev 0)
+++ arm/trunk/src/interface/connections/listings.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -0,0 +1,322 @@
+"""
+Entries for connections related to the Tor process.
+"""
+
+import time
+
+from util import connections, enum, hostnames, torTools, uiTools
+
+# Connection Categories:
+#   Inbound     Relay connection, coming to us.
+#   Outbound    Relay connection, leaving us.
+#   DNS         Relayed dns queries.
+#   Socks       Application client connection.
+#   Client      Circuits for our client traffic.
+#   Directory   Fetching tor consensus information.
+#   Control     Tor controller (arm, vidalia, etc).
+
+# TODO: add recognizing of CLIENT connection type
+Category = enum.Enum("INBOUND", "OUTBOUND", "DNS", "SOCKS", "CLIENT", "DIRECTORY", "CONTROL")
+CATEGORY_COLOR = {Category.INBOUND: "green", Category.OUTBOUND: "blue",
+                  Category.DNS: "blue",      Category.SOCKS: "cyan",
+                  Category.CLIENT: "cyan",   Category.DIRECTORY: "magenta",
+                  Category.CONTROL: "red"}
+
+class Endpoint:
+  """
+  Collection of attributes associated with a connection endpoint. This is a
+  thin wrapper for torUtil functions, making use of its caching for
+  performance.
+  """
+  
+  def __init__(self, ipAddr, port):
+    self.ipAddr = ipAddr
+    self.port = port
+    
+    # if true, we treat the port as an ORPort when searching for matching
+    # fingerprints (otherwise the ORPort is assumed to be unknown)
+    self.isORPort = False
+  
+  def getIpAddr(self):
+    """
+    Provides the IP address of the endpoint.
+    """
+    
+    return self.ipAddr
+  
+  def getPort(self):
+    """
+    Provides the port of the endpoint.
+    """
+    
+    return self.port
+  
+  def getHostname(self, default = None):
+    """
+    Provides the hostname associated with the relay's address. This is a
+    non-blocking call and returns None if the address either can't be resolved
+    or hasn't been resolved yet.
+    
+    Arguments:
+      default - return value if no hostname is available
+    """
+    
+    myHostname = hostnames.resolve(self.ipAddr)
+    if not myHostname: return default
+    else: return myHostname
+  
+  def getLocale(self):
+    """
+    Provides the two letter country code for the IP address' locale. This
+    proivdes None if it can't be determined.
+    """
+    
+    conn = torTools.getConn()
+    return conn.getInfo("ip-to-country/%s" % self.ipAddr)
+  
+  def getFingerprint(self):
+    """
+    Provides the fingerprint of the relay, returning "UNKNOWN" if it can't be
+    determined.
+    """
+    
+    conn = torTools.getConn()
+    orPort = self.port if self.isORPort else None
+    myFingerprint = conn.getRelayFingerprint(self.ipAddr, orPort)
+    
+    if myFingerprint: return myFingerprint
+    else: return "UNKNOWN"
+  
+  def getNickname(self):
+    """
+    Provides the nickname of the relay, retuning "UNKNOWN" if it can't be
+    determined.
+    """
+    
+    conn = torTools.getConn()
+    orPort = self.port if self.isORPort else None
+    myFingerprint = conn.getRelayFingerprint(self.ipAddr, orPort)
+    
+    if myFingerprint: return conn.getRelayNickname(myFingerprint)
+    else: return "UNKNOWN"
+
+class ConnectionEntry:
+  """
+  Represents a connection being made to or from this system. These only
+  concern real connections so it only includes the inbound, outbound,
+  directory, application, and controller categories.
+  """
+  
+  def __init__(self, lIpAddr, lPort, fIpAddr, fPort):
+    self.local = Endpoint(lIpAddr, lPort)
+    self.foreign = Endpoint(fIpAddr, fPort)
+    self.startTime = time.time()
+    
+    self._labelCache = ""
+    self._labelCacheArgs = (None, None)
+    
+    conn = torTools.getConn()
+    myOrPort = conn.getOption("ORPort")
+    myDirPort = conn.getOption("DirPort")
+    mySocksPort = conn.getOption("SocksPort", "9050")
+    myCtlPort = conn.getOption("ControlPort")
+    myAuthorities = conn.getMyDirAuthorities()
+    
+    # the ORListenAddress can overwrite the ORPort
+    listenAddr = conn.getOption("ORListenAddress")
+    if listenAddr and ":" in listenAddr:
+      myOrPort = listenAddr[listenAddr.find(":") + 1:]
+    
+    if lPort in (myOrPort, myDirPort):
+      self.type = Category.INBOUND
+      self.local.isORPort = True
+    elif lPort == mySocksPort:
+      self.type = Category.SOCKS
+    elif lPort == myCtlPort:
+      self.type = Category.CONTROL
+    elif (fIpAddr, fPort) in myAuthorities:
+      self.type = Category.DIRECTORY
+    elif fPort == "53":
+      # TODO: also check if this was a UDP connection (gonna take a bit more work...)
+      self.type = Category.DNS
+    else:
+      self.type = Category.OUTBOUND
+      self.foreign.isORPort = True
+  
+  def isPrivate(self):
+    """
+    Returns true if the endpoint is private, possibly belonging to a client
+    connection or exit traffic.
+    """
+    
+    if self.type == Category.INBOUND:
+      # if the connection doesn't belong to a known relay then it might be
+      # client traffic
+      
+      return self.foreign.getFingerprint() == "UNKNOWN"
+    elif self.type == Category.OUTBOUND:
+      # if it's both not a relay and obeys our exit policy then it may belong
+      # to exit traffic
+      
+      conn = torTools.getConn()
+      isExitingAllowed = conn.isExitingAllowed(self.foreign.getIpAddr(), self.foreign.getPort())
+      return self.foreign.getFingerprint() == "UNKNOWN" and isExitingAllowed
+    else:
+      # for control, application, and directory connections this isn't a concern
+      return False
+  
+  def getLabel(self, listingType, width):
+    """
+    Provides the formatted display string for this entry in the listing with
+    the given constraints. Labels are made up of six components:
+      <src>  -->  <dst>     <etc>     <uptime> (<type>)
+    this provides the first three components padded to fill up to the uptime.
+    
+    Listing.IP:
+      src - <internal addr:port> --> <external addr:port>
+      dst - <destination addr:port>
+      etc - <fingerprint> <nickname>
+    
+    Listing.HOSTNAME:
+      src - localhost:<port>
+      dst - <destination hostname:port>
+      etc - <destination addr:port> <fingerprint> <nickname>
+    
+    Listing.FINGERPRINT:
+      src - localhost
+      dst - <destination fingerprint>
+      etc - <nickname> <destination addr:port>
+    
+    Listing.NICKNAME:
+      src - <source nickname>
+      dst - <destination nickname>
+      etc - <fingerprint> <destination addr:port>
+    
+    Arguments:
+      listingType - primary attribute we're listing connections by
+      width       - maximum length of the entry
+    """
+    
+    # late import for the Listing enum (doing it in the header errors due to a
+    # circular import)
+    from interface.connections import connPanel
+    
+    # if our cached entries are still valid then use that
+    if self._labelCacheArgs == (listingType, width):
+      return self._labelCache
+    
+    conn = torTools.getConn()
+    
+    # destination of the connection
+    dstAddress = "<scrubbed>"
+    if not self.isPrivate():
+      dstAddress = "%s:%s" % (self.foreign.getIpAddr(), self.foreign.getPort())
+      
+      # if this isn't on the local network then also include the country
+      if not connections.isIpAddressPrivate(self.foreign.getIpAddr()):
+        dstAddress += " (%s)" % self.foreign.getLocale()
+    
+    src, dst, etc = "", "", ""
+    if listingType == connPanel.Listing.IP:
+      # base data requires 73 characters
+      myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr())
+      addrDiffer = myExternalIpAddr != self.local.getIpAddr()
+      
+      srcAddress = "%s:%s" % (myExternalIpAddr, self.local.getPort())
+      src = "%-21s" % srcAddress # ip:port = max of 21 characters
+      dst = "%-26s" % dstAddress # ip:port (xx) = max of 26 characters
+      
+      if width > 115:
+        # show fingerprint (column width: 42 characters)
+        etc += "%-40s  " % self.foreign.getFingerprint()
+      
+      if addrDiffer and width > 143:
+        # include the internal address in the src (extra 28 characters)
+        internalAddress = "%s:%s" % (self.local.getIpAddr(), self.local.getPort())
+        src = "%-21s  -->  %s" % (internalAddress, src)
+      
+      if (not addrDiffer and width > 143) or width > 155:
+        # show nickname (column width: remainder)
+        nicknameSpace = width - 146
+        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
+    elif listingType == connPanel.Listing.HOSTNAME:
+      # base data requires 80 characters
+      src = "localhost:%-5s" % self.local.getPort()
+      
+      # space available for foreign hostname (stretched to claim any free space)
+      hostnameSpace = width - 42
+      
+      if width > 108:
+        # show destination ip/port/locale (column width: 28 characters)
+        hostnameSpace -= 28
+        etc += "%-26s  " % dstAddress
+      
+      if width > 134:
+        # show fingerprint (column width: 42 characters)
+        hostnameSpace -= 42
+        etc += "%-40s  " % self.foreign.getFingerprint()
+      
+      if width > 151:
+        # show nickname (column width: min 17 characters, uses half of the remainder)
+        nicknameSpace = 15 + (width - 151) / 2
+        hostnameSpace -= (nicknameSpace + 2)
+        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
+      
+      if self.isPrivate():
+        dst = ("%%-%is" % hostnameSpace) % "<scrubbed>"
+      else:
+        hostname = self.foreign.getHostname(self.foreign.getIpAddr())
+        port = self.foreign.getPort()
+        
+        # exclude space needed for the ':<port>'
+        hostnameSpace -= len(port) + 1
+        
+        # truncates long hostnames and sets dst to <hostname>:<port>
+        hostname = uiTools.cropStr(hostname, hostnameSpace, 0)
+        dst = ("%%-%is:%%-5s" % hostnameSpace) % (hostname, port)
+    elif listingType == connPanel.Listing.FINGERPRINT:
+      # base data requires 75 characters
+      src = "localhost"
+      if self.type == Category.CONTROL: dst = "localhost"
+      else: dst = self.foreign.getFingerprint()
+      dst = "%-40s" % dst
+      
+      if width > 92:
+        # show nickname (column width: min 17 characters, uses remainder if extra room's available)
+        nicknameSpace = width - 78 if width < 126 else width - 106
+        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
+      
+      if width > 125:
+        # show destination ip/port/locale (column width: 28 characters)
+        etc += "%-26s  " % dstAddress
+    else:
+      # base data uses whatever extra room's available (using minimun of 50 characters)
+      src = self.local.getNickname()
+      if self.type == Category.CONTROL: dst = self.local.getNickname()
+      else: dst = self.foreign.getNickname()
+      
+      # space available for foreign nickname
+      nicknameSpace = width - len(src) - 27
+      
+      if width > 92:
+        # show fingerprint (column width: 42 characters)
+        nicknameSpace -= 42
+        etc += "%-40s  " % self.foreign.getFingerprint()
+      
+      if width > 120:
+        # show destination ip/port/locale (column width: 28 characters)
+        nicknameSpace -= 28
+        etc += "%-26s  " % dstAddress
+      
+      dst = ("%%-%is" % nicknameSpace) % dst
+    
+    if self.type == Category.INBOUND: src, dst = dst, src
+    padding = width - len(src) - len(dst) - len(etc) - 27
+    self._labelCache = "%s  -->  %s  %s%s" % (src, dst, etc, " " * padding)
+    self._labelCacheArgs = (listingType, width)
+    
+    return self._labelCache
+

Modified: arm/trunk/src/interface/controller.py
===================================================================
--- arm/trunk/src/interface/controller.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/interface/controller.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -24,11 +24,14 @@
 import descriptorPopup
 import fileDescriptorPopup
 
+import interface.connections.connPanel
 from util import conf, log, connections, hostnames, panel, sysTools, torConfig, torTools, uiTools
 import graphing.bandwidthStats
 import graphing.connStats
 import graphing.resourceStats
 
+INCLUDE_CONNPANEL_2 = False
+
 CONFIRM_QUIT = True
 REFRESH_RATE = 5        # seconds between redrawing screen
 MAX_REGEX_FILTERS = 5   # maximum number of previous regex filters that'll be remembered
@@ -43,8 +46,12 @@
   ["conn"],
   ["config"],
   ["torrc"]]
-PAUSEABLE = ["header", "graph", "log", "conn"]
 
+if INCLUDE_CONNPANEL_2:
+  PAGES.append(["conn2"])
+
+PAUSEABLE = ["header", "graph", "log", "conn", "conn2"]
+
 CONFIG = {"log.torrc.readFailed": log.WARN,
           "features.graph.type": 1,
           "features.config.prepopulateEditValues": True,
@@ -394,7 +401,7 @@
   
   if connections.isResolverAlive("tor"):
     resolver = connections.getResolver("tor")
-    resolver.setPaused(eventType == torTools.TOR_CLOSED)
+    resolver.setPaused(eventType == torTools.State.CLOSED)
 
 def selectiveRefresh(panels, page):
   """
@@ -471,12 +478,12 @@
     duplicateOptions, defaultOptions, mismatchLines, missingOptions = [], [], [], []
     
     for lineNum, issue, msg in corrections:
-      if issue == torConfig.VAL_DUPLICATE:
+      if issue == torConfig.ValidationError.DUPLICATE:
         duplicateOptions.append("%s (line %i)" % (msg, lineNum + 1))
-      elif issue == torConfig.VAL_IS_DEFAULT:
+      elif issue == torConfig.ValidationError.IS_DEFAULT:
         defaultOptions.append("%s (line %i)" % (msg, lineNum + 1))
-      elif issue == torConfig.VAL_MISMATCH: mismatchLines.append(lineNum + 1)
-      elif issue == torConfig.VAL_MISSING: missingOptions.append(msg)
+      elif issue == torConfig.ValidationError.MISMATCH: mismatchLines.append(lineNum + 1)
+      elif issue == torConfig.ValidationError.MISSING: missingOptions.append(msg)
     
     if duplicateOptions or defaultOptions:
       msg = "Unneeded torrc entries found. They've been highlighted in blue on the torrc page."
@@ -552,9 +559,10 @@
   panels["log"].setPaused(True)
   
   panels["conn"] = connPanel.ConnPanel(stdscr, conn, isBlindMode)
+  panels["conn2"] = interface.connections.connPanel.ConnectionPanel(stdscr, config)
   panels["control"] = ControlPanel(stdscr, isBlindMode)
-  panels["config"] = configPanel.ConfigPanel(stdscr, configPanel.TOR_STATE, config)
-  panels["torrc"] = torrcPanel.TorrcPanel(stdscr, torrcPanel.TORRC, config)
+  panels["config"] = configPanel.ConfigPanel(stdscr, configPanel.State.TOR, config)
+  panels["torrc"] = torrcPanel.TorrcPanel(stdscr, torrcPanel.Config.TORRC, config)
   
   # provides error if pid coulnd't be determined (hopefully shouldn't happen...)
   if not torPid: log.log(log.WARN, "Unable to resolve tor pid, abandoning connection listing")
@@ -604,16 +612,17 @@
   # tells revised panels to run as daemons
   panels["header"].start()
   panels["log"].start()
+  panels["conn2"].start()
   
   # warns if tor isn't updating descriptors
-  try:
-    if conn.get_option("FetchUselessDescriptors")[0][1] == "0" and conn.get_option("DirPort")[0][1] == "0":
-      warning = """Descriptors won't be updated (causing some connection information to be stale) unless:
-  a. 'FetchUselessDescriptors 1' is set in your torrc
-  b. the directory service is provided ('DirPort' defined)
-  c. or tor is used as a client"""
-      log.log(log.WARN, warning)
-  except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
+  #try:
+  #  if conn.get_option("FetchUselessDescriptors")[0][1] == "0" and conn.get_option("DirPort")[0][1] == "0":
+  #    warning = """Descriptors won't be updated (causing some connection information to be stale) unless:
+  #a. 'FetchUselessDescriptors 1' is set in your torrc
+  #b. the directory service is provided ('DirPort' defined)
+  #c. or tor is used as a client"""
+  #    log.log(log.WARN, warning)
+  #except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
   
   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
@@ -641,6 +650,11 @@
   
   lastSize = None
   
+  # sets initial visiblity for the pages
+  for i in range(len(PAGES)):
+    isVisible = i == page
+    for entry in PAGES[i]: panels[entry].setVisible(isVisible)
+  
   # TODO: come up with a nice, clean method for other threads to immediately
   # terminate the draw loop and provide a stacktrace
   while True:
@@ -846,6 +860,11 @@
       # (otherwise they'll wait on the curses lock which might get demanding)
       setPauseState(panels, isPaused, page)
       
+      # prevents panels on other pages from redrawing
+      for i in range(len(PAGES)):
+        isVisible = i == page
+        for entry in PAGES[i]: panels[entry].setVisible(isVisible)
+      
       panels["control"].page = page + 1
       
       # TODO: this redraw doesn't seem necessary (redraws anyway after this
@@ -916,7 +935,7 @@
           popup.addfstr(2, 41, "<b>n</b>: decrease graph size")
           popup.addfstr(3, 2, "<b>s</b>: graphed stats (<b>%s</b>)" % graphedStats)
           popup.addfstr(3, 41, "<b>i</b>: graph update interval (<b>%s</b>)" % graphing.graphPanel.UPDATE_INTERVALS[panels["graph"].updateInterval][0])
-          popup.addfstr(4, 2, "<b>b</b>: graph bounds (<b>%s</b>)" % graphing.graphPanel.BOUND_LABELS[panels["graph"].bounds])
+          popup.addfstr(4, 2, "<b>b</b>: graph bounds (<b>%s</b>)" % panels["graph"].bounds.lower())
           popup.addfstr(4, 41, "<b>d</b>: file descriptors")
           popup.addfstr(5, 2, "<b>e</b>: change logged events")
           
@@ -942,7 +961,6 @@
           
           resolverUtil = connections.getResolver("tor").overwriteResolver
           if resolverUtil == None: resolverUtil = "auto"
-          else: resolverUtil = connections.CMD_STR[resolverUtil]
           popup.addfstr(4, 41, "<b>u</b>: resolving utility (<b>%s</b>)" % resolverUtil)
           
           allowDnsLabel = "allow" if panels["conn"].allowDNS else "disallow"
@@ -1056,7 +1074,7 @@
       selectiveRefresh(panels, page)
     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) % 3
+      panels["graph"].bounds = graphing.graphPanel.Bounds.next(panels["graph"].bounds)
       
       selectiveRefresh(panels, page)
     elif page == 0 and key in (ord('d'), ord('D')):
@@ -1459,11 +1477,11 @@
         panels["conn"].sortConnections()
     elif page == 1 and (key == ord('u') or key == ord('U')):
       # provides menu to pick identification resolving utility
-      optionTypes = [None, connections.CMD_PROC, connections.CMD_NETSTAT, connections.CMD_SOCKSTAT, connections.CMD_LSOF, connections.CMD_SS, connections.CMD_BSD_SOCKSTAT, connections.CMD_BSD_PROCSTAT]
-      options = ["auto"] + [connections.CMD_STR[util] for util in optionTypes[1:]]
+      options = ["auto"] + connections.Resolver.values()
       
-      initialSelection = connections.getResolver("tor").overwriteResolver # enums correspond to indices
-      if initialSelection == None: initialSelection = 0
+      currentOverwrite = connections.getResolver("tor").overwriteResolver # enums correspond to indices
+      if currentOverwrite == None: initialSelection = 0
+      else: initialSelection = options.index(currentOverwrite)
       
       # hides top label of conn panel and pauses panels
       panels["conn"].showLabel = False
@@ -1471,14 +1489,15 @@
       setPauseState(panels, isPaused, page, True)
       
       selection = showMenu(stdscr, panels["popup"], "Resolver Util:", options, initialSelection)
+      selectedOption = options[selection] if selection != "auto" else None
       
       # reverts changes made for popup
       panels["conn"].showLabel = True
       setPauseState(panels, isPaused, page)
       
       # applies new setting
-      if selection != -1 and optionTypes[selection] != connections.getResolver("tor").overwriteResolver:
-        connections.getResolver("tor").overwriteResolver = optionTypes[selection]
+      if selection != -1 and selectedOption != connections.getResolver("tor").overwriteResolver:
+        connections.getResolver("tor").overwriteResolver = selectedOption
     elif page == 1 and (key == ord('s') or key == ord('S')):
       # set ordering for connection listing
       titleLabel = "Connection Ordering:"
@@ -1701,9 +1720,9 @@
     elif page == 2 and (key == ord('s') or key == ord('S')):
       # set ordering for config options
       titleLabel = "Config Option Ordering:"
-      options = [configPanel.FIELD_ATTR[i][0] for i in range(8)]
-      oldSelection = [configPanel.FIELD_ATTR[entry][0] for entry in panels["config"].sortOrdering]
-      optionColors = dict([configPanel.FIELD_ATTR[i] for i in range(8)])
+      options = [configPanel.FIELD_ATTR[field][0] for field in configPanel.Field.values()]
+      oldSelection = [configPanel.FIELD_ATTR[field][0] for field in panels["config"].sortOrdering]
+      optionColors = dict([configPanel.FIELD_ATTR[field] for field in configPanel.Field.values()])
       results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
       
       if results:
@@ -1727,13 +1746,13 @@
         
         # provides prompt
         selection = panels["config"].getSelection()
-        configOption = selection.get(configPanel.FIELD_OPTION)
+        configOption = selection.get(configPanel.Field.OPTION)
         titleMsg = "%s Value (esc to cancel): " % configOption
         panels["control"].setMsg(titleMsg)
         panels["control"].redraw(True)
         
         displayWidth = panels["control"].getPreferredSize()[1]
-        initialValue = selection.get(configPanel.FIELD_VALUE)
+        initialValue = selection.get(configPanel.Field.VALUE)
         
         # initial input for the text field
         initialText = ""
@@ -1747,19 +1766,19 @@
           conn = torTools.getConn()
           
           # if the value's a boolean then allow for 'true' and 'false' inputs
-          if selection.get(configPanel.FIELD_TYPE) == "Boolean":
+          if selection.get(configPanel.Field.TYPE) == "Boolean":
             if newConfigValue.lower() == "true": newConfigValue = "1"
             elif newConfigValue.lower() == "false": newConfigValue = "0"
           
           try:
-            if selection.get(configPanel.FIELD_TYPE) == "LineList":
+            if selection.get(configPanel.Field.TYPE) == "LineList":
               newConfigValue = newConfigValue.split(",")
             
             conn.setOption(configOption, newConfigValue)
             
             # resets the isDefault flag
             customOptions = torConfig.getCustomOptions()
-            selection.fields[configPanel.FIELD_IS_DEFAULT] = not configOption in customOptions
+            selection.fields[configPanel.Field.IS_DEFAULT] = not configOption in customOptions
             
             panels["config"].redraw(True)
           except Exception, exc:
@@ -1808,6 +1827,8 @@
       panels["config"].handleKey(key)
     elif page == 3:
       panels["torrc"].handleKey(key)
+    elif page == 4:
+      panels["conn2"].handleKey(key)
 
 def startTorMonitor(startTime, loggedEvents, isBlindMode):
   try:

Modified: arm/trunk/src/interface/graphing/__init__.py
===================================================================
--- arm/trunk/src/interface/graphing/__init__.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/interface/graphing/__init__.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -2,5 +2,5 @@
 Panels, popups, and handlers comprising the arm user interface.
 """
 
-__all__ = ["graphPanel.py", "bandwidthStats", "connStats", "resourceStats"]
+__all__ = ["graphPanel", "bandwidthStats", "connStats", "resourceStats"]
 

Modified: arm/trunk/src/interface/graphing/bandwidthStats.py
===================================================================
--- arm/trunk/src/interface/graphing/bandwidthStats.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/interface/graphing/bandwidthStats.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -47,7 +47,7 @@
     # rate/burst and if tor's using accounting
     conn = torTools.getConn()
     self._titleStats, self.isAccounting = [], False
-    self.resetListener(conn, torTools.TOR_INIT) # initializes values
+    self.resetListener(conn, torTools.State.INIT) # initializes values
     conn.addStatusListener(self.resetListener)
   
   def resetListener(self, conn, eventType):
@@ -55,7 +55,7 @@
     self._titleStats = []     # force reset of title
     self.new_desc_event(None) # updates title params
     
-    if eventType == torTools.TOR_INIT and self._config["features.graph.bw.accounting.show"]:
+    if eventType == torTools.State.INIT and self._config["features.graph.bw.accounting.show"]:
       self.isAccounting = conn.getInfo('accounting/enabled') == '1'
   
   def prepopulateFromState(self):

Modified: arm/trunk/src/interface/graphing/connStats.py
===================================================================
--- arm/trunk/src/interface/graphing/connStats.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/interface/graphing/connStats.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -17,11 +17,11 @@
     # listens for tor reload (sighup) events which can reset the ports tor uses
     conn = torTools.getConn()
     self.orPort, self.dirPort, self.controlPort = "0", "0", "0"
-    self.resetListener(conn, torTools.TOR_INIT) # initialize port values
+    self.resetListener(conn, torTools.State.INIT) # initialize port values
     conn.addStatusListener(self.resetListener)
   
   def resetListener(self, conn, eventType):
-    if eventType == torTools.TOR_INIT:
+    if eventType == torTools.State.INIT:
       self.orPort = conn.getOption("ORPort", "0")
       self.dirPort = conn.getOption("DirPort", "0")
       self.controlPort = conn.getOption("ControlPort", "0")

Modified: arm/trunk/src/interface/graphing/graphPanel.py
===================================================================
--- arm/trunk/src/interface/graphing/graphPanel.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/interface/graphing/graphPanel.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -20,7 +20,7 @@
 import curses
 from TorCtl import TorCtl
 
-from util import panel, uiTools
+from util import enum, panel, uiTools
 
 # time intervals at which graphs can be updated
 UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5),   ("30 seconds", 30),
@@ -32,11 +32,10 @@
 MIN_GRAPH_HEIGHT = 1
 
 # enums for graph bounds:
-#   BOUNDS_GLOBAL_MAX - global maximum (highest value ever seen)
-#   BOUNDS_LOCAL_MAX - local maximum (highest value currently on the graph)
-#   BOUNDS_TIGHT - local maximum and minimum
-BOUNDS_GLOBAL_MAX, BOUNDS_LOCAL_MAX, BOUNDS_TIGHT = range(3)
-BOUND_LABELS = {BOUNDS_GLOBAL_MAX: "global max", BOUNDS_LOCAL_MAX: "local max", BOUNDS_TIGHT: "tight"}
+#   Bounds.GLOBAL_MAX - global maximum (highest value ever seen)
+#   Bounds.LOCAL_MAX - local maximum (highest value currently on the graph)
+#   Bounds.TIGHT - local maximum and minimum
+Bounds = enum.Enum("GLOBAL_MAX", "LOCAL_MAX", "TIGHT")
 
 WIDE_LABELING_GRAPH_COL = 50  # minimum graph columns to use wide spacing for x-axis labels
 
@@ -248,7 +247,7 @@
   def __init__(self, stdscr):
     panel.Panel.__init__(self, stdscr, "graph", 0)
     self.updateInterval = CONFIG["features.graph.interval"]
-    self.bounds = CONFIG["features.graph.bound"]
+    self.bounds = Bounds.values()[CONFIG["features.graph.bound"]]
     self.graphHeight = CONFIG["features.graph.height"]
     self.currentDisplay = None    # label of the stats currently being displayed
     self.stats = {}               # available stats (mappings of label -> instance)
@@ -294,11 +293,11 @@
       if right: self.addstr(1, graphCol + 5, right, curses.A_BOLD | secondaryColor)
       
       # determines max/min value on the graph
-      if self.bounds == BOUNDS_GLOBAL_MAX:
+      if self.bounds == Bounds.GLOBAL_MAX:
         primaryMaxBound = int(param.maxPrimary[self.updateInterval])
         secondaryMaxBound = int(param.maxSecondary[self.updateInterval])
       else:
-        # both BOUNDS_LOCAL_MAX and BOUNDS_TIGHT use local maxima
+        # both Bounds.LOCAL_MAX and Bounds.TIGHT use local maxima
         if graphCol < 2:
           # nothing being displayed
           primaryMaxBound, secondaryMaxBound = 0, 0
@@ -307,7 +306,7 @@
           secondaryMaxBound = int(max(param.secondaryCounts[self.updateInterval][1:graphCol + 1]))
       
       primaryMinBound = secondaryMinBound = 0
-      if self.bounds == BOUNDS_TIGHT:
+      if self.bounds == Bounds.TIGHT:
         primaryMinBound = int(min(param.primaryCounts[self.updateInterval][1:graphCol + 1]))
         secondaryMinBound = int(min(param.secondaryCounts[self.updateInterval][1:graphCol + 1]))
         

Modified: arm/trunk/src/interface/headerPanel.py
===================================================================
--- arm/trunk/src/interface/headerPanel.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/interface/headerPanel.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -239,7 +239,7 @@
   
   def run(self):
     """
-    Keeps stats updated, querying new information at a set rate.
+    Keeps stats updated, checking for new information at a set rate.
     """
     
     lastDraw = time.time() - 1
@@ -288,14 +288,14 @@
       eventType - type of event detected
     """
     
-    if eventType == torTools.TOR_INIT:
+    if eventType == torTools.State.INIT:
       self._isTorConnected = True
       if self._isPaused: self._haltTime = time.time()
       else: self._haltTime = None
       
       self._update(True)
       self.redraw(True)
-    elif eventType == torTools.TOR_CLOSED:
+    elif eventType == torTools.State.CLOSED:
       self._isTorConnected = False
       self._haltTime = time.time()
       self._update()

Modified: arm/trunk/src/interface/logPanel.py
===================================================================
--- arm/trunk/src/interface/logPanel.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/interface/logPanel.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -32,8 +32,8 @@
           12345 arm runlevel+            X No Events
           67890 torctl runlevel+         U Unknown Events"""
 
-RUNLEVELS = ["DEBUG", "INFO", "NOTICE", "WARN", "ERR"]
-RUNLEVEL_EVENT_COLOR = {"DEBUG": "magenta", "INFO": "blue", "NOTICE": "green", "WARN": "yellow", "ERR": "red"}
+RUNLEVEL_EVENT_COLOR = {log.DEBUG: "magenta", log.INFO: "blue", log.NOTICE: "green",
+                        log.WARN: "yellow", log.ERR: "red"}
 DAYBREAK_EVENT = "DAYBREAK" # special event for marking when the date changes
 TIMEZONE_OFFSET = time.altzone if time.localtime()[8] else time.timezone
 
@@ -112,8 +112,8 @@
   
   for flag in eventAbbr:
     if flag == "A":
-      armRunlevels = ["ARM_" + runlevel for runlevel in RUNLEVELS]
-      torctlRunlevels = ["TORCTL_" + runlevel for runlevel in RUNLEVELS]
+      armRunlevels = ["ARM_" + runlevel for runlevel in log.Runlevel.values()]
+      torctlRunlevels = ["TORCTL_" + runlevel for runlevel in log.Runlevel.values()]
       expandedEvents = set(TOR_EVENT_TYPES.values() + armRunlevels + torctlRunlevels + ["UNKNOWN"])
       break
     elif flag == "X":
@@ -131,7 +131,7 @@
       elif flag in "W49": runlevelIndex = 3
       elif flag in "E50": runlevelIndex = 4
       
-      runlevelSet = [typePrefix + runlevel for runlevel in RUNLEVELS[runlevelIndex:]]
+      runlevelSet = [typePrefix + runlevel for runlevel in log.Runlevel.values()[runlevelIndex:]]
       expandedEvents = expandedEvents.union(set(runlevelSet))
     elif flag == "U":
       expandedEvents.add("UNKNOWN")
@@ -210,16 +210,17 @@
   
   # if the runlevels argument is a superset of the log file then we can
   # limit the read contents to the addLimit
+  runlevels = log.Runlevel.values()
   loggingTypes = loggingTypes.upper()
   if addLimit and (not readLimit or readLimit > addLimit):
     if "-" in loggingTypes:
       divIndex = loggingTypes.find("-")
-      sIndex = RUNLEVELS.index(loggingTypes[:divIndex])
-      eIndex = RUNLEVELS.index(loggingTypes[divIndex+1:])
-      logFileRunlevels = RUNLEVELS[sIndex:eIndex+1]
+      sIndex = runlevels.index(loggingTypes[:divIndex])
+      eIndex = runlevels.index(loggingTypes[divIndex+1:])
+      logFileRunlevels = runlevels[sIndex:eIndex+1]
     else:
-      sIndex = RUNLEVELS.index(loggingTypes)
-      logFileRunlevels = RUNLEVELS[sIndex:]
+      sIndex = runlevels.index(loggingTypes)
+      logFileRunlevels = runlevels[sIndex:]
     
     # checks if runlevels we're reporting are a superset of the file's contents
     isFileSubset = True
@@ -570,7 +571,7 @@
     # fetches past tor events from log file, if available
     torEventBacklog = []
     if self._config["features.log.prepopulate"]:
-      setRunlevels = list(set.intersection(set(self.loggedEvents), set(RUNLEVELS)))
+      setRunlevels = list(set.intersection(set(self.loggedEvents), set(log.Runlevel.values())))
       readLimit = self._config["features.log.prepopulateReadLimit"]
       addLimit = self._config["cache.logPanel.size"]
       torEventBacklog = getLogFileEntries(setRunlevels, readLimit, addLimit, self._config)
@@ -584,13 +585,12 @@
       # gets the set of arm events we're logging
       setRunlevels = []
       for i in range(len(armRunlevels)):
-        if "ARM_" + RUNLEVELS[i] in self.loggedEvents:
+        if "ARM_" + log.Runlevel.values()[i] in self.loggedEvents:
           setRunlevels.append(armRunlevels[i])
       
       armEventBacklog = []
       for level, msg, eventTime in log._getEntries(setRunlevels):
-        runlevelStr = log.RUNLEVEL_STR[level]
-        armEventEntry = LogEntry(eventTime, "ARM_" + runlevelStr, msg, RUNLEVEL_EVENT_COLOR[runlevelStr])
+        armEventEntry = LogEntry(eventTime, "ARM_" + level, msg, RUNLEVEL_EVENT_COLOR[level])
         armEventBacklog.insert(0, armEventEntry)
       
       # joins armEventBacklog and torEventBacklog chronologically into msgLog
@@ -879,7 +879,7 @@
             if lineOffset == maxEntriesPerLine - 1:
               msg = uiTools.cropStr(msg, maxMsgSize)
             else:
-              msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.END_WITH_HYPHEN, True)
+              msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.Ending.HYPHEN, True)
               displayQueue.insert(0, (remainder.strip(), format, includeBreak))
             
             includeBreak = True
@@ -1019,7 +1019,7 @@
       runlevelRanges = [] # tuple of type, startLevel, endLevel for ranges to be consensed
       
       # reverses runlevels and types so they're appended in the right order
-      reversedRunlevels = list(RUNLEVELS)
+      reversedRunlevels = log.Runlevel.values()
       reversedRunlevels.reverse()
       for prefix in ("TORCTL_", "ARM_", ""):
         # blank ending runlevel forces the break condition to be reached at the end

Modified: arm/trunk/src/interface/torrcPanel.py
===================================================================
--- arm/trunk/src/interface/torrcPanel.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/interface/torrcPanel.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -6,14 +6,14 @@
 import curses
 import threading
 
-from util import conf, panel, torConfig, uiTools
+from util import conf, enum, panel, torConfig, uiTools
 
 DEFAULT_CONFIG = {"features.config.file.showScrollbars": True,
                   "features.config.file.maxLinesPerEntry": 8}
 
 # TODO: The armrc use case is incomplete. There should be equivilant reloading
 # and validation capabilities to the torrc.
-TORRC, ARMRC = range(1, 3) # configuration file types that can be displayed
+Config = enum.Enum("TORRC", "ARMRC") # configuration file types that can be displayed
 
 class TorrcPanel(panel.Panel):
   """
@@ -74,7 +74,7 @@
     self.scroll = max(0, min(self.scroll, self._lastContentHeight - height + 1))
     
     renderedContents, corrections, confLocation = None, {}, None
-    if self.configType == TORRC:
+    if self.configType == Config.TORRC:
       loadedTorrc = torConfig.getTorrc()
       loadedTorrc.getLock().acquire()
       confLocation = loadedTorrc.getConfigLocation()
@@ -109,7 +109,7 @@
     
     # draws the top label
     if self.showLabel:
-      sourceLabel = "Tor" if self.configType == TORRC else "Arm"
+      sourceLabel = "Tor" if self.configType == Config.TORRC else "Arm"
       locationLabel = " (%s)" % confLocation if confLocation else ""
       self.addstr(0, 0, "%s Configuration File%s:" % (sourceLabel, locationLabel), curses.A_STANDOUT)
     
@@ -157,10 +157,10 @@
       if lineNumber in corrections:
         lineIssue, lineIssueMsg = corrections[lineNumber]
         
-        if lineIssue in (torConfig.VAL_DUPLICATE, torConfig.VAL_IS_DEFAULT):
+        if lineIssue in (torConfig.ValidationError.DUPLICATE, torConfig.ValidationError.IS_DEFAULT):
           lineComp["option"][1] = curses.A_BOLD | uiTools.getColor("blue")
           lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("blue")
-        elif lineIssue == torConfig.VAL_MISMATCH:
+        elif lineIssue == torConfig.ValidationError.MISMATCH:
           lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("red")
           lineComp["correction"][0] = " (%s)" % lineIssueMsg
         else:
@@ -189,7 +189,7 @@
             msg = uiTools.cropStr(msg, maxMsgSize)
           else:
             includeBreak = True
-            msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.END_WITH_HYPHEN, True)
+            msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.Ending.HYPHEN, True)
             displayQueue.insert(0, (remainder.strip(), format))
         
         drawLine = displayLine + lineOffset

Modified: arm/trunk/src/starter.py
===================================================================
--- arm/trunk/src/starter.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/starter.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -36,8 +36,9 @@
           "startup.interface.port": 9051,
           "startup.blindModeEnabled": False,
           "startup.events": "N3",
-          "data.cache.path": "~/.arm/cache",
+          "startup.dataDirectory": "~/.arm",
           "features.config.descriptions.enabled": True,
+          "features.config.descriptions.persist": True,
           "log.configDescriptions.readManPageSuccess": util.log.INFO,
           "log.configDescriptions.readManPageFailed": util.log.NOTICE,
           "log.configDescriptions.internalLoadSuccess": util.log.NOTICE,
@@ -102,13 +103,14 @@
     isConfigDescriptionsLoaded = False
     
     # determines the path where cached descriptions should be persisted (left
-    # undefined of arm caching is disabled)
-    cachePath, descriptorPath = CONFIG["data.cache.path"], None
+    # undefined if caching is disabled)
+    descriptorPath = None
+    if CONFIG["features.config.descriptions.persist"]:
+      dataDir = CONFIG["startup.dataDirectory"]
+      if not dataDir.endswith("/"): dataDir += "/"
+      
+      descriptorPath = os.path.expanduser(dataDir + "cache/") + CONFIG_DESC_FILENAME
     
-    if cachePath:
-      if not cachePath.endswith("/"): cachePath += "/"
-      descriptorPath = os.path.expanduser(cachePath) + CONFIG_DESC_FILENAME
-    
     # attempts to load configuration descriptions cached in the data directory
     if descriptorPath:
       try:

Modified: arm/trunk/src/test.py
===================================================================
--- arm/trunk/src/test.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/test.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -44,7 +44,7 @@
       connectionResults.sort()
       allConnectionResults.append(connectionResults)
       
-      resolverLabel = "%-10s" % connections.CMD_STR[resolver]
+      resolverLabel = "%-10s" % resolver
       countLabel = "%4i results" % len(connectionResults)
       timeLabel = "%0.4f seconds" % (time.time() - startTime)
       print "%s %s     %s" % (resolverLabel, countLabel, timeLabel)
@@ -68,8 +68,9 @@
       # provide the selection options
       printDivider()
       print("Select a resolver:")
-      for i in range(1, 8):
-        print("  %i. %s" % (i, connections.CMD_STR[i]))
+      availableResolvers = connections.Resolver.values()
+      for i in range(len(availableResolvers)):
+        print("  %i. %s" % (i, availableResolvers[i]))
       print("  q. Go back to the main menu")
       
       userSelection = raw_input("\nSelection: ")

Modified: arm/trunk/src/util/__init__.py
===================================================================
--- arm/trunk/src/util/__init__.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/util/__init__.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -4,5 +4,5 @@
 and safely working with curses (hiding some of the gory details).
 """
 
-__all__ = ["conf", "connections", "hostnames", "log", "panel", "procTools", "sysTools", "torConfig", "torTools", "uiTools"]
+__all__ = ["conf", "connections", "enum", "hostnames", "log", "panel", "procTools", "sysTools", "torConfig", "torTools", "uiTools"]
 

Modified: arm/trunk/src/util/conf.py
===================================================================
--- arm/trunk/src/util/conf.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/util/conf.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -105,17 +105,16 @@
       default  - value provided if no such key exists
     """
     
-    callDefault = log.runlevelToStr(default) if key.startswith("log.") else default
     isMultivalue = isinstance(default, list) or isinstance(default, dict)
-    val = self.getValue(key, callDefault, isMultivalue)
+    val = self.getValue(key, default, isMultivalue)
     if val == default: return val
     
     if key.startswith("log."):
-      if val.lower() in ("none", "debug", "info", "notice", "warn", "err"):
-        val = log.strToRunlevel(val)
+      if val.upper() == "NONE": val = None
+      elif val.upper() in log.Runlevel.values(): val = val.upper()
       else:
         msg = "config entry '%s' is expected to be a runlevel" % key
-        if default != None: msg += ", defaulting to '%s'" % callDefault
+        if default != None: msg += ", defaulting to '%s'" % default
         log.log(CONFIG["log.configEntryTypeError"], msg)
         val = default
     elif isinstance(default, bool):

Modified: arm/trunk/src/util/connections.py
===================================================================
--- arm/trunk/src/util/connections.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/util/connections.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -21,17 +21,16 @@
 import time
 import threading
 
-from util import log, procTools, sysTools
+from util import enum, log, procTools, sysTools
 
 # enums for connection resolution utilities
-CMD_PROC, CMD_NETSTAT, CMD_SOCKSTAT, CMD_LSOF, CMD_SS, CMD_BSD_SOCKSTAT, CMD_BSD_PROCSTAT = range(1, 8)
-CMD_STR = {CMD_PROC: "proc",
-           CMD_NETSTAT: "netstat",
-           CMD_SS: "ss",
-           CMD_LSOF: "lsof",
-           CMD_SOCKSTAT: "sockstat",
-           CMD_BSD_SOCKSTAT: "sockstat (bsd)",
-           CMD_BSD_PROCSTAT: "procstat (bsd)"}
+Resolver = enum.Enum(("PROC", "proc"),
+                     ("NETSTAT", "netstat"),
+                     ("SS", "ss"),
+                     ("LSOF", "lsof"),
+                     ("SOCKSTAT", "sockstat"),
+                     ("BSD_SOCKSTAT", "sockstat (bsd)"),
+                     ("BSD_PROCSTAT", "procstat (bsd)"))
 
 # If true this provides new instantiations for resolvers if the old one has
 # been stopped. This can make it difficult ensure all threads are terminated
@@ -100,7 +99,7 @@
   
   return True
 
-def ipAddressIsPrivate(ipAddr):
+def isIpAddressPrivate(ipAddr):
   """
   Provides true if the IP address belongs on the local network or belongs to
   loopback, false otherwise. These include:
@@ -138,19 +137,19 @@
   
   if not processPid:
     # the pid is required for procstat resolution
-    if resolutionCmd == CMD_BSD_PROCSTAT:
+    if resolutionCmd == Resolver.BSD_PROCSTAT:
       raise ValueError("procstat resolution requires a pid")
     
     # if the pid was undefined then match any in that field
     processPid = "[0-9]*"
   
-  if resolutionCmd == CMD_PROC: return ""
-  elif resolutionCmd == CMD_NETSTAT: return RUN_NETSTAT % (processPid, processName)
-  elif resolutionCmd == CMD_SS: return RUN_SS % (processName, processPid)
-  elif resolutionCmd == CMD_LSOF: return RUN_LSOF % (processName, processPid)
-  elif resolutionCmd == CMD_SOCKSTAT: return RUN_SOCKSTAT % (processName, processPid)
-  elif resolutionCmd == CMD_BSD_SOCKSTAT: return RUN_BSD_SOCKSTAT % (processName, processPid)
-  elif resolutionCmd == CMD_BSD_PROCSTAT: return RUN_BSD_PROCSTAT % processPid
+  if resolutionCmd == Resolver.PROC: return ""
+  elif resolutionCmd == Resolver.NETSTAT: return RUN_NETSTAT % (processPid, processName)
+  elif resolutionCmd == Resolver.SS: return RUN_SS % (processName, processPid)
+  elif resolutionCmd == Resolver.LSOF: return RUN_LSOF % (processName, processPid)
+  elif resolutionCmd == Resolver.SOCKSTAT: return RUN_SOCKSTAT % (processName, processPid)
+  elif resolutionCmd == Resolver.BSD_SOCKSTAT: return RUN_BSD_SOCKSTAT % (processName, processPid)
+  elif resolutionCmd == Resolver.BSD_PROCSTAT: return RUN_BSD_PROCSTAT % processPid
   else: raise ValueError("Unrecognized resolution type: %s" % resolutionCmd)
 
 def getConnections(resolutionCmd, processName, processPid = ""):
@@ -170,7 +169,7 @@
     processPid    - process ID (this helps improve accuracy)
   """
   
-  if resolutionCmd == CMD_PROC:
+  if resolutionCmd == Resolver.PROC:
     # Attempts resolution via checking the proc contents.
     if not processPid:
       raise ValueError("proc resolution requires a pid")
@@ -190,30 +189,30 @@
     # parses results for the resolution command
     conn = []
     for line in results:
-      if resolutionCmd == CMD_LSOF:
+      if resolutionCmd == Resolver.LSOF:
         # Different versions of lsof have different numbers of columns, so
         # stripping off the optional 'established' entry so we can just use
         # the last one.
         comp = line.replace("(ESTABLISHED)", "").strip().split()
       else: comp = line.split()
       
-      if resolutionCmd == CMD_NETSTAT:
+      if resolutionCmd == Resolver.NETSTAT:
         localIp, localPort = comp[3].split(":")
         foreignIp, foreignPort = comp[4].split(":")
-      elif resolutionCmd == CMD_SS:
+      elif resolutionCmd == Resolver.SS:
         localIp, localPort = comp[4].split(":")
         foreignIp, foreignPort = comp[5].split(":")
-      elif resolutionCmd == CMD_LSOF:
+      elif resolutionCmd == Resolver.LSOF:
         local, foreign = comp[-1].split("->")
         localIp, localPort = local.split(":")
         foreignIp, foreignPort = foreign.split(":")
-      elif resolutionCmd == CMD_SOCKSTAT:
+      elif resolutionCmd == Resolver.SOCKSTAT:
         localIp, localPort = comp[4].split(":")
         foreignIp, foreignPort = comp[5].split(":")
-      elif resolutionCmd == CMD_BSD_SOCKSTAT:
+      elif resolutionCmd == Resolver.BSD_SOCKSTAT:
         localIp, localPort = comp[5].split(":")
         foreignIp, foreignPort = comp[6].split(":")
-      elif resolutionCmd == CMD_BSD_PROCSTAT:
+      elif resolutionCmd == Resolver.BSD_PROCSTAT:
         localIp, localPort = comp[9].split(":")
         foreignIp, foreignPort = comp[10].split(":")
       
@@ -280,13 +279,13 @@
   if osType == None: osType = os.uname()[0]
   
   if osType == "FreeBSD":
-    resolvers = [CMD_BSD_SOCKSTAT, CMD_BSD_PROCSTAT, CMD_LSOF]
+    resolvers = [Resolver.BSD_SOCKSTAT, Resolver.BSD_PROCSTAT, Resolver.LSOF]
   else:
-    resolvers = [CMD_NETSTAT, CMD_SOCKSTAT, CMD_LSOF, CMD_SS]
+    resolvers = [Resolver.NETSTAT, Resolver.SOCKSTAT, Resolver.LSOF, Resolver.SS]
   
   # proc resolution, by far, outperforms the others so defaults to this is able
   if procTools.isProcAvailable():
-    resolvers = [CMD_PROC] + resolvers
+    resolvers = [Resolver.PROC] + resolvers
   
   return resolvers
 
@@ -356,18 +355,21 @@
     self.defaultRate = CONFIG["queries.connections.minRate"]
     self.lastLookup = -1
     self.overwriteResolver = None
-    self.defaultResolver = CMD_PROC
+    self.defaultResolver = Resolver.PROC
     
     osType = os.uname()[0]
     self.resolverOptions = getSystemResolvers(osType)
     
-    resolverLabels = ", ".join([CMD_STR[option] for option in self.resolverOptions])
-    log.log(CONFIG["log.connResolverOptions"], "Operating System: %s, Connection Resolvers: %s" % (osType, resolverLabels))
+    log.log(CONFIG["log.connResolverOptions"], "Operating System: %s, Connection Resolvers: %s" % (osType, ", ".join(self.resolverOptions)))
     
     # sets the default resolver to be the first found in the system's PATH
     # (left as netstat if none are found)
     for resolver in self.resolverOptions:
-      if resolver == CMD_PROC or sysTools.isAvailable(CMD_STR[resolver]):
+      # Resolver strings correspond to their command with the exception of bsd
+      # resolvers.
+      resolverCmd = resolver.replace(" (bsd)", "")
+      
+      if resolver == Resolver.PROC or sysTools.isAvailable(resolverCmd):
         self.defaultResolver = resolver
         break
     
@@ -450,7 +452,7 @@
             
             if newResolver:
               # provide notice that failures have occurred and resolver is changing
-              msg = RESOLVER_SERIAL_FAILURE_MSG % (CMD_STR[resolver], CMD_STR[newResolver])
+              msg = RESOLVER_SERIAL_FAILURE_MSG % (resolver, newResolver)
               log.log(CONFIG["log.connLookupFailover"], msg)
             else:
               # exhausted all resolvers, give warning

Added: arm/trunk/src/util/enum.py
===================================================================
--- arm/trunk/src/util/enum.py	                        (rev 0)
+++ arm/trunk/src/util/enum.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -0,0 +1,103 @@
+"""
+Basic enumeration, providing ordered types for collections. These can be
+constructed as simple type listings, ie:
+>>> insects = Enum("ANT", "WASP", "LADYBUG", "FIREFLY")
+>>> insects.ANT
+'Ant'
+>>> insects.values()
+['Ant', 'Wasp', 'Ladybug', 'Firefly']
+
+with overwritten string counterparts:
+>>> pets = Enum(("DOG", "Skippy"), "CAT", ("FISH", "Nemo"))
+>>> pets.DOG
+'Skippy'
+>>> pets.CAT
+"Cat"
+
+or with entirely custom string components as an unordered enum with:
+>>> pets = LEnum(DOG="Skippy", CAT="Kitty", FISH="Nemo")
+>>> pets.CAT
+"Kitty"
+"""
+
+def toCamelCase(label):
+  """
+  Converts the given string to camel case, ie:
+  >>> toCamelCase("I_LIKE_PEPPERJACK!")
+  'I Like Pepperjack!'
+  
+  Arguments:
+    label - input string to be converted
+  """
+  
+  words = []
+  for entry in label.split("_"):
+    if len(entry) == 1: words.append(entry.upper())
+    else: words.append(entry[0].upper() + entry[1:].lower())
+  
+  return " ".join(words)
+
+class Enum:
+  """
+  Basic enumaration.
+  """
+  
+  def __init__(self, *args):
+    self.orderedValues = []
+    
+    for entry in args:
+      if isinstance(entry, str):
+        key, val = entry, toCamelCase(entry)
+      elif isinstance(entry, tuple) and len(entry) == 2:
+        key, val = entry
+      else: raise ValueError("Unrecognized input: %s" % args)
+      
+      self.__dict__[key] = val
+      self.orderedValues.append(val)
+  
+  def values(self):
+    """
+    Provides an ordered listing of the enumerations in this set.
+    """
+    
+    return list(self.orderedValues)
+  
+  def next(self, value):
+    """
+    Provides the next enumeration after the given value, raising a ValueError
+    if no such enum exists.
+    
+    Arguments:
+      value - enumeration for which to get the next entry
+    """
+    
+    if not value in self.orderedValues:
+      raise ValueError("No such enumeration exists: %s (options: %s)" % (value, ", ".join(self.orderedValues)))
+    
+    nextIndex = (self.orderedValues.index(value) + 1) % len(self.orderedValues)
+    return self.orderedValues[nextIndex]
+  
+  def previous(self, value):
+    """
+    Provides the previous enumeration before the given value, raising a
+    ValueError if no such enum exists.
+    
+    Arguments:
+      value - enumeration for which to get the previous entry
+    """
+    
+    if not value in self.orderedValues:
+      raise ValueError("No such enumeration exists: %s (options: %s)" % (value, ", ".join(self.orderedValues)))
+    
+    prevIndex = (self.orderedValues.index(value) - 1) % len(self.orderedValues)
+    return self.orderedValues[prevIndex]
+
+class LEnum(Enum):
+  """
+  Enumeration that accepts custom string mappings.
+  """
+  
+  def __init__(self, **args):
+    self.__dict__.update(args)
+    self.orderedValues = sorted(args.values())
+

Modified: arm/trunk/src/util/log.py
===================================================================
--- arm/trunk/src/util/log.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/util/log.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -11,19 +11,23 @@
 from sys import maxint
 from threading import RLock
 
-# logging runlevels
-DEBUG, INFO, NOTICE, WARN, ERR = range(1, 6)
-RUNLEVEL_STR = {DEBUG: "DEBUG", INFO: "INFO", NOTICE: "NOTICE", WARN: "WARN", ERR: "ERR"}
+from util import enum
 
+# Logging runlevels. These are *very* commonly used so including shorter
+# aliases (so they can be referenced as log.DEBUG, log.WARN, etc).
+Runlevel = enum.Enum(("DEBUG", "DEBUG"), ("INFO", "INFO"), ("NOTICE", "NOTICE"),
+                     ("WARN", "WARN"), ("ERR", "ERR"))
+DEBUG, INFO, NOTICE, WARN, ERR = Runlevel.values()
+
 # provides thread safety for logging operations
 LOG_LOCK = RLock()
 
 # chronologically ordered records of events for each runlevel, stored as tuples
 # consisting of: (time, message)
-_backlog = dict([(level, []) for level in range(1, 6)])
+_backlog = dict([(level, []) for level in Runlevel.values()])
 
 # mapping of runlevels to the listeners interested in receiving events from it
-_listeners = dict([(level, []) for level in range(1, 6)])
+_listeners = dict([(level, []) for level in Runlevel.values()])
 
 CONFIG = {"cache.armLog.size": 1000,
           "cache.armLog.trimSize": 200}
@@ -55,36 +59,6 @@
   
   DUMP_FILE = open(logPath, "w")
 
-def strToRunlevel(runlevelStr):
-  """
-  Converts runlevel strings ("DEBUG", "INFO", "NOTICE", etc) to their
-  corresponding enumeations. This isn't case sensitive and provides None if
-  unrecognized.
-  
-  Arguments:
-    runlevelStr - string to be converted to runlevel
-  """
-  
-  if not runlevelStr: return None
-  
-  runlevelStr = runlevelStr.upper()
-  for enum, level in RUNLEVEL_STR.items():
-    if level == runlevelStr: return enum
-  
-  return None
-
-def runlevelToStr(runlevelEnum):
-  """
-  Converts runlevel enumerations to corresponding string. If unrecognized then
-  this provides "NONE".
-  
-  Arguments:
-    runlevelEnum - enumeration to be converted to string
-  """
-  
-  if runlevelEnum in RUNLEVEL_STR: return RUNLEVEL_STR[runlevelEnum]
-  else: return "NONE"
-
 def log(level, msg, eventTime = None):
   """
   Registers an event, directing it to interested listeners and preserving it in
@@ -128,7 +102,7 @@
       try:
         entryTime = time.localtime(eventTime)
         timeLabel = "%i/%i/%i %02i:%02i:%02i" % (entryTime[1], entryTime[2], entryTime[0], entryTime[3], entryTime[4], entryTime[5])
-        logEntry = "%s [%s] %s\n" % (timeLabel, runlevelToStr(level), msg)
+        logEntry = "%s [%s] %s\n" % (timeLabel, level, msg)
         DUMP_FILE.write(logEntry)
         DUMP_FILE.flush()
       except IOError, exc:
@@ -137,7 +111,7 @@
     
     # notifies listeners
     for callback in _listeners[level]:
-      callback(RUNLEVEL_STR[level], msg, eventTime)
+      callback(level, msg, eventTime)
   finally:
     LOG_LOCK.release()
 
@@ -175,7 +149,7 @@
     
     if dumpBacklog:
       for level, msg, eventTime in _getEntries(levels):
-        callback(RUNLEVEL_STR[level], msg, eventTime)
+        callback(level, msg, eventTime)
   finally:
     LOG_LOCK.release()
 

Modified: arm/trunk/src/util/panel.py
===================================================================
--- arm/trunk/src/util/panel.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/util/panel.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -56,8 +56,9 @@
     # implementations aren't entirely deterministic (for instance panels
     # might chose their height based on its parent's current width).
     
+    self.panelName = name
     self.parent = parent
-    self.panelName = name
+    self.visible = True
     self.top = top
     self.height = height
     self.width = width
@@ -99,6 +100,23 @@
       self.parent = parent
       self.win = None
   
+  def isVisible(self):
+    """
+    Provides if the panel's configured to be visible or not.
+    """
+    
+    return self.visible
+  
+  def setVisible(self, isVisible):
+    """
+    Toggles if the panel is visible or not.
+    
+    Arguments:
+      isVisible - panel is redrawn when requested if true, skipped otherwise
+    """
+    
+    self.visible = isVisible
+  
   def getTop(self):
     """
     Provides the position subwindows are placed at within its parent.
@@ -198,6 +216,9 @@
                     abandoned
     """
     
+    # skipped if not currently visible
+    if not self.isVisible(): return
+    
     # if the panel's completely outside its parent then this is a no-op
     newHeight, newWidth = self.getPreferredSize()
     if newHeight == 0:

Modified: arm/trunk/src/util/procTools.py
===================================================================
--- arm/trunk/src/util/procTools.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/util/procTools.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -20,12 +20,12 @@
 import socket
 import base64
 
-from util import log
+from util import enum, log
 
 # cached system values
 SYS_START_TIME, SYS_PHYSICAL_MEMORY = None, None
 CLOCK_TICKS = os.sysconf(os.sysconf_names["SC_CLK_TCK"])
-STAT_COMMAND, STAT_CPU_UTIME, STAT_CPU_STIME, STAT_START_TIME = range(4)
+Stat = enum.Enum("COMMAND", "CPU_UTIME", "CPU_STIME", "START_TIME")
 
 CONFIG = {"queries.useProc": True,
           "log.procCallMade": log.DEBUG}
@@ -128,10 +128,10 @@
 def getStats(pid, *statTypes):
   """
   Provides process specific information. Options are:
-  STAT_COMMAND      command name under which the process is running
-  STAT_CPU_UTIME    total user time spent on the process
-  STAT_CPU_STIME    total system time spent on the process
-  STAT_START_TIME   when this process began, in unix time
+  Stat.COMMAND      command name under which the process is running
+  Stat.CPU_UTIME    total user time spent on the process
+  Stat.CPU_STIME    total system time spent on the process
+  Stat.START_TIME   when this process began, in unix time
   
   Arguments:
     pid       - queried process
@@ -159,19 +159,19 @@
   
   results, queriedStats = [], []
   for statType in statTypes:
-    if statType == STAT_COMMAND:
+    if statType == Stat.COMMAND:
       queriedStats.append("command")
       if pid == 0: results.append("sched")
       else: results.append(statComp[1])
-    elif statType == STAT_CPU_UTIME:
+    elif statType == Stat.CPU_UTIME:
       queriedStats.append("utime")
       if pid == 0: results.append("0")
       else: results.append(str(float(statComp[13]) / CLOCK_TICKS))
-    elif statType == STAT_CPU_STIME:
+    elif statType == Stat.CPU_STIME:
       queriedStats.append("stime")
       if pid == 0: results.append("0")
       else: results.append(str(float(statComp[14]) / CLOCK_TICKS))
-    elif statType == STAT_START_TIME:
+    elif statType == Stat.START_TIME:
       queriedStats.append("start time")
       if pid == 0: return getSystemStartTime()
       else:

Modified: arm/trunk/src/util/sysTools.py
===================================================================
--- arm/trunk/src/util/sysTools.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/util/sysTools.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -126,7 +126,7 @@
   # fetch it from proc contents if available
   if procTools.isProcAvailable():
     try:
-      processName = procTools.getStats(pid, procTools.STAT_COMMAND)[0]
+      processName = procTools.getStats(pid, procTools.Stat.COMMAND)[0]
     except IOError, exc:
       raisedExc = exc
   
@@ -466,7 +466,7 @@
       newValues = {}
       try:
         if self._useProc:
-          utime, stime, startTime = procTools.getStats(self.processPid, procTools.STAT_CPU_UTIME, procTools.STAT_CPU_STIME, procTools.STAT_START_TIME)
+          utime, stime, startTime = procTools.getStats(self.processPid, procTools.Stat.CPU_UTIME, procTools.Stat.CPU_STIME, procTools.Stat.START_TIME)
           totalCpuTime = float(utime) + float(stime)
           cpuDelta = totalCpuTime - self._lastCpuTotal
           newValues["cpuSampling"] = cpuDelta / timeSinceReset

Modified: arm/trunk/src/util/torConfig.py
===================================================================
--- arm/trunk/src/util/torConfig.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/util/torConfig.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -5,7 +5,7 @@
 import os
 import threading
 
-from util import log, sysTools, torTools, uiTools
+from util import enum, log, sysTools, torTools, uiTools
 
 CONFIG = {"features.torrc.validate": True,
           "config.important": [],
@@ -23,27 +23,23 @@
           "log.configDescriptions.unrecognizedCategory": log.NOTICE}
 
 # enums and values for numeric torrc entries
-UNRECOGNIZED, SIZE_VALUE, TIME_VALUE = range(1, 4)
+ValueType = enum.Enum("UNRECOGNIZED", "SIZE", "TIME")
 SIZE_MULT = {"b": 1, "kb": 1024, "mb": 1048576, "gb": 1073741824, "tb": 1099511627776}
 TIME_MULT = {"sec": 1, "min": 60, "hour": 3600, "day": 86400, "week": 604800}
 
 # enums for issues found during torrc validation:
-# VAL_DUPLICATE  - entry is ignored due to being a duplicate
-# VAL_MISMATCH   - the value doesn't match tor's current state
-# VAL_MISSING    - value differs from its default but is missing from the torrc
-# VAL_IS_DEFAULT - the configuration option matches tor's default
-VAL_DUPLICATE, VAL_MISMATCH, VAL_MISSING, VAL_IS_DEFAULT = range(1, 5)
+# DUPLICATE  - entry is ignored due to being a duplicate
+# MISMATCH   - the value doesn't match tor's current state
+# MISSING    - value differs from its default but is missing from the torrc
+# IS_DEFAULT - the configuration option matches tor's default
+ValidationError = enum.Enum("DUPLICATE", "MISMATCH", "MISSING", "IS_DEFAULT")
 
 # descriptions of tor's configuration options fetched from its man page
 CONFIG_DESCRIPTIONS_LOCK = threading.RLock()
 CONFIG_DESCRIPTIONS = {}
 
 # categories for tor configuration options
-GENERAL, CLIENT, SERVER, DIRECTORY, AUTHORITY, HIDDEN_SERVICE, TESTING, UNKNOWN = range(1, 9)
-OPTION_CATEGORY_STR = {GENERAL: "General",     CLIENT: "Client",
-                       SERVER: "Relay",        DIRECTORY: "Directory",
-                       AUTHORITY: "Authority", HIDDEN_SERVICE: "Hidden Service",
-                       TESTING: "Testing",     UNKNOWN: "Unknown"}
+Category = enum.Enum("GENERAL", "CLIENT", "RELAY", "DIRECTORY", "AUTHORITY", "HIDDEN_SERVICE", "TESTING", "UNKNOWN")
 
 TORRC = None # singleton torrc instance
 MAN_OPT_INDENT = 7 # indentation before options in the man page
@@ -123,9 +119,6 @@
       inputFileContents = inputFile.readlines()
       inputFile.close()
       
-      # constructs a reverse mapping for categories
-      strToCat = dict([(OPTION_CATEGORY_STR[cat], cat) for cat in OPTION_CATEGORY_STR])
-      
       try:
         versionLine = inputFileContents.pop(0).rstrip()
         
@@ -142,10 +135,8 @@
         
         while inputFileContents:
           # gets category enum, failing if it doesn't exist
-          categoryStr = inputFileContents.pop(0).rstrip()
-          if categoryStr in strToCat:
-            category = strToCat[categoryStr]
-          else:
+          category = inputFileContents.pop(0).rstrip()
+          if not category in Category.values():
             baseMsg = "invalid category in input file: '%s'"
             raise IOError(baseMsg % categoryStr)
           
@@ -187,7 +178,7 @@
         validOptions = [line[:line.find(" ")].lower() for line in configOptionQuery]
       
       optionCount, lastOption, lastArg = 0, None, None
-      lastCategory, lastDescription = GENERAL, ""
+      lastCategory, lastDescription = Category.GENERAL, ""
       for line in manCallResults:
         line = uiTools.getPrintable(line)
         strippedLine = line.strip()
@@ -221,13 +212,13 @@
           
           # if this is a category header then switch it
           if isCategoryLine:
-            if line.startswith("OPTIONS"): lastCategory = GENERAL
-            elif line.startswith("CLIENT"): lastCategory = CLIENT
-            elif line.startswith("SERVER"): lastCategory = SERVER
-            elif line.startswith("DIRECTORY SERVER"): lastCategory = DIRECTORY
-            elif line.startswith("DIRECTORY AUTHORITY SERVER"): lastCategory = AUTHORITY
-            elif line.startswith("HIDDEN SERVICE"): lastCategory = HIDDEN_SERVICE
-            elif line.startswith("TESTING NETWORK"): lastCategory = TESTING
+            if line.startswith("OPTIONS"): lastCategory = Category.GENERAL
+            elif line.startswith("CLIENT"): lastCategory = Category.CLIENT
+            elif line.startswith("SERVER"): lastCategory = Category.RELAY
+            elif line.startswith("DIRECTORY SERVER"): lastCategory = Category.DIRECTORY
+            elif line.startswith("DIRECTORY AUTHORITY SERVER"): lastCategory = Category.AUTHORITY
+            elif line.startswith("HIDDEN SERVICE"): lastCategory = Category.HIDDEN_SERVICE
+            elif line.startswith("TESTING NETWORK"): lastCategory = Category.TESTING
             else:
               msg = "Unrecognized category in the man page: %s" % line.strip()
               log.log(CONFIG["log.configDescriptions.unrecognizedCategory"], msg)
@@ -273,7 +264,7 @@
   for i in range(len(sortedOptions)):
     option = sortedOptions[i]
     manEntry = getConfigDescription(option)
-    outputFile.write("%s\nindex: %i\n%s\n%s\n%s\n" % (OPTION_CATEGORY_STR[manEntry.category], manEntry.index, option, manEntry.argUsage, manEntry.description))
+    outputFile.write("%s\nindex: %i\n%s\n%s\n%s\n" % (manEntry.category, manEntry.index, option, manEntry.argUsage, manEntry.description))
     if i != len(sortedOptions) - 1: outputFile.write(PERSIST_ENTRY_DIVIDER)
   
   outputFile.close()
@@ -418,13 +409,13 @@
     
     # most parameters are overwritten if defined multiple times
     if option in seenOptions and not option in getMultilineParameters():
-      issuesFound.append((lineNumber, VAL_DUPLICATE, option))
+      issuesFound.append((lineNumber, ValidationError.DUPLICATE, option))
       continue
     else: seenOptions.append(option)
     
     # checks if the value isn't necessary due to matching the defaults
     if not option in customOptions:
-      issuesFound.append((lineNumber, VAL_IS_DEFAULT, option))
+      issuesFound.append((lineNumber, ValidationError.IS_DEFAULT, option))
     
     # replace aliases with their recognized representation
     if option in CONFIG["torrc.alias"]:
@@ -459,17 +450,17 @@
       if not isBlankMatch and not val in torValues:
         # converts corrections to reader friedly size values
         displayValues = torValues
-        if valueType == SIZE_VALUE:
+        if valueType == ValueType.SIZE:
           displayValues = [uiTools.getSizeLabel(int(val)) for val in torValues]
-        elif valueType == TIME_VALUE:
+        elif valueType == ValueType.TIME:
           displayValues = [uiTools.getTimeLabel(int(val)) for val in torValues]
         
-        issuesFound.append((lineNumber, VAL_MISMATCH, ", ".join(displayValues)))
+        issuesFound.append((lineNumber, ValidationError.MISMATCH, ", ".join(displayValues)))
   
   # checks if any custom options are missing from the torrc
   for option in customOptions:
     if not option in seenOptions:
-      issuesFound.append((None, VAL_MISSING, option))
+      issuesFound.append((None, ValidationError.MISSING, option))
   
   return issuesFound
 
@@ -485,13 +476,13 @@
   
   if confArg.count(" ") == 1:
     val, unit = confArg.lower().split(" ", 1)
-    if not val.isdigit(): return confArg, UNRECOGNIZED
+    if not val.isdigit(): return confArg, ValueType.UNRECOGNIZED
     mult, multType = _getUnitType(unit)
     
     if mult != None:
       return str(int(val) * mult), multType
   
-  return confArg, UNRECOGNIZED
+  return confArg, ValueType.UNRECOGNIZED
 
 def _getUnitType(unit):
   """
@@ -504,13 +495,13 @@
   
   for label in SIZE_MULT:
     if unit in CONFIG["torrc.label.size." + label]:
-      return SIZE_MULT[label], SIZE_VALUE
+      return SIZE_MULT[label], ValueType.SIZE
   
   for label in TIME_MULT:
     if unit in CONFIG["torrc.label.time." + label]:
-      return TIME_MULT[label], TIME_VALUE
+      return TIME_MULT[label], ValueType.TIME
   
-  return None, UNRECOGNIZED
+  return None, ValueType.UNRECOGNIZED
 
 def _stripComments(contents):
   """

Modified: arm/trunk/src/util/torTools.py
===================================================================
--- arm/trunk/src/util/torTools.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/util/torTools.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -17,12 +17,12 @@
 
 from TorCtl import TorCtl, TorUtil
 
-from util import log, procTools, sysTools, uiTools
+from util import enum, log, procTools, sysTools, uiTools
 
 # enums for tor's controller state:
-# TOR_INIT - attached to a new controller or restart/sighup signal received
-# TOR_CLOSED - control port closed
-TOR_INIT, TOR_CLOSED = range(1, 3)
+# INIT - attached to a new controller or restart/sighup signal received
+# CLOSED - control port closed
+State = enum.Enum("INIT", "CLOSED")
 
 # Addresses of the default directory authorities for tor version 0.2.3.0-alpha
 # (this comes from the dirservers array in src/or/config.c).
@@ -237,7 +237,7 @@
     self._fingerprintsAttachedCache = None # cache of relays we're connected to
     self._nicknameLookupCache = {}      # lookup cache with fingerprint -> nickname mappings
     self._isReset = False               # internal flag for tracking resets
-    self._status = TOR_CLOSED           # current status of the attached control port
+    self._status = State.CLOSED         # current status of the attached control port
     self._statusTime = 0                # unix time-stamp for the duration of the status
     self.lastHeartbeat = 0              # time of the last tor event
     
@@ -298,12 +298,12 @@
       
       self.connLock.release()
       
-      self._status = TOR_INIT
+      self._status = State.INIT
       self._statusTime = time.time()
       
       # notifies listeners that a new controller is available
       if not NO_SPAWN:
-        thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,))
+        thread.start_new_thread(self._notifyStatusListeners, (State.INIT,))
   
   def close(self):
     """
@@ -313,16 +313,29 @@
     self.connLock.acquire()
     if self.conn:
       self.conn.close()
-      self.conn._thread.join()
+      
+      # If we're closing due to an event from TorCtl (for instance, tor was
+      # stopped) then TorCtl is shutting itself down and there's no need to
+      # join on its thread (actually, this *is* the TorCtl thread in that
+      # case so joining on it causes deadlock).
+      # 
+      # This poses a slight possability of shutting down with a live orphaned
+      # thread if Tor is shut down, then arm shuts down before TorCtl has a
+      # chance to terminate. However, I've never seen that occure so leaving
+      # that alone for now.
+      
+      if not threading.currentThread() == self.conn._thread:
+        self.conn._thread.join()
+      
       self.conn = None
       self.connLock.release()
       
-      self._status = TOR_CLOSED
+      self._status = State.CLOSED
       self._statusTime = time.time()
       
       # notifies listeners that the controller's been shut down
       if not NO_SPAWN:
-        thread.start_new_thread(self._notifyStatusListeners, (TOR_CLOSED,))
+        thread.start_new_thread(self._notifyStatusListeners, (State.CLOSED,))
     else: self.connLock.release()
   
   def isAlive(self):
@@ -1067,11 +1080,11 @@
     if event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)"):
       self._isReset = True
       
-      self._status = TOR_INIT
+      self._status = State.INIT
       self._statusTime = time.time()
       
       if not NO_SPAWN:
-        thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,))
+        thread.start_new_thread(self._notifyStatusListeners, (State.INIT,))
   
   def ns_event(self, event):
     self._updateHeartbeat()
@@ -1443,7 +1456,7 @@
         if myPid:
           try:
             if procTools.isProcAvailable():
-              result = float(procTools.getStats(myPid, procTools.STAT_START_TIME)[0])
+              result = float(procTools.getStats(myPid, procTools.Stat.START_TIME)[0])
             else:
               psCall = sysTools.call("ps -p %s -o etime" % myPid)
               
@@ -1497,7 +1510,7 @@
     self._cachedConf = {}
     
     # gives a notice that the control port has closed
-    if eventType == TOR_CLOSED:
+    if eventType == State.CLOSED:
       log.log(CONFIG["log.torCtlPortClosed"], "Tor control port closed")
     
     for callback in self.statusListeners:

Modified: arm/trunk/src/util/uiTools.py
===================================================================
--- arm/trunk/src/util/uiTools.py	2011-02-06 20:25:40 UTC (rev 24186)
+++ arm/trunk/src/util/uiTools.py	2011-02-07 03:53:26 UTC (rev 24187)
@@ -9,7 +9,7 @@
 import curses
 
 from curses.ascii import isprint
-from util import log
+from util import enum, log
 
 # colors curses can handle
 COLOR_LIST = {"red": curses.COLOR_RED,        "green": curses.COLOR_GREEN,
@@ -32,7 +32,7 @@
 TIME_UNITS = [(86400.0, "d", " day"), (3600.0, "h", " hour"),
               (60.0, "m", " minute"), (1.0, "s", " second")]
 
-END_WITH_ELLIPSE, END_WITH_HYPHEN = range(1, 3)
+Ending = enum.Enum("ELLIPSE", "HYPHEN")
 SCROLL_KEYS = (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE, curses.KEY_HOME, curses.KEY_END)
 CONFIG = {"features.colorInterface": True,
           "log.cursesColorSupport": log.INFO}
@@ -117,7 +117,7 @@
   if not COLOR_ATTR_INITIALIZED: _initColors()
   return COLOR_ATTR[color]
 
-def cropStr(msg, size, minWordLen = 4, minCrop = 0, endType = END_WITH_ELLIPSE, getRemainder = False):
+def cropStr(msg, size, minWordLen = 4, minCrop = 0, endType = Ending.ELLIPSE, getRemainder = False):
   """
   Provides the msg constrained to the given length, truncating on word breaks.
   If the last words is long this truncates mid-word with an ellipse. If there
@@ -143,8 +143,8 @@
     minCrop      - minimum characters that must be dropped if a word's cropped
     endType      - type of ending used when truncating:
                    None - blank ending
-                   END_WITH_ELLIPSE - includes an ellipse
-                   END_WITH_HYPHEN - adds hyphen when breaking words
+                   Ending.ELLIPSE - includes an ellipse
+                   Ending.HYPHEN - adds hyphen when breaking words
     getRemainder - returns a tuple instead, with the second part being the
                    cropped portion of the message
   """
@@ -161,8 +161,8 @@
   
   # since we're cropping, the effective space available is less with an
   # ellipse, and cropping words requires an extra space for hyphens
-  if endType == END_WITH_ELLIPSE: size -= 3
-  elif endType == END_WITH_HYPHEN and minWordLen != None: minWordLen += 1
+  if endType == Ending.ELLIPSE: size -= 3
+  elif endType == Ending.HYPHEN and minWordLen != None: minWordLen += 1
   
   # checks if there isn't the minimum space needed to include anything
   lastWordbreak = msg.rfind(" ", 0, size + 1)
@@ -181,7 +181,7 @@
   
   if includeCrop:
     returnMsg, remainder = msg[:size], msg[size:]
-    if endType == END_WITH_HYPHEN:
+    if endType == Ending.HYPHEN:
       remainder = returnMsg[-1] + remainder
       returnMsg = returnMsg[:-1] + "-"
   else: returnMsg, remainder = msg[:lastWordbreak], msg[lastWordbreak:]
@@ -189,7 +189,7 @@
   # 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 == END_WITH_ELLIPSE: returnMsg += "..."
+  if endType == Ending.ELLIPSE: returnMsg += "..."
   
   if getRemainder: return (returnMsg, remainder)
   else: return returnMsg