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

[or-cvs] r23873: {arm} Merging to release for version 1.4.0 (in arm/release: . src src/interface src/interface/graphing src/util)



Author: atagar
Date: 2010-11-28 10:58:57 +0000 (Sun, 28 Nov 2010)
New Revision: 23873

Added:
   arm/release/arm.1
   arm/release/armrc.sample
   arm/release/src/interface/configPanel.py
   arm/release/src/interface/torrcPanel.py
   arm/release/src/settings.cfg
   arm/release/src/util/torConfig.py
Removed:
   arm/release/armrc.sample
   arm/release/debian/
   arm/release/src/armrc.defaults
   arm/release/src/interface/confPanel.py
Modified:
   arm/release/
   arm/release/ChangeLog
   arm/release/README
   arm/release/TODO
   arm/release/arm
   arm/release/install
   arm/release/setup.py
   arm/release/src/interface/__init__.py
   arm/release/src/interface/connPanel.py
   arm/release/src/interface/controller.py
   arm/release/src/interface/graphing/bandwidthStats.py
   arm/release/src/interface/graphing/graphPanel.py
   arm/release/src/interface/graphing/psStats.py
   arm/release/src/interface/headerPanel.py
   arm/release/src/interface/logPanel.py
   arm/release/src/starter.py
   arm/release/src/uninstall
   arm/release/src/util/__init__.py
   arm/release/src/util/conf.py
   arm/release/src/util/connections.py
   arm/release/src/util/hostnames.py
   arm/release/src/util/log.py
   arm/release/src/util/panel.py
   arm/release/src/util/sysTools.py
   arm/release/src/util/torTools.py
   arm/release/src/util/uiTools.py
   arm/release/src/version.py
Log:
Merging to release for version 1.4.0




Property changes on: arm/release
___________________________________________________________________
Modified: svn:mergeinfo
   - /arm/trunk:22227-23438
   + /arm/trunk:22227-23872

Modified: arm/release/ChangeLog
===================================================================
--- arm/release/ChangeLog	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/ChangeLog	2010-11-28 10:58:57 UTC (rev 23873)
@@ -1,6 +1,49 @@
 CHANGE LOG
 
-10/6/10 - version 1.3.7
+11/27/10 - version 1.4.0
+Introducing a new page for managing tor's configuration, along with several other improvements.
+
+    * added: editor for the tor configuration, providing:
+          o a simple method for setting config values and saving the new torrc
+          o descriptions and usage information for the tor configuration options, fetched from its man page
+          o color and bolding to indication option categories and if they're default or custom values
+          o sorting by any of the config attributes
+    * change: numerous revisions in preparation for being included in debian, thanks to weasel
+          o moved deb/rpm build resources out of the source repository and added helper scripts
+          o moved the arm install location to /usr/share/arm
+          o purging the autogenerated egg file from the deb build
+          o using temporary file utility for man page compression to avoid potential security issues (thanks to asn)
+          o including dh_pysupport flag so it'll recognize the private python module (thanks to Emilio Pozuelo Monfort)
+          o small revisions to several bits of debian metadata
+    * change: full rewrite of the log panel, providing:
+          o added: scrollbar and scrolling by displayed content rather than line numbers
+          o added: checking for torrc entries that are pointless due to matching the default value
+          o added: validation warning when custom entries are missing from the torrc
+          o added: handling for the multiline torrc entry support that was added in tor 0.2.2.17-alpha
+          o change: simplified and expanded on the config display and validation (performance improvements, human friendly units for torrc corrections, etc)
+          o fix: torrc validation didn't recognize 'second' and 'byte' arguments
+          o fix: scrolling was buggy if comments were being stripped
+          o fix: more helpful messages for validation errors
+          o fix: unnecessary whitespace was being stripped
+    * added: INFO level logging for the arm startup time
+    * change: removing all references to the controller password after we've connected to tor (request by ioerror)
+    * change: using curses.textpad to improve text fields (supports arrow keys, emacs keybindings, etc)
+    * change: revised the arm config interface (simplified and expanded to include maps)
+    * fix: verbose logging was causing the application to freeze due to an n^2 deduplication implementation, disabling this feature for now when it takes too long (caught by NightMonkey)
+    * fix: wasn't loading the settings.cfg if starting starter from the src directory (caught by NightMonkey)
+    * fix: displaying empty conf contents caused crashes when calling math.log10(0) (caught by NightMonkey)
+    * fix: persisting results from scraping the man page to greatly reduce startup time (idea by nickm)
+    * fix: path for the sample armrc was wrong in the man page (caught by weasel)
+    * fix: the arm starter was only executable from the arm directory
+    * fix: not all worker threads were daemons, causing the process to persist in a broken state after exceptions and when quitting via ctrl+c
+    * fix: custom armrcs resulted in the parsing config options being unavailable
+    * fix: rounding error in rendering the scrollbar, causing it to shrink a line when at the bottom
+    * fix: crashing issue when the 'queries.ps.rate' config value was undefined and the stats graph was displayed
+    * fix: making the interface more resilient to being resized while popups are visible
+    * fix: log panel wasn't respecting the prepopulate* log level config options
+    * fix: off by one error when wrapping lines in the log panel
+
+10/6/10 - version 1.3.7 (r23439)
 Numerous improvements, most notably being an expanded log panel, installer, and deb/rpm builds.
 
     * added: installation/removal scripts and man page (thanks to kaner)
@@ -50,6 +93,7 @@
     * fix: race condition between heartbeat detection and getting the first BW event
     * fix: refreshing after popups to make the interface seem more responsive
     * fix: crashing and minor display issues if orport was left unset
+    * fix (10/7/10, r23463): crashing from type issue in the graph panel (caught by tomb)
 
 6/7/10 - version 1.3.6 (r22617)
 Rewrite of the first third of the interface, providing vastly improved performance, maintainability, and a few very nice features. This improved the refresh rate (which is also related to system resource usage) from 30ms to 4ms (an 87% improvement).

Modified: arm/release/README
===================================================================
--- arm/release/README	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/README	2010-11-28 10:58:57 UTC (rev 23873)
@@ -88,6 +88,7 @@
   arm     - startup script
   install - installation script
   
+  arm.1        - man page
   armrc.sample - example arm configuration file with defaults
   ChangeLog    - revision history
   LICENSE      - copy of the gpl v3
@@ -95,17 +96,13 @@
   TODO         - known issues, future plans, etc
   setup.py     - distutils installation script for arm
   
-  debian/     - resources for generating debs and rpms (most is metadata)
-    make-deb  - script for generating debian installer
-    make-rpm  - script for generating red hat installer
-    arm.1.gz  - man page
-  
   src/
     __init__.py
-    starter.py  - parses and validates commandline parameters
-    prereq.py   - checks python version and for required packages
-    version.py  - version and last modified information
-    uninstall   - removal script
+    starter.py   - parses and validates commandline parameters
+    prereq.py    - checks python version and for required packages
+    version.py   - version and last modified information
+    settings.cfg - attributes loaded for parsing tor related data
+    uninstall    - removal script
     
     interface/
       graphing/
@@ -125,7 +122,8 @@
       connPanel.py           - (page 2) displays information on tor connections
       descriptorPopup.py     - (popup) displays connection descriptor data
       
-      confPanel.py           - (page 3) displays torrc and performs validation
+      configPanel.py         - (page 3) editor panel for the tor configuration
+      torrcPanel.py          - (page 4) displays torrc and validation
     
     util/
       __init__.py
@@ -135,6 +133,7 @@
       log.py         - aggregator for application events
       panel.py       - wrapper for safely working with curses subwindows
       sysTools.py    - helper for system calls, providing client side caching
+      torConfig.py   - functions for working with the torrc and config options
       torTools.py    - TorCtl wrapper, providing caching and derived information
       uiTools.py     - helper functions for presenting the user interface
 

Modified: arm/release/TODO
===================================================================
--- arm/release/TODO	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/TODO	2010-11-28 10:58:57 UTC (rev 23873)
@@ -1,6 +1,6 @@
 TODO
 
-- Roadmap and completed work for next release (1.3.8)
+- Roadmap and completed work for next release (1.4.1)
   [ ] 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
@@ -9,34 +9,45 @@
       progress - /init and /util are done and /interface is partly done. Known
       bugs are being fixed while refactoring.
       
-      [ ] conf panel
-        - move torrc validation into util
-        - fetch text via getinfo rather than reading directly?
-           conn.get_info("config-text")
-        - improve parsing failure notice to give line number
-          just giving "[ARM-WARN] Unable to validate torrc" isn't very
-          helpful...
       [ ] conn panel
         - expand client connections and note location in circuit (entry-exit)
-        - for clients list all connections to detect what's going through tor
-          and what isn't? If not then netstat calls are unnecessary.
-        - check family connections to see if they're alive (VERSION cell
+        - 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
+        - identify controller connections (if it's arm, vidalia, etc) with
           special detail page for them
-        - provide bridge / client country statistics
+        - provide bridge / client country / exiting port statistics
           Include bridge related data via GETINFO option (feature request
           by waltman and ioerror).
         - 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
-        - give usage stats for exit port usage (popup?)
-        - country data for client connections (requested by ioerror)
+      [ ] 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
+      [ ] low hanging fruit from the "client mode use cases" below
+  * release prep
+    * pylint --indent-string="  " --disable=C,R interface/foo.py | less
+    * double check __init__.py and README for changes
+
+- Roadmap for version 1.4.2
+  [ ] 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
@@ -49,6 +60,12 @@
         - allow arm to resume after restarting tor
             This requires a full move to the torTools controller.
         - provide measurements for startup time, and try to improve bottlenecks
+  [ ] 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)
   [ ] setup scripts for arm
       [ ] updater (checks for a new tarball and installs it automatically)
         - attempt to verify download signature, providing a warning if unable
@@ -60,7 +77,10 @@
             - http://www.linuxjournal.com/article/5737
 
 - Bugs
-  * path for sample armrc in man page is wrong
+  * 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 are assuming that tor is running under the default command name
       attempt to determine the command name at runtime (if the pid is available
@@ -73,18 +93,11 @@
       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
   
-  * conf panel:
-    * torrc validation doesn't catch if parameters are missing
-    * scrolling in the torrc isn't working properly when comments are stripped
-        Current method of displaying torrc is pretty stupid (lots of repeated
-        work in display loop). When rewritten fixing this bug should be
-        trivial.
-    * "ExitPolicy" entry in torrc (without path)
-        Produces "May 26 22:11:03.484 [warn] The abbreviation 'ExitPolic' is
-        deprecated. Please use 'ExitPolicy' instead". This is an error in the
-        torrc parsing when only the key is provided.
-  
   * conn panel:
     * *never* do reverse dns lookups for first hops (could be resolving via
       tor and hence leaking to the exit)
@@ -108,22 +121,6 @@
     * connections aren't cleared when control port closes
 
 - Future Features
-  * 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
-  * 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)
   * client mode use cases
     * not sure what sort of information would be useful in the header (to
       replace the orport, fingerprint, flags, etc)
@@ -145,13 +142,15 @@
           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 for ideas
+    * look at Vidalia and TorK for ideas
     * need to solicit for ideas on what would be most helpful to clients
-  * general purpose method of erroring nicely
-    Some errors cause portions of the display to die, but curses limps along
-    and overwrites the stacktrace. This has been mostly solved, but all errors
-    should result in a clean death, with the stacktrace saved and a nice
-    message for the user.
+    * dialog with bridge statuses (idea by mikeperry)
+      https://trac.vidalia-project.net/ticket/570
+      https://trac.torproject.org/projects/tor/ticket/2068
+  * 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
@@ -179,6 +178,9 @@
         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
+  * 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>")?
@@ -216,6 +218,9 @@
   * 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

Modified: arm/release/arm
===================================================================
--- arm/release/arm	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/arm	2010-11-28 10:58:57 UTC (rev 23873)
@@ -1,8 +1,8 @@
 #!/bin/sh
 if [ $0 = /usr/bin/arm ]; then
-  arm_base=/usr/lib/arm/
+  arm_base=/usr/share/arm/
 else
-  arm_base=src/
+  arm_base=$( dirname $0 )/src/
 fi
 
 python ${arm_base}prereq.py

Copied: arm/release/arm.1 (from rev 23872, arm/trunk/arm.1)
===================================================================
--- arm/release/arm.1	                        (rev 0)
+++ arm/release/arm.1	2010-11-28 10:58:57 UTC (rev 23873)
@@ -0,0 +1,73 @@
+.TH arm 1 "27 August 2010"
+.SH NAME
+arm - Terminal Tor status monitor
+
+.SH SYNOPSIS
+arm [\fIOPTION\fR]
+
+.SH DESCRIPTION
+The anonymizing relay monitor (arm) is a terminal status monitor for Tor
+relays, intended for command-line aficionados, ssh connections, and anyone
+stuck with a tty terminal. This works much like top does for system usage,
+providing real time statistics for:
+  * bandwidth, cpu, and memory usage
+  * relay's current configuration
+  * logged events
+  * connection details (ip, hostname, fingerprint, and consensus data)
+  * etc
+
+Defaults and interface properties are configurable via a user provided
+configuration file (for an example see the provided \fBarmrc.sample\fR).
+Releases and information are available at \fIhttp://www.atagar.com/arm\fR.
+
+.SH OPTIONS
+.TP
+\fB\-i\fR, \fB\-\-interface [ADDRESS:]PORT\fR
+tor control port arm should attach to (default is \fB127.0.0.1:9051\fR)
+
+.TP
+\fB\-c\fR, \fB\-\-config CONFIG_PATH\fR
+user provided configuration file (default is \fB~/.armrc\fR)
+
+.TP
+\fB\-b\fR, \fB\-\-blind\fR
+disable connection lookups (netstat, lsof, and ss), dropping the parts of the
+interface that rely on this information
+
+.TP
+\fB\-e\fR, \fB\-\-event EVENT_FLAGS\fR
+flags for tor, arm, and torctl events to be logged (default is \fBN3\fR)
+
+  d DEBUG      a ADDRMAP           k DESCCHANGED   s STREAM
+  i INFO       f AUTHDIR_NEWDESCS  g GUARD         r STREAM_BW
+  n NOTICE     h BUILDTIMEOUT_SET  l NEWCONSENSUS  t STATUS_CLIENT
+  w WARN       b BW                m NEWDESC       u STATUS_GENERAL
+  e ERR        c CIRC              p NS            v STATUS_SERVER
+               j CLIENTS_SEEN      q ORCONN
+    DINWE tor runlevel+            A All Events
+    12345 arm runlevel+            X No Events
+    67890 torctl runlevel+         U Unknown Events
+
+.TP
+\fB\-v\fR, \fB\-\-verion\fR
+provides version information
+
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+provides usage information
+
+.SH FILES
+.TP
+\fB~/.armrc\fR
+Your personal arm configuration file
+
+.TP
+\fB/usr/share/doc/arm/armrc.sample\fR
+Sample armrc configuration file that documents all options
+
+.SH AUTHOR
+Written by Damian Johnson (atagar1@xxxxxxxxx)
+
+.SH COPYRIGHT
+GNU GPL version 3, \fIhttp://gnu.org/licenses/gpl.html\fR
+

Deleted: arm/release/armrc.sample
===================================================================
--- arm/release/armrc.sample	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/armrc.sample	2010-11-28 10:58:57 UTC (rev 23873)
@@ -1 +0,0 @@
-link src/armrc.defaults
\ No newline at end of file

Copied: arm/release/armrc.sample (from rev 23872, arm/trunk/armrc.sample)
===================================================================
--- arm/release/armrc.sample	                        (rev 0)
+++ arm/release/armrc.sample	2010-11-28 10:58:57 UTC (rev 23873)
@@ -0,0 +1,227 @@
+# startup options
+startup.controlPassword
+startup.interface.ipAddress 127.0.0.1
+startup.interface.port 9051
+startup.blindModeEnabled false
+startup.events N3
+
+# Seconds between querying information
+queries.ps.rate 5
+queries.connections.minRate 5
+queries.refreshRate.rate 5
+
+# Renders the interface with color if set and the terminal supports it
+features.colorInterface true
+
+# Checks the torrc for issues, warning and hilighting problems if true
+features.torrc.validate true
+
+# Set this if you're running in a chroot jail or other environment where tor's
+# resources (log, state, etc) should have a prefix in their paths.
+features.pathPrefix
+
+# If set, arm appends any log messages it reports while running to the given
+# log file. This does not take filters into account or include prepopulated
+# events.
+features.logFile 
+
+# Paremters for the log panel
+# ---------------------------
+# showDateDividers
+#   show borders with dates for entries from previous days
+# showDuplicateEntries
+#   shows all log entries if true, otherwise collapses similar entries with an
+#   indicator for how much is being hidden
+# entryDuration
+#   number of days log entries are kept before being dropped (if zero then
+#   they're kept until cropped due to caching limits)
+# maxLinesPerEntry
+#   max number of lines to display for a single log entry
+# prepopulate
+#   attempts to read past events from the log file if true
+# prepopulateReadLimit
+#   maximum entries read from the log file, used to prevent huge log files from
+#   causing a slow startup time.
+# maxRefreshRate
+#   rate limiting (in milliseconds) for drawing the log if updates are made
+#   rapidly (for instance, when at the DEBUG runlevel)
+
+features.log.showDateDividers true
+features.log.showDuplicateEntries false
+features.log.entryDuration 7
+features.log.maxLinesPerEntry 4
+features.log.prepopulate true
+features.log.prepopulateReadLimit 5000
+features.log.maxRefreshRate 300
+
+# 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
+# selectionDetails.height
+#   rows of data for the panel showing details on the current selection, this
+#   is disabled entirely if zero
+# features.config.prepopulateEditValues
+#   when editing config values the current value is prepopulated if true, and
+#   left blank otherwise
+# state.colWidth.*
+#   column content width
+# state.showPrivateOptions
+#   tor provides config options of the form "__<option>" that can be dangerous
+#   to set, if true arm provides these on the config panel
+# state.showVirtualOptions
+#   virtual options are placeholders for other option groups, never having
+#   values or being setable themselves
+# file.showScrollbars
+#   displays scrollbars when the torrc content is longer than the display
+# 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.selectionDetails.height 6
+features.config.prepopulateEditValues true
+features.config.state.colWidth.option 25
+features.config.state.colWidth.value 10
+features.config.state.showPrivateOptions false
+features.config.state.showVirtualOptions false
+features.config.file.showScrollbars true
+features.config.file.maxLinesPerEntry 8
+
+# Descriptions for tor's configuration options can be loaded from its man page
+# to give usage information on the settings page. They can also be persisted to
+# a file to speed future lookups.
+# ---------------------------
+# 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)
+
+features.config.descriptions.enabled true
+features.config.descriptions.persistPath /tmp/arm/torConfigDescriptions.txt
+
+# General graph parameters
+# ------------------------
+# height
+#   height of graphed stats
+# maxWidth
+#   maximum number of graphed entries
+# interval
+#   0 -> each second,   1 -> 5 seconds,     2 -> 30 seconds,  3 -> minutely,      
+#   4 -> 15 minutes,    5 -> half hour,     6 -> hourly,      7 -> daily
+# bound
+#   0 -> global maxima, 1 -> local maxima,  2 -> tight
+# type
+#   0 -> None, 1 -> Bandwidth, 2 -> Connections, 3 -> System Resources
+# showIntermediateBounds
+#   shows y-axis increments between the top/bottom bounds
+
+features.graph.height 7
+features.graph.maxWidth 150
+features.graph.interval 0
+features.graph.bound 1
+features.graph.type 1
+features.graph.showIntermediateBounds true
+
+# Parameters for graphing bandwidth stats
+# ---------------------------------------
+# 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)
+# transferInBystes
+#   shows rate measurments in bytes if true, bits otherwise
+# accounting.show
+#   provides accounting stats if AccountingMax was set
+# accounting.rate
+#   seconds between querying accounting stats
+# accounting.isTimeLong
+#   provides verbose measurements of time if true
+
+features.graph.bw.prepopulate true
+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 graphing ps stats
+# --------------------------------
+# primary/secondaryStat
+#   any numeric field provided by the ps command
+# cachedOnly
+#   determines if the graph should query ps or rely on cached results (this
+#   lowers the call volume but limits the graph's granularity)
+
+features.graph.ps.primaryStat %cpu
+features.graph.ps.secondaryStat rss
+features.graph.ps.cachedOnly 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.
+
+queries.hostnames.poolSize 5
+
+# Method of resolving hostnames
+# If true, uses python's internal "socket.gethostbyaddr" to resolve addresses
+# rather than the host command. This is ignored if the system's unable to make
+# parallel requests. Resolving this way seems to be much slower than host calls
+# in practice.
+
+queries.hostnames.useSocketModule false
+
+# Caching parameters
+cache.sysCalls.size 600
+cache.hostnames.size 700000
+cache.hostnames.trimSize 200000
+cache.logPanel.size 1000
+cache.armLog.size 1000
+cache.armLog.trimSize 200
+
+# Runlevels at which arm logs its events
+log.startTime INFO
+log.refreshRate DEBUG
+log.configEntryNotFound NONE
+log.configEntryUndefined NOTICE
+log.configEntryTypeError NOTICE
+log.torCtlPortClosed NOTICE
+log.torGetInfo DEBUG
+log.torGetConf DEBUG
+log.torSetConf INFO
+log.torEventTypeUnrecognized NOTICE
+log.torPrefixPathInvalid NOTICE
+log.sysCallMade DEBUG
+log.sysCallCached NONE
+log.sysCallFailed INFO
+log.sysCallCacheGrowing INFO
+log.panelRecreated DEBUG
+log.graph.ps.invalidStat WARN
+log.graph.ps.abandon WARN
+log.graph.bw.prepopulateSuccess NOTICE
+log.graph.bw.prepopulateFailure NOTICE
+log.logPanel.prepopulateSuccess INFO
+log.logPanel.prepopulateFailed WARN
+log.logPanel.logFileOpened NOTICE
+log.logPanel.logFileWriteFailed ERR
+log.logPanel.forceDoubleRedraw DEBUG
+log.torrc.readFailed WARN
+log.torrc.validation.torStateDiffers WARN
+log.torrc.validation.unnecessaryTorrcEntries WARN
+log.configDescriptions.readManPageSuccess INFO
+log.configDescriptions.readManPageFailed WARN
+log.configDescriptions.unrecognizedCategory NOTICE
+log.configDescriptions.persistance.loadSuccess INFO
+log.configDescriptions.persistance.loadFailed INFO
+log.configDescriptions.persistance.saveSuccess NOTICE
+log.configDescriptions.persistance.saveFailed NOTICE
+log.connLookupFailed INFO
+log.connLookupFailover NOTICE
+log.connLookupAbandon WARN
+log.connLookupRateGrowing NONE
+log.hostnameCacheTrimmed INFO
+log.cursesColorSupport INFO
+

Modified: arm/release/install
===================================================================
--- arm/release/install	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/install	2010-11-28 10:58:57 UTC (rev 23873)
@@ -2,11 +2,11 @@
 python src/prereq.py
 
 if [ $? = 0 ]; then
-  python setup.py -q install --install-purelib /usr/lib
+  python setup.py -q install
   
   # provide notice if we installed successfully
   if [ $? = 0 ]; then
-    echo "installed to /usr/lib/arm"
+    echo "installed to /usr/share/arm"
   fi
   
   # cleans up the automatically built temporary files

Modified: arm/release/setup.py
===================================================================
--- arm/release/setup.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/setup.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -1,9 +1,54 @@
 #!/usr/bin/env python
 import os
 import sys
+import gzip
+import tempfile
 from src.version import VERSION
 from distutils.core import setup
 
