[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"