[tor-commits] r24555: {arm} Arm version 1.4.2 release. (in arm/release: . src src/interface src/interface/connections src/interface/graphing src/util)

Damian Johnson atagar1 at gmail.com
Mon Apr 4 15:22:31 UTC 2011


Author: atagar
Date: 2011-04-04 15:22:31 +0000 (Mon, 04 Apr 2011)
New Revision: 24555

Added:
   arm/release/src/interface/connections/
   arm/release/src/interface/connections/__init__.py
   arm/release/src/interface/connections/circEntry.py
   arm/release/src/interface/connections/connEntry.py
   arm/release/src/interface/connections/connPanel.py
   arm/release/src/interface/connections/entries.py
   arm/release/src/util/enum.py
Removed:
   arm/release/TODO
   arm/release/src/interface/connections/__init__.py
   arm/release/src/interface/connections/circEntry.py
   arm/release/src/interface/connections/connEntry.py
   arm/release/src/interface/connections/connPanel.py
   arm/release/src/interface/connections/entries.py
Modified:
   arm/release/
   arm/release/ChangeLog
   arm/release/README
   arm/release/armrc.sample
   arm/release/src/interface/configPanel.py
   arm/release/src/interface/connPanel.py
   arm/release/src/interface/controller.py
   arm/release/src/interface/descriptorPopup.py
   arm/release/src/interface/graphing/__init__.py
   arm/release/src/interface/graphing/bandwidthStats.py
   arm/release/src/interface/graphing/connStats.py
   arm/release/src/interface/graphing/graphPanel.py
   arm/release/src/interface/headerPanel.py
   arm/release/src/interface/logPanel.py
   arm/release/src/interface/torrcPanel.py
   arm/release/src/settings.cfg
   arm/release/src/starter.py
   arm/release/src/test.py
   arm/release/src/util/__init__.py
   arm/release/src/util/conf.py
   arm/release/src/util/connections.py
   arm/release/src/util/log.py
   arm/release/src/util/panel.py
   arm/release/src/util/procTools.py
   arm/release/src/util/sysTools.py
   arm/release/src/util/torConfig.py
   arm/release/src/util/torTools.py
   arm/release/src/util/uiTools.py
   arm/release/src/version.py
Log:
Arm version 1.4.2 release.




Property changes on: arm/release
___________________________________________________________________
Modified: svn:mergeinfo
   - /arm/trunk:22227-24074
   + /arm/trunk:22227-24554

Modified: arm/release/ChangeLog
===================================================================
--- arm/release/ChangeLog	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/ChangeLog	2011-04-04 15:22:31 UTC (rev 24555)
@@ -1,5 +1,53 @@
 CHANGE LOG
 