+# Use 'tor-arm' instead of 'arm' in the path for the sample armrc if we're
+# building for debian.
+
+isDebInstall = False
+for arg in sys.argv:
+  if "tor-arm" in arg:
+    isDebInstall = True
+    break
+
+docPath = "/usr/share/doc/%s" % ("tor-arm" if isDebInstall else "arm")
+
+# Provides the configuration option to install to "/usr/share" rather than as a
+# python module. Alternatives are to either provide this as an input argument
+# (not an option for deb/rpm builds) or add a setup.cfg with:
+#   [install]
+#   install-purelib=/usr/share
+# which would mean a bit more unnecessary clutter.
+
+manFilename = "arm.1"
+if "install" in sys.argv:
+  sys.argv += ["--install-purelib", "/usr/share"]
+  
+  # Compresses the man page. This is a temporary file that we'll install. If
+  # something goes wrong then we'll print the issue and use the uncompressed man
+  # page instead.
+  
+  try:
+    manInputFile = open('arm.1', 'r')
+    manContents = manInputFile.read()
+    manInputFile.close()
+    
+    # temporary destination for the man page guarenteed to be unoccupied (to
+    # avoid conflicting with files that are already there)
+    manOutputFile = gzip.open(tempfile.mktemp("/arm.1.gz"), 'wb')
+    manOutputFile.write(manContents)
+    manOutputFile.close()
+    
+    # places in tmp rather than a relative path to avoid having this copy appear
+    # in the deb and rpm builds
+    manFilename = manOutputFile.name
+  except IOError, exc:
+    print "Unable to compress man page: %s" % exc
+
 setup(name='arm',
       version=VERSION,
       description='Terminal tor status monitor',
@@ -14,16 +59,23 @@
       packages=['arm', 'arm.interface', 'arm.interface.graphing', 'arm.util', 'arm.TorCtl'],
       package_dir={'arm': 'src'},
       data_files=[("/usr/bin", ["arm"]),
-                  ("/usr/lib/arm", ["src/armrc.defaults"]),
-                  ("/usr/share/man/man1", ["debian/arm.1.gz"])],
+                  ("/usr/share/man/man1", [manFilename]),
+                  (docPath, ["armrc.sample"]),
+                  ("/usr/share/arm", ["src/settings.cfg"])],
      )
 
+# Cleans up the temporary compressed man page.
+if manFilename != 'arm.1' and os.path.isfile(manFilename):
+  if "-q" not in sys.argv: print "Removing %s" % manFilename
+  os.remove(manFilename)
+
 # Removes the egg_info file. Apparently it is not optional during setup
 # (hardcoded in distutils/command/install.py), nor are there any arguments to
 # bypass its creation.
 # TODO: not sure how to remove this from the deb build too...
-eggPath = '/usr/lib/arm-%s.egg-info' % VERSION
-if os.path.isfile(eggPath):
+eggPath = '/usr/share/arm-%s.egg-info' % VERSION
+
+if not isDebInstall and os.path.isfile(eggPath):
   if "-q" not in sys.argv: print "Removing %s" % eggPath
   os.remove(eggPath)
 

Deleted: arm/release/src/armrc.defaults
===================================================================
--- arm/release/src/armrc.defaults	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/armrc.defaults	2010-11-28 10:58:57 UTC (rev 23873)
@@ -1,229 +0,0 @@
-# startup options
-startup.controlPassword
-startup.interface.ipAddress 127.0.0.1
-startup.interface.port 9051
-startup.blindModeEnabled false
-startup.events N3
-
-# Seconds between querying information
-queries.ps.rate 5
-queries.connections.minRate 5
-queries.refreshRate.rate 5
-
-# Renders the interface with color if set and the terminal supports it
-features.colorInterface true
-
-# Set this if you're running in a chroot jail or other environment where tor's
-# resources (log, state, etc) should have a prefix in their paths.
-features.pathPrefix
-
-# If set, arm appends any log messages it reports while running to the given
-# log file. This does not take filters into account or include prepopulated
-# events.
-features.logFile 
-
-# Paremters for the log panel
-# ---------------------------
-# showDateDividers
-#   show borders with dates for entries from previous days
-# showDuplicateEntries
-#   shows all log entries if true, otherwise collapses similar entries with an
-#   indicator for how much is being hidden
-# entryDuration
-#   number of days log entries are kept before being dropped (if zero then
-#   they're kept until cropped due to caching limits)
-# maxLinesPerEntry
-#   max number of lines to display for a single log entry
-# prepopulate
-#   attempts to read past events from the log file if true
-# prepopulateReadLimit
-#   maximum entries read from the log file, used to prevent huge log files from
-#   causing a slow startup time.
-# maxRefreshRate
-#   rate limiting (in milliseconds) for drawing the log if updates are made
-#   rapidly (for instance, when at the DEBUG runlevel)
-
-features.log.showDateDividers true
-features.log.showDuplicateEntries false
-features.log.entryDuration 7
-features.log.maxLinesPerEntry 4
-features.log.prepopulate true
-features.log.prepopulateReadLimit 5000
-features.log.maxRefreshRate 300
-
-# General graph parameters
-# ------------------------
-# height
-#   height of graphed stats
-# maxWidth
-#   maximum number of graphed entries
-# interval
-#   0 -> each second,   1 -> 5 seconds,     2 -> 30 seconds,  3 -> minutely,      
-#   4 -> 15 minutes,    5 -> half hour,     6 -> hourly,      7 -> daily
-# bound
-#   0 -> global maxima, 1 -> local maxima,  2 -> tight
-# type
-#   0 -> None, 1 -> Bandwidth, 2 -> Connections, 3 -> System Resources
-# showIntermediateBounds
-#   shows y-axis increments between the top/bottom bounds
-
-features.graph.height 7
-features.graph.maxWidth 150
-features.graph.interval 0
-features.graph.bound 1
-features.graph.type 1
-features.graph.showIntermediateBounds true
-
-# Parameters for graphing bandwidth stats
-# ---------------------------------------
-# 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)
-# transferInBystes
-#   shows rate measurments in bytes if true, bits otherwise
-# accounting.show
-#   provides accounting stats if AccountingMax was set
-# accounting.rate
-#   seconds between querying accounting stats
-# accounting.isTimeLong
-#   provides verbose measurements of time if true
-
-features.graph.bw.prepopulate true
-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 graphing ps stats
-# --------------------------------
-# primary/secondaryStat
-#   any numeric field provided by the ps command
-# cachedOnly
-#   determines if the graph should query ps or rely on cached results (this
-#   lowers the call volume but limits the graph's granularity)
-
-features.graph.ps.primaryStat %cpu
-features.graph.ps.secondaryStat rss
-features.graph.ps.cachedOnly 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.
-
-queries.hostnames.poolSize 5
-
-# Method of resolving hostnames
-# If true, uses python's internal "socket.gethostbyaddr" to resolve addresses
-# rather than the host command. This is ignored if the system's unable to make
-# parallel requests. Resolving this way seems to be much slower than host calls
-# in practice.
-
-queries.hostnames.useSocketModule false
-
-# Caching parameters
-cache.sysCalls.size 600
-cache.hostnames.size 700000
-cache.hostnames.trimSize 200000
-cache.logPanel.size 1000
-cache.armLog.size 1000
-cache.armLog.trimSize 200
-
-# Runlevels at which arm logs its events
-log.refreshRate DEBUG
-log.configEntryNotFound NONE
-log.configEntryUndefined NOTICE
-log.configEntryTypeError NOTICE
-log.torCtlPortClosed NOTICE
-log.torGetInfo DEBUG
-log.torGetConf DEBUG
-log.torEventTypeUnrecognized NOTICE
-log.torPrefixPathInvalid NOTICE
-log.sysCallMade DEBUG
-log.sysCallCached NONE
-log.sysCallFailed INFO
-log.sysCallCacheGrowing INFO
-log.panelRecreated DEBUG
-log.graph.ps.invalidStat WARN
-log.graph.ps.abandon WARN
-log.graph.bw.prepopulateSuccess NOTICE
-log.graph.bw.prepopulateFailure NOTICE
-log.logPanel.prepopulateSuccess INFO
-log.logPanel.prepopulateFailed WARN
-log.logPanel.logFileOpened NOTICE
-log.logPanel.logFileWriteFailed ERR
-log.logPanel.forceDoubleRedraw DEBUG
-log.connLookupFailed INFO
-log.connLookupFailover NOTICE
-log.connLookupAbandon WARN
-log.connLookupRateGrowing NONE
-log.hostnameCacheTrimmed INFO
-log.cursesColorSupport INFO
-
-# 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
-# start of both messages then the entries are flagged as duplicates. If the
-# entry begins with an asterisk (*) then it checks if the substrings exist
-# anywhere in the messages.
-# 
-# Examples for the complete messages:
-# [BW] READ: 0, WRITTEN: 0
-# [DEBUG] connection_handle_write(): After TLS write of 512: 0 read, 586 written
-# [DEBUG] flush_chunk_tls(): flushed 512 bytes, 0 ready to flush, 0 remain.
-# [DEBUG] conn_read_callback(): socket 7 wants to read.
-# [DEBUG] conn_write_callback(): socket 51 wants to write.
-# [DEBUG] connection_remove(): removing socket -1 (type OR), n_conns now 50
-# [DEBUG] connection_or_process_cells_from_inbuf(): 7: starting, inbuf_datalen 0 (0 pending in tls object).
-# [DEBUG] connection_read_to_buf(): 38: starting, inbuf_datalen 0 (0 pending in tls object). at_most 12800.
-# [DEBUG] connection_read_to_buf(): TLS connection closed on read. Closing. (Nickname moria1, address 128.31.0.34)
-# [INFO] run_connection_housekeeping(): Expiring non-open OR connection to fd 16 (79.193.61.171:443).
-# [INFO] rep_hist_downrate_old_runs(): Discounting all old stability info by a factor of 0.950000
-# [NOTICE] We stalled too much while trying to write 150 bytes to address
-#          [scrubbed].  If this happens a lot, either something is wrong with
-#          your network connection, or something is wrong with theirs. (fd 238,
-#          type Directory, state 1, marked at main.c:702).
-# [NOTICE] I learned some more directory information, but not enough to build a
-#          circuit: We have only 469/2027 usable descriptors.
-# [NOTICE] Attempt by %s to open a stream from unknown relay. Closing.
-# [WARN] You specified a server "Amunet8" by name, but this name is not
-#        registered
-# [WARN] I have no descriptor for the router named "Amunet8" in my declared
-#        family; I'll use the nickname as is, but this   may confuse clients.
-# [WARN] Problem bootstrapping. Stuck at 80%: Connecting to the Tor network.
-#        (Network is unreachable; NOROUTE; count 47;    recommendation warn)
-# [WARN] 4 unknown, 1 missing key, 3 good, 0 bad, 1 no signature, 4 required
-# [ARM_DEBUG] refresh rate: 0.001 seconds
-# [ARM_DEBUG] system call: ps -p 2354 -o %cpu,rss,%mem,etime (runtime: 0.02)
-# [ARM_DEBUG] system call: netstat -npt | grep 2354/tor (runtime: 0.02)
-# [ARM_DEBUG] recreating panel 'graph' with the dimensions of 14/124
-# [ARM_DEBUG] redrawing the log panel with the corrected content height (estimat was off by 4)
-# [ARM_DEBUG] GETINFO accounting/bytes-left (runtime: 0.0006)
-
-msg.BW READ:
-msg.DEBUG connection_handle_write(): After TLS write of
-msg.DEBUG flush_chunk_tls(): flushed
-msg.DEBUG conn_read_callback(): socket
-msg.DEBUG conn_write_callback(): socket
-msg.DEBUG connection_remove(): removing socket
-msg.DEBUG connection_or_process_cells_from_inbuf():
-msg.DEBUG *pending in tls object). at_most
-msg.DEBUG connection_read_to_buf(): TLS connection closed on read. Closing.
-msg.INFO run_connection_housekeeping(): Expiring
-msg.INFO rep_hist_downrate_old_runs(): Discounting all old stability info by a factor of
-msg.NOTICE We stalled too much while trying to write
-msg.NOTICE I learned some more directory information, but not enough to build a circuit
-msg.NOTICE Attempt by
-msg.WARN You specified a server
-msg.WARN I have no descriptor for the router named
-msg.WARN Problem bootstrapping. Stuck at
-msg.WARN *missing key,
-msg.ARM_DEBUG refresh rate:
-msg.ARM_DEBUG system call: ps
-msg.ARM_DEBUG system call: netstat
-msg.ARM_DEBUG recreating panel '
-msg.ARM_DEBUG redrawing the log panel with the corrected content height (
-msg.ARM_DEBUG GETINFO accounting/bytes
-msg.ARM_DEBUG GETINFO accounting/bytes-left
-msg.ARM_DEBUG GETINFO accounting/interval-end
-msg.ARM_DEBUG GETINFO accounting/hibernating
-

Modified: arm/release/src/interface/__init__.py
===================================================================
--- arm/release/src/interface/__init__.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/interface/__init__.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -2,5 +2,5 @@
 Panels, popups, and handlers comprising the arm user interface.
 """
 
-__all__ = ["confPanel", "connPanel", "controller", "descriptorPopup", "fileDescriptorPopup", "headerPanel", "logPanel"]
+__all__ = ["configPanel", "connPanel", "controller", "descriptorPopup", "fileDescriptorPopup", "headerPanel", "logPanel", "torrcPanel"]
 

Deleted: arm/release/src/interface/confPanel.py
===================================================================
--- arm/release/src/interface/confPanel.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/interface/confPanel.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -1,292 +0,0 @@
-#!/usr/bin/env python
-# confPanel.py -- Presents torrc with syntax highlighting.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import math
-import curses
-import socket
-
-import controller
-from TorCtl import TorCtl
-from util import log, panel, torTools, uiTools
-
-# torrc parameters that can be defined multiple times without overwriting
-# from src/or/config.c (entries with LINELIST or LINELIST_S)
-# last updated for tor version 0.2.1.19
-MULTI_LINE_PARAM = ["AlternateBridgeAuthority", "AlternateDirAuthority", "AlternateHSAuthority", "AuthDirBadDir", "AuthDirBadExit", "AuthDirInvalid", "AuthDirReject", "Bridge", "ControlListenAddress", "ControlSocket", "DirListenAddress", "DirPolicy", "DirServer", "DNSListenAddress", "ExitPolicy", "HashedControlPassword", "HiddenServiceDir", "HiddenServiceOptions", "HiddenServicePort", "HiddenServiceVersion", "HiddenServiceAuthorizeClient", "HidServAuth", "Log", "MapAddress", "NatdListenAddress", "NodeFamily", "ORListenAddress", "ReachableAddresses", "ReachableDirAddresses", "ReachableORAddresses", "RecommendedVersions", "RecommendedClientVersions", "RecommendedServerVersions", "SocksListenAddress", "SocksPolicy", "TransListenAddress", "__HashedControlSessionPassword"]
-
-# hidden service options need to be fetched with HiddenServiceOptions
-HIDDEN_SERVICE_PARAM = ["HiddenServiceDir", "HiddenServiceOptions", "HiddenServicePort", "HiddenServiceVersion", "HiddenServiceAuthorizeClient"]
-HIDDEN_SERVICE_FETCH_PARAM = "HiddenServiceOptions"
-
-# size modifiers allowed by config.c
-LABEL_KB = ["kb", "kbyte", "kbytes", "kilobyte", "kilobytes"]
-LABEL_MB = ["m", "mb", "mbyte", "mbytes", "megabyte", "megabytes"]
-LABEL_GB = ["gb", "gbyte", "gbytes", "gigabyte", "gigabytes"]
-LABEL_TB = ["tb", "terabyte", "terabytes"]
-
-# GETCONF aliases (from the _option_abbrevs struct of src/or/config.c)
-# fix for: https://trac.torproject.org/projects/tor/ticket/1798
-# TODO: this has been fixed in tor- wait for a while then retest and remove
-# TODO: the following alias entry doesn't work on Tor 0.2.1.19:
-# "HashedControlPassword": "__HashedControlSessionPassword"
-CONF_ALIASES = {"l": "Log",
-                "AllowUnverifiedNodes": "AllowInvalidNodes",
-                "AutomapHostSuffixes": "AutomapHostsSuffixes",
-                "AutomapHostOnResolve": "AutomapHostsOnResolve",
-                "BandwidthRateBytes": "BandwidthRate",
-                "BandwidthBurstBytes": "BandwidthBurst",
-                "DirFetchPostPeriod": "StatusFetchPeriod",
-                "MaxConn": "ConnLimit",
-                "ORBindAddress": "ORListenAddress",
-                "DirBindAddress": "DirListenAddress",
-                "SocksBindAddress": "SocksListenAddress",
-                "UseHelperNodes": "UseEntryGuards",
-                "NumHelperNodes": "NumEntryGuards",
-                "UseEntryNodes": "UseEntryGuards",
-                "NumEntryNodes": "NumEntryGuards",
-                "ResolvConf": "ServerDNSResolvConfFile",
-                "SearchDomains": "ServerDNSSearchDomains",
-                "ServerDNSAllowBrokenResolvConf": "ServerDNSAllowBrokenConfig",
-                "PreferTunnelledDirConns": "PreferTunneledDirConns",
-                "BridgeAuthoritativeDirectory": "BridgeAuthoritativeDir",
-                "StrictEntryNodes": "StrictNodes",
-                "StrictExitNodes": "StrictNodes"}
-
-
-# time modifiers allowed by config.c
-LABEL_MIN = ["minute", "minutes"]
-LABEL_HOUR = ["hour", "hours"]
-LABEL_DAY = ["day", "days"]
-LABEL_WEEK = ["week", "weeks"]
-
-class ConfPanel(panel.Panel):
-  """
-  Presents torrc with syntax highlighting in a scroll-able area.
-  """
-  
-  def __init__(self, stdscr, confLocation, conn):
-    panel.Panel.__init__(self, stdscr, "conf", 0)
-    self.confLocation = confLocation
-    self.showLineNum = True
-    self.stripComments = False
-    self.confContents = []
-    self.scroll = 0
-    
-    # lines that don't matter due to duplicates
-    self.irrelevantLines = []
-    
-    # used to check consistency with tor's actual values - corrections mapping
-    # is of line numbers (one-indexed) to tor's actual values
-    self.corrections = {}
-    self.conn = conn
-    
-    self.reset()
-  
-  def reset(self, logErrors=True):
-    """
-    Reloads torrc contents and resets scroll height. Returns True if
-    successful, else false.
-    """
-    
-    try:
-      resetSuccessful = True
-      
-      confFile = open(torTools.getPathPrefix() + self.confLocation, "r")
-      self.confContents = confFile.readlines()
-      confFile.close()
-      
-      # checks if torrc differs from get_option data
-      self.irrelevantLines = []
-      self.corrections = {}
-      parsedCommands = {}       # mapping of parsed commands to line numbers
-      
-      for lineNumber in range(len(self.confContents)):
-        lineText = self.confContents[lineNumber].strip()
-        
-        if lineText and lineText[0] != "#":
-          # relevant to tor (not blank nor comment)
-          ctlEnd = lineText.find(" ")   # end of command
-          argEnd = lineText.find("#")   # end of argument (start of comment or end of line)
-          if argEnd == -1: argEnd = len(lineText)
-          command, argument = lineText[:ctlEnd], lineText[ctlEnd:argEnd].strip()
-          
-          # replace aliases with the internal representation of the command
-          if command in CONF_ALIASES: command = CONF_ALIASES[command]
-          
-          # tor appears to replace tabs with a space, for instance:
-          # "accept\t*:563" is read back as "accept *:563"
-          argument = argument.replace("\t", " ")
-          
-          # expands value if it's a size or time
-          comp = argument.strip().lower().split(" ")
-          if len(comp) > 1:
-            size = 0
-            if comp[1] in LABEL_KB: size = int(comp[0]) * 1024
-            elif comp[1] in LABEL_MB: size = int(comp[0]) * 1048576
-            elif comp[1] in LABEL_GB: size = int(comp[0]) * 1073741824
-            elif comp[1] in LABEL_TB: size = int(comp[0]) * 1099511627776
-            elif comp[1] in LABEL_MIN: size = int(comp[0]) * 60
-            elif comp[1] in LABEL_HOUR: size = int(comp[0]) * 3600
-            elif comp[1] in LABEL_DAY: size = int(comp[0]) * 86400
-            elif comp[1] in LABEL_WEEK: size = int(comp[0]) * 604800
-            if size != 0: argument = str(size)
-              
-          # most parameters are overwritten if defined multiple times, if so
-          # it's erased from corrections and noted as duplicate instead
-          if not command in MULTI_LINE_PARAM and command in parsedCommands.keys():
-            previousLineNum = parsedCommands[command]
-            self.irrelevantLines.append(previousLineNum)
-            if previousLineNum in self.corrections.keys(): del self.corrections[previousLineNum]
-          
-          parsedCommands[command] = lineNumber + 1
-          
-          # check validity against tor's actual state
-          try:
-            actualValues = []
-            if command in HIDDEN_SERVICE_PARAM:
-              # hidden services are fetched via a special command
-              hsInfo = self.conn.get_option(HIDDEN_SERVICE_FETCH_PARAM)
-              for entry in hsInfo:
-                if entry[0] == command:
-                  actualValues.append(entry[1])
-                  break
-            else:
-              # general case - fetch all valid values
-              for key, val in self.conn.get_option(command):
-                if val == None:
-                  # TODO: investigate situations where this might occure
-                  # (happens if trying to parse HIDDEN_SERVICE_PARAM)
-                  if logErrors: log.log(log.WARN, "BUG: Failed to find torrc value for %s" % key)
-                  continue
-                
-                # TODO: check for a better way of figuring out CSV parameters
-                # (kinda doubt this is right... in config.c its listed as being
-                # a 'LINELIST') - still, good enough for common cases
-                if command in MULTI_LINE_PARAM: toAdd = val.split(",")
-                else: toAdd = [val]
-                
-                for newVal in toAdd:
-                  newVal = newVal.strip()
-                  if newVal not in actualValues: actualValues.append(newVal)
-            
-            # there might be multiple values on a single line - if so, check each
-            if command in MULTI_LINE_PARAM and "," in argument:
-              arguments = []
-              for entry in argument.split(","):
-                arguments.append(entry.strip())
-            else:
-              arguments = [argument]
-            
-            for entry in arguments:
-              if not entry in actualValues:
-                self.corrections[lineNumber + 1] = ", ".join(actualValues)
-          except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
-            if logErrors: log.log(log.WARN, "Unable to validate line %i of the torrc: %s" % (lineNumber + 1, lineText))
-      
-      # logs issues that arose
-      if self.irrelevantLines and logErrors:
-        if len(self.irrelevantLines) > 1: first, second, third = "Entries", "are", ", including lines"
-        else: first, second, third = "Entry", "is", " on line"
-        baseMsg = "%s in your torrc %s ignored due to duplication%s" % (first, second, third)
-        
-        log.log(log.NOTICE, "%s: %s (highlighted in blue)" % (baseMsg, ", ".join([str(val) for val in self.irrelevantLines])))
-      
-      if self.corrections and logErrors:
-        log.log(log.WARN, "Tor's state differs from loaded torrc")
-    except IOError, exc:
-      resetSuccessful = False
-      self.confContents = ["### Unable to load torrc ###"]
-      if logErrors: log.log(log.WARN, "Unable to load torrc (%s)" % str(exc))
-    
-    self.scroll = 0
-    return resetSuccessful
-  
-  def handleKey(self, key):
-    if uiTools.isScrollKey(key):
-      pageHeight = self.getPreferredSize()[0] - 1
-      contentHeight = len(self.confContents)
-      self.scroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, contentHeight)
-    elif key == ord('n') or key == ord('N'): self.showLineNum = not self.showLineNum
-    elif key == ord('s') or key == ord('S'):
-      self.stripComments = not self.stripComments
-      self.scroll = 0
-    self.redraw(True)
-  
-  def draw(self, subwindow, width, height):
-    self.addstr(0, 0, "Tor Config (%s):" % self.confLocation, curses.A_STANDOUT)
-    
-    pageHeight = height - 1
-    if self.confContents: numFieldWidth = int(math.log10(len(self.confContents))) + 1
-    else: numFieldWidth = 0 # torrc is blank
-    lineNum, displayLineNum = self.scroll + 1, 1 # lineNum corresponds to torrc, displayLineNum concerns what's presented
-    
-    # determine the ending line in the display (prevents us from going to the 
-    # effort of displaying lines that aren't visible - isn't really a 
-    # noticeable improvement unless the torrc is bazaarly long) 
-    if not self.stripComments:
-      endingLine = min(len(self.confContents), self.scroll + pageHeight)
-    else:
-      # checks for the last line of displayable content (ie, non-comment)
-      endingLine = self.scroll
-      displayedLines = 0        # number of lines of content
-      for i in range(self.scroll, len(self.confContents)):
-        endingLine += 1
-        lineText = self.confContents[i].strip()
-        
-        if lineText and lineText[0] != "#":
-          displayedLines += 1
-          if displayedLines == pageHeight: break
-    
-    for i in range(self.scroll, endingLine):
-      lineText = self.confContents[i].strip()
-      skipLine = False # true if we're not presenting line due to stripping
-      
-      command, argument, correction, comment = "", "", "", ""
-      commandColor, argumentColor, correctionColor, commentColor = "green", "cyan", "cyan", "white"
-      
-      if not lineText:
-        # no text
-        if self.stripComments: skipLine = True
-      elif lineText[0] == "#":
-        # whole line is commented out
-        comment = lineText
-        if self.stripComments: skipLine = True
-      else:
-        # parse out command, argument, and possible comment
-        ctlEnd = lineText.find(" ")   # end of command
-        argEnd = lineText.find("#")   # end of argument (start of comment or end of line)
-        if argEnd == -1: argEnd = len(lineText)
-        
-        command, argument, comment = lineText[:ctlEnd], lineText[ctlEnd:argEnd], lineText[argEnd:]
-        if self.stripComments: comment = ""
-        
-        # Tabs print as three spaces. Keeping them as tabs is problematic for
-        # the layout since it's counted as a single character, but occupies
-        # several cells.
-        argument = argument.replace("\t", "   ")
-        
-        # changes presentation if value's incorrect or irrelevant
-        if lineNum in self.corrections.keys():
-          argumentColor = "red"
-          correction = " (%s)" % self.corrections[lineNum]
-        elif lineNum in self.irrelevantLines:
-          commandColor = "blue"
-          argumentColor = "blue"
-      
-      if not skipLine:
-        numOffset = 0     # offset for line numbering
-        if self.showLineNum:
-          self.addstr(displayLineNum, 0, ("%%%ii" % numFieldWidth) % lineNum, curses.A_BOLD | uiTools.getColor("yellow"))
-          numOffset = numFieldWidth + 1
-        
-        xLoc = 0
-        displayLineNum, xLoc = controller.addstr_wrap(self, displayLineNum, xLoc, command, curses.A_BOLD | uiTools.getColor(commandColor), numOffset)
-        displayLineNum, xLoc = controller.addstr_wrap(self, displayLineNum, xLoc, argument, curses.A_BOLD | uiTools.getColor(argumentColor), numOffset)
-        displayLineNum, xLoc = controller.addstr_wrap(self, displayLineNum, xLoc, correction, curses.A_BOLD | uiTools.getColor(correctionColor), numOffset)
-        displayLineNum, xLoc = controller.addstr_wrap(self, displayLineNum, xLoc, comment, uiTools.getColor(commentColor), numOffset)
-        
-        displayLineNum += 1
-      
-      lineNum += 1
-

Copied: arm/release/src/interface/configPanel.py (from rev 23872, arm/trunk/src/interface/configPanel.py)
===================================================================
--- arm/release/src/interface/configPanel.py	                        (rev 0)
+++ arm/release/src/interface/configPanel.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -0,0 +1,312 @@
+"""
+Panel presenting the configuration state for tor or arm. Options can be edited
+and the resulting configuration files saved.
+"""
+
+import curses
+import threading
+
+from util import conf, 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}
+
+# 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
+
+# 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"}
+
+# attributes of a ConfigEntry
+FIELD_CATEGORY, FIELD_OPTION, FIELD_VALUE, FIELD_TYPE, FIELD_ARG_USAGE, FIELD_DESCRIPTION, FIELD_MAN_ENTRY, FIELD_IS_DEFAULT = range(8)
+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_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, manEntry):
+    self.fields = {}
+    self.fields[FIELD_OPTION] = option
+    self.fields[FIELD_TYPE] = type
+    self.fields[FIELD_IS_DEFAULT] = isDefault
+    
+    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
+    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] = ""
+  
+  def get(self, field):
+    """
+    Provides back the value in the given field.
+    
+    Arguments:
+      field - enum for the field to be provided back
+    """
+    
+    if field == FIELD_VALUE: return self._getValue()
+    else: return self.fields[field]
+  
+  def _getValue(self):
+    """
+    Provides the current value of the configuration entry, taking advantage of
+    the torTools caching to effectively query the accurate value. This uses the
+    value's type to provide a user friendly representation if able.
+    """
+    
+    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"):
+      confValue = "False" if confValue == "0" else "True"
+    elif self.get(FIELD_TYPE) == "DataSize" and confValue.isdigit():
+      confValue = uiTools.getSizeLabel(int(confValue))
+    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):
+  """
+  Renders a listing of the tor or arm configuration state, allowing options to
+  be selected and edited.
+  """
+  
+  def __init__(self, stdscr, configType, config=None):
+    panel.Panel.__init__(self, stdscr, "configState", 0)
+    
+    self.sortOrdering = DEFAULT_SORT_ORDER
+    self._config = dict(DEFAULT_CONFIG)
+    if config:
+      config.update(self._config, {
+        "features.config.selectionDetails.height": 0,
+        "features.config.state.colWidth.option": 5,
+        "features.config.state.colWidth.value": 5})
+      
+      self.sortOrdering = config.getIntCSV("features.config.order", self.sortOrdering, 3, 0, 6)
+    
+    self.configType = configType
+    self.confContents = []
+    self.scroller = uiTools.Scroller(True)
+    self.valsLock = threading.RLock()
+    
+    if self.configType == TOR_STATE:
+      conn = torTools.getConn()
+      customOptions = torConfig.getCustomOptions()
+      configOptionLines = conn.getInfo("config/names", "").strip().split("\n")
+      
+      for line in configOptionLines:
+        # lines are of the form "<option> <type>", like:
+        # UseEntryGuards Boolean
+        confOption, confType = line.strip().split(" ", 1)
+        
+        # skips private and virtual entries if not set 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
+        
+        manEntry = torConfig.getConfigDescription(confOption)
+        self.confContents.append(ConfigEntry(confOption, confType, not confOption in customOptions, manEntry))
+      
+      self.setSortOrder() # initial sorting of the contents
+    elif self.configType == ARM_STATE:
+      # loaded via the conf utility
+      armConf = conf.getConfig("arm")
+      for key in armConf.getKeys():
+        pass # TODO: implement
+  
+  def getSelection(self):
+    """
+    Provides the currently selected entry.
+    """
+    
+    return self.scroller.getCursorSelection(self.confContents)
+  
+  def setSortOrder(self, ordering = None):
+    """
+    Sets the configuration 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.confContents.sort(key=lambda i: (i.getAttr(self.sortOrdering)))
+    self.valsLock.release()
+  
+  def handleKey(self, key):
+    self.valsLock.acquire()
+    if uiTools.isScrollKey(key):
+      pageHeight = self.getPreferredSize()[0] - 1
+      detailPanelHeight = self._config["features.config.selectionDetails.height"]
+      if detailPanelHeight > 0 and detailPanelHeight + 2 <= pageHeight:
+        pageHeight -= (detailPanelHeight + 1)
+      
+      isChanged = self.scroller.handleKey(key, self.confContents, pageHeight)
+      if isChanged: self.redraw(True)
+    self.valsLock.release()
+  
+  def draw(self, subwindow, 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)
+    
+    # panel with details for the current selection
+    detailPanelHeight = self._config["features.config.selectionDetails.height"]
+    if detailPanelHeight == 0 or detailPanelHeight + 2 >= height:
+      # no detail panel
+      detailPanelHeight = 0
+      scrollLoc = self.scroller.getScrollLoc(self.confContents, height - 1)
+      cursorSelection = self.getSelection()
+    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)
+      cursorSelection = self.getSelection()
+      
+      self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, titleLabel)
+    
+    # draws left-hand scroll bar if content's longer than the height
+    scrollOffset = 0
+    if len(self.confContents) > height - detailPanelHeight - 1:
+      scrollOffset = 3
+      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self.confContents), 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]
+      drawLine = lineNum + detailPanelHeight + 1 - scrollLoc
+      
+      optionLabel = uiTools.cropStr(entry.get(FIELD_OPTION), optionWidth)
+      valueLabel = uiTools.cropStr(entry.get(FIELD_VALUE), valueWidth)
+      
+      # ends description at the first newline
+      descriptionLabel = uiTools.cropStr(entry.get(FIELD_DESCRIPTION).split("\n")[0], 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)])
+      if entry == cursorSelection: lineFormat |= curses.A_STANDOUT
+      
+      lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, descriptionWidth)
+      lineText = lineTextLayout % (optionLabel, valueLabel, descriptionLabel)
+      self.addstr(drawLine, scrollOffset, lineText, lineFormat)
+      
+      if drawLine >= height: break
+    
+    self.valsLock.release()
+  
+  def _drawSelectionPanel(self, cursorSelection, width, detailPanelHeight, titleLabel):
+    """
+    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)
+    
+    # border (sides)
+    self.win.vline(1, 0, curses.ACS_VLINE, detailPanelHeight - 1)
+    self.win.vline(1, width, curses.ACS_VLINE, detailPanelHeight - 1)
+    
+    # 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)
+    
+    # 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)))
+      valueAttrLabel = ", ".join(valueAttr)
+      
+      valueLabelWidth = width - 12 - len(valueAttrLabel)
+      valueLabel = uiTools.cropStr(cursorSelection.get(FIELD_VALUE), valueLabelWidth)
+      
+      self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat)
+    
+    # remainder is filled with the man page description
+    descriptionHeight = max(0, detailPanelHeight - 3)
+    descriptionContent = "Description: " + cursorSelection.get(FIELD_DESCRIPTION)
+    
+    for i in range(descriptionHeight):
+      # checks if we're done writing the description
+      if not descriptionContent: break
+      
+      # there's a leading indent after the first line
+      if i > 0: descriptionContent = "  " + descriptionContent
+      
+      # we only want to work with content up until the next newline
+      if "\n" in descriptionContent:
+        lineContent, descriptionContent = descriptionContent.split("\n", 1)
+      else: lineContent, descriptionContent = descriptionContent, ""
+      
+      if i != descriptionHeight - 1:
+        # there's more lines to display
+        msg, remainder = uiTools.cropStr(lineContent, width - 2, 4, 4, uiTools.END_WITH_HYPHEN, True)
+        descriptionContent = remainder.strip() + descriptionContent
+      else:
+        # this is the last line, end it with an ellipse
+        msg = uiTools.cropStr(lineContent, width - 2, 4, 4)
+      
+      self.addstr(3 + i, 2, msg, selectionFormat)
+

Modified: arm/release/src/interface/connPanel.py
===================================================================
--- arm/release/src/interface/connPanel.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/interface/connPanel.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -89,7 +89,8 @@
         elif label == "Country Code": color = "yellow"
         elif label == "Connection Time": color = "magenta"
       
-      if color: return "<%s>%s</%s>" % (color, label, color)
+      #if color: return "<%s>%s</%s>" % (color, label, color)
+      if color: return (label, color)
       else: return label
   
   raise ValueError(sortType)

Modified: arm/release/src/interface/controller.py
===================================================================
--- arm/release/src/interface/controller.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/interface/controller.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -6,10 +6,12 @@
 Curses (terminal) interface for the arm relay status monitor.
 """
 
+import os
 import re
 import math
 import time
 import curses
+import curses.textpad
 import socket
 from TorCtl import TorCtl
 from TorCtl import TorUtil
@@ -18,11 +20,12 @@
 import graphing.graphPanel
 import logPanel
 import connPanel
-import confPanel
+import configPanel
+import torrcPanel
 import descriptorPopup
 import fileDescriptorPopup
 
-from util import conf, log, connections, hostnames, panel, sysTools, torTools, uiTools
+from util import conf, log, connections, hostnames, panel, sysTools, torConfig, torTools, uiTools
 import graphing.bandwidthStats
 import graphing.connStats
 import graphing.psStats
@@ -39,15 +42,21 @@
 PAGES = [
   ["graph", "log"],
   ["conn"],
+  ["config"],
   ["torrc"]]
 PAUSEABLE = ["header", "graph", "log", "conn"]
 
-CONFIG = {"features.graph.type": 1,
+CONFIG = {"log.torrc.readFailed": log.WARN,
+          "features.graph.type": 1,
+          "features.config.prepopulateEditValues": True,
           "queries.refreshRate.rate": 5,
           "log.torEventTypeUnrecognized": log.NOTICE,
           "features.graph.bw.prepopulate": True,
+          "log.startTime": log.INFO,
           "log.refreshRate": log.DEBUG,
-          "log.configEntryUndefined": log.NOTICE}
+          "log.configEntryUndefined": log.NOTICE,
+          "log.torrc.validation.torStateDiffers": log.WARN,
+          "log.torrc.validation.unnecessaryTorrcEntries": log.WARN}
 
 class ControlPanel(panel.Panel):
   """ Draws single line label for interface controls. """
@@ -259,6 +268,98 @@
   
   return selection
 
+def showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors):
+  """
+  Displays a sorting dialog of the form:
+  
+  Current Order: <previous selection>
+  New Order: <selections made>
+  
+  <option 1>    <option 2>    <option 3>   Cancel
+  
+  Options are colored when among the "Current Order" or "New Order", but not
+  when an option below them. If cancel is selected or the user presses escape
+  then this returns None. Otherwise, the new ordering is provided.
+  
+  Arguments:
+    stdscr, panels, isPaused, page - boiler plate arguments of the controller
+        (should be refactored away when rewriting)
+    
+    titleLabel   - title displayed for the popup window
+    options      - ordered listing of option labels
+    oldSelection - current ordering
+    optionColors - mappings of options to their color
+  
+  """
+  
+  panel.CURSES_LOCK.acquire()
+  newSelections = []  # new ordering
+  
+  try:
+    setPauseState(panels, isPaused, page, True)
+    curses.cbreak() # wait indefinitely for key presses (no timeout)
+    
+    popup = panels["popup"]
+    cursorLoc = 0       # index of highlighted option
+    
+    # label for the inital ordering
+    formattedPrevListing = []
+    for sortType in oldSelection:
+      colorStr = optionColors.get(sortType, "white")
+      formattedPrevListing.append("<%s>%s</%s>" % (colorStr, sortType, colorStr))
+    prevOrderingLabel = "<b>Current Order: %s</b>" % ", ".join(formattedPrevListing)
+    
+    selectionOptions = list(options)
+    selectionOptions.append("Cancel")
+    
+    while len(newSelections) < len(oldSelection):
+      popup.clear()
+      popup.win.box()
+      popup.addstr(0, 0, titleLabel, curses.A_STANDOUT)
+      popup.addfstr(1, 2, prevOrderingLabel)
+      
+      # provides new ordering
+      formattedNewListing = []
+      for sortType in newSelections:
+        colorStr = optionColors.get(sortType, "white")
+        formattedNewListing.append("<%s>%s</%s>" % (colorStr, sortType, colorStr))
+      newOrderingLabel = "<b>New Order: %s</b>" % ", ".join(formattedNewListing)
+      popup.addfstr(2, 2, newOrderingLabel)
+      
+      # presents remaining options, each row having up to four options with
+      # spacing of nineteen cells
+      row, col = 4, 0
+      for i in range(len(selectionOptions)):
+        popup.addstr(row, col * 19 + 2, selectionOptions[i], curses.A_STANDOUT if cursorLoc == i else curses.A_NORMAL)
+        col += 1
+        if col == 4: row, col = row + 1, 0
+      
+      popup.refresh()
+      
+      key = stdscr.getch()
+      if key == curses.KEY_LEFT: cursorLoc = max(0, cursorLoc - 1)
+      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(' ')):
+        # selected entry (the ord of '10' seems needed to pick up enter)
+        selection = selectionOptions[cursorLoc]
+        if selection == "Cancel": break
+        else:
+          newSelections.append(selection)
+          selectionOptions.remove(selection)
+          cursorLoc = min(cursorLoc, len(selectionOptions) - 1)
+      elif key == 27: break # esc - cancel
+      
+    setPauseState(panels, isPaused, page)
+    curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+  finally:
+    panel.CURSES_LOCK.release()
+  
+  if len(newSelections) == len(oldSelection):
+    return newSelections
+  else: return None
+
 def setEventListening(selectedEvents, isBlindMode):
   # creates a local copy, note that a suspected python bug causes *very*
   # puzzling results otherwise when trying to discard entries (silently
@@ -304,7 +405,7 @@
   for panelKey in PAGES[page]:
     panels[panelKey].redraw(True)
 