+4/4/11 - version 1.4.2
+This release chiefly consists of a fully reimplemented connection panel. Besides being a sane, maintainable implementation this includes numerous new features and improvements like full circuit paths, applications involved for local connections, and better type identification.
+
+    * added: full rewrite of the connection panel, providing:
+          o listing the full paths involved in active circuits
+          o identification of socks, hidden service, and controller applications (arm, vidalia, polipo, etc)
+          o identification of exit connections with the common usage for the port they're using
+          o display of the local -> internal -> external address when room is available (original patch by Fabian Keil)
+          o better accuracy and performance in identifying client and directory connections
+          o marking the uptimes for initial connections (arm only tracks connection uptimes since starting, so these entries are just minimum durations)
+          o lazily loading the initial IP -> fingerprint mappings to improve the startup time
+          o using the circuit-status to disambiguating multiple relays on the same IP address
+          o smarter space utilization, filling in smaller columns if there isn't room for higher priority but larger entries
+          o connection details popup changes:
+                + using the consensus exit policies rather than the longer descriptor versions when available
+                + displaying connection details no longer freezes the rest of the display
+                + detail panel uses the full screen width and is dynamically resizable
+                + more resilient to missing descriptors
+    * change: hiding most tor config values by default (idea by arma)
+    * change: dropping warning suggesting that users set the FetchUselessDescriptors option (suggestion by Sebastian and others)
+    * change: always starting the bandwidth field from zero rather than using the state file total, which only contains the last day's worth of data (thanks to guilhem)
+    * change: suggesting authentication and giving steps for it in the readme (suggestion by Sebastian)
+    * change: caching config display lines, which reduces the CPU usage when scrolling by around 40%
+    * change: added summaries for the remaining tor configuration options
+    * change: using a dedicated enum class rather than tuple sets
+    * fix: torrc validation requires 'GETINFO config-text' which was introduced in Tor verison 0.2.2.7 (caught by Sjon, talion, and torland, https://trac.torproject.org/projects/tor/ticket/2501)
+    * fix: off-by-one issue with the displayed line numbers for torrc errors (caught by Sjon)
+    * fix: bin function wasn't available before python 2.6 (caught by Paul Menzel)
+    * fix: mis-parsing family entries when there's no entry after the comma (caught by StrangeCharm, https://trac.torproject.org/projects/tor/ticket/2414)
+    * fix: preventing SOCKS and CONTROL connections from being expanded (patch by Fabian Keil)
+    * fix: disabling name resolution for application queries to avoid leaking to resolvers (patch by Fabian Keil)
+    * fix: reversing src and dst addresses of SOCKS and CONTROL connections (caught by Fabian Keil)
+    * fix: changing the 'APPLICATION' type to 'SOCKS' since the previous label was too long (caught by Fabian Keil)
+    * fix: crashing issue from unknown relay nicknames (caught by krkhan)
+    * fix: concurrency bug occasionally causing "syshook" stacktraces when shutting down
+    * fix: header panel displayed the wrong IP address if it changed since we first started (https://trac.torproject.org/projects/tor/ticket/2776)
+    * fix: unchecked OSError could cause us to crash when making directories (for instance if there was a permissions issue)
+    * fix: the availability check for bsd resolvers was broken, probably causing resolution to fail for a few seconds on that platform
+    * fix: dropping the pointless 'Log notice stdout' entry provided by config-text queries (https://trac.torproject.org/projects/tor/ticket/2362)
+    * fix: taking DirServer and AlternateDirAuthority into account when determining the directory authorities
+    * fix: consuming a little extra space in the connection panel when scrollbars aren't visible
+    * fix: dropping the deprecated 'features.config.descriptions.persistPath' config option
+    * fix: failed connection attempts to the control port were generating zombie connections (https://trac.torproject.org/projects/tor/ticket/2812)
+    * fix: concurrency bug in joining on the TorCtl thread when tor shut down
+    * fix: the 'startup.dataDirectory' config option was being ignored
+    * fix: recognizing the proper private ip ranges of the 172.* block
+    * fix: missing 'is default' option from config sort ordering
+
 1/7/11 - version 1.4.1 (r24054)
 Platform specific enhancements including BSD compatibility and vastly improved performance on Linux.
 

Modified: arm/release/README
===================================================================
--- arm/release/README	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/README	2011-04-04 15:22:31 UTC (rev 24555)
@@ -25,6 +25,22 @@
   ... starting Tor with '--controlport <PORT>'
   ... or including 'ControlPort <PORT>' in your torrc
 
+It's also highly suggested for the control port to require authentication.
+This can be done either with a cookie or password:
+  * Cookie Authentication - Controllers authenticate to Tor by providing the
+    contents of the control_auth_cookie file. To set this up...
+    - add "CookieAuthentication 1" to your torrc
+    - either restart Tor or run "pkill -sighup tor"
+    - this method of authentication is automatically handled by arm, so you
+      can still start arm as you normally would
+  
+  * Password Authentication - Attaching to the control port requires a
+    password. To set this up...
+    - run "tor --hash-password <your password>"
+    - add "HashedControlPassword <hashed password>" to your torrc
+    - either restart Tor or run "pkill -sighup tor"
+    - when starting up arm will prompt you for this password
+
 For full functionality this also needs:
 - To be ran with the same user as tor to avoid permission issues with
   connection resolution and reading the torrc.
@@ -111,7 +127,6 @@
   ChangeLog    - revision history
   LICENSE      - copy of the gpl v3
   README       - um... guess you figured this one out
-  TODO         - known issues, future plans, etc
   setup.py     - distutils installation script for arm
   
   src/
@@ -125,6 +140,13 @@
     uninstall         - removal script
     
     interface/
+      connections/
+        __init__.py
+        connPanel.py      - (page 2) lists the active tor connections
+        circEntry.py      - circuit entries in the connection panel
+        connEntry.py      - individual connections to or from the system
+        entries.py        - common parent for connPanel display entries
+      
       graphing/
         __init__.py
         graphPanel.py     - (page 1) presents graphs for data instances
@@ -139,7 +161,7 @@
       logPanel.py            - (page 1) displays tor, arm, and torctl events
       fileDescriptorPopup.py - (popup) displays file descriptors used by tor
       
-      connPanel.py           - (page 2) displays information on tor connections
+      connPanel.py           - (page 2) deprecated counterpart for connections/*
       descriptorPopup.py     - (popup) displays connection descriptor data
       
       configPanel.py         - (page 3) editor panel for the tor configuration
@@ -149,6 +171,7 @@
       __init__.py
       conf.py        - loading and persistence for user configuration
       connections.py - service providing periodic connection lookups
+      enum.py        - enumerations for ordered collections
       hostnames.py   - service providing nonblocking reverse dns lookups
       log.py         - aggregator for application events
       panel.py       - wrapper for safely working with curses subwindows

Deleted: arm/release/TODO
===================================================================
--- arm/release/TODO	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/TODO	2011-04-04 15:22:31 UTC (rev 24555)
@@ -1,281 +0,0 @@
-TODO
-
-- Roadmap and completed work for next release (1.4.2)
-  [ ] refactor panels
-      Currently the interface is a bit of a rat's nest (especially the
-      controller). The goal is to use better modularization to both simplify
-      the codebase and make it possible to use smarter caching to improve
-      performance (far too much is done in the ui logic). This work is in
-      progress - /init and /util are done and /interface is partly done. Known
-      bugs are being fixed while refactoring.
-      
-      * conn panel
-        - expand client connections and note location in circuit (entry-exit)
-        - for clients give an option to list all connections, to tell which are
-          going through tor and which might be leaking
-        - check family members to see if they're alive (VERSION cell
-          handshake?)
-        - fallback when pid or connection querying via pid is unavailable
-          List all connections listed both by netstat and the consensus
-        - note when connection times are estimates (color?), ie connection
-          was established before arm
-        - connection uptime to associate inbound/outbound connections?
-        - identify controller connections (if it's arm, vidalia, etc) with
-          special detail page for them
-        - provide bridge / client country / exiting port statistics
-          Include bridge related data via GETINFO option (feature request
-          by waltman and ioerror).
-        - note the common port usage along with the exit statistics
-        - show the port used in scrubbed exit connections
-        - pick apart applications like iftop and pktstat to see how they get
-          per-connection bandwidth usage. Forum thread discussing it:
-          https://bbs.archlinux.org/viewtopic.php?pid=715906
-        - include an option to show both the internal and external ips for the
-          local connection, ie:
-          myInternal --> myExternal --> foreign
-          idea and initial patch by Fabian Keil
-        - give a warning if family relays don't name us
-      * classify config options as useful (defaultly shown), standard, and
-        deprecated (configured to be hidden by default)
-      * check tor source for deprecated options like 'group' (are they
-        ignored? idea is thanks to NightMonkey)
-      * elaborate on the password prompt (suggestion by weasel)
-  * release prep
-    * pylint --indent-string="  " --disable=C,R interface/foo.py | less
-    * double check __init__.py and README for added or removed files
-    * wait a week, then bump package versions
-      * Debian
-        Contact: weasel (Peter Palfrader)
-        Initial Release: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=603056
-        Update Instructions:
-          * TBD
-      
-      * Gentoo
-        Contact: NightMonkey (Jesse Adelman)
-        Initial Release: https://bugs.gentoo.org/show_bug.cgi?id=341731
-        Update Instructions:
-          * go to https://bugs.gentoo.org
-          * make a generic bug with "net-misc/arm-X.X.X version bump, please"
-      
-      * ArchLinux
-        Contact: Spider.007
-        Initial Release: http://aur.archlinux.org/packages.php?ID=44172
-        Update Instructions:
-          * go to aur.archlinux.org
-          * select "Out-of-date" for the package
-
-- Roadmap for version 1.4.3
-  [ ] refactor panels
-      [ ] controller and popup panels
-      [ ] attempt to clear controller password from memory
-        - http://www.codexon.com/posts/clearing-passwords-in-memory-with-python
-  * release prep
-    * pylint --indent-string="  " --disable=C,R interface/foo.py | less
-    * double check __init__.py and README for changes
-
-- Roadmap for version 1.3.9
-  [ ] refactor panels
-      [ ] controller and popup panels
-        - allow arm to resume after restarting tor
-            This requires a full move to the torTools controller.
-        - improve on performance bottlenecks for startup time and cpu usage
-        - intermittent concurrency bugs during shutdown, one possible source:
-          https://trac.torproject.org/projects/tor/ticket/2144
-  [ ] setup scripts for arm
-      [ ] updater (checks for a new tarball and installs it automatically)
-        - attempt to verify download signature, providing a warning if unable
-          to do so
-      [ ] look into CAPs to get around permission issues for connection
-          listing sudo wrapper for arm to help arm run as the same user as
-          tor? Irc suggestions:
-            - man capabilities
-            - http://www.linuxjournal.com/article/5737
-
-- Bugs
-  * The default resolver isn't configurable.
-  * 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.
-  * The arm header panel doesn't properly reflect when the ip address
-    changes. This provides a notice event saying:
-    "Our IP Address has changed from X to Y; rebuilding descriptor (source Z)."
-  * The cpu usage spikes for scrollable content when the key's held. Try
-    coalescing the events.
-  * The manpage layout is system dependent, so the scraper needs to be more
-    resilient against being confused by whitespace. Another improvement is
-    including fallback results if the man page can't be parsed (suggested by
-    rransom, issue caught by NightMonkey).
-  * Log deduplication is currently an n^2 operation. Hence it can't handle
-    large logs (for instance, when at the DEBUG runlevel). Currently we're
-    timing out the function if it takes too long, but a more efficient method
-    for deduplication would be preferable.
-  * when in client mode and tor stops the header panel doesn't say so
-  * util/torTools.py: effective bandwidth rate/burst measurements don't take
-      SETCONF into consideration, blocked on:
-      https://trac.torproject.org/projects/tor/ticket/1692
-  * log prepopulation fails to limit entries to the current tor instance if
-      the file isn't logged to at the NOTICE level. A fix is to use the
-      timestamps to see if it belongs to this tor instance. This requires
-      tor's uptime - blocked on implementation of the following proposal:
-      https://gitweb.torproject.org/tor.git/blob/HEAD:/doc/spec/proposals/173-getinfo-option-expansion.txt
-  * the STATUS_SERVER event may not be supported
-      18:52 < mikeperry> atagar: I believe there is no event parsing for STATUS_SERVER
-      18:53 < mikeperry> atagar: see TorCtl.EventSink and classes that inherit from it
-      18:54 < mikeperry> specifically, TorCtl.EventHandler._decode1, _handle1, and _map1
-  
-  * conn panel:
-    * *never* do reverse dns lookups for first hops (could be resolving via
-      tor and hence leaking to the exit)
-    * If there's duplicate family entries (and harder case: both nickname and
-      fingerprint entries for the same relay) then the duplicate should be
-      removed. This is also causing a bad scrolling bug where the cursor can't
-      get past the pair of duplicate entries.
-    * revise multikey sort of connections
-        Currently using a pretty ugly hack. Look at:
-        http://www.velocityreviews.com/forums/
-          t356461-sorting-a-list-of-objects-by-multiple-attributes.html
-        and check for performance difference.
-    * replace checks against exit policy with Mike's torctl version
-        My version still isn't handling all inputs anyway (still need to handle
-        masks, private keyword, and prepended policy). Parse it from the rest
-        of the router if too heavy ("TorCtl.Router.will_exit_to instead").
-    * avoid hostname lookups of private connections
-        Stripped most of them but suspect there might be others (have assertions
-        check for this in a debug mode?)
-    * connection uptimes shouldn't show fractions of a second
-    * connections aren't cleared when control port closes
-
-- Packaging
-  * OpenWrt - OpenWrt uses the opkg packaging format which could make use of
-    arm's current deb packages. Packaging for this platform would help with
-    the Torouter project:
-    https://trac.torproject.org/projects/tor/wiki/TheOnionRouter/Torouter
-  * Mac - Couple of options include macport and dmg...
-    * macport (http://guide.macports.org/#development)
-      Build-from-source distribution method (like BSD portinstall). This has
-      been suggested by several people.
-      
-    * dmg (http://en.wikipedia.org/wiki/Apple_Disk_Image)
-      Most conventional method of software distribution on mac. This is just
-      a container (no updating/removal support), but could contain an icon
-      for the dock that starts a terminal with arm. This might include a pkg
-      installer.
-    
-    * mpkg (http://pypi.python.org/pypi/bdist_mpkg/)
-      Plugin for distutils. Like most mac packaging, this can only run on a
-      mac. It also requires setuptools:
-      http://www.errorhelp.com/search/details/74034/importerror-no-module-named-setuptools
-
-- Future Features
-  * client mode use cases
-    * not sure what sort of information would be useful in the header (to
-      replace the orport, fingerprint, flags, etc)
-      * one idea by velope:
-        "whether you configured a dnsport, transport, etc. and whether they
-        were successfully opened. might be nice to know this after the log
-        messages might be gone."
-        [notice] Opening Socks listener on 127.0.0.1:9050
-        [notice] Opening Transparent pf/netfilter listener on 127.0.0.1:9040
-        [notice] Opening DNS listener on 127.0.0.1:53
-    * rdns and whois lookups (to find ISP, country, and jurisdiction, etc)
-      To avoid disclosing connection data to third parties this needs to be
-      an all-or-nothing operation (ie, needs to fetch information on all
-      relays or none of them). Plan is something like:
-        * add resolving/caching capabilities to fetch information on all relays
-          and distil whois entries to just what we care about (hosting provider
-          or ISP), by default updating the cache on a daily basis
-        * construct tarball and make this available for download rather than
-          fetching everything at each client
-        * possibly make these archives downloadable from peer relays (this is a
-          no-go for clients) via torrents or some dirport like scheme
-    * look at Vidalia and TorK for ideas
-    * need to solicit for ideas on what would be most helpful to clients
-    * dialog with bridge statuses (idea by mikeperry)
-      https://trac.vidalia-project.net/ticket/570
-      https://trac.torproject.org/projects/tor/ticket/2068
-  * menus
-    * http://gnosis.cx/publish/programming/charming_python_6.html ?
-    * additional options:
-      * make update rates configurable via the ui
-      * dialog with flag descriptions and other help
-      * menu with all torrc options (making them editable/toggleable)
-  * control port interpreter (interactive prompt)
-    Panel and startup option (-t maybe?) for providing raw control port
-    access along with usability improvements (piggybacking on the arm
-    connection):
-    * irc like help (ex "/help GETINFO" could provide a summary of
-      getinfo commands, partly using the results from
-      "GETINFO info/names")
-    * tab completion and up/down for previous commands
-    * warn and get confirmation if command would disrupt arm (for
-      instance 'SETEVENTS')
-    * 'safe' option that restricts to read-only access (start with this)
-    * issue sighup reset
-  * make use of the new process/* GETINFO options
-    They'll be available in the next tor release, as per:
-    https://trac.torproject.org/projects/tor/ticket/2291
-  * feature parity for arm's config values (armrc entries)
-    * editability
-    * parse descriptions from the man page? autogeneration of the man page from
-      something storing the descriptions
-  * handle mutiple tor instances
-    * screen style (dialog for switching between instances)
-    * extra window with whatever stats can be aggregated over all instances,
-      or a config option to aggregate stats for bw, resource usage, etc
-  * option to save the current settings to the config
-    * provide warning at startup if the armrc doesn't exist, with instructions
-      for generating it
-  * email alerts for changes to the relay's status, similar to tor-weather
-    * simple alert if tor shuts down
-    * accounting and alerts for if the bandwidth drops to zero
-    * daily/weekly/etc alerts for basic status (log output, bandwidth history,
-        etc), borrowing from the consensus tracker for some of the formatting
-  * tab completion for input fields that expect a filesystem path
-  * look through vidalia's tickets for more ideas
-    https://trac.vidalia-project.net/
-  * look into additions to the used apis
-    * curses (python 2.6 extended?): http://docs.python.org/library/curses.html
-    * new control options (like "desc-annotations/id/<OR identity>")?
-  * look into better supporting hidden services (what could be useful here?)
-  * provide option for a consensus page
-    Shows full consensus with an interface similar to the connection panel.
-    For this Mike's ConsensusTracker would be helpful (though boost the
-    startup time by several seconds)
-  * show qos stats
-    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
-  * 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?
-    Another alternative would be that when arm is started and tor isn't
-    running offer to start tor as a client, relay, or bridge. (idea by ioerror)
-  * audit what tor does
-    * Provide warnings if tor connections misbehaves, for instance:
-      * ensuring ExitPolicyRejectPrivate is being obeyed
-      * check that ExitPolicy violations don't occur (not possible yet since
-        not all relays aren't identified)
-      * check that all connections are properly related to a circuit, for
-        instance no outbound connections without a corresponding inbound (not
-        possible yet due to being unable to correlate connections to circuits)
-    * check file descriptors being accessed by tor to see if they're outside a
-        known pattern
-  * script that dumps relay stats to stdout
-    Derived from an idea by StrangeCharm. Django has a small terminal coloring
-    module that could be nice for formatting. Could possibly include:
-      * desc / ns information for our relay
-      * ps / netstat stats like load, uptime, and connection counts, etc
-  * implement control-spec proposals:
-    * https://gitweb.torproject.org/tor.git/blob/HEAD:/doc/spec/proposals/172-circ-getinfo-option.txt
-    * https://gitweb.torproject.org/tor.git/blob/HEAD:/doc/spec/proposals/173-getinfo-option-expansion.txt
-  * gui frontend (gtk?)
-    Look into if the arm utilities and codebase would fit nicely for a gui
-    controller like Vidalia and TorK.
-  * unit tests
-    Primarily for util, for instance 'addfstr' would be a good candidate.
-  * python 3 compatibility
-    Currently blocked on TorCtl support.
-

Modified: arm/release/armrc.sample
===================================================================
--- arm/release/armrc.sample	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/armrc.sample	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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,12 +86,11 @@
 # 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 7, 1, 8
 features.config.selectionDetails.height 6
 features.config.prepopulateEditValues true
 features.config.state.colWidth.option 25
-features.config.state.colWidth.value 10
+features.config.state.colWidth.value 15
 features.config.state.showPrivateOptions false
 features.config.state.showVirtualOptions false
 features.config.file.showScrollbars true
@@ -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
 # ------------------------
@@ -139,6 +136,9 @@
 # prepopulate
 #   attempts to use tor's state file to prepopulate the bandwidth graph at the
 #   15-minute interval (this requires the minimum of a day's worth of uptime)
+# prepopulateTotal
+#   populates the total stat from the state file if true (this only contains
+#   the last day's worth of information, so this metric isn't the true total)
 # transferInBystes
 #   shows rate measurments in bytes if true, bits otherwise
 # accounting.show
@@ -149,11 +149,57 @@
 #   provides verbose measurements of time if true
 
 features.graph.bw.prepopulate true
+features.graph.bw.prepopulateTotal false
 features.graph.bw.transferInBytes false
 features.graph.bw.accounting.show true
 features.graph.bw.accounting.rate 10
 features.graph.bw.accounting.isTimeLong false
 
+# Parameters for connection display
+# ---------------------------------
+# oldPanel
+#   includes the old connection panel in the interface
+# newPanel
+#   includes the new connection panel in the interface
+# listingType
+#   the primary category of information shown by default, options including:
+#   0 -> IP Address / Port              1 -> Hostname
+#   2 -> Fingerprint                    3 -> Nickname
+# order
+#   three comma separated configuration attributes, options including:
+#   0 -> Category,  1 -> Uptime,        2 -> Listing,     3 -> IP Address,
+#   4 -> Port,      5 -> Hostname,      6 -> Fingerprint, 7 -> Nickname,
+#   8 -> Country
+# refreshRate
+#   rate at which the connection panel contents is redrawn (if higher than the
+#   connection resolution rate then reducing this won't casue new data to
+#   appear more frequently - just increase the rate at which the uptime field
+#   is updated)
+# resolveApps
+#   issues lsof queries to determining the applications involved in local
+#   SOCKS and CONTROL connections
+# markInitialConnections
+#   if true, the uptime of the initial connections when we start are marked
+#   with a '+' (these uptimes are estimates since arm can only track a
+#   connection's duration while it runs)
+# showExitPort
+#   shows port related information of exit connections we relay if true
+# showColumn.*
+#   toggles the visability of the connection table columns
+
+features.connection.oldPanel false
+features.connection.newPanel true
+features.connection.listingType 0
+features.connection.order 0, 2, 1
+features.connection.refreshRate 5
+features.connection.resolveApps true
+features.connection.markInitialConnections true
+features.connection.showExitPort true
+features.connection.showColumn.fingerprint true
+features.connection.showColumn.nickname true
+features.connection.showColumn.destination true
+features.connection.showColumn.expandedIp true
+
 # Thread pool size for hostname resolutions
 # Determines the maximum number of concurrent requests. Upping this to around
 # thirty or so seems to be problematic, causing intermittently seizing.
@@ -187,6 +233,7 @@
 log.torGetInfo DEBUG
 log.torGetInfoCache NONE
 log.torGetConf DEBUG
+log.torGetConfCache NONE
 log.torSetConf INFO
 log.torEventTypeUnrecognized NOTICE
 log.torPrefixPathInvalid NOTICE
@@ -224,6 +271,7 @@
 log.cursesColorSupport INFO
 log.bsdJailFound INFO
 log.unknownBsdJailId WARN
+log.geoipUnavailable WARN
 log.stats.failedProcResolution DEBUG
 log.stats.procResolutionFailover INFO
 log.stats.failedPsResolution INFO

Modified: arm/release/src/interface/configPanel.py
===================================================================
--- arm/release/src/interface/configPanel.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/configPanel.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -6,65 +6,76 @@
 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,
                   "features.config.state.showVirtualOptions": False,
                   "features.config.state.colWidth.option": 25,
-                  "features.config.state.colWidth.value": 10}
+                  "features.config.state.colWidth.value": 15}
 
 # 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.MAN_ENTRY, Field.OPTION, 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():
   """
   Configuration option in the panel.
   """
   
-  def __init__(self, option, type, isDefault, summary, manEntry):
+  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.
+    summary = torConfig.getConfigSummary(option)
+    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] = ""
     
-    self.fields[FIELD_SUMMARY] = summary if summary != None else 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]
+    
+    # cache of what's displayed for this configuration option
+    self.labelCache = None
+    self.labelCacheArgs = None
   
   def get(self, field):
     """
@@ -74,9 +85,44 @@
       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):
+    """
+    Provides back a list with the given field values.
+    
+    Arguments:
+      field - enums for the fields to be provided back
+    """
+    
+    return [self.get(field) for field in fields]
+  
+  def getLabel(self, optionWidth, valueWidth, summaryWidth):
+    """
+    Provides display string of the configuration entry with the given
+    constraints on the width of the contents.
+    
+    Arguments:
+      optionWidth  - width of the option column
+      valueWidth   - width of the value column
+      summaryWidth - width of the summary column
+    """
+    
+    # Fetching the display entries is very common so this caches the values.
+    # Doing this substantially drops cpu usage when scrolling (by around 40%).
+    
+    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)
+      lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, summaryWidth)
+      self.labelCache = lineTextLayout % (optionLabel, valueLabel, summaryLabel)
+      self.labelCacheArgs = argSet
+    
+    return self.labelCache
+  
   def _getValue(self):
     """
     Provides the current value of the configuration entry, taking advantage of
@@ -84,28 +130,18 @@
     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
-  
-  def getAttr(self, argTypes):
-    """
-    Provides back a list with the given parameters.
-    
-    Arguments:
-      argTypes - list of enums for the arguments to be provided back
-    """
-    
-    return [self.get(field) for field in argTypes]
 
 class ConfigPanel(panel.Panel):
   """
@@ -124,14 +160,22 @@
         "features.config.state.colWidth.option": 5,
         "features.config.state.colWidth.value": 5})
       
-      self.sortOrdering = config.getIntCSV("features.config.order", self.sortOrdering, 3, 0, 6)
+      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 = []
     self.scroller = uiTools.Scroller(True)
     self.valsLock = threading.RLock()
     
-    if self.configType == TOR_STATE:
+    # shows all configuration options if true, otherwise only the ones with
+    # the 'important' flag are shown
+    self.showAll = False
+    
+    if self.configType == State.TOR:
       conn = torTools.getConn()
       customOptions = torConfig.getCustomOptions()
       configOptionLines = conn.getInfo("config/names", "").strip().split("\n")
@@ -141,29 +185,37 @@
         # UseEntryGuards Boolean
         confOption, confType = line.strip().split(" ", 1)
         
-        # skips private and virtual entries if not set to show them
+        # skips private and virtual entries if not configured to show them
         if not self._config["features.config.state.showPrivateOptions"] and confOption.startswith("__"):
           continue
         elif not self._config["features.config.state.showVirtualOptions"] and confType == "Virtual":
           continue
         
-        summary = torConfig.getConfigSummary(confOption)
-        manEntry = torConfig.getConfigDescription(confOption)
-        self.confContents.append(ConfigEntry(confOption, confType, not confOption in customOptions, summary, manEntry))
-      
-      self.setSortOrder() # initial sorting of the contents
-    elif self.configType == ARM_STATE:
+        self.confContents.append(ConfigEntry(confOption, confType, not confOption in customOptions))
+    elif self.configType == State.ARM:
       # loaded via the conf utility
       armConf = conf.getConfig("arm")
       for key in armConf.getKeys():
         pass # TODO: implement
+    
+    # mirror listing with only the important configuration options
+    self.confImportantContents = []
+    for entry in self.confContents:
+      if torConfig.isImportant(entry.get(Field.OPTION)):
+        self.confImportantContents.append(entry)
+    
+    # if there aren't any important options then show everything
+    if not self.confImportantContents:
+      self.confImportantContents = self.confContents
+    
+    self.setSortOrder() # initial sorting of the contents
   
   def getSelection(self):
     """
     Provides the currently selected entry.
     """
     
-    return self.scroller.getCursorSelection(self.confContents)
+    return self.scroller.getCursorSelection(self._getConfigOptions())
   
   def setSortOrder(self, ordering = None):
     """
@@ -177,7 +229,8 @@
     
     self.valsLock.acquire()
     if ordering: self.sortOrdering = ordering
-    self.confContents.sort(key=lambda i: (i.getAttr(self.sortOrdering)))
+    self.confContents.sort(key=lambda i: (i.getAll(self.sortOrdering)))
+    self.confImportantContents.sort(key=lambda i: (i.getAll(self.sortOrdering)))
     self.valsLock.release()
   
   def handleKey(self, key):
@@ -188,107 +241,104 @@
       if detailPanelHeight > 0 and detailPanelHeight + 2 <= pageHeight:
         pageHeight -= (detailPanelHeight + 1)
       
-      isChanged = self.scroller.handleKey(key, self.confContents, pageHeight)
+      isChanged = self.scroller.handleKey(key, self._getConfigOptions(), pageHeight)
       if isChanged: self.redraw(True)
+    elif key == ord('a') or key == ord('A'):
+      self.showAll = not self.showAll
+      self.redraw(True)
     self.valsLock.release()
   
-  def draw(self, subwindow, width, height):
+  def draw(self, width, height):
     self.valsLock.acquire()
     
     # draws the top label
-    titleLabel = "%s Configuration:" % ("Tor" if self.configType == TOR_STATE else "Arm")
-    self.addstr(0, 0, titleLabel, curses.A_STANDOUT)
+    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"
     
     # panel with details for the current selection
     detailPanelHeight = self._config["features.config.selectionDetails.height"]
+    isScrollbarVisible = False
     if detailPanelHeight == 0 or detailPanelHeight + 2 >= height:
       # no detail panel
       detailPanelHeight = 0
-      scrollLoc = self.scroller.getScrollLoc(self.confContents, height - 1)
+      scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1)
       cursorSelection = self.getSelection()
+      isScrollbarVisible = len(self._getConfigOptions()) > height - 1
     else:
       # Shrink detail panel if there isn't sufficient room for the whole
       # thing. The extra line is for the bottom border.
       detailPanelHeight = min(height - 1, detailPanelHeight + 1)
-      scrollLoc = self.scroller.getScrollLoc(self.confContents, height - 1 - detailPanelHeight)
+      scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1 - detailPanelHeight)
       cursorSelection = self.getSelection()
+      isScrollbarVisible = len(self._getConfigOptions()) > height - detailPanelHeight - 1
       
-      self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, titleLabel)
+      self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, isScrollbarVisible)
     
+    titleLabel = "%s Configuration (%s):" % (configType, hiddenMsg)
+    self.addstr(0, 0, titleLabel, curses.A_STANDOUT)
+    
     # draws left-hand scroll bar if content's longer than the height
-    scrollOffset = 0
-    if len(self.confContents) > height - detailPanelHeight - 1:
+    scrollOffset = 1
+    if isScrollbarVisible:
       scrollOffset = 3
-      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self.confContents), 1 + detailPanelHeight)
+      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self._getConfigOptions()), 1 + detailPanelHeight)
     
     optionWidth = self._config["features.config.state.colWidth.option"]
     valueWidth = self._config["features.config.state.colWidth.value"]
     descriptionWidth = max(0, width - scrollOffset - optionWidth - valueWidth - 2)
     
-    for lineNum in range(scrollLoc, len(self.confContents)):
-      entry = self.confContents[lineNum]
+    for lineNum in range(scrollLoc, len(self._getConfigOptions())):
+      entry = self._getConfigOptions()[lineNum]
       drawLine = lineNum + detailPanelHeight + 1 - scrollLoc
       
-      optionLabel = uiTools.cropStr(entry.get(FIELD_OPTION), optionWidth)
-      valueLabel = uiTools.cropStr(entry.get(FIELD_VALUE), valueWidth)
-      summaryLabel = uiTools.cropStr(entry.get(FIELD_SUMMARY), descriptionWidth, None)
-      
-      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
       
-      lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, descriptionWidth)
-      lineText = lineTextLayout % (optionLabel, valueLabel, summaryLabel)
+      lineText = entry.getLabel(optionWidth, valueWidth, descriptionWidth)
       self.addstr(drawLine, scrollOffset, lineText, lineFormat)
       
       if drawLine >= height: break
     
     self.valsLock.release()
   
-  def _drawSelectionPanel(self, cursorSelection, width, detailPanelHeight, titleLabel):
+  def _getConfigOptions(self):
+    return self.confContents if self.showAll else self.confImportantContents
+  
+  def _drawSelectionPanel(self, selection, width, detailPanelHeight, isScrollbarVisible):
     """
     Renders a panel for the selected configuration option.
     """
     
-    # border (top)
-    if width >= len(titleLabel):
-      self.win.hline(0, len(titleLabel), curses.ACS_HLINE, width - len(titleLabel))
-      self.win.addch(0, width, curses.ACS_URCORNER)
+    # This is a solid border unless the scrollbar is visible, in which case a
+    # 'T' pipe connects the border to the bar.
+    uiTools.drawBox(self, 0, 0, width, detailPanelHeight + 1)
+    if isScrollbarVisible: self.addch(detailPanelHeight, 1, curses.ACS_TTEE)
     
-    # border (sides)
-    self.win.vline(1, 0, curses.ACS_VLINE, detailPanelHeight - 1)
-    self.win.vline(1, width, curses.ACS_VLINE, detailPanelHeight - 1)
+    selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[selection.get(Field.CATEGORY)])
     
-    # border (bottom)
-    self.win.addch(detailPanelHeight, 0, curses.ACS_LLCORNER)
-    if width >= 2: self.win.addch(detailPanelHeight, 1, curses.ACS_TTEE)
-    if width >= 3: self.win.hline(detailPanelHeight, 2, curses.ACS_HLINE, width - 2)
-    self.win.addch(detailPanelHeight, width, curses.ACS_LRCORNER)
-    
-    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)" % selection.get(Field.CATEGORY)
+    self.addstr(1, 2, selection.get(Field.OPTION) + optionLabel, selectionFormat)
     
     # second entry:
     # Value: <value> ([default|custom], <type>, usage: <argument usage>)
     if detailPanelHeight >= 3:
       valueAttr = []
-      valueAttr.append("default" if cursorSelection.get(FIELD_IS_DEFAULT) else "custom")
-      valueAttr.append(cursorSelection.get(FIELD_TYPE))
-      valueAttr.append("usage: %s" % (cursorSelection.get(FIELD_ARG_USAGE)))
+      valueAttr.append("default" if selection.get(Field.IS_DEFAULT) else "custom")
+      valueAttr.append(selection.get(Field.TYPE))
+      valueAttr.append("usage: %s" % (selection.get(Field.ARG_USAGE)))
       valueAttrLabel = ", ".join(valueAttr)
       
       valueLabelWidth = width - 12 - len(valueAttrLabel)
-      valueLabel = uiTools.cropStr(cursorSelection.get(FIELD_VALUE), valueLabelWidth)
+      valueLabel = uiTools.cropStr(selection.get(Field.VALUE), valueLabelWidth)
       
       self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat)
     
     # remainder is filled with the man page description
     descriptionHeight = max(0, detailPanelHeight - 3)
-    descriptionContent = "Description: " + cursorSelection.get(FIELD_DESCRIPTION)
+    descriptionContent = "Description: " + selection.get(Field.DESCRIPTION)
     
     for i in range(descriptionHeight):
       # checks if we're done writing the description
@@ -304,7 +354,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

Modified: arm/release/src/interface/connPanel.py
===================================================================
--- arm/release/src/interface/connPanel.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/connPanel.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -508,7 +508,7 @@
     else: return # skip following redraw
     self.redraw(True)
   
-  def draw(self, subwindow, width, height):
+  def draw(self, width, height):
     self.connectionsLock.acquire()
     try:
       # hostnames frequently get updated so frequent sorting needed
@@ -529,7 +529,7 @@
         if self.showingDetails:
           listingHeight -= 8
           isScrollBarVisible = len(self.connections) > height - 9
-          if width > 80: subwindow.hline(8, 80, curses.ACS_HLINE, width - 81)
+          if width > 80: self.win.hline(8, 80, curses.ACS_HLINE, width - 81)
         else:
           isScrollBarVisible = len(self.connections) > height - 1
         xOffset = 3 if isScrollBarVisible else 0 # content offset for scroll bar
@@ -878,6 +878,8 @@
     self.familyFingerprints = {}
     
     for familyEntry in self.family:
+      if not familyEntry: continue
+      
       if familyEntry[0] == "$":
         # relay identified by fingerprint
         self.familyFingerprints[familyEntry] = familyEntry[1:]

Deleted: arm/release/src/interface/connections/__init__.py
===================================================================
--- arm/trunk/src/interface/connections/__init__.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/connections/__init__.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -1,6 +0,0 @@
-"""
-Panels, popups, and handlers comprising the arm user interface.
-"""
-
-__all__ = ["circEntry", "connEntry", "connPanel", "entries"]
-

Copied: arm/release/src/interface/connections/__init__.py (from rev 24554, arm/trunk/src/interface/connections/__init__.py)
===================================================================
--- arm/release/src/interface/connections/__init__.py	                        (rev 0)
+++ arm/release/src/interface/connections/__init__.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -0,0 +1,6 @@
+"""
+Panels, popups, and handlers comprising the arm user interface.
+"""
+
+__all__ = ["circEntry", "connEntry", "connPanel", "entries"]
+

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

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

Deleted: arm/release/src/interface/connections/connEntry.py
===================================================================
--- arm/trunk/src/interface/connections/connEntry.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/connections/connEntry.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -1,864 +0,0 @@
-"""
-Connection panel entries related to actual connections to or from the system
-(ie, results seen by netstat, lsof, etc).
-"""
-
-import time
-import curses
-
-from util import connections, enum, torTools, uiTools
-from interface.connections import entries
-
-# Connection Categories:
-#   Inbound      Relay connection, coming to us.
-#   Outbound     Relay connection, leaving us.
-#   Exit         Outbound relay connection leaving the Tor network.
-#   Hidden       Connections to a hidden service we're providing.
-#   Socks        Socks connections for applications using Tor.
-#   Circuit      Circuits our tor client has created.
-#   Directory    Fetching tor consensus information.
-#   Control      Tor controller (arm, vidalia, etc).
-
-Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "HIDDEN", "SOCKS", "CIRCUIT", "DIRECTORY", "CONTROL")
-CATEGORY_COLOR = {Category.INBOUND: "green",      Category.OUTBOUND: "blue",
-                  Category.EXIT: "red",           Category.HIDDEN: "magenta",
-                  Category.SOCKS: "yellow",       Category.CIRCUIT: "cyan",
-                  Category.DIRECTORY: "magenta",  Category.CONTROL: "red"}
-
-# static data for listing format
-# <src>  -->  <dst>  <etc><padding>
-LABEL_FORMAT = "%s  -->  %s  %s%s"
-LABEL_MIN_PADDING = 2 # min space between listing label and following data
-
-# sort value for scrubbed ip addresses
-SCRUBBED_IP_VAL = 255 ** 4
-
-CONFIG = {"features.connection.markInitialConnections": True,
-          "features.connection.showExitPort": True,
-          "features.connection.showColumn.fingerprint": True,
-          "features.connection.showColumn.nickname": True,
-          "features.connection.showColumn.destination": True,
-          "features.connection.showColumn.expandedIp": True}
-
-def loadConfig(config):
-  config.update(CONFIG)
-
-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
-    
-    # if set then this overwrites fingerprint lookups
-    self.fingerprintOverwrite = None
-  
-  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
-    """
-    
-    # TODO: skipping all hostname resolution to be safe for now
-    #try:
-    #  myHostname = hostnames.resolve(self.ipAddr)
-    #except:
-    #  # either a ValueError or IOError depending on the source of the lookup failure
-    #  myHostname = None
-    #
-    #if not myHostname: return default
-    #else: return myHostname
-    
-    return default
-  
-  def getLocale(self, default=None):
-    """
-    Provides the two letter country code for the IP address' locale.
-    
-    Arguments:
-      default - return value if no locale information is available
-    """
-    
-    conn = torTools.getConn()
-    return conn.getInfo("ip-to-country/%s" % self.ipAddr, default)
-  
-  def getFingerprint(self):
-    """
-    Provides the fingerprint of the relay, returning "UNKNOWN" if it can't be
-    determined.
-    """
-    
-    if self.fingerprintOverwrite:
-      return self.fingerprintOverwrite
-    
-    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.
-    """
-    
-    myFingerprint = self.getFingerprint()
-    
-    if myFingerprint != "UNKNOWN":
-      conn = torTools.getConn()
-      myNickname = conn.getRelayNickname(myFingerprint)
-      
-      if myNickname: return myNickname
-      else: return "UNKNOWN"
-    else: return "UNKNOWN"
-
-class ConnectionEntry(entries.ConnectionPanelEntry):
-  """
-  Represents a connection being made to or from this system. These only
-  concern real connections so it includes the inbound, outbound, directory,
-  application, and controller categories.
-  """
-  
-  def __init__(self, lIpAddr, lPort, fIpAddr, fPort):
-    entries.ConnectionPanelEntry.__init__(self)
-    self.lines = [ConnectionLine(lIpAddr, lPort, fIpAddr, fPort)]
-  
-  def getSortValue(self, attr, listingType):
-    """
-    Provides the value of a single attribute used for sorting purposes.
-    """
-    
-    connLine = self.lines[0]
-    if attr == entries.SortAttr.IP_ADDRESS:
-      if connLine.isPrivate(): return SCRUBBED_IP_VAL # orders at the end
-      return connLine.sortIpAddr
-    elif attr == entries.SortAttr.PORT:
-      return connLine.sortPort
-    elif attr == entries.SortAttr.HOSTNAME:
-      if connLine.isPrivate(): return ""
-      return connLine.foreign.getHostname("")
-    elif attr == entries.SortAttr.FINGERPRINT:
-      return connLine.foreign.getFingerprint()
-    elif attr == entries.SortAttr.NICKNAME:
-      myNickname = connLine.foreign.getNickname()
-      if myNickname == "UNKNOWN": return "z" * 20 # orders at the end
-      else: return myNickname.lower()
-    elif attr == entries.SortAttr.CATEGORY:
-      return Category.indexOf(connLine.getType())
-    elif attr == entries.SortAttr.UPTIME:
-      return connLine.startTime
-    elif attr == entries.SortAttr.COUNTRY:
-      if connections.isIpAddressPrivate(self.lines[0].foreign.getIpAddr()): return ""
-      else: return connLine.foreign.getLocale("")
-    else:
-      return entries.ConnectionPanelEntry.getSortValue(self, attr, listingType)
-
-class ConnectionLine(entries.ConnectionPanelLine):
-  """
-  Display component of the ConnectionEntry.
-  """
-  
-  def __init__(self, lIpAddr, lPort, fIpAddr, fPort, includePort=True, includeExpandedIpAddr=True):
-    entries.ConnectionPanelLine.__init__(self)
-    
-    self.local = Endpoint(lIpAddr, lPort)
-    self.foreign = Endpoint(fIpAddr, fPort)
-    self.startTime = time.time()
-    self.isInitialConnection = False
-    
-    # overwrite the local fingerprint with ours
-    conn = torTools.getConn()
-    self.local.fingerprintOverwrite = conn.getInfo("fingerprint")
-    
-    # True if the connection has matched the properties of a client/directory
-    # connection every time we've checked. The criteria we check is...
-    #   client    - first hop in an established circuit
-    #   directory - matches an established single-hop circuit (probably a
-    #               directory mirror)
-    
-    self._possibleClient = True
-    self._possibleDirectory = True
-    
-    # attributes for SOCKS, HIDDEN, and CONTROL connections
-    self.appName = None
-    self.appPid = None
-    self.isAppResolving = False
-    
-    myOrPort = conn.getOption("ORPort")
-    myDirPort = conn.getOption("DirPort")
-    mySocksPort = conn.getOption("SocksPort", "9050")
-    myCtlPort = conn.getOption("ControlPort")
-    myHiddenServicePorts = conn.getHiddenServicePorts()
-    
-    # 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.baseType = Category.INBOUND
-      self.local.isORPort = True
-    elif lPort == mySocksPort:
-      self.baseType = Category.SOCKS
-    elif fPort in myHiddenServicePorts:
-      self.baseType = Category.HIDDEN
-    elif lPort == myCtlPort:
-      self.baseType = Category.CONTROL
-    else:
-      self.baseType = Category.OUTBOUND
-      self.foreign.isORPort = True
-    
-    self.cachedType = None
-    
-    # includes the port or expanded ip address field when displaying listing
-    # information if true
-    self.includePort = includePort
-    self.includeExpandedIpAddr = includeExpandedIpAddr
-    
-    # cached immutable values used for sorting
-    self.sortIpAddr = connections.ipToInt(self.foreign.getIpAddr())
-    self.sortPort = int(self.foreign.getPort())
-  
-  def getListingEntry(self, width, currentTime, listingType):
-    """
-    Provides the DrawEntry for this connection's listing. Lines are composed
-    of the following components:
-      <src>  -->  <dst>     <etc>     <uptime> (<type>)
-    
-    ListingType.IP_ADDRESS:
-      src - <internal addr:port> --> <external addr:port>
-      dst - <destination addr:port>
-      etc - <fingerprint> <nickname>
-    
-    ListingType.HOSTNAME:
-      src - localhost:<port>
-      dst - <destination hostname:port>
-      etc - <destination addr:port> <fingerprint> <nickname>
-    
-    ListingType.FINGERPRINT:
-      src - localhost
-      dst - <destination fingerprint>
-      etc - <nickname> <destination addr:port>
-    
-    ListingType.NICKNAME:
-      src - <source nickname>
-      dst - <destination nickname>
-      etc - <fingerprint> <destination addr:port>
-    
-    Arguments:
-      width       - maximum length of the line
-      currentTime - unix timestamp for what the results should consider to be
-                    the current time
-      listingType - primary attribute we're listing connections by
-    """
-    
-    # fetch our (most likely cached) display entry for the listing
-    myListing = entries.ConnectionPanelLine.getListingEntry(self, width, currentTime, listingType)
-    
-    # fill in the current uptime and return the results
-    if CONFIG["features.connection.markInitialConnections"]:
-      timePrefix = "+" if self.isInitialConnection else " "
-    else: timePrefix = ""
-    
-    timeEntry = myListing.getNext()
-    timeEntry.text = timePrefix + "%5s" % uiTools.getTimeLabel(currentTime - self.startTime, 1)
-    
-    return myListing
-  
-  def isUnresolvedApp(self):
-    """
-    True if our display uses application information that hasn't yet been resolved.
-    """
-    
-    return self.appName == None and self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL)
-  
-  def _getListingEntry(self, width, currentTime, listingType):
-    entryType = self.getType()
-    
-    # Lines are split into the following components in reverse:
-    # content  - "<src>  -->  <dst>     <etc>     "
-    # time     - "<uptime>"
-    # preType  - " ("
-    # category - "<type>"
-    # postType - ")   "
-    
-    lineFormat = uiTools.getColor(CATEGORY_COLOR[entryType])
-    timeWidth = 6 if CONFIG["features.connection.markInitialConnections"] else 5
-    
-    drawEntry = uiTools.DrawEntry(")" + " " * (9 - len(entryType)), lineFormat)
-    drawEntry = uiTools.DrawEntry(entryType.upper(), lineFormat | curses.A_BOLD, drawEntry)
-    drawEntry = uiTools.DrawEntry(" (", lineFormat, drawEntry)
-    drawEntry = uiTools.DrawEntry(" " * timeWidth, lineFormat, drawEntry)
-    drawEntry = uiTools.DrawEntry(self._getListingContent(width - (12 + timeWidth), listingType), lineFormat, drawEntry)
-    return drawEntry
-  
-  def _getDetails(self, width):
-    """
-    Provides details on the connection, correlated against available consensus
-    data.
-    
-    Arguments:
-      width - available space to display in
-    """
-    
-    detailFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[self.getType()])
-    return [uiTools.DrawEntry(line, detailFormat) for line in self._getDetailContent(width)]
-  
-  def _getDescriptors(self, width):
-    """
-    Provides raw descriptor information for the relay.
-    
-    Arguments:
-      width - available space to display in
-    """
-    
-    # TODO: Porting and refactoring the descriptorPopup.py functionality is
-    # gonna take quite a bit of work. This is a very rarely used feature and
-    # not worth delaying the 1.4.2 release any further, so this will be a part
-    # of 1.4.3.
-    
-    return []
-  
-  def resetDisplay(self):
-    entries.ConnectionPanelLine.resetDisplay(self)
-    self.cachedType = None
-  
-  def isPrivate(self):
-    """
-    Returns true if the endpoint is private, possibly belonging to a client
-    connection or exit traffic.
-    """
-    
-    # This is used to scrub private information from the interface. Relaying
-    # etiquette (and wiretapping laws) say these are bad things to look at so
-    # DON'T CHANGE THIS UNLESS YOU HAVE A DAMN GOOD REASON!
-    
-    myType = self.getType()
-    
-    if myType == Category.INBOUND:
-      # if we're a guard or bridge and the connection doesn't belong to a
-      # known relay then it might be client traffic
-      
-      conn = torTools.getConn()
-      if "Guard" in conn.getMyFlags() or conn.getOption("BridgeRelay") == "1":
-        allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True)
-        return allMatches == []
-    elif myType == Category.EXIT:
-      # DNS connections exiting us aren't private (since they're hitting our
-      # resolvers). Everything else, however, is.
-      
-      # TODO: Ideally this would also double check that it's a UDP connection
-      # (since DNS is the only UDP connections Tor will relay), however this
-      # will take a bit more work to propagate the information up from the
-      # connection resolver.
-      return self.foreign.getPort() != "53"
-    
-    # for everything else this isn't a concern
-    return False
-  
-  def getType(self):
-    """
-    Provides our best guess at the current type of the connection. This
-    depends on consensus results, our current client circuits, etc. Results
-    are cached until this entry's display is reset.
-    """
-    
-    # caches both to simplify the calls and to keep the type consistent until
-    # we want to reflect changes
-    if not self.cachedType:
-      if self.baseType == Category.OUTBOUND:
-        # Currently the only non-static categories are OUTBOUND vs...
-        # - EXIT since this depends on the current consensus
-        # - CIRCUIT if this is likely to belong to our guard usage
-        # - DIRECTORY if this is a single-hop circuit (directory mirror?)
-        # 
-        # The exitability, circuits, and fingerprints are all cached by the
-        # torTools util keeping this a quick lookup.
-        
-        conn = torTools.getConn()
-        destFingerprint = self.foreign.getFingerprint()
-        
-        if destFingerprint == "UNKNOWN":
-          # Not a known relay. This might be an exit connection.
-          
-          if conn.isExitingAllowed(self.foreign.getIpAddr(), self.foreign.getPort()):
-            self.cachedType = Category.EXIT
-        elif self._possibleClient or self._possibleDirectory:
-          # This belongs to a known relay. If we haven't eliminated ourselves as
-          # a possible client or directory connection then check if it still
-          # holds true.
-          
-          myCircuits = conn.getCircuits()
-          
-          if self._possibleClient:
-            # Checks that this belongs to the first hop in a circuit that's
-            # either unestablished or longer than a single hop (ie, anything but
-            # a built 1-hop connection since those are most likely a directory
-            # mirror).
-            
-            for _, status, _, path in myCircuits:
-              if path[0] == destFingerprint and (status != "BUILT" or len(path) > 1):
-                self.cachedType = Category.CIRCUIT # matched a probable guard connection
-            
-            # if we fell through, we can eliminate ourselves as a guard in the future
-            if not self.cachedType:
-              self._possibleClient = False
-          
-          if self._possibleDirectory:
-            # Checks if we match a built, single hop circuit.
-            
-            for _, status, _, path in myCircuits:
-              if path[0] == destFingerprint and status == "BUILT" and len(path) == 1:
-                self.cachedType = Category.DIRECTORY
-            
-            # if we fell through, eliminate ourselves as a directory connection
-            if not self.cachedType:
-              self._possibleDirectory = False
-      
-      if not self.cachedType:
-        self.cachedType = self.baseType
-    
-    return self.cachedType
-  
-  def getEtcContent(self, width, listingType):
-    """
-    Provides the optional content for the connection.
-    
-    Arguments:
-      width       - maximum length of the line
-      listingType - primary attribute we're listing connections by
-    """
-    
-    # for applications show the command/pid
-    if self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL):
-      displayLabel = ""
-      
-      if self.appName:
-        if self.appPid: displayLabel = "%s (%s)" % (self.appName, self.appPid)
-        else: displayLabel = self.appName
-      elif self.isAppResolving:
-        displayLabel = "resolving..."
-      else: displayLabel = "UNKNOWN"
-      
-      if len(displayLabel) < width:
-        return ("%%-%is" % width) % displayLabel
-      else: return ""
-    
-    # for everything else display connection/consensus information
-    dstAddress = self.getDestinationLabel(26, includeLocale = True)
-    etc, usedSpace = "", 0
-    if listingType == entries.ListingType.IP_ADDRESS:
-      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
-        # show fingerprint (column width: 42 characters)
-        etc += "%-40s  " % self.foreign.getFingerprint()
-        usedSpace += 42
-      
-      if width > usedSpace + 10 and CONFIG["features.connection.showColumn.nickname"]:
-        # show nickname (column width: remainder)
-        nicknameSpace = width - usedSpace
-        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
-        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
-        usedSpace += nicknameSpace + 2
-    elif listingType == entries.ListingType.HOSTNAME:
-      if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
-        # show destination ip/port/locale (column width: 28 characters)
-        etc += "%-26s  " % dstAddress
-        usedSpace += 28
-      
-      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
-        # show fingerprint (column width: 42 characters)
-        etc += "%-40s  " % self.foreign.getFingerprint()
-        usedSpace += 42
-      
-      if width > usedSpace + 17 and CONFIG["features.connection.showColumn.nickname"]:
-        # show nickname (column width: min 17 characters, uses half of the remainder)
-        nicknameSpace = 15 + (width - (usedSpace + 17)) / 2
-        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
-        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
-        usedSpace += (nicknameSpace + 2)
-    elif listingType == entries.ListingType.FINGERPRINT:
-      if width > usedSpace + 17:
-        # show nickname (column width: min 17 characters, consumes any remaining space)
-        nicknameSpace = width - usedSpace - 2
-        
-        # if there's room then also show a column with the destination
-        # ip/port/locale (column width: 28 characters)
-        isIpLocaleIncluded = width > usedSpace + 45
-        isIpLocaleIncluded &= CONFIG["features.connection.showColumn.destination"]
-        if isIpLocaleIncluded: nicknameSpace -= 28
-        
-        if CONFIG["features.connection.showColumn.nickname"]:
-          nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
-          etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
-          usedSpace += nicknameSpace + 2
-        
-        if isIpLocaleIncluded:
-          etc += "%-26s  " % dstAddress
-          usedSpace += 28
-    else:
-      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
-        # show fingerprint (column width: 42 characters)
-        etc += "%-40s  " % self.foreign.getFingerprint()
-        usedSpace += 42
-      
-      if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
-        # show destination ip/port/locale (column width: 28 characters)
-        etc += "%-26s  " % dstAddress
-        usedSpace += 28
-    
-    return ("%%-%is" % width) % etc
-  
-  def _getListingContent(self, width, listingType):
-    """
-    Provides the source, destination, and extra info for our listing.
-    
-    Arguments:
-      width       - maximum length of the line
-      listingType - primary attribute we're listing connections by
-    """
-    
-    conn = torTools.getConn()
-    myType = self.getType()
-    dstAddress = self.getDestinationLabel(26, includeLocale = True)
-    
-    # The required widths are the sum of the following:
-    # - room for LABEL_FORMAT and LABEL_MIN_PADDING (11 characters)
-    # - base data for the listing
-    # - that extra field plus any previous
-    
-    usedSpace = len(LABEL_FORMAT % tuple([""] * 4)) + LABEL_MIN_PADDING
-    localPort = ":%s" % self.local.getPort() if self.includePort else ""
-    
-    src, dst, etc = "", "", ""
-    if listingType == entries.ListingType.IP_ADDRESS:
-      myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr())
-      addrDiffer = myExternalIpAddr != self.local.getIpAddr()
-      
-      # Expanding doesn't make sense, if the connection isn't actually
-      # going through Tor's external IP address. As there isn't a known
-      # method for checking if it is, we're checking the type instead.
-      #
-      # This isn't entirely correct. It might be a better idea to check if
-      # the source and destination addresses are both private, but that might
-      # not be perfectly reliable either.
-      
-      isExpansionType = not myType in (Category.SOCKS, Category.HIDDEN, Category.CONTROL)
-      
-      if isExpansionType: srcAddress = myExternalIpAddr + localPort
-      else: srcAddress = self.local.getIpAddr() + localPort
-      
-      if myType in (Category.SOCKS, Category.CONTROL):
-        # Like inbound connections these need their source and destination to
-        # be swapped. However, this only applies when listing by IP or hostname
-        # (their fingerprint and nickname are both for us). Reversing the
-        # fields here to keep the same column alignments.
-        
-        src = "%-21s" % dstAddress
-        dst = "%-26s" % srcAddress
-      else:
-        src = "%-21s" % srcAddress # ip:port = max of 21 characters
-        dst = "%-26s" % dstAddress # ip:port (xx) = max of 26 characters
-      
-      usedSpace += len(src) + len(dst) # base data requires 47 characters
-      
-      # Showing the fingerprint (which has the width of 42) has priority over
-      # an expanded address field. Hence check if we either have space for
-      # both or wouldn't be showing the fingerprint regardless.
-      
-      isExpandedAddrVisible = width > usedSpace + 28
-      if isExpandedAddrVisible and CONFIG["features.connection.showColumn.fingerprint"]:
-        isExpandedAddrVisible = width < usedSpace + 42 or width > usedSpace + 70
-      
-      if addrDiffer and isExpansionType and isExpandedAddrVisible and self.includeExpandedIpAddr and CONFIG["features.connection.showColumn.expandedIp"]:
-        # include the internal address in the src (extra 28 characters)
-        internalAddress = self.local.getIpAddr() + localPort
-        
-        # If this is an inbound connection then reverse ordering so it's:
-        # <foreign> --> <external> --> <internal>
-        # when the src and dst are swapped later
-        
-        if myType == Category.INBOUND: src = "%-21s  -->  %s" % (src, internalAddress)
-        else: src = "%-21s  -->  %s" % (internalAddress, src)
-        
-        usedSpace += 28
-      
-      etc = self.getEtcContent(width - usedSpace, listingType)
-      usedSpace += len(etc)
-    elif listingType == entries.ListingType.HOSTNAME:
-      # 15 characters for source, and a min of 40 reserved for the destination
-      # TODO: when actually functional the src and dst need to be swapped for
-      # SOCKS and CONTROL connections
-      src = "localhost%-6s" % localPort
-      usedSpace += len(src)
-      minHostnameSpace = 40
-      
-      etc = self.getEtcContent(width - usedSpace - minHostnameSpace, listingType)
-      usedSpace += len(etc)
-      
-      hostnameSpace = width - usedSpace
-      usedSpace = width # prevents padding at the end
-      if self.isPrivate():
-        dst = ("%%-%is" % hostnameSpace) % "<scrubbed>"
-      else:
-        hostname = self.foreign.getHostname(self.foreign.getIpAddr())
-        portLabel = ":%-5s" % self.foreign.getPort() if self.includePort else ""
-        
-        # truncates long hostnames and sets dst to <hostname>:<port>
-        hostname = uiTools.cropStr(hostname, hostnameSpace, 0)
-        dst = ("%%-%is" % hostnameSpace) % (hostname + portLabel)
-    elif listingType == entries.ListingType.FINGERPRINT:
-      src = "localhost"
-      if myType == Category.CONTROL: dst = "localhost"
-      else: dst = self.foreign.getFingerprint()
-      dst = "%-40s" % dst
-      
-      usedSpace += len(src) + len(dst) # base data requires 49 characters
-      
-      etc = self.getEtcContent(width - usedSpace, listingType)
-      usedSpace += len(etc)
-    else:
-      # base data requires 50 min characters
-      src = self.local.getNickname()
-      if myType == Category.CONTROL: dst = self.local.getNickname()
-      else: dst = self.foreign.getNickname()
-      minBaseSpace = 50
-      
-      etc = self.getEtcContent(width - usedSpace - minBaseSpace, listingType)
-      usedSpace += len(etc)
-      
-      baseSpace = width - usedSpace
-      usedSpace = width # prevents padding at the end
-      
-      if len(src) + len(dst) > baseSpace:
-        src = uiTools.cropStr(src, baseSpace / 3)
-        dst = uiTools.cropStr(dst, baseSpace - len(src))
-      
-      # pads dst entry to its max space
-      dst = ("%%-%is" % (baseSpace - len(src))) % dst
-    
-    if myType == Category.INBOUND: src, dst = dst, src
-    padding = " " * (width - usedSpace + LABEL_MIN_PADDING)
-    return LABEL_FORMAT % (src, dst, etc, padding)
-  
-  def _getDetailContent(self, width):
-    """
-    Provides a list with detailed information for this connection.
-    
-    Arguments:
-      width - max length of lines
-    """
-    
-    lines = [""] * 7
-    lines[0] = "address: %s" % self.getDestinationLabel(width - 11)
-    lines[1] = "locale: %s" % ("??" if self.isPrivate() else self.foreign.getLocale("??"))
-    
-    # Remaining data concerns the consensus results, with three possible cases:
-    # - if there's a single match then display its details
-    # - if there's multiple potential relays then list all of the combinations
-    #   of ORPorts / Fingerprints
-    # - if no consensus data is available then say so (probably a client or
-    #   exit connection)
-    
-    fingerprint = self.foreign.getFingerprint()
-    conn = torTools.getConn()
-    
-    if fingerprint != "UNKNOWN":
-      # single match - display information available about it
-      nsEntry = conn.getConsensusEntry(fingerprint)
-      descEntry = conn.getDescriptorEntry(fingerprint)
-      
-      # append the fingerprint to the second line
-      lines[1] = "%-13sfingerprint: %s" % (lines[1], fingerprint)
-      
-      if nsEntry:
-        # example consensus entry:
-        # r murble R8sCM1ar1sS2GulQYFVmvN95xsk RJr6q+wkTFG+ng5v2bdCbVVFfA4 2011-02-21 00:25:32 195.43.157.85 443 0
-        # s Exit Fast Guard Named Running Stable Valid
-        # w Bandwidth=2540
-        # p accept 20-23,43,53,79-81,88,110,143,194,443
-        
-        nsLines = nsEntry.split("\n")
-        
-        firstLineComp = nsLines[0].split(" ")
-        if len(firstLineComp) >= 9:
-          _, nickname, _, _, pubDate, pubTime, _, orPort, dirPort = firstLineComp[:9]
-        else: nickname, pubDate, pubTime, orPort, dirPort = "", "", "", "", ""
-        
-        flags = "unknown"
-        if len(nsLines) >= 2 and nsLines[1].startswith("s "):
-          flags = nsLines[1][2:]
-        
-        # The network status exit policy doesn't exist for older tor versions.
-        # If unavailable we'll need the full exit policy which is on the
-        # descriptor (if that's available).
-        
-        exitPolicy = "unknown"
-        if len(nsLines) >= 4 and nsLines[3].startswith("p "):
-          exitPolicy = nsLines[3][2:].replace(",", ", ")
-        elif descEntry:
-          # the descriptor has an individual line for each entry in the exit policy
-          exitPolicyEntries = []
-          
-          for line in descEntry.split("\n"):
-            if line.startswith("accept") or line.startswith("reject"):
-              exitPolicyEntries.append(line.strip())
-          
-          exitPolicy = ", ".join(exitPolicyEntries)
-        
-        dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort
-        lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel)
-        lines[3] = "published: %s %s" % (pubDate, pubTime)
-        lines[4] = "flags: %s" % flags.replace(" ", ", ")
-        lines[5] = "exit policy: %s" % exitPolicy
-      
-      if descEntry:
-        torVersion, platform, contact = "", "", ""
-        
-        for descLine in descEntry.split("\n"):
-          if descLine.startswith("platform"):
-            # has the tor version and platform, ex:
-            # platform Tor 0.2.1.29 (r318f470bc5f2ad43) on Linux x86_64
-            
-            torVersion = descLine[13:descLine.find(" ", 13)]
-            platform = descLine[descLine.rfind(" on ") + 4:]
-          elif descLine.startswith("contact"):
-            contact = descLine[8:]
-            
-            # clears up some highly common obscuring
-            for alias in (" at ", " AT "): contact = contact.replace(alias, "@")
-            for alias in (" dot ", " DOT "): contact = contact.replace(alias, ".")
-            
-            break # contact lines come after the platform
-        
-        lines[3] = "%-35s os: %-14s version: %s" % (lines[3], platform, torVersion)
-        
-        # contact information is an optional field
-        if contact: lines[6] = "contact: %s" % contact
-    else:
-      allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True)
-      
-      if allMatches:
-        # multiple matches
-        lines[2] = "Multiple matches, possible fingerprints are:"
-        
-        for i in range(len(allMatches)):
-          isLastLine = i == 3
-          
-          relayPort, relayFingerprint = allMatches[i]
-          lineText = "%i. or port: %-5s fingerprint: %s" % (i, relayPort, relayFingerprint)
-          
-          # if there's multiple lines remaining at the end then give a count
-          remainingRelays = len(allMatches) - i
-          if isLastLine and remainingRelays > 1:
-            lineText = "... %i more" % remainingRelays
-          
-          lines[3 + i] = lineText
-          
-          if isLastLine: break
-      else:
-        # no consensus entry for this ip address
-        lines[2] = "No consensus data found"
-    
-    # crops any lines that are too long
-    for i in range(len(lines)):
-      lines[i] = uiTools.cropStr(lines[i], width - 2)
-    
-    return lines
-  
-  def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False):
-    """
-    Provides a short description of the destination. This is made up of two
-    components, the base <ip addr>:<port> and an extra piece of information in
-    parentheses. The IP address is scrubbed from private connections.
-    
-    Extra information is...
-    - the port's purpose for exit connections
-    - the locale and/or hostname if set to do so, the address isn't private,
-      and isn't on the local network
-    - nothing otherwise
-    
-    Arguments:
-      maxLength       - maximum length of the string returned
-      includeLocale   - possibly includes the locale
-      includeHostname - possibly includes the hostname
-    """
-    
-    # the port and port derived data can be hidden by config or without includePort
-    includePort = self.includePort and (CONFIG["features.connection.showExitPort"] or self.getType() != Category.EXIT)
-    
-    # destination of the connection
-    ipLabel = "<scrubbed>" if self.isPrivate() else self.foreign.getIpAddr()
-    portLabel = ":%s" % self.foreign.getPort() if includePort else ""
-    dstAddress = ipLabel + portLabel
-    
-    # Only append the extra info if there's at least a couple characters of
-    # space (this is what's needed for the country codes).
-    if len(dstAddress) + 5 <= maxLength:
-      spaceAvailable = maxLength - len(dstAddress) - 3
-      
-      if self.getType() == Category.EXIT and includePort:
-        purpose = connections.getPortUsage(self.foreign.getPort())
-        
-        if purpose:
-          # BitTorrent is a common protocol to truncate, so just use "Torrent"
-          # if there's not enough room.
-          if len(purpose) > spaceAvailable and purpose == "BitTorrent":
-            purpose = "Torrent"
-          
-          # crops with a hyphen if too long
-          purpose = uiTools.cropStr(purpose, spaceAvailable, endType = uiTools.Ending.HYPHEN)
-          
-          dstAddress += " (%s)" % purpose
-      elif not connections.isIpAddressPrivate(self.foreign.getIpAddr()):
-        extraInfo = []
-        
-        if includeLocale:
-          foreignLocale = self.foreign.getLocale("??")
-          extraInfo.append(foreignLocale)
-          spaceAvailable -= len(foreignLocale) + 2
-        
-        if includeHostname:
-          dstHostname = self.foreign.getHostname()
-          
-          if dstHostname:
-            # determines the full space available, taking into account the ", "
-            # dividers if there's multiple pieces of extra data
-            
-            maxHostnameSpace = spaceAvailable - 2 * len(extraInfo)
-            dstHostname = uiTools.cropStr(dstHostname, maxHostnameSpace)
-            extraInfo.append(dstHostname)
-            spaceAvailable -= len(dstHostname)
-        
-        if extraInfo:
-          dstAddress += " (%s)" % ", ".join(extraInfo)
-    
-    return dstAddress[:maxLength]
-

Copied: arm/release/src/interface/connections/connEntry.py (from rev 24554, arm/trunk/src/interface/connections/connEntry.py)
===================================================================
--- arm/release/src/interface/connections/connEntry.py	                        (rev 0)
+++ arm/release/src/interface/connections/connEntry.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -0,0 +1,864 @@
+"""
+Connection panel entries related to actual connections to or from the system
+(ie, results seen by netstat, lsof, etc).
+"""
+
+import time
+import curses
+
+from util import connections, enum, torTools, uiTools
+from interface.connections import entries
+
+# Connection Categories:
+#   Inbound      Relay connection, coming to us.
+#   Outbound     Relay connection, leaving us.
+#   Exit         Outbound relay connection leaving the Tor network.
+#   Hidden       Connections to a hidden service we're providing.
+#   Socks        Socks connections for applications using Tor.
+#   Circuit      Circuits our tor client has created.
+#   Directory    Fetching tor consensus information.
+#   Control      Tor controller (arm, vidalia, etc).
+
+Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "HIDDEN", "SOCKS", "CIRCUIT", "DIRECTORY", "CONTROL")
+CATEGORY_COLOR = {Category.INBOUND: "green",      Category.OUTBOUND: "blue",
+                  Category.EXIT: "red",           Category.HIDDEN: "magenta",
+                  Category.SOCKS: "yellow",       Category.CIRCUIT: "cyan",
+                  Category.DIRECTORY: "magenta",  Category.CONTROL: "red"}
+
+# static data for listing format
+# <src>  -->  <dst>  <etc><padding>
+LABEL_FORMAT = "%s  -->  %s  %s%s"
+LABEL_MIN_PADDING = 2 # min space between listing label and following data
+
+# sort value for scrubbed ip addresses
+SCRUBBED_IP_VAL = 255 ** 4
+
+CONFIG = {"features.connection.markInitialConnections": True,
+          "features.connection.showExitPort": True,
+          "features.connection.showColumn.fingerprint": True,
+          "features.connection.showColumn.nickname": True,
+          "features.connection.showColumn.destination": True,
+          "features.connection.showColumn.expandedIp": True}
+
+def loadConfig(config):
+  config.update(CONFIG)
+
+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
+    
+    # if set then this overwrites fingerprint lookups
+    self.fingerprintOverwrite = None
+  
+  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
+    """
+    
+    # TODO: skipping all hostname resolution to be safe for now
+    #try:
+    #  myHostname = hostnames.resolve(self.ipAddr)
+    #except:
+    #  # either a ValueError or IOError depending on the source of the lookup failure
+    #  myHostname = None
+    #
+    #if not myHostname: return default
+    #else: return myHostname
+    
+    return default
+  
+  def getLocale(self, default=None):
+    """
+    Provides the two letter country code for the IP address' locale.
+    
+    Arguments:
+      default - return value if no locale information is available
+    """
+    
+    conn = torTools.getConn()
+    return conn.getInfo("ip-to-country/%s" % self.ipAddr, default)
+  
+  def getFingerprint(self):
+    """
+    Provides the fingerprint of the relay, returning "UNKNOWN" if it can't be
+    determined.
+    """
+    
+    if self.fingerprintOverwrite:
+      return self.fingerprintOverwrite
+    
+    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.
+    """
+    
+    myFingerprint = self.getFingerprint()
+    
+    if myFingerprint != "UNKNOWN":
+      conn = torTools.getConn()
+      myNickname = conn.getRelayNickname(myFingerprint)
+      
+      if myNickname: return myNickname
+      else: return "UNKNOWN"
+    else: return "UNKNOWN"
+
+class ConnectionEntry(entries.ConnectionPanelEntry):
+  """
+  Represents a connection being made to or from this system. These only
+  concern real connections so it includes the inbound, outbound, directory,
+  application, and controller categories.
+  """
+  
+  def __init__(self, lIpAddr, lPort, fIpAddr, fPort):
+    entries.ConnectionPanelEntry.__init__(self)
+    self.lines = [ConnectionLine(lIpAddr, lPort, fIpAddr, fPort)]
+  
+  def getSortValue(self, attr, listingType):
+    """
+    Provides the value of a single attribute used for sorting purposes.
+    """
+    
+    connLine = self.lines[0]
+    if attr == entries.SortAttr.IP_ADDRESS:
+      if connLine.isPrivate(): return SCRUBBED_IP_VAL # orders at the end
+      return connLine.sortIpAddr
+    elif attr == entries.SortAttr.PORT:
+      return connLine.sortPort
+    elif attr == entries.SortAttr.HOSTNAME:
+      if connLine.isPrivate(): return ""
+      return connLine.foreign.getHostname("")
+    elif attr == entries.SortAttr.FINGERPRINT:
+      return connLine.foreign.getFingerprint()
+    elif attr == entries.SortAttr.NICKNAME:
+      myNickname = connLine.foreign.getNickname()
+      if myNickname == "UNKNOWN": return "z" * 20 # orders at the end
+      else: return myNickname.lower()
+    elif attr == entries.SortAttr.CATEGORY:
+      return Category.indexOf(connLine.getType())
+    elif attr == entries.SortAttr.UPTIME:
+      return connLine.startTime
+    elif attr == entries.SortAttr.COUNTRY:
+      if connections.isIpAddressPrivate(self.lines[0].foreign.getIpAddr()): return ""
+      else: return connLine.foreign.getLocale("")
+    else:
+      return entries.ConnectionPanelEntry.getSortValue(self, attr, listingType)
+
+class ConnectionLine(entries.ConnectionPanelLine):
+  """
+  Display component of the ConnectionEntry.
+  """
+  
+  def __init__(self, lIpAddr, lPort, fIpAddr, fPort, includePort=True, includeExpandedIpAddr=True):
+    entries.ConnectionPanelLine.__init__(self)
+    
+    self.local = Endpoint(lIpAddr, lPort)
+    self.foreign = Endpoint(fIpAddr, fPort)
+    self.startTime = time.time()
+    self.isInitialConnection = False
+    
+    # overwrite the local fingerprint with ours
+    conn = torTools.getConn()
+    self.local.fingerprintOverwrite = conn.getInfo("fingerprint")
+    
+    # True if the connection has matched the properties of a client/directory
+    # connection every time we've checked. The criteria we check is...
+    #   client    - first hop in an established circuit
+    #   directory - matches an established single-hop circuit (probably a
+    #               directory mirror)
+    
+    self._possibleClient = True
+    self._possibleDirectory = True
+    
+    # attributes for SOCKS, HIDDEN, and CONTROL connections
+    self.appName = None
+    self.appPid = None
+    self.isAppResolving = False
+    
+    myOrPort = conn.getOption("ORPort")
+    myDirPort = conn.getOption("DirPort")
+    mySocksPort = conn.getOption("SocksPort", "9050")
+    myCtlPort = conn.getOption("ControlPort")
+    myHiddenServicePorts = conn.getHiddenServicePorts()
+    
+    # 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.baseType = Category.INBOUND
+      self.local.isORPort = True
+    elif lPort == mySocksPort:
+      self.baseType = Category.SOCKS
+    elif fPort in myHiddenServicePorts:
+      self.baseType = Category.HIDDEN
+    elif lPort == myCtlPort:
+      self.baseType = Category.CONTROL
+    else:
+      self.baseType = Category.OUTBOUND
+      self.foreign.isORPort = True
+    
+    self.cachedType = None
+    
+    # includes the port or expanded ip address field when displaying listing
+    # information if true
+    self.includePort = includePort
+    self.includeExpandedIpAddr = includeExpandedIpAddr
+    
+    # cached immutable values used for sorting
+    self.sortIpAddr = connections.ipToInt(self.foreign.getIpAddr())
+    self.sortPort = int(self.foreign.getPort())
+  
+  def getListingEntry(self, width, currentTime, listingType):
+    """
+    Provides the DrawEntry for this connection's listing. Lines are composed
+    of the following components:
+      <src>  -->  <dst>     <etc>     <uptime> (<type>)
+    
+    ListingType.IP_ADDRESS:
+      src - <internal addr:port> --> <external addr:port>
+      dst - <destination addr:port>
+      etc - <fingerprint> <nickname>
+    
+    ListingType.HOSTNAME:
+      src - localhost:<port>
+      dst - <destination hostname:port>
+      etc - <destination addr:port> <fingerprint> <nickname>
+    
+    ListingType.FINGERPRINT:
+      src - localhost
+      dst - <destination fingerprint>
+      etc - <nickname> <destination addr:port>
+    
+    ListingType.NICKNAME:
+      src - <source nickname>
+      dst - <destination nickname>
+      etc - <fingerprint> <destination addr:port>
+    
+    Arguments:
+      width       - maximum length of the line
+      currentTime - unix timestamp for what the results should consider to be
+                    the current time
+      listingType - primary attribute we're listing connections by
+    """
+    
+    # fetch our (most likely cached) display entry for the listing
+    myListing = entries.ConnectionPanelLine.getListingEntry(self, width, currentTime, listingType)
+    
+    # fill in the current uptime and return the results
+    if CONFIG["features.connection.markInitialConnections"]:
+      timePrefix = "+" if self.isInitialConnection else " "
+    else: timePrefix = ""
+    
+    timeEntry = myListing.getNext()
+    timeEntry.text = timePrefix + "%5s" % uiTools.getTimeLabel(currentTime - self.startTime, 1)
+    
+    return myListing
+  
+  def isUnresolvedApp(self):
+    """
+    True if our display uses application information that hasn't yet been resolved.
+    """
+    
+    return self.appName == None and self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL)
+  
+  def _getListingEntry(self, width, currentTime, listingType):
+    entryType = self.getType()
+    
+    # Lines are split into the following components in reverse:
+    # content  - "<src>  -->  <dst>     <etc>     "
+    # time     - "<uptime>"
+    # preType  - " ("
+    # category - "<type>"
+    # postType - ")   "
+    
+    lineFormat = uiTools.getColor(CATEGORY_COLOR[entryType])
+    timeWidth = 6 if CONFIG["features.connection.markInitialConnections"] else 5
+    
+    drawEntry = uiTools.DrawEntry(")" + " " * (9 - len(entryType)), lineFormat)
+    drawEntry = uiTools.DrawEntry(entryType.upper(), lineFormat | curses.A_BOLD, drawEntry)
+    drawEntry = uiTools.DrawEntry(" (", lineFormat, drawEntry)
+    drawEntry = uiTools.DrawEntry(" " * timeWidth, lineFormat, drawEntry)
+    drawEntry = uiTools.DrawEntry(self._getListingContent(width - (12 + timeWidth), listingType), lineFormat, drawEntry)
+    return drawEntry
+  
+  def _getDetails(self, width):
+    """
+    Provides details on the connection, correlated against available consensus
+    data.
+    
+    Arguments:
+      width - available space to display in
+    """
+    
+    detailFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[self.getType()])
+    return [uiTools.DrawEntry(line, detailFormat) for line in self._getDetailContent(width)]
+  
+  def _getDescriptors(self, width):
+    """
+    Provides raw descriptor information for the relay.
+    
+    Arguments:
+      width - available space to display in
+    """
+    
+    # TODO: Porting and refactoring the descriptorPopup.py functionality is
+    # gonna take quite a bit of work. This is a very rarely used feature and
+    # not worth delaying the 1.4.2 release any further, so this will be a part
+    # of 1.4.3.
+    
+    return []
+  
+  def resetDisplay(self):
+    entries.ConnectionPanelLine.resetDisplay(self)
+    self.cachedType = None
+  
+  def isPrivate(self):
+    """
+    Returns true if the endpoint is private, possibly belonging to a client
+    connection or exit traffic.
+    """
+    
+    # This is used to scrub private information from the interface. Relaying
+    # etiquette (and wiretapping laws) say these are bad things to look at so
+    # DON'T CHANGE THIS UNLESS YOU HAVE A DAMN GOOD REASON!
+    
+    myType = self.getType()
+    
+    if myType == Category.INBOUND:
+      # if we're a guard or bridge and the connection doesn't belong to a
+      # known relay then it might be client traffic
+      
+      conn = torTools.getConn()
+      if "Guard" in conn.getMyFlags() or conn.getOption("BridgeRelay") == "1":
+        allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True)
+        return allMatches == []
+    elif myType == Category.EXIT:
+      # DNS connections exiting us aren't private (since they're hitting our
+      # resolvers). Everything else, however, is.
+      
+      # TODO: Ideally this would also double check that it's a UDP connection
+      # (since DNS is the only UDP connections Tor will relay), however this
+      # will take a bit more work to propagate the information up from the
+      # connection resolver.
+      return self.foreign.getPort() != "53"
+    
+    # for everything else this isn't a concern
+    return False
+  
+  def getType(self):
+    """
+    Provides our best guess at the current type of the connection. This
+    depends on consensus results, our current client circuits, etc. Results
+    are cached until this entry's display is reset.
+    """
+    
+    # caches both to simplify the calls and to keep the type consistent until
+    # we want to reflect changes
+    if not self.cachedType:
+      if self.baseType == Category.OUTBOUND:
+        # Currently the only non-static categories are OUTBOUND vs...
+        # - EXIT since this depends on the current consensus
+        # - CIRCUIT if this is likely to belong to our guard usage
+        # - DIRECTORY if this is a single-hop circuit (directory mirror?)
+        # 
+        # The exitability, circuits, and fingerprints are all cached by the
+        # torTools util keeping this a quick lookup.
+        
+        conn = torTools.getConn()
+        destFingerprint = self.foreign.getFingerprint()
+        
+        if destFingerprint == "UNKNOWN":
+          # Not a known relay. This might be an exit connection.
+          
+          if conn.isExitingAllowed(self.foreign.getIpAddr(), self.foreign.getPort()):
+            self.cachedType = Category.EXIT
+        elif self._possibleClient or self._possibleDirectory:
+          # This belongs to a known relay. If we haven't eliminated ourselves as
+          # a possible client or directory connection then check if it still
+          # holds true.
+          
+          myCircuits = conn.getCircuits()
+          
+          if self._possibleClient:
+            # Checks that this belongs to the first hop in a circuit that's
+            # either unestablished or longer than a single hop (ie, anything but
+            # a built 1-hop connection since those are most likely a directory
+            # mirror).
+            
+            for _, status, _, path in myCircuits:
+              if path[0] == destFingerprint and (status != "BUILT" or len(path) > 1):
+                self.cachedType = Category.CIRCUIT # matched a probable guard connection
+            
+            # if we fell through, we can eliminate ourselves as a guard in the future
+            if not self.cachedType:
+              self._possibleClient = False
+          
+          if self._possibleDirectory:
+            # Checks if we match a built, single hop circuit.
+            
+            for _, status, _, path in myCircuits:
+              if path[0] == destFingerprint and status == "BUILT" and len(path) == 1:
+                self.cachedType = Category.DIRECTORY
+            
+            # if we fell through, eliminate ourselves as a directory connection
+            if not self.cachedType:
+              self._possibleDirectory = False
+      
+      if not self.cachedType:
+        self.cachedType = self.baseType
+    
+    return self.cachedType
+  
+  def getEtcContent(self, width, listingType):
+    """
+    Provides the optional content for the connection.
+    
+    Arguments:
+      width       - maximum length of the line
+      listingType - primary attribute we're listing connections by
+    """
+    
+    # for applications show the command/pid
+    if self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL):
+      displayLabel = ""
+      
+      if self.appName:
+        if self.appPid: displayLabel = "%s (%s)" % (self.appName, self.appPid)
+        else: displayLabel = self.appName
+      elif self.isAppResolving:
+        displayLabel = "resolving..."
+      else: displayLabel = "UNKNOWN"
+      
+      if len(displayLabel) < width:
+        return ("%%-%is" % width) % displayLabel
+      else: return ""
+    
+    # for everything else display connection/consensus information
+    dstAddress = self.getDestinationLabel(26, includeLocale = True)
+    etc, usedSpace = "", 0
+    if listingType == entries.ListingType.IP_ADDRESS:
+      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
+        # show fingerprint (column width: 42 characters)
+        etc += "%-40s  " % self.foreign.getFingerprint()
+        usedSpace += 42
+      
+      if width > usedSpace + 10 and CONFIG["features.connection.showColumn.nickname"]:
+        # show nickname (column width: remainder)
+        nicknameSpace = width - usedSpace
+        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
+        usedSpace += nicknameSpace + 2
+    elif listingType == entries.ListingType.HOSTNAME:
+      if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
+        # show destination ip/port/locale (column width: 28 characters)
+        etc += "%-26s  " % dstAddress
+        usedSpace += 28
+      
+      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
+        # show fingerprint (column width: 42 characters)
+        etc += "%-40s  " % self.foreign.getFingerprint()
+        usedSpace += 42
+      
+      if width > usedSpace + 17 and CONFIG["features.connection.showColumn.nickname"]:
+        # show nickname (column width: min 17 characters, uses half of the remainder)
+        nicknameSpace = 15 + (width - (usedSpace + 17)) / 2
+        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
+        usedSpace += (nicknameSpace + 2)
+    elif listingType == entries.ListingType.FINGERPRINT:
+      if width > usedSpace + 17:
+        # show nickname (column width: min 17 characters, consumes any remaining space)
+        nicknameSpace = width - usedSpace - 2
+        
+        # if there's room then also show a column with the destination
+        # ip/port/locale (column width: 28 characters)
+        isIpLocaleIncluded = width > usedSpace + 45
+        isIpLocaleIncluded &= CONFIG["features.connection.showColumn.destination"]
+        if isIpLocaleIncluded: nicknameSpace -= 28
+        
+        if CONFIG["features.connection.showColumn.nickname"]:
+          nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+          etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
+          usedSpace += nicknameSpace + 2
+        
+        if isIpLocaleIncluded:
+          etc += "%-26s  " % dstAddress
+          usedSpace += 28
+    else:
+      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
+        # show fingerprint (column width: 42 characters)
+        etc += "%-40s  " % self.foreign.getFingerprint()
+        usedSpace += 42
+      
+      if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
+        # show destination ip/port/locale (column width: 28 characters)
+        etc += "%-26s  " % dstAddress
+        usedSpace += 28
+    
+    return ("%%-%is" % width) % etc
+  
+  def _getListingContent(self, width, listingType):
+    """
+    Provides the source, destination, and extra info for our listing.
+    
+    Arguments:
+      width       - maximum length of the line
+      listingType - primary attribute we're listing connections by
+    """
+    
+    conn = torTools.getConn()
+    myType = self.getType()
+    dstAddress = self.getDestinationLabel(26, includeLocale = True)
+    
+    # The required widths are the sum of the following:
+    # - room for LABEL_FORMAT and LABEL_MIN_PADDING (11 characters)
+    # - base data for the listing
+    # - that extra field plus any previous
+    
+    usedSpace = len(LABEL_FORMAT % tuple([""] * 4)) + LABEL_MIN_PADDING
+    localPort = ":%s" % self.local.getPort() if self.includePort else ""
+    
+    src, dst, etc = "", "", ""
+    if listingType == entries.ListingType.IP_ADDRESS:
+      myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr())
+      addrDiffer = myExternalIpAddr != self.local.getIpAddr()
+      
+      # Expanding doesn't make sense, if the connection isn't actually
+      # going through Tor's external IP address. As there isn't a known
+      # method for checking if it is, we're checking the type instead.
+      #
+      # This isn't entirely correct. It might be a better idea to check if
+      # the source and destination addresses are both private, but that might
+      # not be perfectly reliable either.
+      
+      isExpansionType = not myType in (Category.SOCKS, Category.HIDDEN, Category.CONTROL)
+      
+      if isExpansionType: srcAddress = myExternalIpAddr + localPort
+      else: srcAddress = self.local.getIpAddr() + localPort
+      
+      if myType in (Category.SOCKS, Category.CONTROL):
+        # Like inbound connections these need their source and destination to
+        # be swapped. However, this only applies when listing by IP or hostname
+        # (their fingerprint and nickname are both for us). Reversing the
+        # fields here to keep the same column alignments.
+        
+        src = "%-21s" % dstAddress
+        dst = "%-26s" % srcAddress
+      else:
+        src = "%-21s" % srcAddress # ip:port = max of 21 characters
+        dst = "%-26s" % dstAddress # ip:port (xx) = max of 26 characters
+      
+      usedSpace += len(src) + len(dst) # base data requires 47 characters
+      
+      # Showing the fingerprint (which has the width of 42) has priority over
+      # an expanded address field. Hence check if we either have space for
+      # both or wouldn't be showing the fingerprint regardless.
+      
+      isExpandedAddrVisible = width > usedSpace + 28
+      if isExpandedAddrVisible and CONFIG["features.connection.showColumn.fingerprint"]:
+        isExpandedAddrVisible = width < usedSpace + 42 or width > usedSpace + 70
+      
+      if addrDiffer and isExpansionType and isExpandedAddrVisible and self.includeExpandedIpAddr and CONFIG["features.connection.showColumn.expandedIp"]:
+        # include the internal address in the src (extra 28 characters)
+        internalAddress = self.local.getIpAddr() + localPort
+        
+        # If this is an inbound connection then reverse ordering so it's:
+        # <foreign> --> <external> --> <internal>
+        # when the src and dst are swapped later
+        
+        if myType == Category.INBOUND: src = "%-21s  -->  %s" % (src, internalAddress)
+        else: src = "%-21s  -->  %s" % (internalAddress, src)
+        
+        usedSpace += 28
+      
+      etc = self.getEtcContent(width - usedSpace, listingType)
+      usedSpace += len(etc)
+    elif listingType == entries.ListingType.HOSTNAME:
+      # 15 characters for source, and a min of 40 reserved for the destination
+      # TODO: when actually functional the src and dst need to be swapped for
+      # SOCKS and CONTROL connections
+      src = "localhost%-6s" % localPort
+      usedSpace += len(src)
+      minHostnameSpace = 40
+      
+      etc = self.getEtcContent(width - usedSpace - minHostnameSpace, listingType)
+      usedSpace += len(etc)
+      
+      hostnameSpace = width - usedSpace
+      usedSpace = width # prevents padding at the end
+      if self.isPrivate():
+        dst = ("%%-%is" % hostnameSpace) % "<scrubbed>"
+      else:
+        hostname = self.foreign.getHostname(self.foreign.getIpAddr())
+        portLabel = ":%-5s" % self.foreign.getPort() if self.includePort else ""
+        
+        # truncates long hostnames and sets dst to <hostname>:<port>
+        hostname = uiTools.cropStr(hostname, hostnameSpace, 0)
+        dst = ("%%-%is" % hostnameSpace) % (hostname + portLabel)
+    elif listingType == entries.ListingType.FINGERPRINT:
+      src = "localhost"
+      if myType == Category.CONTROL: dst = "localhost"
+      else: dst = self.foreign.getFingerprint()
+      dst = "%-40s" % dst
+      
+      usedSpace += len(src) + len(dst) # base data requires 49 characters
+      
+      etc = self.getEtcContent(width - usedSpace, listingType)
+      usedSpace += len(etc)
+    else:
+      # base data requires 50 min characters
+      src = self.local.getNickname()
+      if myType == Category.CONTROL: dst = self.local.getNickname()
+      else: dst = self.foreign.getNickname()
+      minBaseSpace = 50
+      
+      etc = self.getEtcContent(width - usedSpace - minBaseSpace, listingType)
+      usedSpace += len(etc)
+      
+      baseSpace = width - usedSpace
+      usedSpace = width # prevents padding at the end
+      
+      if len(src) + len(dst) > baseSpace:
+        src = uiTools.cropStr(src, baseSpace / 3)
+        dst = uiTools.cropStr(dst, baseSpace - len(src))
+      
+      # pads dst entry to its max space
+      dst = ("%%-%is" % (baseSpace - len(src))) % dst
+    
+    if myType == Category.INBOUND: src, dst = dst, src
+    padding = " " * (width - usedSpace + LABEL_MIN_PADDING)
+    return LABEL_FORMAT % (src, dst, etc, padding)
+  
+  def _getDetailContent(self, width):
+    """
+    Provides a list with detailed information for this connection.
+    
+    Arguments:
+      width - max length of lines
+    """
+    
+    lines = [""] * 7
+    lines[0] = "address: %s" % self.getDestinationLabel(width - 11)
+    lines[1] = "locale: %s" % ("??" if self.isPrivate() else self.foreign.getLocale("??"))
+    
+    # Remaining data concerns the consensus results, with three possible cases:
+    # - if there's a single match then display its details
+    # - if there's multiple potential relays then list all of the combinations
+    #   of ORPorts / Fingerprints
+    # - if no consensus data is available then say so (probably a client or
+    #   exit connection)
+    
+    fingerprint = self.foreign.getFingerprint()
+    conn = torTools.getConn()
+    
+    if fingerprint != "UNKNOWN":
+      # single match - display information available about it
+      nsEntry = conn.getConsensusEntry(fingerprint)
+      descEntry = conn.getDescriptorEntry(fingerprint)
+      
+      # append the fingerprint to the second line
+      lines[1] = "%-13sfingerprint: %s" % (lines[1], fingerprint)
+      
+      if nsEntry:
+        # example consensus entry:
+        # r murble R8sCM1ar1sS2GulQYFVmvN95xsk RJr6q+wkTFG+ng5v2bdCbVVFfA4 2011-02-21 00:25:32 195.43.157.85 443 0
+        # s Exit Fast Guard Named Running Stable Valid
+        # w Bandwidth=2540
+        # p accept 20-23,43,53,79-81,88,110,143,194,443
+        
+        nsLines = nsEntry.split("\n")
+        
+        firstLineComp = nsLines[0].split(" ")
+        if len(firstLineComp) >= 9:
+          _, nickname, _, _, pubDate, pubTime, _, orPort, dirPort = firstLineComp[:9]
+        else: nickname, pubDate, pubTime, orPort, dirPort = "", "", "", "", ""
+        
+        flags = "unknown"
+        if len(nsLines) >= 2 and nsLines[1].startswith("s "):
+          flags = nsLines[1][2:]
+        
+        # The network status exit policy doesn't exist for older tor versions.
+        # If unavailable we'll need the full exit policy which is on the
+        # descriptor (if that's available).
+        
+        exitPolicy = "unknown"
+        if len(nsLines) >= 4 and nsLines[3].startswith("p "):
+          exitPolicy = nsLines[3][2:].replace(",", ", ")
+        elif descEntry:
+          # the descriptor has an individual line for each entry in the exit policy
+          exitPolicyEntries = []
+          
+          for line in descEntry.split("\n"):
+            if line.startswith("accept") or line.startswith("reject"):
+              exitPolicyEntries.append(line.strip())
+          
+          exitPolicy = ", ".join(exitPolicyEntries)
+        
+        dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort
+        lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel)
+        lines[3] = "published: %s %s" % (pubDate, pubTime)
+        lines[4] = "flags: %s" % flags.replace(" ", ", ")
+        lines[5] = "exit policy: %s" % exitPolicy
+      
+      if descEntry:
+        torVersion, platform, contact = "", "", ""
+        
+        for descLine in descEntry.split("\n"):
+          if descLine.startswith("platform"):
+            # has the tor version and platform, ex:
+            # platform Tor 0.2.1.29 (r318f470bc5f2ad43) on Linux x86_64
+            
+            torVersion = descLine[13:descLine.find(" ", 13)]
+            platform = descLine[descLine.rfind(" on ") + 4:]
+          elif descLine.startswith("contact"):
+            contact = descLine[8:]
+            
+            # clears up some highly common obscuring
+            for alias in (" at ", " AT "): contact = contact.replace(alias, "@")
+            for alias in (" dot ", " DOT "): contact = contact.replace(alias, ".")
+            
+            break # contact lines come after the platform
+        
+        lines[3] = "%-35s os: %-14s version: %s" % (lines[3], platform, torVersion)
+        
+        # contact information is an optional field
+        if contact: lines[6] = "contact: %s" % contact
+    else:
+      allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True)
+      
+      if allMatches:
+        # multiple matches
+        lines[2] = "Multiple matches, possible fingerprints are:"
+        
+        for i in range(len(allMatches)):
+          isLastLine = i == 3
+          
+          relayPort, relayFingerprint = allMatches[i]
+          lineText = "%i. or port: %-5s fingerprint: %s" % (i, relayPort, relayFingerprint)
+          
+          # if there's multiple lines remaining at the end then give a count
+          remainingRelays = len(allMatches) - i
+          if isLastLine and remainingRelays > 1:
+            lineText = "... %i more" % remainingRelays
+          
+          lines[3 + i] = lineText
+          
+          if isLastLine: break
+      else:
+        # no consensus entry for this ip address
+        lines[2] = "No consensus data found"
+    
+    # crops any lines that are too long
+    for i in range(len(lines)):
+      lines[i] = uiTools.cropStr(lines[i], width - 2)
+    
+    return lines
+  
+  def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False):
+    """
+    Provides a short description of the destination. This is made up of two
+    components, the base <ip addr>:<port> and an extra piece of information in
+    parentheses. The IP address is scrubbed from private connections.
+    
+    Extra information is...
+    - the port's purpose for exit connections
+    - the locale and/or hostname if set to do so, the address isn't private,
+      and isn't on the local network
+    - nothing otherwise
+    
+    Arguments:
+      maxLength       - maximum length of the string returned
+      includeLocale   - possibly includes the locale
+      includeHostname - possibly includes the hostname
+    """
+    
+    # the port and port derived data can be hidden by config or without includePort
+    includePort = self.includePort and (CONFIG["features.connection.showExitPort"] or self.getType() != Category.EXIT)
+    
+    # destination of the connection
+    ipLabel = "<scrubbed>" if self.isPrivate() else self.foreign.getIpAddr()
+    portLabel = ":%s" % self.foreign.getPort() if includePort else ""
+    dstAddress = ipLabel + portLabel
+    
+    # Only append the extra info if there's at least a couple characters of
+    # space (this is what's needed for the country codes).
+    if len(dstAddress) + 5 <= maxLength:
+      spaceAvailable = maxLength - len(dstAddress) - 3
+      
+      if self.getType() == Category.EXIT and includePort:
+        purpose = connections.getPortUsage(self.foreign.getPort())
+        
+        if purpose:
+          # BitTorrent is a common protocol to truncate, so just use "Torrent"
+          # if there's not enough room.
+          if len(purpose) > spaceAvailable and purpose == "BitTorrent":
+            purpose = "Torrent"
+          
+          # crops with a hyphen if too long
+          purpose = uiTools.cropStr(purpose, spaceAvailable, endType = uiTools.Ending.HYPHEN)
+          
+          dstAddress += " (%s)" % purpose
+      elif not connections.isIpAddressPrivate(self.foreign.getIpAddr()):
+        extraInfo = []
+        
+        if includeLocale:
+          foreignLocale = self.foreign.getLocale("??")
+          extraInfo.append(foreignLocale)
+          spaceAvailable -= len(foreignLocale) + 2
+        
+        if includeHostname:
+          dstHostname = self.foreign.getHostname()
+          
+          if dstHostname:
+            # determines the full space available, taking into account the ", "
+            # dividers if there's multiple pieces of extra data
+            
+            maxHostnameSpace = spaceAvailable - 2 * len(extraInfo)
+            dstHostname = uiTools.cropStr(dstHostname, maxHostnameSpace)
+            extraInfo.append(dstHostname)
+            spaceAvailable -= len(dstHostname)
+        
+        if extraInfo:
+          dstAddress += " (%s)" % ", ".join(extraInfo)
+    
+    return dstAddress[:maxLength]
+

Deleted: arm/release/src/interface/connections/connPanel.py
===================================================================
--- arm/trunk/src/interface/connections/connPanel.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/connections/connPanel.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -1,398 +0,0 @@
-"""
-Listing of the currently established connections tor has made.
-"""
-
-import time
-import curses
-import threading
-
-from interface.connections import entries, connEntry, circEntry
-from util import connections, enum, panel, torTools, uiTools
-
-DEFAULT_CONFIG = {"features.connection.resolveApps": True,
-                  "features.connection.listingType": 0,
-                  "features.connection.refreshRate": 5}
-
-# height of the detail panel content, not counting top and bottom border
-DETAILS_HEIGHT = 7
-
-# listing types
-Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
-
-DEFAULT_SORT_ORDER = (entries.SortAttr.CATEGORY, entries.SortAttr.LISTING, entries.SortAttr.UPTIME)
-
-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, "conn", 0)
-    threading.Thread.__init__(self)
-    self.setDaemon(True)
-    
-    self._sortOrdering = DEFAULT_SORT_ORDER
-    self._config = dict(DEFAULT_CONFIG)
-    
-    if config:
-      config.update(self._config, {
-        "features.connection.listingType": (0, len(Listing.values()) - 1),
-        "features.connection.refreshRate": 1})
-      
-      sortFields = entries.SortAttr.values()
-      customOrdering = config.getIntCSV("features.connection.order", None, 3, 0, len(sortFields))
-      
-      if customOrdering:
-        self._sortOrdering = [sortFields[i] for i in customOrdering]
-    
-    self._listingType = Listing.values()[self._config["features.connection.listingType"]]
-    self._scroller = uiTools.Scroller(True)
-    self._title = "Connections:" # title line of the panel
-    self._entries = []          # last fetched display entries
-    self._entryLines = []       # individual lines rendered from the entries listing
-    self._showDetails = False   # presents the details panel if true
-    
-    self._lastUpdate = -1       # time the content was last revised
-    self._isTorRunning = True   # indicates if tor is currently running or not
-    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
-    self.valsLock = threading.RLock()
-    
-    # Last sampling received from the ConnectionResolver, used to detect when
-    # it changes.
-    self._lastResourceFetch = -1
-    
-    # resolver for the command/pid associated with SOCKS, HIDDEN, and CONTROL connections
-    self._appResolver = connections.AppResolver("arm")
-    
-    # rate limits appResolver queries to once per update
-    self.appResolveSinceUpdate = False
-    
-    self._update()            # populates initial entries
-    self._resolveApps(False)  # resolves initial applications
-    
-    # mark the initially exitsing connection uptimes as being estimates
-    for entry in self._entries:
-      if isinstance(entry, connEntry.ConnectionEntry):
-        entry.getLines()[0].isInitialConnection = True
-    
-    # listens for when tor stops so we know to stop reflecting changes
-    torTools.getConn().addStatusListener(self.torStateListener)
-  
-  def torStateListener(self, conn, eventType):
-    """
-    Freezes the connection contents when Tor stops.
-    
-    Arguments:
-      conn      - tor controller
-      eventType - type of event detected
-    """
-    
-    self._isTorRunning = eventType == torTools.State.INIT
-    
-    if self._isPaused or not self._isTorRunning:
-      if not self._pauseTime: self._pauseTime = time.time()
-    else: self._pauseTime = None
-    
-    self.redraw(True)
-  
-  def setPaused(self, isPause):
-    """
-    If true, prevents the panel from updating.
-    """
-    
-    if not self._isPaused == isPause:
-      self._isPaused = isPause
-      
-      if isPause or not self._isTorRunning:
-        if not self._pauseTime: 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 setSortOrder(self, ordering = None):
-    """
-    Sets the connection attributes we're sorting by and resorts the contents.
-    
-    Arguments:
-      ordering - new ordering, if undefined then this resorts with the last
-                 set ordering
-    """
-    
-    self.valsLock.acquire()
-    if ordering: self._sortOrdering = ordering
-    self._entries.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType)))
-    
-    self._entryLines = []
-    for entry in self._entries:
-      self._entryLines += entry.getLines()
-    self.valsLock.release()
-  
-  def setListingType(self, listingType):
-    """
-    Sets the priority information presented by the panel.
-    
-    Arguments:
-      listingType - Listing instance for the primary information to be shown
-    """
-    
-    self.valsLock.acquire()
-    self._listingType = listingType
-    
-    # if we're sorting by the listing then we need to resort
-    if entries.SortAttr.LISTING in self._sortOrdering:
-      self.setSortOrder()
-    
-    self.valsLock.release()
-  
-  def handleKey(self, key):
-    self.valsLock.acquire()
-    
-    if uiTools.isScrollKey(key):
-      pageHeight = self.getPreferredSize()[0] - 1
-      if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1)
-      isChanged = self._scroller.handleKey(key, self._entryLines, pageHeight)
-      if isChanged: self.redraw(True)
-    elif uiTools.isSelectionKey(key):
-      self._showDetails = not self._showDetails
-      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 not self._isTorRunning or currentTime - lastDraw < self._config["features.connection.refreshRate"]:
-        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)
-        
-        # we may have missed multiple updates due to being paused, showing
-        # another panel, etc so lastDraw might need to jump multiple ticks
-        drawTicks = (time.time() - lastDraw) / self._config["features.connection.refreshRate"]
-        lastDraw += self._config["features.connection.refreshRate"] * drawTicks
-  
-  def draw(self, width, height):
-    self.valsLock.acquire()
-    
-    # extra line when showing the detail panel is for the bottom border
-    detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0
-    isScrollbarVisible = len(self._entryLines) > height - detailPanelOffset - 1
-    
-    scrollLoc = self._scroller.getScrollLoc(self._entryLines, height - detailPanelOffset - 1)
-    cursorSelection = self._scroller.getCursorSelection(self._entryLines)
-    
-    # draws the detail panel if currently displaying it
-    if self._showDetails:
-      # This is a solid border unless the scrollbar is visible, in which case a
-      # 'T' pipe connects the border to the bar.
-      uiTools.drawBox(self, 0, 0, width, DETAILS_HEIGHT + 2)
-      if isScrollbarVisible: self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
-      
-      drawEntries = cursorSelection.getDetails(width)
-      for i in range(min(len(drawEntries), DETAILS_HEIGHT)):
-        drawEntries[i].render(self, 1 + i, 2)
-    
-    # title label with connection counts
-    title = "Connection Details:" if self._showDetails else self._title
-    self.addstr(0, 0, title, curses.A_STANDOUT)
-    
-    scrollOffset = 1
-    if isScrollbarVisible:
-      scrollOffset = 3
-      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._entryLines), 1 + detailPanelOffset)
-    
-    currentTime = self._pauseTime if self._pauseTime else time.time()
-    for lineNum in range(scrollLoc, len(self._entryLines)):
-      entryLine = self._entryLines[lineNum]
-      
-      # if this is an unresolved SOCKS, HIDDEN, or CONTROL entry then queue up
-      # resolution for the applicaitions they belong to
-      if isinstance(entryLine, connEntry.ConnectionLine) and entryLine.isUnresolvedApp():
-        self._resolveApps()
-      
-      # hilighting if this is the selected line
-      extraFormat = curses.A_STANDOUT if entryLine == cursorSelection else curses.A_NORMAL
-      
-      drawEntry = entryLine.getListingEntry(width - scrollOffset, currentTime, self._listingType)
-      drawLine = lineNum + detailPanelOffset + 1 - scrollLoc
-      drawEntry.render(self, drawLine, scrollOffset, extraFormat)
-      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()
-    self.appResolveSinceUpdate = False
-    
-    if self._lastResourceFetch != currentResolutionCount:
-      self.valsLock.acquire()
-      
-      newEntries = [] # the new results we'll display
-      
-      # Fetches new connections and client circuits...
-      # newConnections  [(local ip, local port, foreign ip, foreign port)...]
-      # newCircuits     {circuitID => (status, purpose, path)...}
-      
-      newConnections = connResolver.getConnections()
-      newCircuits = {}
-      
-      for circuitID, status, purpose, path in torTools.getConn().getCircuits():
-        # Skips established single-hop circuits (these are for directory
-        # fetches, not client circuits)
-        if not (status == "BUILT" and len(path) == 1):
-          newCircuits[circuitID] = (status, purpose, path)
-      
-      # Populates newEntries with any of our old entries that still exist.
-      # This is both for performance and to keep from resetting the uptime
-      # attributes. Note that CircEntries are a ConnectionEntry subclass so
-      # we need to check for them first.
-      
-      for oldEntry in self._entries:
-        if isinstance(oldEntry, circEntry.CircEntry):
-          newEntry = newCircuits.get(oldEntry.circuitID)
-          
-          if newEntry:
-            oldEntry.update(newEntry[0], newEntry[2])
-            newEntries.append(oldEntry)
-            del newCircuits[oldEntry.circuitID]
-        elif isinstance(oldEntry, connEntry.ConnectionEntry):
-          connLine = oldEntry.getLines()[0]
-          connAttr = (connLine.local.getIpAddr(), connLine.local.getPort(),
-                      connLine.foreign.getIpAddr(), connLine.foreign.getPort())
-          
-          if connAttr in newConnections:
-            newEntries.append(oldEntry)
-            newConnections.remove(connAttr)
-      
-      # Reset any display attributes for the entries we're keeping
-      for entry in newEntries: entry.resetDisplay()
-      
-      # Adds any new connection and circuit entries.
-      for lIp, lPort, fIp, fPort in newConnections:
-        newConnEntry = connEntry.ConnectionEntry(lIp, lPort, fIp, fPort)
-        if newConnEntry.getLines()[0].getType() != connEntry.Category.CIRCUIT:
-          newEntries.append(newConnEntry)
-      
-      for circuitID in newCircuits:
-        status, purpose, path = newCircuits[circuitID]
-        newEntries.append(circEntry.CircEntry(circuitID, status, purpose, path))
-      
-      # Counts the relays in each of the categories. This also flushes the
-      # type cache for all of the connections (in case its changed since last
-      # fetched).
-      
-      categoryTypes = connEntry.Category.values()
-      typeCounts = dict((type, 0) for type in categoryTypes)
-      for entry in newEntries:
-        if isinstance(entry, connEntry.ConnectionEntry):
-          typeCounts[entry.getLines()[0].getType()] += 1
-        elif isinstance(entry, circEntry.CircEntry):
-          typeCounts[connEntry.Category.CIRCUIT] += 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._entries = newEntries
-      
-      self._entryLines = []
-      for entry in self._entries:
-        self._entryLines += entry.getLines()
-      
-      self.setSortOrder()
-      self._lastResourceFetch = currentResolutionCount
-      self.valsLock.release()
-  
-  def _resolveApps(self, flagQuery = True):
-    """
-    Triggers an asynchronous query for all unresolved SOCKS, HIDDEN, and
-    CONTROL entries.
-    
-    Arguments:
-      flagQuery - sets a flag to prevent further call from being respected
-                  until the next update if true
-    """
-    
-    if self.appResolveSinceUpdate or not self._config["features.connection.resolveApps"]: return
-    unresolvedLines = [l for l in self._entryLines if isinstance(l, connEntry.ConnectionLine) and l.isUnresolvedApp()]
-    
-    # get the ports used for unresolved applications
-    appPorts = []
-    
-    for line in unresolvedLines:
-      appConn = line.local if line.getType() == connEntry.Category.HIDDEN else line.foreign
-      appPorts.append(appConn.getPort())
-    
-    # Queue up resolution for the unresolved ports (skips if it's still working
-    # on the last query).
-    if appPorts and not self._appResolver.isResolving:
-      self._appResolver.resolve(appPorts)
-    
-    # Fetches results. If the query finishes quickly then this is what we just
-    # asked for, otherwise these belong to an earlier resolution.
-    #
-    # The application resolver might have given up querying (for instance, if
-    # the lsof lookups aren't working on this platform or lacks permissions).
-    # The isAppResolving flag lets the unresolved entries indicate if there's
-    # a lookup in progress for them or not.
-    
-    appResults = self._appResolver.getResults(0.2)
-    
-    for line in unresolvedLines:
-      isLocal = line.getType() == connEntry.Category.HIDDEN
-      linePort = line.local.getPort() if isLocal else line.foreign.getPort()
-      
-      if linePort in appResults:
-        # sets application attributes if there's a result with this as the
-        # inbound port
-        for inboundPort, outboundPort, cmd, pid in appResults[linePort]:
-          appPort = outboundPort if isLocal else inboundPort
-          
-          if linePort == appPort:
-            line.appName = cmd
-            line.appPid = pid
-            line.isAppResolving = False
-      else:
-        line.isAppResolving = self._appResolver.isResolving
-    
-    if flagQuery:
-      self.appResolveSinceUpdate = True
-

Copied: arm/release/src/interface/connections/connPanel.py (from rev 24554, arm/trunk/src/interface/connections/connPanel.py)
===================================================================
--- arm/release/src/interface/connections/connPanel.py	                        (rev 0)
+++ arm/release/src/interface/connections/connPanel.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -0,0 +1,398 @@
+"""
+Listing of the currently established connections tor has made.
+"""
+
+import time
+import curses
+import threading
+
+from interface.connections import entries, connEntry, circEntry
+from util import connections, enum, panel, torTools, uiTools
+
+DEFAULT_CONFIG = {"features.connection.resolveApps": True,
+                  "features.connection.listingType": 0,
+                  "features.connection.refreshRate": 5}
+
+# height of the detail panel content, not counting top and bottom border
+DETAILS_HEIGHT = 7
+
+# listing types
+Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
+
+DEFAULT_SORT_ORDER = (entries.SortAttr.CATEGORY, entries.SortAttr.LISTING, entries.SortAttr.UPTIME)
+
+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, "conn", 0)
+    threading.Thread.__init__(self)
+    self.setDaemon(True)
+    
+    self._sortOrdering = DEFAULT_SORT_ORDER
+    self._config = dict(DEFAULT_CONFIG)
+    
+    if config:
+      config.update(self._config, {
+        "features.connection.listingType": (0, len(Listing.values()) - 1),
+        "features.connection.refreshRate": 1})
+      
+      sortFields = entries.SortAttr.values()
+      customOrdering = config.getIntCSV("features.connection.order", None, 3, 0, len(sortFields))
+      
+      if customOrdering:
+        self._sortOrdering = [sortFields[i] for i in customOrdering]
+    
+    self._listingType = Listing.values()[self._config["features.connection.listingType"]]
+    self._scroller = uiTools.Scroller(True)
+    self._title = "Connections:" # title line of the panel
+    self._entries = []          # last fetched display entries
+    self._entryLines = []       # individual lines rendered from the entries listing
+    self._showDetails = False   # presents the details panel if true
+    
+    self._lastUpdate = -1       # time the content was last revised
+    self._isTorRunning = True   # indicates if tor is currently running or not
+    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
+    self.valsLock = threading.RLock()
+    
+    # Last sampling received from the ConnectionResolver, used to detect when
+    # it changes.
+    self._lastResourceFetch = -1
+    
+    # resolver for the command/pid associated with SOCKS, HIDDEN, and CONTROL connections
+    self._appResolver = connections.AppResolver("arm")
+    
+    # rate limits appResolver queries to once per update
+    self.appResolveSinceUpdate = False
+    
+    self._update()            # populates initial entries
+    self._resolveApps(False)  # resolves initial applications
+    
+    # mark the initially exitsing connection uptimes as being estimates
+    for entry in self._entries:
+      if isinstance(entry, connEntry.ConnectionEntry):
+        entry.getLines()[0].isInitialConnection = True
+    
+    # listens for when tor stops so we know to stop reflecting changes
+    torTools.getConn().addStatusListener(self.torStateListener)
+  
+  def torStateListener(self, conn, eventType):
+    """
+    Freezes the connection contents when Tor stops.
+    
+    Arguments:
+      conn      - tor controller
+      eventType - type of event detected
+    """
+    
+    self._isTorRunning = eventType == torTools.State.INIT
+    
+    if self._isPaused or not self._isTorRunning:
+      if not self._pauseTime: self._pauseTime = time.time()
+    else: self._pauseTime = None
+    
+    self.redraw(True)
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents the panel from updating.
+    """
+    
+    if not self._isPaused == isPause:
+      self._isPaused = isPause
+      
+      if isPause or not self._isTorRunning:
+        if not self._pauseTime: 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 setSortOrder(self, ordering = None):
+    """
+    Sets the connection attributes we're sorting by and resorts the contents.
+    
+    Arguments:
+      ordering - new ordering, if undefined then this resorts with the last
+                 set ordering
+    """
+    
+    self.valsLock.acquire()
+    if ordering: self._sortOrdering = ordering
+    self._entries.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType)))
+    
+    self._entryLines = []
+    for entry in self._entries:
+      self._entryLines += entry.getLines()
+    self.valsLock.release()
+  
+  def setListingType(self, listingType):
+    """
+    Sets the priority information presented by the panel.
+    
+    Arguments:
+      listingType - Listing instance for the primary information to be shown
+    """
+    
+    self.valsLock.acquire()
+    self._listingType = listingType
+    
+    # if we're sorting by the listing then we need to resort
+    if entries.SortAttr.LISTING in self._sortOrdering:
+      self.setSortOrder()
+    
+    self.valsLock.release()
+  
+  def handleKey(self, key):
+    self.valsLock.acquire()
+    
+    if uiTools.isScrollKey(key):
+      pageHeight = self.getPreferredSize()[0] - 1
+      if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1)
+      isChanged = self._scroller.handleKey(key, self._entryLines, pageHeight)
+      if isChanged: self.redraw(True)
+    elif uiTools.isSelectionKey(key):
+      self._showDetails = not self._showDetails
+      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 not self._isTorRunning or currentTime - lastDraw < self._config["features.connection.refreshRate"]:
+        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)
+        
+        # we may have missed multiple updates due to being paused, showing
+        # another panel, etc so lastDraw might need to jump multiple ticks
+        drawTicks = (time.time() - lastDraw) / self._config["features.connection.refreshRate"]
+        lastDraw += self._config["features.connection.refreshRate"] * drawTicks
+  
+  def draw(self, width, height):
+    self.valsLock.acquire()
+    
+    # extra line when showing the detail panel is for the bottom border
+    detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0
+    isScrollbarVisible = len(self._entryLines) > height - detailPanelOffset - 1
+    
+    scrollLoc = self._scroller.getScrollLoc(self._entryLines, height - detailPanelOffset - 1)
+    cursorSelection = self._scroller.getCursorSelection(self._entryLines)
+    
+    # draws the detail panel if currently displaying it
+    if self._showDetails:
+      # This is a solid border unless the scrollbar is visible, in which case a
+      # 'T' pipe connects the border to the bar.
+      uiTools.drawBox(self, 0, 0, width, DETAILS_HEIGHT + 2)
+      if isScrollbarVisible: self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
+      
+      drawEntries = cursorSelection.getDetails(width)
+      for i in range(min(len(drawEntries), DETAILS_HEIGHT)):
+        drawEntries[i].render(self, 1 + i, 2)
+    
+    # title label with connection counts
+    title = "Connection Details:" if self._showDetails else self._title
+    self.addstr(0, 0, title, curses.A_STANDOUT)
+    
+    scrollOffset = 1
+    if isScrollbarVisible:
+      scrollOffset = 3
+      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._entryLines), 1 + detailPanelOffset)
+    
+    currentTime = self._pauseTime if self._pauseTime else time.time()
+    for lineNum in range(scrollLoc, len(self._entryLines)):
+      entryLine = self._entryLines[lineNum]
+      
+      # if this is an unresolved SOCKS, HIDDEN, or CONTROL entry then queue up
+      # resolution for the applicaitions they belong to
+      if isinstance(entryLine, connEntry.ConnectionLine) and entryLine.isUnresolvedApp():
+        self._resolveApps()
+      
+      # hilighting if this is the selected line
+      extraFormat = curses.A_STANDOUT if entryLine == cursorSelection else curses.A_NORMAL
+      
+      drawEntry = entryLine.getListingEntry(width - scrollOffset, currentTime, self._listingType)
+      drawLine = lineNum + detailPanelOffset + 1 - scrollLoc
+      drawEntry.render(self, drawLine, scrollOffset, extraFormat)
+      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()
+    self.appResolveSinceUpdate = False
+    
+    if self._lastResourceFetch != currentResolutionCount:
+      self.valsLock.acquire()
+      
+      newEntries = [] # the new results we'll display
+      
+      # Fetches new connections and client circuits...
+      # newConnections  [(local ip, local port, foreign ip, foreign port)...]
+      # newCircuits     {circuitID => (status, purpose, path)...}
+      
+      newConnections = connResolver.getConnections()
+      newCircuits = {}
+      
+      for circuitID, status, purpose, path in torTools.getConn().getCircuits():
+        # Skips established single-hop circuits (these are for directory
+        # fetches, not client circuits)
+        if not (status == "BUILT" and len(path) == 1):
+          newCircuits[circuitID] = (status, purpose, path)
+      
+      # Populates newEntries with any of our old entries that still exist.
+      # This is both for performance and to keep from resetting the uptime
+      # attributes. Note that CircEntries are a ConnectionEntry subclass so
+      # we need to check for them first.
+      
+      for oldEntry in self._entries:
+        if isinstance(oldEntry, circEntry.CircEntry):
+          newEntry = newCircuits.get(oldEntry.circuitID)
+          
+          if newEntry:
+            oldEntry.update(newEntry[0], newEntry[2])
+            newEntries.append(oldEntry)
+            del newCircuits[oldEntry.circuitID]
+        elif isinstance(oldEntry, connEntry.ConnectionEntry):
+          connLine = oldEntry.getLines()[0]
+          connAttr = (connLine.local.getIpAddr(), connLine.local.getPort(),
+                      connLine.foreign.getIpAddr(), connLine.foreign.getPort())
+          
+          if connAttr in newConnections:
+            newEntries.append(oldEntry)
+            newConnections.remove(connAttr)
+      
+      # Reset any display attributes for the entries we're keeping
+      for entry in newEntries: entry.resetDisplay()
+      
+      # Adds any new connection and circuit entries.
+      for lIp, lPort, fIp, fPort in newConnections:
+        newConnEntry = connEntry.ConnectionEntry(lIp, lPort, fIp, fPort)
+        if newConnEntry.getLines()[0].getType() != connEntry.Category.CIRCUIT:
+          newEntries.append(newConnEntry)
+      
+      for circuitID in newCircuits:
+        status, purpose, path = newCircuits[circuitID]
+        newEntries.append(circEntry.CircEntry(circuitID, status, purpose, path))
+      
+      # Counts the relays in each of the categories. This also flushes the
+      # type cache for all of the connections (in case its changed since last
+      # fetched).
+      
+      categoryTypes = connEntry.Category.values()
+      typeCounts = dict((type, 0) for type in categoryTypes)
+      for entry in newEntries:
+        if isinstance(entry, connEntry.ConnectionEntry):
+          typeCounts[entry.getLines()[0].getType()] += 1
+        elif isinstance(entry, circEntry.CircEntry):
+          typeCounts[connEntry.Category.CIRCUIT] += 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._entries = newEntries
+      
+      self._entryLines = []
+      for entry in self._entries:
+        self._entryLines += entry.getLines()
+      
+      self.setSortOrder()
+      self._lastResourceFetch = currentResolutionCount
+      self.valsLock.release()
+  
+  def _resolveApps(self, flagQuery = True):
+    """
+    Triggers an asynchronous query for all unresolved SOCKS, HIDDEN, and
+    CONTROL entries.
+    
+    Arguments:
+      flagQuery - sets a flag to prevent further call from being respected
+                  until the next update if true
+    """
+    
+    if self.appResolveSinceUpdate or not self._config["features.connection.resolveApps"]: return
+    unresolvedLines = [l for l in self._entryLines if isinstance(l, connEntry.ConnectionLine) and l.isUnresolvedApp()]
+    
+    # get the ports used for unresolved applications
+    appPorts = []
+    
+    for line in unresolvedLines:
+      appConn = line.local if line.getType() == connEntry.Category.HIDDEN else line.foreign
+      appPorts.append(appConn.getPort())
+    
+    # Queue up resolution for the unresolved ports (skips if it's still working
+    # on the last query).
+    if appPorts and not self._appResolver.isResolving:
+      self._appResolver.resolve(appPorts)
+    
+    # Fetches results. If the query finishes quickly then this is what we just
+    # asked for, otherwise these belong to an earlier resolution.
+    #
+    # The application resolver might have given up querying (for instance, if
+    # the lsof lookups aren't working on this platform or lacks permissions).
+    # The isAppResolving flag lets the unresolved entries indicate if there's
+    # a lookup in progress for them or not.
+    
+    appResults = self._appResolver.getResults(0.2)
+    
+    for line in unresolvedLines:
+      isLocal = line.getType() == connEntry.Category.HIDDEN
+      linePort = line.local.getPort() if isLocal else line.foreign.getPort()
+      
+      if linePort in appResults:
+        # sets application attributes if there's a result with this as the
+        # inbound port
+        for inboundPort, outboundPort, cmd, pid in appResults[linePort]:
+          appPort = outboundPort if isLocal else inboundPort
+          
+          if linePort == appPort:
+            line.appName = cmd
+            line.appPid = pid
+            line.isAppResolving = False
+      else:
+        line.isAppResolving = self._appResolver.isResolving
+    
+    if flagQuery:
+      self.appResolveSinceUpdate = True
+

Deleted: arm/release/src/interface/connections/entries.py
===================================================================
--- arm/trunk/src/interface/connections/entries.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/connections/entries.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -1,183 +0,0 @@
-"""
-Interface for entries in the connection panel. These consist of two parts: the
-entry itself (ie, Tor connection, client circuit, etc) and the lines it
-consists of in the listing.
-"""
-
-from util import enum
-
-# attributes we can list entries by
-ListingType = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
-
-SortAttr = enum.Enum("CATEGORY", "UPTIME", "LISTING", "IP_ADDRESS", "PORT",
-                     "HOSTNAME", "FINGERPRINT", "NICKNAME", "COUNTRY")
-
-SORT_COLORS = {SortAttr.CATEGORY: "red",      SortAttr.UPTIME: "yellow",
-               SortAttr.LISTING: "green",     SortAttr.IP_ADDRESS: "blue",
-               SortAttr.PORT: "blue",         SortAttr.HOSTNAME: "magenta",
-               SortAttr.FINGERPRINT: "cyan",  SortAttr.NICKNAME: "cyan",
-               SortAttr.COUNTRY: "blue"}
-
-# maximum number of ports a system can have
-PORT_COUNT = 65536
-
-class ConnectionPanelEntry:
-  """
-  Common parent for connection panel entries. This consists of a list of lines
-  in the panel listing. This caches results until the display indicates that
-  they should be flushed.
-  """
-  
-  def __init__(self):
-    self.lines = []
-    self.flushCache = True
-  
-  def getLines(self):
-    """
-    Provides the individual lines in the connection listing.
-    """
-    
-    if self.flushCache:
-      self.lines = self._getLines(self.lines)
-      self.flushCache = False
-    
-    return self.lines
-  
-  def _getLines(self, oldResults):
-    # implementation of getLines
-    
-    for line in oldResults:
-      line.resetDisplay()
-    
-    return oldResults
-  
-  def getSortValues(self, sortAttrs, listingType):
-    """
-    Provides the value used in comparisons to sort based on the given
-    attribute.
-    
-    Arguments:
-      sortAttrs   - list of SortAttr values for the field being sorted on
-      listingType - ListingType enumeration for the attribute we're listing
-                    entries by
-    """
-    
-    return [self.getSortValue(attr, listingType) for attr in sortAttrs]
-  
-  def getSortValue(self, attr, listingType):
-    """
-    Provides the value of a single attribute used for sorting purposes.
-    
-    Arguments:
-      attr        - list of SortAttr values for the field being sorted on
-      listingType - ListingType enumeration for the attribute we're listing
-                    entries by
-    """
-    
-    if attr == SortAttr.LISTING:
-      if listingType == ListingType.IP_ADDRESS:
-        # uses the IP address as the primary value, and port as secondary
-        sortValue = self.getSortValue(SortAttr.IP_ADDRESS, listingType) * PORT_COUNT
-        sortValue += self.getSortValue(SortAttr.PORT, listingType)
-        return sortValue
-      elif listingType == ListingType.HOSTNAME:
-        return self.getSortValue(SortAttr.HOSTNAME, listingType)
-      elif listingType == ListingType.FINGERPRINT:
-        return self.getSortValue(SortAttr.FINGERPRINT, listingType)
-      elif listingType == ListingType.NICKNAME:
-        return self.getSortValue(SortAttr.NICKNAME, listingType)
-    
-    return ""
-  
-  def resetDisplay(self):
-    """
-    Flushes cached display results.
-    """
-    
-    self.flushCache = True
-
-class ConnectionPanelLine:
-  """
-  Individual line in the connection panel listing.
-  """
-  
-  def __init__(self):
-    # cache for displayed information
-    self._listingCache = None
-    self._listingCacheArgs = (None, None)
-    
-    self._detailsCache = None
-    self._detailsCacheArgs = None
-    
-    self._descriptorCache = None
-    self._descriptorCacheArgs = None
-  
-  def getListingEntry(self, width, currentTime, listingType):
-    """
-    Provides a DrawEntry instance for contents to be displayed in the
-    connection panel listing.
-    
-    Arguments:
-      width       - available space to display in
-      currentTime - unix timestamp for what the results should consider to be
-                    the current time (this may be ignored due to caching)
-      listingType - ListingType enumeration for the highest priority content
-                    to be displayed
-    """
-    
-    if self._listingCacheArgs != (width, listingType):
-      self._listingCache = self._getListingEntry(width, currentTime, listingType)
-      self._listingCacheArgs = (width, listingType)
-    
-    return self._listingCache
-  
-  def _getListingEntry(self, width, currentTime, listingType):
-    # implementation of getListingEntry
-    return None
-  
-  def getDetails(self, width):
-    """
-    Provides a list of DrawEntry instances with detailed information for this
-    connection.
-    
-    Arguments:
-      width - available space to display in
-    """
-    
-    if self._detailsCacheArgs != width:
-      self._detailsCache = self._getDetails(width)
-      self._detailsCacheArgs = width
-    
-    return self._detailsCache
-  
-  def _getDetails(self, width):
-    # implementation of getDetails
-    return []
-  
-  def getDescriptor(self, width):
-    """
-    Provides a list of DrawEntry instances with descriptor information for
-    this connection.
-    
-    Arguments:
-      width - available space to display in
-    """
-    
-    if self._descriptorCacheArgs != width:
-      self._descriptorCache = self._getDescriptor(width)
-      self._descriptorCacheArgs = width
-    
-    return self._descriptorCache
-  
-  def _getDescriptor(self, width):
-    # implementation of getDescriptor
-    return []
-  
-  def resetDisplay(self):
-    """
-    Flushes cached display results.
-    """
-    
-    self._listingCacheArgs = (None, None)
-    self._detailsCacheArgs = None
-

Copied: arm/release/src/interface/connections/entries.py (from rev 24554, arm/trunk/src/interface/connections/entries.py)
===================================================================
--- arm/release/src/interface/connections/entries.py	                        (rev 0)
+++ arm/release/src/interface/connections/entries.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -0,0 +1,183 @@
+"""
+Interface for entries in the connection panel. These consist of two parts: the
+entry itself (ie, Tor connection, client circuit, etc) and the lines it
+consists of in the listing.
+"""
+
+from util import enum
+
+# attributes we can list entries by
+ListingType = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
+
+SortAttr = enum.Enum("CATEGORY", "UPTIME", "LISTING", "IP_ADDRESS", "PORT",
+                     "HOSTNAME", "FINGERPRINT", "NICKNAME", "COUNTRY")
+
+SORT_COLORS = {SortAttr.CATEGORY: "red",      SortAttr.UPTIME: "yellow",
+               SortAttr.LISTING: "green",     SortAttr.IP_ADDRESS: "blue",
+               SortAttr.PORT: "blue",         SortAttr.HOSTNAME: "magenta",
+               SortAttr.FINGERPRINT: "cyan",  SortAttr.NICKNAME: "cyan",
+               SortAttr.COUNTRY: "blue"}
+
+# maximum number of ports a system can have
+PORT_COUNT = 65536
+
+class ConnectionPanelEntry:
+  """
+  Common parent for connection panel entries. This consists of a list of lines
+  in the panel listing. This caches results until the display indicates that
+  they should be flushed.
+  """
+  
+  def __init__(self):
+    self.lines = []
+    self.flushCache = True
+  
+  def getLines(self):
+    """
+    Provides the individual lines in the connection listing.
+    """
+    
+    if self.flushCache:
+      self.lines = self._getLines(self.lines)
+      self.flushCache = False
+    
+    return self.lines
+  
+  def _getLines(self, oldResults):
+    # implementation of getLines
+    
+    for line in oldResults:
+      line.resetDisplay()
+    
+    return oldResults
+  
+  def getSortValues(self, sortAttrs, listingType):
+    """
+    Provides the value used in comparisons to sort based on the given
+    attribute.
+    
+    Arguments:
+      sortAttrs   - list of SortAttr values for the field being sorted on
+      listingType - ListingType enumeration for the attribute we're listing
+                    entries by
+    """
+    
+    return [self.getSortValue(attr, listingType) for attr in sortAttrs]
+  
+  def getSortValue(self, attr, listingType):
+    """
+    Provides the value of a single attribute used for sorting purposes.
+    
+    Arguments:
+      attr        - list of SortAttr values for the field being sorted on
+      listingType - ListingType enumeration for the attribute we're listing
+                    entries by
+    """
+    
+    if attr == SortAttr.LISTING:
+      if listingType == ListingType.IP_ADDRESS:
+        # uses the IP address as the primary value, and port as secondary
+        sortValue = self.getSortValue(SortAttr.IP_ADDRESS, listingType) * PORT_COUNT
+        sortValue += self.getSortValue(SortAttr.PORT, listingType)
+        return sortValue
+      elif listingType == ListingType.HOSTNAME:
+        return self.getSortValue(SortAttr.HOSTNAME, listingType)
+      elif listingType == ListingType.FINGERPRINT:
+        return self.getSortValue(SortAttr.FINGERPRINT, listingType)
+      elif listingType == ListingType.NICKNAME:
+        return self.getSortValue(SortAttr.NICKNAME, listingType)
+    
+    return ""
+  
+  def resetDisplay(self):
+    """
+    Flushes cached display results.
+    """
+    
+    self.flushCache = True
+
+class ConnectionPanelLine:
+  """
+  Individual line in the connection panel listing.
+  """
+  
+  def __init__(self):
+    # cache for displayed information
+    self._listingCache = None
+    self._listingCacheArgs = (None, None)
+    
+    self._detailsCache = None
+    self._detailsCacheArgs = None
+    
+    self._descriptorCache = None
+    self._descriptorCacheArgs = None
+  
+  def getListingEntry(self, width, currentTime, listingType):
+    """
+    Provides a DrawEntry instance for contents to be displayed in the
+    connection panel listing.
+    
+    Arguments:
+      width       - available space to display in
+      currentTime - unix timestamp for what the results should consider to be
+                    the current time (this may be ignored due to caching)
+      listingType - ListingType enumeration for the highest priority content
+                    to be displayed
+    """
+    
+    if self._listingCacheArgs != (width, listingType):
+      self._listingCache = self._getListingEntry(width, currentTime, listingType)
+      self._listingCacheArgs = (width, listingType)
+    
+    return self._listingCache
+  
+  def _getListingEntry(self, width, currentTime, listingType):
+    # implementation of getListingEntry
+    return None
+  
+  def getDetails(self, width):
+    """
+    Provides a list of DrawEntry instances with detailed information for this
+    connection.
+    
+    Arguments:
+      width - available space to display in
+    """
+    
+    if self._detailsCacheArgs != width:
+      self._detailsCache = self._getDetails(width)
+      self._detailsCacheArgs = width
+    
+    return self._detailsCache
+  
+  def _getDetails(self, width):
+    # implementation of getDetails
+    return []
+  
+  def getDescriptor(self, width):
+    """
+    Provides a list of DrawEntry instances with descriptor information for
+    this connection.
+    
+    Arguments:
+      width - available space to display in
+    """
+    
+    if self._descriptorCacheArgs != width:
+      self._descriptorCache = self._getDescriptor(width)
+      self._descriptorCacheArgs = width
+    
+    return self._descriptorCache
+  
+  def _getDescriptor(self, width):
+    # implementation of getDescriptor
+    return []
+  
+  def resetDisplay(self):
+    """
+    Flushes cached display results.
+    """
+    
+    self._listingCacheArgs = (None, None)
+    self._detailsCacheArgs = None
+

Modified: arm/release/src/interface/controller.py
===================================================================
--- arm/release/src/interface/controller.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/controller.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -24,6 +24,9 @@
 import descriptorPopup
 import fileDescriptorPopup
 
+import interface.connections.connPanel
+import interface.connections.connEntry
+import interface.connections.entries
 from util import conf, log, connections, hostnames, panel, sysTools, torConfig, torTools, uiTools
 import graphing.bandwidthStats
 import graphing.connStats
@@ -41,13 +44,17 @@
 PAGES = [
   ["graph", "log"],
   ["conn"],
+  ["conn2"],
   ["config"],
   ["torrc"]]
-PAUSEABLE = ["header", "graph", "log", "conn"]
 
+PAUSEABLE = ["header", "graph", "log", "conn", "conn2"]
+
 CONFIG = {"log.torrc.readFailed": log.WARN,
           "features.graph.type": 1,
           "features.config.prepopulateEditValues": True,
+          "features.connection.oldPanel": False,
+          "features.connection.newPanel": True,
           "queries.refreshRate.rate": 5,
           "log.torEventTypeUnrecognized": log.NOTICE,
           "features.graph.bw.prepopulate": True,
@@ -78,7 +85,7 @@
     self.msgText = msgText
     self.msgAttr = msgAttr
   
-  def draw(self, subwindow, width, height):
+  def draw(self, width, height):
     msgText = self.msgText
     msgAttr = self.msgAttr
     barTab = 2                # space between msgText and progress bar
@@ -113,7 +120,11 @@
         currentPage = self.page
         pageCount = len(PAGES)
         
-        if self.isBlindMode:
+        if not CONFIG["features.connection.newPanel"]:
+          if currentPage >= 3: currentPage -= 1
+          pageCount -= 1
+        
+        if self.isBlindMode or not CONFIG["features.connection.oldPanel"]:
           if currentPage >= 2: currentPage -= 1
           pageCount -= 1
         
@@ -240,7 +251,7 @@
       popup.recreate(stdscr, newWidth)
       
       key = 0
-      while key not in (curses.KEY_ENTER, 10, ord(' ')):
+      while not uiTools.isSelectionKey(key):
         popup.clear()
         popup.win.box()
         popup.addstr(0, 0, title, curses.A_STANDOUT)
@@ -341,7 +352,7 @@
       elif key == curses.KEY_RIGHT: cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 1)
       elif key == curses.KEY_UP: cursorLoc = max(0, cursorLoc - 4)
       elif key == curses.KEY_DOWN: cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 4)
-      elif key in (curses.KEY_ENTER, 10, ord(' ')):
+      elif uiTools.isSelectionKey(key):
         # selected entry (the ord of '10' seems needed to pick up enter)
         selection = selectionOptions[cursorLoc]
         if selection == "Cancel": break
@@ -394,7 +405,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):
   """
@@ -419,6 +430,7 @@
   config = conf.getConfig("arm")
   config.update(CONFIG)
   graphing.graphPanel.loadConfig(config)
+  interface.connections.connEntry.loadConfig(config)
   
   # adds events needed for arm functionality to the torTools REQ_EVENTS mapping
   # (they're then included with any setControllerEvents call, and log a more
@@ -471,12 +483,12 @@
     duplicateOptions, defaultOptions, mismatchLines, missingOptions = [], [], [], []
     
     for lineNum, issue, msg in corrections:
-      if issue == torConfig.VAL_DUPLICATE:
-        duplicateOptions.append("%s (line %i)" % (msg, lineNum))
-      elif issue == torConfig.VAL_IS_DEFAULT:
-        defaultOptions.append("%s (line %i)" % (msg, lineNum))
-      elif issue == torConfig.VAL_MISMATCH: mismatchLines.append(lineNum)
-      elif issue == torConfig.VAL_MISSING: missingOptions.append(msg)
+      if issue == torConfig.ValidationError.DUPLICATE:
+        duplicateOptions.append("%s (line %i)" % (msg, lineNum + 1))
+      elif issue == torConfig.ValidationError.IS_DEFAULT:
+        defaultOptions.append("%s (line %i)" % (msg, lineNum + 1))
+      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."
@@ -551,10 +563,21 @@
   # before being positioned - the following is a quick hack til rewritten
   panels["log"].setPaused(True)
   
-  panels["conn"] = connPanel.ConnPanel(stdscr, conn, isBlindMode)
+  if CONFIG["features.connection.oldPanel"]:
+    panels["conn"] = connPanel.ConnPanel(stdscr, conn, isBlindMode)
+  else:
+    panels["conn"] = panel.Panel(stdscr, "blank", 0, 0, 0)
+    PAUSEABLE.remove("conn")
+  
+  if CONFIG["features.connection.newPanel"]:
+    panels["conn2"] = interface.connections.connPanel.ConnectionPanel(stdscr, config)
+  else:
+    panels["conn2"] = panel.Panel(stdscr, "blank", 0, 0, 0)
+    PAUSEABLE.remove("conn2")
+  
   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")
@@ -577,7 +600,8 @@
   conn.add_event_listener(panels["graph"].stats["bandwidth"])
   conn.add_event_listener(panels["graph"].stats["system resources"])
   if not isBlindMode: conn.add_event_listener(panels["graph"].stats["connections"])
-  conn.add_event_listener(panels["conn"])
+  if CONFIG["features.connection.oldPanel"]:
+    conn.add_event_listener(panels["conn"])
   conn.add_event_listener(sighupTracker)
   
   # prepopulates bandwidth values from state file
@@ -604,16 +628,18 @@
   # tells revised panels to run as daemons
   panels["header"].start()
   panels["log"].start()
+  if CONFIG["features.connection.newPanel"]:
+    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 +667,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:
@@ -657,7 +688,8 @@
         #panels["header"]._updateParams(True)
         
         # other panels that use torrc data
-        panels["conn"].resetOptions()
+        if CONFIG["features.connection.oldPanel"]:
+          panels["conn"].resetOptions()
         #if not isBlindMode: panels["graph"].stats["connections"].resetOptions(conn)
         #panels["graph"].stats["bandwidth"].resetOptions()
         
@@ -718,7 +750,8 @@
           isUnresponsive = False
           log.log(log.NOTICE, "Relay resumed")
       
-      panels["conn"].reset()
+      if CONFIG["features.connection.oldPanel"]:
+        panels["conn"].reset()
       
       # TODO: part two of hack to prevent premature drawing by log panel
       if page == 0 and not isPaused: panels["log"].setPaused(False)
@@ -731,7 +764,7 @@
           isResize = lastSize != newSize
           lastSize = newSize
           
-          if panelKey in ("header", "graph", "log", "config", "torrc"):
+          if panelKey in ("header", "graph", "log", "config", "torrc", "conn2"):
             # revised panel (manages its own content refreshing)
             panels[panelKey].redraw(isResize)
           else:
@@ -808,11 +841,15 @@
         # this appears to be a python bug: http://bugs.python.org/issue3014
         # (haven't seen this is quite some time... mysteriously resolved?)
         
+        torTools.NO_SPAWN = True # prevents further worker threads from being spawned
+        
         # stops panel daemons
         panels["header"].stop()
+        if CONFIG["features.connection.newPanel"]: panels["conn2"].stop()
         panels["log"].stop()
         
         panels["header"].join()
+        if CONFIG["features.connection.newPanel"]: panels["conn2"].join()
         panels["log"].join()
         
         # joins on utility daemon threads - this might take a moment since
@@ -835,15 +872,25 @@
       if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
       else: page = (page + 1) % len(PAGES)
       
-      # skip connections listing if it's disabled
-      if page == 1 and isBlindMode:
-        if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
-        else: page = (page + 1) % len(PAGES)
+      # skip connections listings if it's disabled
+      while True:
+        if page == 1 and (isBlindMode or not CONFIG["features.connection.oldPanel"]):
+          if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
+          else: page = (page + 1) % len(PAGES)
+        elif page == 2 and (isBlindMode or not CONFIG["features.connection.newPanel"]):
+          if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
+          else: page = (page + 1) % len(PAGES)
+        else: break
       
       # pauses panels that aren't visible to prevent events from accumilating
       # (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
@@ -914,7 +961,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")
           
@@ -940,10 +987,11 @@
           
           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"
+          if CONFIG["features.connection.oldPanel"]:
+            allowDnsLabel = "allow" if panels["conn"].allowDNS else "disallow"
+          else: allowDnsLabel = "disallow"
           popup.addfstr(5, 2, "<b>r</b>: permit DNS resolution (<b>%s</b>)" % allowDnsLabel)
           
           popup.addfstr(5, 41, "<b>s</b>: sort ordering")
@@ -959,8 +1007,18 @@
           popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
           
           popup.addfstr(3, 2, "<b>enter</b>: edit configuration option")
-          popup.addfstr(3, 41, "<b>w</b>: save current configuration")
-          popup.addfstr(4, 2, "<b>s</b>: sort ordering")
+          #popup.addfstr(3, 41, "<b>d</b>: raw consensus descriptor")
+          
+          listingType = panels["conn2"]._listingType.lower()
+          popup.addfstr(4, 2, "<b>l</b>: listed identity (<b>%s</b>)" % listingType)
+          
+          popup.addfstr(4, 41, "<b>s</b>: sort ordering")
+          
+          resolverUtil = connections.getResolver("tor").overwriteResolver
+          if resolverUtil == None: resolverUtil = "auto"
+          popup.addfstr(3, 41, "<b>u</b>: resolving utility (<b>%s</b>)" % resolverUtil)
+          
+          pageOverrideKeys = (ord('l'), ord('s'), ord('u'))
         elif page == 3:
           popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
           popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
@@ -975,6 +1033,12 @@
           
           popup.addfstr(4, 2, "<b>r</b>: reload torrc")
           popup.addfstr(4, 41, "<b>x</b>: reset tor (issue sighup)")
+        elif page == 4:
+          popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
+          popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
+          popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
+          popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
+          popup.addfstr(3, 2, "<b>enter</b>: connection details")
         
         popup.addstr(7, 2, "Press any key...")
         popup.refresh()
@@ -1054,7 +1118,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')):
@@ -1245,13 +1309,16 @@
         setPauseState(panels, isPaused, page)
       finally:
         panel.CURSES_LOCK.release()
-    elif key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingCounter != -1:
+    elif CONFIG["features.connection.oldPanel"] and key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingCounter != -1:
       # canceling hostname resolution (esc on any page)
       panels["conn"].listingType = connPanel.LIST_IP
       panels["control"].resolvingCounter = -1
       hostnames.setPaused(True)
       panels["conn"].sortConnections()
-    elif page == 1 and panels["conn"].isCursorEnabled and key in (curses.KEY_ENTER, 10, ord(' ')):
+    elif page == 1 and panels["conn"].isCursorEnabled and uiTools.isSelectionKey(key):
+      # TODO: deprecated when migrated to the new connection panel, thought as
+      # well keep around until there's a counterpart for hostname fetching
+      
       # provides details on selected connection
       panel.CURSES_LOCK.acquire()
       try:
@@ -1269,7 +1336,7 @@
         curses.cbreak() # wait indefinitely for key presses (no timeout)
         key = 0
         
-        while key not in (curses.KEY_ENTER, 10, ord(' ')):
+        while not uiTools.isSelectionKey(key):
           popup.clear()
           popup.win.box()
           popup.addstr(0, 0, "Connection Details:", curses.A_STANDOUT)
@@ -1455,13 +1522,13 @@
           hostnames.setPaused(True)
         
         panels["conn"].sortConnections()
-    elif page == 1 and (key == ord('u') or key == ord('U')):
+    elif page in (1, 2) 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
@@ -1469,14 +1536,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:"
@@ -1543,7 +1611,44 @@
         setPauseState(panels, isPaused, page)
       finally:
         panel.CURSES_LOCK.release()
-    elif page == 2 and (key == ord('c') or key == ord('C')) and False:
+    elif page == 2 and (key == ord('l') or key == ord('L')):
+      # provides a menu to pick the primary information we list connections by
+      options = interface.connections.entries.ListingType.values()
+      
+      # dropping the HOSTNAME listing type until we support displaying that content
+      options.remove(interface.connections.entries.ListingType.HOSTNAME)
+      
+      initialSelection = options.index(panels["conn2"]._listingType)
+      
+      # hides top label of connection panel and pauses the display
+      panelTitle = panels["conn2"]._title
+      panels["conn2"]._title = ""
+      panels["conn2"].redraw(True)
+      setPauseState(panels, isPaused, page, True)
+      
+      selection = showMenu(stdscr, panels["popup"], "List By:", options, initialSelection)
+      
+      # reverts changes made for popup
+      panels["conn2"]._title = panelTitle
+      setPauseState(panels, isPaused, page)
+      
+      # applies new setting
+      if selection != -1 and options[selection] != panels["conn2"]._listingType:
+        panels["conn2"].setListingType(options[selection])
+        panels["conn2"].redraw(True)
+    elif page == 2 and (key == ord('s') or key == ord('S')):
+      # set ordering for connection options
+      titleLabel = "Connection Ordering:"
+      options = interface.connections.entries.SortAttr.values()
+      oldSelection = panels["conn2"]._sortOrdering
+      optionColors = dict([(attr, interface.connections.entries.SORT_COLORS[attr]) for attr in options])
+      results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
+      
+      if results:
+        panels["conn2"].setSortOrder(results)
+      
+      panels["conn2"].redraw(True)
+    elif page == 3 and (key == ord('c') or key == ord('C')) and False:
       # TODO: disabled for now (probably gonna be going with separate pages
       # rather than popup menu)
       # provides menu to pick config being displayed
@@ -1566,12 +1671,11 @@
       if selection != -1: panels["torrc"].setConfigType(selection)
       
       selectiveRefresh(panels, page)
-    elif page == 2 and (key == ord('w') or key == ord('W')):
+    elif page == 3 and (key == ord('w') or key == ord('W')):
       # display a popup for saving the current configuration
       panel.CURSES_LOCK.acquire()
       try:
-        configText = torTools.getConn().getInfo("config-text", "").strip()
-        configLines = configText.split("\n")
+        configLines = torConfig.getCustomOptions(True)
         
         # lists event types
         popup = panels["popup"]
@@ -1594,7 +1698,7 @@
           popup.recreate(stdscr)
         
         key, selection = 0, 2
-        while key not in (curses.KEY_ENTER, 10, ord(' ')):
+        while not uiTools.isSelectionKey(key):
           # if the popup has been resized then recreate it (needed for the
           # proper border height)
           newHeight, newWidth = panels["popup"].getPreferredSize()
@@ -1669,7 +1773,7 @@
               
               # saves the configuration to the file
               configFile = open(configLocation, "w")
-              configFile.write(configText)
+              configFile.write("\n".join(configLines))
               configFile.close()
               
               # reloads the cached torrc if overwriting it
@@ -1696,12 +1800,12 @@
         panel.CURSES_LOCK.release()
       
       panels["config"].redraw(True)
-    elif page == 2 and (key == ord('s') or key == ord('S')):
+    elif page == 3 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:
@@ -1717,7 +1821,7 @@
         panels["config"].setSortOrder(resultEnums)
       
       panels["config"].redraw(True)
-    elif page == 2 and key in (curses.KEY_ENTER, 10, ord(' ')):
+    elif page == 3 and uiTools.isSelectionKey(key):
       # let the user edit the configuration value, unchanged if left blank
       panel.CURSES_LOCK.acquire()
       try:
@@ -1725,13 +1829,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 = ""
@@ -1745,19 +1849,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:
@@ -1773,7 +1877,7 @@
         setPauseState(panels, isPaused, page)
       finally:
         panel.CURSES_LOCK.release()
-    elif page == 3 and key == ord('r') or key == ord('R'):
+    elif page == 4 and key == ord('r') or key == ord('R'):
       # reloads torrc, providing a notice if successful or not
       loadedTorrc = torConfig.getTorrc()
       loadedTorrc.getLock().acquire()
@@ -1803,8 +1907,10 @@
     elif page == 1:
       panels["conn"].handleKey(key)
     elif page == 2:
+      panels["conn2"].handleKey(key)
+    elif page == 3:
       panels["config"].handleKey(key)
-    elif page == 3:
+    elif page == 4:
       panels["torrc"].handleKey(key)
 
 def startTorMonitor(startTime, loggedEvents, isBlindMode):

Modified: arm/release/src/interface/descriptorPopup.py
===================================================================
--- arm/release/src/interface/descriptorPopup.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/descriptorPopup.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -106,7 +106,7 @@
         draw(popup, properties)
         key = stdscr.getch()
         
-        if key in (curses.KEY_ENTER, 10, ord(' '), ord('d'), ord('D')):
+        if uiTools.isSelectionKey(key) or key in (ord('d'), ord('D')):
           # closes popup
           isVisible = False
         elif key in (curses.KEY_LEFT, curses.KEY_RIGHT):

Modified: arm/release/src/interface/graphing/__init__.py
===================================================================
--- arm/release/src/interface/graphing/__init__.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/graphing/__init__.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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/release/src/interface/graphing/bandwidthStats.py
===================================================================
--- arm/release/src/interface/graphing/bandwidthStats.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/graphing/bandwidthStats.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -20,7 +20,8 @@
 PREPOPULATE_SUCCESS_MSG = "Read the last day of bandwidth history from the state file"
 PREPOPULATE_FAILURE_MSG = "Unable to prepopulate bandwidth information (%s)"
 
-DEFAULT_CONFIG = {"features.graph.bw.transferInBytes": False,
+DEFAULT_CONFIG = {"features.graph.bw.prepopulateTotal": False,
+                  "features.graph.bw.transferInBytes": False,
                   "features.graph.bw.accounting.show": True,
                   "features.graph.bw.accounting.rate": 10,
                   "features.graph.bw.accounting.isTimeLong": False,
@@ -39,6 +40,11 @@
     if config:
       config.update(self._config, {"features.graph.bw.accounting.rate": 1})
     
+    # stats prepopulated from tor's state file
+    self.prepopulatePrimaryTotal = 0
+    self.prepopulateSecondaryTotal = 0
+    self.prepopulateTicks = 0
+    
     # accounting data (set by _updateAccountingInfo method)
     self.accountingLastUpdated = 0
     self.accountingInfo = dict([(arg, "") for arg in ACCOUNTING_ARGS])
@@ -47,7 +53,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 +61,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):
@@ -164,10 +170,11 @@
       readVal, writeVal = bwReadEntries[i], bwWriteEntries[i]
       
       self.lastPrimary, self.lastSecondary = readVal, writeVal
-      self.primaryTotal += readVal * 900
-      self.secondaryTotal += writeVal * 900
-      self.tick += 900
       
+      self.prepopulatePrimaryTotal += readVal * 900
+      self.prepopulateSecondaryTotal += writeVal * 900
+      self.prepopulateTicks += 900
+      
       self.primaryCounts[intervalIndex].insert(0, readVal)
       self.secondaryCounts[intervalIndex].insert(0, writeVal)
     
@@ -316,10 +323,15 @@
   
   def _getAvgLabel(self, isPrimary):
     total = self.primaryTotal if isPrimary else self.secondaryTotal
-    return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick)) * 1024, 1, False, self._config["features.graph.bw.transferInBytes"])
+    total += self.prepopulatePrimaryTotal if isPrimary else self.prepopulateSecondaryTotal
+    return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick + self.prepopulateTicks)) * 1024, 1, False, self._config["features.graph.bw.transferInBytes"])
   
   def _getTotalLabel(self, isPrimary):
     total = self.primaryTotal if isPrimary else self.secondaryTotal
+    
+    if self._config["features.graph.bw.prepopulateTotal"]:
+      total += self.prepopulatePrimaryTotal if isPrimary else self.prepopulateSecondaryTotal
+    
     return "total: %s" % uiTools.getSizeLabel(total * 1024, 1)
   
   def _updateAccountingInfo(self):

Modified: arm/release/src/interface/graphing/connStats.py
===================================================================
--- arm/release/src/interface/graphing/connStats.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/graphing/connStats.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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/release/src/interface/graphing/graphPanel.py
===================================================================
--- arm/release/src/interface/graphing/graphPanel.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/graphing/graphPanel.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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)
@@ -276,7 +275,7 @@
     
     self.graphHeight = max(MIN_GRAPH_HEIGHT, newGraphHeight)
   
-  def draw(self, subwindow, width, height):
+  def draw(self, width, height):
     """ Redraws graph panel """
     
     if self.currentDisplay:
@@ -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/release/src/interface/headerPanel.py
===================================================================
--- arm/release/src/interface/headerPanel.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/headerPanel.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -37,8 +37,8 @@
   Top area contenting tor settings and system information. Stats are stored in
   the vals mapping, keys including:
     tor/  version, versionStatus, nickname, orPort, dirPort, controlPort,
-          exitPolicy, isAuthPassword (bool), isAuthCookie (bool)
-          *address, *fingerprint, *flags, pid, startTime
+          exitPolicy, isAuthPassword (bool), isAuthCookie (bool),
+          orListenAddr, *address, *fingerprint, *flags, pid, startTime
     sys/  hostname, os, version
     stat/ *%torCpu, *%armCpu, *rss, *%mem
   
@@ -95,7 +95,7 @@
     if self.vals["tor/orPort"]: return 4 if isWide else 6
     else: return 3 if isWide else 4
   
-  def draw(self, subwindow, width, height):
+  def draw(self, width, height):
     self.valsLock.acquire()
     isWide = width + 1 >= MIN_DUAL_COL_WIDTH
     
@@ -127,10 +127,14 @@
     
     # Line 2 / Line 2 Left (tor ip/port information)
     if self.vals["tor/orPort"]:
+      myAddress = "Unknown"
+      if self.vals["tor/orListenAddr"]: myAddress = self.vals["tor/orListenAddr"]
+      elif self.vals["tor/address"]: myAddress = self.vals["tor/address"]
+      
       # acting as a relay (we can assume certain parameters are set
       entry = ""
       dirPortLabel = ", Dir Port: %s" % self.vals["tor/dirPort"] if self.vals["tor/dirPort"] != "0" else ""
-      for label in (self.vals["tor/nickname"], " - " + self.vals["tor/address"], ":" + self.vals["tor/orPort"], dirPortLabel):
+      for label in (self.vals["tor/nickname"], " - " + myAddress, ":" + self.vals["tor/orPort"], dirPortLabel):
         if len(entry) + len(label) <= leftWidth: entry += label
         else: break
     else:
@@ -239,7 +243,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 +292,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()
@@ -329,15 +333,15 @@
       if self.vals["tor/orPort"] == "0": self.vals["tor/orPort"] = ""
       
       # overwrite address if ORListenAddress is set (and possibly orPort too)
-      self.vals["tor/address"] = "Unknown"
+      self.vals["tor/orListenAddr"] = ""
       listenAddr = conn.getOption("ORListenAddress")
       if listenAddr:
         if ":" in listenAddr:
           # both ip and port overwritten
-          self.vals["tor/address"] = listenAddr[:listenAddr.find(":")]
+          self.vals["tor/orListenAddr"] = listenAddr[:listenAddr.find(":")]
           self.vals["tor/orPort"] = listenAddr[listenAddr.find(":") + 1:]
         else:
-          self.vals["tor/address"] = listenAddr
+          self.vals["tor/orListenAddr"] = listenAddr
       
       # fetch exit policy (might span over multiple lines)
       policyEntries = []
@@ -368,8 +372,7 @@
     # sets volatile parameters
     # TODO: This can change, being reported by STATUS_SERVER -> EXTERNAL_ADDRESS
     # events. Introduce caching via torTools?
-    if self.vals["tor/address"] == "Unknown":
-      self.vals["tor/address"] = conn.getInfo("address", self.vals["tor/address"])
+    self.vals["tor/address"] = conn.getInfo("address", "")
     
     self.vals["tor/fingerprint"] = conn.getInfo("fingerprint", self.vals["tor/fingerprint"])
     self.vals["tor/flags"] = conn.getMyFlags(self.vals["tor/flags"])

Modified: arm/release/src/interface/logPanel.py
===================================================================
--- arm/release/src/interface/logPanel.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/logPanel.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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
@@ -621,14 +621,14 @@
     if self._config["features.logFile"]:
       logPath = self._config["features.logFile"]
       
-      # make dir if the path doesn't already exist
-      baseDir = os.path.dirname(logPath)
-      if not os.path.exists(baseDir): os.makedirs(baseDir)
-      
       try:
+        # make dir if the path doesn't already exist
+        baseDir = os.path.dirname(logPath)
+        if not os.path.exists(baseDir): os.makedirs(baseDir)
+        
         self.logFile = open(logPath, "a")
         log.log(self._config["log.logPanel.logFileOpened"], "arm %s opening log file (%s)" % (VERSION, logPath))
-      except IOError, exc:
+      except (IOError, OSError), exc:
         log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
         self.logFile = None
   
@@ -778,7 +778,7 @@
       self.redraw(True)
       self.valsLock.release()
   
-  def draw(self, subwindow, width, height):
+  def draw(self, width, height):
     """
     Redraws message log. Entries stretch to use available space and may
     contain up to two lines. Starts with newest entries.
@@ -831,23 +831,22 @@
         # bottom of the divider
         if seenFirstDateDivider:
           if lineCount >= 1 and lineCount < height and showDaybreaks:
-            self.win.vline(lineCount, dividerIndent, curses.ACS_LLCORNER | dividerAttr, 1)
-            self.win.hline(lineCount, dividerIndent + 1, curses.ACS_HLINE | dividerAttr, width - dividerIndent - 1)
-            self.win.vline(lineCount, width, curses.ACS_LRCORNER | dividerAttr, 1)
+            self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER,  dividerAttr)
+            self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 1, dividerAttr)
+            self.addch(lineCount, width, curses.ACS_LRCORNER, dividerAttr)
           
           lineCount += 1
         
         # top of the divider
         if lineCount >= 1 and lineCount < height and showDaybreaks:
           timeLabel = time.strftime(" %B %d, %Y ", time.localtime(entry.timestamp))
-          self.win.vline(lineCount, dividerIndent, curses.ACS_ULCORNER | dividerAttr, 1)
-          self.win.hline(lineCount, dividerIndent + 1, curses.ACS_HLINE | dividerAttr, 1)
+          self.addch(lineCount, dividerIndent, curses.ACS_ULCORNER, dividerAttr)
+          self.addch(lineCount, dividerIndent + 1, curses.ACS_HLINE, dividerAttr)
           self.addstr(lineCount, dividerIndent + 2, timeLabel, curses.A_BOLD | dividerAttr)
           
-          if dividerIndent + len(timeLabel) + 2 <= width:
-            lineLength = width - dividerIndent - len(timeLabel) - 2
-            self.win.hline(lineCount, dividerIndent + len(timeLabel) + 2, curses.ACS_HLINE | dividerAttr, lineLength)
-            self.win.vline(lineCount, dividerIndent + len(timeLabel) + 2 + lineLength, curses.ACS_URCORNER | dividerAttr, 1)
+          lineLength = width - dividerIndent - len(timeLabel) - 2
+          self.hline(lineCount, dividerIndent + len(timeLabel) + 2, lineLength, dividerAttr)
+          self.addch(lineCount, dividerIndent + len(timeLabel) + 2 + lineLength, curses.ACS_URCORNER, dividerAttr)
         
         seenFirstDateDivider = True
         lineCount += 1
@@ -879,15 +878,15 @@
             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
           
           if drawLine < height and drawLine >= 1:
             if seenFirstDateDivider and width - dividerIndent >= 3 and showDaybreaks:
-              self.win.vline(drawLine, dividerIndent, curses.ACS_VLINE | dividerAttr, 1)
-              self.win.vline(drawLine, width, curses.ACS_VLINE | dividerAttr, 1)
+              self.addch(drawLine, dividerIndent, curses.ACS_VLINE, dividerAttr)
+              self.addch(drawLine, width, curses.ACS_VLINE, dividerAttr)
             
             self.addstr(drawLine, cursorLoc, msg, format)
           
@@ -902,13 +901,9 @@
       # if this is the last line and there's room, then draw the bottom of the divider
       if not deduplicatedLog and seenFirstDateDivider:
         if lineCount < height and showDaybreaks:
-          # when resizing with a small width the following entries can be
-          # problematc (though I'm not sure why)
-          try:
-            self.win.vline(lineCount, dividerIndent, curses.ACS_LLCORNER | dividerAttr, 1)
-            self.win.hline(lineCount, dividerIndent + 1, curses.ACS_HLINE | dividerAttr, width - dividerIndent - 1)
-            self.win.vline(lineCount, width, curses.ACS_LRCORNER | dividerAttr, 1)
-          except: pass
+          self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER, dividerAttr)
+          self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 1, dividerAttr)
+          self.addch(lineCount, width, curses.ACS_LRCORNER, dividerAttr)
         
         lineCount += 1
     
@@ -1019,7 +1014,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/release/src/interface/torrcPanel.py
===================================================================
--- arm/release/src/interface/torrcPanel.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/interface/torrcPanel.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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):
   """
@@ -60,7 +60,7 @@
     
     self.valsLock.release()
   
-  def draw(self, subwindow, width, height):
+  def draw(self, width, height):
     self.valsLock.acquire()
     
     # If true, we assume that the cached value in self._lastContentHeight is
@@ -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/release/src/settings.cfg
===================================================================
--- arm/release/src/settings.cfg	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/settings.cfg	2011-04-04 15:22:31 UTC (rev 24555)
@@ -1,3 +1,32 @@
+# Important tor configuration options (shown by default)
+config.important BandwidthRate
+config.important BandwidthBurst
+config.important RelayBandwidthRate
+config.important RelayBandwidthBurst
+config.important ControlPort
+config.important HashedControlPassword
+config.important CookieAuthentication
+config.important DataDirectory
+config.important Log
+config.important RunAsDaemon
+config.important User
+config.important Bridge
+config.important ExcludeNodes
+config.important SocksPort
+config.important UseBridges
+config.important BridgeRelay
+config.important ContactInfo
+config.important ExitPolicy
+config.important MyFamily
+config.important Nickname
+config.important ORPort
+config.important AccountingMax
+config.important AccountingStart
+config.important DirPortFrontPage
+config.important DirPort
+config.important HiddenServiceDir
+config.important HiddenServicePort
+
 # Summary descriptions for Tor configuration options
 # General Config Options
 config.summary.BandwidthRate Average bandwidth usage limit
@@ -145,6 +174,58 @@
 config.summary.ExitPortStatistics Toggles storing traffic and port usage data to disk
 config.summary.ExtraInfoStatistics Publishes statistic data in the extra-info documents
 
+# Directory Server Options
+config.summary.AuthoritativeDirectory act as a directory authority
+config.summary.DirPortFrontPage publish this html file on the DirPort
+config.summary.V1AuthoritativeDirectory generates a version 1 consensus
+config.summary.V2AuthoritativeDirectory generates a version 2 consensus
+config.summary.V3AuthoritativeDirectory generates a version 3 consensus
+config.summary.VersioningAuthoritativeDirectory provides opinions on recommended versions of tor
+config.summary.NamingAuthoritativeDirectory provides opinions on fingerprint to nickname bindings
+config.summary.HSAuthoritativeDir toggles accepting hidden service descriptors
+config.summary.HidServDirectoryV2 toggles accepting version 2 hidden service descriptors
+config.summary.BridgeAuthoritativeDir acts as a bridge authority
+config.summary.MinUptimeHidServDirectoryV2 required uptime before accepting hidden service directory
+config.summary.DirPort port for directory connections
+config.summary.DirListenAddress address the directory service is bound to
+config.summary.DirPolicy access policy for the DirPort
+
+# Directory Authority Server Options
+config.summary.RecommendedVersions tor versions believed to be safe
+config.summary.RecommendedClientVersions tor versions believed to be safe for clients
+config.summary.RecommendedServerVersions tor versions believed to be safe for relays
+config.summary.ConsensusParams params entry of the networkstatus vote
+config.summary.DirAllowPrivateAddresses toggles allowing arbitrary input or non-public IPs in descriptors
+config.summary.AuthDirBadDir relays to be flagged as bad directory caches
+config.summary.AuthDirBadExit relays to be flagged as bad exits
+config.summary.AuthDirInvalid relays from which the valid flag is withheld
+config.summary.AuthDirReject relays to be dropped from the consensus
+config.summary.AuthDirListBadDirs toggles if we provide an opinion on bad directory caches
+config.summary.AuthDirListBadExits toggles if we provide an opinion on bad exits
+config.summary.AuthDirRejectUnlisted rejects further relay descriptors
+config.summary.AuthDirMaxServersPerAddr limit on the number of relays accepted per ip
+config.summary.AuthDirMaxServersPerAuthAddr limit on the number of relays accepted per an authority's ip
+config.summary.V3AuthVotingInterval consensus voting interval
+config.summary.V3AuthVoteDelay wait time to collect votes of other authorities
+config.summary.V3AuthDistDelay wait time to collect the signatures of other authorities
+config.summary.V3AuthNIntervalsValid number of voting intervals a consensus is valid for
+config.summary.V3BandwidthsFile path to a file containing measured relay bandwidths
+
+# Hidden Service Options
+config.summary.HiddenServiceDir directory contents for the hidden service
+config.summary.HiddenServicePort port the hidden service is provided on
+config.summary.PublishHidServDescriptors toggles automated publishing of the hidden service to the rendezvous directory
+config.summary.HiddenServiceAuthorizeClient restricts access to the hidden service
+config.summary.RendPostPeriod period at which the rendezvous service descriptors are refreshed
+
+# Testing Network Options
+config.summary.TestingTorNetwork overrides other options to be a testing network
+config.summary.TestingV3AuthInitialVotingInterval overrides V3AuthVotingInterval for the first consensus
+config.summary.TestingV3AuthInitialVoteDelay overrides TestingV3AuthInitialVoteDelay for the first consensus
+config.summary.TestingV3AuthInitialDistDelay overrides TestingV3AuthInitialDistDelay for the first consensus
+config.summary.TestingAuthDirTimeToLearnReachability delay until opinions are given about which relays are running or not
+config.summary.TestingEstimatedDescriptorPropagationTime delay before clients attempt to fetch descriptors from directory caches
+
 # Snippets from common log messages
 # These are static bits of log messages, used to determine when entries with
 # dynamic content (hostnames, numbers, etc) are the same. If this matches the
@@ -285,3 +366,287 @@
 torrc.label.time.day day, days
 torrc.label.time.week week, weeks
 
+# Common usages for ports based on:
+# https://secure.wikimedia.org/wikipedia/en/wiki/List_of_TCP_and_UDP_port_numbers
+# http://isc.sans.edu/services.html
+# 
+# Including all the official low ports (< 1024), and higher ones I recognize.
+
+port.label.1 TCPMUX
+port.label.2 CompressNET
+port.label.3 CompressNET
+port.label.5 RJE
+port.label.7 Echo
+port.label.9 Discard
+port.label.11 SYSTAT
+port.label.13 Daytime
+port.label.15 netstat
+port.label.17 QOTD
+port.label.18 MSP
+port.label.19 CHARGEN
+port.label.20 FTP
+port.label.21 FTP
+port.label.22 SSH
+port.label.23 Telnet
+port.label.24 Priv-mail
+port.label.25 SMTP
+port.label.34 RF
+port.label.35 Printer
+port.label.37 TIME
+port.label.39 RLP
+port.label.41 Graphics
+port.label.42 WINS
+port.label.43 WHOIS
+port.label.47 NI FTP
+port.label.49 TACACS
+port.label.50 Remote Mail
+port.label.51 IMP
+port.label.52 XNS
+port.label.53 DNS
+port.label.54 XNS
+port.label.55 ISI-GL
+port.label.56 RAP
+port.label.57 MTP
+port.label.58 XNS
+port.label.67 BOOTP
+port.label.68 BOOTP
+port.label.69 TFTP
+port.label.70 Gopher
+port.label.79 Finger
+port.label.80 HTTP
+port.label.81 Torpark
+port.label.82 Torpark
+port.label.83 MIT ML
+port.label.88 Kerberos
+port.label.90 dnsix
+port.label.99 WIP
+port.label.101 NIC
+port.label.102 ISO-TSAP
+port.label.104 ACR/NEMA
+port.label.105 CCSO
+port.label.107 Telnet
+port.label.108 SNA
+port.label.109 POP2
+port.label.110 POP3
+port.label.111 ONC RPC
+port.label.113 ident
+port.label.115 SFTP
+port.label.117 UUCP
+port.label.118 SQL
+port.label.119 NNTP
+port.label.123 NTP
+port.label.135 DCE
+port.label.137 NetBIOS
+port.label.138 NetBIOS
+port.label.139 NetBIOS
+port.label.143 IMAP
+port.label.152 BFTP
+port.label.153 SGMP
+port.label.156 SQL
+port.label.158 DMSP
+port.label.161 SNMP
+port.label.162 SNMPTRAP
+port.label.170 Print-srv
+port.label.177 XDMCP
+port.label.179 BGP
+port.label.194 IRC
+port.label.199 SMUX
+port.label.201 AppleTalk
+port.label.209 QMTP
+port.label.210 ANSI
+port.label.213 IPX
+port.label.218 MPP
+port.label.220 IMAP
+port.label.256 2DEV
+port.label.259 ESRO
+port.label.264 BGMP
+port.label.308 Novastor
+port.label.311 OSX Admin
+port.label.318 PKIX TSP
+port.label.319 PTP
+port.label.320 PTP
+port.label.323 IMMP
+port.label.350 MATIP
+port.label.351 MATIP
+port.label.366 ODMR
+port.label.369 Rpc2portmap
+port.label.370 codaauth2
+port.label.371 ClearCase
+port.label.383 HP Alarm Mgr
+port.label.384 ARNS
+port.label.387 AURP
+port.label.389 LDAP
+port.label.401 UPS
+port.label.402 Altiris
+port.label.427 SLP
+port.label.443 HTTPS
+port.label.444 SNPP
+port.label.445 SMB
+port.label.464 Kerberos
+port.label.465 SMTP
+port.label.475 tcpnethaspsrv
+port.label.497 Retrospect
+port.label.500 ISAKMP
+port.label.501 STMF
+port.label.502 Modbus
+port.label.504 Citadel
+port.label.510 FirstClass
+port.label.512 Rexec
+port.label.513 rlogin
+port.label.514 rsh
+port.label.515 LPD
+port.label.517 Talk
+port.label.518 NTalk
+port.label.520 efs
+port.label.524 NCP
+port.label.530 RPC
+port.label.531 AIM/IRC
+port.label.532 netnews
+port.label.533 netwall
+port.label.540 UUCP
+port.label.542 commerce
+port.label.543 klogin
+port.label.544 klogin
+port.label.545 OSISoft PI
+port.label.546 DHCPv6
+port.label.547 DHCPv6
+port.label.548 AFP
+port.label.550 new-who
+port.label.554 RTSP
+port.label.556 RFS
+port.label.560 rmonitor
+port.label.561 monitor
+port.label.563 NNTPS
+port.label.587 SMTP
+port.label.591 FileMaker
+port.label.593 HTTP RPC
+port.label.604 TUNNEL
+port.label.623 ASF-RMCP
+port.label.631 CUPS
+port.label.635 RLZ DBase
+port.label.636 LDAPS
+port.label.639 MSDP
+port.label.641 SupportSoft
+port.label.646 LDP
+port.label.647 DHCP
+port.label.648 RRP
+port.label.651 IEEE-MMS
+port.label.652 DTCP
+port.label.653 SupportSoft
+port.label.654 MMS/MMP
+port.label.657 RMC
+port.label.660 OSX Admin
+port.label.665 sun-dr
+port.label.666 Doom
+port.label.674 ACAP
+port.label.691 MS Exchange
+port.label.692 Hyperwave-ISP
+port.label.694 Linux-HA
+port.label.695 IEEE-MMS-SSL
+port.label.698 OLSR
+port.label.699 Access Network
+port.label.700 EPP
+port.label.701 LMP
+port.label.702 IRIS
+port.label.706 SILC
+port.label.711 MPLS
+port.label.712 TBRPF
+port.label.720 SMQP
+port.label.749 Kerberos
+port.label.750 rfile
+port.label.751 pump
+port.label.752 qrh
+port.label.753 rrh
+port.label.754 tell send
+port.label.760 ns
+port.label.782 Conserver
+port.label.783 spamd
+port.label.829 CMP
+port.label.843 Flash
+port.label.847 DHCP
+port.label.860 iSCSI
+port.label.873 rsync
+port.label.888 CDDB
+port.label.901 SWAT
+port.label.902 VMware
+port.label.903 VMware
+port.label.904 VMware
+port.label.911 NCA
+port.label.953 DNS RNDC
+port.label.981 SofaWare
+port.label.989 FTPS
+port.label.990 FTPS
+port.label.991 NAS
+port.label.992 Telnet
+port.label.993 IMAPS
+port.label.995 POP3S
+port.label.999 ScimoreDB
+port.label.1001 JtoMB
+port.label.1002 cogbot
+
+port.label.1080 SOCKS
+port.label.1085 WebObjects
+port.label.1109 KPOP
+port.label.1169 Tripwire
+port.label.1194 OpenVPN
+port.label.1214 Kazaa
+port.label.1220 QuickTime
+port.label.1234 VLC
+port.label.1241 Nessus
+port.label.1270 SCOM
+port.label.1293 IPSec
+port.label.1433 MSSQL
+port.label.1434 MSSQL
+port.label.1503 MSN
+port.label.1512 WINS
+port.label.1521 Oracle
+port.label.1526 Oracle
+port.label.1666 Perforce
+port.label.1725 Steam
+port.label.1863 MSNP
+port.label.2049 NFS
+port.label.2086 GNUnet
+port.label.2401 CVS
+port.label.2525 SMTP
+port.label.2710 BitTorrent
+port.label.3074 XBox LIVE
+port.label.3101 BlackBerry
+port.label.3306 MySQL
+port.label.3690 SVN
+port.label.3723 Battle.net
+port.label.3724 WoW
+port.label.4662 eMule
+port.label.5003 FileMaker
+port.label.5050 Yahoo IM
+port.label.5060 SIP
+port.label.5061 SIP
+port.label.5190 AIM/ICQ
+port.label.5222 Jabber
+port.label.5223 Jabber
+port.label.5269 Jabber
+port.label.5298 Jabber
+port.label.5432 PostgreSQL
+port.label.5500 VNC
+port.label.5556 Freeciv
+port.label.5666 NRPE
+port.label.5667 NSCA
+port.label.5800 VNC
+port.label.5900 VNC
+port.label.6346 gnutella
+port.label.6347 gnutella
+port.label.6660-6669 IRC
+port.label.6679 IRC
+port.label.6697 IRC
+port.label.6881-6999 BitTorrent
+port.label.8008 HTTP
+port.label.8010 XMPP
+port.label.8080 Tomcat
+port.label.8118 Privoxy
+port.label.8123 Polipo
+port.label.9030 Tor
+port.label.9050 Tor
+port.label.9051 Tor
+port.label.23399 Skype
+port.label.30301 BitTorrent
+port.label.33434 traceroute
+

Modified: arm/release/src/starter.py
===================================================================
--- arm/release/src/starter.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/starter.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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,
@@ -88,29 +89,6 @@
 # torrc entries that are scrubbed when dumping
 PRIVATE_TORRC_ENTRIES = ["HashedControlPassword", "Bridge", "HiddenServiceDir"]
 
-def isValidIpAddr(ipStr):
-  """
-  Returns true if input is a valid IPv4 address, false otherwise.
-  """
-  
-  for i in range(4):
-    if i < 3:
-      divIndex = ipStr.find(".")
-      if divIndex == -1: return False # expected a period to be valid
-      octetStr = ipStr[:divIndex]
-      ipStr = ipStr[divIndex + 1:]
-    else:
-      octetStr = ipStr
-    
-    try:
-      octet = int(octetStr)
-      if not octet >= 0 or not octet <= 255: return False
-    except ValueError:
-      # address value isn't an integer
-      return False
-  
-  return True
-
 def _loadConfigurationDescriptions(pathPrefix):
   """
   Attempts to load descriptions for tor's configuration options, fetching them
@@ -125,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:
@@ -166,7 +145,7 @@
           
           msg = DESC_SAVE_SUCCESS_MSG % (descriptorPath, time.time() - loadStartTime)
           util.log.log(CONFIG["log.configDescriptions.persistance.loadSuccess"], msg)
-        except IOError, exc:
+        except (IOError, OSError), exc:
           msg = DESC_SAVE_FAILED_MSG % util.sysTools.getFileErrorMsg(exc)
           util.log.log(CONFIG["log.configDescriptions.persistance.saveFailed"], msg)
     
@@ -324,7 +303,7 @@
   controlAddr = param["startup.interface.ipAddress"]
   controlPort = param["startup.interface.port"]
   
-  if not isValidIpAddr(controlAddr):
+  if not util.connections.isValidIpAddress(controlAddr):
     print "'%s' isn't a valid IP address" % controlAddr
     sys.exit()
   elif controlPort < 0 or controlPort > 65535:
@@ -409,5 +388,4 @@
     _dumpConfig()
   
   interface.controller.startTorMonitor(time.time() - initTime, expandedEvents, param["startup.blindModeEnabled"])
-  conn.close()
 

Modified: arm/release/src/test.py
===================================================================
--- arm/release/src/test.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/test.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -11,6 +11,7 @@
   1. Resolver Performance Test
   2. Resolver Dump
   3. Glyph Demo
+  4. Exit Policy Check
   q. Quit
 
 Selection: """
@@ -23,7 +24,7 @@
   userInput = raw_input(MENU)
   
   # initiate the TorCtl connection if the test needs it
-  if userInput in ("1", "2") and not conn:
+  if userInput in ("1", "2", "4") and not conn:
     conn = torTools.getConn()
     conn.init()
     
@@ -43,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)
@@ -67,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: ")
@@ -101,6 +103,42 @@
     # Switching to a curses context and back repeatedly seems to screw up the
     # terminal. Just to be safe this ends the process after the demo.
     break
+  elif userInput == "4":
+    # display the current exit policy and query if destinations are allowed by it
+    exitPolicy = conn.getExitPolicy()
+    print("Exit Policy: %s" % exitPolicy)
+    printDivider()
+    
+    while True:
+      # provide the selection options
+      userSelection = raw_input("\nCheck if destination is allowed (q to go back): ")
+      userSelection = userSelection.replace(" ", "").strip() # removes all whitespace
+      
+      isValidQuery, isExitAllowed = True, False
+      if userSelection == "q":
+        printDivider()
+        break
+      elif connections.isValidIpAddress(userSelection):
+        # just an ip address (use port 80)
+        isExitAllowed = exitPolicy.check(userSelection, 80)
+      elif userSelection.isdigit():
+        # just a port (use a common ip like 4.2.2.2)
+        isExitAllowed = exitPolicy.check("4.2.2.2", userSelection)
+      elif ":" in userSelection:
+        # ip/port combination
+        ipAddr, port = userSelection.split(":", 1)
+        
+        if connections.isValidIpAddress(ipAddr) and port.isdigit():
+          isExitAllowed = exitPolicy.check(ipAddr, port)
+        else: isValidQuery = False
+      else: isValidQuery = False # invalid input
+      
+      if isValidQuery:
+        resultStr = "is" if isExitAllowed else "is *not*"
+        print("Exiting %s allowed to that destination" % resultStr)
+      else:
+        print("'%s' isn't a valid destination (should be an ip, port, or ip:port)\n" % userSelection)
+    
   else:
     print("'%s' isn't a valid selection\n" % userInput)
 

Modified: arm/release/src/util/__init__.py
===================================================================
--- arm/release/src/util/__init__.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/util/__init__.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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/release/src/util/conf.py
===================================================================
--- arm/release/src/util/conf.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/util/conf.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -105,36 +105,35 @@
       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
+        msg = "Config entry '%s' is expected to be a runlevel" % key
+        if default != None: msg += ", defaulting to '%s'" % default
         log.log(CONFIG["log.configEntryTypeError"], msg)
         val = default
     elif isinstance(default, bool):
       if val.lower() == "true": val = True
       elif val.lower() == "false": val = False
       else:
-        msg = "config entry '%s' is expected to be a boolean, defaulting to '%s'" % (key, str(default))
+        msg = "Config entry '%s' is expected to be a boolean, defaulting to '%s'" % (key, str(default))
         log.log(CONFIG["log.configEntryTypeError"], msg)
         val = default
     elif isinstance(default, int):
       try: val = int(val)
       except ValueError:
-        msg = "config entry '%s' is expected to be an integer, defaulting to '%i'" % (key, default)
+        msg = "Config entry '%s' is expected to be an integer, defaulting to '%i'" % (key, default)
         log.log(CONFIG["log.configEntryTypeError"], msg)
         val = default
     elif isinstance(default, float):
       try: val = float(val)
       except ValueError:
-        msg = "config entry '%s' is expected to be a float, defaulting to '%f'" % (key, default)
+        msg = "Config entry '%s' is expected to be a float, defaulting to '%f'" % (key, default)
         log.log(CONFIG["log.configEntryTypeError"], msg)
         val = default
     elif isinstance(default, list):
@@ -146,7 +145,7 @@
           entryKey, entryVal = entry.split("=>", 1)
           valMap[entryKey.strip()] = entryVal.strip()
         else:
-          msg = "ignoring invalid %s config entry (expected a mapping, but \"%s\" was missing \"=>\")" % (key, entry)
+          msg = "Ignoring invalid %s config entry (expected a mapping, but \"%s\" was missing \"=>\")" % (key, entry)
           log.log(CONFIG["log.configEntryTypeError"], msg)
       val = valMap
     
@@ -171,7 +170,7 @@
       
       # check if the count doesn't match
       if count != None and len(confComp) != count:
-        msg = "config entry '%s' is expected to be %i comma separated values" % (key, count)
+        msg = "Config entry '%s' is expected to be %i comma separated values" % (key, count)
         if default != None and (isinstance(default, list) or isinstance(default, tuple)):
           defaultStr = ", ".join([str(i) for i in default])
           msg += ", defaulting to '%s'" % defaultStr
@@ -201,7 +200,7 @@
     
     # validates the input, setting the errorMsg if there's a problem
     errorMsg = None
-    baseErrorMsg = "config entry '%s' is expected to %%s" % key
+    baseErrorMsg = "Config entry '%s' is expected to %%s" % key
     if default != None and (isinstance(default, list) or isinstance(default, tuple)):
       defaultStr = ", ".join([str(i) for i in default])
       baseErrorMsg += ", defaulting to '%s'" % defaultStr

Modified: arm/release/src/util/connections.py
===================================================================
--- arm/release/src/util/connections.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/util/connections.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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
@@ -80,11 +79,107 @@
           "log.connLookupFailed": log.INFO,
           "log.connLookupFailover": log.NOTICE,
           "log.connLookupAbandon": log.WARN,
-          "log.connLookupRateGrowing": None}
+          "log.connLookupRateGrowing": None,
+          "log.configEntryTypeError": log.NOTICE}
 
+PORT_USAGE = {}
+
 def loadConfig(config):
   config.update(CONFIG)
+  
+  for configKey in config.getKeys():
+    # fetches any port.label.* values
+    if configKey.startswith("port.label."):
+      portEntry = configKey[11:]
+      purpose = config.get(configKey)
+      
+      divIndex = portEntry.find("-")
+      if divIndex == -1:
+        # single port
+        if portEntry.isdigit():
+          PORT_USAGE[portEntry] = purpose
+        else:
+          msg = "Port value isn't numeric for entry: %s" % configKey
+          log.log(CONFIG["log.configEntryTypeError"], msg)
+      else:
+        try:
+          # range of ports (inclusive)
+          minPort = int(portEntry[:divIndex])
+          maxPort = int(portEntry[divIndex + 1:])
+          if minPort > maxPort: raise ValueError()
+          
+          for port in range(minPort, maxPort + 1):
+            PORT_USAGE[str(port)] = purpose
+        except ValueError:
+          msg = "Unable to parse port range for entry: %s" % configKey
+          log.log(CONFIG["log.configEntryTypeError"], msg)
 
+def isValidIpAddress(ipStr):
+  """
+  Returns true if input is a valid IPv4 address, false otherwise.
+  """
+  
+  # checks if theres four period separated values
+  if not ipStr.count(".") == 3: return False
+  
+  # checks that each value in the octet are decimal values between 0-255
+  for ipComp in ipStr.split("."):
+    if not ipComp.isdigit() or int(ipComp) < 0 or int(ipComp) > 255:
+      return False
+  
+  return True
+
+def isIpAddressPrivate(ipAddr):
+  """
+  Provides true if the IP address belongs on the local network or belongs to
+  loopback, false otherwise. These include:
+  Private ranges: 10.*, 172.16.* - 172.31.*, 192.168.*
+  Loopback: 127.*
+  
+  Arguments:
+    ipAddr - IP address to be checked
+  """
+  
+  # checks for any of the simple wildcard ranges
+  if ipAddr.startswith("10.") or ipAddr.startswith("192.168.") or ipAddr.startswith("127."):
+    return True
+  
+  # checks for the 172.16.* - 172.31.* range
+  if ipAddr.startswith("172.") and ipAddr.count(".") == 3:
+    secondOctet = ipAddr[4:ipAddr.find(".", 4)]
+    
+    if secondOctet.isdigit() and int(secondOctet) >= 16 and int(secondOctet) <= 31:
+      return True
+  
+  return False
+
+def ipToInt(ipAddr):
+  """
+  Provides an integer representation of the ip address, suitable for sorting.
+  
+  Arguments:
+    ipAddr - ip address to be converted
+  """
+  
+  total = 0
+  
+  for comp in ipAddr.split("."):
+    total *= 255
+    total += int(comp)
+  
+  return total
+
+def getPortUsage(port):
+  """
+  Provides the common use of a given port. If no useage is known then this
+  provides None.
+  
+  Arguments:
+    port - port number to look up
+  """
+  
+  return PORT_USAGE.get(port)
+
 def getResolverCommand(resolutionCmd, processName, processPid = ""):
   """
   Provides the command that would be processed for the given resolver type.
@@ -99,19 +194,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 = ""):
@@ -131,7 +226,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")
@@ -151,30 +246,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(":")
       
@@ -241,13 +336,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
 
@@ -317,22 +412,26 @@
     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
     
     self._connections = []        # connection cache (latest results)
+    self._resolutionCounter = 0   # number of successful connection resolutions
     self._isPaused = False
     self._halt = False            # terminates thread if true
     self._cond = threading.Condition()  # used for pausing the thread
@@ -371,6 +470,7 @@
         lookupTime = time.time() - resolveStart
         
         self._connections = connResults
+        self._resolutionCounter += 1
         
         newMinDefaultRate = 100 * lookupTime
         if self.defaultRate < newMinDefaultRate:
@@ -409,7 +509,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
@@ -428,6 +528,14 @@
     if self._halt: return []
     else: return list(self._connections)
   
+  def getResolutionCount(self):
+    """
+    Provides the number of successful resolutions so far. This can be used to
+    determine if the connection results are new for the caller or not.
+    """
+    
+    return self._resolutionCounter
+  
   def setPaused(self, isPause):
     """
     Allows or prevents further connection resolutions (this still makes use of
@@ -451,3 +559,168 @@
     self._cond.notifyAll()
     self._cond.release()
 
+class AppResolver:
+  """
+  Provides the names and pids of appliations attached to the given ports. This
+  stops attempting to query if it fails three times without successfully
+  getting lsof results.
+  """
+  
+  def __init__(self, scriptName = "python"):
+    """
+    Constructs a resolver instance.
+    
+    Arguments:
+      scriptName - name by which to all our own entries
+    """
+    
+    self.scriptName = scriptName
+    self.queryResults = {}
+    self.resultsLock = threading.RLock()
+    self._cond = threading.Condition()  # used for pausing when waiting for results
+    self.isResolving = False  # flag set if we're in the process of making a query
+    self.failureCount = 0     # -1 if we've made a successful query
+  
+  def getResults(self, maxWait=0):
+    """
+    Provides the last queried results. If we're in the process of making a
+    query then we can optionally block for a time to see if it finishes.
+    
+    Arguments:
+      maxWait - maximum second duration to block on getting results before
+                returning
+    """
+    
+    self._cond.acquire()
+    if self.isResolving and maxWait > 0:
+      self._cond.wait(maxWait)
+    self._cond.release()
+    
+    self.resultsLock.acquire()
+    results = dict(self.queryResults)
+    self.resultsLock.release()
+    
+    return results
+  
+  def resolve(self, ports):
+    """
+    Queues the given listing of ports to be resolved. This clears the last set
+    of results when completed.
+    
+    Arguments:
+      ports - list of ports to be resolved to applications
+    """
+    
+    if self.failureCount < 3:
+      self.isResolving = True
+      t = threading.Thread(target = self._queryApplications, kwargs = {"ports": ports})
+      t.setDaemon(True)
+      t.start()
+  
+  def _queryApplications(self, ports=[]):
+    """
+    Performs an lsof lookup on the given ports to get the command/pid tuples.
+    
+    Arguments:
+      ports - list of ports to be resolved to applications
+    """
+    
+    # atagar at fenrir:~/Desktop/arm$ lsof -i tcp:51849 -i tcp:37277
+    # COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
+    # tor     2001 atagar   14u  IPv4  14048      0t0  TCP localhost:9051->localhost:37277 (ESTABLISHED)
+    # tor     2001 atagar   15u  IPv4  22024      0t0  TCP localhost:9051->localhost:51849 (ESTABLISHED)
+    # python  2462 atagar    3u  IPv4  14047      0t0  TCP localhost:37277->localhost:9051 (ESTABLISHED)
+    # python  3444 atagar    3u  IPv4  22023      0t0  TCP localhost:51849->localhost:9051 (ESTABLISHED)
+    
+    if not ports:
+      self.resultsLock.acquire()
+      self.queryResults = {}
+      self.isResolving = False
+      self.resultsLock.release()
+      
+      # wakes threads waiting on results
+      self._cond.acquire()
+      self._cond.notifyAll()
+      self._cond.release()
+      
+      return
+    
+    results = {}
+    lsofArgs = []
+    
+    # Uses results from the last query if we have any, otherwise appends the
+    # port to the lsof command. This has the potential for persisting dirty
+    # results but if we're querying by the dynamic port on the local tcp
+    # connections then this should be very rare (and definitely worth the
+    # chance of being able to skip an lsof query altogether).
+    for port in ports:
+      if port in self.queryResults:
+        results[port] = self.queryResults[port]
+      else: lsofArgs.append("-i tcp:%s" % port)
+    
+    if lsofArgs:
+      lsofResults = sysTools.call("lsof -nP " + " ".join(lsofArgs))
+    else: lsofResults = None
+    
+    if not lsofResults and self.failureCount != -1:
+      # lsof query failed and we aren't yet sure if it's possible to
+      # successfully get results on this platform
+      self.failureCount += 1
+      self.isResolving = False
+      return
+    elif lsofResults:
+      # (iPort, oPort) tuple for our own process, if it was fetched
+      ourConnection = None
+      
+      for line in lsofResults:
+        lineComp = line.split()
+        
+        if len(lineComp) == 10 and lineComp[9] == "(ESTABLISHED)":
+          cmd, pid, _, _, _, _, _, _, portMap, _ = lineComp
+          
+          if "->" in portMap:
+            iPort, oPort = portMap.split("->")
+            iPort = iPort.split(":")[1]
+            oPort = oPort.split(":")[1]
+            
+            # entry belongs to our own process
+            if pid == str(os.getpid()):
+              cmd = self.scriptName
+              ourConnection = (iPort, oPort)
+            
+            if iPort.isdigit() and oPort.isdigit():
+              newEntry = (iPort, oPort, cmd, pid)
+              
+              # adds the entry under the key of whatever we queried it with
+              # (this might be both the inbound _and_ outbound ports)
+              for portMatch in (iPort, oPort):
+                if portMatch in ports:
+                  if portMatch in results:
+                    results[portMatch].append(newEntry)
+                  else: results[portMatch] = [newEntry]
+      
+      # making the lsof call generated an extraneous sh entry for our own connection
+      if ourConnection:
+        for ourPort in ourConnection:
+          if ourPort in results:
+            shIndex = None
+            
+            for i in range(len(results[ourPort])):
+              if results[ourPort][i][2] == "sh":
+                shIndex = i
+                break
+            
+            if shIndex != None:
+              del results[ourPort][shIndex]
+    
+    self.resultsLock.acquire()
+    self.failureCount = -1
+    self.queryResults = results
+    self.isResolving = False
+    self.resultsLock.release()
+    
+    # wakes threads waiting on results
+    self._cond.acquire()
+    self._cond.notifyAll()
+    self._cond.release()
+

Copied: arm/release/src/util/enum.py (from rev 24554, arm/trunk/src/util/enum.py)
===================================================================
--- arm/release/src/util/enum.py	                        (rev 0)
+++ arm/release/src/util/enum.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -0,0 +1,116 @@
+"""
+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) == 0: words.append("")
+    elif len(entry) == 1: words.append(entry.upper())
+    else: words.append(entry[0].upper() + entry[1:].lower())
+  
+  return " ".join(words)
+
+class Enum:
+  """
+  Basic enumeration.
+  """
+  
+  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 indexOf(self, value):
+    """
+    Provides the index of the given value in the collection. This raises a
+    ValueError if no such element exists.
+    
+    Arguments:
+      value - entry to be looked up
+    """
+    
+    return self.orderedValues.index(value)
+  
+  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):
+    Enum.__init__(self)
+    self.__dict__.update(args)
+    self.orderedValues = sorted(args.values())
+

Modified: arm/release/src/util/log.py
===================================================================
--- arm/release/src/util/log.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/util/log.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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/release/src/util/panel.py
===================================================================
--- arm/release/src/util/panel.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/util/panel.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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.
@@ -170,7 +188,7 @@
     if setWidth != -1: newWidth = min(newWidth, setWidth)
     return (newHeight, newWidth)
   
-  def draw(self, subwindow, width, height):
+  def draw(self, width, height):
     """
     Draws display's content. This is meant to be overwritten by 
     implementations and not called directly (use redraw() instead). The
@@ -178,10 +196,8 @@
     a column less than the actual space.
     
     Arguments:
-      sudwindow - panel's current subwindow instance, providing raw access to
-                  its curses functions
-      width     - horizontal space available for content
-      height    - vertical space available for content
+      width  - horizontal space available for content
+      height - vertical space available for content
     """
     
     pass
@@ -198,6 +214,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:
@@ -222,11 +241,70 @@
     try:
       if forceRedraw:
         self.win.erase() # clears any old contents
-        self.draw(self.win, self.maxX - 1, self.maxY)
+        self.draw(self.maxX - 1, self.maxY)
       self.win.refresh()
     finally:
       CURSES_LOCK.release()
   
+  def hline(self, y, x, length, attr=curses.A_NORMAL):
+    """
+    Draws a horizontal line. This should only be called from the context of a
+    panel's draw method.
+    
+    Arguments:
+      y      - vertical location
+      x      - horizontal location
+      length - length the line spans
+      attr   - text attributes
+    """
+    
+    if self.win and self.maxX > x and self.maxY > y:
+      try:
+        drawLength = min(length, self.maxX - x)
+        self.win.hline(y, x, curses.ACS_HLINE | attr, drawLength)
+      except:
+        # in edge cases drawing could cause a _curses.error
+        pass
+  
+  def vline(self, y, x, length, attr=curses.A_NORMAL):
+    """
+    Draws a vertical line. This should only be called from the context of a
+    panel's draw method.
+    
+    Arguments:
+      y      - vertical location
+      x      - horizontal location
+      length - length the line spans
+      attr   - text attributes
+    """
+    
+    if self.win and self.maxX > x and self.maxY > y:
+      try:
+        drawLength = min(length, self.maxY - y)
+        self.win.vline(y, x, curses.ACS_VLINE | attr, drawLength)
+      except:
+        # in edge cases drawing could cause a _curses.error
+        pass
+  
+  def addch(self, y, x, char, attr=curses.A_NORMAL):
+    """
+    Draws a single character. This should only be called from the context of a
+    panel's draw method.
+    
+    Arguments:
+      y    - vertical location
+      x    - horizontal location
+      char - character to be drawn
+      attr - text attributes
+    """
+    
+    if self.win and self.maxX > x and self.maxY > y:
+      try:
+        self.win.addch(y, x, char, attr)
+      except:
+        # in edge cases drawing could cause a _curses.error
+        pass
+  
   def addstr(self, y, x, msg, attr=curses.A_NORMAL):
     """
     Writes string to subwindow if able. This takes into account screen bounds

Modified: arm/release/src/util/procTools.py
===================================================================
--- arm/release/src/util/procTools.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/util/procTools.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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/release/src/util/sysTools.py
===================================================================
--- arm/release/src/util/sysTools.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/util/sysTools.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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/release/src/util/torConfig.py
===================================================================
--- arm/release/src/util/torConfig.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/util/torConfig.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -5,9 +5,10 @@
 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": [],
           "torrc.alias": {},
           "torrc.label.size.b": [],
           "torrc.label.size.kb": [],
@@ -22,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
@@ -51,10 +48,13 @@
 MULTILINE_PARAM = None # cached multiline parameters (lazily loaded)
 
 def loadConfig(config):
-  CONFIG["torrc.alias"] = config.get("torrc.alias", {})
+  config.update(CONFIG)
   
-  # fetches any config.summary.* values
+  # stores lowercase entries to drop case sensitivity
+  CONFIG["config.important"] = [entry.lower() for entry in CONFIG["config.important"]]
+  
   for configKey in config.getKeys():
+    # fetches any config.summary.* values
     if configKey.startswith("config.summary."):
       CONFIG[configKey.lower()] = config.get(configKey)
   
@@ -119,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()
         
@@ -138,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)
           
@@ -183,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()
@@ -217,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)
@@ -249,7 +244,7 @@
 def saveOptionDescriptions(path):
   """
   Preserves the current configuration descriptors to the given path. This
-  raises an IOError if unable to do so.
+  raises an IOError or OSError if unable to do so.
   
   Arguments:
     path - location to persist configuration descriptors
@@ -269,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()
@@ -286,6 +281,17 @@
   
   return CONFIG.get("config.summary.%s" % option.lower())
 
+def isImportant(option):
+  """
+  Provides True if the option has the 'important' flag in the configuration,
+  False otherwise.
+  
+  Arguments:
+    option - tor config option
+  """
+  
+  return option.lower() in CONFIG["config.important"]
+
 def getConfigDescription(option):
   """
   Provides ManPageEntry instances populated with information fetched from the
@@ -345,19 +351,31 @@
   
   return tuple(MULTILINE_PARAM)
 
-def getCustomOptions():
+def getCustomOptions(includeValue = False):
   """
-  Provides the set of torrc parameters that differ from their defaults.
+  Provides the torrc parameters that differ from their defaults.
+  
+  Arguments:
+    includeValue - provides the current value with results if true, otherwise
+                   this just contains the options
   """
   
-  customOptions, conn = set(), torTools.getConn()
-  configTextQuery = conn.getInfo("config-text", "").strip().split("\n")
+  configText = torTools.getConn().getInfo("config-text", "").strip()
+  configLines = configText.split("\n")
   
-  for entry in configTextQuery:
-    # tor provides a Log entry even if it matches the default
-    if entry != "Log notice stdout":
-      customOptions.add(entry[:entry.find(" ")])
-  return customOptions
+  # removes any duplicates
+  configLines = list(set(configLines))
+  
+  # The "GETINFO config-text" query only provides options that differ
+  # from Tor's defaults with the exception of its Log entry which, even
+  # if undefined, returns "Log notice stdout" as per:
+  # https://trac.torproject.org/projects/tor/ticket/2362
+  
+  try: configLines.remove("Log notice stdout")
+  except ValueError: pass
+  
+  if includeValue: return configLines
+  else: return [line[:line.find(" ")] for line in configLines]
 
 def validate(contents = None):
   """
@@ -403,13 +421,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"]:
@@ -444,17 +462,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
 
@@ -470,13 +488,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):
   """
@@ -489,13 +507,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):
   """
@@ -622,13 +640,23 @@
     
     self.valsLock.acquire()
     
+    # The torrc validation relies on 'GETINFO config-text' which was
+    # introduced in tor 0.2.2.7-alpha so if we're using an earlier version
+    # (or configured to skip torrc validation) then this is a no-op. For more
+    # information see:
+    # https://trac.torproject.org/projects/tor/ticket/2501
+    
     if not self.isLoaded(): returnVal = None
-    elif not CONFIG["features.torrc.validate"]: returnVal = {}
     else:
-      if self.corrections == None:
-        self.corrections = validate(self.contents)
+      skipValidation = not CONFIG["features.torrc.validate"]
+      skipValidation |= not torTools.getConn().isVersion("0.2.2.7-alpha")
       
-      returnVal = list(self.corrections)
+      if skipValidation: returnVal = {}
+      else:
+        if self.corrections == None:
+          self.corrections = validate(self.contents)
+        
+        returnVal = list(self.corrections)
     
     self.valsLock.release()
     return returnVal

Modified: arm/release/src/util/torTools.py
===================================================================
--- arm/release/src/util/torTools.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/util/torTools.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -17,13 +17,25 @@
 
 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).
+DIR_SERVERS = [("86.59.21.38", "80"),         # tor26
+               ("128.31.0.39", "9031"),       # moria1
+               ("216.224.124.114", "9030"),   # ides
+               ("212.112.245.170", "80"),     # gabelmoo
+               ("194.109.206.212", "80"),     # dizum
+               ("193.23.244.244", "80"),      # dannenberg
+               ("208.83.223.34", "443"),      # urras
+               ("213.115.239.118", "443"),    # maatuska
+               ("82.94.251.203", "80")]       # Tonga
+
 # message logged by default when a controller can't set an event type
 DEFAULT_FAILED_EVENT_MSG = "Unsupported event type: %s"
 
@@ -41,9 +53,21 @@
 # options (unchangable, even with a SETCONF) and other useful stats
 CACHE_ARGS = ("version", "config-file", "exit-policy/default", "fingerprint",
               "config/names", "info/names", "features/names", "events/names",
-              "nsEntry", "descEntry", "bwRate", "bwBurst", "bwObserved",
-              "bwMeasured", "flags", "pid", "pathPrefix", "startTime")
+              "nsEntry", "descEntry", "address", "bwRate", "bwBurst",
+              "bwObserved", "bwMeasured", "flags", "parsedVersion", "pid",
+              "pathPrefix", "startTime", "authorities", "circuits", "hsPorts")
+CACHE_GETINFO_PREFIX_ARGS = ("ip-to-country/", )
 
+# Tor has a couple messages (in or/router.c) for when our ip address changes:
+# "Our IP Address has changed from <previous> to <current>; rebuilding
+#   descriptor (source: <source>)."
+# "Guessed our IP address as <current> (source: <source>)."
+# 
+# It would probably be preferable to use the EXTERNAL_ADDRESS event, but I'm
+# not quite sure why it's not provided by check_descriptor_ipaddress_changed
+# so erring on the side of inclusiveness by using the notice event instead.
+ADDR_CHANGED_MSG_PREFIX = ("Our IP Address has changed from", "Guessed our IP address as")
+
 TOR_CTL_CLOSE_MSG = "Tor closed control connection. Exiting event thread."
 UNKNOWN = "UNKNOWN" # value used by cached information if undefined
 CONFIG = {"torrc.map": {},
@@ -52,6 +76,7 @@
           "log.torGetInfo": log.DEBUG,
           "log.torGetInfoCache": None,
           "log.torGetConf": log.DEBUG,
+          "log.torGetConfCache": None,
           "log.torSetConf": log.INFO,
           "log.torPrefixPathInvalid": log.NOTICE,
           "log.bsdJailFound": log.INFO,
@@ -65,9 +90,22 @@
               "NS": "information related to the consensus will grow stale",
               "NEWCONSENSUS": "information related to the consensus will grow stale"}
 
+# number of sequential attempts before we decide that the Tor geoip database
+# is unavailable
+GEOIP_FAILURE_THRESHOLD = 5
+
 # provides int -> str mappings for torctl event runlevels
 TORCTL_RUNLEVELS = dict([(val, key) for (key, val) in TorUtil.loglevels.items()])
 
+# ip address ranges substituted by the 'private' keyword
+PRIVATE_IP_RANGES = ("0.0.0.0/8", "169.254.0.0/16", "127.0.0.0/8", "192.168.0.0/16", "10.0.0.0/8", "172.16.0.0/12")
+
+# This prevents controllers from spawning worker threads (and by extension
+# notifying status listeners). This is important when shutting down to prevent
+# rogue threads from being alive during shutdown.
+
+NO_SPAWN = False
+
 def loadConfig(config):
   config.update(CONFIG)
 
@@ -185,6 +223,41 @@
   log.log(CONFIG["log.unknownBsdJailId"], "Failed to figure out the FreeBSD jail id. Assuming 0.")
   return 0
 
+def parseVersion(versionStr):
+  """
+  Parses the given version string into its expected components, for instance...
+  '0.2.2.13-alpha (git-feb8c1b5f67f2c6f)'
+  
+  would provide:
+  (0, 2, 2, 13, 'alpha')
+  
+  If the input isn't recognized then this returns None.
+  
+  Arguments:
+    versionStr - version string to be parsed
+  """
+  
+  # crops off extra arguments, for instance:
+  # '0.2.2.13-alpha (git-feb8c1b5f67f2c6f)' -> '0.2.2.13-alpha'
+  versionStr = versionStr.split()[0]
+  
+  result = None
+  if versionStr.count(".") in (2, 3):
+    # parses the optional suffix ('alpha', 'release', etc)
+    if versionStr.count("-") == 1:
+      versionStr, versionSuffix = versionStr.split("-")
+    else: versionSuffix = ""
+    
+    # Parses the numeric portion of the version. This can have three or four
+    # entries depending on if an optional patch level was provided.
+    try:
+      versionComp = [int(entry) for entry in versionStr.split(".")]
+      if len(versionComp) == 3: versionComp += [0]
+      result = tuple(versionComp + [versionSuffix])
+    except ValueError: pass
+  
+  return result
+
 def getConn():
   """
   Singleton constructor for a Controller. Be aware that this starts as being
@@ -210,18 +283,29 @@
     self.torctlListeners = []           # callback functions for TorCtl events
     self.statusListeners = []           # callback functions for tor's state changes
     self.controllerEvents = []          # list of successfully set controller events
+    self._fingerprintMappings = None    # mappings of ip -> [(port, fingerprint), ...]
+    self._fingerprintLookupCache = {}   # lookup cache with (ip, port) -> fingerprint mappings
+    self._fingerprintsAttachedCache = None # cache of relays we're connected to
+    self._nicknameLookupCache = {}      # lookup cache with fingerprint -> nickname mappings
+    self._consensusLookupCache = {}     # lookup cache with network status entries
+    self._descriptorLookupCache = {}    # lookup cache with relay descriptors
     self._isReset = False               # internal flag for tracking resets
-    self._status = 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
     
+    self._exitPolicyChecker = None
+    self._isExitingAllowed = False
+    self._exitPolicyLookupCache = {}    # mappings of ip/port tuples to if they were accepted by the policy or not
+    
     # Logs issues and notices when fetching the path prefix if true. This is
     # only done once for the duration of the application to avoid pointless
     # messages.
     self._pathPrefixLogging = True
     
-    # cached GETINFO parameters (None if unset or possibly changed)
-    self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
+    # cached parameters for GETINFO and custom getters (None if unset or
+    # possibly changed)
+    self._cachedParam = {}
     
     # cached GETCONF parameters, entries consisting of:
     # (option, fetch_type) => value
@@ -230,6 +314,9 @@
     # directs TorCtl to notify us of events
     TorUtil.logger = self
     TorUtil.loglevel = "DEBUG"
+    
+    # tracks the number of sequential geoip lookup failures
+    self.geoipFailureCount = 0
   
   def init(self, conn=None):
     """
@@ -254,17 +341,30 @@
       self.conn.add_event_listener(self)
       for listener in self.eventListeners: self.conn.add_event_listener(listener)
       
+      # reset caches for ip -> fingerprint lookups
+      self._fingerprintMappings = None
+      self._fingerprintLookupCache = {}
+      self._fingerprintsAttachedCache = None
+      self._nicknameLookupCache = {}
+      self._consensusLookupCache = {}
+      self._descriptorLookupCache = {}
+      
+      self._exitPolicyChecker = self.getExitPolicy()
+      self._isExitingAllowed = self._exitPolicyChecker.isExitingAllowed()
+      self._exitPolicyLookupCache = {}
+      
       # sets the events listened for by the new controller (incompatible events
       # are dropped with a logged warning)
       self.setControllerEvents(self.controllerEvents)
       
       self.connLock.release()
       
-      self._status = TOR_INIT
+      self._status = State.INIT
       self._statusTime = time.time()
       
       # notifies listeners that a new controller is available
-      thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,))
+      if not NO_SPAWN:
+        thread.start_new_thread(self._notifyStatusListeners, (State.INIT,))
   
   def close(self):
     """
@@ -274,14 +374,29 @@
     self.connLock.acquire()
     if self.conn:
       self.conn.close()
+      
+      # 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
-      thread.start_new_thread(self._notifyStatusListeners, (TOR_CLOSED,))
+      if not NO_SPAWN:
+        thread.start_new_thread(self._notifyStatusListeners, (State.CLOSED,))
     else: self.connLock.release()
   
   def isAlive(self):
@@ -337,21 +452,44 @@
     
     self.connLock.acquire()
     
+    isGeoipRequest = param.startswith("ip-to-country/")
+    
+    # checks if this is an arg caching covers
+    isCacheArg = param in CACHE_ARGS
+    
+    if not isCacheArg:
+      for prefix in CACHE_GETINFO_PREFIX_ARGS:
+        if param.startswith(prefix):
+          isCacheArg = True
+          break
+    
     startTime = time.time()
     result, raisedExc, isFromCache = default, None, False
     if self.isAlive():
-      if param in CACHE_ARGS and self._cachedParam[param]:
-        result = self._cachedParam[param]
+      cachedValue = self._cachedParam.get(param)
+      
+      if isCacheArg and cachedValue:
+        result = cachedValue
         isFromCache = True
+      elif isGeoipRequest and self.geoipFailureCount == GEOIP_FAILURE_THRESHOLD:
+        # the geoip database aleady looks to be unavailable - abort the request
+        raisedExc = TorCtl.ErrorReply("Tor geoip database is unavailable.")
       else:
         try:
           getInfoVal = self.conn.get_info(param)[param]
           if getInfoVal != None: result = getInfoVal
+          if isGeoipRequest: self.geoipFailureCount = 0
         except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
           if type(exc) == TorCtl.TorCtlClosed: self.close()
           raisedExc = exc
+          
+          if isGeoipRequest:
+            self.geoipFailureCount += 1
+            
+            if self.geoipFailureCount == GEOIP_FAILURE_THRESHOLD:
+              log.log(CONFIG["log.geoipUnavailable"], "Tor geoip database is unavailable.")
     
-    if not isFromCache and result and param in CACHE_ARGS:
+    if isCacheArg and result and not isFromCache:
       self._cachedParam[param] = result
     
     if isFromCache:
@@ -439,9 +577,9 @@
     result = {} if fetchType == "map" else []
     
     if self.isAlive():
-      if (param, fetchType) in self._cachedConf:
+      if (param.lower(), fetchType) in self._cachedConf:
         isFromCache = True
-        result = self._cachedConf[(param, fetchType)]
+        result = self._cachedConf[(param.lower(), fetchType)]
       else:
         try:
           if fetchType == "str":
@@ -458,15 +596,18 @@
           if type(exc) == TorCtl.TorCtlClosed: self.close()
           result, raisedExc = default, exc
     
-    if not isFromCache and result:
+    if not isFromCache:
       cacheValue = result
       if fetchType == "list": cacheValue = list(result)
       elif fetchType == "map": cacheValue = dict(result)
-      self._cachedConf[(param, fetchType)] = cacheValue
+      self._cachedConf[(param.lower(), fetchType)] = cacheValue
     
-    runtimeLabel = "cache fetch" if isFromCache else "runtime: %0.4f" % (time.time() - startTime)
-    msg = "GETCONF %s (%s)" % (param, runtimeLabel)
-    log.log(CONFIG["log.torGetConf"], msg)
+    if isFromCache:
+      msg = "GETCONF %s (cache fetch)" % param
+      log.log(CONFIG["log.torGetConfCache"], msg)
+    else:
+      msg = "GETCONF %s (runtime: %0.4f)" % (param, time.time() - startTime)
+      log.log(CONFIG["log.torGetConf"], msg)
     
     self.connLock.release()
     
@@ -496,10 +637,16 @@
         
         # flushing cached values (needed until we can detect SETCONF calls)
         for fetchType in ("str", "list", "map"):
-          entry = (param, fetchType)
+          entry = (param.lower(), fetchType)
           
           if entry in self._cachedConf:
             del self._cachedConf[entry]
+        
+        # special caches for the exit policy
+        if param.lower() == "exitpolicy":
+          self._exitPolicyChecker = self.getExitPolicy()
+          self._isExitingAllowed = self._exitPolicyChecker.isExitingAllowed()
+          self._exitPolicyLookupCache = {}
       except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
         if type(exc) == TorCtl.TorCtlClosed: self.close()
         elif type(exc) == TorCtl.ErrorReply:
@@ -527,6 +674,27 @@
     
     if raisedExc: raise raisedExc
   
+  def getCircuits(self, default = []):
+    """
+    This provides a list with tuples of the form:
+    (circuitID, status, purpose, (fingerprint1, fingerprint2...))
+    
+    Arguments:
+      default - value provided back if unable to query the circuit-status
+    """
+    
+    return self._getRelayAttr("circuits", default)
+  
+  def getHiddenServicePorts(self, default = []):
+    """
+    Provides the target ports hidden services are configured to use.
+    
+    Arguments:
+      default - value provided back if unable to query the hidden service ports
+    """
+    
+    return self._getRelayAttr("hsPorts", default)
+  
   def getMyNetworkStatus(self, default = None):
     """
     Provides the network status entry for this relay if available. This is
@@ -615,6 +783,53 @@
     
     return self._getRelayAttr("flags", default)
   
+  def isVersion(self, minVersionStr):
+    """
+    Checks if we meet the given version. Recognized versions are of the form:
+    <major>.<minor>.<micro>[.<patch>][-<status_tag>]
+    
+    for instance, "0.2.2.13-alpha" or "0.2.1.5". This raises a ValueError if
+    the input isn't recognized, and returns False if unable to fetch our
+    instance's version.
+    
+    According to the spec the status_tag is purely informal, so it's ignored
+    in comparisons.
+    
+    Arguments:
+      minVersionStr - version to be compared against
+    """
+    
+    minVersion = parseVersion(minVersionStr)
+    
+    if minVersion == None:
+      raise ValueError("unrecognized version: %s" % minVersionStr)
+    
+    self.connLock.acquire()
+    
+    result = False
+    if self.isAlive():
+      myVersion = self._getRelayAttr("parsedVersion", None)
+      
+      if not myVersion:
+        result = False
+      elif myVersion[:4] == minVersion[:4]:
+        result = True # versions match
+      else:
+        # compares each of the numeric portions of the version
+        for i in range(4):
+          myVal, minVal = myVersion[i], minVersion[i]
+          
+          if myVal > minVal:
+            result = True
+            break
+          elif myVal < minVal:
+            result = False
+            break
+    
+    self.connLock.release()
+    
+    return result
+  
   def getMyPid(self):
     """
     Provides the pid of the attached tor process (None if no controller exists
@@ -623,6 +838,16 @@
     
     return self._getRelayAttr("pid", None)
   
+  def getMyDirAuthorities(self):
+    """
+    Provides a listing of IP/port tuples for the directory authorities we've
+    been configured to use. If set in the configuration then these are custom
+    authorities, otherwise its an estimate of what Tor has been hardcoded to
+    use (unfortunately, this might be out of date).
+    """
+    
+    return self._getRelayAttr("authorities", [])
+  
   def getPathPrefix(self):
     """
     Provides the path prefix that should be used for fetching tor resources.
@@ -630,10 +855,7 @@
     jail's path.
     """
     
-    result = self._getRelayAttr("pathPrefix", "")
-    
-    if result == UNKNOWN: return ""
-    else: return result
+    return self._getRelayAttr("pathPrefix", "")
   
   def getStartTime(self):
     """
@@ -641,10 +863,7 @@
     can't be determined then this provides None.
     """
     
-    result = self._getRelayAttr("startTime", None)
-    
-    if result == UNKNOWN: return None
-    else: return result
+    return self._getRelayAttr("startTime", None)
   
   def getStatus(self):
     """
@@ -655,6 +874,199 @@
     
     return (self._status, self._statusTime)
   
+  def isExitingAllowed(self, ipAddress, port):
+    """
+    Checks if the given destination can be exited to by this relay, returning
+    True if so and False otherwise.
+    """
+    
+    self.connLock.acquire()
+    
+    result = False
+    if self.isAlive():
+      # query the policy if it isn't yet cached
+      if not (ipAddress, port) in self._exitPolicyLookupCache:
+        # If we allow any exiting then this could be relayed DNS queries,
+        # otherwise the policy is checked. Tor still makes DNS connections to
+        # test when exiting isn't allowed, but nothing is relayed over them.
+        # I'm registering these as non-exiting to avoid likely user confusion:
+        # https://trac.torproject.org/projects/tor/ticket/965
+        
+        if self._isExitingAllowed and port == "53": isAccepted = True
+        else: isAccepted = self._exitPolicyChecker.check(ipAddress, port)
+        self._exitPolicyLookupCache[(ipAddress, port)] = isAccepted
+      
+      result = self._exitPolicyLookupCache[(ipAddress, port)]
+    
+    self.connLock.release()
+    
+    return result
+  
+  def getExitPolicy(self):
+    """
+    Provides an ExitPolicy instance for the head of this relay's exit policy
+    chain. If there's no active connection then this provides None.
+    """
+    
+    self.connLock.acquire()
+    
+    result = None
+    if self.isAlive():
+      if self.getOption("ORPort"):
+        policyEntries = []
+        for exitPolicy in self.getOption("ExitPolicy", [], True):
+          policyEntries += [policy.strip() for policy in exitPolicy.split(",")]
+        
+        # appends the default exit policy
+        defaultExitPolicy = self.getInfo("exit-policy/default")
+        
+        if defaultExitPolicy:
+          policyEntries += defaultExitPolicy.split(",")
+        
+        # construct the policy chain backwards
+        policyEntries.reverse()
+        
+        for entry in policyEntries:
+          result = ExitPolicy(entry, result)
+        
+        # Checks if we are rejecting private connections. If set, this appends
+        # 'reject private' and 'reject <my ip>' to the start of our policy chain.
+        isPrivateRejected = self.getOption("ExitPolicyRejectPrivate", True)
+        
+        if isPrivateRejected:
+          result = ExitPolicy("reject private", result)
+          
+          myAddress = self.getInfo("address")
+          if myAddress: result = ExitPolicy("reject %s" % myAddress, result)
+      else:
+        # no ORPort is set so all relaying is disabled
+        result = ExitPolicy("reject *:*")
+    
+    self.connLock.release()
+    
+    return result
+  
+  def getConsensusEntry(self, relayFingerprint):
+    """
+    Provides the most recently available consensus information for the given
+    relay. This is none if no such information exists.
+    
+    Arguments:
+      relayFingerprint - fingerprint of the relay
+    """
+    
+    self.connLock.acquire()
+    
+    result = None
+    if self.isAlive():
+      if not relayFingerprint in self._consensusLookupCache:
+        nsEntry = self.getInfo("ns/id/%s" % relayFingerprint)
+        self._consensusLookupCache[relayFingerprint] = nsEntry
+      
+      result = self._consensusLookupCache[relayFingerprint]
+    
+    self.connLock.release()
+    
+    return result
+  
+  def getDescriptorEntry(self, relayFingerprint):
+    """
+    Provides the most recently available descriptor information for the given
+    relay. Unless FetchUselessDescriptors is set this may frequently be
+    unavailable. If no such descriptor is available then this returns None.
+    
+    Arguments:
+      relayFingerprint - fingerprint of the relay
+    """
+    
+    self.connLock.acquire()
+    
+    result = None
+    if self.isAlive():
+      if not relayFingerprint in self._descriptorLookupCache:
+        descEntry = self.getInfo("desc/id/%s" % relayFingerprint)
+        self._descriptorLookupCache[relayFingerprint] = descEntry
+      
+      result = self._descriptorLookupCache[relayFingerprint]
+    
+    self.connLock.release()
+    
+    return result
+  
+  def getRelayFingerprint(self, relayAddress, relayPort = None, getAllMatches = False):
+    """
+    Provides the fingerprint associated with the given address. If there's
+    multiple potential matches or the mapping is unknown then this returns
+    None. This disambiguates the fingerprint if there's multiple relays on
+    the same ip address by several methods, one of them being to pick relays
+    we have a connection with.
+    
+    Arguments:
+      relayAddress  - address of relay to be returned
+      relayPort     - orport of relay (to further narrow the results)
+      getAllMatches - ignores the relayPort and provides all of the
+                      (port, fingerprint) tuples matching the given
+                      address
+    """
+    
+    self.connLock.acquire()
+    
+    result = None
+    if self.isAlive():
+      if getAllMatches:
+        # populates the ip -> fingerprint mappings if not yet available
+        if self._fingerprintMappings == None:
+          self._fingerprintMappings = self._getFingerprintMappings()
+        
+        if relayAddress in self._fingerprintMappings:
+          result = self._fingerprintMappings[relayAddress]
+        else: result = []
+      else:
+        # query the fingerprint if it isn't yet cached
+        if not (relayAddress, relayPort) in self._fingerprintLookupCache:
+          relayFingerprint = self._getRelayFingerprint(relayAddress, relayPort)
+          self._fingerprintLookupCache[(relayAddress, relayPort)] = relayFingerprint
+        
+        result = self._fingerprintLookupCache[(relayAddress, relayPort)]
+    
+    self.connLock.release()
+    
+    return result
+  
+  def getRelayNickname(self, relayFingerprint):
+    """
+    Provides the nickname associated with the given relay. This provides None
+    if no such relay exists, and "Unnamed" if the name hasn't been set.
+    
+    Arguments:
+      relayFingerprint - fingerprint of the relay
+    """
+    
+    self.connLock.acquire()
+    
+    result = None
+    if self.isAlive():
+      # query the nickname if it isn't yet cached
+      if not relayFingerprint in self._nicknameLookupCache:
+        if relayFingerprint == self.getInfo("fingerprint"):
+          # this is us, simply check the config
+          myNickname = self.getOption("Nickname", "Unnamed")
+          self._nicknameLookupCache[relayFingerprint] = myNickname
+        else:
+          # check the consensus for the relay
+          nsEntry = self.getConsensusEntry(relayFingerprint)
+          
+          if nsEntry: relayNickname = nsEntry[2:nsEntry.find(" ", 2)]
+          else: relayNickname = None
+          
+          self._nicknameLookupCache[relayFingerprint] = relayNickname
+      
+      result = self._nicknameLookupCache[relayFingerprint]
+    
+    self.connLock.release()
+    
+    return result
+  
   def addEventListener(self, listener):
     """
     Directs further tor controller events to callback functions of the
@@ -825,7 +1237,7 @@
       if not issueSighup:
         try:
           self.conn.send_signal("RELOAD")
-          self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
+          self._cachedParam = {}
           self._cachedConf = {}
         except Exception, exc:
           # new torrc parameters caused an error (tor's likely shut down)
@@ -870,7 +1282,7 @@
             if errorLine: raise IOError(" ".join(errorLine.split()[3:]))
             else: raise IOError("failed silently")
           
-          self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
+          self._cachedParam = {}
           self._cachedConf = {}
         except IOError, exc:
           raisedException = exc
@@ -888,13 +1300,15 @@
     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()
       
-      thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,))
+      if not NO_SPAWN:
+        thread.start_new_thread(self._notifyStatusListeners, (State.INIT,))
   
   def ns_event(self, event):
     self._updateHeartbeat()
+    self._consensusLookupCache = {}
     
     myFingerprint = self.getInfo("fingerprint")
     if myFingerprint:
@@ -912,20 +1326,77 @@
   def new_consensus_event(self, event):
     self._updateHeartbeat()
     
+    self.connLock.acquire()
+    
     self._cachedParam["nsEntry"] = None
     self._cachedParam["flags"] = None
     self._cachedParam["bwMeasured"] = None
+    
+    # reconstructs consensus based mappings
+    self._fingerprintLookupCache = {}
+    self._fingerprintsAttachedCache = None
+    self._nicknameLookupCache = {}
+    self._consensusLookupCache = {}
+    
+    if self._fingerprintMappings != None:
+      self._fingerprintMappings = self._getFingerprintMappings(event.nslist)
+    
+    self.connLock.release()
   
   def new_desc_event(self, event):
     self._updateHeartbeat()
     
+    self.connLock.acquire()
+    
     myFingerprint = self.getInfo("fingerprint")
     if not myFingerprint or myFingerprint in event.idlist:
       self._cachedParam["descEntry"] = None
       self._cachedParam["bwObserved"] = None
+    
+    # If we're tracking ip address -> fingerprint mappings then update with
+    # the new relays.
+    self._fingerprintLookupCache = {}
+    self._fingerprintsAttachedCache = None
+    self._descriptorLookupCache = {}
+    
+    if self._fingerprintMappings != None:
+      for fingerprint in event.idlist:
+        # gets consensus data for the new descriptor
+        try: nsLookup = self.conn.get_network_status("id/%s" % fingerprint)
+        except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): continue
+        
+        if len(nsLookup) > 1:
+          # multiple records for fingerprint (shouldn't happen)
+          log.log(log.WARN, "Multiple consensus entries for fingerprint: %s" % fingerprint)
+          continue
+        
+        # updates fingerprintMappings with new data
+        newRelay = nsLookup[0]
+        if newRelay.ip in self._fingerprintMappings:
+          # if entry already exists with the same orport, remove it
+          orportMatch = None
+          for entryPort, entryFingerprint in self._fingerprintMappings[newRelay.ip]:
+            if entryPort == newRelay.orport:
+              orportMatch = (entryPort, entryFingerprint)
+              break
+          
+          if orportMatch: self._fingerprintMappings[newRelay.ip].remove(orportMatch)
+          
+          # add the new entry
+          self._fingerprintMappings[newRelay.ip].append((newRelay.orport, newRelay.idhex))
+        else:
+          self._fingerprintMappings[newRelay.ip] = [(newRelay.orport, newRelay.idhex)]
+    
+    self.connLock.release()
   
   def circ_status_event(self, event):
     self._updateHeartbeat()
+    
+    # CIRC events aren't required, but if one's received then flush this cache
+    # since it uses circuit-status results.
+    self._fingerprintsAttachedCache = None
+    
+    self._cachedParam["circuits"] = None
   
   def buildtimeout_set_event(self, event):
     self._updateHeartbeat()
@@ -959,6 +1430,13 @@
     
     # checks if TorCtl is providing a notice that control port is closed
     if TOR_CTL_CLOSE_MSG in msg: self.close()
+    
+    # if the message is informing us of our ip address changing then clear
+    # its cached value
+    for prefix in ADDR_CHANGED_MSG_PREFIX:
+      if msg.startswith(prefix):
+        self._cachedParam["address"] = None
+        break
   
   def _updateHeartbeat(self):
     """
@@ -968,6 +1446,126 @@
     # alternative is to use the event's timestamp (via event.arrived_at)
     self.lastHeartbeat = time.time()
   
+  def _getFingerprintMappings(self, nsList = None):
+    """
+    Provides IP address to (port, fingerprint) tuple mappings for all of the
+    currently cached relays.
+    
+    Arguments:
+      nsList - network status listing (fetched if not provided)
+    """
+    
+    results = {}
+    if self.isAlive():
+      # fetch the current network status if not provided
+      if not nsList:
+        try: nsList = self.conn.get_network_status()
+        except (socket.error, TorCtl.TorCtlClosed, TorCtl.ErrorReply): nsList = []
+      
+      # construct mappings of ips to relay data
+      for relay in nsList:
+        if relay.ip in results: results[relay.ip].append((relay.orport, relay.idhex))
+        else: results[relay.ip] = [(relay.orport, relay.idhex)]
+    
+    return results
+  
+  def _getRelayFingerprint(self, relayAddress, relayPort):
+    """
+    Provides the fingerprint associated with the address/port combination.
+    
+    Arguments:
+      relayAddress - address of relay to be returned
+      relayPort    - orport of relay (to further narrow the results)
+    """
+    
+    # If we were provided with a string port then convert to an int (so
+    # lookups won't mismatch based on type).
+    if isinstance(relayPort, str): relayPort = int(relayPort)
+    
+    # checks if this matches us
+    if relayAddress == self.getInfo("address"):
+      if not relayPort or relayPort == self.getOption("ORPort"):
+        return self.getInfo("fingerprint")
+    
+    # if we haven't yet populated the ip -> fingerprint mappings then do so
+    if self._fingerprintMappings == None:
+      self._fingerprintMappings = self._getFingerprintMappings()
+    
+    potentialMatches = self._fingerprintMappings.get(relayAddress)
+    if not potentialMatches: return None # no relay matches this ip address
+    
+    if len(potentialMatches) == 1:
+      # There's only one relay belonging to this ip address. If the port
+      # matches then we're done.
+      match = potentialMatches[0]
+      
+      if relayPort and match[0] != relayPort: return None
+      else: return match[1]
+    elif relayPort:
+      # Multiple potential matches, so trying to match based on the port.
+      for entryPort, entryFingerprint in potentialMatches:
+        if entryPort == relayPort:
+          return entryFingerprint
+    
+    # Disambiguates based on our orconn-status and circuit-status results.
+    # This only includes relays we're connected to, so chances are pretty
+    # slim that we'll still have a problem narrowing this down. Note that we
+    # aren't necessarily checking for events that can create new client
+    # circuits (so this cache might be a little dirty).
+    
+    # populates the cache
+    if self._fingerprintsAttachedCache == None:
+      self._fingerprintsAttachedCache = []
+      
+      # orconn-status has entries of the form:
+      # $33173252B70A50FE3928C7453077936D71E45C52=shiven CONNECTED
+      orconnResults = self.getInfo("orconn-status")
+      if orconnResults:
+        for line in orconnResults.split("\n"):
+          self._fingerprintsAttachedCache.append(line[1:line.find("=")])
+      
+      # circuit-status results (we only make connections to the first hop)
+      for _, _, _, path in self.getCircuits():
+        self._fingerprintsAttachedCache.append(path[0])
+    
+    # narrow to only relays we have a connection to
+    attachedMatches = []
+    for _, entryFingerprint in potentialMatches:
+      if entryFingerprint in self._fingerprintsAttachedCache:
+        attachedMatches.append(entryFingerprint)
+    
+    if len(attachedMatches) == 1:
+      return attachedMatches[0]
+    
+    # Highly unlikely, but still haven't found it. Last we'll use some
+    # tricks from Mike's ConsensusTracker, excluding possiblities that
+    # have...
+    # - lost their Running flag
+    # - list a bandwidth of 0
+    # - have 'opt hibernating' set
+    # 
+    # This involves constructing a TorCtl Router and checking its 'down'
+    # flag (which is set by the three conditions above). This is the last
+    # resort since it involves a couple GETINFO queries.
+    
+    for entryPort, entryFingerprint in list(potentialMatches):
+      try:
+        nsCall = self.conn.get_network_status("id/%s" % entryFingerprint)
+        if not nsCall: raise TorCtl.ErrorReply() # network consensus couldn't be fetched
+        nsEntry = nsCall[0]
+        
+        descEntry = self.getInfo("desc/id/%s" % entryFingerprint)
+        if not descEntry: raise TorCtl.ErrorReply() # relay descriptor couldn't be fetched
+        descLines = descEntry.split("\n")
+        
+        isDown = TorCtl.Router.build_from_desc(descLines, nsEntry).down
+        if isDown: potentialMatches.remove((entryPort, entryFingerprint))
+      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
+    
+    if len(potentialMatches) == 1:
+      return potentialMatches[0][1]
+    else: return None
+  
   def _getRelayAttr(self, key, default, cacheUndefined = True):
     """
     Provides information associated with this relay, using the cached value if
@@ -980,15 +1578,15 @@
                        lookups if true
     """
     
-    currentVal = self._cachedParam[key]
-    if currentVal:
+    currentVal = self._cachedParam.get(key)
+    if currentVal != None:
       if currentVal == UNKNOWN: return default
       else: return currentVal
     
     self.connLock.acquire()
     
-    currentVal, result = self._cachedParam[key], None
-    if not currentVal and self.isAlive():
+    currentVal, result = self._cachedParam.get(key), None
+    if currentVal == None and self.isAlive():
       # still unset - fetch value
       if key in ("nsEntry", "descEntry"):
         myFingerprint = self.getInfo("fingerprint")
@@ -1043,6 +1641,8 @@
           if line.startswith("s "):
             result = line[2:].split()
             break
+      elif key == "parsedVersion":
+        result = parseVersion(self.getInfo("version", ""))
       elif key == "pid":
         result = getPid(int(self.getOption("ControlPort", 9051)), self.getOption("PidFile"))
       elif key == "pathPrefix":
@@ -1084,7 +1684,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)
               
@@ -1092,16 +1692,83 @@
                 etimeEntry = psCall[1].strip()
                 result = time.time() - uiTools.parseShortTimeLabel(etimeEntry)
           except: pass
+      elif key == "authorities":
+        # There's two configuration options that can overwrite the default
+        # authorities: DirServer and AlternateDirAuthority.
+        
+        # TODO: Both options accept a set of flags to more precisely set what they
+        # overwrite. Ideally this would account for these flags to more accurately
+        # identify authority connections from relays.
+        
+        dirServerCfg = self.getOption("DirServer", [], True)
+        altDirAuthCfg = self.getOption("AlternateDirAuthority", [], True)
+        altAuthoritiesCfg = dirServerCfg + altDirAuthCfg
+        
+        if altAuthoritiesCfg:
+          result = []
+          
+          # entries are of the form:
+          # [nickname] [flags] address:port fingerprint
+          for entry in altAuthoritiesCfg:
+            locationComp = entry.split()[-2] # address:port component
+            result.append(tuple(locationComp.split(":", 1)))
+        else: result = list(DIR_SERVERS)
+      elif key == "circuits":
+        # Parses our circuit-status results, for instance
+        #  91 BUILT $E4AE6E2FE320FBBD31924E8577F3289D4BE0B4AD=Qwerty PURPOSE=GENERAL
+        # would belong to a single hop circuit, most likely fetching the
+        # consensus via a directory mirror.
+        circStatusResults = self.getInfo("circuit-status")
+        
+        if circStatusResults == "":
+          result = [] # we don't have any circuits
+        elif circStatusResults != None:
+          result = []
+          
+          for line in circStatusResults.split("\n"):
+            # appends a tuple with the (status, purpose, path)
+            lineComp = line.split(" ")
+            
+            # skips blank lines and circuits without a path, for instance:
+            #  5 LAUNCHED PURPOSE=TESTING
+            if len(lineComp) < 4: continue
+            
+            path = tuple([hopEntry[1:41] for hopEntry in lineComp[2].split(",")])
+            result.append((int(lineComp[0]), lineComp[1], lineComp[3][8:], path))
+      elif key == "hsPorts":
+        result = []
+        hsOptions = self.getOptionMap("HiddenServiceOptions")
+        
+        if hsOptions and "HiddenServicePort" in hsOptions:
+          for hsEntry in hsOptions["HiddenServicePort"]:
+            # hidden service port entries are of the form:
+            # VIRTPORT [TARGET]
+            # with the TARGET being an IP, port, or IP:Port. If the target port
+            # isn't defined then uses the VIRTPORT.
+            
+            hsPort = None
+            
+            if " " in hsEntry:
+              # parses the target, checking if it's a port or IP:Port combination
+              hsTarget = hsEntry.split(" ")[1]
+              
+              if ":" in hsTarget:
+                hsPort = hsTarget.split(":")[1] # target is the IP:Port
+              elif hsTarget.isdigit():
+                hsPort = hsTarget # target is just the port
+            else: hsPort = hsEntry # just has the virtual port
+            
+            if hsPort.isdigit():
+              result.append(hsPort)
       
       # cache value
-      if result: self._cachedParam[key] = result
+      if result != None: self._cachedParam[key] = result
       elif cacheUndefined: self._cachedParam[key] = UNKNOWN
-    elif currentVal == UNKNOWN: result = currentVal
     
     self.connLock.release()
     
-    if result: return result
-    else: return default
+    if result == None or result == UNKNOWN: return default
+    else: return result
   
   def _notifyStatusListeners(self, eventType):
     """
@@ -1113,13 +1780,156 @@
     """
     
     # resets cached GETINFO and GETCONF parameters
-    self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
+    self._cachedParam = {}
     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:
       callback(self, eventType)
 
+class ExitPolicy:
+  """
+  Single rule from the user's exit policy. These are chained together to form
+  complete policies.
+  """
+  
+  def __init__(self, ruleEntry, nextRule):
+    """
+    Exit policy rule constructor.
+    
+    Arguments:
+      ruleEntry - tor exit policy rule (for instance, "reject *:135-139")
+      nextRule  - next rule to be checked when queries don't match this policy
+    """
+    
+    # sanitize the input a bit, cleaning up tabs and stripping quotes
+    ruleEntry = ruleEntry.replace("\\t", " ").replace("\"", "")
+    
+    self.ruleEntry = ruleEntry
+    self.nextRule = nextRule
+    self.isAccept = ruleEntry.startswith("accept")
+    
+    # strips off "accept " or "reject " and extra spaces
+    ruleEntry = ruleEntry[7:].replace(" ", "")
+    
+    # split ip address (with mask if provided) and port
+    if ":" in ruleEntry: entryIp, entryPort = ruleEntry.split(":", 1)
+    else: entryIp, entryPort = ruleEntry, "*"
+    
+    # sets the ip address component
+    self.isIpWildcard = entryIp == "*" or entryIp.endswith("/0")
+    
+    # checks for the private alias (which expands this to a chain of entries)
+    if entryIp.lower() == "private":
+      entryIp = PRIVATE_IP_RANGES[0]
+      
+      # constructs the chain backwards (last first)
+      lastHop = self.nextRule
+      prefix = "accept " if self.isAccept else "reject "
+      suffix = ":" + entryPort
+      for addr in PRIVATE_IP_RANGES[-1:0:-1]:
+        lastHop = ExitPolicy(prefix + addr + suffix, lastHop)
+      
+      self.nextRule = lastHop # our next hop is the start of the chain
+    
+    if "/" in entryIp:
+      ipComp = entryIp.split("/", 1)
+      self.ipAddress = ipComp[0]
+      self.ipMask = int(ipComp[1])
+    else:
+      self.ipAddress = entryIp
+      self.ipMask = 32
+    
+    # constructs the binary address just in case of comparison with a mask
+    if self.ipAddress != "*":
+      self.ipAddressBin = ""
+      for octet in self.ipAddress.split("."):
+        # Converts the int to a binary string, padded with zeros. Source:
+        # http://www.daniweb.com/code/snippet216539.html
+        self.ipAddressBin += "".join([str((int(octet) >> y) & 1) for y in range(7, -1, -1)])
+    else:
+      self.ipAddressBin = "0" * 32
+    
+    # sets the port component
+    self.minPort, self.maxPort = 0, 0
+    self.isPortWildcard = entryPort == "*"
+    
+    if entryPort != "*":
+      if "-" in entryPort:
+        portComp = entryPort.split("-", 1)
+        self.minPort = int(portComp[0])
+        self.maxPort = int(portComp[1])
+      else:
+        self.minPort = int(entryPort)
+        self.maxPort = int(entryPort)
+    
+    # if both the address and port are wildcards then we're effectively the
+    # last entry so cut off the remaining chain
+    if self.isIpWildcard and self.isPortWildcard:
+      self.nextRule = None
+  
+  def isExitingAllowed(self):
+    """
+    Provides true if the policy allows exiting whatsoever, false otherwise.
+    """
+    
+    if self.isAccept: return True
+    elif self.isIpWildcard and self.isPortWildcard: return False
+    elif not self.nextRule: return False # fell off policy (shouldn't happen)
+    else: return self.nextRule.isExitingAllowed()
+  
+  def check(self, ipAddress, port):
+    """
+    Checks if the rule chain allows exiting to this address, returning true if
+    so and false otherwise.
+    """
+    
+    port = int(port)
+    
+    # does the port check first since comparing ip masks is more work
+    isPortMatch = self.isPortWildcard or (port >= self.minPort and port <= self.maxPort)
+    
+    if isPortMatch:
+      isIpMatch = self.isIpWildcard or self.ipAddress == ipAddress
+      
+      # expands the check to include the mask if it has one
+      if not isIpMatch and self.ipMask != 32:
+        inputAddressBin = ""
+        for octet in ipAddress.split("."):
+          inputAddressBin += "".join([str((int(octet) >> y) & 1) for y in range(7, -1, -1)])
+        
+        isIpMatch = self.ipAddressBin[:self.ipMask] == inputAddressBin[:self.ipMask]
+      
+      if isIpMatch: return self.isAccept
+    
+    # our policy doesn't concern this address, move on to the next one
+    if self.nextRule: return self.nextRule.check(ipAddress, port)
+    else: return True # fell off the chain without a conclusion (shouldn't happen...)
+  
+  def __str__(self):
+    # This provides the actual policy rather than the entry used to construct
+    # it so the 'private' keyword is expanded.
+    
+    acceptanceLabel = "accept" if self.isAccept else "reject"
+    
+    if self.isIpWildcard:
+      ipLabel = "*"
+    elif self.ipMask != 32:
+      ipLabel = "%s/%i" % (self.ipAddress, self.ipMask)
+    else: ipLabel = self.ipAddress
+    
+    if self.isPortWildcard:
+      portLabel = "*"
+    elif self.minPort != self.maxPort:
+      portLabel = "%i-%i" % (self.minPort, self.maxPort)
+    else: portLabel = str(self.minPort)
+    
+    myPolicy = "%s %s:%s" % (acceptanceLabel, ipLabel, portLabel)
+    
+    if self.nextRule:
+      return myPolicy + ", " + str(self.nextRule)
+    else: return myPolicy
+

Modified: arm/release/src/util/uiTools.py
===================================================================
--- arm/release/src/util/uiTools.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/util/uiTools.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -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,11 +161,12 @@
   
   # 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)
+  lastWordbreak = len(msg[:lastWordbreak].rstrip()) # drops extra ending whitespaces
   if (minWordLen != None and size < minWordLen) or (minWordLen == None and lastWordbreak < 1):
     if getRemainder: return ("", msg)
     else: return ""
@@ -181,23 +182,64 @@
   
   if includeCrop:
     returnMsg, remainder = msg[:size], msg[size:]
-    if endType == END_WITH_HYPHEN:
+    if endType == Ending.HYPHEN:
       remainder = returnMsg[-1] + remainder
-      returnMsg = returnMsg[:-1] + "-"
+      returnMsg = returnMsg[:-1].rstrip() + "-"
   else: returnMsg, remainder = msg[:lastWordbreak], msg[lastWordbreak:]
   
   # if this is ending with a comma or period then strip it off
   if not getRemainder and returnMsg[-1] in (",", "."): returnMsg = returnMsg[:-1]
   
-  if endType == END_WITH_ELLIPSE: returnMsg += "..."
+  if endType == Ending.ELLIPSE:
+    returnMsg = returnMsg.rstrip() + "..."
   
   if getRemainder: return (returnMsg, remainder)
   else: return returnMsg
 
+def drawBox(panel, top, left, width, height, attr=curses.A_NORMAL):
+  """
+  Draws a box in the panel with the given bounds.
+  
+  Arguments:
+    panel  - panel in which to draw
+    top    - vertical position of the box's top
+    left   - horizontal position of the box's left side
+    width  - width of the drawn box
+    height - height of the drawn box
+    attr   - text attributes
+  """
+  
+  # draws the top and bottom
+  panel.hline(top, left + 1, width - 1, attr)
+  panel.hline(top + height - 1, left + 1, width - 1, attr)
+  
+  # draws the left and right sides
+  panel.vline(top + 1, left, height - 2, attr)
+  panel.vline(top + 1, left + width, height - 2, attr)
+  
+  # draws the corners
+  panel.addch(top, left, curses.ACS_ULCORNER, attr)
+  panel.addch(top, left + width, curses.ACS_URCORNER, attr)
+  panel.addch(top + height - 1, left, curses.ACS_LLCORNER, attr)
+  panel.addch(top + height - 1, left + width, curses.ACS_LRCORNER, attr)
+
+def isSelectionKey(key):
+  """
+  Returns true if the keycode matches the enter or space keys.
+  
+  Argument:
+    key - keycode to be checked
+  """
+  
+  return key in (curses.KEY_ENTER, 10, ord(' '))
+
 def isScrollKey(key):
   """
   Returns true if the keycode is recognized by the getScrollPosition function
   for scrolling.
+  
+  Argument:
+    key - keycode to be checked
   """
   
   return key in SCROLL_KEYS
@@ -367,6 +409,73 @@
   except ValueError:
     raise ValueError(errorMsg)
 
+class DrawEntry:
+  """
+  Renderable content, encapsulating the text and formatting. These can be
+  chained together to compose lines with multiple types of formatting.
+  """
+  
+  def __init__(self, text, format=curses.A_NORMAL, nextEntry=None, lockFormat=False):
+    """
+    Constructor for prepared draw entries.
+    
+    Arguments:
+      text       - content to be drawn, this can either be a string or list of
+                   integer character codes
+      format     - properties to apply when drawing
+      nextEntry  - entry to be drawn after this one
+      lockFormat - prevents extra formatting attributes from being applied
+                   when rendered if true
+    """
+    
+    self.text = text
+    self.format = format
+    self.nextEntry = nextEntry
+    self.lockFormat = lockFormat
+  
+  def getNext(self):
+    """
+    Provides the next DrawEntry in the chain.
+    """
+    
+    return self.nextEntry
+  
+  def setNext(self, nextEntry):
+    """
+    Sets additional content to be drawn after this entry. If None then
+    rendering is terminated after this entry.
+    
+    Arguments:
+      nextEntry - DrawEntry instance to be rendered after this one
+    """
+    
+    self.nextEntry = nextEntry
+  
+  def render(self, drawPanel, y, x, extraFormat=curses.A_NORMAL):
+    """
+    Draws this content at the given position.
+    
+    Arguments:
+      drawPanel   - context in which to be drawn
+      y           - vertical location
+      x           - horizontal location
+      extraFormat - additional formatting
+    """
+    
+    if self.lockFormat: drawFormat = self.format
+    else: drawFormat = self.format | extraFormat
+    
+    if isinstance(self.text, str):
+      drawPanel.addstr(y, x, self.text, drawFormat)
+    else:
+      for i in range(len(self.text)):
+        drawChar = self.text[i]
+        drawPanel.addch(y, x + i, drawChar, drawFormat)
+    
+    # if there's additional content to show then render it too
+    if self.nextEntry:
+      self.nextEntry.render(drawPanel, y, x + len(self.text), extraFormat)
+
 class Scroller:
   """
   Tracks the scrolling position when there might be a visible cursor. This
@@ -394,10 +503,16 @@
       if self.isCursorEnabled:
         self.getCursorSelection(content) # resets the cursor location
         
+        # makes sure the cursor is visible
         if self.cursorLoc < self.scrollLoc:
           self.scrollLoc = self.cursorLoc
         elif self.cursorLoc > self.scrollLoc + pageHeight - 1:
           self.scrollLoc = self.cursorLoc - pageHeight + 1
+      
+      # checks if the bottom would run off the content (this could be the
+      # case when the content's size is dynamic and entries are removed)
+      if len(content) > pageHeight:
+        self.scrollLoc = min(self.scrollLoc, len(content) - pageHeight)
     
     return self.scrollLoc
   

Modified: arm/release/src/version.py
===================================================================
--- arm/release/src/version.py	2011-04-04 15:15:20 UTC (rev 24554)
+++ arm/release/src/version.py	2011-04-04 15:22:31 UTC (rev 24555)
@@ -2,6 +2,6 @@
 Provides arm's version and release date.
 """
 
-VERSION = '1.4.1.3'
-LAST_MODIFIED = "January 15, 2011"
+VERSION = '1.4.2'
+LAST_MODIFIED = "April 4, 2011"
 



More information about the tor-commits mailing list