-def drawTorMonitor(stdscr, loggedEvents, isBlindMode):
+def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
   """
   Starts arm interface reflecting information on provided control port.
   
@@ -344,17 +445,87 @@
   # attempts to determine tor's current pid (left as None if unresolveable, logging an error later)
   torPid = torTools.getConn().getMyPid()
   
+  #try:
+  #  confLocation = conn.get_info("config-file")["config-file"]
+  #  if confLocation[0] != "/":
+  #    # relative path - attempt to add process pwd
+  #    try:
+  #      results = sysTools.call("pwdx %s" % torPid)
+  #      if len(results) == 1 and len(results[0].split()) == 2: confLocation = "%s/%s" % (results[0].split()[1], confLocation)
+  #    except IOError: pass # pwdx call failed
+  #except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
+  #  confLocation = ""
+  
+  # loads the torrc and provides warnings in case of validation errors
+  loadedTorrc = torConfig.getTorrc()
+  loadedTorrc.getLock().acquire()
+  
   try:
-    confLocation = conn.get_info("config-file")["config-file"]
-    if confLocation[0] != "/":
-      # relative path - attempt to add process pwd
-      try:
-        results = sysTools.call("pwdx %s" % torPid)
-        if len(results) == 1 and len(results[0].split()) == 2: confLocation = "%s/%s" % (results[0].split()[1], confLocation)
-      except IOError: pass # pwdx call failed
-  except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
-    confLocation = ""
+    loadedTorrc.load()
+  except IOError, exc:
+    msg = "Unable to load torrc (%s)" % sysTools.getFileErrorMsg(exc)
+    log.log(CONFIG["log.torrc.readFailed"], msg)
   
+  if loadedTorrc.isLoaded():
+    corrections = loadedTorrc.getCorrections()
+    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 duplicateOptions or defaultOptions:
+      msg = "Unneeded torrc entries found. They've been highlighted in blue on the torrc page."
+      
+      if duplicateOptions:
+        if len(duplicateOptions) > 1:
+          msg += "\n- entries ignored due to having duplicates: "
+        else:
+          msg += "\n- entry ignored due to having a duplicate: "
+        
+        duplicateOptions.sort()
+        msg += ", ".join(duplicateOptions)
+      
+      if defaultOptions:
+        if len(defaultOptions) > 1:
+          msg += "\n- entries match their default values"
+        else:
+          msg += "\n- entry matches its default value"
+        
+        defaultOptions.sort()
+        msg += ", ".join(defaultOptions)
+      
+      log.log(CONFIG["log.torrc.validation.unnecessaryTorrcEntries"], msg)
+    
+    if mismatchLines or missingOptions:
+      msg = "The torrc differ from what tor's using. You can issue a sighup to reload the torrc values by pressing x."
+      
+      if mismatchLines:
+        if len(mismatchLines) > 1:
+          msg += "\n- torrc values differ on line lines: "
+        else:
+          msg += "\n- torrc value differs on line line: "
+        
+        mismatchLines.sort()
+        msg += ", ".join([str(val + 1) for val in mismatchLines])
+        
+      if missingOptions:
+        if len(missingOptions) > 1:
+          msg += "\n-configuration values are missing from the torrc: "
+        else:
+          msg += "\n-configuration value is missing from the torrc: "
+        
+        missingOptions.sort()
+        msg += ", ".join(missingOptions)
+      
+      log.log(CONFIG["log.torrc.validation.torStateDiffers"], msg)
+  
+  loadedTorrc.getLock().release()
+  
   # minor refinements for connection resolver
   if not isBlindMode:
     resolver = connections.getResolver("tor")
@@ -378,7 +549,8 @@
   
   panels["conn"] = connPanel.ConnPanel(stdscr, conn, isBlindMode)
   panels["control"] = ControlPanel(stdscr, isBlindMode)
-  panels["torrc"] = confPanel.ConfPanel(stdscr, confLocation, conn)
+  panels["config"] = configPanel.ConfigPanel(stdscr, configPanel.TOR_STATE, config)
+  panels["torrc"] = torrcPanel.TorrcPanel(stdscr, torrcPanel.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")
@@ -448,7 +620,7 @@
   
   # provides notice about any unused config keys
   for key in config.getUnusedKeys():
-    log.log(CONFIG["log.configEntryUndefined"], "unrecognized configuration entry: %s" % key)
+    log.log(CONFIG["log.configEntryUndefined"], "unused configuration entry: %s" % key)
   
   lastPerformanceLog = 0 # ensures we don't do performance logging too frequently
   redrawStartTime = time.time()
@@ -456,6 +628,9 @@
   # TODO: popups need to force the panels it covers to redraw (or better, have
   # a global refresh function for after changing pages, popups, etc)
   
+  initTime = time.time() - startTime
+  log.log(CONFIG["log.startTime"], "arm started (initialization took %0.3f seconds)" % initTime)
+  
   # TODO: come up with a nice, clean method for other threads to immediately
   # terminate the draw loop and provide a stacktrace
   while True:
@@ -480,7 +655,8 @@
         if panels["graph"].currentDisplay == "bandwidth":
           panels["graph"].setHeight(panels["graph"].stats["bandwidth"].getContentHeight())
         
-        panels["torrc"].reset()
+        # TODO: should redraw the torrcPanel
+        #panels["torrc"].loadConfig()
         sighupTracker.isReset = False
       
       # gives panels a chance to take advantage of the maximum bounds
@@ -531,8 +707,8 @@
       for panelKey in (PAGE_S + PAGES[page]):
         # redrawing popup can result in display flicker when it should be hidden
         if panelKey != "popup":
-          if panelKey in ("header", "graph", "log"):
-            # revised panel (handles its own content refreshing)
+          if panelKey in ("header", "graph", "log", "config", "torrc"):
+            # revised panel (manages its own content refreshing)
             panels[panelKey].redraw()
           else:
             panels[panelKey].redraw(True)
@@ -630,6 +806,35 @@
         panel.CURSES_LOCK.release()
       
       selectiveRefresh(panels, page)
+    elif key == ord('x') or key == ord('X'):
+      # provides prompt to confirm that arm should issue a sighup
+      panel.CURSES_LOCK.acquire()
+      try:
+        setPauseState(panels, isPaused, page, True)
+        
+        # provides prompt
+        panels["control"].setMsg("This will reset Tor's internal state. Are you sure (x again to confirm)?", curses.A_BOLD)
+        panels["control"].redraw(True)
+        
+        curses.cbreak()
+        confirmationKey = stdscr.getch()
+        if confirmationKey in (ord('x'), ord('X')):
+          try:
+            torTools.getConn().reload()
+          except IOError, exc:
+            log.log(log.ERR, "Error detected when reloading tor: %s" % sysTools.getFileErrorMsg(exc))
+            
+            #errorMsg = " (%s)" % str(err) if str(err) else ""
+            #panels["control"].setMsg("Sighup failed%s" % errorMsg, curses.A_STANDOUT)
+            #panels["control"].redraw(True)
+            #time.sleep(2)
+        
+        # reverts display settings
+        curses.halfdelay(REFRESH_RATE * 10)
+        panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+        setPauseState(panels, isPaused, page)
+      finally:
+        panel.CURSES_LOCK.release()
     elif key == ord('h') or key == ord('H'):
       # displays popup for current page's controls
       panel.CURSES_LOCK.acquire()
@@ -662,7 +867,7 @@
           
           hiddenEntryLabel = "visible" if panels["log"].showDuplicates else "hidden"
           popup.addfstr(6, 2, "<b>u</b>: duplicate log entries (<b>%s</b>)" % hiddenEntryLabel)
-          popup.addfstr(6, 41, "<b>x</b>: clear event log")
+          popup.addfstr(6, 41, "<b>c</b>: clear event log")
           popup.addfstr(7, 41, "<b>a</b>: save snapshot of the log")
           
           pageOverrideKeys = (ord('m'), ord('n'), ord('s'), ord('i'), ord('d'), ord('e'), ord('r'), ord('f'), ord('x'))
@@ -697,6 +902,15 @@
           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>: edit configuration option")
+          popup.addfstr(3, 41, "<b>w</b>: save current configuration")
+          popup.addfstr(4, 2, "<b>s</b>: sort ordering")
+        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")
+          popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
+          popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
+          
           strippingLabel = "on" if panels["torrc"].stripComments else "off"
           popup.addfstr(3, 2, "<b>s</b>: comment stripping (<b>%s</b>)" % strippingLabel)
           
@@ -812,28 +1026,17 @@
         panels["control"].setMsg("Path to save log snapshot: ")
         panels["control"].redraw(True)
         
-        # makes cursor and typing visible
-        try: curses.curs_set(1)
-        except curses.error: pass
-        curses.echo()
-        
         # gets user input (this blocks monitor updates)
-        pathInput = panels["control"].win.getstr(0, 27)
+        pathInput = panels["control"].getstr(0, 27)
         
-        # reverts visability settings
-        try: curses.curs_set(0)
-        except curses.error: pass
-        curses.noecho()
-        curses.halfdelay(REFRESH_RATE * 10) # evidenlty previous tweaks reset this...
-        
-        if pathInput != "":
+        if pathInput:
           try:
             panels["log"].saveSnapshot(pathInput)
             panels["control"].setMsg("Saved: %s" % pathInput, curses.A_STANDOUT)
             panels["control"].redraw(True)
             time.sleep(2)
           except IOError, exc:
-            panels["control"].setMsg("Unable to save snapshot: %s" % str(exc), curses.A_STANDOUT)
+            panels["control"].setMsg("Unable to save snapshot: %s" % sysTools.getFileErrorMsg(exc), curses.A_STANDOUT)
             panels["control"].redraw(True)
             time.sleep(2)
         
@@ -853,11 +1056,6 @@
         panels["control"].setMsg("Events to log: ")
         panels["control"].redraw(True)
         
-        # makes cursor and typing visible
-        try: curses.curs_set(1)
-        except curses.error: pass
-        curses.echo()
-        
         # lists event types
         popup = panels["popup"]
         popup.height = 11
@@ -874,17 +1072,11 @@
         popup.refresh()
         
         # gets user input (this blocks monitor updates)
-        eventsInput = panels["control"].win.getstr(0, 15)
-        eventsInput = eventsInput.replace(' ', '') # strips spaces
+        eventsInput = panels["control"].getstr(0, 15)
+        if eventsInput: eventsInput = eventsInput.replace(' ', '') # strips spaces
         
-        # reverts visability settings
-        try: curses.curs_set(0)
-        except curses.error: pass
-        curses.noecho()
-        curses.halfdelay(REFRESH_RATE * 10) # evidenlty previous tweaks reset this...
-        
         # it would be nice to quit on esc, but looks like this might not be possible...
-        if eventsInput != "":
+        if eventsInput:
           try:
             expandedEvents = logPanel.expandEvents(eventsInput)
             loggedEvents = setEventListening(expandedEvents, isBlindMode)
@@ -929,21 +1121,10 @@
           panels["control"].setMsg("Regular expression: ")
           panels["control"].redraw(True)
           
-          # makes cursor and typing visible
-          try: curses.curs_set(1)
-          except curses.error: pass
-          curses.echo()
-          
           # gets user input (this blocks monitor updates)
-          regexInput = panels["control"].win.getstr(0, 20)
+          regexInput = panels["control"].getstr(0, 20)
           
-          # reverts visability settings
-          try: curses.curs_set(0)
-          except curses.error: pass
-          curses.noecho()
-          curses.halfdelay(REFRESH_RATE * 10)
-          
-          if regexInput != "":
+          if regexInput:
             try:
               panels["log"].setFilter(re.compile(regexInput))
               if regexInput in regexFilters: regexFilters.remove(regexInput)
@@ -988,19 +1169,19 @@
         
         if currentHeight < maxHeight + 1:
           panels["graph"].setGraphHeight(panels["graph"].graphHeight + 1)
-    elif page == 0 and (key == ord('x') or key == ord('X')):
+    elif page == 0 and (key == ord('c') or key == ord('C')):
       # provides prompt to confirm that arm should clear the log
       panel.CURSES_LOCK.acquire()
       try:
         setPauseState(panels, isPaused, page, True)
         
         # provides prompt
-        panels["control"].setMsg("This will clear the log. Are you sure (x again to confirm)?", curses.A_BOLD)
+        panels["control"].setMsg("This will clear the log. Are you sure (c again to confirm)?", curses.A_BOLD)
         panels["control"].redraw(True)
         
         curses.cbreak()
         confirmationKey = stdscr.getch()
-        if confirmationKey in (ord('x'), ord('X')): panels["log"].clear()
+        if confirmationKey in (ord('c'), ord('C')): panels["log"].clear()
         
         # reverts display settings
         curses.halfdelay(REFRESH_RATE * 10)
@@ -1242,71 +1423,20 @@
         connections.getResolver("tor").overwriteResolver = optionTypes[selection]
     elif page == 1 and (key == ord('s') or key == ord('S')):
       # set ordering for connection listing
-      panel.CURSES_LOCK.acquire()
-      try:
-        setPauseState(panels, isPaused, page, True)
-        curses.cbreak() # wait indefinitely for key presses (no timeout)
-        
-        # lists event types
-        popup = panels["popup"]
-        selections = []     # new ordering
-        cursorLoc = 0       # index of highlighted option
-        
-        # listing of inital ordering
-        prevOrdering = "<b>Current Order: "
-        for sort in panels["conn"].sortOrdering: prevOrdering += connPanel.getSortLabel(sort, True) + ", "
-        prevOrdering = prevOrdering[:-2] + "</b>"
-        
-        # Makes listing of all options
-        options = []
-        for (type, label, func) in connPanel.SORT_TYPES: options.append(connPanel.getSortLabel(type))
-        options.append("Cancel")
-        
-        while len(selections) < 3:
-          popup.clear()
-          popup.win.box()
-          popup.addstr(0, 0, "Connection Ordering:", curses.A_STANDOUT)
-          popup.addfstr(1, 2, prevOrdering)
-          
-          # provides new ordering
-          newOrdering = "<b>New Order: "
-          if selections:
-            for sort in selections: newOrdering += connPanel.getSortLabel(sort, True) + ", "
-            newOrdering = newOrdering[:-2] + "</b>"
-          else: newOrdering += "</b>"
-          popup.addfstr(2, 2, newOrdering)
-          
-          row, col, index = 4, 0, 0
-          for option in options:
-            popup.addstr(row, col * 19 + 2, option, curses.A_STANDOUT if cursorLoc == index else curses.A_NORMAL)
-            col += 1
-            index += 1
-            if col == 4: row, col = row + 1, 0
-          
-          popup.refresh()
-          
-          key = stdscr.getch()
-          if key == curses.KEY_LEFT: cursorLoc = max(0, cursorLoc - 1)
-          elif key == curses.KEY_RIGHT: cursorLoc = min(len(options) - 1, cursorLoc + 1)
-          elif key == curses.KEY_UP: cursorLoc = max(0, cursorLoc - 4)
-          elif key == curses.KEY_DOWN: cursorLoc = min(len(options) - 1, cursorLoc + 4)
-          elif key in (curses.KEY_ENTER, 10, ord(' ')):
-            # selected entry (the ord of '10' seems needed to pick up enter)
-            selection = options[cursorLoc]
-            if selection == "Cancel": break
-            else:
-              selections.append(connPanel.getSortType(selection.replace("Tor ID", "Fingerprint")))
-              options.remove(selection)
-              cursorLoc = min(cursorLoc, len(options) - 1)
-          elif key == 27: break # esc - cancel
-          
-        if len(selections) == 3:
-          panels["conn"].sortOrdering = selections
-          panels["conn"].sortConnections()
-        setPauseState(panels, isPaused, page)
-        curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
-      finally:
-        panel.CURSES_LOCK.release()
+      titleLabel = "Connection Ordering:"
+      options = [connPanel.getSortLabel(i) for i in range(9)]
+      oldSelection = [connPanel.getSortLabel(entry) for entry in panels["conn"].sortOrdering]
+      optionColors = dict([connPanel.getSortLabel(i, True) for i in range(9)])
+      results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
+      
+      if results:
+        # converts labels back to enums
+        resultEnums = [connPanel.getSortType(entry) for entry in results]
+        panels["conn"].sortOrdering = resultEnums
+        panels["conn"].sortConnections()
+      
+      # TODO: not necessary until the connection panel rewrite
+      #panels["conn"].redraw(True)
     elif page == 1 and (key == ord('c') or key == ord('C')):
       # displays popup with client circuits
       clientCircuits = None
@@ -1357,56 +1487,273 @@
         setPauseState(panels, isPaused, page)
       finally:
         panel.CURSES_LOCK.release()
-    elif page == 2 and key == ord('r') or key == ord('R'):
-      # reloads torrc, providing a notice if successful or not
-      isSuccessful = panels["torrc"].reset(False)
-      resetMsg = "torrc reloaded" if isSuccessful else "failed to reload torrc"
-      if isSuccessful: panels["torrc"].redraw(True)
+    elif page == 2 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
+      #options = [confPanel.CONFIG_LABELS[confType] for confType in range(4)]
+      options = []
+      initialSelection = panels["torrc"].configType
       
-      panels["control"].setMsg(resetMsg, curses.A_STANDOUT)
-      panels["control"].redraw(True)
-      time.sleep(1)
+      # hides top label of the graph panel and pauses panels
+      panels["torrc"].showLabel = False
+      panels["torrc"].redraw(True)
+      setPauseState(panels, isPaused, page, True)
       
-      panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-    elif page == 2 and (key == ord('x') or key == ord('X')):
-      # provides prompt to confirm that arm should issue a sighup
+      selection = showMenu(stdscr, panels["popup"], "Configuration:", options, initialSelection)
+      
+      # reverts changes made for popup
+      panels["torrc"].showLabel = True
+      setPauseState(panels, isPaused, page)
+      
+      # applies new setting
+      if selection != -1: panels["torrc"].setConfigType(selection)
+      
+      selectiveRefresh(panels, page)
+    elif page == 2 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")
+        
+        # lists event types
+        popup = panels["popup"]
+        popup.height = len(configLines) + 3
+        popup.recreate(stdscr)
+        displayHeight, displayWidth = panels["popup"].getPreferredSize()
+        
+        # displayed options (truncating the labels if there's limited room)
+        if displayWidth >= 30: selectionOptions = ("Save", "Save As...", "Cancel")
+        else: selectionOptions = ("Save", "Save As", "X")
+        
+        # checks if we can show options beside the last line of visible content
+        lastIndex = min(displayHeight - 3, len(configLines) - 1)
+        isOptionLineSeparate = displayWidth < (30 + len(configLines[lastIndex]))
+        
+        # if we're showing all the content and have room to display selection
+        # options besides the text then shrink the popup by a row
+        if not isOptionLineSeparate and displayHeight == len(configLines) + 3:
+          popup.height -= 1
+          popup.recreate(stdscr)
+        
+        key, selection = 0, 2
+        while key not in (curses.KEY_ENTER, 10, ord(' ')):
+          # if the popup has been resized then recreate it (needed for the
+          # proper border height)
+          newHeight, newWidth = panels["popup"].getPreferredSize()
+          if (displayHeight, displayWidth) != (newHeight, newWidth):
+            displayHeight, displayWidth = newHeight, newWidth
+            popup.recreate(stdscr)
+          
+          # if there isn't room to display the popup then cancel it
+          if displayHeight <= 2:
+            selection = 2
+            break
+          
+          popup.clear()
+          popup.win.box()
+          popup.addstr(0, 0, "Configuration being saved:", curses.A_STANDOUT)
+          
+          visibleConfigLines = displayHeight - 3 if isOptionLineSeparate else displayHeight - 2
+          for i in range(visibleConfigLines):
+            line = uiTools.cropStr(configLines[i], displayWidth - 2)
+            
+            if " " in line:
+              option, arg = line.split(" ", 1)
+              popup.addstr(i + 1, 1, option, curses.A_BOLD | uiTools.getColor("green"))
+              popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD | uiTools.getColor("cyan"))
+            else:
+              popup.addstr(i + 1, 1, line, curses.A_BOLD | uiTools.getColor("green"))
+          
+          # draws 'T' between the lower left and the covered panel's scroll bar
+          if displayWidth > 1: popup.win.addch(displayHeight - 1, 1, curses.ACS_TTEE)
+          
+          # draws selection options (drawn right to left)
+          drawX = displayWidth - 1
+          for i in range(len(selectionOptions) - 1, -1, -1):
+            optionLabel = selectionOptions[i]
+            drawX -= (len(optionLabel) + 2)
+            
+            # if we've run out of room then drop the option (this will only
+            # occure on tiny displays)
+            if drawX < 1: break
+            
+            selectionFormat = curses.A_STANDOUT if i == selection else curses.A_NORMAL
+            popup.addstr(displayHeight - 2, drawX, "[")
+            popup.addstr(displayHeight - 2, drawX + 1, optionLabel, selectionFormat | curses.A_BOLD)
+            popup.addstr(displayHeight - 2, drawX + len(optionLabel) + 1, "]")
+            
+            drawX -= 1 # space gap between the options
+          
+          popup.refresh()
+          
+          key = stdscr.getch()
+          if key == curses.KEY_LEFT: selection = max(0, selection - 1)
+          elif key == curses.KEY_RIGHT: selection = min(len(selectionOptions) - 1, selection + 1)
+        
+        if selection in (0, 1):
+          loadedTorrc = torConfig.getTorrc()
+          try: configLocation = loadedTorrc.getConfigLocation()
+          except IOError: configLocation = ""
+          
+          if selection == 1:
+            # prompts user for a configuration location
+            promptMsg = "Save to (esc to cancel): "
+            panels["control"].setMsg(promptMsg)
+            panels["control"].redraw(True)
+            configLocation = panels["control"].getstr(0, len(promptMsg), configLocation)
+            if configLocation: configLocation = os.path.abspath(configLocation)
+          
+          if configLocation:
+            try:
+              # make dir if the path doesn't already exist
+              baseDir = os.path.dirname(configLocation)
+              if not os.path.exists(baseDir): os.makedirs(baseDir)
+              
+              # saves the configuration to the file
+              configFile = open(configLocation, "w")
+              configFile.write(configText)
+              configFile.close()
+              
+              # reloads the cached torrc if overwriting it
+              if configLocation == loadedTorrc.getConfigLocation():
+                try:
+                  loadedTorrc.load()
+                  panels["torrc"]._lastContentHeightArgs = None
+                except IOError: pass
+              
+              msg = "Saved configuration to %s" % configLocation
+            except (IOError, OSError), exc:
+              msg = "Unable to save configuration (%s)" % sysTools.getFileErrorMsg(exc)
+            
+            panels["control"].setMsg(msg, curses.A_STANDOUT)
+            panels["control"].redraw(True)
+            time.sleep(2)
+          
+          panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+        
+        # reverts popup dimensions
+        popup.height = 9
+        popup.recreate(stdscr, 80)
+      finally:
+        panel.CURSES_LOCK.release()
+      
+      panels["config"].redraw(True)
+    elif page == 2 and (key == ord('s') or key == ord('S')):
+      # set ordering for config options
+      titleLabel = "Config Option Ordering:"
+      options = [configPanel.FIELD_ATTR[i][0] for i in range(8)]
+      oldSelection = [configPanel.FIELD_ATTR[entry][0] for entry in panels["config"].sortOrdering]
+      optionColors = dict([configPanel.FIELD_ATTR[i] for i in range(8)])
+      results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
+      
+      if results:
+        # converts labels back to enums
+        resultEnums = []
+        
+        for label in results:
+          for entryEnum in configPanel.FIELD_ATTR:
+            if label == configPanel.FIELD_ATTR[entryEnum][0]:
+              resultEnums.append(entryEnum)
+              break
+        
+        panels["config"].setSortOrder(resultEnums)
+      
+      panels["config"].redraw(True)
+    elif page == 2 and key in (curses.KEY_ENTER, 10, ord(' ')):
+      # let the user edit the configuration value, unchanged if left blank
+      panel.CURSES_LOCK.acquire()
+      try:
         setPauseState(panels, isPaused, page, True)
         
         # provides prompt
-        panels["control"].setMsg("This will reset Tor's internal state. Are you sure (x again to confirm)?", curses.A_BOLD)
+        selection = panels["config"].getSelection()
+        configOption = selection.get(configPanel.FIELD_OPTION)
+        titleMsg = "%s Value (esc to cancel): " % configOption
+        panels["control"].setMsg(titleMsg)
         panels["control"].redraw(True)
         
-        curses.cbreak()
-        confirmationKey = stdscr.getch()
-        if confirmationKey in (ord('x'), ord('X')):
+        displayWidth = panels["control"].getPreferredSize()[1]
+        initialValue = selection.get(configPanel.FIELD_VALUE)
+        
+        # initial input for the text field
+        initialText = ""
+        if CONFIG["features.config.prepopulateEditValues"] and initialValue != "<none>":
+          initialText = initialValue
+        
+        newConfigValue = panels["control"].getstr(0, len(titleMsg), initialText)
+        
+        # it would be nice to quit on esc, but looks like this might not be possible...
+        if newConfigValue != None and newConfigValue != initialValue:
+          conn = torTools.getConn()
+          
+          # if the value's a boolean then allow for 'true' and 'false' inputs
+          if selection.get(configPanel.FIELD_TYPE) == "Boolean":
+            if newConfigValue.lower() == "true": newConfigValue = "1"
+            elif newConfigValue.lower() == "false": newConfigValue = "0"
+          
           try:
-            torTools.getConn().reload()
-          except IOError, exc:
-            log.log(log.ERR, "Error detected when reloading tor: %s" % str(exc))
+            if selection.get(configPanel.FIELD_TYPE) == "LineList":
+              newConfigValue = newConfigValue.split(",")
             
-            #errorMsg = " (%s)" % str(err) if str(err) else ""
-            #panels["control"].setMsg("Sighup failed%s" % errorMsg, curses.A_STANDOUT)
-            #panels["control"].redraw(True)
-            #time.sleep(2)
+            conn.setOption(configOption, newConfigValue)
+            
+            # resets the isDefault flag
+            customOptions = torConfig.getCustomOptions()
+            selection.fields[configPanel.FIELD_IS_DEFAULT] = not configOption in customOptions
+            
+            panels["config"].redraw(True)
+          except Exception, exc:
+            errorMsg = "%s (press any key)" % exc
+            panels["control"].setMsg(uiTools.cropStr(errorMsg, displayWidth), curses.A_STANDOUT)
+            panels["control"].redraw(True)
+            
+            curses.cbreak() # wait indefinitely for key presses (no timeout)
+            stdscr.getch()
+            curses.halfdelay(REFRESH_RATE * 10)
         
-        # reverts display settings
-        curses.halfdelay(REFRESH_RATE * 10)
         panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
         setPauseState(panels, isPaused, page)
       finally:
         panel.CURSES_LOCK.release()
+    elif page == 3 and key == ord('r') or key == ord('R'):
+      # reloads torrc, providing a notice if successful or not
+      loadedTorrc = torConfig.getTorrc()
+      loadedTorrc.getLock().acquire()
+      
+      try:
+        loadedTorrc.load()
+        isSuccessful = True
+      except IOError:
+        isSuccessful = False
+      
+      loadedTorrc.getLock().release()
+      
+      #isSuccessful = panels["torrc"].loadConfig(logErrors = False)
+      #confTypeLabel = confPanel.CONFIG_LABELS[panels["torrc"].configType]
+      resetMsg = "torrc reloaded" if isSuccessful else "failed to reload torrc"
+      if isSuccessful:
+        panels["torrc"]._lastContentHeightArgs = None
+        panels["torrc"].redraw(True)
+      
+      panels["control"].setMsg(resetMsg, curses.A_STANDOUT)
+      panels["control"].redraw(True)
+      time.sleep(1)
+      
+      panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
     elif page == 0:
       panels["log"].handleKey(key)
     elif page == 1:
       panels["conn"].handleKey(key)
     elif page == 2:
+      panels["config"].handleKey(key)
+    elif page == 3:
       panels["torrc"].handleKey(key)
 
-def startTorMonitor(loggedEvents, isBlindMode):
+def startTorMonitor(startTime, loggedEvents, isBlindMode):
   try:
-    curses.wrapper(drawTorMonitor, loggedEvents, isBlindMode)
+    curses.wrapper(drawTorMonitor, startTime, loggedEvents, isBlindMode)
   except KeyboardInterrupt:
     pass # skip printing stack trace in case of keyboard interrupt
 

Modified: arm/release/src/interface/graphing/bandwidthStats.py
===================================================================
--- arm/release/src/interface/graphing/bandwidthStats.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/interface/graphing/bandwidthStats.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -37,8 +37,7 @@
     
     self._config = dict(DEFAULT_CONFIG)
     if config:
-      config.update(self._config)
-      self._config["features.graph.bw.accounting.rate"] = max(1, self._config["features.graph.bw.accounting.rate"])
+      config.update(self._config, {"features.graph.bw.accounting.rate": 1})
     
     # accounting data (set by _updateAccountingInfo method)
     self.accountingLastUpdated = 0

Modified: arm/release/src/interface/graphing/graphPanel.py
===================================================================
--- arm/release/src/interface/graphing/graphPanel.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/interface/graphing/graphPanel.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -48,11 +48,11 @@
           "features.graph.showIntermediateBounds": True}
 
 def loadConfig(config):
-  config.update(CONFIG)
-  CONFIG["features.graph.height"] = max(MIN_GRAPH_HEIGHT, CONFIG["features.graph.height"])
-  CONFIG["features.graph.maxWidth"] = max(1, CONFIG["features.graph.maxWidth"])
-  CONFIG["features.graph.interval"] = min(len(UPDATE_INTERVALS) - 1, max(0, CONFIG["features.graph.interval"]))
-  CONFIG["features.graph.bound"] = min(2, max(0, CONFIG["features.graph.bound"]))
+  config.update(CONFIG, {
+    "features.graph.height": MIN_GRAPH_HEIGHT,
+    "features.graph.maxWidth": 1,
+    "features.graph.interval": (0, len(UPDATE_INTERVALS) - 1),
+    "features.graph.bound": (0, 2)})
 
 class GraphStats(TorCtl.PostEventListener):
   """

Modified: arm/release/src/interface/graphing/psStats.py
===================================================================
--- arm/release/src/interface/graphing/psStats.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/interface/graphing/psStats.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -50,7 +50,7 @@
   def getRefreshRate(self):
     # provides the rate at which the panel has new stats to display
     if self._config["features.graph.ps.cachedOnly"]:
-      return int(conf.getConfig("arm").get("queries.ps.rate"))
+      return int(conf.getConfig("arm").get("queries.ps.rate", 5))
     else: return 1
   
   def getHeaderLabel(self, width, isPrimary):

Modified: arm/release/src/interface/headerPanel.py
===================================================================
--- arm/release/src/interface/headerPanel.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/interface/headerPanel.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -62,8 +62,7 @@
     self._config = dict(DEFAULT_CONFIG)
     
     if config:
-      config.update(self._config)
-      self._config["queries.ps.rate"] = max(self._config["queries.ps.rate"], 1)
+      config.update(self._config, {"queries.ps.rate": 1})
     
     self.vals = {}
     self.valsLock = threading.RLock()

Modified: arm/release/src/interface/logPanel.py
===================================================================
--- arm/release/src/interface/logPanel.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/interface/logPanel.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -8,7 +8,6 @@
 import os
 import curses
 import threading
-from curses.ascii import isprint
 
 from TorCtl import TorCtl
 
@@ -73,6 +72,9 @@
 CACHED_DUPLICATES_ARGUMENTS = None # events
 CACHED_DUPLICATES_RESULT = None
 
+# duration we'll wait for the deduplication function before giving up (in ms)
+DEDUPLICATION_TIMEOUT = 100
+
 def daysSince(timestamp=None):
   """
   Provides the number of days since the epoch converted to local time (rounded
@@ -168,10 +170,10 @@
   for confKey in armConf.getKeys():
     if confKey.startswith("msg."):
       eventType = confKey[4:].upper()
-      messages = armConf.get(confKey)
+      messages = armConf.get(confKey, [])
       COMMON_LOG_MESSAGES[eventType] = messages
 
-def getLogFileEntries(runlevels, readLimit = None, addLimit = None):
+def getLogFileEntries(runlevels, readLimit = None, addLimit = None, config = None):
   """
   Parses tor's log file for past events matching the given runlevels, providing
   a list of log entries (ordered newest to oldest). Limiting the number of read
@@ -182,11 +184,15 @@
     runlevels - event types (DEBUG - ERR) to be returned
     readLimit - max lines of the log file that'll be read (unlimited if None)
     addLimit  - maximum entries to provide back (unlimited if None)
+    config    - configuration parameters related to this panel, uses defaults
+                if left as None
   """
   
   startTime = time.time()
   if not runlevels: return []
   
+  if not config: config = DEFAULT_CONFIG
+  
   # checks tor's configuration for the log file's location (if any exists)
   loggingTypes, loggingLocation = None, None
   for loggingEntry in torTools.getConn().getOption("Log", [], True):
@@ -236,7 +242,7 @@
       logFile.close()
   except IOError:
     msg = "Unable to read tor's log file: %s" % loggingLocation
-    log.log(DEFAULT_CONFIG["log.logPanel.prepopulateFailed"], msg)
+    log.log(config["log.logPanel.prepopulateFailed"], msg)
   
   if not lines: return []
   
@@ -278,7 +284,7 @@
   
   if addLimit: loggedEvents = loggedEvents[:addLimit]
   msg = "Read %i entries from tor's log file: %s (read limit: %i, runtime: %0.3f)" % (len(loggedEvents), loggingLocation, readLimit, time.time() - startTime)
-  log.log(DEFAULT_CONFIG["log.logPanel.prepopulateSuccess"], msg)
+  log.log(config["log.logPanel.prepopulateSuccess"], msg)
   return loggedEvents
 
 def getDaybreaks(events, ignoreTimeForCache = False):
@@ -323,7 +329,8 @@
   """
   Deduplicates a list of log entries, providing back a tuple listing with the
   log entry and count of duplicates following it. Entries in different days are
-  not considered to be duplicates.
+  not considered to be duplicates. This times out, returning None if it takes
+  longer than DEDUPLICATION_TIMEOUT.
   
   Arguments:
     events - chronologically ordered listing of events
@@ -336,35 +343,17 @@
   # loads common log entries from the config if they haven't been
   if COMMON_LOG_MESSAGES == None: loadLogMessages()
   
+  startTime = time.time()
   eventsRemaining = list(events)
   returnEvents = []
   
   while eventsRemaining:
     entry = eventsRemaining.pop(0)
-    duplicateIndices = []
+    duplicateIndices = isDuplicate(entry, eventsRemaining, True)
     
-    for i in range(len(eventsRemaining)):
-      forwardEntry = eventsRemaining[i]
-      
-      # if showing dates then do duplicate detection for each day, rather
-      # than globally
-      if forwardEntry.type == DAYBREAK_EVENT: break
-      
-      if entry.type == forwardEntry.type:
-        isDuplicate = False
-        if entry.msg == forwardEntry.msg: isDuplicate = True
-        elif entry.type in COMMON_LOG_MESSAGES:
-          for commonMsg in COMMON_LOG_MESSAGES[entry.type]:
-            # if it starts with an asterisk then check the whole message rather
-            # than just the start
-            if commonMsg[0] == "*":
-              isDuplicate = commonMsg[1:] in entry.msg and commonMsg[1:] in forwardEntry.msg
-            else:
-              isDuplicate = entry.msg.startswith(commonMsg) and forwardEntry.msg.startswith(commonMsg)
-            
-            if isDuplicate: break
-        
-        if isDuplicate: duplicateIndices.append(i)
+    # checks if the call timeout has been reached
+    if (time.time() - startTime) > DEDUPLICATION_TIMEOUT / 1000.0:
+      return None
     
     # drops duplicate entries
     duplicateIndices.reverse()
@@ -377,6 +366,48 @@
   
   return returnEvents
 
+def isDuplicate(event, eventSet, getDuplicates = False):
+  """
+  True if the event is a duplicate for something in the eventSet, false
+  otherwise. If the getDuplicates flag is set this provides the indices of
+  the duplicates instead.
+  
+  Arguments:
+    event         - event to search for duplicates of
+    eventSet      - set to look for the event in
+    getDuplicates - instead of providing back a boolean this gives a list of
+                    the duplicate indices in the eventSet
+  """
+  
+  duplicateIndices = []
+  for i in range(len(eventSet)):
+    forwardEntry = eventSet[i]
+    
+    # if showing dates then do duplicate detection for each day, rather
+    # than globally
+    if forwardEntry.type == DAYBREAK_EVENT: break
+    
+    if event.type == forwardEntry.type:
+      isDuplicate = False
+      if event.msg == forwardEntry.msg: isDuplicate = True
+      elif event.type in COMMON_LOG_MESSAGES:
+        for commonMsg in COMMON_LOG_MESSAGES[event.type]:
+          # if it starts with an asterisk then check the whole message rather
+          # than just the start
+          if commonMsg[0] == "*":
+            isDuplicate = commonMsg[1:] in event.msg and commonMsg[1:] in forwardEntry.msg
+          else:
+            isDuplicate = event.msg.startswith(commonMsg) and forwardEntry.msg.startswith(commonMsg)
+          
+          if isDuplicate: break
+      
+      if isDuplicate:
+        if getDuplicates: duplicateIndices.append(i)
+        else: return True
+  
+  if getDuplicates: return duplicateIndices
+  else: return False
+
 class LogEntry():
   """
   Individual log file entry, having the following attributes:
@@ -496,17 +527,16 @@
   def __init__(self, stdscr, loggedEvents, config=None):
     panel.Panel.__init__(self, stdscr, "log", 0)
     threading.Thread.__init__(self)
+    self.setDaemon(True)
     
     self._config = dict(DEFAULT_CONFIG)
     
     if config:
-      config.update(self._config)
-      
-      # ensures prepopulation and cache sizes are sane
-      self._config["features.log.maxLinesPerEntry"] = max(self._config["features.log.maxLinesPerEntry"], 1)
-      self._config["features.log.prepopulateReadLimit"] = max(self._config["features.log.prepopulateReadLimit"], 0)
-      self._config["features.log.maxRefreshRate"] = max(self._config["features.log.maxRefreshRate"], 10)
-      self._config["cache.logPanel.size"] = max(self._config["cache.logPanel.size"], 50)
+      config.update(self._config, {
+        "features.log.maxLinesPerEntry": 1,
+        "features.log.prepopulateReadLimit": 0,
+        "features.log.maxRefreshRate": 10,
+        "cache.logPanel.size": 1000})
     
     # collapses duplicate log entries if false, showing only the most recent
     self.showDuplicates = self._config["features.log.showDuplicateEntries"]
@@ -543,7 +573,7 @@
       setRunlevels = list(set.intersection(set(self.loggedEvents), set(RUNLEVELS)))
       readLimit = self._config["features.log.prepopulateReadLimit"]
       addLimit = self._config["cache.logPanel.size"]
-      torEventBacklog = getLogFileEntries(setRunlevels, readLimit, addLimit)
+      torEventBacklog = getLogFileEntries(setRunlevels, readLimit, addLimit, self._config)
     
     # adds arm listener and fetches past events
     log.LOG_LOCK.acquire()
@@ -561,7 +591,7 @@
       for level, msg, eventTime in log._getEntries(setRunlevels):
         runlevelStr = log.RUNLEVEL_STR[level]
         armEventEntry = LogEntry(eventTime, "ARM_" + runlevelStr, msg, RUNLEVEL_EVENT_COLOR[runlevelStr])
-        armEventBacklog.append(armEventEntry)
+        armEventBacklog.insert(0, armEventEntry)
       
       # joins armEventBacklog and torEventBacklog chronologically into msgLog
       while armEventBacklog or torEventBacklog:
@@ -599,7 +629,7 @@
         self.logFile = open(logPath, "a")
         log.log(self._config["log.logPanel.logFileOpened"], "arm %s opening log file (%s)" % (VERSION, logPath))
       except IOError, exc:
-        log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % exc)
+        log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
         self.logFile = None
   
   def registerEvent(self, event):
@@ -613,7 +643,7 @@
     if not event.type in self.loggedEvents: return
     
     # strips control characters to avoid screwing up the terminal
-    event.msg = "".join([char for char in event.msg if (isprint(char) or char == "\n")])
+    event.msg = uiTools.getPrintable(event.msg)
     
     # note event in the log file if we're saving them
     if self.logFile:
@@ -621,10 +651,9 @@
         self.logFile.write(event.getDisplayMessage(True) + "\n")
         self.logFile.flush()
       except IOError, exc:
-        log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % exc)
+        log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
         self.logFile = None
     
-    cacheSize = self._config["cache.logPanel.size"]
     if self._isPaused:
       self.valsLock.acquire()
       self._pauseBuffer.insert(0, event)
@@ -778,7 +807,14 @@
     
     isDatesShown = self.regexFilter == None and self._config["features.log.showDateDividers"]
     eventLog = getDaybreaks(self.msgLog, self._isPaused) if isDatesShown else list(self.msgLog)
-    if not self.showDuplicates: deduplicatedLog = getDuplicates(eventLog)
+    if not self.showDuplicates:
+      deduplicatedLog = getDuplicates(eventLog)
+      
+      if deduplicatedLog == None:
+        msg = "Deduplication took too long. Its current implementation has difficulty handling large logs so disabling it to keep the interface responsive."
+        log.log(log.WARN, msg)
+        self.showDuplicates = True
+        deduplicatedLog = [(entry, 0) for entry in eventLog]
     else: deduplicatedLog = [(entry, 0) for entry in eventLog]
     
     # determines if we have the minimum width to show date dividers
@@ -838,7 +874,7 @@
           if lineOffset == maxEntriesPerLine: break
           
           maxMsgSize = width - cursorLoc
-          if len(msg) >= maxMsgSize:
+          if len(msg) > maxMsgSize:
             # message is too long - break it up
             if lineOffset == maxEntriesPerLine - 1:
               msg = uiTools.cropStr(msg, maxMsgSize)
@@ -1047,6 +1083,8 @@
     - grown beyond the cache limit
     - outlived the configured log duration
     
+    Argument:
+      eventListing - listing of log entries
     """
     
     cacheSize = self._config["cache.logPanel.size"]

Copied: arm/release/src/interface/torrcPanel.py (from rev 23872, arm/trunk/src/interface/torrcPanel.py)
===================================================================
--- arm/release/src/interface/torrcPanel.py	                        (rev 0)
+++ arm/release/src/interface/torrcPanel.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -0,0 +1,221 @@
+"""
+Panel displaying the torrc or armrc with the validation done against it.
+"""
+
+import math
+import curses
+import threading
+
+from util import conf, 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
+
+class TorrcPanel(panel.Panel):
+  """
+  Renders the current torrc or armrc with syntax highlighting in a scrollable
+  area.
+  """
+  
+  def __init__(self, stdscr, configType, config=None):
+    panel.Panel.__init__(self, stdscr, "configFile", 0)
+    
+    self._config = dict(DEFAULT_CONFIG)
+    if config:
+      config.update(self._config, {"features.config.file.maxLinesPerEntry": 1})
+    
+    self.valsLock = threading.RLock()
+    self.configType = configType
+    self.scroll = 0
+    self.showLabel = True       # shows top label (hides otherwise)
+    self.showLineNum = True     # shows left aligned line numbers
+    self.stripComments = False  # drops comments and extra whitespace
+    
+    # height of the content when last rendered (the cached value is invalid if
+    # _lastContentHeightArgs is None or differs from the current dimensions)
+    self._lastContentHeight = 1
+    self._lastContentHeightArgs = None
+  
+  def handleKey(self, key):
+    self.valsLock.acquire()
+    if uiTools.isScrollKey(key):
+      pageHeight = self.getPreferredSize()[0] - 1
+      newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self._lastContentHeight)
+      
+      if self.scroll != newScroll:
+        self.scroll = newScroll
+        self.redraw(True)
+    elif key == ord('n') or key == ord('N'):
+      self.showLineNum = not self.showLineNum
+      self._lastContentHeightArgs = None
+      self.redraw(True)
+    elif key == ord('s') or key == ord('S'):
+      self.stripComments = not self.stripComments
+      self._lastContentHeightArgs = None
+      self.redraw(True)
+    
+    self.valsLock.release()
+  
+  def draw(self, subwindow, width, height):
+    self.valsLock.acquire()
+    
+    # If true, we assume that the cached value in self._lastContentHeight is
+    # still accurate, and stop drawing when there's nothing more to display.
+    # Otherwise the self._lastContentHeight is suspect, and we'll process all
+    # the content to check if it's right (and redraw again with the corrected
+    # height if not).
+    trustLastContentHeight = self._lastContentHeightArgs == (width, height)
+    
+    # restricts scroll location to valid bounds
+    self.scroll = max(0, min(self.scroll, self._lastContentHeight - height + 1))
+    
+    renderedContents, corrections, confLocation = None, {}, None
+    if self.configType == TORRC:
+      loadedTorrc = torConfig.getTorrc()
+      loadedTorrc.getLock().acquire()
+      confLocation = loadedTorrc.getConfigLocation()
+      
+      if not loadedTorrc.isLoaded():
+        renderedContents = ["### Unable to load the torrc ###"]
+      else:
+        renderedContents = loadedTorrc.getDisplayContents(self.stripComments)
+        
+        # constructs a mapping of line numbers to the issue on it
+        corrections = dict((lineNum, (issue, msg)) for lineNum, issue, msg in loadedTorrc.getCorrections())
+      
+      loadedTorrc.getLock().release()
+    else:
+      loadedArmrc = conf.getConfig("arm")
+      confLocation = loadedArmrc.path
+      renderedContents = list(loadedArmrc.rawContents)
+    
+    # offset to make room for the line numbers
+    lineNumOffset = 0
+    if self.showLineNum:
+      if len(renderedContents) == 0: lineNumOffset = 2
+      else: lineNumOffset = int(math.log10(len(renderedContents))) + 2
+    
+    # draws left-hand scroll bar if content's longer than the height
+    scrollOffset = 0
+    if self._config["features.config.file.showScrollbars"] and self._lastContentHeight > height - 1:
+      scrollOffset = 3
+      self.addScrollBar(self.scroll, self.scroll + height - 1, self._lastContentHeight, 1)
+    
+    displayLine = -self.scroll + 1 # line we're drawing on
+    
+    # draws the top label
+    if self.showLabel:
+      sourceLabel = "Tor" if self.configType == TORRC else "Arm"
+      locationLabel = " (%s)" % confLocation if confLocation else ""
+      self.addstr(0, 0, "%s Configuration File%s:" % (sourceLabel, locationLabel), curses.A_STANDOUT)
+    
+    isMultiline = False # true if we're in the middle of a multiline torrc entry
+    for lineNumber in range(0, len(renderedContents)):
+      lineText = renderedContents[lineNumber]
+      lineText = lineText.rstrip() # remove ending whitespace
+      
+      # blank lines are hidden when stripping comments
+      if self.stripComments and not lineText: continue
+      
+      # splits the line into its component (msg, format) tuples
+      lineComp = {"option": ["", curses.A_BOLD | uiTools.getColor("green")],
+                  "argument": ["", curses.A_BOLD | uiTools.getColor("cyan")],
+                  "correction": ["", curses.A_BOLD | uiTools.getColor("cyan")],
+                  "comment": ["", uiTools.getColor("white")]}
+      
+      # parses the comment
+      commentIndex = lineText.find("#")
+      if commentIndex != -1:
+        lineComp["comment"][0] = lineText[commentIndex:]
+        lineText = lineText[:commentIndex]
+      
+      # splits the option and argument, preserving any whitespace around them
+      strippedLine = lineText.strip()
+      optionIndex = strippedLine.find(" ")
+      if isMultiline:
+        # part of a multiline entry started on a previous line so everything
+        # is part of the argument
+        lineComp["argument"][0] = lineText
+      elif optionIndex == -1:
+        # no argument provided
+        lineComp["option"][0] = lineText
+      else:
+        optionText = strippedLine[:optionIndex]
+        optionEnd = lineText.find(optionText) + len(optionText)
+        lineComp["option"][0] = lineText[:optionEnd]
+        lineComp["argument"][0] = lineText[optionEnd:]
+      
+      # flags following lines as belonging to this multiline entry if it ends
+      # with a slash
+      if strippedLine: isMultiline = strippedLine.endswith("\\")
+      
+      # gets the correction
+      if lineNumber in corrections:
+        lineIssue, lineIssueMsg = corrections[lineNumber]
+        
+        if lineIssue in (torConfig.VAL_DUPLICATE, torConfig.VAL_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:
+          lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("red")
+          lineComp["correction"][0] = " (%s)" % lineIssueMsg
+        else:
+          # For some types of configs the correction field is simply used to
+          # provide extra data (for instance, the type for tor state fields).
+          lineComp["correction"][0] = " (%s)" % lineIssueMsg
+          lineComp["correction"][1] = curses.A_BOLD | uiTools.getColor("magenta")
+      
+      # draws the line number
+      if self.showLineNum and displayLine < height and displayLine >= 1:
+        lineNumStr = ("%%%ii" % (lineNumOffset - 1)) % (lineNumber + 1)
+        self.addstr(displayLine, scrollOffset, lineNumStr, curses.A_BOLD | uiTools.getColor("yellow"))
+      
+      # draws the rest of the components with line wrap
+      cursorLoc, lineOffset = lineNumOffset + scrollOffset, 0
+      maxLinesPerEntry = self._config["features.config.file.maxLinesPerEntry"]
+      displayQueue = [lineComp[entry] for entry in ("option", "argument", "correction", "comment")]
+      
+      while displayQueue:
+        msg, format = displayQueue.pop(0)
+        
+        maxMsgSize, includeBreak = width - cursorLoc, False
+        if len(msg) >= maxMsgSize:
+          # message is too long - break it up
+          if lineOffset == maxLinesPerEntry - 1:
+            msg = uiTools.cropStr(msg, maxMsgSize)
+          else:
+            includeBreak = True
+            msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.END_WITH_HYPHEN, True)
+            displayQueue.insert(0, (remainder.strip(), format))
+        
+        drawLine = displayLine + lineOffset
+        if msg and drawLine < height and drawLine >= 1:
+          self.addstr(drawLine, cursorLoc, msg, format)
+        
+        # If we're done, and have added content to this line, then start
+        # further content on the next line.
+        cursorLoc += len(msg)
+        includeBreak |= not displayQueue and cursorLoc != lineNumOffset + scrollOffset
+        
+        if includeBreak:
+          lineOffset += 1
+          cursorLoc = lineNumOffset + scrollOffset
+      
+      displayLine += max(lineOffset, 1)
+      
+      if trustLastContentHeight and displayLine >= height: break
+    
+    if not trustLastContentHeight:
+      self._lastContentHeightArgs = (width, height)
+      newContentHeight = displayLine + self.scroll - 1
+      
+      if self._lastContentHeight != newContentHeight:
+        self._lastContentHeight = newContentHeight
+        self.redraw(True)
+    
+    self.valsLock.release()
+

Copied: arm/release/src/settings.cfg (from rev 23872, arm/trunk/src/settings.cfg)
===================================================================
--- arm/release/src/settings.cfg	                        (rev 0)
+++ arm/release/src/settings.cfg	2010-11-28 10:58:57 UTC (rev 23873)
@@ -0,0 +1,130 @@
+# 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
+# start of both messages then the entries are flagged as duplicates. If the
+# entry begins with an asterisk (*) then it checks if the substrings exist
+# anywhere in the messages.
+# 
+# Examples for the complete messages:
+# [BW] READ: 0, WRITTEN: 0
+# [DEBUG] connection_handle_write(): After TLS write of 512: 0 read, 586 written
+# [DEBUG] flush_chunk_tls(): flushed 512 bytes, 0 ready to flush, 0 remain.
+# [DEBUG] conn_read_callback(): socket 7 wants to read.
+# [DEBUG] conn_write_callback(): socket 51 wants to write.
+# [DEBUG] connection_remove(): removing socket -1 (type OR), n_conns now 50
+# [DEBUG] connection_or_process_cells_from_inbuf(): 7: starting, inbuf_datalen
+#         0 (0 pending in tls object).
+# [DEBUG] connection_read_to_buf(): 38: starting, inbuf_datalen 0 (0 pending in
+#         tls object). at_most 12800.
+# [DEBUG] connection_read_to_buf(): TLS connection closed on read. Closing.
+#         (Nickname moria1, address 128.31.0.34)
+# [INFO] run_connection_housekeeping(): Expiring non-open OR connection to fd
+#        16 (79.193.61.171:443).
+# [INFO] rep_hist_downrate_old_runs(): Discounting all old stability info by a
+#        factor of 0.950000
+# [NOTICE] We stalled too much while trying to write 150 bytes to address
+#          [scrubbed].  If this happens a lot, either something is wrong with
+#          your network connection, or something is wrong with theirs. (fd 238,
+#          type Directory, state 1, marked at main.c:702).
+# [NOTICE] I learned some more directory information, but not enough to build a
+#          circuit: We have only 469/2027 usable descriptors.
+# [NOTICE] Attempt by %s to open a stream from unknown relay. Closing.
+# [WARN] You specified a server "Amunet8" by name, but this name is not
+#        registered
+# [WARN] I have no descriptor for the router named "Amunet8" in my declared
+#        family; I'll use the nickname as is, but this   may confuse clients.
+# [WARN] Controller gave us config lines that didn't validate: Value
+#        'BandwidthRate  ' is malformed or out of bounds.
+# [WARN] Problem bootstrapping. Stuck at 80%: Connecting to the Tor network.
+#        (Network is unreachable; NOROUTE; count 47;    recommendation warn)
+# [WARN] 4 unknown, 1 missing key, 3 good, 0 bad, 1 no signature, 4 required
+# [ARM_DEBUG] refresh rate: 0.001 seconds
+# [ARM_DEBUG] system call: ps -p 2354 -o %cpu,rss,%mem,etime (runtime: 0.02)
+# [ARM_DEBUG] system call: netstat -npt | grep 2354/tor (runtime: 0.02)
+# [ARM_DEBUG] recreating panel 'graph' with the dimensions of 14/124
+# [ARM_DEBUG] redrawing the log panel with the corrected content height (estimat was off by 4)
+# [ARM_DEBUG] GETINFO accounting/bytes-left (runtime: 0.0006)
+# [ARM_DEBUG] GETCONF MyFamily (runtime: 0.0007)
+
+msg.BW READ:
+msg.DEBUG connection_handle_write(): After TLS write of
+msg.DEBUG flush_chunk_tls(): flushed
+msg.DEBUG conn_read_callback(): socket
+msg.DEBUG conn_write_callback(): socket
+msg.DEBUG connection_remove(): removing socket
+msg.DEBUG connection_or_process_cells_from_inbuf():
+msg.DEBUG *pending in tls object). at_most
+msg.DEBUG connection_read_to_buf(): TLS connection closed on read. Closing.
+msg.INFO run_connection_housekeeping(): Expiring
+msg.INFO rep_hist_downrate_old_runs(): Discounting all old stability info by a factor of
+msg.NOTICE We stalled too much while trying to write
+msg.NOTICE I learned some more directory information, but not enough to build a circuit
+msg.NOTICE Attempt by
+msg.WARN You specified a server
+msg.WARN I have no descriptor for the router named
+msg.WARN Controller gave us config lines that didn't validate
+msg.WARN Problem bootstrapping. Stuck at
+msg.WARN *missing key,
+msg.ARM_DEBUG refresh rate:
+msg.ARM_DEBUG system call: ps
+msg.ARM_DEBUG system call: netstat
+msg.ARM_DEBUG recreating panel '
+msg.ARM_DEBUG redrawing the log panel with the corrected content height (
+msg.ARM_DEBUG GETINFO accounting/bytes
+msg.ARM_DEBUG GETINFO accounting/bytes-left
+msg.ARM_DEBUG GETINFO accounting/interval-end
+msg.ARM_DEBUG GETINFO accounting/hibernating
+msg.ARM_DEBUG GETCONF
+
+# some config options are fetched via special values
+torrc.map HiddenServiceDir => HiddenServiceOptions
+torrc.map HiddenServicePort => HiddenServiceOptions
+torrc.map HiddenServiceVersion => HiddenServiceOptions
+torrc.map HiddenServiceAuthorizeClient => HiddenServiceOptions
+torrc.map HiddenServiceOptions => HiddenServiceOptions
+
+# valid torrc aliases from the _option_abbrevs struct of src/or/config.c
+# These couldn't be requested via GETCONF (in 0.2.1.19), but I think this has
+# been fixed. Discussion is in:
+# https://trac.torproject.org/projects/tor/ticket/1802
+# 
+# TODO: This workaround should be dropped after a few releases.
+torrc.alias l => Log
+torrc.alias AllowUnverifiedNodes => AllowInvalidNodes
+torrc.alias AutomapHostSuffixes => AutomapHostsSuffixes
+torrc.alias AutomapHostOnResolve => AutomapHostsOnResolve
+torrc.alias BandwidthRateBytes => BandwidthRate
+torrc.alias BandwidthBurstBytes => BandwidthBurst
+torrc.alias DirFetchPostPeriod => StatusFetchPeriod
+torrc.alias MaxConn => ConnLimit
+torrc.alias ORBindAddress => ORListenAddress
+torrc.alias DirBindAddress => DirListenAddress
+torrc.alias SocksBindAddress => SocksListenAddress
+torrc.alias UseHelperNodes => UseEntryGuards
+torrc.alias NumHelperNodes => NumEntryGuards
+torrc.alias UseEntryNodes => UseEntryGuards
+torrc.alias NumEntryNodes => NumEntryGuards
+torrc.alias ResolvConf => ServerDNSResolvConfFile
+torrc.alias SearchDomains => ServerDNSSearchDomains
+torrc.alias ServerDNSAllowBrokenResolvConf => ServerDNSAllowBrokenConfig
+torrc.alias PreferTunnelledDirConns => PreferTunneledDirConns
+torrc.alias BridgeAuthoritativeDirectory => BridgeAuthoritativeDir
+torrc.alias StrictEntryNodes => StrictNodes
+torrc.alias StrictExitNodes => StrictNodes
+
+# using the following entry is problematic, despite being among the
+# __option_abbrevs mappings
+#torrc.alias HashedControlPassword => __HashedControlSessionPassword
+
+# size and time modifiers allowed by config.c
+torrc.label.size.b b, byte, bytes
+torrc.label.size.kb kb, kbyte, kbytes, kilobyte, kilobytes
+torrc.label.size.mb m, mb, mbyte, mbytes, megabyte, megabytes
+torrc.label.size.gb gb, gbyte, gbytes, gigabyte, gigabytes
+torrc.label.size.tb tb, terabyte, terabytes
+torrc.label.time.sec second, seconds
+torrc.label.time.min minute, minutes
+torrc.label.time.hour hour, hours
+torrc.label.time.day day, days
+torrc.label.time.week week, weeks
+

Modified: arm/release/src/starter.py
===================================================================
--- arm/release/src/starter.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/starter.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -8,6 +8,7 @@
 
 import os
 import sys
+import time
 import getopt
 
 import version
@@ -19,17 +20,26 @@
 import util.log
 import util.panel
 import util.sysTools
+import util.torConfig
 import util.torTools
 import util.uiTools
 import TorCtl.TorCtl
 import TorCtl.TorUtil
 
 DEFAULT_CONFIG = os.path.expanduser("~/.armrc")
-DEFAULTS = {"startup.controlPassword": None,
-            "startup.interface.ipAddress": "127.0.0.1",
-            "startup.interface.port": 9051,
-            "startup.blindModeEnabled": False,
-            "startup.events": "N3"}
+CONFIG = {"startup.controlPassword": None,
+          "startup.interface.ipAddress": "127.0.0.1",
+          "startup.interface.port": 9051,
+          "startup.blindModeEnabled": False,
+          "startup.events": "N3",
+          "features.config.descriptions.enabled": True,
+          "features.config.descriptions.persistPath": "/tmp/arm/torConfigDescriptions.txt",
+          "log.configDescriptions.readManPageSuccess": util.log.INFO,
+          "log.configDescriptions.readManPageFailed": util.log.WARN,
+          "log.configDescriptions.persistance.loadSuccess": util.log.INFO,
+          "log.configDescriptions.persistance.loadFailed": util.log.INFO,
+          "log.configDescriptions.persistance.saveSuccess": util.log.INFO,
+          "log.configDescriptions.persistance.saveFailed": util.log.NOTICE}
 
 OPT = "i:c:be:vh"
 OPT_EXPANDED = ["interface=", "config=", "blind", "event=", "version", "help"]
@@ -48,8 +58,20 @@
 Example:
 arm -b -i 1643          hide connection data, attaching to control port 1643
 arm -e we -c /tmp/cfg   use this configuration file with 'WARN'/'ERR' events
-""" % (DEFAULTS["startup.interface.ipAddress"], DEFAULTS["startup.interface.port"], DEFAULT_CONFIG, DEFAULTS["startup.events"], interface.logPanel.EVENT_LISTING)
+""" % (CONFIG["startup.interface.ipAddress"], CONFIG["startup.interface.port"], DEFAULT_CONFIG, CONFIG["startup.events"], interface.logPanel.EVENT_LISTING)
 
+# messages related to loading the tor configuration descriptions
+DESC_LOAD_SUCCESS_MSG = "Loaded configuration descriptions from '%s' (runtime: %0.3f)"
+DESC_LOAD_FAILED_MSG = "Unable to load configuration descriptions (%s)"
+DESC_READ_MAN_SUCCESS_MSG = "Read descriptions for tor's configuration options from its man page (runtime %0.3f)"
+DESC_READ_MAN_FAILED_MSG = "Unable to read descriptions for tor's configuration options from its man page (%s)"
+DESC_SAVE_SUCCESS_MSG = "Saved configuration descriptions to '%s' (runtime: %0.3f)"
+DESC_SAVE_FAILED_MSG = "Unable to save configuration descriptions (%s)"
+
+NO_INTERNAL_CFG_MSG = "Failed to load the parsing configuration. This will be problematic for a few things like torrc validation and log duplication detection (%s)"
+STANDARD_CFG_LOAD_FAILED_MSG = "Failed to load configuration (using defaults): \"%s\""
+STANDARD_CFG_NOT_FOUND_MSG = "No configuration found at '%s', using defaults"
+
 def isValidIpAddr(ipStr):
   """
   Returns true if input is a valid IPv4 address, false otherwise.
@@ -73,9 +95,62 @@
   
   return True
 
+def _loadConfigurationDescriptions():
+  """
+  Attempts to load descriptions for tor's configuration options, fetching them
+  from the man page and persisting them to a file to speed future startups.
+  """
+  
+  # It is important that this is loaded before entering the curses context,
+  # otherwise the man call pegs the cpu for around a minute (I'm not sure
+  # why... curses must mess the terminal in a way that's important to man).
+  
+  if CONFIG["features.config.descriptions.enabled"]:
+    isConfigDescriptionsLoaded = False
+    descriptorPath = CONFIG["features.config.descriptions.persistPath"]
+    
+    # attempts to load persisted configuration descriptions
+    if descriptorPath:
+      try:
+        loadStartTime = time.time()
+        util.torConfig.loadOptionDescriptions(descriptorPath)
+        isConfigDescriptionsLoaded = True
+        
+        msg = DESC_LOAD_SUCCESS_MSG % (descriptorPath, time.time() - loadStartTime)
+        util.log.log(CONFIG["log.configDescriptions.persistance.loadSuccess"], msg)
+      except IOError, exc:
+        msg = DESC_LOAD_FAILED_MSG % util.sysTools.getFileErrorMsg(exc)
+        util.log.log(CONFIG["log.configDescriptions.persistance.loadFailed"], msg)
+    
+    if not isConfigDescriptionsLoaded:
+      try:
+        # fetches configuration options from the man page
+        loadStartTime = time.time()
+        util.torConfig.loadOptionDescriptions()
+        isConfigDescriptionsLoaded = True
+        
+        msg = DESC_READ_MAN_SUCCESS_MSG % (time.time() - loadStartTime)
+        util.log.log(CONFIG["log.configDescriptions.readManPageSuccess"], msg)
+      except IOError, exc:
+        msg = DESC_READ_MAN_FAILED_MSG % util.sysTools.getFileErrorMsg(exc)
+        util.log.log(CONFIG["log.configDescriptions.readManPageFailed"], msg)
+      
+      # persists configuration descriptions 
+      if isConfigDescriptionsLoaded and descriptorPath:
+        try:
+          loadStartTime = time.time()
+          util.torConfig.saveOptionDescriptions(descriptorPath)
+          
+          msg = DESC_SAVE_SUCCESS_MSG % (descriptorPath, time.time() - loadStartTime)
+          util.log.log(CONFIG["log.configDescriptions.persistance.loadSuccess"], msg)
+        except IOError, exc:
+          msg = DESC_SAVE_FAILED_MSG % util.sysTools.getFileErrorMsg(exc)
+          util.log.log(CONFIG["log.configDescriptions.persistance.saveFailed"], msg)
+
 if __name__ == '__main__':
-  param = dict([(key, None) for key in DEFAULTS.keys()])
-  configPath = DEFAULT_CONFIG            # path used for customized configuration
+  startTime = time.time()
+  param = dict([(key, None) for key in CONFIG.keys()])
+  configPath = DEFAULT_CONFIG # path used for customized configuration
   
   # parses user input, noting any issues
   try:
@@ -114,36 +189,41 @@
       print HELP_MSG
       sys.exit()
   
-  # attempts to load user's custom configuration, using defaults if not found
-  if not os.path.exists(configPath):
-    msg = "No configuration found at '%s', using defaults" % configPath
-    util.log.log(util.log.NOTICE, msg)
-    configPath = "%s/armrc.defaults" % os.path.dirname(sys.argv[0])
-  
   config = util.conf.getConfig("arm")
-  config.path = configPath
   
+  # attempts to fetch attributes for parsing tor's logs, configuration, etc
+  try:
+    pathPrefix = os.path.dirname(sys.argv[0])
+    if pathPrefix and not pathPrefix.endswith("/"):
+      pathPrefix = pathPrefix + "/"
+    
+    config.load("%ssettings.cfg" % pathPrefix)
+  except IOError, exc:
+    msg = NO_INTERNAL_CFG_MSG % util.sysTools.getFileErrorMsg(exc)
+    util.log.log(util.log.WARN, msg)
+  
+  # loads user's personal armrc if available
   if os.path.exists(configPath):
     try:
-      config.load()
-      
-      # revises defaults to match user's configuration
-      config.update(DEFAULTS)
-      
-      # loads user preferences for utilities
-      for utilModule in (util.conf, util.connections, util.hostnames, util.log, util.panel, util.sysTools, util.torTools, util.uiTools):
-        utilModule.loadConfig(config)
+      config.load(configPath)
     except IOError, exc:
-      msg = "Failed to load configuration (using defaults): \"%s\"" % str(exc)
+      msg = STANDARD_CFG_LOAD_FAILED_MSG % util.sysTools.getFileErrorMsg(exc)
       util.log.log(util.log.WARN, msg)
   else:
-    # no local copy of the armrc defaults, so fall back to values in the source
-    msg = "defaults file not found, falling back (log duplicate detection will be mostly nonfunctional)"
-    util.log.log(util.log.WARN, msg)
+    # no armrc found, falling back to the defaults in the source
+    msg = STANDARD_CFG_NOT_FOUND_MSG % configPath
+    util.log.log(util.log.NOTICE, msg)
   
+  # revises defaults to match user's configuration
+  config.update(CONFIG)
+  
+  # loads user preferences for utilities
+  for utilModule in (util.conf, util.connections, util.hostnames, util.log, util.panel, util.sysTools, util.torConfig, util.torTools, util.uiTools):
+    utilModule.loadConfig(config)
+  
   # overwrites undefined parameters with defaults
   for key in param.keys():
-    if param[key] == None: param[key] = DEFAULTS[key]
+    if param[key] == None: param[key] = CONFIG[key]
   
   # validates that input has a valid ip address and port
   controlAddr = param["startup.interface.ipAddress"]
@@ -170,13 +250,35 @@
   # sets up TorCtl connection, prompting for the passphrase if necessary and
   # sending problems to stdout if they arise
   TorCtl.INCORRECT_PASSWORD_MSG = "Controller password found in '%s' was incorrect" % configPath
-  authPassword = config.get("startup.controlPassword", DEFAULTS["startup.controlPassword"])
+  authPassword = config.get("startup.controlPassword", CONFIG["startup.controlPassword"])
   conn = TorCtl.TorCtl.connect(controlAddr, controlPort, authPassword)
   if conn == None: sys.exit(1)
   
+  # removing references to the controller password so the memory can be freed
+  # (unfortunately python does allow for direct access to the memory so this
+  # is the best we can do)
+  del authPassword
+  if "startup.controlPassword" in config.contents:
+    del config.contents["startup.controlPassword"]
+    
+    pwLineNum = None
+    for i in range(len(config.rawContents)):
+      if config.rawContents[i].strip().startswith("startup.controlPassword"):
+        pwLineNum = i
+        break
+    
+    if pwLineNum != None:
+      del config.rawContents[i]
+  
+  # initializing the connection may require user input (for the password)
+  # skewing the startup time results so this isn't counted
+  initTime = time.time() - startTime
   controller = util.torTools.getConn()
   controller.init(conn)
   
-  interface.controller.startTorMonitor(expandedEvents, param["startup.blindModeEnabled"])
+  # fetches descriptions for tor's configuration options
+  _loadConfigurationDescriptions()
+  
+  interface.controller.startTorMonitor(time.time() - initTime, expandedEvents, param["startup.blindModeEnabled"])
   conn.close()
 

Modified: arm/release/src/uninstall
===================================================================
--- arm/release/src/uninstall	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/uninstall	2010-11-28 10:58:57 UTC (rev 23873)
@@ -1,5 +1,5 @@
 #!/bin/sh
-files="/usr/bin/arm /usr/share/man/man1/arm.1.gz /usr/lib/arm"
+files="/usr/bin/arm /usr/share/man/man1/arm.1.gz /usr/share/arm"
 
 for i in $files 
 do

Modified: arm/release/src/util/__init__.py
===================================================================
--- arm/release/src/util/__init__.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/util/__init__.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -4,5 +4,5 @@
 and safely working with curses (hiding some of the gory details).
 """
 
-__all__ = ["conf", "connections", "hostnames", "log", "panel", "sysTools", "torTools", "uiTools"]
+__all__ = ["conf", "connections", "hostnames", "log", "panel", "sysTools", "torConfig", "torTools", "uiTools"]
 

Modified: arm/release/src/util/conf.py
===================================================================
--- arm/release/src/util/conf.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/util/conf.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -14,18 +14,14 @@
 If a key's defined multiple times then the last instance of it is used.
 """
 
-import os
 import threading
 
 from util import log
 
 CONFS = {}  # mapping of identifier to singleton instances of configs
 CONFIG = {"log.configEntryNotFound": None,
-          "log.configEntryTypeError": log.INFO}
+          "log.configEntryTypeError": log.NOTICE}
 
-# key prefixes that can contain multiple values
-LIST_KEYS = ["msg."]
-
 def loadConfig(config):
   config.update(CONFIG)
 
@@ -42,21 +38,6 @@
   if not handle in CONFS: CONFS[handle] = Config()
   return CONFS[handle]
 
-def isListKey(configKey):
-  """
-  Provides true if the given configuration key can have multiple values (being
-  a list), false otherwise.
-  
-  Arguments:
-    configKey - configuration key to check
-  """
-  
-  for listKeyPrefix in LIST_KEYS:
-    if configKey.startswith(listKeyPrefix):
-      return True
-  
-  return False
-
 class Config():
   """
   Handler for easily working with custom configurations, providing persistence
@@ -73,27 +54,29 @@
     Creates a new configuration instance.
     """
     
-    self.path = None        # path to the associated configuration file
+    self.path = None        # location last loaded from
     self.contents = {}      # configuration key/value pairs
     self.contentsLock = threading.RLock()
     self.requestedKeys = set()
     self.rawContents = []   # raw contents read from configuration file
   
-  def getValue(self, key, default=None):
+  def getValue(self, key, default=None, multiple=False):
     """
-    This provides the currently value associated with a given key, and a list
-    of values if isListKey(key) is true. If no such key exists then this
-    provides the default.
+    This provides the currently value associated with a given key. If no such
+    key exists then this provides the default.
     
     Arguments:
-      key     - config setting to be fetched
-      default - value provided if no such key exists
+      key      - config setting to be fetched
+      default  - value provided if no such key exists
+      multiple - provides back a list of all values if true, otherwise this
+                 returns the last loaded configuration value
     """
     
     self.contentsLock.acquire()
     
     if key in self.contents:
       val = self.contents[key]
+      if not multiple: val = val[-1]
       self.requestedKeys.add(key)
     else:
       msg = "config entry '%s' not found, defaulting to '%s'" % (key, str(default))
@@ -104,35 +87,35 @@
     
     return val
   
-  def get(self, key, default=None, minValue=0, maxValue=None):
+  def get(self, key, default=None):
     """
     Fetches the given configuration, using the key and default value to hint
     the type it should be. Recognized types are:
+    - logging runlevel if key starts with "log."
     - boolean if default is a boolean (valid values are 'true' and 'false',
       anything else provides the default)
     - integer or float if default is a number (provides default if fails to
       cast)
-    - logging runlevel if key starts with "log."
-    - list if isListKey(key) is true
+    - list of all defined values default is a list
+    - mapping of all defined values (key/value split via "=>") if the default
+      is a dict
     
     Arguments:
       key      - config setting to be fetched
       default  - value provided if no such key exists
-      minValue - if set and default value is numeric then uses this constraint
-      maxValue - if set and default value is numeric then uses this constraint
     """
     
     callDefault = log.runlevelToStr(default) if key.startswith("log.") else default
-    val = self.getValue(key, callDefault)
+    isMultivalue = isinstance(default, list) or isinstance(default, dict)
+    val = self.getValue(key, callDefault, isMultivalue)
     if val == default: return val
     
-    if isinstance(val, list):
-      pass
-    elif key.startswith("log."):
+    if key.startswith("log."):
       if val.lower() in ("none", "debug", "info", "notice", "warn", "err"):
         val = log.strToRunlevel(val)
       else:
-        msg = "config entry '%s' is expected to be a runlevel, defaulting to '%s'" % (key, callDefault)
+        msg = "config entry '%s' is expected to be a runlevel" % key
+        if default != None: msg += ", defaulting to '%s'" % callDefault
         log.log(CONFIG["log.configEntryTypeError"], msg)
         val = default
     elif isinstance(default, bool):
@@ -143,37 +126,124 @@
         log.log(CONFIG["log.configEntryTypeError"], msg)
         val = default
     elif isinstance(default, int):
-      try:
-        val = int(val)
-        if minValue: val = max(val, minValue)
-        if maxValue: val = min(val, maxValue)
+      try: val = int(val)
       except ValueError:
         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)
-        if minValue: val = max(val, minValue)
-        if maxValue: val = min(val, maxValue)
+      try: val = float(val)
       except ValueError:
         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):
+      pass # nothing special to do (already a list)
+    elif isinstance(default, dict):
+      valMap = {}
+      for entry in val:
+        if "=>" in entry:
+          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)
+          log.log(CONFIG["log.configEntryTypeError"], msg)
+      val = valMap
     
     return val
   
-  def update(self, confMappings):
+  def getStrCSV(self, key, default = None, count = None):
     """
+    Fetches the given key as a comma separated value. This provides back a list
+    with the stripped values.
+    
+    Arguments:
+      key     - config setting to be fetched
+      default - value provided if no such key exists or doesn't match the count
+      count   - if set, then a TypeError is logged (and default returned) if
+                the number of elements doesn't match the count
+    """
+    
+    confValue = self.getValue(key)
+    if confValue == None: return default
+    else:
+      confComp = [entry.strip() for entry in confValue.split(",")]
+      
+      # 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)
+        if default != None and (isinstance(default, list) or isinstance(default, tuple)):
+          defaultStr = ", ".join([str(i) for i in default])
+          msg += ", defaulting to '%s'" % defaultStr
+        
+        log.log(CONFIG["log.configEntryTypeError"], msg)
+        return default
+      
+      return confComp
+  
+  def getIntCSV(self, key, default = None, count = None, minValue = None, maxValue = None):
+    """
+    Fetches the given comma separated value, logging a TypeError (and returning
+    the default) if the values arne't ints or aren't constrained to the given
+    bounds.
+    
+    Arguments:
+      key      - config setting to be fetched
+      default  - value provided if no such key exists, doesn't match the count,
+                 values aren't all integers, or doesn't match the bounds
+      count    - checks that the number of values matches this if set
+      minValue - checks that all values are over this if set
+      maxValue - checks that all values are less than this if set
+    """
+    
+    confComp = self.getStrCSV(key, default, count)
+    if confComp == default: return default
+    
+    # validates the input, setting the errorMsg if there's a problem
+    errorMsg = None
+    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
+    
+    for val in confComp:
+      if not val.isdigit():
+        errorMsg = baseErrorMsg % "only have integer values"
+        break
+      else:
+        if minValue != None and int(val) < minValue:
+          errorMsg = baseErrorMsg % "only have values over %i" % minValue
+          break
+        elif maxValue != None and int(val) > maxValue:
+          errorMsg = baseErrorMsg % "only have values less than %i" % maxValue
+          break
+    
+    if errorMsg:
+      log.log(CONFIG["log.configEntryTypeError"], errorMsg)
+      return default
+    else: return [int(val) for val in confComp]
+
+  def update(self, confMappings, limits = {}):
+    """
     Revises a set of key/value mappings to reflect the current configuration.
     Undefined values are left with their current values.
     
     Arguments:
       confMappings - configuration key/value mappings to be revised
+      limits       - mappings of limits on numeric values, expected to be of
+                     the form "configKey -> min" or "configKey -> (min, max)"
     """
     
     for entry in confMappings.keys():
-      confMappings[entry] = self.get(entry, confMappings[entry])
+      val = self.get(entry, confMappings[entry])
+      
+      if entry in limits and (isinstance(val, int) or isinstance(val, float)):
+        if isinstance(limits[entry], tuple):
+          val = max(val, limits[entry][0])
+          val = min(val, limits[entry][1])
+        else: val = max(val, limits[entry])
+      
+      confMappings[entry] = val
   
   def getKeys(self):
     """
@@ -211,45 +281,39 @@
     self.contents.clear()
     self.contentsLock.release()
   
-  def load(self):
+  def load(self, path):
     """
-    Reads in the contents of the currently set configuration file (appending
-    any results to the current configuration). If the file's empty or doesn't
-    exist then this doesn't do anything.
+    Reads in the contents of the given path, adding its configuration values
+    and overwriting any that already exist. If the file's empty then this
+    doesn't do anything. Other issues (like having insufficient permissions or
+    if the file doesn't exist) result in an IOError.
     
-    Other issues (like having an unset path or insufficient permissions) result
-    in an IOError.
+    Arguments:
+      path - file path to be loaded
     """
     
-    if not self.path: raise IOError("unable to load (config path undefined)")
+    configFile = open(path, "r")
+    self.rawContents = configFile.readlines()
+    configFile.close()
     
-    if os.path.exists(self.path):
-      configFile = open(self.path, "r")
-      self.rawContents = configFile.readlines()
-      configFile.close()
+    self.contentsLock.acquire()
+    
+    for line in self.rawContents:
+      # strips any commenting or excess whitespace
+      commentStart = line.find("#")
+      if commentStart != -1: line = line[:commentStart]
+      line = line.strip()
       
-      self.contentsLock.acquire()
-      
-      for line in self.rawContents:
-        # strips any commenting or excess whitespace
-        commentStart = line.find("#")
-        if commentStart != -1: line = line[:commentStart]
-        line = line.strip()
+      # parse the key/value pair
+      if line and " " in line:
+        key, value = line.split(" ", 1)
+        value = value.strip()
         
-        # parse the key/value pair
-        if line:
-          key, value = line, ""
-          
-          # gets the key/value pair (no value was given if there isn't a space)
-          if " " in line: key, value = line.split(" ", 1)
-          
-          if isListKey(key):
-            if key in self.contents: self.contents[key].append(value)
-            else: self.contents[key] = [value]
-          else:
-            self.contents[key] = value
-      
-      self.contentsLock.release()
+        if key in self.contents: self.contents[key].append(value)
+        else: self.contents[key] = [value]
+    
+    self.path = path
+    self.contentsLock.release()
   
   def save(self, saveBackup=True):
     """

Modified: arm/release/src/util/connections.py
===================================================================
--- arm/release/src/util/connections.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/util/connections.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -147,7 +147,7 @@
   else: RESOLVERS[haltedIndex] = r
   return r
 
-if __name__ == '__main__':
+def test():
   # quick method for testing connection resolution
   userInput = raw_input("Enter query (<ss, netstat, lsof> PROCESS_NAME [PID]): ").split()
   

Modified: arm/release/src/util/hostnames.py
===================================================================
--- arm/release/src/util/hostnames.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/util/hostnames.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -47,12 +47,12 @@
           "log.hostnameCacheTrimmed": log.INFO}
 
 def loadConfig(config):
-  config.update(CONFIG)
+  config.update(CONFIG, {
+    "queries.hostnames.poolSize": 1,
+    "cache.hostnames.size": 100,
+    "cache.hostnames.trimSize": 10})
   
-  # ensures sane config values
-  CONFIG["queries.hostnames.poolSize"] = max(1, CONFIG["queries.hostnames.poolSize"])
-  CONFIG["cache.hostnames.size"] = max(100, CONFIG["cache.hostnames.size"])
-  CONFIG["cache.hostnames.trimSize"] = max(10, min(CONFIG["cache.hostnames.trimSize"], CONFIG["cache.hostnames.size"] / 2))
+  CONFIG["cache.hostnames.trimSize"] = min(CONFIG["cache.hostnames.trimSize"], CONFIG["cache.hostnames.size"] / 2)
 
 def start():
   """

Modified: arm/release/src/util/log.py
===================================================================
--- arm/release/src/util/log.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/util/log.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -28,11 +28,11 @@
           "cache.armLog.trimSize": 200}
 
 def loadConfig(config):
-  config.update(CONFIG)
+  config.update(CONFIG, {
+    "cache.armLog.size": 10,
+    "cache.armLog.trimSize": 5})
   
-  # ensures sane config values
-  CONFIG["cache.armLog.size"] = max(10, CONFIG["cache.armLog.size"])
-  CONFIG["cache.armLog.trimSize"] = max(5, min(CONFIG["cache.armLog.trimSize"], CONFIG["cache.armLog.size"] / 2))
+  CONFIG["cache.armLog.trimSize"] = min(CONFIG["cache.armLog.trimSize"], CONFIG["cache.armLog.size"] / 2)
 
 def strToRunlevel(runlevelStr):
   """

Modified: arm/release/src/util/panel.py
===================================================================
--- arm/release/src/util/panel.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/util/panel.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -2,7 +2,6 @@
 Wrapper for safely working with curses subwindows.
 """
 
-import sys
 import traceback
 import curses
 from threading import RLock
@@ -224,15 +223,6 @@
         self.win.erase() # clears any old contents
         self.draw(self.win, self.maxX - 1, self.maxY)
       self.win.refresh()
-    except:
-      # without terminating curses continues in a zombie state (requiring a
-      # kill signal to quit, and screwing up the terminal)
-      # TODO: provide a nicer, general purpose handler for unexpected exceptions
-      try:
-        tracebackFile = open("/tmp/armTraceback", "w")
-        traceback.print_exc(file=tracebackFile)
-      finally:
-        sys.exit(1)
     finally:
       CURSES_LOCK.release()
   
@@ -252,7 +242,12 @@
     # subwindows need a single character buffer (either in the x or y 
     # direction) from actual content to prevent crash when shrank
     if self.win and self.maxX > x and self.maxY > y:
-      self.win.addstr(y, x, msg[:self.maxX - x - 1], attr)
+      try:
+        self.win.addstr(y, x, msg[:self.maxX - x - 1], attr)
+      except:
+        # this might produce a _curses.error during edge cases, for instance
+        # when resizing with visible popups
+        pass
   
   def addfstr(self, y, x, msg):
     """
@@ -337,6 +332,43 @@
         baseMsg = "Unclosed formatting tag%s:" % ("s" if len(expectedCloseTags) > 1 else "")
         raise ValueError("%s: '%s'\n  \"%s\"" % (baseMsg, "', '".join(expectedCloseTags), msg))
   
+  def getstr(self, y, x, initialText = ""):
+    """
+    Provides a text field where the user can input a string, blocking until
+    they've done so and returning the result. If the user presses escape then
+    this terminates and provides back None. This should only be called from
+    the context of a panel's draw method.
+    
+    Arguments:
+      y           - vertical location
+      x           - horizontal location
+      initialText - starting text in this field
+    """
+    
+    # makes cursor visible
+    try: previousCursorState = curses.curs_set(1)
+    except curses.error: previousCursorState = 0
+    
+    # temporary subwindow for user input
+    displayWidth = self.getPreferredSize()[1]
+    inputSubwindow = self.parent.subwin(1, displayWidth - x, self.top, x)
+    
+    # prepopulates the initial text
+    if initialText: inputSubwindow.addstr(0, 0, initialText)
+    
+    # Displays the text field, blocking until the user's done. This closes the
+    # text panel and returns userInput to the initial text if the user presses
+    # escape.
+    textbox = curses.textpad.Textbox(inputSubwindow, True)
+    userInput = textbox.edit(lambda key: _textboxValidate(textbox, key)).strip()
+    if textbox.lastcmd == curses.ascii.BEL: userInput = None
+    
+    # reverts visability settings
+    try: curses.curs_set(previousCursorState)
+    except curses.error: pass
+    
+    return userInput
+  
   def addScrollBar(self, top, bottom, size, drawTop = 0, drawBottom = -1):
     """
     Draws a left justified scroll bar reflecting position within a vertical
@@ -375,6 +407,10 @@
     if top > 0: sliderTop = max(sliderTop, 1)
     if bottom != size: sliderTop = min(sliderTop, scrollbarHeight - sliderSize - 2)
     
+    # avoids a rounding error that causes the scrollbar to be too low when at
+    # the bottom
+    if bottom == size: sliderTop = scrollbarHeight - sliderSize - 1
+    
     # draws scrollbar slider
     for i in range(scrollbarHeight):
       if i >= sliderTop and i <= sliderTop + sliderSize:
@@ -382,8 +418,8 @@
     
     # draws box around the scroll bar
     self.win.vline(drawTop, 1, curses.ACS_VLINE, self.maxY - 2)
-    self.win.vline(drawBottom, 1, curses.ACS_LRCORNER, 1)
-    self.win.hline(drawBottom, 0, curses.ACS_HLINE, 1)
+    self.win.addch(drawBottom, 1, curses.ACS_LRCORNER)
+    self.win.addch(drawBottom, 0, curses.ACS_HLINE)
   
   def _resetSubwindow(self):
     """
@@ -426,4 +462,38 @@
       msg = "recreating panel '%s' with the dimensions of %i/%i" % (self.getName(), newHeight, newWidth)
       log.log(CONFIG["log.panelRecreated"], msg)
     return recreate
+
+def _textboxValidate(textbox, key):
+  """
+  Interceptor for keystrokes given to a textbox, doing the following:
+  - quits by setting the input to curses.ascii.BEL when escape is pressed
+  - stops the cursor at the end of the box's content when pressing the right
+    arrow
+  - home and end keys move to the start/end of the line
+  """
   
+  y, x = textbox.win.getyx()
+  if key == 27:
+    # curses.ascii.BEL is a character codes that causes textpad to terminate
+    return curses.ascii.BEL
+  elif key == curses.KEY_HOME:
+    textbox.win.move(y, 0)
+    return None
+  elif key in (curses.KEY_END, curses.KEY_RIGHT):
+    msgLen = len(textbox.gather())
+    textbox.win.move(y, x) # reverts cursor movement during gather call
+    
+    if key == curses.KEY_END and msgLen > 0 and x < msgLen - 1:
+      # if we're in the content then move to the end
+      textbox.win.move(y, msgLen - 1)
+      return None
+    elif key == curses.KEY_RIGHT and x >= msgLen - 1:
+      # don't move the cursor if there's no content after it
+      return None
+  elif key == 410:
+    # if we're resizing the display during text entry then cancel it
+    # (otherwise the input field is filled with nonprintable characters)
+    return curses.ascii.BEL
+  
+  return key
+

Modified: arm/release/src/util/sysTools.py
===================================================================
--- arm/release/src/util/sysTools.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/util/sysTools.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -54,6 +54,26 @@
     CMD_AVAILABLE_CACHE[command] = cmdExists
     return cmdExists
 
+def getFileErrorMsg(exc):
+  """
+  Strips off the error number prefix for file related IOError messages. For
+  instance, instead of saying:
+  [Errno 2] No such file or directory
+  
+  this would return:
+  no such file or directory
+  
+  Arguments:
+    exc - file related IOError exception
+  """
+  
+  excStr = str(exc)
+  if excStr.startswith("[Errno ") and "] " in excStr:
+    excStr = excStr[excStr.find("] ") + 2:].strip()
+    excStr = excStr[0].lower() + excStr[1:]
+  
+  return excStr
+
 def call(command, cacheAge=0, suppressExc=False, quiet=True):
   """
   Convenience function for performing system calls, providing:

Copied: arm/release/src/util/torConfig.py (from rev 23872, arm/trunk/src/util/torConfig.py)
===================================================================
--- arm/release/src/util/torConfig.py	                        (rev 0)
+++ arm/release/src/util/torConfig.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -0,0 +1,659 @@
+"""
+Helper functions for working with tor's configuration file.
+"""
+
+import os
+import threading
+
+from util import log, sysTools, torTools, uiTools
+
+CONFIG = {"features.torrc.validate": True,
+          "torrc.alias": {},
+          "torrc.label.size.b": [],
+          "torrc.label.size.kb": [],
+          "torrc.label.size.mb": [],
+          "torrc.label.size.gb": [],
+          "torrc.label.size.tb": [],
+          "torrc.label.time.sec": [],
+          "torrc.label.time.min": [],
+          "torrc.label.time.hour": [],
+          "torrc.label.time.day": [],
+          "torrc.label.time.week": [],
+          "log.configDescriptions.unrecognizedCategory": log.NOTICE}
+
+# enums and values for numeric torrc entries
+UNRECOGNIZED, SIZE_VALUE, TIME_VALUE = range(1, 4)
+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)
+
+# 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"}
+
+TORRC = None # singleton torrc instance
+MAN_OPT_INDENT = 7 # indentation before options in the man page
+MAN_EX_INDENT = 15 # indentation used for man page examples
+PERSIST_ENTRY_DIVIDER = "-" * 80 + "\n" # splits config entries when saving to a file
+MULTILINE_PARAM = None # cached multiline parameters (lazily loaded)
+
+def loadConfig(config):
+  CONFIG["torrc.alias"] = config.get("torrc.alias", {})
+  
+  # all the torrc.label.* values are comma separated lists
+  for configKey in CONFIG.keys():
+    if configKey.startswith("torrc.label."):
+      configValues = config.get(configKey, "").split(",")
+      if configValues: CONFIG[configKey] = [val.strip() for val in configValues]
+
+class ManPageEntry:
+  """
+  Information provided about a tor configuration option in its man page entry.
+  """
+  
+  def __init__(self, index, category, argUsage, description):
+    self.index = index
+    self.category = category
+    self.argUsage = argUsage
+    self.description = description
+
+def getTorrc():
+  """
+  Singleton constructor for a Controller. Be aware that this starts as being
+  unloaded, needing the torrc contents to be loaded before being functional.
+  """
+  
+  global TORRC
+  if TORRC == None: TORRC = Torrc()
+  return TORRC
+
+def loadOptionDescriptions(loadPath = None):
+  """
+  Fetches and parses descriptions for tor's configuration options from its man
+  page. This can be a somewhat lengthy call, and raises an IOError if issues
+  occure.
+  
+  If available, this can load the configuration descriptions from a file where
+  they were previously persisted to cut down on the load time (latency for this
+  is around 200ms).
+  
+  Arguments:
+    loadPath - if set, this attempts to fetch the configuration descriptions
+               from the given path instead of the man page
+  """
+  
+  CONFIG_DESCRIPTIONS_LOCK.acquire()
+  CONFIG_DESCRIPTIONS.clear()
+  
+  raisedExc = None
+  try:
+    if loadPath:
+      # Input file is expected to be of the form:
+      # <option>
+      # <arg description>
+      # <description, possibly multiple lines>
+      # <PERSIST_ENTRY_DIVIDER>
+      inputFile = open(loadPath, "r")
+      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()
+        
+        if versionLine.startswith("Tor Version "):
+          fileVersion = versionLine[12:]
+          torVersion = torTools.getConn().getInfo("version", "")
+          if fileVersion != torVersion:
+            msg = "wrong version, tor is %s but the file's from %s" % (torVersion, fileVersion)
+            raise IOError(msg)
+        else:
+          raise IOError("unable to parse version")
+        
+        while inputFileContents:
+          # gets category enum, failing if it doesn't exist
+          categoryStr = inputFileContents.pop(0).rstrip()
+          if categoryStr in strToCat:
+            category = strToCat[categoryStr]
+          else:
+            baseMsg = "invalid category in input file: '%s'"
+            raise IOError(baseMsg % categoryStr)
+          
+          # gets the position in the man page
+          indexArg, indexStr = -1, inputFileContents.pop(0).rstrip()
+          
+          if indexStr.startswith("index: "):
+            indexStr = indexStr[7:]
+            
+            if indexStr.isdigit(): indexArg = int(indexStr)
+            else: raise IOError("non-numeric index value: %s" % indexStr)
+          else: raise IOError("malformed index argument: %s"% indexStr)
+          
+          option = inputFileContents.pop(0).rstrip()
+          argument = inputFileContents.pop(0).rstrip()
+          
+          description, loadedLine = "", inputFileContents.pop(0)
+          while loadedLine != PERSIST_ENTRY_DIVIDER:
+            description += loadedLine
+            
+            if inputFileContents: loadedLine = inputFileContents.pop(0)
+            else: break
+          
+          CONFIG_DESCRIPTIONS[option.lower()] = ManPageEntry(indexArg, category, argument, description.rstrip())
+      except IndexError:
+        CONFIG_DESCRIPTIONS.clear()
+        raise IOError("input file format is invalid")
+    else:
+      manCallResults = sysTools.call("man tor")
+      
+      # Fetches all options available with this tor instance. This isn't
+      # vital, and the validOptions are left empty if the call fails.
+      conn, validOptions = torTools.getConn(), []
+      configOptionQuery = conn.getInfo("config/names").strip().split("\n")
+      if configOptionQuery:
+        validOptions = [line[:line.find(" ")].lower() for line in configOptionQuery]
+      
+      optionCount, lastOption, lastArg = 0, None, None
+      lastCategory, lastDescription = GENERAL, ""
+      for line in manCallResults:
+        line = uiTools.getPrintable(line)
+        strippedLine = line.strip()
+        
+        # we have content, but an indent less than an option (ignore line)
+        #if strippedLine and not line.startswith(" " * MAN_OPT_INDENT): continue
+        
+        # line starts with an indent equivilant to a new config option
+        isOptIndent = line.startswith(" " * MAN_OPT_INDENT) and line[MAN_OPT_INDENT] != " "
+        
+        isCategoryLine = not line.startswith(" ") and "OPTIONS" in line
+        
+        # if this is a category header or a new option, add an entry using the
+        # buffered results
+        if isOptIndent or isCategoryLine:
+          # Filters the line based on if the option is recognized by tor or
+          # not. This isn't necessary for arm, so if unable to make the check
+          # then we skip filtering (no loss, the map will just have some extra
+          # noise).
+          strippedDescription = lastDescription.strip()
+          if lastOption and (not validOptions or lastOption.lower() in validOptions):
+            CONFIG_DESCRIPTIONS[lastOption.lower()] = ManPageEntry(optionCount, lastCategory, lastArg, strippedDescription)
+            optionCount += 1
+          lastDescription = ""
+          
+          # parses the option and argument
+          line = line.strip()
+          divIndex = line.find(" ")
+          if divIndex != -1:
+            lastOption, lastArg = line[:divIndex], line[divIndex + 1:]
+          
+          # 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
+            else:
+              msg = "Unrecognized category in the man page: %s" % line.strip()
+              log.log(CONFIG["log.configDescriptions.unrecognizedCategory"], msg)
+        else:
+          # Appends the text to the running description. Empty lines and lines
+          # starting with a specific indentation are used for formatting, for
+          # instance the ExitPolicy and TestingTorNetwork entries.
+          if lastDescription and lastDescription[-1] != "\n":
+            lastDescription += " "
+          
+          if not strippedLine:
+            lastDescription += "\n\n"
+          elif line.startswith(" " * MAN_EX_INDENT):
+            lastDescription += "    %s\n" % strippedLine
+          else: lastDescription += strippedLine
+  except IOError, exc:
+    raisedExc = exc
+  
+  CONFIG_DESCRIPTIONS_LOCK.release()
+  if raisedExc: raise raisedExc
+
+def saveOptionDescriptions(path):
+  """
+  Preserves the current configuration descriptors to the given path. This
+  raises an IOError if unable to do so.
+  
+  Arguments:
+    path - location to persist configuration descriptors
+  """
+  
+  # make dir if the path doesn't already exist
+  baseDir = os.path.dirname(path)
+  if not os.path.exists(baseDir): os.makedirs(baseDir)
+  outputFile = open(path, "w")
+  
+  CONFIG_DESCRIPTIONS_LOCK.acquire()
+  sortedOptions = CONFIG_DESCRIPTIONS.keys()
+  sortedOptions.sort()
+  
+  torVersion = torTools.getConn().getInfo("version", "")
+  outputFile.write("Tor Version %s\n" % torVersion)
+  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))
+    if i != len(sortedOptions) - 1: outputFile.write(PERSIST_ENTRY_DIVIDER)
+  
+  outputFile.close()
+  CONFIG_DESCRIPTIONS_LOCK.release()
+
+def getConfigDescription(option):
+  """
+  Provides ManPageEntry instances populated with information fetched from the
+  tor man page. This provides None if no such option has been loaded. If the
+  man page is in the process of being loaded then this call blocks until it
+  finishes.
+  
+  Arguments:
+    option - tor config option
+  """
+  
+  CONFIG_DESCRIPTIONS_LOCK.acquire()
+  
+  if option.lower() in CONFIG_DESCRIPTIONS:
+    returnVal = CONFIG_DESCRIPTIONS[option.lower()]
+  else: returnVal = None
+  
+  CONFIG_DESCRIPTIONS_LOCK.release()
+  return returnVal
+
+def getConfigLocation():
+  """
+  Provides the location of the torrc, raising an IOError with the reason if the
+  path can't be determined.
+  """
+  
+  conn = torTools.getConn()
+  configLocation = conn.getInfo("config-file")
+  if not configLocation: raise IOError("unable to query the torrc location")
+  
+  # checks if this is a relative path, needing the tor pwd to be appended
+  if configLocation[0] != "/":
+    torPid = conn.getMyPid()
+    failureMsg = "querying tor's pwd failed because %s"
+    if not torPid: raise IOError(failureMsg % "we couldn't get the pid")
+    
+    try:
+      # pwdx results are of the form:
+      # 3799: /home/atagar
+      # 5839: No such process
+      results = sysTools.call("pwdx %s" % torPid)
+      if not results:
+        raise IOError(failureMsg % "pwdx didn't return any results")
+      elif results[0].endswith("No such process"):
+        raise IOError(failureMsg % ("pwdx reported no process for pid " + torPid))
+      elif len(results) != 1 or results.count(" ") != 1:
+        raise IOError(failureMsg % "we got unexpected output from pwdx")
+      else:
+        pwdPath = results[0][results[0].find(" ") + 1:]
+        configLocation = "%s/%s" % (pwdPath, configLocation)
+    except IOError, exc:
+      raise IOError(failureMsg % ("the pwdx call failed: " + str(exc)))
+  
+  return torTools.getPathPrefix() + configLocation
+
+def getMultilineParameters():
+  """
+  Provides parameters that can be defined multiple times in the torrc without
+  overwriting the value.
+  """
+  
+  # fetches config options with the LINELIST (aka 'LineList'), LINELIST_S (aka
+  # 'Dependent'), and LINELIST_V (aka 'Virtual') types
+  global MULTILINE_PARAM
+  if MULTILINE_PARAM == None:
+    conn = torTools.getConn()
+    configOptionQuery = conn.getInfo("config/names", "").strip().split("\n")
+    
+    multilineEntries = []
+    for line in configOptionQuery:
+      confOption, confType = line.strip().split(" ", 1)
+      if confType in ("LineList", "Dependant", "Virtual"):
+        multilineEntries.append(confOption)
+    
+    MULTILINE_PARAM = multilineEntries
+  
+  return tuple(MULTILINE_PARAM)
+
+def getCustomOptions():
+  """
+  Provides the set of torrc parameters that differ from their defaults.
+  """
+  
+  customOptions, conn = set(), torTools.getConn()
+  configTextQuery = conn.getInfo("config-text", "").strip().split("\n")
+  for entry in configTextQuery: customOptions.add(entry[:entry.find(" ")])
+  return customOptions
+
+def validate(contents = None):
+  """
+  Performs validation on the given torrc contents, providing back a listing of
+  (line number, issue, msg) tuples for issues found. If the issue occures on a
+  multiline torrc entry then the line number is for the last line of the entry.
+  
+  Arguments:
+    contents - torrc contents
+  """
+  
+  conn = torTools.getConn()
+  customOptions = getCustomOptions()
+  issuesFound, seenOptions = [], []
+  
+  # Strips comments and collapses multiline multi-line entries, for more
+  # information see:
+  # https://trac.torproject.org/projects/tor/ticket/1929
+  strippedContents, multilineBuffer = [], ""
+  for line in _stripComments(contents):
+    if not line: strippedContents.append("")
+    else:
+      line = multilineBuffer + line
+      multilineBuffer = ""
+      
+      if line.endswith("\\"):
+        multilineBuffer = line[:-1]
+        strippedContents.append("")
+      else:
+        strippedContents.append(line.strip())
+  
+  for lineNumber in range(len(strippedContents) - 1, -1, -1):
+    lineText = strippedContents[lineNumber]
+    if not lineText: continue
+    
+    lineComp = lineText.split(None, 1)
+    if len(lineComp) == 2: option, value = lineComp
+    else: option, value = lineText, ""
+    
+    # if an aliased option then use its real name
+    if option in CONFIG["torrc.alias"]:
+      option = CONFIG["torrc.alias"][option]
+    
+    # most parameters are overwritten if defined multiple times
+    if option in seenOptions and not option in getMultilineParameters():
+      issuesFound.append((lineNumber, VAL_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))
+    
+    # replace aliases with their recognized representation
+    if option in CONFIG["torrc.alias"]:
+      option = CONFIG["torrc.alias"][option]
+    
+    # tor appears to replace tabs with a space, for instance:
+    # "accept\t*:563" is read back as "accept *:563"
+    value = value.replace("\t", " ")
+    
+    # parse value if it's a size or time, expanding the units
+    value, valueType = _parseConfValue(value)
+    
+    # issues GETCONF to get the values tor's currently configured to use
+    torValues = conn.getOption(option, [], True)
+    
+    # Some singleline entries are lists, in which case tor provides csv values
+    # without spaces, such as:
+    # lolcat1,lolcat2,cutebunny,extracutebunny,birthdaynode
+    # so we need to strip spaces in comma separated values.
+    
+    if "," in value:
+      value = ",".join([val.strip() for val in value.split(",")])
+    
+    # multiline entries can be comma separated values (for both tor and conf)
+    valueList = [value]
+    if option in getMultilineParameters():
+      valueList = [val.strip() for val in value.split(",")]
+      
+      fetchedValues, torValues = torValues, []
+      for fetchedValue in fetchedValues:
+        for fetchedEntry in fetchedValue.split(","):
+          fetchedEntry = fetchedEntry.strip()
+          if not fetchedEntry in torValues:
+            torValues.append(fetchedEntry)
+    
+    for val in valueList:
+      # checks if both the argument and tor's value are empty
+      isBlankMatch = not val and not torValues
+      
+      if not isBlankMatch and not val in torValues:
+        # converts corrections to reader friedly size values
+        displayValues = torValues
+        if valueType == SIZE_VALUE:
+          displayValues = [uiTools.getSizeLabel(int(val)) for val in torValues]
+        elif valueType == TIME_VALUE:
+          displayValues = [uiTools.getTimeLabel(int(val)) for val in torValues]
+        
+        issuesFound.append((lineNumber, VAL_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))
+  
+  return issuesFound
+
+def _parseConfValue(confArg):
+  """
+  Converts size or time values to their lowest units (bytes or seconds) which
+  is what GETCONF calls provide. The returned is a tuple of the value and unit
+  type.
+  
+  Arguments:
+    confArg - torrc argument
+  """
+  
+  if confArg.count(" ") == 1:
+    val, unit = confArg.lower().split(" ", 1)
+    if not val.isdigit(): return confArg, UNRECOGNIZED
+    mult, multType = _getUnitType(unit)
+    
+    if mult != None:
+      return str(int(val) * mult), multType
+  
+  return confArg, UNRECOGNIZED
+
+def _getUnitType(unit):
+  """
+  Provides the type and multiplier for an argument's unit. The multiplier is
+  None if the unit isn't recognized.
+  
+  Arguments:
+    unit - string representation of a unit
+  """
+  
+  for label in SIZE_MULT:
+    if unit in CONFIG["torrc.label.size." + label]:
+      return SIZE_MULT[label], SIZE_VALUE
+  
+  for label in TIME_MULT:
+    if unit in CONFIG["torrc.label.time." + label]:
+      return TIME_MULT[label], TIME_VALUE
+  
+  return None, UNRECOGNIZED
+
+def _stripComments(contents):
+  """
+  Removes comments and extra whitespace from the given torrc contents.
+  
+  Arguments:
+    contents - torrc contents
+  """
+  
+  strippedContents = []
+  for line in contents:
+    if line and "#" in line: line = line[:line.find("#")]
+    strippedContents.append(line.strip())
+  return strippedContents
+
+class Torrc():
+  """
+  Wrapper for the torrc. All getters provide None if the contents are unloaded.
+  """
+  
+  def __init__(self):
+    self.contents = None
+    self.configLocation = None
+    self.valsLock = threading.RLock()
+    
+    # cached results for the current contents
+    self.displayableContents = None
+    self.strippedContents = None
+    self.corrections = None
+  
+  def load(self):
+    """
+    Loads or reloads the torrc contents, raising an IOError if there's a
+    problem.
+    """
+    
+    self.valsLock.acquire()
+    
+    # clears contents and caches
+    self.contents, self.configLocation = None, None
+    self.displayableContents = None
+    self.strippedContents = None
+    self.corrections = None
+    
+    try:
+      self.configLocation = getConfigLocation()
+      configFile = open(self.configLocation, "r")
+      self.contents = configFile.readlines()
+      configFile.close()
+    except IOError, exc:
+      self.valsLock.release()
+      raise exc
+    
+    self.valsLock.release()
+  
+  def isLoaded(self):
+    """
+    Provides true if there's loaded contents, false otherwise.
+    """
+    
+    return self.contents != None
+  
+  def getConfigLocation(self):
+    """
+    Provides the location of the loaded configuration contents. This may be
+    available, even if the torrc failed to be loaded.
+    """
+    
+    return self.configLocation
+  
+  def getContents(self):
+    """
+    Provides the contents of the configuration file.
+    """
+    
+    self.valsLock.acquire()
+    returnVal = list(self.contents) if self.contents else None
+    self.valsLock.release()
+    return returnVal
+  
+  def getDisplayContents(self, strip = False):
+    """
+    Provides the contents of the configuration file, formatted in a rendering
+    frindly fashion:
+    - Tabs print as three spaces. Keeping them as tabs is problematic for
+      layouts since it's counted as a single character, but occupies several
+      cells.
+    - Strips control and unprintable characters.
+    
+    Arguments:
+      strip - removes comments and extra whitespace if true
+    """
+    
+    self.valsLock.acquire()
+    
+    if not self.isLoaded(): returnVal = None
+    else:
+      if self.displayableContents == None:
+        # restricts contents to displayable characters
+        self.displayableContents = []
+        
+        for lineNum in range(len(self.contents)):
+          lineText = self.contents[lineNum]
+          lineText = lineText.replace("\t", "   ")
+          lineText = uiTools.getPrintable(lineText)
+          self.displayableContents.append(lineText)
+      
+      if strip:
+        if self.strippedContents == None:
+          self.strippedContents = _stripComments(self.displayableContents)
+        
+        returnVal = list(self.strippedContents)
+      else: returnVal = list(self.displayableContents)
+    
+    self.valsLock.release()
+    return returnVal
+  
+  def getCorrections(self):
+    """
+    Performs validation on the loaded contents and provides back the
+    corrections. If validation is disabled then this won't provide any
+    results.
+    """
+    
+    self.valsLock.acquire()
+    
+    if not self.isLoaded(): returnVal = None
+    elif not CONFIG["features.torrc.validate"]: returnVal = {}
+    else:
+      if self.corrections == None:
+        self.corrections = validate(self.contents)
+      
+      returnVal = list(self.corrections)
+    
+    self.valsLock.release()
+    return returnVal
+  
+  def getLock(self):
+    """
+    Provides the lock governing concurrent access to the contents.
+    """
+    
+    return self.valsLock
+
+def _testConfigDescriptions():
+  """
+  Tester for the loadOptionDescriptions function, fetching the man page
+  contents and dumping its parsed results.
+  """
+  
+  loadOptionDescriptions()
+  sortedOptions = CONFIG_DESCRIPTIONS.keys()
+  sortedOptions.sort()
+  
+  for i in range(len(sortedOptions)):
+    option = sortedOptions[i]
+    argument, description = getConfigDescription(option)
+    optLabel = "OPTION: \"%s\"" % option
+    argLabel = "ARGUMENT: \"%s\"" % argument
+    
+    print "     %-45s %s" % (optLabel, argLabel)
+    print "\"%s\"" % description
+    if i != len(sortedOptions) - 1: print "-" * 80
+

Modified: arm/release/src/util/torTools.py
===================================================================
--- arm/release/src/util/torTools.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/util/torTools.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -46,10 +46,12 @@
 
 TOR_CTL_CLOSE_MSG = "Tor closed control connection. Exiting event thread."
 UNKNOWN = "UNKNOWN" # value used by cached information if undefined
-CONFIG = {"features.pathPrefix": "",
+CONFIG = {"torrc.map": {},
+          "features.pathPrefix": "",
           "log.torCtlPortClosed": log.NOTICE,
           "log.torGetInfo": log.DEBUG,
           "log.torGetConf": log.DEBUG,
+          "log.torSetConf": log.INFO,
           "log.torPrefixPathInvalid": log.NOTICE}
 
 # events used for controller functionality:
@@ -152,7 +154,7 @@
 
 def getConn():
   """
-  Singleton constructor for a Controller. Be aware that this start
+  Singleton constructor for a Controller. Be aware that this starts as being
   uninitialized, needing a TorCtl instance before it's fully functional.
   """
   
@@ -180,9 +182,13 @@
     self._statusTime = 0                # unix time-stamp for the duration of the status
     self.lastHeartbeat = 0              # time of the last tor event
     
-    # cached getInfo parameters (None if unset or possibly changed)
+    # cached GETINFO parameters (None if unset or possibly changed)
     self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
     
+    # cached GETCONF parameters, entries consisting of:
+    # (option, fetch_type) => value
+    self._cachedConf = {}
+    
     # directs TorCtl to notify us of events
     TorUtil.logger = self
     TorUtil.loglevel = "DEBUG"
@@ -319,9 +325,6 @@
     if not suppressExc and raisedExc: raise raisedExc
     else: return result
   
-  # TODO: This could have client side caching if there were events to indicate
-  # SETCONF events. See:
-  # https://trac.torproject.org/projects/tor/ticket/1692
   def getOption(self, param, default = None, multiple = False, suppressExc = True):
     """
     Queries the control port for the given configuration option, providing the
@@ -332,29 +335,96 @@
     Arguments:
       param       - configuration option to be queried
       default     - result if the query fails and exception's suppressed
-      multiple    - provides a list of results if true, otherwise this just
-                    returns the first value
+      multiple    - provides a list with all returned values if true, otherwise
+                    this just provides the first result
       suppressExc - suppresses lookup errors (returning the default) if true,
                     otherwise this raises the original exception
     """
     
+    fetchType = "list" if multiple else "str"
+    
+    if param in CONFIG["torrc.map"]:
+      # This is among the options fetched via a special command. The results
+      # are a set of values that (hopefully) contain the one we were
+      # requesting.
+      configMappings = self._getOption(CONFIG["torrc.map"][param], default, "map", suppressExc)
+      if param in configMappings:
+        if fetchType == "list": return configMappings[param]
+        else: return configMappings[param][0]
+      else: return default
+    else:
+      return self._getOption(param, default, fetchType, suppressExc)
+  
+  def getOptionMap(self, param, default = None, suppressExc = True):
+    """
+    Queries the control port for the given configuration option, providing back
+    a mapping of config options to a list of the values returned.
+    
+    There's three use cases for GETCONF:
+    - a single value is provided
+    - multiple values are provided for the option queried
+    - a set of options that weren't necessarily requested are returned (for
+      instance querying HiddenServiceOptions gives HiddenServiceDir,
+      HiddenServicePort, etc)
+    
+    The vast majority of the options fall into the first two catagories, in
+    which case calling getOption is sufficient. However, for the special
+    options that give a set of values this provides back the full response. As
+    of tor version 0.2.1.25 HiddenServiceOptions was the only option like this.
+    
+    The getOption function accounts for these special mappings, and the only
+    advantage to this funtion is that it provides all related values in a
+    single response.
+    
+    Arguments:
+      param       - configuration option to be queried
+      default     - result if the query fails and exception's suppressed
+      suppressExc - suppresses lookup errors (returning the default) if true,
+                    otherwise this raises the original exception
+    """
+    
+    return self._getOption(param, default, "map", suppressExc)
+  
+  # TODO: cache isn't updated (or invalidated) during SETCONF events:
+  # https://trac.torproject.org/projects/tor/ticket/1692
+  def _getOption(self, param, default, fetchType, suppressExc):
+    if not fetchType in ("str", "list", "map"):
+      msg = "BUG: unrecognized fetchType in torTools._getOption (%s)" % fetchType
+      log.log(log.ERR, msg)
+      return default
+    
     self.connLock.acquire()
+    startTime, raisedExc, isFromCache = time.time(), None, False
+    result = {} if fetchType == "map" else []
     
-    startTime = time.time()
-    result, raisedExc = [], None
     if self.isAlive():
-      try:
-        if multiple:
-          for key, value in self.conn.get_option(param):
-            if value != None: result.append(value)
-        else:
-          getConfVal = self.conn.get_option(param)[0][1]
-          if getConfVal != None: result = getConfVal
-      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
-        if type(exc) == TorCtl.TorCtlClosed: self.close()
-        result, raisedExc = default, exc
+      if (param, fetchType) in self._cachedConf:
+        isFromCache = True
+        result = self._cachedConf[(param, fetchType)]
+      else:
+        try:
+          if fetchType == "str":
+            getConfVal = self.conn.get_option(param)[0][1]
+            if getConfVal != None: result = getConfVal
+          else:
+            for key, value in self.conn.get_option(param):
+              if value != None:
+                if fetchType == "list": result.append(value)
+                elif fetchType == "map":
+                  if key in result: result.append(value)
+                  else: result[key] = [value]
+        except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
+          if type(exc) == TorCtl.TorCtlClosed: self.close()
+          result, raisedExc = default, exc
     
-    msg = "GETCONF %s (runtime: %0.4f)" % (param, time.time() - startTime)
+    if not isFromCache and result:
+      cacheValue = result
+      if fetchType == "list": cacheValue = list(result)
+      elif fetchType == "map": cacheValue = dict(result)
+      self._cachedConf[(param, 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)
     
     self.connLock.release()
@@ -363,6 +433,59 @@
     elif result == []: return default
     else: return result
   
+  def setOption(self, param, value):
+    """
+    Issues a SETCONF to set the given option/value pair. An exeptions raised
+    if it fails to be set.
+    
+    Arguments:
+      param - configuration option to be set
+      value - value to set the parameter to (this can be either a string or a
+              list of strings)
+    """
+    
+    isMultiple = isinstance(value, list) or isinstance(value, tuple)
+    self.connLock.acquire()
+    
+    startTime, raisedExc = time.time(), None
+    if self.isAlive():
+      try:
+        if isMultiple: self.conn.set_options([(param, val) for val in value])
+        else: self.conn.set_option(param, value)
+        
+        # flushing cached values (needed until we can detect SETCONF calls)
+        for fetchType in ("str", "list", "map"):
+          entry = (param, fetchType)
+          
+          if entry in self._cachedConf:
+            del self._cachedConf[entry]
+      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
+        if type(exc) == TorCtl.TorCtlClosed: self.close()
+        elif type(exc) == TorCtl.ErrorReply:
+          excStr = str(exc)
+          if excStr.startswith("513 Unacceptable option value: "):
+            # crops off the common error prefix
+            excStr = excStr[31:]
+            
+            # Truncates messages like:
+            # Value 'BandwidthRate la de da' is malformed or out of bounds.
+            # to: Value 'la de da' is malformed or out of bounds.
+            if excStr.startswith("Value '"):
+              excStr = excStr.replace("%s " % param, "", 1)
+            
+            exc = TorCtl.ErrorReply(excStr)
+        
+        raisedExc = exc
+    
+    self.connLock.release()
+    
+    setCall = "%s %s" % (param, ", ".join(value) if isMultiple else value)
+    excLabel = "failed: \"%s\", " % raisedExc if raisedExc else ""
+    msg = "SETCONF %s (%sruntime: %0.4f)" % (setCall.strip(), excLabel, time.time() - startTime)
+    log.log(CONFIG["log.torSetConf"], msg)
+    
+    if raisedExc: raise raisedExc
+  
   def getMyNetworkStatus(self, default = None):
     """
     Provides the network status entry for this relay if available. This is
@@ -639,6 +762,7 @@
         try:
           self.conn.send_signal("RELOAD")
           self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
+          self._cachedConf = {}
         except Exception, exc:
           # new torrc parameters caused an error (tor's likely shut down)
           # BUG: this doesn't work - torrc errors still cause TorCtl to crash... :(
@@ -683,6 +807,7 @@
             else: raise IOError("failed silently")
           
           self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
+          self._cachedConf = {}
         except IOError, exc:
           raisedException = exc
     
@@ -876,8 +1001,9 @@
       eventType - enum representing tor's new status
     """
     
-    # resets cached getInfo parameters
+    # resets cached GETINFO and GETCONF parameters
     self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
+    self._cachedConf = {}
     
     # gives a notice that the control port has closed
     if eventType == TOR_CLOSED:

Modified: arm/release/src/util/uiTools.py
===================================================================
--- arm/release/src/util/uiTools.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/util/uiTools.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -8,6 +8,7 @@
 import sys
 import curses
 
+from curses.ascii import isprint
 from util import log
 
 # colors curses can handle
@@ -28,8 +29,8 @@
 SIZE_UNITS_BYTES = [(1125899906842624.0, " PB", " Petabyte"), (1099511627776.0, " TB", " Terabyte"),
                     (1073741824.0, " GB", " Gigabyte"),       (1048576.0, " MB", " Megabyte"),
                     (1024.0, " KB", " Kilobyte"),             (1.0, " B", " Byte")]
-TIME_UNITS = [(86400.0, "d", " day"),                   (3600.0, "h", " hour"),
-              (60.0, "m", " minute"),                   (1.0, "s", " second")]
+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)
 SCROLL_KEYS = (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE, curses.KEY_HOME, curses.KEY_END)
@@ -39,6 +40,66 @@
 def loadConfig(config):
   config.update(CONFIG)
 
+def demoGlyphs():
+  """
+  Displays all ACS options with their corresponding representation. These are
+  undocumented in the pydocs. For more information see the following man page:
+  http://www.mkssoftware.com/docs/man5/terminfo.5.asp
+  """
+  
+  try: curses.wrapper(_showGlyphs)
+  except KeyboardInterrupt: pass # quit
+
+def _showGlyphs(stdscr):
+  """
+  Renders a chart with the ACS glyphs.
+  """
+  
+  # allows things like semi-transparent backgrounds
+  try: curses.use_default_colors()
+  except curses.error: pass
+  
+  # attempts to make the cursor invisible
+  try: curses.curs_set(0)
+  except curses.error: pass
+  
+  acsOptions = [item for item in curses.__dict__.items() if item[0].startswith("ACS_")]
+  acsOptions.sort(key=lambda i: (i[1])) # order by character codes
+  
+  # displays a chart with all the glyphs and their representations
+  height, width = stdscr.getmaxyx()
+  if width < 30: return # not enough room to show a column
+  columns = width / 30
+  
+  # display title
+  stdscr.addstr(0, 0, "Curses Glyphs:", curses.A_STANDOUT)
+  
+  x, y = 0, 1
+  while acsOptions:
+    name, keycode = acsOptions.pop(0)
+    stdscr.addstr(y, x * 30, "%s (%i)" % (name, keycode))
+    stdscr.addch(y, (x * 30) + 25, keycode)
+    
+    x += 1
+    if x >= columns:
+      x, y = 0, y + 1
+      if y >= height: break
+  
+  stdscr.getch() # quit on keyboard input
+
+def getPrintable(line, keepNewlines = True):
+  """
+  Provides the line back with non-printable characters stripped.
+  
+  Arguments:
+    line          - string to be processed
+    stripNewlines - retains newlines if true, stripped otherwise
+  """
+  
+  line = line.replace('\xc2', "'")
+  line = "".join([char for char in line if (isprint(char) or (keepNewlines and char == "\n"))])
+  return line
+
 def getColor(color):
   """
   Provides attribute corresponding to a given text color. Supported colors
@@ -61,7 +122,9 @@
   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
   isn't room for even a truncated single word (or one word plus the ellipse if
-  including those) then this provides an empty string. Examples:
+  including those) then this provides an empty string. If a cropped string ends
+  with a comma or period then it's stripped (unless we're providing the
+  remainder back). Examples:
   
   cropStr("This is a looooong message", 17)
   "This is a looo..."
@@ -86,26 +149,28 @@
                    cropped portion of the message
   """
   
-  if minWordLen == None: minWordLen = sys.maxint
-  minWordLen = max(0, minWordLen)
-  minCrop = max(0, minCrop)
-  
   # checks if there's room for the whole message
   if len(msg) <= size:
     if getRemainder: return (msg, "")
     else: return msg
   
+  # avoids negative input
+  size = max(0, size)
+  if minWordLen != None: minWordLen = max(0, minWordLen)
+  minCrop = max(0, minCrop)
+  
   # 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: minWordLen += 1
+  elif endType == END_WITH_HYPHEN and minWordLen != None: minWordLen += 1
   
   # checks if there isn't the minimum space needed to include anything
-  if size <= minWordLen:
+  lastWordbreak = msg.rfind(" ", 0, size + 1)
+  if (minWordLen != None and size < minWordLen) or (minWordLen == None and lastWordbreak < 1):
     if getRemainder: return ("", msg)
     else: return ""
   
-  lastWordbreak = msg.rfind(" ", 0, size + 1)
+  if minWordLen == None: minWordLen = sys.maxint
   includeCrop = size - lastWordbreak - 1 >= minWordLen
   
   # if there's a max crop size then make sure we're cropping at least that many characters
@@ -122,7 +187,7 @@
   else: returnMsg, remainder = msg[:lastWordbreak], msg[lastWordbreak:]
   
   # if this is ending with a comma or period then strip it off
-  if returnMsg[-1] in (",", "."): returnMsg = returnMsg[:-1]
+  if not getRemainder and returnMsg[-1] in (",", "."): returnMsg = returnMsg[:-1]
   
   if endType == END_WITH_ELLIPSE: returnMsg += "..."
   
@@ -137,7 +202,7 @@
   
   return key in SCROLL_KEYS
 
-def getScrollPosition(key, position, pageHeight, contentHeight):
+def getScrollPosition(key, position, pageHeight, contentHeight, isCursor = False):
   """
   Parses navigation keys, providing the new scroll possition the panel should
   use. Position is always between zero and (contentHeight - pageHeight). This
@@ -154,19 +219,21 @@
     position      - starting position
     pageHeight    - size of a single screen's worth of content
     contentHeight - total lines of content that can be scrolled
+    isCursor      - tracks a cursor position rather than scroll if true
   """
   
   if isScrollKey(key):
     shift = 0
     if key == curses.KEY_UP: shift = -1
     elif key == curses.KEY_DOWN: shift = 1
-    elif key == curses.KEY_PPAGE: shift = -pageHeight
-    elif key == curses.KEY_NPAGE: shift = pageHeight
+    elif key == curses.KEY_PPAGE: shift = -pageHeight + 1 if isCursor else -pageHeight
+    elif key == curses.KEY_NPAGE: shift = pageHeight - 1 if isCursor else pageHeight
     elif key == curses.KEY_HOME: shift = -contentHeight
     elif key == curses.KEY_END: shift = contentHeight
     
     # returns the shift, restricted to valid bounds
-    return max(0, min(position + shift, contentHeight - pageHeight))
+    maxLoc = contentHeight - 1 if isCursor else contentHeight - pageHeight
+    return max(0, min(position + shift, maxLoc))
   else: return position
 
 def getSizeLabel(bytes, decimal = 0, isLong = False, isBytes=True):
@@ -238,6 +305,90 @@
   
   return timeLabels
 
+class Scroller:
+  """
+  Tracks the scrolling position when there might be a visible cursor. This
+  expects that there is a single line displayed per an entry in the contents.
+  """
+  
+  def __init__(self, isCursorEnabled):
+    self.scrollLoc, self.cursorLoc = 0, 0
+    self.cursorSelection = None
+    self.isCursorEnabled = isCursorEnabled
+  
+  def getScrollLoc(self, content, pageHeight):
+    """
+    Provides the scrolling location, taking into account its cursor's location
+    content size, and page height.
+    
+    Arguments:
+      content    - displayed content
+      pageHeight - height of the display area for the content
+    """
+    
+    if content and pageHeight:
+      self.scrollLoc = max(0, min(self.scrollLoc, len(content) - pageHeight + 1))
+      
+      if self.isCursorEnabled:
+        self.getCursorSelection(content) # resets the cursor location
+        
+        if self.cursorLoc < self.scrollLoc:
+          self.scrollLoc = self.cursorLoc
+        elif self.cursorLoc > self.scrollLoc + pageHeight - 1:
+          self.scrollLoc = self.cursorLoc - pageHeight + 1
+    
+    return self.scrollLoc
+  
+  def getCursorSelection(self, content):
+    """
+    Provides the selected item in the content. This is the same entry until
+    the cursor moves or it's no longer available (in which case it moves on to
+    the next entry).
+    
+    Arguments:
+      content - displayed content
+    """
+    
+    # TODO: needs to handle duplicate entries when using this for the
+    # connection panel
+    
+    if not self.isCursorEnabled: return None
+    elif not content:
+      self.cursorLoc, self.cursorSelection = 0, None
+      return None
+    
+    self.cursorLoc = min(self.cursorLoc, len(content) - 1)
+    if self.cursorSelection != None and self.cursorSelection in content:
+      # moves cursor location to track the selection
+      self.cursorLoc = content.index(self.cursorSelection)
+    else:
+      # select the next closest entry
+      self.cursorSelection = content[self.cursorLoc]
+    
+    return self.cursorSelection
+  
+  def handleKey(self, key, content, pageHeight):
+    """
+    Moves either the scroll or cursor according to the given input.
+    
+    Arguments:
+      key        - key code of user input
+      content    - displayed content
+      pageHeight - height of the display area for the content
+    """
+    
+    if self.isCursorEnabled:
+      self.getCursorSelection(content) # resets the cursor location
+      startLoc = self.cursorLoc
+    else: startLoc = self.scrollLoc
+    
+    newLoc = getScrollPosition(key, startLoc, pageHeight, len(content), self.isCursorEnabled)
+    if startLoc != newLoc:
+      if self.isCursorEnabled: self.cursorSelection = content[newLoc]
+      else: self.scrollLoc = newLoc
+      return True
+    else: return False
+
 def _getLabel(units, count, decimal, isLong):
   """
   Provides label corresponding to units of the highest significance in the

Modified: arm/release/src/version.py
===================================================================
--- arm/release/src/version.py	2010-11-28 10:55:19 UTC (rev 23872)
+++ arm/release/src/version.py	2010-11-28 10:58:57 UTC (rev 23873)
@@ -2,6 +2,6 @@
 Provides arm's version and release date.
 """
 
-VERSION = '1.3.7-1'
-LAST_MODIFIED = "October 7, 2010"
+VERSION = '1.4.0'
+LAST_MODIFIED = "November 27, 2010"