[or-cvs] r23010: {arm} added: setup script for installing to '/usr/local/arm'. This (in arm/trunk: . src src/interface src/interface/graphing src/util)

Damian Johnson atagar1 at gmail.com
Sat Aug 21 20:38:47 UTC 2010


Author: atagar
Date: 2010-08-21 20:38:47 +0000 (Sat, 21 Aug 2010)
New Revision: 23010

Added:
   arm/trunk/setup.cfg
   arm/trunk/setup.py
   arm/trunk/src/
   arm/trunk/src/TorCtl/
   arm/trunk/src/__init__.py
   arm/trunk/src/interface/
   arm/trunk/src/interface/confPanel.py
   arm/trunk/src/interface/controller.py
   arm/trunk/src/interface/graphing/bandwidthStats.py
   arm/trunk/src/interface/graphing/graphPanel.py
   arm/trunk/src/interface/graphing/psStats.py
   arm/trunk/src/interface/logPanel.py
   arm/trunk/src/prereq.py
   arm/trunk/src/starter.py
   arm/trunk/src/util/
   arm/trunk/src/util/torTools.py
Removed:
   arm/trunk/init/
   arm/trunk/interface/
   arm/trunk/src/interface/confPanel.py
   arm/trunk/src/interface/controller.py
   arm/trunk/src/interface/graphing/bandwidthStats.py
   arm/trunk/src/interface/graphing/graphPanel.py
   arm/trunk/src/interface/graphing/psStats.py
   arm/trunk/src/interface/logPanel.py
   arm/trunk/src/util/torTools.py
   arm/trunk/util/
Modified:
   arm/trunk/README
   arm/trunk/arm
Log:
added: setup script for installing to '/usr/local/arm'. This is not yet functional since it doesn't add a '/usr/local/bin' entry yet.
- changing the layout to accomidate distutils
- removing the consensus tracker (this will be added to the tor tools when the following is resolved:
    https://trac.torproject.org/projects/tor/ticket/1737



Modified: arm/trunk/README
===================================================================
--- arm/trunk/README	2010-08-21 15:33:23 UTC (rev 23009)
+++ arm/trunk/README	2010-08-21 20:38:47 UTC (rev 23010)
@@ -94,39 +94,39 @@
   README       - um... guess you figured this one out
   TODO         - known issues, future plans, etc
   
-  init/
+  src/arm/
     __init__.py
     starter.py - parses and validates commandline parameters
     prereq.py  - checks python version and for required packages
-  
-  interface/
-    graphing/
+    
+    interface/
+      graphing/
+        __init__.py
+        graphPanel.py     - (page 1) presents graphs for data instances
+        bandwidthStats.py - tracks tor bandwidth usage
+        psStats.py        - tracks system information (such as cpu/memory usage)
+        connStats.py      - tracks number of tor connections
+      
       __init__.py
-      graphPanel.py     - (page 1) presents graphs for data instances
-      bandwidthStats.py - tracks tor bandwidth usage
-      psStats.py        - tracks system information (such as cpu/memory usage)
-      connStats.py      - tracks number of tor connections
+      controller.py          - main display loop, handling input and layout
+      headerPanel.py         - top of all pages, providing general information
+      
+      logPanel.py            - (page 1) displays tor, arm, and torctl events
+      fileDescriptorPopup.py - (popup) displays file descriptors used by tor
+      
+      connPanel.py           - (page 2) displays information on tor connections
+      descriptorPopup.py     - (popup) displays connection descriptor data
+      
+      confPanel.py           - (page 3) displays torrc and performs validation
     
-    __init__.py
-    controller.py          - main display loop, handling input and layout
-    headerPanel.py         - top of all pages, providing general information
-    
-    logPanel.py            - (page 1) displays tor, arm, and torctl events
-    fileDescriptorPopup.py - (popup) displays file descriptors used by tor
-    
-    connPanel.py           - (page 2) displays information on tor connections
-    descriptorPopup.py     - (popup) displays connection descriptor data
-    
-    confPanel.py           - (page 3) displays torrc and performs validation
-  
-  util/
-    __init__.py
-    conf.py        - loading and persistence for user configuration
-    connections.py - service providing periodic connection lookups
-    hostnames.py   - service providing nonblocking reverse dns lookups
-    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
-    torTools.py    - TorCtl wrapper, providing caching and derived information
-    uiTools.py     - helper functions for presenting the user interface
+    util/
+      __init__.py
+      conf.py        - loading and persistence for user configuration
+      connections.py - service providing periodic connection lookups
+      hostnames.py   - service providing nonblocking reverse dns lookups
+      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
+      torTools.py    - TorCtl wrapper, providing caching and derived information
+      uiTools.py     - helper functions for presenting the user interface
 

Modified: arm/trunk/arm
===================================================================
--- arm/trunk/arm	2010-08-21 15:33:23 UTC (rev 23009)
+++ arm/trunk/arm	2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,8 +1,8 @@
 #!/bin/sh
-python init/prereq.py
+python src/arm/prereq.py
 
 if [ $? = 0 ]
 then
-  python -W ignore::DeprecationWarning init/starter.py $*
+  python -W ignore::DeprecationWarning src/arm/starter.py $*
 fi
 

Added: arm/trunk/setup.cfg
===================================================================
--- arm/trunk/setup.cfg	                        (rev 0)
+++ arm/trunk/setup.cfg	2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,2 @@
+[install]
+install-purelib=/usr/local

Added: arm/trunk/setup.py
===================================================================
--- arm/trunk/setup.py	                        (rev 0)
+++ arm/trunk/setup.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+
+from distutils.core import setup
+
+setup(name='arm',
+      version='1.3.6_dev',
+      description='Terminal tor status monitor',
+      license='GPL v3',
+      author='Damian Johnson',
+      author_email='atagar at torproject.org',
+      url='http://www.atagar.com/arm/',
+      packages=['arm', 'arm.interface', 'arm.util', 'arm.TorCtl'],
+      package_dir={'arm': 'src'},
+     )
+

Added: arm/trunk/src/__init__.py
===================================================================
--- arm/trunk/src/__init__.py	                        (rev 0)
+++ arm/trunk/src/__init__.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,6 @@
+"""
+Scripts involved in validating user input, system state, and initializing arm.
+"""
+
+__all__ = ["starter", "prereq"]
+

Deleted: arm/trunk/src/interface/confPanel.py
===================================================================
--- arm/trunk/interface/confPanel.py	2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/interface/confPanel.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -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, 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: remove if/when fixed in tor
-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",
-                "HashedControlPassword": "__HashedControlSessionPassword",
-                "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(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):
-    pageHeight = self.getPreferredSize()[0] - 1
-    if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
-    elif key == curses.KEY_DOWN: self.scroll = max(0, min(self.scroll + 1, len(self.confContents) - pageHeight))
-    elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - pageHeight, 0)
-    elif key == curses.KEY_NPAGE: self.scroll = max(0, min(self.scroll + pageHeight, len(self.confContents) - pageHeight))
-    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/trunk/src/interface/confPanel.py (from rev 22994, arm/trunk/interface/confPanel.py)
===================================================================
--- arm/trunk/src/interface/confPanel.py	                        (rev 0)
+++ arm/trunk/src/interface/confPanel.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,293 @@
+#!/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, 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: remove if/when fixed in tor
+# 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(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):
+    pageHeight = self.getPreferredSize()[0] - 1
+    if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
+    elif key == curses.KEY_DOWN: self.scroll = max(0, min(self.scroll + 1, len(self.confContents) - pageHeight))
+    elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - pageHeight, 0)
+    elif key == curses.KEY_NPAGE: self.scroll = max(0, min(self.scroll + pageHeight, len(self.confContents) - pageHeight))
+    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
+

Deleted: arm/trunk/src/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/interface/controller.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,1294 +0,0 @@
-#!/usr/bin/env python
-# controller.py -- arm interface (curses monitor for relay status)
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-"""
-Curses (terminal) interface for the arm relay status monitor.
-"""
-
-import re
-import math
-import time
-import curses
-import socket
-from TorCtl import TorCtl
-from TorCtl import TorUtil
-
-import headerPanel
-import graphing.graphPanel
-import logPanel
-import connPanel
-import confPanel
-import descriptorPopup
-import fileDescriptorPopup
-
-from util import conf, log, connections, hostnames, panel, sysTools, torTools, uiTools
-import graphing.bandwidthStats
-import graphing.connStats
-import graphing.psStats
-
-CONFIRM_QUIT = True
-REFRESH_RATE = 5        # seconds between redrawing screen
-MAX_REGEX_FILTERS = 5   # maximum number of previous regex filters that'll be remembered
-
-# enums for message in control label
-CTL_HELP, CTL_PAUSED = range(2)
-
-# panel order per page
-PAGE_S = ["header", "control", "popup"] # sticky (ie, always available) page
-PAGES = [
-  ["graph", "log"],
-  ["conn"],
-  ["torrc"]]
-PAUSEABLE = ["header", "graph", "log", "conn"]
-
-CONFIG = {"logging.rate.refreshRate": 5, "features.graph.type": 1, "features.graph.bw.prepopulate": True, "log.refreshRate": log.DEBUG, "log.configEntryUndefined": log.NOTICE}
-
-class ControlPanel(panel.Panel):
-  """ Draws single line label for interface controls. """
-  
-  def __init__(self, stdscr, isBlindMode):
-    panel.Panel.__init__(self, stdscr, "control", 0, 1)
-    self.msgText = CTL_HELP           # message text to be displyed
-    self.msgAttr = curses.A_NORMAL    # formatting attributes
-    self.page = 1                     # page number currently being displayed
-    self.resolvingCounter = -1        # count of resolver when starting (-1 if we aren't working on a batch)
-    self.isBlindMode = isBlindMode
-  
-  def setMsg(self, msgText, msgAttr=curses.A_NORMAL):
-    """
-    Sets the message and display attributes. If msgType matches CTL_HELP or
-    CTL_PAUSED then uses the default message for those statuses.
-    """
-    
-    self.msgText = msgText
-    self.msgAttr = msgAttr
-  
-  def draw(self, subwindow, width, height):
-    msgText = self.msgText
-    msgAttr = self.msgAttr
-    barTab = 2                # space between msgText and progress bar
-    barWidthMax = 40          # max width to progress bar
-    barWidth = -1             # space between "[ ]" in progress bar (not visible if -1)
-    barProgress = 0           # cells to fill
-    
-    if msgText == CTL_HELP:
-      msgAttr = curses.A_NORMAL
-      
-      if self.resolvingCounter != -1:
-        if hostnames.isPaused() or not hostnames.isResolving():
-          # done resolving dns batch
-          self.resolvingCounter = -1
-          curses.halfdelay(REFRESH_RATE * 10) # revert to normal refresh rate
-        else:
-          batchSize = hostnames.getRequestCount() - self.resolvingCounter
-          entryCount = batchSize - hostnames.getPendingCount()
-          if batchSize > 0: progress = 100 * entryCount / batchSize
-          else: progress = 0
-          
-          additive = "or l " if self.page == 2 else ""
-          batchSizeDigits = int(math.log10(batchSize)) + 1
-          entryCountLabel = ("%%%ii" % batchSizeDigits) % entryCount
-          #msgText = "Resolving hostnames (%i / %i, %i%%) - press esc %sto cancel" % (entryCount, batchSize, progress, additive)
-          msgText = "Resolving hostnames (press esc %sto cancel) - %s / %i, %2i%%" % (additive, entryCountLabel, batchSize, progress)
-          
-          barWidth = min(barWidthMax, width - len(msgText) - 3 - barTab)
-          barProgress = barWidth * entryCount / batchSize
-      
-      if self.resolvingCounter == -1:
-        currentPage = self.page
-        pageCount = len(PAGES)
-        
-        if self.isBlindMode:
-          if currentPage >= 2: currentPage -= 1
-          pageCount -= 1
-        
-        msgText = "page %i / %i - q: quit, p: pause, h: page help" % (currentPage, pageCount)
-    elif msgText == CTL_PAUSED:
-      msgText = "Paused"
-      msgAttr = curses.A_STANDOUT
-    
-    self.addstr(0, 0, msgText, msgAttr)
-    if barWidth > -1:
-      xLoc = len(msgText) + barTab
-      self.addstr(0, xLoc, "[", curses.A_BOLD)
-      self.addstr(0, xLoc + 1, " " * barProgress, curses.A_STANDOUT | uiTools.getColor("red"))
-      self.addstr(0, xLoc + barWidth + 1, "]", curses.A_BOLD)
-
-class Popup(panel.Panel):
-  """
-  Temporarily providing old panel methods until permanent workaround for popup
-  can be derrived (this passive drawing method is horrible - I'll need to
-  provide a version using the more active repaint design later in the
-  revision).
-  """
-  
-  def __init__(self, stdscr, height):
-    panel.Panel.__init__(self, stdscr, "popup", 0, height)
-  
-  # The following methods are to emulate old panel functionality (this was the
-  # only implementations to use these methods and will require a complete
-  # rewrite when refactoring gets here)
-  def clear(self):
-    if self.win:
-      self.isDisplaced = self.top > self.win.getparyx()[0]
-      if not self.isDisplaced: self.win.erase()
-  
-  def refresh(self):
-    if self.win and not self.isDisplaced: self.win.refresh()
-  
-  def recreate(self, stdscr, newWidth=-1, newTop=None):
-    self.setParent(stdscr)
-    self.setWidth(newWidth)
-    if newTop != None: self.setTop(newTop)
-    
-    newHeight, newWidth = self.getPreferredSize()
-    if newHeight > 0:
-      self.win = self.parent.subwin(newHeight, newWidth, self.top, 0)
-    elif self.win == None:
-      # don't want to leave the window as none (in very edge cases could cause
-      # problems) - rather, create a displaced instance
-      self.win = self.parent.subwin(1, newWidth, 0, 0)
-    
-    self.maxY, self.maxX = self.win.getmaxyx()
-
-def addstr_wrap(panel, y, x, text, formatting, startX = 0, endX = -1, maxY = -1):
-  """
-  Writes text with word wrapping, returning the ending y/x coordinate.
-  y: starting write line
-  x: column offset from startX
-  text / formatting: content to be written
-  startX / endX: column bounds in which text may be written
-  """
-  
-  # moved out of panel (trying not to polute new code!)
-  # TODO: unpleaseantly complex usage - replace with something else when
-  # rewriting confPanel and descriptorPopup (the only places this is used)
-  if not text: return (y, x)          # nothing to write
-  if endX == -1: endX = panel.maxX     # defaults to writing to end of panel
-  if maxY == -1: maxY = panel.maxY + 1 # defaults to writing to bottom of panel
-  lineWidth = endX - startX           # room for text
-  while True:
-    if len(text) > lineWidth - x - 1:
-      chunkSize = text.rfind(" ", 0, lineWidth - x)
-      writeText = text[:chunkSize]
-      text = text[chunkSize:].strip()
-      
-      panel.addstr(y, x + startX, writeText, formatting)
-      y, x = y + 1, 0
-      if y >= maxY: return (y, x)
-    else:
-      panel.addstr(y, x + startX, text, formatting)
-      return (y, x + len(text))
-
-class sighupListener(TorCtl.PostEventListener):
-  """
-  Listens for reload signal (hup), which is produced by:
-  pkill -sighup tor
-  causing the torrc and internal state to be reset.
-  """
-  
-  def __init__(self):
-    TorCtl.PostEventListener.__init__(self)
-    self.isReset = False
-  
-  def msg_event(self, event):
-    self.isReset |= event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)")
-
-def setPauseState(panels, monitorIsPaused, currentPage, overwrite=False):
-  """
-  Resets the isPaused state of panels. If overwrite is True then this pauses
-  reguardless of the monitor is paused or not.
-  """
-  
-  for key in PAUSEABLE: panels[key].setPaused(overwrite or monitorIsPaused or (key not in PAGES[currentPage] and key not in PAGE_S))
-
-def showMenu(stdscr, popup, title, options, initialSelection):
-  """
-  Provides menu with options laid out in a single column. User can cancel
-  selection with the escape key, in which case this proives -1. Otherwise this
-  returns the index of the selection. If initialSelection is -1 then the first
-  option is used and the carrot indicating past selection is ommitted.
-  """
-  
-  selection = initialSelection if initialSelection != -1 else 0
-  
-  if popup.win:
-    if not panel.CURSES_LOCK.acquire(False): return -1
-    try:
-      # TODO: should pause interface (to avoid event accumilation)
-      curses.cbreak() # wait indefinitely for key presses (no timeout)
-      
-      # uses smaller dimentions more fitting for small content
-      popup.height = len(options) + 2
-      
-      newWidth = max([len(label) for label in options]) + 9
-      popup.recreate(stdscr, newWidth)
-      
-      key = 0
-      while key not in (curses.KEY_ENTER, 10, ord(' ')):
-        popup.clear()
-        popup.win.box()
-        popup.addstr(0, 0, title, curses.A_STANDOUT)
-        
-        for i in range(len(options)):
-          label = options[i]
-          format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
-          tab = "> " if i == initialSelection else "  "
-          popup.addstr(i + 1, 2, tab)
-          popup.addstr(i + 1, 4, " %s " % label, format)
-        
-        popup.refresh()
-        key = stdscr.getch()
-        if key == curses.KEY_UP: selection = max(0, selection - 1)
-        elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1)
-        elif key == 27: selection, key = -1, curses.KEY_ENTER # esc - cancel
-      
-      # reverts popup dimensions and conn panel label
-      popup.height = 9
-      popup.recreate(stdscr, 80)
-      
-      curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
-    finally:
-      panel.CURSES_LOCK.release()
-  
-  return selection
-
-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
-  # returning out of this function!)
-  events = set(selectedEvents)
-  
-  # removes special types only used in arm (UNKNOWN, TORCTL, ARM_DEBUG, etc)
-  toDiscard = []
-  for eventType in events:
-    if eventType not in logPanel.TOR_EVENT_TYPES.values(): toDiscard += [eventType]
-  
-  for eventType in list(toDiscard): events.discard(eventType)
-  
-  setEvents = torTools.getConn().setControllerEvents(list(events))
-  
-  # temporary hack for providing user selected events minus those that failed
-  # (wouldn't be a problem if I wasn't storing tor and non-tor events together...)
-  returnVal = list(selectedEvents.difference(torTools.FAILED_EVENTS))
-  returnVal.sort() # alphabetizes
-  return returnVal
-
-def connResetListener(conn, eventType):
-  """
-  Pauses connection resolution when tor's shut down, and resumes if started
-  again.
-  """
-  
-  if connections.isResolverAlive("tor"):
-    resolver = connections.getResolver("tor")
-    resolver.setPaused(eventType == torTools.TOR_CLOSED)
-
-def selectiveRefresh(panels, page):
-  """
-  This forces a redraw of content on the currently active page (should be done
-  after changing pages, popups, or anything else that overwrites panels).
-  """
-  
-  for panelKey in PAGES[page]:
-    panels[panelKey].redraw(True)
-
-def drawTorMonitor(stdscr, loggedEvents, isBlindMode):
-  """
-  Starts arm interface reflecting information on provided control port.
-  
-  stdscr - curses window
-  conn - active Tor control port connection
-  loggedEvents - types of events to be logged (plus an optional "UNKNOWN" for
-    otherwise unrecognized events)
-  """
-  
-  # loads config for various interface components
-  config = conf.getConfig("arm")
-  config.update(CONFIG)
-  config.update(graphing.graphPanel.CONFIG)
-  
-  # adds events needed for arm functionality to the torTools REQ_EVENTS mapping
-  # (they're then included with any setControllerEvents call, and log a more
-  # helpful error if unavailable)
-  torTools.REQ_EVENTS["BW"] = "bandwidth graph won't function"
-  
-  if not isBlindMode:
-    torTools.REQ_EVENTS["CIRC"] = "may cause issues in identifying client connections"
-  
-  # pauses/unpauses connection resolution according to if tor's connected or not
-  torTools.getConn().addStatusListener(connResetListener)
-  
-  # TODO: incrementally drop this requirement until everything's using the singleton
-  conn = torTools.getConn().getTorCtl()
-  
-  curses.halfdelay(REFRESH_RATE * 10)   # uses getch call as timer for REFRESH_RATE seconds
-  try: curses.use_default_colors()      # allows things like semi-transparent backgrounds (call can fail with ERR)
-  except curses.error: pass
-  
-  # attempts to make the cursor invisible (not supported in all terminals)
-  try: curses.curs_set(0)
-  except curses.error: pass
-  
-  # 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 = ""
-  
-  # minor refinements for connection resolver
-  if not isBlindMode:
-    resolver = connections.getResolver("tor")
-    if torPid: resolver.processPid = torPid # helps narrow connection results
-  
-  # hack to display a better (arm specific) notice if all resolvers fail
-  connections.RESOLVER_FINAL_FAILURE_MSG += " (connection related portions of the monitor won't function)"
-  
-  panels = {
-    "header": headerPanel.HeaderPanel(stdscr, config),
-    "popup": Popup(stdscr, 9),
-    "graph": graphing.graphPanel.GraphPanel(stdscr),
-    "log": logPanel.LogMonitor(stdscr, conn, loggedEvents)}
-  
-  # TODO: later it would be good to set the right 'top' values during initialization, 
-  # but for now this is just necessary for the log panel (and a hack in the log...)
-  
-  # TODO: bug from not setting top is that the log panel might attempt to draw
-  # before being positioned - the following is a quick hack til rewritten
-  panels["log"].setPaused(True)
-  
-  panels["conn"] = connPanel.ConnPanel(stdscr, conn, isBlindMode)
-  panels["control"] = ControlPanel(stdscr, isBlindMode)
-  panels["torrc"] = confPanel.ConfPanel(stdscr, confLocation, conn)
-  
-  # 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")
-  
-  # statistical monitors for graph
-  panels["graph"].addStats("bandwidth", graphing.bandwidthStats.BandwidthStats(config))
-  panels["graph"].addStats("system resources", graphing.psStats.PsStats(config))
-  if not isBlindMode: panels["graph"].addStats("connections", graphing.connStats.ConnStats())
-  
-  # sets graph based on config parameter
-  graphType = CONFIG["features.graph.type"]
-  if graphType == 0: panels["graph"].setStats(None)
-  elif graphType == 1: panels["graph"].setStats("bandwidth")
-  elif graphType == 2 and not isBlindMode: panels["graph"].setStats("connections")
-  elif graphType == 3: panels["graph"].setStats("system resources")
-  
-  # listeners that update bandwidth and log panels with Tor status
-  sighupTracker = sighupListener()
-  conn.add_event_listener(panels["log"])
-  conn.add_event_listener(panels["graph"].stats["bandwidth"])
-  conn.add_event_listener(panels["graph"].stats["system resources"])
-  if not isBlindMode: conn.add_event_listener(panels["graph"].stats["connections"])
-  conn.add_event_listener(panels["conn"])
-  conn.add_event_listener(sighupTracker)
-  
-  # prepopulates bandwidth values from state file
-  if CONFIG["features.graph.bw.prepopulate"]:
-    isSuccessful = panels["graph"].stats["bandwidth"].prepopulateFromState()
-    if isSuccessful: panels["graph"].updateInterval = 4
-  
-  # tells Tor to listen to the events we're interested
-  loggedEvents = setEventListening(loggedEvents, isBlindMode)
-  panels["log"].loggedEvents = loggedEvents # strips any that couldn't be set
-  
-  # directs logged TorCtl events to log panel
-  TorUtil.loglevel = "DEBUG"
-  TorUtil.logfile = panels["log"]
-  
-  # tells revised panels to run as daemons
-  panels["header"].start()
-  
-  # warns if tor isn't updating descriptors
-  try:
-    if conn.get_option("FetchUselessDescriptors")[0][1] == "0" and conn.get_option("DirPort")[0][1] == "0":
-      warning = ["Descriptors won't be updated (causing some connection information to be stale) unless:", \
-                "  a. 'FetchUselessDescriptors 1' is set in your torrc", \
-                "  b. the directory service is provided ('DirPort' defined)", \
-                "  c. or tor is used as a client"]
-      log.log(log.WARN, warning)
-  except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
-  
-  isUnresponsive = False    # true if it's been over ten seconds since the last BW event (probably due to Tor closing)
-  isPaused = False          # if true updates are frozen
-  overrideKey = None        # immediately runs with this input rather than waiting for the user if set
-  page = 0
-  regexFilters = []             # previously used log regex filters
-  panels["popup"].redraw(True)  # hack to make sure popup has a window instance (not entirely sure why...)
-  
-  # provides notice about any unused config keys
-  for key in config.getUnusedKeys():
-    log.log(CONFIG["log.configEntryUndefined"], "unrecognized configuration entry: %s" % key)
-  
-  lastPerformanceLog = 0 # ensures we don't do performance logging too frequently
-  redrawStartTime = time.time()
-  
-  # TODO: popups need to force the panels it covers to redraw (or better, have
-  # a global refresh function for after changing pages, popups, etc)
-  while True:
-    # tried only refreshing when the screen was resized but it caused a
-    # noticeable lag when resizing and didn't have an appreciable effect
-    # on system usage
-    
-    panel.CURSES_LOCK.acquire()
-    try:
-      redrawStartTime = time.time()
-      
-      # if sighup received then reload related information
-      if sighupTracker.isReset:
-        #panels["header"]._updateParams(True)
-        
-        # other panels that use torrc data
-        panels["conn"].resetOptions()
-        #if not isBlindMode: panels["graph"].stats["connections"].resetOptions(conn)
-        #panels["graph"].stats["bandwidth"].resetOptions()
-        
-        # if bandwidth graph is being shown then height might have changed
-        if panels["graph"].currentDisplay == "bandwidth":
-          panels["graph"].setHeight(panels["graph"].stats["bandwidth"].getPreferredHeight())
-        
-        panels["torrc"].reset()
-        sighupTracker.isReset = False
-      
-      # gives panels a chance to take advantage of the maximum bounds
-      # originally this checked in the bounds changed but 'recreate' is a no-op
-      # if panel properties are unchanged and checking every redraw is more
-      # resilient in case of funky changes (such as resizing during popups)
-      
-      # hack to make sure header picks layout before using the dimensions below
-      #panels["header"].getPreferredSize()
-      
-      startY = 0
-      for panelKey in PAGE_S[:2]:
-        #panels[panelKey].recreate(stdscr, -1, startY)
-        panels[panelKey].setParent(stdscr)
-        panels[panelKey].setWidth(-1)
-        panels[panelKey].setTop(startY)
-        startY += panels[panelKey].getHeight()
-      
-      panels["popup"].recreate(stdscr, 80, startY)
-      
-      for panelSet in PAGES:
-        tmpStartY = startY
-        
-        for panelKey in panelSet:
-          #panels[panelKey].recreate(stdscr, -1, tmpStartY)
-          panels[panelKey].setParent(stdscr)
-          panels[panelKey].setWidth(-1)
-          panels[panelKey].setTop(tmpStartY)
-          tmpStartY += panels[panelKey].getHeight()
-      
-      # provides a notice if there's been ten seconds since the last BW event
-      lastHeartbeat = torTools.getConn().getHeartbeat()
-      if torTools.getConn().isAlive() and "BW" in torTools.getConn().getControllerEvents() and lastHeartbeat != 0:
-        if not isUnresponsive and (time.time() - lastHeartbeat) >= 10:
-          isUnresponsive = True
-          log.log(log.NOTICE, "Relay unresponsive (last heartbeat: %s)" % time.ctime(lastHeartbeat))
-        elif isUnresponsive and (time.time() - lastHeartbeat) < 10:
-          # really shouldn't happen (meant Tor froze for a bit)
-          isUnresponsive = False
-          log.log(log.NOTICE, "Relay resumed")
-      
-      panels["conn"].reset()
-      
-      # TODO: part two of hack to prevent premature drawing by log panel
-      if page == 0 and not isPaused: panels["log"].setPaused(False)
-      
-      # I haven't the foggiest why, but doesn't work if redrawn out of order...
-      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"):
-            # revised panel (handles its own content refreshing)
-            panels[panelKey].redraw()
-          else:
-            panels[panelKey].redraw(True)
-      
-      stdscr.refresh()
-      
-      currentTime = time.time()
-      if currentTime - lastPerformanceLog >= CONFIG["logging.rate.refreshRate"]:
-        log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds" % (currentTime - redrawStartTime))
-        lastPerformanceLog = currentTime
-    finally:
-      panel.CURSES_LOCK.release()
-    
-    # wait for user keyboard input until timeout (unless an override was set)
-    if overrideKey:
-      key = overrideKey
-      overrideKey = None
-    else:
-      key = stdscr.getch()
-    
-    if key == ord('q') or key == ord('Q'):
-      quitConfirmed = not CONFIRM_QUIT
-      
-      # provides prompt to confirm that arm should exit
-      if CONFIRM_QUIT:
-        panel.CURSES_LOCK.acquire()
-        try:
-          setPauseState(panels, isPaused, page, True)
-          
-          # provides prompt
-          panels["control"].setMsg("Are you sure (q again to confirm)?", curses.A_BOLD)
-          panels["control"].redraw(True)
-          
-          curses.cbreak()
-          confirmationKey = stdscr.getch()
-          quitConfirmed = confirmationKey in (ord('q'), ord('Q'))
-          curses.halfdelay(REFRESH_RATE * 10)
-          
-          panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-          setPauseState(panels, isPaused, page)
-        finally:
-          panel.CURSES_LOCK.release()
-      
-      if quitConfirmed:
-        # quits arm
-        # very occasionally stderr gets "close failed: [Errno 11] Resource temporarily unavailable"
-        # this appears to be a python bug: http://bugs.python.org/issue3014
-        # (haven't seen this is quite some time... mysteriously resolved?)
-        
-        # joins on utility daemon threads - this might take a moment since
-        # the internal threadpools being joined might be sleeping
-        resolver = connections.getResolver("tor") if connections.isResolverAlive("tor") else None
-        if resolver: resolver.stop()  # sets halt flag (returning immediately)
-        hostnames.stop()              # halts and joins on hostname worker thread pool
-        if resolver: resolver.join()  # joins on halted resolver
-        
-        # stops panel daemons
-        panels["header"].stop()
-        panels["header"].join()
-        
-        conn.close() # joins on TorCtl event thread
-        break
-    elif key == curses.KEY_LEFT or key == curses.KEY_RIGHT:
-      # switch page
-      if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
-      else: page = (page + 1) % len(PAGES)
-      
-      # skip connections listing if it's disabled
-      if page == 1 and isBlindMode:
-        if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
-        else: page = (page + 1) % len(PAGES)
-      
-      # pauses panels that aren't visible to prevent events from accumilating
-      # (otherwise they'll wait on the curses lock which might get demanding)
-      setPauseState(panels, isPaused, page)
-      
-      panels["control"].page = page + 1
-      
-      # TODO: this redraw doesn't seem necessary (redraws anyway after this
-      # loop) - look into this when refactoring
-      panels["control"].redraw(True)
-      
-      selectiveRefresh(panels, page)
-    elif key == ord('p') or key == ord('P'):
-      # toggles update freezing
-      panel.CURSES_LOCK.acquire()
-      try:
-        isPaused = not isPaused
-        setPauseState(panels, isPaused, page)
-        panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-      finally:
-        panel.CURSES_LOCK.release()
-      
-      selectiveRefresh(panels, page)
-    elif key == ord('h') or key == ord('H'):
-      # displays popup for current page's controls
-      panel.CURSES_LOCK.acquire()
-      try:
-        setPauseState(panels, isPaused, page, True)
-        
-        # lists commands
-        popup = panels["popup"]
-        popup.clear()
-        popup.win.box()
-        popup.addstr(0, 0, "Page %i Commands:" % (page + 1), curses.A_STANDOUT)
-        
-        pageOverrideKeys = ()
-        
-        if page == 0:
-          graphedStats = panels["graph"].currentDisplay
-          if not graphedStats: graphedStats = "none"
-          popup.addfstr(1, 2, "<b>s</b>: graphed stats (<b>%s</b>)" % graphedStats)
-          popup.addfstr(1, 41, "<b>i</b>: graph update interval (<b>%s</b>)" % graphing.graphPanel.UPDATE_INTERVALS[panels["graph"].updateInterval][0])
-          popup.addfstr(2, 2, "<b>b</b>: graph bounds (<b>%s</b>)" % graphing.graphPanel.BOUND_LABELS[panels["graph"].bounds])
-          popup.addfstr(2, 41, "<b>d</b>: file descriptors")
-          popup.addfstr(3, 2, "<b>e</b>: change logged events")
-          
-          regexLabel = "enabled" if panels["log"].regexFilter else "disabled"
-          popup.addfstr(3, 41, "<b>f</b>: log regex filter (<b>%s</b>)" % regexLabel)
-          
-          pageOverrideKeys = (ord('s'), ord('i'), ord('d'), ord('e'), ord('r'), ord('f'))
-        if page == 1:
-          popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
-          popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
-          popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
-          popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
-          popup.addfstr(3, 2, "<b>enter</b>: connection details")
-          popup.addfstr(3, 41, "<b>d</b>: raw consensus descriptor")
-          
-          listingType = connPanel.LIST_LABEL[panels["conn"].listingType].lower()
-          popup.addfstr(4, 2, "<b>l</b>: listed identity (<b>%s</b>)" % listingType)
-          
-          resolverUtil = connections.getResolver("tor").overwriteResolver
-          if resolverUtil == None: resolverUtil = "auto"
-          else: resolverUtil = connections.CMD_STR[resolverUtil]
-          popup.addfstr(4, 41, "<b>u</b>: resolving utility (<b>%s</b>)" % resolverUtil)
-          
-          allowDnsLabel = "allow" if panels["conn"].allowDNS else "disallow"
-          popup.addfstr(5, 2, "<b>r</b>: permit DNS resolution (<b>%s</b>)" % allowDnsLabel)
-          
-          popup.addfstr(5, 41, "<b>s</b>: sort ordering")
-          popup.addfstr(6, 2, "<b>c</b>: client circuits")
-          
-          #popup.addfstr(5, 41, "c: toggle cursor (<b>%s</b>)" % ("on" if panels["conn"].isCursorEnabled else "off"))
-          
-          pageOverrideKeys = (ord('d'), ord('l'), ord('s'), ord('c'))
-        elif page == 2:
-          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)
-          
-          lineNumLabel = "on" if panels["torrc"].showLineNum else "off"
-          popup.addfstr(3, 41, "<b>n</b>: line numbering (<b>%s</b>)" % lineNumLabel)
-          
-          popup.addfstr(4, 2, "<b>r</b>: reload torrc")
-          popup.addfstr(4, 41, "<b>x</b>: reset tor (issue sighup)")
-        
-        popup.addstr(7, 2, "Press any key...")
-        popup.refresh()
-        
-        # waits for user to hit a key, if it belongs to a command then executes it
-        curses.cbreak()
-        helpExitKey = stdscr.getch()
-        if helpExitKey in pageOverrideKeys: overrideKey = helpExitKey
-        curses.halfdelay(REFRESH_RATE * 10)
-        
-        setPauseState(panels, isPaused, page)
-        selectiveRefresh(panels, page)
-      finally:
-        panel.CURSES_LOCK.release()
-    elif page == 0 and (key == ord('s') or key == ord('S')):
-      # provides menu to pick stats to be graphed
-      #options = ["None"] + [label for label in panels["graph"].stats.keys()]
-      options = ["None"]
-      
-      # appends stats labels with first letters of each word capitalized
-      initialSelection, i = -1, 1
-      if not panels["graph"].currentDisplay: initialSelection = 0
-      graphLabels = panels["graph"].stats.keys()
-      graphLabels.sort()
-      for label in graphLabels:
-        if label == panels["graph"].currentDisplay: initialSelection = i
-        words = label.split()
-        options.append(" ".join(word[0].upper() + word[1:] for word in words))
-        i += 1
-      
-      # hides top label of the graph panel and pauses panels
-      if panels["graph"].currentDisplay:
-        panels["graph"].showLabel = False
-        panels["graph"].redraw(True)
-      setPauseState(panels, isPaused, page, True)
-      
-      selection = showMenu(stdscr, panels["popup"], "Graphed Stats:", options, initialSelection)
-      
-      # reverts changes made for popup
-      panels["graph"].showLabel = True
-      setPauseState(panels, isPaused, page)
-      
-      # applies new setting
-      if selection != -1 and selection != initialSelection:
-        if selection == 0: panels["graph"].setStats(None)
-        else: panels["graph"].setStats(options[selection].lower())
-      
-      selectiveRefresh(panels, page)
-    elif page == 0 and (key == ord('i') or key == ord('I')):
-      # provides menu to pick graph panel update interval
-      options = [label for (label, intervalTime) in graphing.graphPanel.UPDATE_INTERVALS]
-      
-      initialSelection = panels["graph"].updateInterval
-      
-      #initialSelection = -1
-      #for i in range(len(options)):
-      #  if options[i] == panels["graph"].updateInterval: initialSelection = i
-      
-      # hides top label of the graph panel and pauses panels
-      if panels["graph"].currentDisplay:
-        panels["graph"].showLabel = False
-        panels["graph"].redraw(True)
-      setPauseState(panels, isPaused, page, True)
-      
-      selection = showMenu(stdscr, panels["popup"], "Update Interval:", options, initialSelection)
-      
-      # reverts changes made for popup
-      panels["graph"].showLabel = True
-      setPauseState(panels, isPaused, page)
-      
-      # applies new setting
-      if selection != -1: panels["graph"].updateInterval = selection
-      
-      selectiveRefresh(panels, page)
-    elif page == 0 and (key == ord('b') or key == ord('B')):
-      # uses the next boundary type for graph
-      panels["graph"].bounds = (panels["graph"].bounds + 1) % 3
-      
-      selectiveRefresh(panels, page)
-    elif page == 0 and key in (ord('d'), ord('D')):
-      # provides popup with file descriptors
-      panel.CURSES_LOCK.acquire()
-      try:
-        setPauseState(panels, isPaused, page, True)
-        curses.cbreak() # wait indefinitely for key presses (no timeout)
-        
-        fileDescriptorPopup.showFileDescriptorPopup(panels["popup"], stdscr, torPid)
-        
-        setPauseState(panels, isPaused, page)
-        curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
-      finally:
-        panel.CURSES_LOCK.release()
-    elif page == 0 and (key == ord('e') or key == ord('E')):
-      # allow user to enter new types of events to log - unchanged if left blank
-      panel.CURSES_LOCK.acquire()
-      try:
-        setPauseState(panels, isPaused, page, True)
-        
-        # provides prompt
-        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
-        popup.recreate(stdscr, 80)
-        
-        popup.clear()
-        popup.win.box()
-        popup.addstr(0, 0, "Event Types:", curses.A_STANDOUT)
-        lineNum = 1
-        for line in logPanel.EVENT_LISTING.split("\n"):
-          line = line[6:]
-          popup.addstr(lineNum, 1, line)
-          lineNum += 1
-        popup.refresh()
-        
-        # gets user input (this blocks monitor updates)
-        eventsInput = panels["control"].win.getstr(0, 15)
-        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 != "":
-          try:
-            expandedEvents = logPanel.expandEvents(eventsInput)
-            loggedEvents = setEventListening(expandedEvents, isBlindMode)
-            panels["log"].loggedEvents = loggedEvents
-          except ValueError, exc:
-            panels["control"].setMsg("Invalid flags: %s" % str(exc), curses.A_STANDOUT)
-            panels["control"].redraw(True)
-            time.sleep(2)
-        
-        # reverts popup dimensions
-        popup.height = 9
-        popup.recreate(stdscr, 80)
-        
-        panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-        setPauseState(panels, isPaused, page)
-      finally:
-        panel.CURSES_LOCK.release()
-    elif page == 0 and (key == ord('f') or key == ord('F')):
-      # provides menu to pick previous regular expression filters or to add a new one
-      # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
-      options = ["None"] + regexFilters + ["New..."]
-      initialSelection = 0 if not panels["log"].regexFilter else 1
-      
-      # hides top label of the graph panel and pauses panels
-      if panels["graph"].currentDisplay:
-        panels["graph"].showLabel = False
-        panels["graph"].redraw(True)
-      setPauseState(panels, isPaused, page, True)
-      
-      selection = showMenu(stdscr, panels["popup"], "Log Filter:", options, initialSelection)
-      
-      # applies new setting
-      if selection == 0:
-        panels["log"].regexFilter = None
-      elif selection == len(options) - 1:
-        # selected 'New...' option - prompt user to input regular expression
-        panel.CURSES_LOCK.acquire()
-        try:
-          # provides prompt
-          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)
-          
-          # reverts visability settings
-          try: curses.curs_set(0)
-          except curses.error: pass
-          curses.noecho()
-          curses.halfdelay(REFRESH_RATE * 10)
-          
-          if regexInput != "":
-            try:
-              panels["log"].regexFilter = re.compile(regexInput)
-              if regexInput in regexFilters: regexFilters.remove(regexInput)
-              regexFilters = [regexInput] + regexFilters
-            except re.error, exc:
-              panels["control"].setMsg("Unable to compile expression: %s" % str(exc), curses.A_STANDOUT)
-              panels["control"].redraw(True)
-              time.sleep(2)
-          panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-        finally:
-          panel.CURSES_LOCK.release()
-      elif selection != -1:
-        try:
-          panels["log"].regexFilter = re.compile(regexFilters[selection - 1])
-          
-          # move selection to top
-          regexFilters = [regexFilters[selection - 1]] + regexFilters
-          del regexFilters[selection]
-        except re.error, exc:
-          # shouldn't happen since we've already checked validity
-          log.log(log.WARN, "Invalid regular expression ('%s': %s) - removing from listing" % (regexFilters[selection - 1], str(exc)))
-          del regexFilters[selection - 1]
-      
-      if len(regexFilters) > MAX_REGEX_FILTERS: del regexFilters[MAX_REGEX_FILTERS:]
-      
-      # reverts changes made for popup
-      panels["graph"].showLabel = True
-      setPauseState(panels, isPaused, page)
-    elif key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingCounter != -1:
-      # canceling hostname resolution (esc on any page)
-      panels["conn"].listingType = connPanel.LIST_IP
-      panels["control"].resolvingCounter = -1
-      hostnames.setPaused(True)
-      panels["conn"].sortConnections()
-    elif page == 1 and panels["conn"].isCursorEnabled and key in (curses.KEY_ENTER, 10, ord(' ')):
-      # provides details on selected connection
-      panel.CURSES_LOCK.acquire()
-      try:
-        setPauseState(panels, isPaused, page, True)
-        popup = panels["popup"]
-        
-        # reconfigures connection panel to accomidate details dialog
-        panels["conn"].showLabel = False
-        panels["conn"].showingDetails = True
-        panels["conn"].redraw(True)
-        
-        hostnames.setPaused(not panels["conn"].allowDNS)
-        relayLookupCache = {} # temporary cache of entry -> (ns data, desc data)
-        
-        curses.cbreak() # wait indefinitely for key presses (no timeout)
-        key = 0
-        
-        while key not in (curses.KEY_ENTER, 10, ord(' ')):
-          popup.clear()
-          popup.win.box()
-          popup.addstr(0, 0, "Connection Details:", curses.A_STANDOUT)
-          
-          selection = panels["conn"].cursorSelection
-          if not selection or not panels["conn"].connections: break
-          selectionColor = connPanel.TYPE_COLORS[selection[connPanel.CONN_TYPE]]
-          format = uiTools.getColor(selectionColor) | curses.A_BOLD
-          
-          selectedIp = selection[connPanel.CONN_F_IP]
-          selectedPort = selection[connPanel.CONN_F_PORT]
-          selectedIsPrivate = selection[connPanel.CONN_PRIVATE]
-          
-          addrLabel = "address: %s:%s" % (selectedIp, selectedPort)
-          
-          if selection[connPanel.CONN_TYPE] == "family" and int(selection[connPanel.CONN_L_PORT]) > 65535:
-            # unresolved family entry - unknown ip/port
-            addrLabel = "address: unknown"
-          
-          if selectedIsPrivate: hostname = None
-          else:
-            try: hostname = hostnames.resolve(selectedIp)
-            except ValueError: hostname = "unknown" # hostname couldn't be resolved
-          
-          if hostname == None:
-            if hostnames.isPaused() or selectedIsPrivate: hostname = "DNS resolution disallowed"
-            else:
-              # if hostname is still being resolved refresh panel every half-second until it's completed
-              curses.halfdelay(5)
-              hostname = "resolving..."
-          elif len(hostname) > 73 - len(addrLabel):
-            # hostname too long - truncate
-            hostname = "%s..." % hostname[:70 - len(addrLabel)]
-          
-          if selectedIsPrivate:
-            popup.addstr(1, 2, "address: <scrubbed> (unknown)", format)
-            popup.addstr(2, 2, "locale: ??", format)
-            popup.addstr(3, 2, "No consensus data found", format)
-          else:
-            popup.addstr(1, 2, "%s (%s)" % (addrLabel, hostname), format)
-            
-            locale = selection[connPanel.CONN_COUNTRY]
-            popup.addstr(2, 2, "locale: %s" % locale, format)
-            
-            # provides consensus data for selection (needs fingerprint to get anywhere...)
-            fingerprint = panels["conn"].getFingerprint(selectedIp, selectedPort)
-            
-            if fingerprint == "UNKNOWN":
-              if selectedIp not in panels["conn"].fingerprintMappings.keys():
-                # no consensus entry for this ip address
-                popup.addstr(3, 2, "No consensus data found", format)
-              else:
-                # couldn't resolve due to multiple matches - list them all
-                popup.addstr(3, 2, "Muliple matches, possible fingerprints are:", format)
-                matchings = panels["conn"].fingerprintMappings[selectedIp]
-                
-                line = 4
-                for (matchPort, matchFingerprint, matchNickname) in matchings:
-                  popup.addstr(line, 2, "%i. or port: %-5s fingerprint: %s" % (line - 3, matchPort, matchFingerprint), format)
-                  line += 1
-                  
-                  if line == 7 and len(matchings) > 4:
-                    popup.addstr(8, 2, "... %i more" % len(matchings) - 3, format)
-                    break
-            else:
-              # fingerprint found - retrieve related data
-              lookupErrored = False
-              if selection in relayLookupCache.keys(): nsEntry, descEntry = relayLookupCache[selection]
-              else:
-                try:
-                  nsCall = conn.get_network_status("id/%s" % fingerprint)
-                  if len(nsCall) == 0: raise TorCtl.ErrorReply() # no results provided
-                except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
-                  # ns lookup fails or provides empty results - can happen with
-                  # localhost lookups if relay's having problems (orport not
-                  # reachable) and this will be empty if network consensus
-                  # couldn't be fetched
-                  lookupErrored = True
-                
-                if not lookupErrored and nsCall:
-                  if len(nsCall) > 1:
-                    # multiple records for fingerprint (shouldn't happen)
-                    log.log(log.WARN, "Multiple consensus entries for fingerprint: %s" % fingerprint)
-                  
-                  nsEntry = nsCall[0]
-                  
-                  try:
-                    descLookupCmd = "desc/id/%s" % fingerprint
-                    descEntry = TorCtl.Router.build_from_desc(conn.get_info(descLookupCmd)[descLookupCmd].split("\n"), nsEntry)
-                    relayLookupCache[selection] = (nsEntry, descEntry)
-                  except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): lookupErrored = True # desc lookup failed
-              
-              if lookupErrored:
-                popup.addstr(3, 2, "Unable to retrieve consensus data", format)
-              else:
-                popup.addstr(2, 15, "fingerprint: %s" % fingerprint, format)
-                
-                nickname = panels["conn"].getNickname(selectedIp, selectedPort)
-                dirPortLabel = "dirport: %i" % nsEntry.dirport if nsEntry.dirport else ""
-                popup.addstr(3, 2, "nickname: %-25s orport: %-10i %s" % (nickname, nsEntry.orport, dirPortLabel), format)
-                
-                popup.addstr(4, 2, "published: %-24s os: %-14s version: %s" % (descEntry.published, descEntry.os, descEntry.version), format)
-                popup.addstr(5, 2, "flags: %s" % ", ".join(nsEntry.flags), format)
-                
-                exitLine = ", ".join([str(k) for k in descEntry.exitpolicy])
-                if len(exitLine) > 63: exitLine = "%s..." % exitLine[:60]
-                popup.addstr(6, 2, "exit policy: %s" % exitLine, format)
-                
-                if descEntry.contact:
-                  # clears up some common obscuring
-                  contactAddr = descEntry.contact
-                  obscuring = [(" at ", "@"), (" AT ", "@"), ("AT", "@"), (" dot ", "."), (" DOT ", ".")]
-                  for match, replace in obscuring: contactAddr = contactAddr.replace(match, replace)
-                  if len(contactAddr) > 67: contactAddr = "%s..." % contactAddr[:64]
-                  popup.addstr(7, 2, "contact: %s" % contactAddr, format)
-            
-          popup.refresh()
-          key = stdscr.getch()
-          
-          if key == curses.KEY_RIGHT: key = curses.KEY_DOWN
-          elif key == curses.KEY_LEFT: key = curses.KEY_UP
-          
-          if key in (curses.KEY_DOWN, curses.KEY_UP, curses.KEY_PPAGE, curses.KEY_NPAGE):
-            panels["conn"].handleKey(key)
-          elif key in (ord('d'), ord('D')):
-            descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, conn, panels["conn"])
-            panels["conn"].redraw(True)
-        
-        panels["conn"].showLabel = True
-        panels["conn"].showingDetails = False
-        hostnames.setPaused(not panels["conn"].allowDNS and panels["conn"].listingType == connPanel.LIST_HOSTNAME)
-        setPauseState(panels, isPaused, page)
-        curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
-      finally:
-        panel.CURSES_LOCK.release()
-    elif page == 1 and panels["conn"].isCursorEnabled and key in (ord('d'), ord('D')):
-      # presents popup for raw consensus data
-      panel.CURSES_LOCK.acquire()
-      try:
-        setPauseState(panels, isPaused, page, True)
-        curses.cbreak() # wait indefinitely for key presses (no timeout)
-        panels["conn"].showLabel = False
-        panels["conn"].redraw(True)
-        
-        descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, conn, panels["conn"])
-        
-        setPauseState(panels, isPaused, page)
-        curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
-        panels["conn"].showLabel = True
-      finally:
-        panel.CURSES_LOCK.release()
-    elif page == 1 and (key == ord('l') or key == ord('L')):
-      # provides menu to pick identification info listed for connections
-      optionTypes = [connPanel.LIST_IP, connPanel.LIST_HOSTNAME, connPanel.LIST_FINGERPRINT, connPanel.LIST_NICKNAME]
-      options = [connPanel.LIST_LABEL[sortType] for sortType in optionTypes]
-      initialSelection = panels["conn"].listingType   # enums correspond to index
-      
-      # hides top label of conn panel and pauses panels
-      panels["conn"].showLabel = False
-      panels["conn"].redraw(True)
-      setPauseState(panels, isPaused, page, True)
-      
-      selection = showMenu(stdscr, panels["popup"], "List By:", options, initialSelection)
-      
-      # reverts changes made for popup
-      panels["conn"].showLabel = True
-      setPauseState(panels, isPaused, page)
-      
-      # applies new setting
-      if selection != -1 and optionTypes[selection] != panels["conn"].listingType:
-        panels["conn"].listingType = optionTypes[selection]
-        
-        if panels["conn"].listingType == connPanel.LIST_HOSTNAME:
-          curses.halfdelay(10) # refreshes display every second until done resolving
-          panels["control"].resolvingCounter = hostnames.getRequestCount() - hostnames.getPendingCount()
-          
-          hostnames.setPaused(not panels["conn"].allowDNS)
-          for connEntry in panels["conn"].connections:
-            try: hostnames.resolve(connEntry[connPanel.CONN_F_IP])
-            except ValueError: pass
-        else:
-          panels["control"].resolvingCounter = -1
-          hostnames.setPaused(True)
-        
-        panels["conn"].sortConnections()
-    elif page == 1 and (key == ord('u') or key == ord('U')):
-      # provides menu to pick identification resolving utility
-      optionTypes = [None, connections.CMD_NETSTAT, connections.CMD_SS, connections.CMD_LSOF]
-      options = ["auto"] + [connections.CMD_STR[util] for util in optionTypes[1:]]
-      
-      initialSelection = connections.getResolver("tor").overwriteResolver # enums correspond to indices
-      if initialSelection == None: initialSelection = 0
-      
-      # hides top label of conn panel and pauses panels
-      panels["conn"].showLabel = False
-      panels["conn"].redraw(True)
-      setPauseState(panels, isPaused, page, True)
-      
-      selection = showMenu(stdscr, panels["popup"], "Resolver Util:", options, initialSelection)
-      
-      # reverts changes made for popup
-      panels["conn"].showLabel = True
-      setPauseState(panels, isPaused, page)
-      
-      # applies new setting
-      if selection != -1 and optionTypes[selection] != connections.getResolver("tor").overwriteResolver:
-        connections.getResolver("tor").overwriteResolver = optionTypes[selection]
-    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()
-    elif page == 1 and (key == ord('c') or key == ord('C')):
-      # displays popup with client circuits
-      clientCircuits = None
-      try:
-        clientCircuits = conn.get_info("circuit-status")["circuit-status"].split("\n")
-      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
-      
-      maxEntryLength = 0
-      if clientCircuits:
-        for clientEntry in clientCircuits: maxEntryLength = max(len(clientEntry), maxEntryLength)
-      
-      panel.CURSES_LOCK.acquire()
-      try:
-        setPauseState(panels, isPaused, page, True)
-        
-        # makes sure there's room for the longest entry
-        popup = panels["popup"]
-        if clientCircuits and maxEntryLength + 4 > popup.getPreferredSize()[1]:
-          popup.height = max(popup.height, len(clientCircuits) + 3)
-          popup.recreate(stdscr, maxEntryLength + 4)
-        
-        # lists commands
-        popup.clear()
-        popup.win.box()
-        popup.addstr(0, 0, "Client Circuits:", curses.A_STANDOUT)
-        
-        if clientCircuits == None:
-          popup.addstr(1, 2, "Unable to retireve current circuits")
-        elif len(clientCircuits) == 1 and clientCircuits[0] == "":
-          popup.addstr(1, 2, "No active client circuits")
-        else:
-          line = 1
-          for clientEntry in clientCircuits:
-            popup.addstr(line, 2, clientEntry)
-            line += 1
-            
-        popup.addstr(popup.height - 2, 2, "Press any key...")
-        popup.refresh()
-        
-        curses.cbreak()
-        stdscr.getch()
-        curses.halfdelay(REFRESH_RATE * 10)
-        
-        # reverts popup dimensions
-        popup.height = 9
-        popup.recreate(stdscr, 80)
-        
-        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)
-      
-      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 == 2 and (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" % str(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 page == 0:
-      panels["log"].handleKey(key)
-    elif page == 1:
-      panels["conn"].handleKey(key)
-    elif page == 2:
-      panels["torrc"].handleKey(key)
-
-def startTorMonitor(loggedEvents, isBlindMode):
-  try:
-    curses.wrapper(drawTorMonitor, loggedEvents, isBlindMode)
-  except KeyboardInterrupt:
-    pass # skip printing stack trace in case of keyboard interrupt
-

Copied: arm/trunk/src/interface/controller.py (from rev 22991, arm/trunk/interface/controller.py)
===================================================================
--- arm/trunk/src/interface/controller.py	                        (rev 0)
+++ arm/trunk/src/interface/controller.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,1313 @@
+#!/usr/bin/env python
+# controller.py -- arm interface (curses monitor for relay status)
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+"""
+Curses (terminal) interface for the arm relay status monitor.
+"""
+
+import re
+import math
+import time
+import curses
+import socket
+from TorCtl import TorCtl
+from TorCtl import TorUtil
+
+import headerPanel
+import graphing.graphPanel
+import logPanel
+import connPanel
+import confPanel
+import descriptorPopup
+import fileDescriptorPopup
+
+from util import conf, log, connections, hostnames, panel, sysTools, torTools, uiTools
+import graphing.bandwidthStats
+import graphing.connStats
+import graphing.psStats
+
+CONFIRM_QUIT = True
+REFRESH_RATE = 5        # seconds between redrawing screen
+MAX_REGEX_FILTERS = 5   # maximum number of previous regex filters that'll be remembered
+
+# enums for message in control label
+CTL_HELP, CTL_PAUSED = range(2)
+
+# panel order per page
+PAGE_S = ["header", "control", "popup"] # sticky (ie, always available) page
+PAGES = [
+  ["graph", "log"],
+  ["conn"],
+  ["torrc"]]
+PAUSEABLE = ["header", "graph", "log", "conn"]
+
+CONFIG = {"logging.rate.refreshRate": 5, "features.graph.type": 1, "features.graph.bw.prepopulate": True, "log.refreshRate": log.DEBUG, "log.configEntryUndefined": log.NOTICE}
+
+class ControlPanel(panel.Panel):
+  """ Draws single line label for interface controls. """
+  
+  def __init__(self, stdscr, isBlindMode):
+    panel.Panel.__init__(self, stdscr, "control", 0, 1)
+    self.msgText = CTL_HELP           # message text to be displyed
+    self.msgAttr = curses.A_NORMAL    # formatting attributes
+    self.page = 1                     # page number currently being displayed
+    self.resolvingCounter = -1        # count of resolver when starting (-1 if we aren't working on a batch)
+    self.isBlindMode = isBlindMode
+  
+  def setMsg(self, msgText, msgAttr=curses.A_NORMAL):
+    """
+    Sets the message and display attributes. If msgType matches CTL_HELP or
+    CTL_PAUSED then uses the default message for those statuses.
+    """
+    
+    self.msgText = msgText
+    self.msgAttr = msgAttr
+  
+  def draw(self, subwindow, width, height):
+    msgText = self.msgText
+    msgAttr = self.msgAttr
+    barTab = 2                # space between msgText and progress bar
+    barWidthMax = 40          # max width to progress bar
+    barWidth = -1             # space between "[ ]" in progress bar (not visible if -1)
+    barProgress = 0           # cells to fill
+    
+    if msgText == CTL_HELP:
+      msgAttr = curses.A_NORMAL
+      
+      if self.resolvingCounter != -1:
+        if hostnames.isPaused() or not hostnames.isResolving():
+          # done resolving dns batch
+          self.resolvingCounter = -1
+          curses.halfdelay(REFRESH_RATE * 10) # revert to normal refresh rate
+        else:
+          batchSize = hostnames.getRequestCount() - self.resolvingCounter
+          entryCount = batchSize - hostnames.getPendingCount()
+          if batchSize > 0: progress = 100 * entryCount / batchSize
+          else: progress = 0
+          
+          additive = "or l " if self.page == 2 else ""
+          batchSizeDigits = int(math.log10(batchSize)) + 1
+          entryCountLabel = ("%%%ii" % batchSizeDigits) % entryCount
+          #msgText = "Resolving hostnames (%i / %i, %i%%) - press esc %sto cancel" % (entryCount, batchSize, progress, additive)
+          msgText = "Resolving hostnames (press esc %sto cancel) - %s / %i, %2i%%" % (additive, entryCountLabel, batchSize, progress)
+          
+          barWidth = min(barWidthMax, width - len(msgText) - 3 - barTab)
+          barProgress = barWidth * entryCount / batchSize
+      
+      if self.resolvingCounter == -1:
+        currentPage = self.page
+        pageCount = len(PAGES)
+        
+        if self.isBlindMode:
+          if currentPage >= 2: currentPage -= 1
+          pageCount -= 1
+        
+        msgText = "page %i / %i - q: quit, p: pause, h: page help" % (currentPage, pageCount)
+    elif msgText == CTL_PAUSED:
+      msgText = "Paused"
+      msgAttr = curses.A_STANDOUT
+    
+    self.addstr(0, 0, msgText, msgAttr)
+    if barWidth > -1:
+      xLoc = len(msgText) + barTab
+      self.addstr(0, xLoc, "[", curses.A_BOLD)
+      self.addstr(0, xLoc + 1, " " * barProgress, curses.A_STANDOUT | uiTools.getColor("red"))
+      self.addstr(0, xLoc + barWidth + 1, "]", curses.A_BOLD)
+
+class Popup(panel.Panel):
+  """
+  Temporarily providing old panel methods until permanent workaround for popup
+  can be derrived (this passive drawing method is horrible - I'll need to
+  provide a version using the more active repaint design later in the
+  revision).
+  """
+  
+  def __init__(self, stdscr, height):
+    panel.Panel.__init__(self, stdscr, "popup", 0, height)
+  
+  # The following methods are to emulate old panel functionality (this was the
+  # only implementations to use these methods and will require a complete
+  # rewrite when refactoring gets here)
+  def clear(self):
+    if self.win:
+      self.isDisplaced = self.top > self.win.getparyx()[0]
+      if not self.isDisplaced: self.win.erase()
+  
+  def refresh(self):
+    if self.win and not self.isDisplaced: self.win.refresh()
+  
+  def recreate(self, stdscr, newWidth=-1, newTop=None):
+    self.setParent(stdscr)
+    self.setWidth(newWidth)
+    if newTop != None: self.setTop(newTop)
+    
+    newHeight, newWidth = self.getPreferredSize()
+    if newHeight > 0:
+      self.win = self.parent.subwin(newHeight, newWidth, self.top, 0)
+    elif self.win == None:
+      # don't want to leave the window as none (in very edge cases could cause
+      # problems) - rather, create a displaced instance
+      self.win = self.parent.subwin(1, newWidth, 0, 0)
+    
+    self.maxY, self.maxX = self.win.getmaxyx()
+
+def addstr_wrap(panel, y, x, text, formatting, startX = 0, endX = -1, maxY = -1):
+  """
+  Writes text with word wrapping, returning the ending y/x coordinate.
+  y: starting write line
+  x: column offset from startX
+  text / formatting: content to be written
+  startX / endX: column bounds in which text may be written
+  """
+  
+  # moved out of panel (trying not to polute new code!)
+  # TODO: unpleaseantly complex usage - replace with something else when
+  # rewriting confPanel and descriptorPopup (the only places this is used)
+  if not text: return (y, x)          # nothing to write
+  if endX == -1: endX = panel.maxX     # defaults to writing to end of panel
+  if maxY == -1: maxY = panel.maxY + 1 # defaults to writing to bottom of panel
+  lineWidth = endX - startX           # room for text
+  while True:
+    if len(text) > lineWidth - x - 1:
+      chunkSize = text.rfind(" ", 0, lineWidth - x)
+      writeText = text[:chunkSize]
+      text = text[chunkSize:].strip()
+      
+      panel.addstr(y, x + startX, writeText, formatting)
+      y, x = y + 1, 0
+      if y >= maxY: return (y, x)
+    else:
+      panel.addstr(y, x + startX, text, formatting)
+      return (y, x + len(text))
+
+class sighupListener(TorCtl.PostEventListener):
+  """
+  Listens for reload signal (hup), which is produced by:
+  pkill -sighup tor
+  causing the torrc and internal state to be reset.
+  """
+  
+  def __init__(self):
+    TorCtl.PostEventListener.__init__(self)
+    self.isReset = False
+  
+  def msg_event(self, event):
+    self.isReset |= event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)")
+
+def setPauseState(panels, monitorIsPaused, currentPage, overwrite=False):
+  """
+  Resets the isPaused state of panels. If overwrite is True then this pauses
+  reguardless of the monitor is paused or not.
+  """
+  
+  for key in PAUSEABLE: panels[key].setPaused(overwrite or monitorIsPaused or (key not in PAGES[currentPage] and key not in PAGE_S))
+
+def showMenu(stdscr, popup, title, options, initialSelection):
+  """
+  Provides menu with options laid out in a single column. User can cancel
+  selection with the escape key, in which case this proives -1. Otherwise this
+  returns the index of the selection. If initialSelection is -1 then the first
+  option is used and the carrot indicating past selection is ommitted.
+  """
+  
+  selection = initialSelection if initialSelection != -1 else 0
+  
+  if popup.win:
+    if not panel.CURSES_LOCK.acquire(False): return -1
+    try:
+      # TODO: should pause interface (to avoid event accumilation)
+      curses.cbreak() # wait indefinitely for key presses (no timeout)
+      
+      # uses smaller dimentions more fitting for small content
+      popup.height = len(options) + 2
+      
+      newWidth = max([len(label) for label in options]) + 9
+      popup.recreate(stdscr, newWidth)
+      
+      key = 0
+      while key not in (curses.KEY_ENTER, 10, ord(' ')):
+        popup.clear()
+        popup.win.box()
+        popup.addstr(0, 0, title, curses.A_STANDOUT)
+        
+        for i in range(len(options)):
+          label = options[i]
+          format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
+          tab = "> " if i == initialSelection else "  "
+          popup.addstr(i + 1, 2, tab)
+          popup.addstr(i + 1, 4, " %s " % label, format)
+        
+        popup.refresh()
+        key = stdscr.getch()
+        if key == curses.KEY_UP: selection = max(0, selection - 1)
+        elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1)
+        elif key == 27: selection, key = -1, curses.KEY_ENTER # esc - cancel
+      
+      # reverts popup dimensions and conn panel label
+      popup.height = 9
+      popup.recreate(stdscr, 80)
+      
+      curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+    finally:
+      panel.CURSES_LOCK.release()
+  
+  return selection
+
+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
+  # returning out of this function!)
+  events = set(selectedEvents)
+  
+  # removes special types only used in arm (UNKNOWN, TORCTL, ARM_DEBUG, etc)
+  toDiscard = []
+  for eventType in events:
+    if eventType not in logPanel.TOR_EVENT_TYPES.values(): toDiscard += [eventType]
+  
+  for eventType in list(toDiscard): events.discard(eventType)
+  
+  setEvents = torTools.getConn().setControllerEvents(list(events))
+  
+  # temporary hack for providing user selected events minus those that failed
+  # (wouldn't be a problem if I wasn't storing tor and non-tor events together...)
+  returnVal = list(selectedEvents.difference(torTools.FAILED_EVENTS))
+  returnVal.sort() # alphabetizes
+  return returnVal
+
+def connResetListener(conn, eventType):
+  """
+  Pauses connection resolution when tor's shut down, and resumes if started
+  again.
+  """
+  
+  if connections.isResolverAlive("tor"):
+    resolver = connections.getResolver("tor")
+    resolver.setPaused(eventType == torTools.TOR_CLOSED)
+
+def selectiveRefresh(panels, page):
+  """
+  This forces a redraw of content on the currently active page (should be done
+  after changing pages, popups, or anything else that overwrites panels).
+  """
+  
+  for panelKey in PAGES[page]:
+    panels[panelKey].redraw(True)
+
+def drawTorMonitor(stdscr, loggedEvents, isBlindMode):
+  """
+  Starts arm interface reflecting information on provided control port.
+  
+  stdscr - curses window
+  conn - active Tor control port connection
+  loggedEvents - types of events to be logged (plus an optional "UNKNOWN" for
+    otherwise unrecognized events)
+  """
+  
+  # loads config for various interface components
+  config = conf.getConfig("arm")
+  config.update(CONFIG)
+  graphing.graphPanel.loadConfig(config)
+  
+  # adds events needed for arm functionality to the torTools REQ_EVENTS mapping
+  # (they're then included with any setControllerEvents call, and log a more
+  # helpful error if unavailable)
+  torTools.REQ_EVENTS["BW"] = "bandwidth graph won't function"
+  
+  if not isBlindMode:
+    torTools.REQ_EVENTS["CIRC"] = "may cause issues in identifying client connections"
+  
+  # pauses/unpauses connection resolution according to if tor's connected or not
+  torTools.getConn().addStatusListener(connResetListener)
+  
+  # TODO: incrementally drop this requirement until everything's using the singleton
+  conn = torTools.getConn().getTorCtl()
+  
+  curses.halfdelay(REFRESH_RATE * 10)   # uses getch call as timer for REFRESH_RATE seconds
+  try: curses.use_default_colors()      # allows things like semi-transparent backgrounds (call can fail with ERR)
+  except curses.error: pass
+  
+  # attempts to make the cursor invisible (not supported in all terminals)
+  try: curses.curs_set(0)
+  except curses.error: pass
+  
+  # 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 = ""
+  
+  # minor refinements for connection resolver
+  if not isBlindMode:
+    resolver = connections.getResolver("tor")
+    if torPid: resolver.processPid = torPid # helps narrow connection results
+  
+  # hack to display a better (arm specific) notice if all resolvers fail
+  connections.RESOLVER_FINAL_FAILURE_MSG += " (connection related portions of the monitor won't function)"
+  
+  panels = {
+    "header": headerPanel.HeaderPanel(stdscr, config),
+    "popup": Popup(stdscr, 9),
+    "graph": graphing.graphPanel.GraphPanel(stdscr),
+    "log": logPanel.LogMonitor(stdscr, conn, loggedEvents)}
+  
+  # TODO: later it would be good to set the right 'top' values during initialization, 
+  # but for now this is just necessary for the log panel (and a hack in the log...)
+  
+  # TODO: bug from not setting top is that the log panel might attempt to draw
+  # before being positioned - the following is a quick hack til rewritten
+  panels["log"].setPaused(True)
+  
+  panels["conn"] = connPanel.ConnPanel(stdscr, conn, isBlindMode)
+  panels["control"] = ControlPanel(stdscr, isBlindMode)
+  panels["torrc"] = confPanel.ConfPanel(stdscr, confLocation, conn)
+  
+  # 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")
+  
+  # statistical monitors for graph
+  panels["graph"].addStats("bandwidth", graphing.bandwidthStats.BandwidthStats(config))
+  panels["graph"].addStats("system resources", graphing.psStats.PsStats(config))
+  if not isBlindMode: panels["graph"].addStats("connections", graphing.connStats.ConnStats())
+  
+  # sets graph based on config parameter
+  graphType = CONFIG["features.graph.type"]
+  if graphType == 0: panels["graph"].setStats(None)
+  elif graphType == 1: panels["graph"].setStats("bandwidth")
+  elif graphType == 2 and not isBlindMode: panels["graph"].setStats("connections")
+  elif graphType == 3: panels["graph"].setStats("system resources")
+  
+  # listeners that update bandwidth and log panels with Tor status
+  sighupTracker = sighupListener()
+  conn.add_event_listener(panels["log"])
+  conn.add_event_listener(panels["graph"].stats["bandwidth"])
+  conn.add_event_listener(panels["graph"].stats["system resources"])
+  if not isBlindMode: conn.add_event_listener(panels["graph"].stats["connections"])
+  conn.add_event_listener(panels["conn"])
+  conn.add_event_listener(sighupTracker)
+  
+  # prepopulates bandwidth values from state file
+  if CONFIG["features.graph.bw.prepopulate"]:
+    isSuccessful = panels["graph"].stats["bandwidth"].prepopulateFromState()
+    if isSuccessful: panels["graph"].updateInterval = 4
+  
+  # tells Tor to listen to the events we're interested
+  loggedEvents = setEventListening(loggedEvents, isBlindMode)
+  panels["log"].loggedEvents = loggedEvents # strips any that couldn't be set
+  
+  # directs logged TorCtl events to log panel
+  TorUtil.loglevel = "DEBUG"
+  TorUtil.logfile = panels["log"]
+  
+  # tells revised panels to run as daemons
+  panels["header"].start()
+  
+  # warns if tor isn't updating descriptors
+  try:
+    if conn.get_option("FetchUselessDescriptors")[0][1] == "0" and conn.get_option("DirPort")[0][1] == "0":
+      warning = ["Descriptors won't be updated (causing some connection information to be stale) unless:", \
+                "  a. 'FetchUselessDescriptors 1' is set in your torrc", \
+                "  b. the directory service is provided ('DirPort' defined)", \
+                "  c. or tor is used as a client"]
+      log.log(log.WARN, warning)
+  except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
+  
+  isUnresponsive = False    # true if it's been over ten seconds since the last BW event (probably due to Tor closing)
+  isPaused = False          # if true updates are frozen
+  overrideKey = None        # immediately runs with this input rather than waiting for the user if set
+  page = 0
+  regexFilters = []             # previously used log regex filters
+  panels["popup"].redraw(True)  # hack to make sure popup has a window instance (not entirely sure why...)
+  
+  # provides notice about any unused config keys
+  for key in config.getUnusedKeys():
+    log.log(CONFIG["log.configEntryUndefined"], "unrecognized configuration entry: %s" % key)
+  
+  lastPerformanceLog = 0 # ensures we don't do performance logging too frequently
+  redrawStartTime = time.time()
+  
+  # TODO: popups need to force the panels it covers to redraw (or better, have
+  # a global refresh function for after changing pages, popups, etc)
+  while True:
+    # tried only refreshing when the screen was resized but it caused a
+    # noticeable lag when resizing and didn't have an appreciable effect
+    # on system usage
+    
+    panel.CURSES_LOCK.acquire()
+    try:
+      redrawStartTime = time.time()
+      
+      # if sighup received then reload related information
+      if sighupTracker.isReset:
+        #panels["header"]._updateParams(True)
+        
+        # other panels that use torrc data
+        panels["conn"].resetOptions()
+        #if not isBlindMode: panels["graph"].stats["connections"].resetOptions(conn)
+        #panels["graph"].stats["bandwidth"].resetOptions()
+        
+        # if bandwidth graph is being shown then height might have changed
+        if panels["graph"].currentDisplay == "bandwidth":
+          panels["graph"].setHeight(panels["graph"].stats["bandwidth"].getPreferredHeight())
+        
+        panels["torrc"].reset()
+        sighupTracker.isReset = False
+      
+      # gives panels a chance to take advantage of the maximum bounds
+      # originally this checked in the bounds changed but 'recreate' is a no-op
+      # if panel properties are unchanged and checking every redraw is more
+      # resilient in case of funky changes (such as resizing during popups)
+      
+      # hack to make sure header picks layout before using the dimensions below
+      #panels["header"].getPreferredSize()
+      
+      startY = 0
+      for panelKey in PAGE_S[:2]:
+        #panels[panelKey].recreate(stdscr, -1, startY)
+        panels[panelKey].setParent(stdscr)
+        panels[panelKey].setWidth(-1)
+        panels[panelKey].setTop(startY)
+        startY += panels[panelKey].getHeight()
+      
+      panels["popup"].recreate(stdscr, 80, startY)
+      
+      for panelSet in PAGES:
+        tmpStartY = startY
+        
+        for panelKey in panelSet:
+          #panels[panelKey].recreate(stdscr, -1, tmpStartY)
+          panels[panelKey].setParent(stdscr)
+          panels[panelKey].setWidth(-1)
+          panels[panelKey].setTop(tmpStartY)
+          tmpStartY += panels[panelKey].getHeight()
+      
+      # provides a notice if there's been ten seconds since the last BW event
+      lastHeartbeat = torTools.getConn().getHeartbeat()
+      if torTools.getConn().isAlive() and "BW" in torTools.getConn().getControllerEvents() and lastHeartbeat != 0:
+        if not isUnresponsive and (time.time() - lastHeartbeat) >= 10:
+          isUnresponsive = True
+          log.log(log.NOTICE, "Relay unresponsive (last heartbeat: %s)" % time.ctime(lastHeartbeat))
+        elif isUnresponsive and (time.time() - lastHeartbeat) < 10:
+          # really shouldn't happen (meant Tor froze for a bit)
+          isUnresponsive = False
+          log.log(log.NOTICE, "Relay resumed")
+      
+      panels["conn"].reset()
+      
+      # TODO: part two of hack to prevent premature drawing by log panel
+      if page == 0 and not isPaused: panels["log"].setPaused(False)
+      
+      # I haven't the foggiest why, but doesn't work if redrawn out of order...
+      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"):
+            # revised panel (handles its own content refreshing)
+            panels[panelKey].redraw()
+          else:
+            panels[panelKey].redraw(True)
+      
+      stdscr.refresh()
+      
+      currentTime = time.time()
+      if currentTime - lastPerformanceLog >= CONFIG["logging.rate.refreshRate"]:
+        log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds" % (currentTime - redrawStartTime))
+        lastPerformanceLog = currentTime
+    finally:
+      panel.CURSES_LOCK.release()
+    
+    # wait for user keyboard input until timeout (unless an override was set)
+    if overrideKey:
+      key = overrideKey
+      overrideKey = None
+    else:
+      key = stdscr.getch()
+    
+    if key == ord('q') or key == ord('Q'):
+      quitConfirmed = not CONFIRM_QUIT
+      
+      # provides prompt to confirm that arm should exit
+      if CONFIRM_QUIT:
+        panel.CURSES_LOCK.acquire()
+        try:
+          setPauseState(panels, isPaused, page, True)
+          
+          # provides prompt
+          panels["control"].setMsg("Are you sure (q again to confirm)?", curses.A_BOLD)
+          panels["control"].redraw(True)
+          
+          curses.cbreak()
+          confirmationKey = stdscr.getch()
+          quitConfirmed = confirmationKey in (ord('q'), ord('Q'))
+          curses.halfdelay(REFRESH_RATE * 10)
+          
+          panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+          setPauseState(panels, isPaused, page)
+        finally:
+          panel.CURSES_LOCK.release()
+      
+      if quitConfirmed:
+        # quits arm
+        # very occasionally stderr gets "close failed: [Errno 11] Resource temporarily unavailable"
+        # this appears to be a python bug: http://bugs.python.org/issue3014
+        # (haven't seen this is quite some time... mysteriously resolved?)
+        
+        # joins on utility daemon threads - this might take a moment since
+        # the internal threadpools being joined might be sleeping
+        resolver = connections.getResolver("tor") if connections.isResolverAlive("tor") else None
+        if resolver: resolver.stop()  # sets halt flag (returning immediately)
+        hostnames.stop()              # halts and joins on hostname worker thread pool
+        if resolver: resolver.join()  # joins on halted resolver
+        
+        # stops panel daemons
+        panels["header"].stop()
+        panels["header"].join()
+        
+        conn.close() # joins on TorCtl event thread
+        break
+    elif key == curses.KEY_LEFT or key == curses.KEY_RIGHT:
+      # switch page
+      if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
+      else: page = (page + 1) % len(PAGES)
+      
+      # skip connections listing if it's disabled
+      if page == 1 and isBlindMode:
+        if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
+        else: page = (page + 1) % len(PAGES)
+      
+      # pauses panels that aren't visible to prevent events from accumilating
+      # (otherwise they'll wait on the curses lock which might get demanding)
+      setPauseState(panels, isPaused, page)
+      
+      panels["control"].page = page + 1
+      
+      # TODO: this redraw doesn't seem necessary (redraws anyway after this
+      # loop) - look into this when refactoring
+      panels["control"].redraw(True)
+      
+      selectiveRefresh(panels, page)
+    elif key == ord('p') or key == ord('P'):
+      # toggles update freezing
+      panel.CURSES_LOCK.acquire()
+      try:
+        isPaused = not isPaused
+        setPauseState(panels, isPaused, page)
+        panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+      finally:
+        panel.CURSES_LOCK.release()
+      
+      selectiveRefresh(panels, page)
+    elif key == ord('h') or key == ord('H'):
+      # displays popup for current page's controls
+      panel.CURSES_LOCK.acquire()
+      try:
+        setPauseState(panels, isPaused, page, True)
+        
+        # lists commands
+        popup = panels["popup"]
+        popup.clear()
+        popup.win.box()
+        popup.addstr(0, 0, "Page %i Commands:" % (page + 1), curses.A_STANDOUT)
+        
+        pageOverrideKeys = ()
+        
+        if page == 0:
+          graphedStats = panels["graph"].currentDisplay
+          if not graphedStats: graphedStats = "none"
+          popup.addfstr(1, 2, "<b>up arrow</b>: scroll log up a line")
+          popup.addfstr(1, 41, "<b>down arrow</b>: scroll log down a line")
+          popup.addfstr(2, 2, "<b>m</b>: increase graph size")
+          popup.addfstr(2, 41, "<b>n</b>: decrease graph size")
+          popup.addfstr(3, 2, "<b>s</b>: graphed stats (<b>%s</b>)" % graphedStats)
+          popup.addfstr(3, 41, "<b>i</b>: graph update interval (<b>%s</b>)" % graphing.graphPanel.UPDATE_INTERVALS[panels["graph"].updateInterval][0])
+          popup.addfstr(4, 2, "<b>b</b>: graph bounds (<b>%s</b>)" % graphing.graphPanel.BOUND_LABELS[panels["graph"].bounds])
+          popup.addfstr(4, 41, "<b>d</b>: file descriptors")
+          popup.addfstr(5, 2, "<b>e</b>: change logged events")
+          
+          regexLabel = "enabled" if panels["log"].regexFilter else "disabled"
+          popup.addfstr(5, 41, "<b>f</b>: log regex filter (<b>%s</b>)" % regexLabel)
+          
+          pageOverrideKeys = (ord('m'), ord('n'), ord('s'), ord('i'), ord('d'), ord('e'), ord('r'), ord('f'))
+        if page == 1:
+          popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
+          popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
+          popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
+          popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
+          popup.addfstr(3, 2, "<b>enter</b>: connection details")
+          popup.addfstr(3, 41, "<b>d</b>: raw consensus descriptor")
+          
+          listingType = connPanel.LIST_LABEL[panels["conn"].listingType].lower()
+          popup.addfstr(4, 2, "<b>l</b>: listed identity (<b>%s</b>)" % listingType)
+          
+          resolverUtil = connections.getResolver("tor").overwriteResolver
+          if resolverUtil == None: resolverUtil = "auto"
+          else: resolverUtil = connections.CMD_STR[resolverUtil]
+          popup.addfstr(4, 41, "<b>u</b>: resolving utility (<b>%s</b>)" % resolverUtil)
+          
+          allowDnsLabel = "allow" if panels["conn"].allowDNS else "disallow"
+          popup.addfstr(5, 2, "<b>r</b>: permit DNS resolution (<b>%s</b>)" % allowDnsLabel)
+          
+          popup.addfstr(5, 41, "<b>s</b>: sort ordering")
+          popup.addfstr(6, 2, "<b>c</b>: client circuits")
+          
+          #popup.addfstr(5, 41, "c: toggle cursor (<b>%s</b>)" % ("on" if panels["conn"].isCursorEnabled else "off"))
+          
+          pageOverrideKeys = (ord('d'), ord('l'), ord('s'), ord('c'))
+        elif page == 2:
+          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)
+          
+          lineNumLabel = "on" if panels["torrc"].showLineNum else "off"
+          popup.addfstr(3, 41, "<b>n</b>: line numbering (<b>%s</b>)" % lineNumLabel)
+          
+          popup.addfstr(4, 2, "<b>r</b>: reload torrc")
+          popup.addfstr(4, 41, "<b>x</b>: reset tor (issue sighup)")
+        
+        popup.addstr(7, 2, "Press any key...")
+        popup.refresh()
+        
+        # waits for user to hit a key, if it belongs to a command then executes it
+        curses.cbreak()
+        helpExitKey = stdscr.getch()
+        if helpExitKey in pageOverrideKeys: overrideKey = helpExitKey
+        curses.halfdelay(REFRESH_RATE * 10)
+        
+        setPauseState(panels, isPaused, page)
+        selectiveRefresh(panels, page)
+      finally:
+        panel.CURSES_LOCK.release()
+    elif page == 0 and (key == ord('s') or key == ord('S')):
+      # provides menu to pick stats to be graphed
+      #options = ["None"] + [label for label in panels["graph"].stats.keys()]
+      options = ["None"]
+      
+      # appends stats labels with first letters of each word capitalized
+      initialSelection, i = -1, 1
+      if not panels["graph"].currentDisplay: initialSelection = 0
+      graphLabels = panels["graph"].stats.keys()
+      graphLabels.sort()
+      for label in graphLabels:
+        if label == panels["graph"].currentDisplay: initialSelection = i
+        words = label.split()
+        options.append(" ".join(word[0].upper() + word[1:] for word in words))
+        i += 1
+      
+      # hides top label of the graph panel and pauses panels
+      if panels["graph"].currentDisplay:
+        panels["graph"].showLabel = False
+        panels["graph"].redraw(True)
+      setPauseState(panels, isPaused, page, True)
+      
+      selection = showMenu(stdscr, panels["popup"], "Graphed Stats:", options, initialSelection)
+      
+      # reverts changes made for popup
+      panels["graph"].showLabel = True
+      setPauseState(panels, isPaused, page)
+      
+      # applies new setting
+      if selection != -1 and selection != initialSelection:
+        if selection == 0: panels["graph"].setStats(None)
+        else: panels["graph"].setStats(options[selection].lower())
+      
+      selectiveRefresh(panels, page)
+    elif page == 0 and (key == ord('i') or key == ord('I')):
+      # provides menu to pick graph panel update interval
+      options = [label for (label, intervalTime) in graphing.graphPanel.UPDATE_INTERVALS]
+      
+      initialSelection = panels["graph"].updateInterval
+      
+      #initialSelection = -1
+      #for i in range(len(options)):
+      #  if options[i] == panels["graph"].updateInterval: initialSelection = i
+      
+      # hides top label of the graph panel and pauses panels
+      if panels["graph"].currentDisplay:
+        panels["graph"].showLabel = False
+        panels["graph"].redraw(True)
+      setPauseState(panels, isPaused, page, True)
+      
+      selection = showMenu(stdscr, panels["popup"], "Update Interval:", options, initialSelection)
+      
+      # reverts changes made for popup
+      panels["graph"].showLabel = True
+      setPauseState(panels, isPaused, page)
+      
+      # applies new setting
+      if selection != -1: panels["graph"].updateInterval = selection
+      
+      selectiveRefresh(panels, page)
+    elif page == 0 and (key == ord('b') or key == ord('B')):
+      # uses the next boundary type for graph
+      panels["graph"].bounds = (panels["graph"].bounds + 1) % 3
+      
+      selectiveRefresh(panels, page)
+    elif page == 0 and key in (ord('d'), ord('D')):
+      # provides popup with file descriptors
+      panel.CURSES_LOCK.acquire()
+      try:
+        setPauseState(panels, isPaused, page, True)
+        curses.cbreak() # wait indefinitely for key presses (no timeout)
+        
+        fileDescriptorPopup.showFileDescriptorPopup(panels["popup"], stdscr, torPid)
+        
+        setPauseState(panels, isPaused, page)
+        curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+      finally:
+        panel.CURSES_LOCK.release()
+    elif page == 0 and (key == ord('e') or key == ord('E')):
+      # allow user to enter new types of events to log - unchanged if left blank
+      panel.CURSES_LOCK.acquire()
+      try:
+        setPauseState(panels, isPaused, page, True)
+        
+        # provides prompt
+        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
+        popup.recreate(stdscr, 80)
+        
+        popup.clear()
+        popup.win.box()
+        popup.addstr(0, 0, "Event Types:", curses.A_STANDOUT)
+        lineNum = 1
+        for line in logPanel.EVENT_LISTING.split("\n"):
+          line = line[6:]
+          popup.addstr(lineNum, 1, line)
+          lineNum += 1
+        popup.refresh()
+        
+        # gets user input (this blocks monitor updates)
+        eventsInput = panels["control"].win.getstr(0, 15)
+        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 != "":
+          try:
+            expandedEvents = logPanel.expandEvents(eventsInput)
+            loggedEvents = setEventListening(expandedEvents, isBlindMode)
+            panels["log"].loggedEvents = loggedEvents
+          except ValueError, exc:
+            panels["control"].setMsg("Invalid flags: %s" % str(exc), curses.A_STANDOUT)
+            panels["control"].redraw(True)
+            time.sleep(2)
+        
+        # reverts popup dimensions
+        popup.height = 9
+        popup.recreate(stdscr, 80)
+        
+        panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+        setPauseState(panels, isPaused, page)
+      finally:
+        panel.CURSES_LOCK.release()
+    elif page == 0 and (key == ord('f') or key == ord('F')):
+      # provides menu to pick previous regular expression filters or to add a new one
+      # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
+      options = ["None"] + regexFilters + ["New..."]
+      initialSelection = 0 if not panels["log"].regexFilter else 1
+      
+      # hides top label of the graph panel and pauses panels
+      if panels["graph"].currentDisplay:
+        panels["graph"].showLabel = False
+        panels["graph"].redraw(True)
+      setPauseState(panels, isPaused, page, True)
+      
+      selection = showMenu(stdscr, panels["popup"], "Log Filter:", options, initialSelection)
+      
+      # applies new setting
+      if selection == 0:
+        panels["log"].regexFilter = None
+      elif selection == len(options) - 1:
+        # selected 'New...' option - prompt user to input regular expression
+        panel.CURSES_LOCK.acquire()
+        try:
+          # provides prompt
+          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)
+          
+          # reverts visability settings
+          try: curses.curs_set(0)
+          except curses.error: pass
+          curses.noecho()
+          curses.halfdelay(REFRESH_RATE * 10)
+          
+          if regexInput != "":
+            try:
+              panels["log"].regexFilter = re.compile(regexInput)
+              if regexInput in regexFilters: regexFilters.remove(regexInput)
+              regexFilters = [regexInput] + regexFilters
+            except re.error, exc:
+              panels["control"].setMsg("Unable to compile expression: %s" % str(exc), curses.A_STANDOUT)
+              panels["control"].redraw(True)
+              time.sleep(2)
+          panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+        finally:
+          panel.CURSES_LOCK.release()
+      elif selection != -1:
+        try:
+          panels["log"].regexFilter = re.compile(regexFilters[selection - 1])
+          
+          # move selection to top
+          regexFilters = [regexFilters[selection - 1]] + regexFilters
+          del regexFilters[selection]
+        except re.error, exc:
+          # shouldn't happen since we've already checked validity
+          log.log(log.WARN, "Invalid regular expression ('%s': %s) - removing from listing" % (regexFilters[selection - 1], str(exc)))
+          del regexFilters[selection - 1]
+      
+      if len(regexFilters) > MAX_REGEX_FILTERS: del regexFilters[MAX_REGEX_FILTERS:]
+      
+      # reverts changes made for popup
+      panels["graph"].showLabel = True
+      setPauseState(panels, isPaused, page)
+    elif page == 0 and key in (ord('n'), ord('N'), ord('m'), ord('M')):
+      # Unfortunately modifier keys don't work with the up/down arrows (sending
+      # multiple keycodes. The only exception to this is shift + left/right,
+      # but for now just gonna use standard characters.
+      
+      if key in (ord('n'), ord('N')):
+        panels["graph"].setGraphHeight(panels["graph"].graphHeight - 1)
+      else:
+        # don't grow the graph if it's already consuming the whole display
+        # (plus an extra line for the graph/log gap)
+        maxHeight = panels["graph"].parent.getmaxyx()[0] - panels["graph"].top
+        currentHeight = panels["graph"].getHeight()
+        
+        if currentHeight < maxHeight + 1:
+          panels["graph"].setGraphHeight(panels["graph"].graphHeight + 1)
+    elif key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingCounter != -1:
+      # canceling hostname resolution (esc on any page)
+      panels["conn"].listingType = connPanel.LIST_IP
+      panels["control"].resolvingCounter = -1
+      hostnames.setPaused(True)
+      panels["conn"].sortConnections()
+    elif page == 1 and panels["conn"].isCursorEnabled and key in (curses.KEY_ENTER, 10, ord(' ')):
+      # provides details on selected connection
+      panel.CURSES_LOCK.acquire()
+      try:
+        setPauseState(panels, isPaused, page, True)
+        popup = panels["popup"]
+        
+        # reconfigures connection panel to accomidate details dialog
+        panels["conn"].showLabel = False
+        panels["conn"].showingDetails = True
+        panels["conn"].redraw(True)
+        
+        hostnames.setPaused(not panels["conn"].allowDNS)
+        relayLookupCache = {} # temporary cache of entry -> (ns data, desc data)
+        
+        curses.cbreak() # wait indefinitely for key presses (no timeout)
+        key = 0
+        
+        while key not in (curses.KEY_ENTER, 10, ord(' ')):
+          popup.clear()
+          popup.win.box()
+          popup.addstr(0, 0, "Connection Details:", curses.A_STANDOUT)
+          
+          selection = panels["conn"].cursorSelection
+          if not selection or not panels["conn"].connections: break
+          selectionColor = connPanel.TYPE_COLORS[selection[connPanel.CONN_TYPE]]
+          format = uiTools.getColor(selectionColor) | curses.A_BOLD
+          
+          selectedIp = selection[connPanel.CONN_F_IP]
+          selectedPort = selection[connPanel.CONN_F_PORT]
+          selectedIsPrivate = selection[connPanel.CONN_PRIVATE]
+          
+          addrLabel = "address: %s:%s" % (selectedIp, selectedPort)
+          
+          if selection[connPanel.CONN_TYPE] == "family" and int(selection[connPanel.CONN_L_PORT]) > 65535:
+            # unresolved family entry - unknown ip/port
+            addrLabel = "address: unknown"
+          
+          if selectedIsPrivate: hostname = None
+          else:
+            try: hostname = hostnames.resolve(selectedIp)
+            except ValueError: hostname = "unknown" # hostname couldn't be resolved
+          
+          if hostname == None:
+            if hostnames.isPaused() or selectedIsPrivate: hostname = "DNS resolution disallowed"
+            else:
+              # if hostname is still being resolved refresh panel every half-second until it's completed
+              curses.halfdelay(5)
+              hostname = "resolving..."
+          elif len(hostname) > 73 - len(addrLabel):
+            # hostname too long - truncate
+            hostname = "%s..." % hostname[:70 - len(addrLabel)]
+          
+          if selectedIsPrivate:
+            popup.addstr(1, 2, "address: <scrubbed> (unknown)", format)
+            popup.addstr(2, 2, "locale: ??", format)
+            popup.addstr(3, 2, "No consensus data found", format)
+          else:
+            popup.addstr(1, 2, "%s (%s)" % (addrLabel, hostname), format)
+            
+            locale = selection[connPanel.CONN_COUNTRY]
+            popup.addstr(2, 2, "locale: %s" % locale, format)
+            
+            # provides consensus data for selection (needs fingerprint to get anywhere...)
+            fingerprint = panels["conn"].getFingerprint(selectedIp, selectedPort)
+            
+            if fingerprint == "UNKNOWN":
+              if selectedIp not in panels["conn"].fingerprintMappings.keys():
+                # no consensus entry for this ip address
+                popup.addstr(3, 2, "No consensus data found", format)
+              else:
+                # couldn't resolve due to multiple matches - list them all
+                popup.addstr(3, 2, "Muliple matches, possible fingerprints are:", format)
+                matchings = panels["conn"].fingerprintMappings[selectedIp]
+                
+                line = 4
+                for (matchPort, matchFingerprint, matchNickname) in matchings:
+                  popup.addstr(line, 2, "%i. or port: %-5s fingerprint: %s" % (line - 3, matchPort, matchFingerprint), format)
+                  line += 1
+                  
+                  if line == 7 and len(matchings) > 4:
+                    popup.addstr(8, 2, "... %i more" % len(matchings) - 3, format)
+                    break
+            else:
+              # fingerprint found - retrieve related data
+              lookupErrored = False
+              if selection in relayLookupCache.keys(): nsEntry, descEntry = relayLookupCache[selection]
+              else:
+                try:
+                  nsCall = conn.get_network_status("id/%s" % fingerprint)
+                  if len(nsCall) == 0: raise TorCtl.ErrorReply() # no results provided
+                except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
+                  # ns lookup fails or provides empty results - can happen with
+                  # localhost lookups if relay's having problems (orport not
+                  # reachable) and this will be empty if network consensus
+                  # couldn't be fetched
+                  lookupErrored = True
+                
+                if not lookupErrored and nsCall:
+                  if len(nsCall) > 1:
+                    # multiple records for fingerprint (shouldn't happen)
+                    log.log(log.WARN, "Multiple consensus entries for fingerprint: %s" % fingerprint)
+                  
+                  nsEntry = nsCall[0]
+                  
+                  try:
+                    descLookupCmd = "desc/id/%s" % fingerprint
+                    descEntry = TorCtl.Router.build_from_desc(conn.get_info(descLookupCmd)[descLookupCmd].split("\n"), nsEntry)
+                    relayLookupCache[selection] = (nsEntry, descEntry)
+                  except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): lookupErrored = True # desc lookup failed
+              
+              if lookupErrored:
+                popup.addstr(3, 2, "Unable to retrieve consensus data", format)
+              else:
+                popup.addstr(2, 15, "fingerprint: %s" % fingerprint, format)
+                
+                nickname = panels["conn"].getNickname(selectedIp, selectedPort)
+                dirPortLabel = "dirport: %i" % nsEntry.dirport if nsEntry.dirport else ""
+                popup.addstr(3, 2, "nickname: %-25s orport: %-10i %s" % (nickname, nsEntry.orport, dirPortLabel), format)
+                
+                popup.addstr(4, 2, "published: %-24s os: %-14s version: %s" % (descEntry.published, descEntry.os, descEntry.version), format)
+                popup.addstr(5, 2, "flags: %s" % ", ".join(nsEntry.flags), format)
+                
+                exitLine = ", ".join([str(k) for k in descEntry.exitpolicy])
+                if len(exitLine) > 63: exitLine = "%s..." % exitLine[:60]
+                popup.addstr(6, 2, "exit policy: %s" % exitLine, format)
+                
+                if descEntry.contact:
+                  # clears up some common obscuring
+                  contactAddr = descEntry.contact
+                  obscuring = [(" at ", "@"), (" AT ", "@"), ("AT", "@"), (" dot ", "."), (" DOT ", ".")]
+                  for match, replace in obscuring: contactAddr = contactAddr.replace(match, replace)
+                  if len(contactAddr) > 67: contactAddr = "%s..." % contactAddr[:64]
+                  popup.addstr(7, 2, "contact: %s" % contactAddr, format)
+            
+          popup.refresh()
+          key = stdscr.getch()
+          
+          if key == curses.KEY_RIGHT: key = curses.KEY_DOWN
+          elif key == curses.KEY_LEFT: key = curses.KEY_UP
+          
+          if key in (curses.KEY_DOWN, curses.KEY_UP, curses.KEY_PPAGE, curses.KEY_NPAGE):
+            panels["conn"].handleKey(key)
+          elif key in (ord('d'), ord('D')):
+            descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, conn, panels["conn"])
+            panels["conn"].redraw(True)
+        
+        panels["conn"].showLabel = True
+        panels["conn"].showingDetails = False
+        hostnames.setPaused(not panels["conn"].allowDNS and panels["conn"].listingType == connPanel.LIST_HOSTNAME)
+        setPauseState(panels, isPaused, page)
+        curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+      finally:
+        panel.CURSES_LOCK.release()
+    elif page == 1 and panels["conn"].isCursorEnabled and key in (ord('d'), ord('D')):
+      # presents popup for raw consensus data
+      panel.CURSES_LOCK.acquire()
+      try:
+        setPauseState(panels, isPaused, page, True)
+        curses.cbreak() # wait indefinitely for key presses (no timeout)
+        panels["conn"].showLabel = False
+        panels["conn"].redraw(True)
+        
+        descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, conn, panels["conn"])
+        
+        setPauseState(panels, isPaused, page)
+        curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+        panels["conn"].showLabel = True
+      finally:
+        panel.CURSES_LOCK.release()
+    elif page == 1 and (key == ord('l') or key == ord('L')):
+      # provides menu to pick identification info listed for connections
+      optionTypes = [connPanel.LIST_IP, connPanel.LIST_HOSTNAME, connPanel.LIST_FINGERPRINT, connPanel.LIST_NICKNAME]
+      options = [connPanel.LIST_LABEL[sortType] for sortType in optionTypes]
+      initialSelection = panels["conn"].listingType   # enums correspond to index
+      
+      # hides top label of conn panel and pauses panels
+      panels["conn"].showLabel = False
+      panels["conn"].redraw(True)
+      setPauseState(panels, isPaused, page, True)
+      
+      selection = showMenu(stdscr, panels["popup"], "List By:", options, initialSelection)
+      
+      # reverts changes made for popup
+      panels["conn"].showLabel = True
+      setPauseState(panels, isPaused, page)
+      
+      # applies new setting
+      if selection != -1 and optionTypes[selection] != panels["conn"].listingType:
+        panels["conn"].listingType = optionTypes[selection]
+        
+        if panels["conn"].listingType == connPanel.LIST_HOSTNAME:
+          curses.halfdelay(10) # refreshes display every second until done resolving
+          panels["control"].resolvingCounter = hostnames.getRequestCount() - hostnames.getPendingCount()
+          
+          hostnames.setPaused(not panels["conn"].allowDNS)
+          for connEntry in panels["conn"].connections:
+            try: hostnames.resolve(connEntry[connPanel.CONN_F_IP])
+            except ValueError: pass
+        else:
+          panels["control"].resolvingCounter = -1
+          hostnames.setPaused(True)
+        
+        panels["conn"].sortConnections()
+    elif page == 1 and (key == ord('u') or key == ord('U')):
+      # provides menu to pick identification resolving utility
+      optionTypes = [None, connections.CMD_NETSTAT, connections.CMD_SS, connections.CMD_LSOF]
+      options = ["auto"] + [connections.CMD_STR[util] for util in optionTypes[1:]]
+      
+      initialSelection = connections.getResolver("tor").overwriteResolver # enums correspond to indices
+      if initialSelection == None: initialSelection = 0
+      
+      # hides top label of conn panel and pauses panels
+      panels["conn"].showLabel = False
+      panels["conn"].redraw(True)
+      setPauseState(panels, isPaused, page, True)
+      
+      selection = showMenu(stdscr, panels["popup"], "Resolver Util:", options, initialSelection)
+      
+      # reverts changes made for popup
+      panels["conn"].showLabel = True
+      setPauseState(panels, isPaused, page)
+      
+      # applies new setting
+      if selection != -1 and optionTypes[selection] != connections.getResolver("tor").overwriteResolver:
+        connections.getResolver("tor").overwriteResolver = optionTypes[selection]
+    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()
+    elif page == 1 and (key == ord('c') or key == ord('C')):
+      # displays popup with client circuits
+      clientCircuits = None
+      try:
+        clientCircuits = conn.get_info("circuit-status")["circuit-status"].split("\n")
+      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
+      
+      maxEntryLength = 0
+      if clientCircuits:
+        for clientEntry in clientCircuits: maxEntryLength = max(len(clientEntry), maxEntryLength)
+      
+      panel.CURSES_LOCK.acquire()
+      try:
+        setPauseState(panels, isPaused, page, True)
+        
+        # makes sure there's room for the longest entry
+        popup = panels["popup"]
+        if clientCircuits and maxEntryLength + 4 > popup.getPreferredSize()[1]:
+          popup.height = max(popup.height, len(clientCircuits) + 3)
+          popup.recreate(stdscr, maxEntryLength + 4)
+        
+        # lists commands
+        popup.clear()
+        popup.win.box()
+        popup.addstr(0, 0, "Client Circuits:", curses.A_STANDOUT)
+        
+        if clientCircuits == None:
+          popup.addstr(1, 2, "Unable to retireve current circuits")
+        elif len(clientCircuits) == 1 and clientCircuits[0] == "":
+          popup.addstr(1, 2, "No active client circuits")
+        else:
+          line = 1
+          for clientEntry in clientCircuits:
+            popup.addstr(line, 2, clientEntry)
+            line += 1
+            
+        popup.addstr(popup.height - 2, 2, "Press any key...")
+        popup.refresh()
+        
+        curses.cbreak()
+        stdscr.getch()
+        curses.halfdelay(REFRESH_RATE * 10)
+        
+        # reverts popup dimensions
+        popup.height = 9
+        popup.recreate(stdscr, 80)
+        
+        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)
+      
+      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 == 2 and (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" % str(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 page == 0:
+      panels["log"].handleKey(key)
+    elif page == 1:
+      panels["conn"].handleKey(key)
+    elif page == 2:
+      panels["torrc"].handleKey(key)
+
+def startTorMonitor(loggedEvents, isBlindMode):
+  try:
+    curses.wrapper(drawTorMonitor, loggedEvents, isBlindMode)
+  except KeyboardInterrupt:
+    pass # skip printing stack trace in case of keyboard interrupt
+

Deleted: arm/trunk/src/interface/graphing/bandwidthStats.py
===================================================================
--- arm/trunk/interface/graphing/bandwidthStats.py	2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/interface/graphing/bandwidthStats.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,354 +0,0 @@
-"""
-Tracks bandwidth usage of the tor process, expanding to include accounting
-stats if they're set.
-"""
-
-import time
-
-import graphPanel
-from util import log, sysTools, torTools, uiTools
-
-DL_COLOR, UL_COLOR = "green", "cyan"
-
-# width at which panel abandons placing optional stats (avg and total) with
-# header in favor of replacing the x-axis label
-COLLAPSE_WIDTH = 135
-
-# valid keys for the accountingInfo mapping
-ACCOUNTING_ARGS = ("status", "resetTime", "read", "written", "readLimit", "writtenLimit")
-
-PREPOPULATE_SUCCESS_MSG = "Read the last day of bandwidth history from the state file"
-PREPOPULATE_FAILURE_MSG = "Unable to prepopulate bandwidth information (%s)"
-
-DEFAULT_CONFIG = {"features.graph.bw.accounting.show": True, "features.graph.bw.accounting.rate": 10, "features.graph.bw.accounting.isTimeLong": False, "log.graph.bw.prepopulateSuccess": log.NOTICE, "log.graph.bw.prepopulateFailure": log.NOTICE}
-
-class BandwidthStats(graphPanel.GraphStats):
-  """
-  Uses tor BW events to generate bandwidth usage graph.
-  """
-  
-  def __init__(self, config=None):
-    graphPanel.GraphStats.__init__(self)
-    
-    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"])
-    
-    # accounting data (set by _updateAccountingInfo method)
-    self.accountingLastUpdated = 0
-    self.accountingInfo = dict([(arg, "") for arg in ACCOUNTING_ARGS])
-    
-    # listens for tor reload (sighup) events which can reset the bandwidth
-    # rate/burst and if tor's using accounting
-    conn = torTools.getConn()
-    self._titleStats, self.isAccounting = [], False
-    self.resetListener(conn, torTools.TOR_INIT) # initializes values
-    conn.addStatusListener(self.resetListener)
-  
-  def resetListener(self, conn, eventType):
-    # updates title parameters and accounting status if they changed
-    self._titleStats = []     # force reset of title
-    self.new_desc_event(None) # updates title params
-    
-    if self._config["features.graph.bw.accounting.show"]:
-      self.isAccounting = conn.getInfo('accounting/enabled') == '1'
-  
-  def prepopulateFromState(self):
-    """
-    Attempts to use tor's state file to prepopulate values for the 15 minute
-    interval via the BWHistoryReadValues/BWHistoryWriteValues values. This
-    returns True if successful and False otherwise.
-    """
-    
-    # checks that this is a relay (if ORPort is unset, then skip)
-    conn = torTools.getConn()
-    orPort = conn.getOption("ORPort")
-    if orPort == "0": return
-    
-    # gets the uptime (using the same parameters as the header panel to take
-    # advantage of caching
-    uptime = None
-    queryPid = conn.getMyPid()
-    if queryPid:
-      queryParam = ["%cpu", "rss", "%mem", "etime"]
-      queryCmd = "ps -p %s -o %s" % (queryPid, ",".join(queryParam))
-      psCall = sysTools.call(queryCmd, 3600, True)
-      
-      if psCall and len(psCall) == 2:
-        stats = psCall[1].strip().split()
-        if len(stats) == 4: uptime = stats[3]
-    
-    # checks if tor has been running for at least a day, the reason being that
-    # the state tracks a day's worth of data and this should only prepopulate
-    # results associated with this tor instance
-    if not uptime or not "-" in uptime:
-      msg = PREPOPULATE_FAILURE_MSG % "insufficient uptime"
-      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
-      return False
-    
-    # get the user's data directory (usually '~/.tor')
-    dataDir = conn.getOption("DataDirectory")
-    if not dataDir:
-      msg = PREPOPULATE_FAILURE_MSG % "data directory not found"
-      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
-      return False
-    
-    # attempt to open the state file
-    try: stateFile = open("%s/state" % dataDir, "r")
-    except IOError:
-      msg = PREPOPULATE_FAILURE_MSG % "unable to read the state file"
-      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
-      return False
-    
-    # get the BWHistory entries (ordered oldest to newest) and number of
-    # intervals since last recorded
-    bwReadEntries, bwWriteEntries = None, None
-    missingReadEntries, missingWriteEntries = None, None
-    
-    # converts from gmt to local with respect to DST
-    if time.localtime()[8]: tz_offset = time.altzone
-    else: tz_offset = time.timezone
-    
-    for line in stateFile:
-      line = line.strip()
-      
-      if line.startswith("BWHistoryReadValues"):
-        bwReadEntries = line[20:].split(",")
-        bwReadEntries = [int(entry) / 1024.0 / 900 for entry in bwReadEntries]
-      elif line.startswith("BWHistoryWriteValues"):
-        bwWriteEntries = line[21:].split(",")
-        bwWriteEntries = [int(entry) / 1024.0 / 900 for entry in bwWriteEntries]
-      elif line.startswith("BWHistoryReadEnds"):
-        lastReadTime = time.mktime(time.strptime(line[18:], "%Y-%m-%d %H:%M:%S")) - tz_offset
-        missingReadEntries = int((time.time() - lastReadTime) / 900)
-      elif line.startswith("BWHistoryWriteEnds"):
-        lastWriteTime = time.mktime(time.strptime(line[19:], "%Y-%m-%d %H:%M:%S")) - tz_offset
-        missingWriteEntries = int((time.time() - lastWriteTime) / 900)
-    
-    if not bwReadEntries or not bwWriteEntries or not lastReadTime or not lastWriteTime:
-      msg = PREPOPULATE_FAILURE_MSG % "bandwidth stats missing from state file"
-      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
-      return False
-    
-    # fills missing entries with the last value
-    bwReadEntries += [bwReadEntries[-1]] * missingReadEntries
-    bwWriteEntries += [bwWriteEntries[-1]] * missingWriteEntries
-    
-    # crops starting entries so they're the same size
-    entryCount = min(len(bwReadEntries), len(bwWriteEntries), self.maxCol)
-    bwReadEntries = bwReadEntries[len(bwReadEntries) - entryCount:]
-    bwWriteEntries = bwWriteEntries[len(bwWriteEntries) - entryCount:]
-    
-    # gets index for 15-minute interval
-    intervalIndex = 0
-    for indexEntry in graphPanel.UPDATE_INTERVALS:
-      if indexEntry[1] == 900: break
-      else: intervalIndex += 1
-    
-    # fills the graphing parameters with state information
-    for i in range(entryCount):
-      readVal, writeVal = bwReadEntries[i], bwWriteEntries[i]
-      
-      self.lastPrimary, self.lastSecondary = readVal, writeVal
-      self.primaryTotal += readVal * 900
-      self.secondaryTotal += writeVal * 900
-      self.tick += 900
-      
-      self.primaryCounts[intervalIndex].insert(0, readVal)
-      self.secondaryCounts[intervalIndex].insert(0, writeVal)
-    
-    self.maxPrimary[intervalIndex] = max(self.primaryCounts)
-    self.maxSecondary[intervalIndex] = max(self.secondaryCounts)
-    del self.primaryCounts[intervalIndex][self.maxCol + 1:]
-    del self.secondaryCounts[intervalIndex][self.maxCol + 1:]
-    
-    msg = PREPOPULATE_SUCCESS_MSG
-    missingSec = time.time() - min(lastReadTime, lastWriteTime)
-    if missingSec: msg += " (%s is missing)" % uiTools.getTimeLabel(missingSec, 0, True)
-    log.log(self._config["log.graph.bw.prepopulateSuccess"], msg)
-    
-    return True
-  
-  def bandwidth_event(self, event):
-    if self.isAccounting and self.isNextTickRedraw():
-      if time.time() - self.accountingLastUpdated >= self._config["features.graph.bw.accounting.rate"]:
-        self._updateAccountingInfo()
-    
-    # scales units from B to KB for graphing
-    self._processEvent(event.read / 1024.0, event.written / 1024.0)
-  
-  def draw(self, panel, width, height):
-    # if display is narrow, overwrites x-axis labels with avg / total stats
-    if width <= COLLAPSE_WIDTH:
-      # clears line
-      panel.addstr(8, 0, " " * width)
-      graphCol = min((width - 10) / 2, self.maxCol)
-      
-      primaryFooter = "%s, %s" % (self._getAvgLabel(True), self._getTotalLabel(True))
-      secondaryFooter = "%s, %s" % (self._getAvgLabel(False), self._getTotalLabel(False))
-      
-      panel.addstr(8, 1, primaryFooter, uiTools.getColor(self.getColor(True)))
-      panel.addstr(8, graphCol + 6, secondaryFooter, uiTools.getColor(self.getColor(False)))
-    
-    # provides accounting stats if enabled
-    if self.isAccounting:
-      if torTools.getConn().isAlive():
-        status = self.accountingInfo["status"]
-        
-        hibernateColor = "green"
-        if status == "soft": hibernateColor = "yellow"
-        elif status == "hard": hibernateColor = "red"
-        elif status == "":
-          # failed to be queried
-          status, hibernateColor = "unknown", "red"
-        
-        panel.addfstr(10, 0, "<b>Accounting (<%s>%s</%s>)</b>" % (hibernateColor, status, hibernateColor))
-        
-        resetTime = self.accountingInfo["resetTime"]
-        if not resetTime: resetTime = "unknown"
-        panel.addstr(10, 35, "Time to reset: %s" % resetTime)
-        
-        used, total = self.accountingInfo["read"], self.accountingInfo["readLimit"]
-        if used and total:
-          panel.addstr(11, 2, "%s / %s" % (used, total), uiTools.getColor(self.getColor(True)))
-        
-        used, total = self.accountingInfo["written"], self.accountingInfo["writtenLimit"]
-        if used and total:
-          panel.addstr(11, 37, "%s / %s" % (used, total), uiTools.getColor(self.getColor(False)))
-      else:
-        panel.addfstr(10, 0, "<b>Accounting:</b> Connection Closed...")
-  
-  def getTitle(self, width):
-    stats = list(self._titleStats)
-    
-    while True:
-      if not stats: return "Bandwidth:"
-      else:
-        label = "Bandwidth (%s):" % ", ".join(stats)
-        
-        if len(label) > width: del stats[-1]
-        else: return label
-  
-  def getHeaderLabel(self, width, isPrimary):
-    graphType = "Downloaded" if isPrimary else "Uploaded"
-    stats = [""]
-    
-    # if wide then avg and total are part of the header, otherwise they're on
-    # the x-axis
-    if width * 2 > COLLAPSE_WIDTH:
-      stats = [""] * 3
-      stats[1] = "- %s" % self._getAvgLabel(isPrimary)
-      stats[2] = ", %s" % self._getTotalLabel(isPrimary)
-    
-    stats[0] = "%-14s" % ("%s/sec" % uiTools.getSizeLabel((self.lastPrimary if isPrimary else self.lastSecondary) * 1024, 1))
-    
-    # drops label's components if there's not enough space
-    labeling = graphType + " (" + "".join(stats).strip() + "):"
-    while len(labeling) >= width:
-      if len(stats) > 1:
-        del stats[-1]
-        labeling = graphType + " (" + "".join(stats).strip() + "):"
-      else:
-        labeling = graphType + ":"
-        break
-    
-    return labeling
-  
-  def getColor(self, isPrimary):
-    return DL_COLOR if isPrimary else UL_COLOR
-  
-  def getPreferredHeight(self):
-    return 13 if self.isAccounting else 10
-  
-  def new_desc_event(self, event):
-    # updates self._titleStats with updated values
-    conn = torTools.getConn()
-    if not conn.isAlive(): return # keep old values
-    
-    myFingerprint = conn.getMyFingerprint()
-    if not self._titleStats or not myFingerprint or (event and myFingerprint in event.idlist):
-      stats = []
-      bwRate = conn.getMyBandwidthRate()
-      bwBurst = conn.getMyBandwidthBurst()
-      bwObserved = conn.getMyBandwidthObserved()
-      bwMeasured = conn.getMyBandwidthMeasured()
-      
-      if bwRate and bwBurst:
-        bwRateLabel = uiTools.getSizeLabel(bwRate, 1)
-        bwBurstLabel = uiTools.getSizeLabel(bwBurst, 1)
-        
-        # if both are using rounded values then strip off the ".0" decimal
-        if ".0" in bwRateLabel and ".0" in bwBurstLabel:
-          bwRateLabel = bwRateLabel.replace(".0", "")
-          bwBurstLabel = bwBurstLabel.replace(".0", "")
-        
-        stats.append("limit: %s" % bwRateLabel)
-        stats.append("burst: %s" % bwBurstLabel)
-      
-      # Provide the observed bandwidth either if the measured bandwidth isn't
-      # available or if the measured bandwidth is the observed (this happens
-      # if there isn't yet enough bandwidth measurements).
-      if bwObserved and (not bwMeasured or bwMeasured == bwObserved):
-        stats.append("observed: %s" % uiTools.getSizeLabel(bwObserved, 1))
-      elif bwMeasured:
-        stats.append("measured: %s" % uiTools.getSizeLabel(bwMeasured, 1))
-      
-      self._titleStats = stats
-  
-  def _getAvgLabel(self, isPrimary):
-    total = self.primaryTotal if isPrimary else self.secondaryTotal
-    return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick)) * 1024, 1)
-  
-  def _getTotalLabel(self, isPrimary):
-    total = self.primaryTotal if isPrimary else self.secondaryTotal
-    return "total: %s" % uiTools.getSizeLabel(total * 1024, 1)
-  
-  def _updateAccountingInfo(self):
-    """
-    Updates mapping used for accounting info. This includes the following keys:
-    status, resetTime, read, written, readLimit, writtenLimit
-    
-    Any failed lookups result in a mapping to an empty string.
-    """
-    
-    conn = torTools.getConn()
-    queried = dict([(arg, "") for arg in ACCOUNTING_ARGS])
-    queried["status"] = conn.getInfo("accounting/hibernating")
-    
-    # provides a nicely formatted reset time
-    endInterval = conn.getInfo("accounting/interval-end")
-    if endInterval:
-      # converts from gmt to local with respect to DST
-      if time.localtime()[8]: tz_offset = time.altzone
-      else: tz_offset = time.timezone
-      
-      sec = time.mktime(time.strptime(endInterval, "%Y-%m-%d %H:%M:%S")) - time.time() - tz_offset
-      if self._config["features.graph.bw.accounting.isTimeLong"]:
-        queried["resetTime"] = ", ".join(uiTools.getTimeLabels(sec, True))
-      else:
-        days = sec / 86400
-        sec %= 86400
-        hours = sec / 3600
-        sec %= 3600
-        minutes = sec / 60
-        sec %= 60
-        queried["resetTime"] = "%i:%02i:%02i:%02i" % (days, hours, minutes, sec)
-    
-    # number of bytes used and in total for the accounting period
-    used = conn.getInfo("accounting/bytes")
-    left = conn.getInfo("accounting/bytes-left")
-    
-    if used and left:
-      usedComp, leftComp = used.split(" "), left.split(" ")
-      read, written = int(usedComp[0]), int(usedComp[1])
-      readLeft, writtenLeft = int(leftComp[0]), int(leftComp[1])
-      
-      queried["read"] = uiTools.getSizeLabel(read)
-      queried["written"] = uiTools.getSizeLabel(written)
-      queried["readLimit"] = uiTools.getSizeLabel(read + readLeft)
-      queried["writtenLimit"] = uiTools.getSizeLabel(written + writtenLeft)
-    
-    self.accountingInfo = queried
-    self.accountingLastUpdated = time.time()
-

Copied: arm/trunk/src/interface/graphing/bandwidthStats.py (from rev 22998, arm/trunk/interface/graphing/bandwidthStats.py)
===================================================================
--- arm/trunk/src/interface/graphing/bandwidthStats.py	                        (rev 0)
+++ arm/trunk/src/interface/graphing/bandwidthStats.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,368 @@
+"""
+Tracks bandwidth usage of the tor process, expanding to include accounting
+stats if they're set.
+"""
+
+import time
+
+import graphPanel
+from util import log, sysTools, torTools, uiTools
+
+DL_COLOR, UL_COLOR = "green", "cyan"
+
+# width at which panel abandons placing optional stats (avg and total) with
+# header in favor of replacing the x-axis label
+COLLAPSE_WIDTH = 135
+
+# valid keys for the accountingInfo mapping
+ACCOUNTING_ARGS = ("status", "resetTime", "read", "written", "readLimit", "writtenLimit")
+
+PREPOPULATE_SUCCESS_MSG = "Read the last day of bandwidth history from the state file"
+PREPOPULATE_FAILURE_MSG = "Unable to prepopulate bandwidth information (%s)"
+
+DEFAULT_CONFIG = {"features.graph.bw.accounting.show": True, "features.graph.bw.accounting.rate": 10, "features.graph.bw.accounting.isTimeLong": False, "log.graph.bw.prepopulateSuccess": log.NOTICE, "log.graph.bw.prepopulateFailure": log.NOTICE}
+
+class BandwidthStats(graphPanel.GraphStats):
+  """
+  Uses tor BW events to generate bandwidth usage graph.
+  """
+  
+  def __init__(self, config=None):
+    graphPanel.GraphStats.__init__(self)
+    
+    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"])
+    
+    # accounting data (set by _updateAccountingInfo method)
+    self.accountingLastUpdated = 0
+    self.accountingInfo = dict([(arg, "") for arg in ACCOUNTING_ARGS])
+    
+    # listens for tor reload (sighup) events which can reset the bandwidth
+    # rate/burst and if tor's using accounting
+    conn = torTools.getConn()
+    self._titleStats, self.isAccounting = [], False
+    self.resetListener(conn, torTools.TOR_INIT) # initializes values
+    conn.addStatusListener(self.resetListener)
+  
+  def resetListener(self, conn, eventType):
+    # updates title parameters and accounting status if they changed
+    self._titleStats = []     # force reset of title
+    self.new_desc_event(None) # updates title params
+    
+    if eventType == torTools.TOR_INIT and self._config["features.graph.bw.accounting.show"]:
+      self.isAccounting = conn.getInfo('accounting/enabled') == '1'
+  
+  def prepopulateFromState(self):
+    """
+    Attempts to use tor's state file to prepopulate values for the 15 minute
+    interval via the BWHistoryReadValues/BWHistoryWriteValues values. This
+    returns True if successful and False otherwise.
+    """
+    
+    # checks that this is a relay (if ORPort is unset, then skip)
+    conn = torTools.getConn()
+    orPort = conn.getOption("ORPort")
+    if orPort == "0": return
+    
+    # gets the uptime (using the same parameters as the header panel to take
+    # advantage of caching
+    uptime = None
+    queryPid = conn.getMyPid()
+    if queryPid:
+      queryParam = ["%cpu", "rss", "%mem", "etime"]
+      queryCmd = "ps -p %s -o %s" % (queryPid, ",".join(queryParam))
+      psCall = sysTools.call(queryCmd, 3600, True)
+      
+      if psCall and len(psCall) == 2:
+        stats = psCall[1].strip().split()
+        if len(stats) == 4: uptime = stats[3]
+    
+    # checks if tor has been running for at least a day, the reason being that
+    # the state tracks a day's worth of data and this should only prepopulate
+    # results associated with this tor instance
+    if not uptime or not "-" in uptime:
+      msg = PREPOPULATE_FAILURE_MSG % "insufficient uptime"
+      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
+      return False
+    
+    # get the user's data directory (usually '~/.tor')
+    dataDir = conn.getOption("DataDirectory")
+    if not dataDir:
+      msg = PREPOPULATE_FAILURE_MSG % "data directory not found"
+      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
+      return False
+    
+    # attempt to open the state file
+    try: stateFile = open("%s/state" % dataDir, "r")
+    except IOError:
+      msg = PREPOPULATE_FAILURE_MSG % "unable to read the state file"
+      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
+      return False
+    
+    # get the BWHistory entries (ordered oldest to newest) and number of
+    # intervals since last recorded
+    bwReadEntries, bwWriteEntries = None, None
+    missingReadEntries, missingWriteEntries = None, None
+    
+    # converts from gmt to local with respect to DST
+    if time.localtime()[8]: tz_offset = time.altzone
+    else: tz_offset = time.timezone
+    
+    for line in stateFile:
+      line = line.strip()
+      
+      # According to the rep_hist_update_state() function the BWHistory*Ends
+      # correspond to the start of the following sampling period. Also, the
+      # most recent values of BWHistory*Values appear to be an incremental
+      # counter for the current sampling period. Hence, offsets are added to
+      # account for both.
+      
+      if line.startswith("BWHistoryReadValues"):
+        bwReadEntries = line[20:].split(",")
+        bwReadEntries = [int(entry) / 1024.0 / 900 for entry in bwReadEntries]
+        bwReadEntries.pop()
+      elif line.startswith("BWHistoryWriteValues"):
+        bwWriteEntries = line[21:].split(",")
+        bwWriteEntries = [int(entry) / 1024.0 / 900 for entry in bwWriteEntries]
+        bwWriteEntries.pop()
+      elif line.startswith("BWHistoryReadEnds"):
+        lastReadTime = time.mktime(time.strptime(line[18:], "%Y-%m-%d %H:%M:%S")) - tz_offset
+        lastReadTime -= 900
+        missingReadEntries = int((time.time() - lastReadTime) / 900)
+      elif line.startswith("BWHistoryWriteEnds"):
+        lastWriteTime = time.mktime(time.strptime(line[19:], "%Y-%m-%d %H:%M:%S")) - tz_offset
+        lastWriteTime -= 900
+        missingWriteEntries = int((time.time() - lastWriteTime) / 900)
+    
+    if not bwReadEntries or not bwWriteEntries or not lastReadTime or not lastWriteTime:
+      msg = PREPOPULATE_FAILURE_MSG % "bandwidth stats missing from state file"
+      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
+      return False
+    
+    # fills missing entries with the last value
+    bwReadEntries += [bwReadEntries[-1]] * missingReadEntries
+    bwWriteEntries += [bwWriteEntries[-1]] * missingWriteEntries
+    
+    # crops starting entries so they're the same size
+    entryCount = min(len(bwReadEntries), len(bwWriteEntries), self.maxCol)
+    bwReadEntries = bwReadEntries[len(bwReadEntries) - entryCount:]
+    bwWriteEntries = bwWriteEntries[len(bwWriteEntries) - entryCount:]
+    
+    # gets index for 15-minute interval
+    intervalIndex = 0
+    for indexEntry in graphPanel.UPDATE_INTERVALS:
+      if indexEntry[1] == 900: break
+      else: intervalIndex += 1
+    
+    # fills the graphing parameters with state information
+    for i in range(entryCount):
+      readVal, writeVal = bwReadEntries[i], bwWriteEntries[i]
+      
+      self.lastPrimary, self.lastSecondary = readVal, writeVal
+      self.primaryTotal += readVal * 900
+      self.secondaryTotal += writeVal * 900
+      self.tick += 900
+      
+      self.primaryCounts[intervalIndex].insert(0, readVal)
+      self.secondaryCounts[intervalIndex].insert(0, writeVal)
+    
+    self.maxPrimary[intervalIndex] = max(self.primaryCounts)
+    self.maxSecondary[intervalIndex] = max(self.secondaryCounts)
+    del self.primaryCounts[intervalIndex][self.maxCol + 1:]
+    del self.secondaryCounts[intervalIndex][self.maxCol + 1:]
+    
+    msg = PREPOPULATE_SUCCESS_MSG
+    missingSec = time.time() - min(lastReadTime, lastWriteTime)
+    if missingSec: msg += " (%s is missing)" % uiTools.getTimeLabel(missingSec, 0, True)
+    log.log(self._config["log.graph.bw.prepopulateSuccess"], msg)
+    
+    return True
+  
+  def bandwidth_event(self, event):
+    if self.isAccounting and self.isNextTickRedraw():
+      if time.time() - self.accountingLastUpdated >= self._config["features.graph.bw.accounting.rate"]:
+        self._updateAccountingInfo()
+    
+    # scales units from B to KB for graphing
+    self._processEvent(event.read / 1024.0, event.written / 1024.0)
+  
+  def draw(self, panel, width, height):
+    # line of the graph's x-axis labeling
+    labelingLine = graphPanel.GraphStats.getContentHeight(self) + panel.graphHeight - 2
+    
+    # if display is narrow, overwrites x-axis labels with avg / total stats
+    if width <= COLLAPSE_WIDTH:
+      # clears line
+      panel.addstr(labelingLine, 0, " " * width)
+      graphCol = min((width - 10) / 2, self.maxCol)
+      
+      primaryFooter = "%s, %s" % (self._getAvgLabel(True), self._getTotalLabel(True))
+      secondaryFooter = "%s, %s" % (self._getAvgLabel(False), self._getTotalLabel(False))
+      
+      panel.addstr(labelingLine, 1, primaryFooter, uiTools.getColor(self.getColor(True)))
+      panel.addstr(labelingLine, graphCol + 6, secondaryFooter, uiTools.getColor(self.getColor(False)))
+    
+    # provides accounting stats if enabled
+    if self.isAccounting:
+      if torTools.getConn().isAlive():
+        status = self.accountingInfo["status"]
+        
+        hibernateColor = "green"
+        if status == "soft": hibernateColor = "yellow"
+        elif status == "hard": hibernateColor = "red"
+        elif status == "":
+          # failed to be queried
+          status, hibernateColor = "unknown", "red"
+        
+        panel.addfstr(labelingLine + 2, 0, "<b>Accounting (<%s>%s</%s>)</b>" % (hibernateColor, status, hibernateColor))
+        
+        resetTime = self.accountingInfo["resetTime"]
+        if not resetTime: resetTime = "unknown"
+        panel.addstr(labelingLine + 2, 35, "Time to reset: %s" % resetTime)
+        
+        used, total = self.accountingInfo["read"], self.accountingInfo["readLimit"]
+        if used and total:
+          panel.addstr(labelingLine + 3, 2, "%s / %s" % (used, total), uiTools.getColor(self.getColor(True)))
+        
+        used, total = self.accountingInfo["written"], self.accountingInfo["writtenLimit"]
+        if used and total:
+          panel.addstr(labelingLine + 3, 37, "%s / %s" % (used, total), uiTools.getColor(self.getColor(False)))
+      else:
+        panel.addfstr(labelingLine + 2, 0, "<b>Accounting:</b> Connection Closed...")
+  
+  def getTitle(self, width):
+    stats = list(self._titleStats)
+    
+    while True:
+      if not stats: return "Bandwidth:"
+      else:
+        label = "Bandwidth (%s):" % ", ".join(stats)
+        
+        if len(label) > width: del stats[-1]
+        else: return label
+  
+  def getHeaderLabel(self, width, isPrimary):
+    graphType = "Downloaded" if isPrimary else "Uploaded"
+    stats = [""]
+    
+    # if wide then avg and total are part of the header, otherwise they're on
+    # the x-axis
+    if width * 2 > COLLAPSE_WIDTH:
+      stats = [""] * 3
+      stats[1] = "- %s" % self._getAvgLabel(isPrimary)
+      stats[2] = ", %s" % self._getTotalLabel(isPrimary)
+    
+    stats[0] = "%-14s" % ("%s/sec" % uiTools.getSizeLabel((self.lastPrimary if isPrimary else self.lastSecondary) * 1024, 1))
+    
+    # drops label's components if there's not enough space
+    labeling = graphType + " (" + "".join(stats).strip() + "):"
+    while len(labeling) >= width:
+      if len(stats) > 1:
+        del stats[-1]
+        labeling = graphType + " (" + "".join(stats).strip() + "):"
+      else:
+        labeling = graphType + ":"
+        break
+    
+    return labeling
+  
+  def getColor(self, isPrimary):
+    return DL_COLOR if isPrimary else UL_COLOR
+  
+  def getContentHeight(self):
+    baseHeight = graphPanel.GraphStats.getContentHeight(self)
+    return baseHeight + 3 if self.isAccounting else baseHeight
+  
+  def new_desc_event(self, event):
+    # updates self._titleStats with updated values
+    conn = torTools.getConn()
+    if not conn.isAlive(): return # keep old values
+    
+    myFingerprint = conn.getMyFingerprint()
+    if not self._titleStats or not myFingerprint or (event and myFingerprint in event.idlist):
+      stats = []
+      bwRate = conn.getMyBandwidthRate()
+      bwBurst = conn.getMyBandwidthBurst()
+      bwObserved = conn.getMyBandwidthObserved()
+      bwMeasured = conn.getMyBandwidthMeasured()
+      
+      if bwRate and bwBurst:
+        bwRateLabel = uiTools.getSizeLabel(bwRate, 1)
+        bwBurstLabel = uiTools.getSizeLabel(bwBurst, 1)
+        
+        # if both are using rounded values then strip off the ".0" decimal
+        if ".0" in bwRateLabel and ".0" in bwBurstLabel:
+          bwRateLabel = bwRateLabel.replace(".0", "")
+          bwBurstLabel = bwBurstLabel.replace(".0", "")
+        
+        stats.append("limit: %s" % bwRateLabel)
+        stats.append("burst: %s" % bwBurstLabel)
+      
+      # Provide the observed bandwidth either if the measured bandwidth isn't
+      # available or if the measured bandwidth is the observed (this happens
+      # if there isn't yet enough bandwidth measurements).
+      if bwObserved and (not bwMeasured or bwMeasured == bwObserved):
+        stats.append("observed: %s" % uiTools.getSizeLabel(bwObserved, 1))
+      elif bwMeasured:
+        stats.append("measured: %s" % uiTools.getSizeLabel(bwMeasured, 1))
+      
+      self._titleStats = stats
+  
+  def _getAvgLabel(self, isPrimary):
+    total = self.primaryTotal if isPrimary else self.secondaryTotal
+    return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick)) * 1024, 1)
+  
+  def _getTotalLabel(self, isPrimary):
+    total = self.primaryTotal if isPrimary else self.secondaryTotal
+    return "total: %s" % uiTools.getSizeLabel(total * 1024, 1)
+  
+  def _updateAccountingInfo(self):
+    """
+    Updates mapping used for accounting info. This includes the following keys:
+    status, resetTime, read, written, readLimit, writtenLimit
+    
+    Any failed lookups result in a mapping to an empty string.
+    """
+    
+    conn = torTools.getConn()
+    queried = dict([(arg, "") for arg in ACCOUNTING_ARGS])
+    queried["status"] = conn.getInfo("accounting/hibernating")
+    
+    # provides a nicely formatted reset time
+    endInterval = conn.getInfo("accounting/interval-end")
+    if endInterval:
+      # converts from gmt to local with respect to DST
+      if time.localtime()[8]: tz_offset = time.altzone
+      else: tz_offset = time.timezone
+      
+      sec = time.mktime(time.strptime(endInterval, "%Y-%m-%d %H:%M:%S")) - time.time() - tz_offset
+      if self._config["features.graph.bw.accounting.isTimeLong"]:
+        queried["resetTime"] = ", ".join(uiTools.getTimeLabels(sec, True))
+      else:
+        days = sec / 86400
+        sec %= 86400
+        hours = sec / 3600
+        sec %= 3600
+        minutes = sec / 60
+        sec %= 60
+        queried["resetTime"] = "%i:%02i:%02i:%02i" % (days, hours, minutes, sec)
+    
+    # number of bytes used and in total for the accounting period
+    used = conn.getInfo("accounting/bytes")
+    left = conn.getInfo("accounting/bytes-left")
+    
+    if used and left:
+      usedComp, leftComp = used.split(" "), left.split(" ")
+      read, written = int(usedComp[0]), int(usedComp[1])
+      readLeft, writtenLeft = int(leftComp[0]), int(leftComp[1])
+      
+      queried["read"] = uiTools.getSizeLabel(read)
+      queried["written"] = uiTools.getSizeLabel(written)
+      queried["readLimit"] = uiTools.getSizeLabel(read + readLeft)
+      queried["writtenLimit"] = uiTools.getSizeLabel(written + writtenLeft)
+    
+    self.accountingInfo = queried
+    self.accountingLastUpdated = time.time()
+

Deleted: arm/trunk/src/interface/graphing/graphPanel.py
===================================================================
--- arm/trunk/interface/graphing/graphPanel.py	2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/interface/graphing/graphPanel.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,363 +0,0 @@
-"""
-Flexible panel for presenting bar graphs for a variety of stats. This panel is
-just concerned with the rendering of information, which is actually collected
-and stored by implementations of the GraphStats interface. Panels are made up
-of a title, followed by headers and graphs for two sets of stats. For
-instance...
-
-Bandwidth (cap: 5 MB, burst: 10 MB):
-Downloaded (0.0 B/sec):           Uploaded (0.0 B/sec):
-  34                                30
-                            *                                 *
-                    **  *   *                          *      **
-      *   *  *      ** **   **          ***  **       ** **   **
-     *********      ******  ******     *********      ******  ******
-   0 ************ ****************   0 ************ ****************
-         25s  50   1m   1.6  2.0           25s  50   1m   1.6  2.0
-"""
-
-import copy
-import curses
-from TorCtl import TorCtl
-
-from util import panel, uiTools
-
-# time intervals at which graphs can be updated
-UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5),   ("30 seconds", 30),
-                    ("minutely", 60),   ("15 minute", 900), ("30 minute", 1800),
-                    ("hourly", 3600),   ("daily", 86400)]
-
-DEFAULT_HEIGHT = 10 # space needed for graph and content
-DEFAULT_COLOR_PRIMARY, DEFAULT_COLOR_SECONDARY = "green", "cyan"
-
-# enums for graph bounds:
-#   BOUNDS_GLOBAL_MAX - global maximum (highest value ever seen)
-#   BOUNDS_LOCAL_MAX - local maximum (highest value currently on the graph)
-#   BOUNDS_TIGHT - local maximum and minimum
-BOUNDS_GLOBAL_MAX, BOUNDS_LOCAL_MAX, BOUNDS_TIGHT = range(3)
-BOUND_LABELS = {BOUNDS_GLOBAL_MAX: "global max", BOUNDS_LOCAL_MAX: "local max", BOUNDS_TIGHT: "tight"}
-
-WIDE_LABELING_GRAPH_COL = 50  # minimum graph columns to use wide spacing for x-axis labels
-
-# used for setting defaults when initializing GraphStats and GraphPanel instances
-CONFIG = {"features.graph.interval": 0, "features.graph.bound": 1, "features.graph.maxSize": 150, "features.graph.frequentRefresh": True}
-
-def loadConfig(config):
-  config.update(CONFIG)
-  CONFIG["features.graph.interval"] = max(len(UPDATE_INTERVALS) - 1, min(0, CONFIG["features.graph.interval"]))
-  CONFIG["features.graph.bound"] = max(2, min(0, CONFIG["features.graph.bound"]))
-  CONFIG["features.graph.maxSize"] = max(CONFIG["features.graph.maxSize"], 1)
-
-class GraphStats(TorCtl.PostEventListener):
-  """
-  Module that's expected to update dynamically and provide attributes to be
-  graphed. Up to two graphs (a 'primary' and 'secondary') can be displayed at a
-  time and timescale parameters use the labels defined in UPDATE_INTERVALS.
-  """
-  
-  def __init__(self, isPauseBuffer=False):
-    """
-    Initializes parameters needed to present a graph.
-    """
-    
-    TorCtl.PostEventListener.__init__(self)
-    
-    # panel to be redrawn when updated (set when added to GraphPanel)
-    self._graphPanel = None
-    
-    # mirror instance used to track updates when paused
-    self.isPaused, self.isPauseBuffer = False, isPauseBuffer
-    if isPauseBuffer: self._pauseBuffer = None
-    else: self._pauseBuffer = GraphStats(True)
-    
-    # tracked stats
-    self.tick = 0                                 # number of processed events
-    self.lastPrimary, self.lastSecondary = 0, 0   # most recent registered stats
-    self.primaryTotal, self.secondaryTotal = 0, 0 # sum of all stats seen
-    
-    # timescale dependent stats
-    self.maxCol = CONFIG["features.graph.maxSize"]
-    self.maxPrimary, self.maxSecondary = {}, {}
-    self.primaryCounts, self.secondaryCounts = {}, {}
-    
-    for i in range(len(UPDATE_INTERVALS)):
-      # recent rates for graph
-      self.maxPrimary[i] = 0
-      self.maxSecondary[i] = 0
-      
-      # historic stats for graph, first is accumulator
-      # iterative insert needed to avoid making shallow copies (nasty, nasty gotcha)
-      self.primaryCounts[i] = (self.maxCol + 1) * [0]
-      self.secondaryCounts[i] = (self.maxCol + 1) * [0]
-  
-  def eventTick(self):
-    """
-    Called when it's time to process another event. All graphs use tor BW
-    events to keep in sync with each other (this happens once a second).
-    """
-    
-    pass
-  
-  def isNextTickRedraw(self):
-    """
-    Provides true if the following tick (call to _processEvent) will result in
-    being redrawn.
-    """
-    
-    if self._graphPanel and not self.isPauseBuffer and not self.isPaused:
-      if CONFIG["features.graph.frequentRefresh"]: return True
-      else:
-        updateRate = UPDATE_INTERVALS[self._graphPanel.updateInterval][1]
-        if (self.tick + 1) % updateRate == 0: return True
-    
-    return False
-  
-  def getTitle(self, width):
-    """
-    Provides top label.
-    """
-    
-    return ""
-  
-  def getHeaderLabel(self, width, isPrimary):
-    """
-    Provides labeling presented at the top of the graph.
-    """
-    
-    return ""
-  
-  def getColor(self, isPrimary):
-    """
-    Provides the color to be used for the graph and stats.
-    """
-    
-    return DEFAULT_COLOR_PRIMARY if isPrimary else DEFAULT_COLOR_SECONDARY
-  
-  def getPreferredHeight(self):
-    """
-    Provides the height content should take up. By default this provides the
-    space needed for the default graph and content.
-    """
-    
-    return DEFAULT_HEIGHT
-  
-  def draw(self, panel, width, height):
-    """
-    Allows for any custom drawing monitor wishes to append.
-    """
-    
-    pass
-  
-  def setPaused(self, isPause):
-    """
-    If true, prevents bandwidth updates from being presented. This is a no-op
-    if a pause buffer.
-    """
-    
-    if isPause == self.isPaused or self.isPauseBuffer: return
-    self.isPaused = isPause
-    
-    if self.isPaused: active, inactive = self._pauseBuffer, self
-    else: active, inactive = self, self._pauseBuffer
-    self._parameterSwap(active, inactive)
-  
-  def bandwidth_event(self, event):
-    self.eventTick()
-  
-  def _parameterSwap(self, active, inactive):
-    """
-    Either overwrites parameters of pauseBuffer or with the current values or
-    vice versa. This is a helper method for setPaused and should be overwritten
-    to append with additional parameters that need to be preserved when paused.
-    """
-    
-    # The pause buffer is constructed as a GraphStats instance which will
-    # become problematic if this is overridden by any implementations (which
-    # currently isn't the case). If this happens then the pause buffer will
-    # need to be of the requester's type (not quite sure how to do this
-    # gracefully...).
-    
-    active.tick = inactive.tick
-    active.lastPrimary = inactive.lastPrimary
-    active.lastSecondary = inactive.lastSecondary
-    active.primaryTotal = inactive.primaryTotal
-    active.secondaryTotal = inactive.secondaryTotal
-    active.maxPrimary = dict(inactive.maxPrimary)
-    active.maxSecondary = dict(inactive.maxSecondary)
-    active.primaryCounts = copy.deepcopy(inactive.primaryCounts)
-    active.secondaryCounts = copy.deepcopy(inactive.secondaryCounts)
-  
-  def _processEvent(self, primary, secondary):
-    """
-    Includes new stats in graphs and notifies associated GraphPanel of changes.
-    """
-    
-    if self.isPaused: self._pauseBuffer._processEvent(primary, secondary)
-    else:
-      isRedraw = self.isNextTickRedraw()
-      
-      self.lastPrimary, self.lastSecondary = primary, secondary
-      self.primaryTotal += primary
-      self.secondaryTotal += secondary
-      
-      # updates for all time intervals
-      self.tick += 1
-      for i in range(len(UPDATE_INTERVALS)):
-        lable, timescale = UPDATE_INTERVALS[i]
-        
-        self.primaryCounts[i][0] += primary
-        self.secondaryCounts[i][0] += secondary
-        
-        if self.tick % timescale == 0:
-          self.maxPrimary[i] = max(self.maxPrimary[i], self.primaryCounts[i][0] / timescale)
-          self.primaryCounts[i][0] /= timescale
-          self.primaryCounts[i].insert(0, 0)
-          del self.primaryCounts[i][self.maxCol + 1:]
-          
-          self.maxSecondary[i] = max(self.maxSecondary[i], self.secondaryCounts[i][0] / timescale)
-          self.secondaryCounts[i][0] /= timescale
-          self.secondaryCounts[i].insert(0, 0)
-          del self.secondaryCounts[i][self.maxCol + 1:]
-      
-      if isRedraw: self._graphPanel.redraw(True)
-
-class GraphPanel(panel.Panel):
-  """
-  Panel displaying a graph, drawing statistics from custom GraphStats
-  implementations.
-  """
-  
-  def __init__(self, stdscr):
-    panel.Panel.__init__(self, stdscr, "graph", 0)
-    self.updateInterval = CONFIG["features.graph.interval"]
-    self.bounds = CONFIG["features.graph.bound"]
-    self.currentDisplay = None    # label of the stats currently being displayed
-    self.stats = {}               # available stats (mappings of label -> instance)
-    self.showLabel = True         # shows top label if true, hides otherwise
-    self.isPaused = False
-  
-  def getHeight(self):
-    """
-    Provides the height requested by the currently displayed GraphStats (zero
-    if hidden).
-    """
-    
-    if self.currentDisplay:
-      return self.stats[self.currentDisplay].getPreferredHeight()
-    else: return 0
-  
-  def draw(self, subwindow, width, height):
-    """ Redraws graph panel """
-    
-    if self.currentDisplay:
-      param = self.stats[self.currentDisplay]
-      graphCol = min((width - 10) / 2, param.maxCol)
-      
-      primaryColor = uiTools.getColor(param.getColor(True))
-      secondaryColor = uiTools.getColor(param.getColor(False))
-      
-      if self.showLabel: self.addstr(0, 0, param.getTitle(width), curses.A_STANDOUT)
-      
-      # top labels
-      left, right = param.getHeaderLabel(width / 2, True), param.getHeaderLabel(width / 2, False)
-      if left: self.addstr(1, 0, left, curses.A_BOLD | primaryColor)
-      if right: self.addstr(1, graphCol + 5, right, curses.A_BOLD | secondaryColor)
-      
-      # determines max/min value on the graph
-      if self.bounds == BOUNDS_GLOBAL_MAX:
-        primaryMaxBound = param.maxPrimary[self.updateInterval]
-        secondaryMaxBound = param.maxSecondary[self.updateInterval]
-      else:
-        # both BOUNDS_LOCAL_MAX and BOUNDS_TIGHT use local maxima
-        if graphCol < 2:
-          # nothing being displayed
-          primaryMaxBound, secondaryMaxBound = 0, 0
-        else:
-          primaryMaxBound = max(param.primaryCounts[self.updateInterval][1:graphCol + 1])
-          secondaryMaxBound = max(param.secondaryCounts[self.updateInterval][1:graphCol + 1])
-      
-      primaryMinBound = secondaryMinBound = 0
-      if self.bounds == BOUNDS_TIGHT:
-        primaryMinBound = min(param.primaryCounts[self.updateInterval][1:graphCol + 1])
-        secondaryMinBound = min(param.secondaryCounts[self.updateInterval][1:graphCol + 1])
-        
-        # if the max = min (ie, all values are the same) then use zero lower
-        # bound so a graph is still displayed
-        if primaryMinBound == primaryMaxBound: primaryMinBound = 0
-        if secondaryMinBound == secondaryMaxBound: secondaryMinBound = 0
-      
-      # displays bound
-      self.addstr(2, 0, "%4i" % primaryMaxBound, primaryColor)
-      self.addstr(7, 0, "%4i" % primaryMinBound, primaryColor)
-      
-      self.addstr(2, graphCol + 5, "%4i" % secondaryMaxBound, secondaryColor)
-      self.addstr(7, graphCol + 5, "%4i" % secondaryMinBound, secondaryColor)
-      
-      # creates bar graph (both primary and secondary)
-      for col in range(graphCol):
-        colCount = param.primaryCounts[self.updateInterval][col + 1] - primaryMinBound
-        colHeight = min(5, 5 * colCount / (max(1, primaryMaxBound) - primaryMinBound))
-        for row in range(colHeight): self.addstr(7 - row, col + 5, " ", curses.A_STANDOUT | primaryColor)
-        
-        colCount = param.secondaryCounts[self.updateInterval][col + 1] - secondaryMinBound
-        colHeight = min(5, 5 * colCount / (max(1, secondaryMaxBound) - secondaryMinBound))
-        for row in range(colHeight): self.addstr(7 - row, col + graphCol + 10, " ", curses.A_STANDOUT | secondaryColor)
-      
-      # bottom labeling of x-axis
-      intervalSec = 1 # seconds per labeling
-      for i in range(len(UPDATE_INTERVALS)):
-        if i == self.updateInterval: intervalSec = UPDATE_INTERVALS[i][1]
-      
-      intervalSpacing = 10 if graphCol >= WIDE_LABELING_GRAPH_COL else 5
-      unitsLabel, decimalPrecision = None, 0
-      for i in range((graphCol - 4) / intervalSpacing):
-        loc = (i + 1) * intervalSpacing
-        timeLabel = uiTools.getTimeLabel(loc * intervalSec, decimalPrecision)
-        
-        if not unitsLabel: unitsLabel = timeLabel[-1]
-        elif unitsLabel != timeLabel[-1]:
-          # upped scale so also up precision of future measurements
-          unitsLabel = timeLabel[-1]
-          decimalPrecision += 1
-        else:
-          # if constrained on space then strips labeling since already provided
-          timeLabel = timeLabel[:-1]
-        
-        self.addstr(8, 4 + loc, timeLabel, primaryColor)
-        self.addstr(8, graphCol + 10 + loc, timeLabel, secondaryColor)
-        
-      param.draw(self, width, height) # allows current stats to modify the display
-  
-  def addStats(self, label, stats):
-    """
-    Makes GraphStats instance available in the panel.
-    """
-    
-    stats._graphPanel = self
-    stats.isPaused = True
-    self.stats[label] = stats
-  
-  def setStats(self, label):
-    """
-    Sets the currently displayed stats instance, hiding panel if None.
-    """
-    
-    if label != self.currentDisplay:
-      if self.currentDisplay: self.stats[self.currentDisplay].setPaused(True)
-      
-      if not label:
-        self.currentDisplay = None
-      elif label in self.stats.keys():
-        self.currentDisplay = label
-        self.stats[label].setPaused(self.isPaused)
-      else: raise ValueError("Unrecognized stats label: %s" % label)
-  
-  def setPaused(self, isPause):
-    """
-    If true, prevents bandwidth updates from being presented.
-    """
-    
-    if isPause == self.isPaused: return
-    self.isPaused = isPause
-    if self.currentDisplay: self.stats[self.currentDisplay].setPaused(self.isPaused)
-

Copied: arm/trunk/src/interface/graphing/graphPanel.py (from rev 22991, arm/trunk/interface/graphing/graphPanel.py)
===================================================================
--- arm/trunk/src/interface/graphing/graphPanel.py	                        (rev 0)
+++ arm/trunk/src/interface/graphing/graphPanel.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,383 @@
+"""
+Flexible panel for presenting bar graphs for a variety of stats. This panel is
+just concerned with the rendering of information, which is actually collected
+and stored by implementations of the GraphStats interface. Panels are made up
+of a title, followed by headers and graphs for two sets of stats. For
+instance...
+
+Bandwidth (cap: 5 MB, burst: 10 MB):
+Downloaded (0.0 B/sec):           Uploaded (0.0 B/sec):
+  34                                30
+                            *                                 *
+                    **  *   *                          *      **
+      *   *  *      ** **   **          ***  **       ** **   **
+     *********      ******  ******     *********      ******  ******
+   0 ************ ****************   0 ************ ****************
+         25s  50   1m   1.6  2.0           25s  50   1m   1.6  2.0
+"""
+
+import copy
+import curses
+from TorCtl import TorCtl
+
+from util import panel, uiTools
+
+# time intervals at which graphs can be updated
+UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5),   ("30 seconds", 30),
+                    ("minutely", 60),   ("15 minute", 900), ("30 minute", 1800),
+                    ("hourly", 3600),   ("daily", 86400)]
+
+DEFAULT_CONTENT_HEIGHT = 4 # space needed for labeling above and below the graph
+DEFAULT_COLOR_PRIMARY, DEFAULT_COLOR_SECONDARY = "green", "cyan"
+MIN_GRAPH_HEIGHT = 1
+
+# enums for graph bounds:
+#   BOUNDS_GLOBAL_MAX - global maximum (highest value ever seen)
+#   BOUNDS_LOCAL_MAX - local maximum (highest value currently on the graph)
+#   BOUNDS_TIGHT - local maximum and minimum
+BOUNDS_GLOBAL_MAX, BOUNDS_LOCAL_MAX, BOUNDS_TIGHT = range(3)
+BOUND_LABELS = {BOUNDS_GLOBAL_MAX: "global max", BOUNDS_LOCAL_MAX: "local max", BOUNDS_TIGHT: "tight"}
+
+WIDE_LABELING_GRAPH_COL = 50  # minimum graph columns to use wide spacing for x-axis labels
+
+# used for setting defaults when initializing GraphStats and GraphPanel instances
+CONFIG = {"features.graph.height": 5, "features.graph.interval": 0, "features.graph.bound": 1, "features.graph.maxWidth": 150, "features.graph.frequentRefresh": 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"]))
+
+class GraphStats(TorCtl.PostEventListener):
+  """
+  Module that's expected to update dynamically and provide attributes to be
+  graphed. Up to two graphs (a 'primary' and 'secondary') can be displayed at a
+  time and timescale parameters use the labels defined in UPDATE_INTERVALS.
+  """
+  
+  def __init__(self, isPauseBuffer=False):
+    """
+    Initializes parameters needed to present a graph.
+    """
+    
+    TorCtl.PostEventListener.__init__(self)
+    
+    # panel to be redrawn when updated (set when added to GraphPanel)
+    self._graphPanel = None
+    
+    # mirror instance used to track updates when paused
+    self.isPaused, self.isPauseBuffer = False, isPauseBuffer
+    if isPauseBuffer: self._pauseBuffer = None
+    else: self._pauseBuffer = GraphStats(True)
+    
+    # tracked stats
+    self.tick = 0                                 # number of processed events
+    self.lastPrimary, self.lastSecondary = 0, 0   # most recent registered stats
+    self.primaryTotal, self.secondaryTotal = 0, 0 # sum of all stats seen
+    
+    # timescale dependent stats
+    self.maxCol = CONFIG["features.graph.maxWidth"]
+    self.maxPrimary, self.maxSecondary = {}, {}
+    self.primaryCounts, self.secondaryCounts = {}, {}
+    
+    for i in range(len(UPDATE_INTERVALS)):
+      # recent rates for graph
+      self.maxPrimary[i] = 0
+      self.maxSecondary[i] = 0
+      
+      # historic stats for graph, first is accumulator
+      # iterative insert needed to avoid making shallow copies (nasty, nasty gotcha)
+      self.primaryCounts[i] = (self.maxCol + 1) * [0]
+      self.secondaryCounts[i] = (self.maxCol + 1) * [0]
+  
+  def eventTick(self):
+    """
+    Called when it's time to process another event. All graphs use tor BW
+    events to keep in sync with each other (this happens once a second).
+    """
+    
+    pass
+  
+  def isNextTickRedraw(self):
+    """
+    Provides true if the following tick (call to _processEvent) will result in
+    being redrawn.
+    """
+    
+    if self._graphPanel and not self.isPauseBuffer and not self.isPaused:
+      if CONFIG["features.graph.frequentRefresh"]: return True
+      else:
+        updateRate = UPDATE_INTERVALS[self._graphPanel.updateInterval][1]
+        if (self.tick + 1) % updateRate == 0: return True
+    
+    return False
+  
+  def getTitle(self, width):
+    """
+    Provides top label.
+    """
+    
+    return ""
+  
+  def getHeaderLabel(self, width, isPrimary):
+    """
+    Provides labeling presented at the top of the graph.
+    """
+    
+    return ""
+  
+  def getColor(self, isPrimary):
+    """
+    Provides the color to be used for the graph and stats.
+    """
+    
+    return DEFAULT_COLOR_PRIMARY if isPrimary else DEFAULT_COLOR_SECONDARY
+  
+  def getContentHeight(self):
+    """
+    Provides the height content should take up (not including the graph).
+    """
+    
+    return DEFAULT_CONTENT_HEIGHT
+  
+  def isVisible(self):
+    """
+    True if the stat has content to present, false if it should be hidden.
+    """
+    
+    return True
+  
+  def draw(self, panel, width, height):
+    """
+    Allows for any custom drawing monitor wishes to append.
+    """
+    
+    pass
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents bandwidth updates from being presented. This is a no-op
+    if a pause buffer.
+    """
+    
+    if isPause == self.isPaused or self.isPauseBuffer: return
+    self.isPaused = isPause
+    
+    if self.isPaused: active, inactive = self._pauseBuffer, self
+    else: active, inactive = self, self._pauseBuffer
+    self._parameterSwap(active, inactive)
+  
+  def bandwidth_event(self, event):
+    self.eventTick()
+  
+  def _parameterSwap(self, active, inactive):
+    """
+    Either overwrites parameters of pauseBuffer or with the current values or
+    vice versa. This is a helper method for setPaused and should be overwritten
+    to append with additional parameters that need to be preserved when paused.
+    """
+    
+    # The pause buffer is constructed as a GraphStats instance which will
+    # become problematic if this is overridden by any implementations (which
+    # currently isn't the case). If this happens then the pause buffer will
+    # need to be of the requester's type (not quite sure how to do this
+    # gracefully...).
+    
+    active.tick = inactive.tick
+    active.lastPrimary = inactive.lastPrimary
+    active.lastSecondary = inactive.lastSecondary
+    active.primaryTotal = inactive.primaryTotal
+    active.secondaryTotal = inactive.secondaryTotal
+    active.maxPrimary = dict(inactive.maxPrimary)
+    active.maxSecondary = dict(inactive.maxSecondary)
+    active.primaryCounts = copy.deepcopy(inactive.primaryCounts)
+    active.secondaryCounts = copy.deepcopy(inactive.secondaryCounts)
+  
+  def _processEvent(self, primary, secondary):
+    """
+    Includes new stats in graphs and notifies associated GraphPanel of changes.
+    """
+    
+    if self.isPaused: self._pauseBuffer._processEvent(primary, secondary)
+    else:
+      isRedraw = self.isNextTickRedraw()
+      
+      self.lastPrimary, self.lastSecondary = primary, secondary
+      self.primaryTotal += primary
+      self.secondaryTotal += secondary
+      
+      # updates for all time intervals
+      self.tick += 1
+      for i in range(len(UPDATE_INTERVALS)):
+        lable, timescale = UPDATE_INTERVALS[i]
+        
+        self.primaryCounts[i][0] += primary
+        self.secondaryCounts[i][0] += secondary
+        
+        if self.tick % timescale == 0:
+          self.maxPrimary[i] = max(self.maxPrimary[i], self.primaryCounts[i][0] / timescale)
+          self.primaryCounts[i][0] /= timescale
+          self.primaryCounts[i].insert(0, 0)
+          del self.primaryCounts[i][self.maxCol + 1:]
+          
+          self.maxSecondary[i] = max(self.maxSecondary[i], self.secondaryCounts[i][0] / timescale)
+          self.secondaryCounts[i][0] /= timescale
+          self.secondaryCounts[i].insert(0, 0)
+          del self.secondaryCounts[i][self.maxCol + 1:]
+      
+      if isRedraw: self._graphPanel.redraw(True)
+
+class GraphPanel(panel.Panel):
+  """
+  Panel displaying a graph, drawing statistics from custom GraphStats
+  implementations.
+  """
+  
+  def __init__(self, stdscr):
+    panel.Panel.__init__(self, stdscr, "graph", 0)
+    self.updateInterval = CONFIG["features.graph.interval"]
+    self.bounds = CONFIG["features.graph.bound"]
+    self.graphHeight = CONFIG["features.graph.height"]
+    self.currentDisplay = None    # label of the stats currently being displayed
+    self.stats = {}               # available stats (mappings of label -> instance)
+    self.showLabel = True         # shows top label if true, hides otherwise
+    self.isPaused = False
+  
+  def getHeight(self):
+    """
+    Provides the height requested by the currently displayed GraphStats (zero
+    if hidden).
+    """
+    
+    if self.currentDisplay and self.stats[self.currentDisplay].isVisible():
+      return self.stats[self.currentDisplay].getContentHeight() + self.graphHeight
+    else: return 0
+  
+  def setGraphHeight(self, newGraphHeight):
+    """
+    Sets the preferred height used for the graph (restricted to the
+    MIN_GRAPH_HEIGHT minimum).
+    
+    Arguments:
+      newGraphHeight - new height for the graph
+    """
+    
+    self.graphHeight = max(MIN_GRAPH_HEIGHT, newGraphHeight)
+  
+  def draw(self, subwindow, width, height):
+    """ Redraws graph panel """
+    
+    if self.currentDisplay:
+      param = self.stats[self.currentDisplay]
+      graphCol = min((width - 10) / 2, param.maxCol)
+      
+      primaryColor = uiTools.getColor(param.getColor(True))
+      secondaryColor = uiTools.getColor(param.getColor(False))
+      
+      if self.showLabel: self.addstr(0, 0, param.getTitle(width), curses.A_STANDOUT)
+      
+      # top labels
+      left, right = param.getHeaderLabel(width / 2, True), param.getHeaderLabel(width / 2, False)
+      if left: self.addstr(1, 0, left, curses.A_BOLD | primaryColor)
+      if right: self.addstr(1, graphCol + 5, right, curses.A_BOLD | secondaryColor)
+      
+      # determines max/min value on the graph
+      if self.bounds == BOUNDS_GLOBAL_MAX:
+        primaryMaxBound = param.maxPrimary[self.updateInterval]
+        secondaryMaxBound = param.maxSecondary[self.updateInterval]
+      else:
+        # both BOUNDS_LOCAL_MAX and BOUNDS_TIGHT use local maxima
+        if graphCol < 2:
+          # nothing being displayed
+          primaryMaxBound, secondaryMaxBound = 0, 0
+        else:
+          primaryMaxBound = max(param.primaryCounts[self.updateInterval][1:graphCol + 1])
+          secondaryMaxBound = max(param.secondaryCounts[self.updateInterval][1:graphCol + 1])
+      
+      primaryMinBound = secondaryMinBound = 0
+      if self.bounds == BOUNDS_TIGHT:
+        primaryMinBound = min(param.primaryCounts[self.updateInterval][1:graphCol + 1])
+        secondaryMinBound = min(param.secondaryCounts[self.updateInterval][1:graphCol + 1])
+        
+        # if the max = min (ie, all values are the same) then use zero lower
+        # bound so a graph is still displayed
+        if primaryMinBound == primaryMaxBound: primaryMinBound = 0
+        if secondaryMinBound == secondaryMaxBound: secondaryMinBound = 0
+      
+      # displays bound
+      self.addstr(2, 0, "%4i" % primaryMaxBound, primaryColor)
+      self.addstr(self.graphHeight + 1, 0, "%4i" % primaryMinBound, primaryColor)
+      
+      self.addstr(2, graphCol + 5, "%4i" % secondaryMaxBound, secondaryColor)
+      self.addstr(self.graphHeight + 1, graphCol + 5, "%4i" % secondaryMinBound, secondaryColor)
+      
+      # creates bar graph (both primary and secondary)
+      for col in range(graphCol):
+        colCount = param.primaryCounts[self.updateInterval][col + 1] - primaryMinBound
+        colHeight = min(self.graphHeight, self.graphHeight * colCount / (max(1, primaryMaxBound) - primaryMinBound))
+        for row in range(colHeight): self.addstr(self.graphHeight + 1 - row, col + 5, " ", curses.A_STANDOUT | primaryColor)
+        
+        colCount = param.secondaryCounts[self.updateInterval][col + 1] - secondaryMinBound
+        colHeight = min(self.graphHeight, self.graphHeight * colCount / (max(1, secondaryMaxBound) - secondaryMinBound))
+        for row in range(colHeight): self.addstr(self.graphHeight + 1 - row, col + graphCol + 10, " ", curses.A_STANDOUT | secondaryColor)
+      
+      # bottom labeling of x-axis
+      intervalSec = 1 # seconds per labeling
+      for i in range(len(UPDATE_INTERVALS)):
+        if i == self.updateInterval: intervalSec = UPDATE_INTERVALS[i][1]
+      
+      intervalSpacing = 10 if graphCol >= WIDE_LABELING_GRAPH_COL else 5
+      unitsLabel, decimalPrecision = None, 0
+      for i in range((graphCol - 4) / intervalSpacing):
+        loc = (i + 1) * intervalSpacing
+        timeLabel = uiTools.getTimeLabel(loc * intervalSec, decimalPrecision)
+        
+        if not unitsLabel: unitsLabel = timeLabel[-1]
+        elif unitsLabel != timeLabel[-1]:
+          # upped scale so also up precision of future measurements
+          unitsLabel = timeLabel[-1]
+          decimalPrecision += 1
+        else:
+          # if constrained on space then strips labeling since already provided
+          timeLabel = timeLabel[:-1]
+        
+        self.addstr(self.graphHeight + 2, 4 + loc, timeLabel, primaryColor)
+        self.addstr(self.graphHeight + 2, graphCol + 10 + loc, timeLabel, secondaryColor)
+        
+      param.draw(self, width, height) # allows current stats to modify the display
+  
+  def addStats(self, label, stats):
+    """
+    Makes GraphStats instance available in the panel.
+    """
+    
+    stats._graphPanel = self
+    stats.isPaused = True
+    self.stats[label] = stats
+  
+  def setStats(self, label):
+    """
+    Sets the currently displayed stats instance, hiding panel if None.
+    """
+    
+    if label != self.currentDisplay:
+      if self.currentDisplay: self.stats[self.currentDisplay].setPaused(True)
+      
+      if not label:
+        self.currentDisplay = None
+      elif label in self.stats.keys():
+        self.currentDisplay = label
+        self.stats[label].setPaused(self.isPaused)
+      else: raise ValueError("Unrecognized stats label: %s" % label)
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents bandwidth updates from being presented.
+    """
+    
+    if isPause == self.isPaused: return
+    self.isPaused = isPause
+    if self.currentDisplay: self.stats[self.currentDisplay].setPaused(self.isPaused)
+

Deleted: arm/trunk/src/interface/graphing/psStats.py
===================================================================
--- arm/trunk/interface/graphing/psStats.py	2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/interface/graphing/psStats.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,129 +0,0 @@
-"""
-Tracks configured ps stats. If non-numeric then this fails, providing a blank
-graph. By default this provides the cpu and memory usage of the tor process.
-"""
-
-import graphPanel
-from util import log, sysTools, torTools, uiTools
-
-# number of subsequent failed queries before giving up
-FAILURE_THRESHOLD = 5
-
-# attempts to use cached results from the header panel's ps calls
-HEADER_PS_PARAM = ["%cpu", "rss", "%mem", "etime"]
-
-DEFAULT_CONFIG = {"features.graph.ps.primaryStat": "%cpu", "features.graph.ps.secondaryStat": "rss", "features.graph.ps.cachedOnly": True, "log.graph.ps.invalidStat": log.WARN, "log.graph.ps.abandon": log.WARN}
-
-class PsStats(graphPanel.GraphStats):
-  """
-  Tracks ps stats, defaulting to system resource usage (cpu and memory usage).
-  """
-  
-  def __init__(self, config=None):
-    graphPanel.GraphStats.__init__(self)
-    self.failedCount = 0      # number of subsequent failed queries
-    
-    self._config = dict(DEFAULT_CONFIG)
-    if config: config.update(self._config)
-    
-    self.queryPid = torTools.getConn().getMyPid()
-    self.queryParam = [self._config["features.graph.ps.primaryStat"], self._config["features.graph.ps.secondaryStat"]]
-    
-    # If we're getting the same stats as the header panel then issues identical
-    # queries to make use of cached results. If not, then disable cache usage.
-    if self.queryParam[0] in HEADER_PS_PARAM and self.queryParam[1] in HEADER_PS_PARAM:
-      self.queryParam = list(HEADER_PS_PARAM)
-    else: self._config["features.graph.ps.cachedOnly"] = False
-    
-    # strips any empty entries
-    while "" in self.queryParam: self.queryParam.remove("")
-    
-    self.cacheTime = 3600 if self._config["features.graph.ps.cachedOnly"] else 1
-  
-  def getTitle(self, width):
-    return "System Resources:"
-  
-  def getHeaderLabel(self, width, isPrimary):
-    avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
-    lastAmount = self.lastPrimary if isPrimary else self.lastSecondary
-    
-    if isPrimary: statName = self._config["features.graph.ps.primaryStat"]
-    else: statName = self._config["features.graph.ps.secondaryStat"]
-    
-    # provides nice labels for failures and common stats
-    if not statName or self.failedCount >= FAILURE_THRESHOLD or not statName in self.queryParam:
-      return ""
-    elif statName == "%cpu":
-      return "CPU (%s%%, avg: %0.1f%%):" % (lastAmount, avg)
-    elif statName in ("rss", "size"):
-      # memory sizes are converted from MB to B before generating labels
-      statLabel = "Memory" if statName == "rss" else "Size"
-      usageLabel = uiTools.getSizeLabel(lastAmount * 1048576, 1)
-      avgLabel = uiTools.getSizeLabel(avg * 1048576, 1)
-      return "%s (%s, avg: %s):" % (statLabel, usageLabel, avgLabel)
-    else:
-      # generic label (first letter of stat name is capitalized)
-      statLabel = statName[0].upper() + statName[1:]
-      return "%s (%s, avg: %s):" % (statLabel, lastAmount, avg)
-  
-  def getPreferredHeight(self):
-    # hides graph if there's nothing to display (provides default otherwise)
-    # provides default height unless there's nothing to 
-    if self.queryPid and self.queryParam and self.failedCount < FAILURE_THRESHOLD:
-      return graphPanel.DEFAULT_HEIGHT
-    else: return 0
-  
-  def eventTick(self):
-    """
-    Processes a ps event.
-    """
-    
-    psResults = {} # mapping of stat names to their results
-    if self.queryPid and self.queryParam and self.failedCount < FAILURE_THRESHOLD:
-      queryCmd = "ps -p %s -o %s" % (self.queryPid, ",".join(self.queryParam))
-      psCall = sysTools.call(queryCmd, self.cacheTime, True)
-      
-      if psCall and len(psCall) == 2:
-        # ps provided results (first line is headers, second is stats)
-        stats = psCall[1].strip().split()
-        
-        if len(self.queryParam) == len(stats):
-          # we have a result to match each stat - constructs mapping
-          psResults = dict([(self.queryParam[i], stats[i]) for i in range(len(stats))])
-          self.failedCount = 0 # had a successful call - reset failure count
-      
-      if not psResults:
-        # ps call failed, if we fail too many times sequentially then abandon
-        # listing (probably due to invalid ps parameters)
-        self.failedCount += 1
-        
-        if self.failedCount == FAILURE_THRESHOLD:
-          msg = "failed several attempts to query '%s', abandoning ps graph" % queryCmd
-          log.log(self._config["log.graph.ps.abandon"], msg)
-    
-    # if something fails (no pid, ps call failed, etc) then uses last results
-    primary, secondary = self.lastPrimary, self.lastSecondary
-    
-    for isPrimary in (True, False):
-      if isPrimary: statName = self._config["features.graph.ps.primaryStat"]
-      else: statName = self._config["features.graph.ps.secondaryStat"]
-      
-      if statName in psResults:
-        try:
-          result = float(psResults[statName])
-          
-          # The 'rss' and 'size' parameters provide memory usage in KB. This is
-          # scaled up to MB so the graph's y-high is a reasonable value.
-          if statName in ("rss", "size"): result /= 1024.0
-          
-          if isPrimary: primary = result
-          else: secondary = result
-        except ValueError:
-          if self.queryParam != HEADER_PS_PARAM:
-            # custom stat provides non-numeric results - give a warning and stop querying it
-            msg = "unable to use non-numeric ps stat '%s' for graphing" % statName
-            log.log(self._config["log.graph.ps.invalidStat"], msg)
-            self.queryParam.remove(statName)
-    
-    self._processEvent(primary, secondary)
-

Copied: arm/trunk/src/interface/graphing/psStats.py (from rev 22952, arm/trunk/interface/graphing/psStats.py)
===================================================================
--- arm/trunk/src/interface/graphing/psStats.py	                        (rev 0)
+++ arm/trunk/src/interface/graphing/psStats.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,131 @@
+"""
+Tracks configured ps stats. If non-numeric then this fails, providing a blank
+graph. By default this provides the cpu and memory usage of the tor process.
+"""
+
+import graphPanel
+from util import log, sysTools, torTools, uiTools
+
+# number of subsequent failed queries before giving up
+FAILURE_THRESHOLD = 5
+
+# attempts to use cached results from the header panel's ps calls
+HEADER_PS_PARAM = ["%cpu", "rss", "%mem", "etime"]
+
+DEFAULT_CONFIG = {"features.graph.ps.primaryStat": "%cpu", "features.graph.ps.secondaryStat": "rss", "features.graph.ps.cachedOnly": True, "log.graph.ps.invalidStat": log.WARN, "log.graph.ps.abandon": log.WARN}
+
+class PsStats(graphPanel.GraphStats):
+  """
+  Tracks ps stats, defaulting to system resource usage (cpu and memory usage).
+  """
+  
+  def __init__(self, config=None):
+    graphPanel.GraphStats.__init__(self)
+    self.failedCount = 0      # number of subsequent failed queries
+    
+    self._config = dict(DEFAULT_CONFIG)
+    if config: config.update(self._config)
+    
+    self.queryPid = torTools.getConn().getMyPid()
+    self.queryParam = [self._config["features.graph.ps.primaryStat"], self._config["features.graph.ps.secondaryStat"]]
+    
+    # If we're getting the same stats as the header panel then issues identical
+    # queries to make use of cached results. If not, then disable cache usage.
+    if self.queryParam[0] in HEADER_PS_PARAM and self.queryParam[1] in HEADER_PS_PARAM:
+      self.queryParam = list(HEADER_PS_PARAM)
+    else: self._config["features.graph.ps.cachedOnly"] = False
+    
+    # strips any empty entries
+    while "" in self.queryParam: self.queryParam.remove("")
+    
+    self.cacheTime = 3600 if self._config["features.graph.ps.cachedOnly"] else 1
+  
+  def getTitle(self, width):
+    return "System Resources:"
+  
+  def getHeaderLabel(self, width, isPrimary):
+    avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
+    lastAmount = self.lastPrimary if isPrimary else self.lastSecondary
+    
+    if isPrimary: statName = self._config["features.graph.ps.primaryStat"]
+    else: statName = self._config["features.graph.ps.secondaryStat"]
+    
+    # provides nice labels for failures and common stats
+    if not statName or self.failedCount >= FAILURE_THRESHOLD or not statName in self.queryParam:
+      return ""
+    elif statName == "%cpu":
+      return "CPU (%s%%, avg: %0.1f%%):" % (lastAmount, avg)
+    elif statName in ("rss", "size"):
+      # memory sizes are converted from MB to B before generating labels
+      statLabel = "Memory" if statName == "rss" else "Size"
+      usageLabel = uiTools.getSizeLabel(lastAmount * 1048576, 1)
+      avgLabel = uiTools.getSizeLabel(avg * 1048576, 1)
+      return "%s (%s, avg: %s):" % (statLabel, usageLabel, avgLabel)
+    else:
+      # generic label (first letter of stat name is capitalized)
+      statLabel = statName[0].upper() + statName[1:]
+      return "%s (%s, avg: %s):" % (statLabel, lastAmount, avg)
+  
+  def isVisible(self):
+    """
+    Hides graph if unable to fetch stats.
+    """
+    
+    if self.queryPid and self.queryParam and self.failedCount < FAILURE_THRESHOLD:
+      return graphPanel.GraphStats.isVisible(self)
+    else: return False
+  
+  def eventTick(self):
+    """
+    Processes a ps event.
+    """
+    
+    psResults = {} # mapping of stat names to their results
+    if self.queryPid and self.queryParam and self.failedCount < FAILURE_THRESHOLD:
+      queryCmd = "ps -p %s -o %s" % (self.queryPid, ",".join(self.queryParam))
+      psCall = sysTools.call(queryCmd, self.cacheTime, True)
+      
+      if psCall and len(psCall) == 2:
+        # ps provided results (first line is headers, second is stats)
+        stats = psCall[1].strip().split()
+        
+        if len(self.queryParam) == len(stats):
+          # we have a result to match each stat - constructs mapping
+          psResults = dict([(self.queryParam[i], stats[i]) for i in range(len(stats))])
+          self.failedCount = 0 # had a successful call - reset failure count
+      
+      if not psResults:
+        # ps call failed, if we fail too many times sequentially then abandon
+        # listing (probably due to invalid ps parameters)
+        self.failedCount += 1
+        
+        if self.failedCount == FAILURE_THRESHOLD:
+          msg = "failed several attempts to query '%s', abandoning ps graph" % queryCmd
+          log.log(self._config["log.graph.ps.abandon"], msg)
+    
+    # if something fails (no pid, ps call failed, etc) then uses last results
+    primary, secondary = self.lastPrimary, self.lastSecondary
+    
+    for isPrimary in (True, False):
+      if isPrimary: statName = self._config["features.graph.ps.primaryStat"]
+      else: statName = self._config["features.graph.ps.secondaryStat"]
+      
+      if statName in psResults:
+        try:
+          result = float(psResults[statName])
+          
+          # The 'rss' and 'size' parameters provide memory usage in KB. This is
+          # scaled up to MB so the graph's y-high is a reasonable value.
+          if statName in ("rss", "size"): result /= 1024.0
+          
+          if isPrimary: primary = result
+          else: secondary = result
+        except ValueError:
+          if self.queryParam != HEADER_PS_PARAM:
+            # custom stat provides non-numeric results - give a warning and stop querying it
+            msg = "unable to use non-numeric ps stat '%s' for graphing" % statName
+            log.log(self._config["log.graph.ps.invalidStat"], msg)
+            self.queryParam.remove(statName)
+    
+    self._processEvent(primary, secondary)
+

Deleted: arm/trunk/src/interface/logPanel.py
===================================================================
--- arm/trunk/interface/logPanel.py	2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/interface/logPanel.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,485 +0,0 @@
-#!/usr/bin/env python
-# logPanel.py -- Resources related to Tor event monitoring.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import time
-import curses
-from curses.ascii import isprint
-from TorCtl import TorCtl
-
-from util import log, panel, sysTools, torTools, uiTools
-
-PRE_POPULATE_LOG = True               # attempts to retrieve events from log file if available
-
-# truncates to the last X log lines (needed to start in a decent time if the log's big)
-PRE_POPULATE_MIN_LIMIT = 1000             # limit in case of verbose logging
-PRE_POPULATE_MAX_LIMIT = 5000             # limit for NOTICE - ERR (since most lines are skipped)
-MAX_LOG_ENTRIES = 1000                # size of log buffer (max number of entries)
-RUNLEVEL_EVENT_COLOR = {"DEBUG": "magenta", "INFO": "blue", "NOTICE": "green", "WARN": "yellow", "ERR": "red"}
-
-TOR_EVENT_TYPES = {
-  "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"}
-
-EVENT_LISTING = """        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"""
-
-TOR_CTL_CLOSE_MSG = "Tor closed control connection. Exiting event thread."
-
-def expandEvents(eventAbbr):
-  """
-  Expands event abbreviations to their full names. Beside mappings privided in
-  TOR_EVENT_TYPES this recognizes the following special events and aliases:
-  U - UKNOWN events
-  A - all events
-  X - no events
-  DINWE - runlevel and higher
-  12345 - arm runlevel and higher (ARM_DEBUG - ARM_ERR)
-  67890 - torctl runlevel and higher (TORCTL_DEBUG - TORCTL_ERR)
-  Raises ValueError with invalid input if any part isn't recognized.
-  
-  Examples:
-  "inUt" -> ["INFO", "NOTICE", "UNKNOWN", "STREAM_BW"]
-  "N4" -> ["NOTICE", "WARN", "ERR", "ARM_WARN", "ARM_ERR"]
-  "cfX" -> []
-  """
-  
-  expandedEvents = set()
-  invalidFlags = ""
-  for flag in eventAbbr:
-    if flag == "A":
-      expandedEvents = set(TOR_EVENT_TYPES.values() + ["ARM_DEBUG", "ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"])
-      break
-    elif flag == "X":
-      expandedEvents = set()
-      break
-    elif flag == "U": expandedEvents.add("UNKNOWN")
-    elif flag == "D": expandedEvents = expandedEvents.union(set(["DEBUG", "INFO", "NOTICE", "WARN", "ERR"]))
-    elif flag == "I": expandedEvents = expandedEvents.union(set(["INFO", "NOTICE", "WARN", "ERR"]))
-    elif flag == "N": expandedEvents = expandedEvents.union(set(["NOTICE", "WARN", "ERR"]))
-    elif flag == "W": expandedEvents = expandedEvents.union(set(["WARN", "ERR"]))
-    elif flag == "E": expandedEvents.add("ERR")
-    elif flag == "1": expandedEvents = expandedEvents.union(set(["ARM_DEBUG", "ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
-    elif flag == "2": expandedEvents = expandedEvents.union(set(["ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
-    elif flag == "3": expandedEvents = expandedEvents.union(set(["ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
-    elif flag == "4": expandedEvents = expandedEvents.union(set(["ARM_WARN", "ARM_ERR"]))
-    elif flag == "5": expandedEvents.add("ARM_ERR")
-    elif flag == "6": expandedEvents = expandedEvents.union(set(["TORCTL_DEBUG", "TORCTL_INFO", "TORCTL_NOTICE", "TORCTL_WARN", "TORCTL_ERR"]))
-    elif flag == "7": expandedEvents = expandedEvents.union(set(["TORCTL_INFO", "TORCTL_NOTICE", "TORCTL_WARN", "TORCTL_ERR"]))
-    elif flag == "8": expandedEvents = expandedEvents.union(set(["TORCTL_NOTICE", "TORCTL_WARN", "TORCTL_ERR"]))
-    elif flag == "9": expandedEvents = expandedEvents.union(set(["TORCTL_WARN", "TORCTL_ERR"]))
-    elif flag == "0": expandedEvents.add("TORCTL_ERR")
-    elif flag in TOR_EVENT_TYPES:
-      expandedEvents.add(TOR_EVENT_TYPES[flag])
-    else:
-      invalidFlags += flag
-  
-  if invalidFlags: raise ValueError(invalidFlags)
-  else: return expandedEvents
-
-class LogMonitor(TorCtl.PostEventListener, panel.Panel):
-  """
-  Tor event listener, noting messages, the time, and their type in a panel.
-  """
-  
-  def __init__(self, stdscr, conn, loggedEvents):
-    TorCtl.PostEventListener.__init__(self)
-    panel.Panel.__init__(self, stdscr, "log", 0)
-    self.scroll = 0
-    self.msgLog = []                      # tuples of (logText, color)
-    self.isPaused = False
-    self.pauseBuffer = []                 # location where messages are buffered if paused
-    self.loggedEvents = loggedEvents      # events we're listening to
-    self.lastHeartbeat = time.time()      # time of last event
-    self.regexFilter = None               # filter for presented log events (no filtering if None)
-    self.eventTimeOverwrite = None        # replaces time for further events with this (uses time it occures if None)
-    self.controlPortClosed = False        # flag set if TorCtl provided notice that control port is closed
-    
-    # prevents attempts to redraw while processing batch of events
-    previousPauseState = self.isPaused
-    self.setPaused(True)
-    log.addListeners([log.DEBUG, log.INFO, log.NOTICE, log.WARN, log.ERR], self.arm_event_wrapper, True)
-    self.setPaused(previousPauseState)
-    
-    # attempts to process events from log file
-    if PRE_POPULATE_LOG:
-      previousPauseState = self.isPaused
-      
-      try:
-        logFileLoc = None
-        loggingLocations = conn.get_option("Log")
-        
-        for entry in loggingLocations:
-          entryComp = entry[1].split()
-          if entryComp[1] == "file":
-            logFileLoc = entryComp[2]
-            break
-        
-        if logFileLoc:
-          # prevents attempts to redraw while processing batch of events
-          self.setPaused(True)
-          
-          # trims log to last entries to deal with logs when they're in the GB or TB range
-          # throws IOError if tail fails (falls to the catch-all later)
-          # TODO: now that this is using sysTools figure out if we can do away with the catch-all...
-          limit = PRE_POPULATE_MIN_LIMIT if ("DEBUG" in self.loggedEvents or "INFO" in self.loggedEvents) else PRE_POPULATE_MAX_LIMIT
-          
-          # truncates to entries for this tor instance
-          lines = sysTools.call("tail -n %i %s" % (limit, logFileLoc))
-          instanceStart = 0
-          for i in range(len(lines) - 1, -1, -1):
-            if "opening log file" in lines[i]:
-              instanceStart = i
-              break
-          
-          for line in lines[instanceStart:]:
-            lineComp = line.split()
-            eventType = lineComp[3][1:-1].upper()
-            
-            if eventType in self.loggedEvents:
-              timeComp = lineComp[2][:lineComp[2].find(".")].split(":")
-              self.eventTimeOverwrite = (0, 0, 0, int(timeComp[0]), int(timeComp[1]), int(timeComp[2]))
-              self.listen(TorCtl.LogEvent(eventType, " ".join(lineComp[4:])))
-      except Exception: pass # disreguard any issues that might arise
-      finally:
-        self.setPaused(previousPauseState)
-        self.eventTimeOverwrite = None
-  
-  def handleKey(self, key):
-    # scroll movement
-    if key in (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE):
-      pageHeight, shift = self.getPreferredSize()[0] - 1, 0
-      
-      # location offset
-      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
-      
-      # restricts to valid bounds and applies
-      maxLoc = self.getLogDisplayLength() - pageHeight
-      self.scroll = max(0, min(self.scroll + shift, maxLoc))
-  
-  # Listens for all event types and redirects to registerEvent
-  def circ_status_event(self, event):
-    if "CIRC" in self.loggedEvents:
-      optionalParams = ""
-      if event.purpose: optionalParams += " PURPOSE: %s" % event.purpose
-      if event.reason: optionalParams += " REASON: %s" % event.reason
-      if event.remote_reason: optionalParams += " REMOTE_REASON: %s" % event.remote_reason
-      self.registerEvent("CIRC", "ID: %-3s STATUS: %-10s PATH: %s%s" % (event.circ_id, event.status, ", ".join(event.path), optionalParams), "yellow")
-  
-  def buildtimeout_set_event(self, event):
-    # TODO: not sure how to stimulate event - needs sanity check
-    try:
-      self.registerEvent("BUILDTIMEOUT_SET", "SET_TYPE: %s, TOTAL_TIMES: %s, TIMEOUT_MS: %s, XM: %s, ALPHA: %s, CUTOFF_QUANTILE: %s" % (event.set_type, event.total_times, event.timeout_ms, event.xm, event.alpha, event.cutoff_quantile), "white")
-    except TypeError:
-      self.registerEvent("BUILDTIMEOUT_SET", "DEBUG -> SET_TYPE: %s, TOTAL_TIMES: %s, TIMEOUT_MS: %s, XM: %s, ALPHA: %s, CUTOFF_QUANTILE: %s" % (type(event.set_type), type(event.total_times), type(event.timeout_ms), type(event.xm), type(event.alpha), type(event.cutoff_quantile)), "white")
-  
-  def stream_status_event(self, event):
-    # TODO: not sure how to stimulate event - needs sanity check
-    try:
-      self.registerEvent("STREAM", "ID: %s STATUS: %s CIRC_ID: %s TARGET: %s:%s REASON: %s REMOTE_REASON: %s SOURCE: %s SOURCE_ADDR: %s PURPOSE: %s" % (event.strm_id, event.status, event.circ_id, event.target_host, event.target_port, event.reason, event.remote_reason, event.source, event.source_addr, event.purpose), "white")
-    except TypeError:
-      self.registerEvent("STREAM", "DEBUG -> ID: %s STATUS: %s CIRC_ID: %s TARGET: %s:%s REASON: %s REMOTE_REASON: %s SOURCE: %s SOURCE_ADDR: %s PURPOSE: %s" % (type(event.strm_id), type(event.status), type(event.circ_id), type(event.target_host), type(event.target_port), type(event.reason), type(event.remote_reason), type(event.source), type(event.source_addr), type(event.purpose)), "white")
-  
-  def or_conn_status_event(self, event):
-    optionalParams = ""
-    if event.age: optionalParams += " AGE: %-3s" % event.age
-    if event.read_bytes: optionalParams += " READ: %-4i" % event.read_bytes
-    if event.wrote_bytes: optionalParams += " WRITTEN: %-4i" % event.wrote_bytes
-    if event.reason: optionalParams += " REASON: %-6s" % event.reason
-    if event.ncircs: optionalParams += " NCIRCS: %i" % event.ncircs
-    self.registerEvent("ORCONN", "STATUS: %-10s ENDPOINT: %-20s%s" % (event.status, event.endpoint, optionalParams), "white")
-  
-  def stream_bw_event(self, event):
-    # TODO: not sure how to stimulate event - needs sanity check
-    try:
-      self.registerEvent("STREAM_BW", "ID: %s READ: %i WRITTEN: %i" % (event.strm_id, event.bytes_read, event.bytes_written), "white")
-    except TypeError:
-      self.registerEvent("STREAM_BW", "DEBUG -> ID: %s READ: %s WRITTEN: %s" % (type(event.strm_id), type(event.bytes_read), type(event.bytes_written)), "white")
-  
-  def bandwidth_event(self, event):
-    self.lastHeartbeat = time.time() # ensures heartbeat at least once a second
-    if "BW" in self.loggedEvents: self.registerEvent("BW", "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
-  
-  def msg_event(self, event):
-    self.registerEvent(event.level, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
-  
-  def new_desc_event(self, event):
-    if "NEWDESC" in self.loggedEvents:
-      idlistStr = [str(item) for item in event.idlist]
-      self.registerEvent("NEWDESC", ", ".join(idlistStr), "white")
-  
-  def address_mapped_event(self, event):
-    self.registerEvent("ADDRMAP", "%s, %s -> %s" % (event.when, event.from_addr, event.to_addr), "white")
-  
-  def ns_event(self, event):
-    # NetworkStatus params: nickname, idhash, orhash, ip, orport (int), dirport (int), flags, idhex, bandwidth, updated (datetime)
-    if "NS" in self.loggedEvents:
-      msg = ""
-      for ns in event.nslist:
-        msg += ", %s (%s:%i)" % (ns.nickname, ns.ip, ns.orport)
-      if len(msg) > 1: msg = msg[2:]
-      self.registerEvent("NS", "Listed (%i): %s" % (len(event.nslist), msg), "blue")
-  
-  def new_consensus_event(self, event):
-    if "NEWCONSENSUS" in self.loggedEvents:
-      msg = ""
-      for ns in event.nslist:
-        msg += ", %s (%s:%i)" % (ns.nickname, ns.ip, ns.orport)
-      self.registerEvent("NEWCONSENSUS", "Listed (%i): %s" % (len(event.nslist), msg), "magenta")
-  
-  def unknown_event(self, event):
-    if "UNKNOWN" in self.loggedEvents: self.registerEvent("UNKNOWN", event.event_string, "red")
-  
-  def arm_event_wrapper(self, level, msg, eventTime):
-    # temporary adaptor hack to use the new logging functions until I'm sure they'll work
-    # TODO: insert into log according to the event's timestamp (harder part
-    # here will be interpreting tor's event timestamps...)
-    self.monitor_event(level, msg)
-  
-  def monitor_event(self, level, msg):
-    # events provided by the arm monitor
-    if "ARM_" + level in self.loggedEvents: self.registerEvent("ARM-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
-  
-  def tor_ctl_event(self, level, msg):
-    # events provided by TorCtl
-    if "TORCTL_" + level in self.loggedEvents: self.registerEvent("TORCTL-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
-  
-  def write(self, msg):
-    """
-    Tracks TorCtl events. Ugly hack since TorCtl/TorUtil.py expects a file.
-    """
-    
-    timestampStart = msg.find("[")
-    timestampEnd = msg.find("]")
-    
-    level = msg[:timestampStart]
-    msg = msg[timestampEnd + 2:].strip()
-    
-    if TOR_CTL_CLOSE_MSG in msg:
-      # TorCtl providing notice that control port is closed
-      self.controlPortClosed = True
-      log.log(log.NOTICE, "Tor control port closed")
-      
-      # Allows the Controller to notice that tor's shut down.
-      # TODO: should make the controller the torctl event listener rather than
-      # this log panel (it'll also make this less hacky)
-      torTools.getConn().isAlive()
-    self.tor_ctl_event(level, msg)
-  
-  def flush(self): pass
-  
-  def registerEvent(self, type, msg, color):
-    """
-    Notes event and redraws log. If paused it's held in a temporary buffer. If 
-    msg is a list then this is expanded to multiple lines.
-    """
-    
-    if not type.startswith("ARM"): self.lastHeartbeat = time.time()
-    eventTime = self.eventTimeOverwrite if self.eventTimeOverwrite else time.localtime()
-    toAdd = []
-    
-    # wraps if a single line message
-    if isinstance(msg, str): msg = [msg]
-    
-    firstLine = True
-    for msgLine in msg:
-      # strips control characters to avoid screwing up the terminal
-      msgLine = "".join([char for char in msgLine if isprint(char)])
-      
-      header = "%02i:%02i:%02i %s" % (eventTime[3], eventTime[4], eventTime[5], "[%s]" % type) if firstLine else ""
-      toAdd.append("%s %s" % (header, msgLine))
-      firstLine = False
-    
-    toAdd.reverse()
-    if self.isPaused:
-      for msgLine in toAdd: self.pauseBuffer.insert(0, (msgLine, color))
-      if len(self.pauseBuffer) > MAX_LOG_ENTRIES: del self.pauseBuffer[MAX_LOG_ENTRIES:]
-    else:
-      for msgLine in toAdd: self.msgLog.insert(0, (msgLine, color))
-      if len(self.msgLog) > MAX_LOG_ENTRIES: del self.msgLog[MAX_LOG_ENTRIES:]
-      self.redraw(True)
-  
-  def draw(self, subwindow, width, height):
-    """
-    Redraws message log. Entries stretch to use available space and may
-    contain up to two lines. Starts with newest entries.
-    """
-    
-    isScrollBarVisible = self.getLogDisplayLength() > height - 1
-    xOffset = 3 if isScrollBarVisible else 0 # content offset for scroll bar
-    
-    # draws label - uses ellipsis if too long, for instance:
-    # Events (DEBUG, INFO, NOTICE, WARN...):
-    eventsLabel = "Events"
-    
-    # separates tor and arm runlevels (might be able to show as range)
-    eventsList = list(self.loggedEvents)
-    torRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, ""))
-    armRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, "ARM_"))
-    torctlRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, "TORCTL_"))
-    
-    if torctlRunlevelLabel: eventsList = ["TORCTL " + torctlRunlevelLabel] + eventsList
-    if armRunlevelLabel: eventsList = ["ARM " + armRunlevelLabel] + eventsList
-    if torRunlevelLabel: eventsList = [torRunlevelLabel] + eventsList
-    
-    eventsListing = ", ".join(eventsList)
-    filterLabel = "" if not self.regexFilter else " - filter: %s" % self.regexFilter.pattern
-    
-    firstLabelLen = eventsListing.find(", ")
-    if firstLabelLen == -1: firstLabelLen = len(eventsListing)
-    else: firstLabelLen += 3
-    
-    if width > 10 + firstLabelLen:
-      eventsLabel += " ("
-      
-      if len(eventsListing) > width - 11:
-        labelBreak = eventsListing[:width - 12].rfind(", ")
-        eventsLabel += "%s..." % eventsListing[:labelBreak]
-      elif len(eventsListing) + len(filterLabel) > width - 11:
-        eventsLabel += eventsListing
-      else: eventsLabel += eventsListing + filterLabel
-      eventsLabel += ")"
-    eventsLabel += ":"
-    
-    self.addstr(0, 0, eventsLabel, curses.A_STANDOUT)
-    
-    # log entries
-    maxLoc = self.getLogDisplayLength() - height + 1
-    self.scroll = max(0, min(self.scroll, maxLoc))
-    lineCount = 1 - self.scroll
-    
-    for (line, color) in self.msgLog:
-      if self.regexFilter and not self.regexFilter.search(line):
-        continue  # filter doesn't match log message - skip
-      
-      # splits over too lines if too long
-      if len(line) < width:
-        if lineCount >= 1: self.addstr(lineCount, xOffset, line, uiTools.getColor(color))
-        lineCount += 1
-      else:
-        (line1, line2) = splitLine(line, width - xOffset)
-        if lineCount >= 1: self.addstr(lineCount, xOffset, line1, uiTools.getColor(color))
-        if lineCount >= 0: self.addstr(lineCount + 1, xOffset, line2, uiTools.getColor(color))
-        lineCount += 2
-      
-      if lineCount >= height: break # further log messages wouldn't fit
-    
-    if isScrollBarVisible: self.addScrollBar(self.scroll, self.scroll + height - 1, self.getLogDisplayLength(), 1)
-  
-  def getLogDisplayLength(self):
-    """
-    Provides the number of lines the log would currently occupy.
-    """
-    
-    logLength = len(self.msgLog)
-    
-    # takes into account filtered and wrapped messages
-    for (line, color) in self.msgLog:
-      if self.regexFilter and not self.regexFilter.search(line): logLength -= 1
-      elif len(line) >= self.getPreferredSize()[1]: logLength += 1
-    
-    return logLength
-  
-  def setPaused(self, isPause):
-    """
-    If true, prevents message log from being updated with new events.
-    """
-    
-    if isPause == self.isPaused: return
-    
-    self.isPaused = isPause
-    if self.isPaused: self.pauseBuffer = []
-    else:
-      self.msgLog = (self.pauseBuffer + self.msgLog)[:MAX_LOG_ENTRIES]
-      if self.win: self.redraw(True) # hack to avoid redrawing during init
-  
-  def getHeartbeat(self):
-    """
-    Provides the number of seconds since the last registered event (this always
-    listens to BW events so should be less than a second if relay's still
-    responsive).
-    """
-    
-    return time.time() - self.lastHeartbeat
-
-def parseRunlevelRanges(eventsList, searchPrefix):
-  """
-  This parses a list of events to provide an ordered list of runlevels, 
-  condensed if three or more are in a contiguous range. This removes parsed 
-  runlevels from the eventsList. For instance:
-  
-  eventsList = ["BW", "ARM_WARN", "ERR", "ARM_ERR", "ARM_DEBUG", "ARM_NOTICE"]
-  searchPrefix = "ARM_"
-  
-  results in:
-  eventsList = ["BW", "ERR"]
-  return value is ["DEBUG", "NOTICE - ERR"]
-  
-  """
-  
-  # blank ending runlevel forces the break condition to be reached at the end
-  runlevels = ["DEBUG", "INFO", "NOTICE", "WARN", "ERR", ""]
-  runlevelLabels = []
-  start, end = "", ""
-  rangeLength = 0
-  
-  for level in runlevels:
-    if searchPrefix + level in eventsList:
-      eventsList.remove(searchPrefix + level)
-      
-      if start:
-        end = level
-        rangeLength += 1
-      else:
-        start = level
-        rangeLength = 1
-    elif rangeLength > 0:
-      # reached a break in the runlevels
-      if rangeLength == 1: runlevelLabels += [start]
-      elif rangeLength == 2: runlevelLabels += [start, end]
-      else: runlevelLabels += ["%s - %s" % (start, end)]
-      
-      start, end = "", ""
-      rangeLength = 0
-  
-  return runlevelLabels
-
-def splitLine(message, x):
-  """
-  Divides message into two lines, attempting to do it on a wordbreak.
-  """
-  
-  lastWordbreak = message[:x].rfind(" ")
-  if x - lastWordbreak < 10:
-    line1 = message[:lastWordbreak]
-    line2 = "  %s" % message[lastWordbreak:].strip()
-  else:
-    # over ten characters until the last word - dividing
-    line1 = "%s-" % message[:x - 2]
-    line2 = "  %s" % message[x - 2:].strip()
-  
-  # ends line with ellipsis if too long
-  if len(line2) > x:
-    lastWordbreak = line2[:x - 4].rfind(" ")
-    
-    # doesn't use wordbreak if it's a long word or the whole line is one 
-    # word (picking up on two space indent to have index 1)
-    if x - lastWordbreak > 10 or lastWordbreak == 1: lastWordbreak = x - 4
-    line2 = "%s..." % line2[:lastWordbreak]
-  
-  return (line1, line2)
-

Copied: arm/trunk/src/interface/logPanel.py (from rev 22948, arm/trunk/interface/logPanel.py)
===================================================================
--- arm/trunk/src/interface/logPanel.py	                        (rev 0)
+++ arm/trunk/src/interface/logPanel.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,485 @@
+#!/usr/bin/env python
+# logPanel.py -- Resources related to Tor event monitoring.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import time
+import curses
+from curses.ascii import isprint
+from TorCtl import TorCtl
+
+from util import log, panel, sysTools, torTools, uiTools
+
+PRE_POPULATE_LOG = True               # attempts to retrieve events from log file if available
+
+# truncates to the last X log lines (needed to start in a decent time if the log's big)
+PRE_POPULATE_MIN_LIMIT = 1000             # limit in case of verbose logging
+PRE_POPULATE_MAX_LIMIT = 5000             # limit for NOTICE - ERR (since most lines are skipped)
+MAX_LOG_ENTRIES = 1000                # size of log buffer (max number of entries)
+RUNLEVEL_EVENT_COLOR = {"DEBUG": "magenta", "INFO": "blue", "NOTICE": "green", "WARN": "yellow", "ERR": "red"}
+
+TOR_EVENT_TYPES = {
+  "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"}
+
+EVENT_LISTING = """        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"""
+
+TOR_CTL_CLOSE_MSG = "Tor closed control connection. Exiting event thread."
+
+def expandEvents(eventAbbr):
+  """
+  Expands event abbreviations to their full names. Beside mappings privided in
+  TOR_EVENT_TYPES this recognizes the following special events and aliases:
+  U - UKNOWN events
+  A - all events
+  X - no events
+  DINWE - runlevel and higher
+  12345 - arm runlevel and higher (ARM_DEBUG - ARM_ERR)
+  67890 - torctl runlevel and higher (TORCTL_DEBUG - TORCTL_ERR)
+  Raises ValueError with invalid input if any part isn't recognized.
+  
+  Examples:
+  "inUt" -> ["INFO", "NOTICE", "UNKNOWN", "STREAM_BW"]
+  "N4" -> ["NOTICE", "WARN", "ERR", "ARM_WARN", "ARM_ERR"]
+  "cfX" -> []
+  """
+  
+  expandedEvents = set()
+  invalidFlags = ""
+  for flag in eventAbbr:
+    if flag == "A":
+      expandedEvents = set(TOR_EVENT_TYPES.values() + ["ARM_DEBUG", "ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"])
+      break
+    elif flag == "X":
+      expandedEvents = set()
+      break
+    elif flag == "U": expandedEvents.add("UNKNOWN")
+    elif flag == "D": expandedEvents = expandedEvents.union(set(["DEBUG", "INFO", "NOTICE", "WARN", "ERR"]))
+    elif flag == "I": expandedEvents = expandedEvents.union(set(["INFO", "NOTICE", "WARN", "ERR"]))
+    elif flag == "N": expandedEvents = expandedEvents.union(set(["NOTICE", "WARN", "ERR"]))
+    elif flag == "W": expandedEvents = expandedEvents.union(set(["WARN", "ERR"]))
+    elif flag == "E": expandedEvents.add("ERR")
+    elif flag == "1": expandedEvents = expandedEvents.union(set(["ARM_DEBUG", "ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
+    elif flag == "2": expandedEvents = expandedEvents.union(set(["ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
+    elif flag == "3": expandedEvents = expandedEvents.union(set(["ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
+    elif flag == "4": expandedEvents = expandedEvents.union(set(["ARM_WARN", "ARM_ERR"]))
+    elif flag == "5": expandedEvents.add("ARM_ERR")
+    elif flag == "6": expandedEvents = expandedEvents.union(set(["TORCTL_DEBUG", "TORCTL_INFO", "TORCTL_NOTICE", "TORCTL_WARN", "TORCTL_ERR"]))
+    elif flag == "7": expandedEvents = expandedEvents.union(set(["TORCTL_INFO", "TORCTL_NOTICE", "TORCTL_WARN", "TORCTL_ERR"]))
+    elif flag == "8": expandedEvents = expandedEvents.union(set(["TORCTL_NOTICE", "TORCTL_WARN", "TORCTL_ERR"]))
+    elif flag == "9": expandedEvents = expandedEvents.union(set(["TORCTL_WARN", "TORCTL_ERR"]))
+    elif flag == "0": expandedEvents.add("TORCTL_ERR")
+    elif flag in TOR_EVENT_TYPES:
+      expandedEvents.add(TOR_EVENT_TYPES[flag])
+    else:
+      invalidFlags += flag
+  
+  if invalidFlags: raise ValueError(invalidFlags)
+  else: return expandedEvents
+
+class LogMonitor(TorCtl.PostEventListener, panel.Panel):
+  """
+  Tor event listener, noting messages, the time, and their type in a panel.
+  """
+  
+  def __init__(self, stdscr, conn, loggedEvents):
+    TorCtl.PostEventListener.__init__(self)
+    panel.Panel.__init__(self, stdscr, "log", 0)
+    self.scroll = 0
+    self.msgLog = []                      # tuples of (logText, color)
+    self.isPaused = False
+    self.pauseBuffer = []                 # location where messages are buffered if paused
+    self.loggedEvents = loggedEvents      # events we're listening to
+    self.lastHeartbeat = time.time()      # time of last event
+    self.regexFilter = None               # filter for presented log events (no filtering if None)
+    self.eventTimeOverwrite = None        # replaces time for further events with this (uses time it occures if None)
+    self.controlPortClosed = False        # flag set if TorCtl provided notice that control port is closed
+    
+    # prevents attempts to redraw while processing batch of events
+    previousPauseState = self.isPaused
+    self.setPaused(True)
+    log.addListeners([log.DEBUG, log.INFO, log.NOTICE, log.WARN, log.ERR], self.arm_event_wrapper, True)
+    self.setPaused(previousPauseState)
+    
+    # attempts to process events from log file
+    if PRE_POPULATE_LOG:
+      previousPauseState = self.isPaused
+      
+      try:
+        logFileLoc = None
+        loggingLocations = conn.get_option("Log")
+        
+        for entry in loggingLocations:
+          entryComp = entry[1].split()
+          if entryComp[1] == "file":
+            logFileLoc = entryComp[2]
+            break
+        
+        if logFileLoc:
+          # prevents attempts to redraw while processing batch of events
+          self.setPaused(True)
+          
+          # trims log to last entries to deal with logs when they're in the GB or TB range
+          # throws IOError if tail fails (falls to the catch-all later)
+          # TODO: now that this is using sysTools figure out if we can do away with the catch-all...
+          limit = PRE_POPULATE_MIN_LIMIT if ("DEBUG" in self.loggedEvents or "INFO" in self.loggedEvents) else PRE_POPULATE_MAX_LIMIT
+          
+          # truncates to entries for this tor instance
+          lines = sysTools.call("tail -n %i %s" % (limit, logFileLoc))
+          instanceStart = 0
+          for i in range(len(lines) - 1, -1, -1):
+            if "opening log file" in lines[i]:
+              instanceStart = i
+              break
+          
+          for line in lines[instanceStart:]:
+            lineComp = line.split()
+            eventType = lineComp[3][1:-1].upper()
+            
+            if eventType in self.loggedEvents:
+              timeComp = lineComp[2][:lineComp[2].find(".")].split(":")
+              self.eventTimeOverwrite = (0, 0, 0, int(timeComp[0]), int(timeComp[1]), int(timeComp[2]))
+              self.listen(TorCtl.LogEvent(eventType, " ".join(lineComp[4:])))
+      except Exception: pass # disreguard any issues that might arise
+      finally:
+        self.setPaused(previousPauseState)
+        self.eventTimeOverwrite = None
+  
+  def handleKey(self, key):
+    # scroll movement
+    if key in (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE):
+      pageHeight, shift = self.getPreferredSize()[0] - 1, 0
+      
+      # location offset
+      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
+      
+      # restricts to valid bounds and applies
+      maxLoc = self.getLogDisplayLength() - pageHeight
+      self.scroll = max(0, min(self.scroll + shift, maxLoc))
+  
+  # Listens for all event types and redirects to registerEvent
+  def circ_status_event(self, event):
+    if "CIRC" in self.loggedEvents:
+      optionalParams = ""
+      if event.purpose: optionalParams += " PURPOSE: %s" % event.purpose
+      if event.reason: optionalParams += " REASON: %s" % event.reason
+      if event.remote_reason: optionalParams += " REMOTE_REASON: %s" % event.remote_reason
+      self.registerEvent("CIRC", "ID: %-3s STATUS: %-10s PATH: %s%s" % (event.circ_id, event.status, ", ".join(event.path), optionalParams), "yellow")
+  
+  def buildtimeout_set_event(self, event):
+    # TODO: not sure how to stimulate event - needs sanity check
+    try:
+      self.registerEvent("BUILDTIMEOUT_SET", "SET_TYPE: %s, TOTAL_TIMES: %s, TIMEOUT_MS: %s, XM: %s, ALPHA: %s, CUTOFF_QUANTILE: %s" % (event.set_type, event.total_times, event.timeout_ms, event.xm, event.alpha, event.cutoff_quantile), "white")
+    except TypeError:
+      self.registerEvent("BUILDTIMEOUT_SET", "DEBUG -> SET_TYPE: %s, TOTAL_TIMES: %s, TIMEOUT_MS: %s, XM: %s, ALPHA: %s, CUTOFF_QUANTILE: %s" % (type(event.set_type), type(event.total_times), type(event.timeout_ms), type(event.xm), type(event.alpha), type(event.cutoff_quantile)), "white")
+  
+  def stream_status_event(self, event):
+    # TODO: not sure how to stimulate event - needs sanity check
+    try:
+      self.registerEvent("STREAM", "ID: %s STATUS: %s CIRC_ID: %s TARGET: %s:%s REASON: %s REMOTE_REASON: %s SOURCE: %s SOURCE_ADDR: %s PURPOSE: %s" % (event.strm_id, event.status, event.circ_id, event.target_host, event.target_port, event.reason, event.remote_reason, event.source, event.source_addr, event.purpose), "white")
+    except TypeError:
+      self.registerEvent("STREAM", "DEBUG -> ID: %s STATUS: %s CIRC_ID: %s TARGET: %s:%s REASON: %s REMOTE_REASON: %s SOURCE: %s SOURCE_ADDR: %s PURPOSE: %s" % (type(event.strm_id), type(event.status), type(event.circ_id), type(event.target_host), type(event.target_port), type(event.reason), type(event.remote_reason), type(event.source), type(event.source_addr), type(event.purpose)), "white")
+  
+  def or_conn_status_event(self, event):
+    optionalParams = ""
+    if event.age: optionalParams += " AGE: %-3s" % event.age
+    if event.read_bytes: optionalParams += " READ: %-4i" % event.read_bytes
+    if event.wrote_bytes: optionalParams += " WRITTEN: %-4i" % event.wrote_bytes
+    if event.reason: optionalParams += " REASON: %-6s" % event.reason
+    if event.ncircs: optionalParams += " NCIRCS: %i" % event.ncircs
+    self.registerEvent("ORCONN", "STATUS: %-10s ENDPOINT: %-20s%s" % (event.status, event.endpoint, optionalParams), "white")
+  
+  def stream_bw_event(self, event):
+    # TODO: not sure how to stimulate event - needs sanity check
+    try:
+      self.registerEvent("STREAM_BW", "ID: %s READ: %i WRITTEN: %i" % (event.strm_id, event.bytes_read, event.bytes_written), "white")
+    except TypeError:
+      self.registerEvent("STREAM_BW", "DEBUG -> ID: %s READ: %s WRITTEN: %s" % (type(event.strm_id), type(event.bytes_read), type(event.bytes_written)), "white")
+  
+  def bandwidth_event(self, event):
+    self.lastHeartbeat = time.time() # ensures heartbeat at least once a second
+    if "BW" in self.loggedEvents: self.registerEvent("BW", "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
+  
+  def msg_event(self, event):
+    self.registerEvent(event.level, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
+  
+  def new_desc_event(self, event):
+    if "NEWDESC" in self.loggedEvents:
+      idlistStr = [str(item) for item in event.idlist]
+      self.registerEvent("NEWDESC", ", ".join(idlistStr), "white")
+  
+  def address_mapped_event(self, event):
+    self.registerEvent("ADDRMAP", "%s, %s -> %s" % (event.when, event.from_addr, event.to_addr), "white")
+  
+  def ns_event(self, event):
+    # NetworkStatus params: nickname, idhash, orhash, ip, orport (int), dirport (int), flags, idhex, bandwidth, updated (datetime)
+    if "NS" in self.loggedEvents:
+      msg = ""
+      for ns in event.nslist:
+        msg += ", %s (%s:%i)" % (ns.nickname, ns.ip, ns.orport)
+      if len(msg) > 1: msg = msg[2:]
+      self.registerEvent("NS", "Listed (%i): %s" % (len(event.nslist), msg), "blue")
+  
+  def new_consensus_event(self, event):
+    if "NEWCONSENSUS" in self.loggedEvents:
+      msg = ""
+      for ns in event.nslist:
+        msg += ", %s (%s:%i)" % (ns.nickname, ns.ip, ns.orport)
+      self.registerEvent("NEWCONSENSUS", "Listed (%i): %s" % (len(event.nslist), msg), "magenta")
+  
+  def unknown_event(self, event):
+    if "UNKNOWN" in self.loggedEvents: self.registerEvent("UNKNOWN", event.event_string, "red")
+  
+  def arm_event_wrapper(self, level, msg, eventTime):
+    # temporary adaptor hack to use the new logging functions until I'm sure they'll work
+    # TODO: insert into log according to the event's timestamp (harder part
+    # here will be interpreting tor's event timestamps...)
+    self.monitor_event(level, msg)
+  
+  def monitor_event(self, level, msg):
+    # events provided by the arm monitor
+    if "ARM_" + level in self.loggedEvents: self.registerEvent("ARM-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
+  
+  def tor_ctl_event(self, level, msg):
+    # events provided by TorCtl
+    if "TORCTL_" + level in self.loggedEvents: self.registerEvent("TORCTL-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
+  
+  def write(self, msg):
+    """
+    Tracks TorCtl events. Ugly hack since TorCtl/TorUtil.py expects a file.
+    """
+    
+    timestampStart = msg.find("[")
+    timestampEnd = msg.find("]")
+    
+    level = msg[:timestampStart]
+    msg = msg[timestampEnd + 2:].strip()
+    
+    if TOR_CTL_CLOSE_MSG in msg:
+      # TorCtl providing notice that control port is closed
+      self.controlPortClosed = True
+      #log.log(log.NOTICE, "Tor control port closed")
+      
+      # Allows the Controller to notice that tor's shut down.
+      # TODO: should make the controller the torctl event listener rather than
+      # this log panel (it'll also make this less hacky)
+      torTools.getConn().isAlive()
+    self.tor_ctl_event(level, msg)
+  
+  def flush(self): pass
+  
+  def registerEvent(self, type, msg, color):
+    """
+    Notes event and redraws log. If paused it's held in a temporary buffer. If 
+    msg is a list then this is expanded to multiple lines.
+    """
+    
+    if not type.startswith("ARM"): self.lastHeartbeat = time.time()
+    eventTime = self.eventTimeOverwrite if self.eventTimeOverwrite else time.localtime()
+    toAdd = []
+    
+    # wraps if a single line message
+    if isinstance(msg, str): msg = [msg]
+    
+    firstLine = True
+    for msgLine in msg:
+      # strips control characters to avoid screwing up the terminal
+      msgLine = "".join([char for char in msgLine if isprint(char)])
+      
+      header = "%02i:%02i:%02i %s" % (eventTime[3], eventTime[4], eventTime[5], "[%s]" % type) if firstLine else ""
+      toAdd.append("%s %s" % (header, msgLine))
+      firstLine = False
+    
+    toAdd.reverse()
+    if self.isPaused:
+      for msgLine in toAdd: self.pauseBuffer.insert(0, (msgLine, color))
+      if len(self.pauseBuffer) > MAX_LOG_ENTRIES: del self.pauseBuffer[MAX_LOG_ENTRIES:]
+    else:
+      for msgLine in toAdd: self.msgLog.insert(0, (msgLine, color))
+      if len(self.msgLog) > MAX_LOG_ENTRIES: del self.msgLog[MAX_LOG_ENTRIES:]
+      self.redraw(True)
+  
+  def draw(self, subwindow, width, height):
+    """
+    Redraws message log. Entries stretch to use available space and may
+    contain up to two lines. Starts with newest entries.
+    """
+    
+    isScrollBarVisible = self.getLogDisplayLength() > height - 1
+    xOffset = 3 if isScrollBarVisible else 0 # content offset for scroll bar
+    
+    # draws label - uses ellipsis if too long, for instance:
+    # Events (DEBUG, INFO, NOTICE, WARN...):
+    eventsLabel = "Events"
+    
+    # separates tor and arm runlevels (might be able to show as range)
+    eventsList = list(self.loggedEvents)
+    torRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, ""))
+    armRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, "ARM_"))
+    torctlRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, "TORCTL_"))
+    
+    if torctlRunlevelLabel: eventsList = ["TORCTL " + torctlRunlevelLabel] + eventsList
+    if armRunlevelLabel: eventsList = ["ARM " + armRunlevelLabel] + eventsList
+    if torRunlevelLabel: eventsList = [torRunlevelLabel] + eventsList
+    
+    eventsListing = ", ".join(eventsList)
+    filterLabel = "" if not self.regexFilter else " - filter: %s" % self.regexFilter.pattern
+    
+    firstLabelLen = eventsListing.find(", ")
+    if firstLabelLen == -1: firstLabelLen = len(eventsListing)
+    else: firstLabelLen += 3
+    
+    if width > 10 + firstLabelLen:
+      eventsLabel += " ("
+      
+      if len(eventsListing) > width - 11:
+        labelBreak = eventsListing[:width - 12].rfind(", ")
+        eventsLabel += "%s..." % eventsListing[:labelBreak]
+      elif len(eventsListing) + len(filterLabel) > width - 11:
+        eventsLabel += eventsListing
+      else: eventsLabel += eventsListing + filterLabel
+      eventsLabel += ")"
+    eventsLabel += ":"
+    
+    self.addstr(0, 0, eventsLabel, curses.A_STANDOUT)
+    
+    # log entries
+    maxLoc = self.getLogDisplayLength() - height + 1
+    self.scroll = max(0, min(self.scroll, maxLoc))
+    lineCount = 1 - self.scroll
+    
+    for (line, color) in self.msgLog:
+      if self.regexFilter and not self.regexFilter.search(line):
+        continue  # filter doesn't match log message - skip
+      
+      # splits over too lines if too long
+      if len(line) < width:
+        if lineCount >= 1: self.addstr(lineCount, xOffset, line, uiTools.getColor(color))
+        lineCount += 1
+      else:
+        (line1, line2) = splitLine(line, width - xOffset)
+        if lineCount >= 1: self.addstr(lineCount, xOffset, line1, uiTools.getColor(color))
+        if lineCount >= 0: self.addstr(lineCount + 1, xOffset, line2, uiTools.getColor(color))
+        lineCount += 2
+      
+      if lineCount >= height: break # further log messages wouldn't fit
+    
+    if isScrollBarVisible: self.addScrollBar(self.scroll, self.scroll + height - 1, self.getLogDisplayLength(), 1)
+  
+  def getLogDisplayLength(self):
+    """
+    Provides the number of lines the log would currently occupy.
+    """
+    
+    logLength = len(self.msgLog)
+    
+    # takes into account filtered and wrapped messages
+    for (line, color) in self.msgLog:
+      if self.regexFilter and not self.regexFilter.search(line): logLength -= 1
+      elif len(line) >= self.getPreferredSize()[1]: logLength += 1
+    
+    return logLength
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents message log from being updated with new events.
+    """
+    
+    if isPause == self.isPaused: return
+    
+    self.isPaused = isPause
+    if self.isPaused: self.pauseBuffer = []
+    else:
+      self.msgLog = (self.pauseBuffer + self.msgLog)[:MAX_LOG_ENTRIES]
+      if self.win: self.redraw(True) # hack to avoid redrawing during init
+  
+  def getHeartbeat(self):
+    """
+    Provides the number of seconds since the last registered event (this always
+    listens to BW events so should be less than a second if relay's still
+    responsive).
+    """
+    
+    return time.time() - self.lastHeartbeat
+
+def parseRunlevelRanges(eventsList, searchPrefix):
+  """
+  This parses a list of events to provide an ordered list of runlevels, 
+  condensed if three or more are in a contiguous range. This removes parsed 
+  runlevels from the eventsList. For instance:
+  
+  eventsList = ["BW", "ARM_WARN", "ERR", "ARM_ERR", "ARM_DEBUG", "ARM_NOTICE"]
+  searchPrefix = "ARM_"
+  
+  results in:
+  eventsList = ["BW", "ERR"]
+  return value is ["DEBUG", "NOTICE - ERR"]
+  
+  """
+  
+  # blank ending runlevel forces the break condition to be reached at the end
+  runlevels = ["DEBUG", "INFO", "NOTICE", "WARN", "ERR", ""]
+  runlevelLabels = []
+  start, end = "", ""
+  rangeLength = 0
+  
+  for level in runlevels:
+    if searchPrefix + level in eventsList:
+      eventsList.remove(searchPrefix + level)
+      
+      if start:
+        end = level
+        rangeLength += 1
+      else:
+        start = level
+        rangeLength = 1
+    elif rangeLength > 0:
+      # reached a break in the runlevels
+      if rangeLength == 1: runlevelLabels += [start]
+      elif rangeLength == 2: runlevelLabels += [start, end]
+      else: runlevelLabels += ["%s - %s" % (start, end)]
+      
+      start, end = "", ""
+      rangeLength = 0
+  
+  return runlevelLabels
+
+def splitLine(message, x):
+  """
+  Divides message into two lines, attempting to do it on a wordbreak.
+  """
+  
+  lastWordbreak = message[:x].rfind(" ")
+  if x - lastWordbreak < 10:
+    line1 = message[:lastWordbreak]
+    line2 = "  %s" % message[lastWordbreak:].strip()
+  else:
+    # over ten characters until the last word - dividing
+    line1 = "%s-" % message[:x - 2]
+    line2 = "  %s" % message[x - 2:].strip()
+  
+  # ends line with ellipsis if too long
+  if len(line2) > x:
+    lastWordbreak = line2[:x - 4].rfind(" ")
+    
+    # doesn't use wordbreak if it's a long word or the whole line is one 
+    # word (picking up on two space indent to have index 1)
+    if x - lastWordbreak > 10 or lastWordbreak == 1: lastWordbreak = x - 4
+    line2 = "%s..." % line2[:lastWordbreak]
+  
+  return (line1, line2)
+

Copied: arm/trunk/src/prereq.py (from rev 22947, arm/trunk/init/prereq.py)
===================================================================
--- arm/trunk/src/prereq.py	                        (rev 0)
+++ arm/trunk/src/prereq.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,23 @@
+"""
+Provides a warning and error code if python version isn't compatible.
+"""
+
+import sys
+
+if __name__ == '__main__':
+  majorVersion = sys.version_info[0]
+  minorVersion = sys.version_info[1]
+  
+  if majorVersion > 2:
+    print("arm isn't compatible beyond the python 2.x series\n")
+    sys.exit(1)
+  elif majorVersion < 2 or minorVersion < 5:
+    print("arm requires python version 2.5 or greater\n")
+    sys.exit(1)
+  
+  try:
+    import curses
+  except ImportError:
+    print("arm requires curses - try installing the python-curses package\n")
+    sys.exit(1)
+

Copied: arm/trunk/src/starter.py (from rev 22947, arm/trunk/init/starter.py)
===================================================================
--- arm/trunk/src/starter.py	                        (rev 0)
+++ arm/trunk/src/starter.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,177 @@
+#!/usr/bin/env python
+
+"""
+Command line application for monitoring Tor relays, providing real time status
+information. This is the starter for the application, handling and validating
+command line parameters.
+"""
+
+import os
+import sys
+import getopt
+
+import interface.controller
+import interface.logPanel
+import util.conf
+import util.connections
+import util.hostnames
+import util.log
+import util.panel
+import util.sysTools
+import util.torTools
+import util.uiTools
+import TorCtl.TorUtil
+
+VERSION = "1.3.6_dev"
+LAST_MODIFIED = "July 7, 2010"
+
+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"}
+
+OPT = "i:c:be:vh"
+OPT_EXPANDED = ["interface=", "config=", "blind", "event=", "version", "help"]
+HELP_MSG = """Usage arm [OPTION]
+Terminal status monitor for Tor relays.
+
+  -i, --interface [ADDRESS:]PORT  change control interface from %s:%i
+  -c, --config CONFIG_PATH        loaded configuration options, CONFIG_PATH
+                                    defaults to: %s
+  -b, --blind                     disable connection lookups
+  -e, --event EVENT_FLAGS         event types in message log  (default: %s)
+%s
+  -v, --version                   provides version information
+  -h, --help                      presents this help
+
+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)
+
+def isValidIpAddr(ipStr):
+  """
+  Returns true if input is a valid IPv4 address, false otherwise.
+  """
+  
+  for i in range(4):
+    if i < 3:
+      divIndex = ipStr.find(".")
+      if divIndex == -1: return False # expected a period to be valid
+      octetStr = ipStr[:divIndex]
+      ipStr = ipStr[divIndex + 1:]
+    else:
+      octetStr = ipStr
+    
+    try:
+      octet = int(octetStr)
+      if not octet >= 0 or not octet <= 255: return False
+    except ValueError:
+      # address value isn't an integer
+      return False
+  
+  return True
+
+if __name__ == '__main__':
+  param = dict([(key, None) for key in DEFAULTS.keys()])
+  configPath = DEFAULT_CONFIG            # path used for customized configuration
+  
+  # parses user input, noting any issues
+  try:
+    opts, args = getopt.getopt(sys.argv[1:], OPT, OPT_EXPANDED)
+  except getopt.GetoptError, exc:
+    print str(exc) + " (for usage provide --help)"
+    sys.exit()
+  
+  for opt, arg in opts:
+    if opt in ("-i", "--interface"):
+      # defines control interface address/port
+      controlAddr, controlPort = None, None
+      divIndex = arg.find(":")
+      
+      try:
+        if divIndex == -1:
+          controlPort = int(arg)
+        else:
+          controlAddr = arg[0:divIndex]
+          controlPort = int(arg[divIndex + 1:])
+      except ValueError:
+        print "'%s' isn't a valid port number" % arg
+        sys.exit()
+      
+      param["startup.interface.ipAddress"] = controlAddr
+      param["startup.interface.port"] = controlPort
+    elif opt in ("-c", "--config"): configPath = arg  # sets path of user's config
+    elif opt in ("-b", "--blind"):
+      param["startup.blindModeEnabled"] = True        # prevents connection lookups
+    elif opt in ("-e", "--event"):
+      param["startup.events"] = arg                   # set event flags
+    elif opt in ("-v", "--version"):
+      print "arm version %s (released %s)\n" % (VERSION, LAST_MODIFIED)
+      sys.exit()
+    elif opt in ("-h", "--help"):
+      print HELP_MSG
+      sys.exit()
+  
+  # attempts to load user's custom configuration
+  config = util.conf.getConfig("arm")
+  config.path = configPath
+  
+  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)
+    except IOError, exc:
+      msg = "Failed to load configuration (using defaults): \"%s\"" % str(exc)
+      util.log.log(util.log.WARN, msg)
+  else:
+    msg = "No configuration found at '%s', using defaults" % configPath
+    util.log.log(util.log.NOTICE, msg)
+  
+  # overwrites undefined parameters with defaults
+  for key in param.keys():
+    if param[key] == None: param[key] = DEFAULTS[key]
+  
+  # validates that input has a valid ip address and port
+  controlAddr = param["startup.interface.ipAddress"]
+  controlPort = param["startup.interface.port"]
+  
+  if not isValidIpAddr(controlAddr):
+    print "'%s' isn't a valid IP address" % controlAddr
+    sys.exit()
+  elif controlPort < 0 or controlPort > 65535:
+    print "'%s' isn't a valid port number (ports range 0-65535)" % controlPort
+    sys.exit()
+  
+  # validates and expands log event flags
+  try:
+    expandedEvents = interface.logPanel.expandEvents(param["startup.events"])
+  except ValueError, exc:
+    for flag in str(exc):
+      print "Unrecognized event flag: %s" % flag
+    sys.exit()
+  
+  # temporarily disables TorCtl logging to prevent issues from going to stdout while starting
+  TorCtl.TorUtil.loglevel = "NONE"
+  
+  # sets up TorCtl connection, prompting for the passphrase if necessary and
+  # sending problems to stdout if they arise
+  util.torTools.INCORRECT_PASSWORD_MSG = "Controller password found in '%s' was incorrect" % configPath
+  authPassword = config.get("startup.controlPassword", DEFAULTS["startup.controlPassword"])
+  conn = util.torTools.connect(controlAddr, controlPort, authPassword)
+  if conn == None: sys.exit(1)
+  
+  controller = util.torTools.getConn()
+  controller.init(conn)
+  
+  interface.controller.startTorMonitor(expandedEvents, param["startup.blindModeEnabled"])
+  conn.close()
+

Deleted: arm/trunk/src/util/torTools.py
===================================================================
--- arm/trunk/util/torTools.py	2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/util/torTools.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,1004 +0,0 @@
-"""
-Helper for working with an active tor process. This both provides a wrapper for
-accessing TorCtl and notifications of state changes to subscribers. To quickly
-fetch a TorCtl instance to experiment with use the following:
-
->>> import util.torTools
->>> conn = util.torTools.connect()
->>> conn.get_info("version")["version"]
-'0.2.1.24'
-"""
-
-import os
-import time
-import socket
-import getpass
-import thread
-import threading
-
-from TorCtl import TorCtl, TorUtil
-
-import log
-import sysTools
-
-# enums for tor's controller state:
-# TOR_INIT - attached to a new controller or restart/sighup signal received
-# TOR_CLOSED - control port closed
-TOR_INIT, TOR_CLOSED = range(1, 3)
-
-# message logged by default when a controller can't set an event type
-DEFAULT_FAILED_EVENT_MSG = "Unsupported event type: %s"
-
-# TODO: check version when reattaching to controller and if version changes, flush?
-# Skips attempting to set events we've failed to set before. This avoids
-# logging duplicate warnings but can be problematic if controllers belonging
-# to multiple versions of tor are attached, making this unreflective of the
-# controller's capabilites. However, this is a pretty bizarre edge case.
-DROP_FAILED_EVENTS = True
-FAILED_EVENTS = set()
-
-CONTROLLER = None # singleton Controller instance
-INCORRECT_PASSWORD_MSG = "Provided passphrase was incorrect"
-
-# valid keys for the controller's getInfo cache
-CACHE_ARGS = ("nsEntry", "descEntry", "bwRate", "bwBurst", "bwObserved",
-              "bwMeasured", "flags", "fingerprint", "pid")
-
-TOR_CTL_CLOSE_MSG = "Tor closed control connection. Exiting event thread."
-UNKNOWN = "UNKNOWN" # value used by cached information if undefined
-CONFIG = {"log.torGetInfo": log.DEBUG, "log.torGetConf": log.DEBUG}
-
-# events used for controller functionality:
-# NOTICE - used to detect when tor is shut down
-# NEWDESC, NS, and NEWCONSENSUS - used for cache invalidation
-REQ_EVENTS = {"NOTICE": "this will be unable to detect when tor is shut down",
-              "NEWDESC": "information related to descriptors will grow stale",
-              "NS": "information related to the consensus will grow stale",
-              "NEWCONSENSUS": "information related to the consensus will grow stale"}
-
-def loadConfig(config):
-  config.update(CONFIG)
-
-def makeCtlConn(controlAddr="127.0.0.1", controlPort=9051):
-  """
-  Opens a socket to the tor controller and queries its authentication type,
-  raising an IOError if problems occur. The result of this function is a tuple
-  of the TorCtl connection and the authentication type, where the later is one
-  of the following:
-  "NONE"          - no authentication required
-  "PASSWORD"      - requires authentication via a hashed password
-  "COOKIE=<FILE>" - requires the specified authentication cookie
-  
-  Arguments:
-    controlAddr - ip address belonging to the controller
-    controlPort - port belonging to the controller
-  """
-  
-  try:
-    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-    s.connect((controlAddr, controlPort))
-    conn = TorCtl.Connection(s)
-  except socket.error, exc:
-    if "Connection refused" in exc.args:
-      # most common case - tor control port isn't available
-      raise IOError("Connection refused. Is the ControlPort enabled?")
-    else: raise IOError("Failed to establish socket: %s" % exc)
-  
-  # check PROTOCOLINFO for authentication type
-  try:
-    authInfo = conn.sendAndRecv("PROTOCOLINFO\r\n")[1][1]
-  except TorCtl.ErrorReply, exc:
-    raise IOError("Unable to query PROTOCOLINFO for authentication type: %s" % exc)
-  
-  if authInfo.startswith("AUTH METHODS=NULL"):
-    # no authentication required
-    return (conn, "NONE")
-  elif authInfo.startswith("AUTH METHODS=HASHEDPASSWORD"):
-    # password authentication
-    return (conn, "PASSWORD")
-  elif authInfo.startswith("AUTH METHODS=COOKIE"):
-    # cookie authentication, parses authentication cookie path
-    start = authInfo.find("COOKIEFILE=\"") + 12
-    end = authInfo.find("\"", start)
-    return (conn, "COOKIE=%s" % authInfo[start:end])
-
-def initCtlConn(conn, authType="NONE", authVal=None):
-  """
-  Authenticates to a tor connection. The authentication type can be any of the
-  following strings:
-  NONE, PASSWORD, COOKIE
-  
-  if the authentication type is anything other than NONE then either a
-  passphrase or path to an authentication cookie is expected. If an issue
-  arises this raises either of the following:
-    - IOError for failures in reading an authentication cookie
-    - TorCtl.ErrorReply for authentication failures
-  
-  Argument:
-    conn     - unauthenticated TorCtl connection
-    authType - type of authentication method to use
-    authVal  - passphrase or path to authentication cookie
-  """
-  
-  # validates input
-  if authType not in ("NONE", "PASSWORD", "COOKIE"):
-    # authentication type unrecognized (possibly a new addition to the controlSpec?)
-    raise TorCtl.ErrorReply("Unrecognized authentication type: %s" % authType)
-  elif authType != "NONE" and authVal == None:
-    typeLabel = "passphrase" if authType == "PASSWORD" else "cookie"
-    raise TorCtl.ErrorReply("Unable to authenticate: no %s provided" % typeLabel)
-  
-  authCookie = None
-  try:
-    if authType == "NONE": conn.authenticate("")
-    elif authType == "PASSWORD": conn.authenticate(authVal)
-    else:
-      authCookie = open(authVal, "r")
-      conn.authenticate_cookie(authCookie)
-      authCookie.close()
-  except TorCtl.ErrorReply, exc:
-    if authCookie: authCookie.close()
-    issue = str(exc)
-    
-    # simplifies message if the wrong credentials were provided (common mistake)
-    if issue.startswith("515 Authentication failed: "):
-      if issue[27:].startswith("Password did not match"):
-        issue = "password incorrect"
-      elif issue[27:] == "Wrong length on authentication cookie.":
-        issue = "cookie value incorrect"
-    
-    raise TorCtl.ErrorReply("Unable to authenticate: %s" % issue)
-  except IOError, exc:
-    if authCookie: authCookie.close()
-    issue = None
-    
-    # cleaner message for common errors
-    if str(exc).startswith("[Errno 13] Permission denied"): issue = "permission denied"
-    elif str(exc).startswith("[Errno 2] No such file or directory"): issue = "file doesn't exist"
-    
-    # if problem's recognized give concise message, otherwise print exception string
-    if issue: raise IOError("Failed to read authentication cookie (%s): %s" % (issue, authVal))
-    else: raise IOError("Failed to read authentication cookie: %s" % exc)
-
-def connect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None):
-  """
-  Convenience method for quickly getting a TorCtl connection. This is very
-  handy for debugging or CLI setup, handling setup and prompting for a password
-  if necessary (if either none is provided as input or it fails). If any issues
-  arise this prints a description of the problem and returns None.
-  
-  Arguments:
-    controlAddr - ip address belonging to the controller
-    controlPort - port belonging to the controller
-    passphrase  - authentication passphrase (if defined this is used rather
-                  than prompting the user)
-  """
-  
-  try:
-    conn, authType = makeCtlConn(controlAddr, controlPort)
-    authValue = None
-    
-    if authType == "PASSWORD":
-      # password authentication, promting for the password if it wasn't provided
-      if passphrase: authValue = passphrase
-      else:
-        try: authValue = getpass.getpass()
-        except KeyboardInterrupt: return None
-    elif authType.startswith("COOKIE"):
-      authType, authValue = authType.split("=", 1)
-    
-    initCtlConn(conn, authType, authValue)
-    return conn
-  except Exception, exc:
-    if passphrase and str(exc) == "Unable to authenticate: password incorrect":
-      # provide a warning that the provided password didn't work, then try
-      # again prompting for the user to enter it
-      print INCORRECT_PASSWORD_MSG
-      return connect(controlAddr, controlPort)
-    else:
-      print exc
-      return None
-
-def getPid(controlPort=9051, pidFilePath=None):
-  """
-  Attempts to determine the process id for a running tor process, using the
-  following:
-  1. GETCONF PidFile
-  2. "pidof tor"
-  3. "netstat -npl | grep 127.0.0.1:%s" % <tor control port>
-  4. "ps -o pid -C tor"
-  
-  If pidof or ps provide multiple tor instances then their results are
-  discarded (since only netstat can differentiate using the control port). This
-  provides None if either no running process exists or it can't be determined.
-  
-  Arguments:
-    controlPort - control port of the tor process if multiple exist
-    pidFilePath - path to the pid file generated by tor
-  """
-  
-  # attempts to fetch via the PidFile, failing if:
-  # - the option is unset
-  # - unable to read the file (such as insufficient permissions)
-  
-  if pidFilePath:
-    try:
-      pidFile = open(pidFilePath, "r")
-      pidEntry = pidFile.readline().strip()
-      pidFile.close()
-      
-      if pidEntry.isdigit(): return pidEntry
-    except Exception: pass
-  
-  # attempts to resolve using pidof, failing if:
-  # - tor's running under a different name
-  # - there's multiple instances of tor
-  try:
-    results = sysTools.call("pidof tor")
-    if len(results) == 1 and len(results[0].split()) == 1:
-      pid = results[0].strip()
-      if pid.isdigit(): return pid
-  except IOError: pass
-  
-  # attempts to resolve using netstat, failing if:
-  # - tor's being run as a different user due to permissions
-  try:
-    results = sysTools.call("netstat -npl | grep 127.0.0.1:%i" % controlPort)
-    
-    if len(results) == 1:
-      results = results[0].split()[6] # process field (ex. "7184/tor")
-      pid = results[:results.find("/")]
-      if pid.isdigit(): return pid
-  except IOError: pass
-  
-  # attempts to resolve using ps, failing if:
-  # - tor's running under a different name
-  # - there's multiple instances of tor
-  try:
-    results = sysTools.call("ps -o pid -C tor")
-    if len(results) == 2:
-      pid = results[1].strip()
-      if pid.isdigit(): return pid
-  except IOError: pass
-  
-  return None
-
-def getConn():
-  """
-  Singleton constructor for a Controller. Be aware that this start
-  uninitialized, needing a TorCtl instance before it's fully functional.
-  """
-  
-  global CONTROLLER
-  if CONTROLLER == None: CONTROLLER = Controller()
-  return CONTROLLER
-
-class Controller(TorCtl.PostEventListener):
-  """
-  TorCtl wrapper providing convenience functions, listener functionality for
-  tor's state, and the capability for controller connections to be restarted
-  if closed.
-  """
-  
-  def __init__(self):
-    TorCtl.PostEventListener.__init__(self)
-    self.conn = None                    # None if uninitialized or controller's been closed
-    self.connLock = threading.RLock()
-    self.eventListeners = []            # instances listening for tor controller events
-    self.torctlListeners = []           # callback functions for TorCtl events
-    self.statusListeners = []           # callback functions for tor's state changes
-    self.controllerEvents = []          # list of successfully set controller events
-    self._isReset = False               # internal flag for tracking resets
-    self._status = TOR_CLOSED           # current status of the attached control port
-    self._statusTime = 0                # unix time-stamp for the duration of the status
-    self.lastHeartbeat = 0              # time of the last tor event
-    
-    # cached getInfo parameters (None if unset or possibly changed)
-    self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
-    
-    # directs TorCtl to notify us of events
-    TorUtil.loglevel = "DEBUG"
-    TorUtil.logfile = self
-  
-  def init(self, conn=None):
-    """
-    Uses the given TorCtl instance for future operations, notifying listeners
-    about the change.
-    
-    Arguments:
-      conn - TorCtl instance to be used, if None then a new instance is fetched
-             via the connect function
-    """
-    
-    if conn == None:
-      conn = connect()
-      
-      if conn == None: raise ValueError("Unable to initialize TorCtl instance.")
-    
-    if conn.is_live() and conn != self.conn:
-      self.connLock.acquire()
-      
-      if self.conn: self.close() # shut down current connection
-      self.conn = conn
-      self.conn.add_event_listener(self)
-      for listener in self.eventListeners: self.conn.add_event_listener(listener)
-      
-      # sets the events listened for by the new controller (incompatible events
-      # are dropped with a logged warning)
-      self.setControllerEvents(self.controllerEvents)
-      
-      self.connLock.release()
-      
-      self._status = TOR_INIT
-      self._statusTime = time.time()
-      
-      # notifies listeners that a new controller is available
-      thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,))
-  
-  def close(self):
-    """
-    Closes the current TorCtl instance and notifies listeners.
-    """
-    
-    self.connLock.acquire()
-    if self.conn:
-      self.conn.close()
-      self.conn = None
-      self.connLock.release()
-      
-      self._status = TOR_CLOSED
-      self._statusTime = time.time()
-      
-      # notifies listeners that the controller's been shut down
-      thread.start_new_thread(self._notifyStatusListeners, (TOR_CLOSED,))
-    else: self.connLock.release()
-  
-  def isAlive(self):
-    """
-    Returns True if this has been initialized with a working TorCtl instance,
-    False otherwise.
-    """
-    
-    self.connLock.acquire()
-    
-    result = False
-    if self.conn:
-      if self.conn.is_live(): result = True
-      else: self.close()
-    
-    self.connLock.release()
-    return result
-  
-  def getHeartbeat(self):
-    """
-    Provides the time of the last registered tor event (if listening for BW
-    events then this should occure every second if relay's still responsive).
-    This returns zero if this has never received an event.
-    """
-    
-    return self.lastHeartbeat
-  
-  def getTorCtl(self):
-    """
-    Provides the current TorCtl connection. If unset or closed then this
-    returns None.
-    """
-    
-    self.connLock.acquire()
-    result = None
-    if self.isAlive(): result = self.conn
-    self.connLock.release()
-    
-    return result
-  
-  def getInfo(self, param, default = None, suppressExc = True):
-    """
-    Queries the control port for the given GETINFO option, providing the
-    default if the response is undefined or fails for any reason (error
-    response, control port closed, initiated, etc).
-    
-    Arguments:
-      param       - GETINFO 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
-    """
-    
-    self.connLock.acquire()
-    
-    startTime = time.time()
-    result, raisedExc = default, None
-    if self.isAlive():
-      try:
-        getInfoVal = self.conn.get_info(param)[param]
-        if getInfoVal != None: result = getInfoVal
-      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
-        if type(exc) == TorCtl.TorCtlClosed: self.close()
-        raisedExc = exc
-    
-    msg = "tor control call: GETINFO %s (runtime: %0.4f)" % (param, time.time() - startTime)
-    log.log(CONFIG["log.torGetInfo"], msg)
-    
-    self.connLock.release()
-    
-    if not suppressExc and raisedExc: raise raisedExc
-    else: return result
-  
-  def getOption(self, param, default = None, multiple = False, suppressExc = True):
-    """
-    Queries the control port for the given configuration option, providing the
-    default if the response is undefined or fails for any reason. If multiple
-    values exist then this arbitrarily returns the first unless the multiple
-    flag is set.
-    
-    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
-      suppressExc - suppresses lookup errors (returning the default) if true,
-                    otherwise this raises the original exception
-    """
-    
-    self.connLock.acquire()
-    
-    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
-    
-    msg = "tor control call: GETCONF %s (runtime: %0.4f)" % (param, time.time() - startTime)
-    log.log(CONFIG["log.torGetConf"], msg)
-    
-    self.connLock.release()
-    
-    if not suppressExc and raisedExc: raise raisedExc
-    elif result == []: return default
-    else: return result
-  
-  def getMyNetworkStatus(self, default = None):
-    """
-    Provides the network status entry for this relay if available. This is
-    occasionally expanded so results may vary depending on tor's version. For
-    0.2.2.13 they contained entries like the following:
-    
-    r caerSidi p1aag7VwarGxqctS7/fS0y5FU+s 9On1TRGCEpljszPpJR1hKqlzaY8 2010-05-26 09:26:06 76.104.132.98 9001 0
-    s Fast HSDir Named Running Stable Valid
-    w Bandwidth=25300
-    p reject 1-65535
-    
-    Arguments:
-      default - result if the query fails
-    """
-    
-    return self._getRelayAttr("nsEntry", default)
-  
-  def getMyDescriptor(self, default = None):
-    """
-    Provides the descriptor entry for this relay if available.
-    
-    Arguments:
-      default - result if the query fails
-    """
-    
-    return self._getRelayAttr("descEntry", default)
-  
-  def getMyBandwidthRate(self, default = None):
-    """
-    Provides the effective relaying bandwidth rate of this relay. Currently
-    this doesn't account for SETCONF events.
-    
-    Arguments:
-      default - result if the query fails
-    """
-    
-    return self._getRelayAttr("bwRate", default)
-  
-  def getMyBandwidthBurst(self, default = None):
-    """
-    Provides the effective bandwidth burst rate of this relay. Currently this
-    doesn't account for SETCONF events.
-    
-    Arguments:
-      default - result if the query fails
-    """
-    
-    return self._getRelayAttr("bwBurst", default)
-  
-  def getMyBandwidthObserved(self, default = None):
-    """
-    Provides the relay's current observed bandwidth (the throughput determined
-    from historical measurements on the client side). This is used in the
-    heuristic used for path selection if the measured bandwidth is undefined.
-    This is fetched from the descriptors and hence will get stale if
-    descriptors aren't periodically updated.
-    
-    Arguments:
-      default - result if the query fails
-    """
-    
-    return self._getRelayAttr("bwObserved", default)
-  
-  def getMyBandwidthMeasured(self, default = None):
-    """
-    Provides the relay's current measured bandwidth (the throughput as noted by
-    the directory authorities and used by clients for relay selection). This is
-    undefined if not in the consensus or with older versions of Tor. Depending
-    on the circumstances this can be from a variety of things (observed,
-    measured, weighted measured, etc) as described by:
-    https://trac.torproject.org/projects/tor/ticket/1566
-    
-    Arguments:
-      default - result if the query fails
-    """
-    
-    return self._getRelayAttr("bwMeasured", default)
-  
-  def getMyFingerprint(self, default = None):
-    """
-    Provides the fingerprint for this relay.
-    
-    Arguments:
-      default - result if the query fails
-    """
-    
-    return self._getRelayAttr("fingerprint", default, False)
-  
-  def getMyFlags(self, default = None):
-    """
-    Provides the flags held by this relay.
-    
-    Arguments:
-      default - result if the query fails or this relay isn't a part of the consensus yet
-    """
-    
-    return self._getRelayAttr("flags", default)
-  
-  def getMyPid(self):
-    """
-    Provides the pid of the attached tor process (None if no controller exists
-    or this can't be determined).
-    """
-    
-    return self._getRelayAttr("pid", None)
-  
-  def getStatus(self):
-    """
-    Provides a tuple consisting of the control port's current status and unix
-    time-stamp for when it became this way (zero if no status has yet to be
-    set).
-    """
-    
-    return (self._status, self._statusTime)
-  
-  def addEventListener(self, listener):
-    """
-    Directs further tor controller events to callback functions of the
-    listener. If a new control connection is initialized then this listener is
-    reattached.
-    
-    Arguments:
-      listener - TorCtl.PostEventListener instance listening for events
-    """
-    
-    self.connLock.acquire()
-    self.eventListeners.append(listener)
-    if self.isAlive(): self.conn.add_event_listener(listener)
-    self.connLock.release()
-  
-  def addTorCtlListener(self, callback):
-    """
-    Directs further TorCtl events to the callback function. Events are composed
-    of a runlevel and message tuple.
-    
-    Arguments:
-      callback - functor that'll accept the events, expected to be of the form:
-                 myFunction(runlevel, msg)
-    """
-    
-    self.torctlListeners.append(callback)
-  
-  def addStatusListener(self, callback):
-    """
-    Directs further events related to tor's controller status to the callback
-    function.
-    
-    Arguments:
-      callback - functor that'll accept the events, expected to be of the form:
-                 myFunction(controller, eventType)
-    """
-    
-    self.statusListeners.append(callback)
-  
-  def removeStatusListener(self, callback):
-    """
-    Stops listener from being notified of further events. This returns true if a
-    listener's removed, false otherwise.
-    
-    Arguments:
-      callback - functor to be removed
-    """
-    
-    if callback in self.statusListeners:
-      self.statusListeners.remove(callback)
-      return True
-    else: return False
-  
-  def getControllerEvents(self):
-    """
-    Provides the events the controller's currently configured to listen for.
-    """
-    
-    return list(self.controllerEvents)
-  
-  def setControllerEvents(self, events):
-    """
-    Sets the events being requested from any attached tor instance, logging
-    warnings for event types that aren't supported (possibly due to version
-    issues). Events in REQ_EVENTS will also be included, logging at the error
-    level with an additional description in case of failure.
-    
-    This remembers the successfully set events and tries to request them from
-    any tor instance it attaches to in the future too (again logging and
-    dropping unsuccessful event types).
-    
-    This returns the listing of event types that were successfully set. If not
-    currently attached to a tor instance then all events are assumed to be ok,
-    then attempted when next attached to a control port.
-    
-    Arguments:
-      events - listing of events to be set
-    """
-    
-    self.connLock.acquire()
-    
-    returnVal = []
-    if self.isAlive():
-      events = set(events)
-      events = events.union(set(REQ_EVENTS.keys()))
-      unavailableEvents = set()
-      
-      # removes anything we've already failed to set
-      if DROP_FAILED_EVENTS:
-        unavailableEvents.update(events.intersection(FAILED_EVENTS))
-        events.difference_update(FAILED_EVENTS)
-      
-      # initial check for event availability, using the 'events/names' GETINFO
-      # option to detect invalid events
-      validEvents = self.getInfo("events/names")
-      
-      if validEvents:
-        validEvents = set(validEvents.split())
-        unavailableEvents.update(events.difference(validEvents))
-        events.intersection_update(validEvents)
-      
-      # attempt to set events via trial and error
-      isEventsSet, isAbandoned = False, False
-      
-      while not isEventsSet and not isAbandoned:
-        try:
-          self.conn.set_events(list(events))
-          isEventsSet = True
-        except TorCtl.ErrorReply, exc:
-          msg = str(exc)
-          
-          if "Unrecognized event" in msg:
-            # figure out type of event we failed to listen for
-            start = msg.find("event \"") + 7
-            end = msg.rfind("\"")
-            failedType = msg[start:end]
-            
-            unavailableEvents.add(failedType)
-            events.discard(failedType)
-          else:
-            # unexpected error, abandon attempt
-            isAbandoned = True
-        except TorCtl.TorCtlClosed:
-          self.close()
-          isAbandoned = True
-      
-      FAILED_EVENTS.update(unavailableEvents)
-      if not isAbandoned:
-        # logs warnings or errors for failed events
-        for eventType in unavailableEvents:
-          defaultMsg = DEFAULT_FAILED_EVENT_MSG % eventType
-          if eventType in REQ_EVENTS:
-            log.log(log.ERR, defaultMsg + " (%s)" % REQ_EVENTS[eventType])
-          else:
-            log.log(log.WARN, defaultMsg)
-        
-        self.controllerEvents = list(events)
-        returnVal = list(events)
-    else:
-      # attempts to set the events when next attached to a control port
-      self.controllerEvents = list(events)
-      returnVal = list(events)
-    
-    self.connLock.release()
-    return returnVal
-  
-  def reload(self, issueSighup = False):
-    """
-    This resets tor (sending a RELOAD signal to the control port) causing tor's
-    internal state to be reset and the torrc reloaded. This can either be done
-    by...
-      - the controller via a RELOAD signal (default and suggested)
-          conn.send_signal("RELOAD")
-      - system reload signal (hup)
-          pkill -sighup tor
-    
-    The later isn't really useful unless there's some reason the RELOAD signal
-    won't do the trick. Both methods raise an IOError in case of failure.
-    
-    Arguments:
-      issueSighup - issues a sighup rather than a controller RELOAD signal
-    """
-    
-    self.connLock.acquire()
-    
-    raisedException = None
-    if self.isAlive():
-      if not issueSighup:
-        try:
-          self.conn.send_signal("RELOAD")
-        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... :(
-          # http://bugs.noreply.org/flyspray/index.php?do=details&id=1329
-          raisedException = IOError(str(exc))
-      else:
-        try:
-          # Redirects stderr to stdout so we can check error status (output
-          # should be empty if successful). Example error:
-          # pkill: 5592 - Operation not permitted
-          #
-          # note that this may provide multiple errors, even if successful,
-          # hence this:
-          #   - only provide an error if Tor fails to log a sighup
-          #   - provide the error message associated with the tor pid (others
-          #     would be a red herring)
-          if not sysTools.isAvailable("pkill"):
-            raise IOError("pkill command is unavailable")
-          
-          self._isReset = False
-          pkillCall = os.popen("pkill -sighup ^tor$ 2> /dev/stdout")
-          pkillOutput = pkillCall.readlines()
-          pkillCall.close()
-          
-          # Give the sighupTracker a moment to detect the sighup signal. This
-          # is, of course, a possible concurrency bug. However I'm not sure
-          # of a better method for blocking on this...
-          waitStart = time.time()
-          while time.time() - waitStart < 1:
-            time.sleep(0.1)
-            if self._isReset: break
-          
-          if not self._isReset:
-            errorLine, torPid = "", self.getMyPid()
-            if torPid:
-              for line in pkillOutput:
-                if line.startswith("pkill: %s - " % torPid):
-                  errorLine = line
-                  break
-            
-            if errorLine: raise IOError(" ".join(errorLine.split()[3:]))
-            else: raise IOError("failed silently")
-        except IOError, exc:
-          raisedException = exc
-    
-    self.connLock.release()
-    
-    if raisedException: raise raisedException
-  
-  def msg_event(self, event):
-    """
-    Listens for reload signal (hup), which is either produced by:
-    causing the torrc and internal state to be reset.
-    """
-    
-    if event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)"):
-      self._isReset = True
-      
-      self._status = TOR_INIT
-      self._statusTime = time.time()
-      
-      thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,))
-  
-  def ns_event(self, event):
-    self._updateHeartbeat()
-    
-    myFingerprint = self.getMyFingerprint()
-    if myFingerprint:
-      for ns in event.nslist:
-        if ns.idhex == myFingerprint:
-          self._cachedParam["nsEntry"] = None
-          self._cachedParam["flags"] = None
-          self._cachedParam["bwMeasured"] = None
-          return
-    else:
-      self._cachedParam["nsEntry"] = None
-      self._cachedParam["flags"] = None
-      self._cachedParam["bwMeasured"] = None
-  
-  def new_consensus_event(self, event):
-    self._updateHeartbeat()
-    
-    self._cachedParam["nsEntry"] = None
-    self._cachedParam["flags"] = None
-    self._cachedParam["bwMeasured"] = None
-  
-  def new_desc_event(self, event):
-    self._updateHeartbeat()
-    
-    myFingerprint = self.getMyFingerprint()
-    if not myFingerprint or myFingerprint in event.idlist:
-      self._cachedParam["descEntry"] = None
-      self._cachedParam["bwObserved"] = None
-  
-  def circ_status_event(self, event):
-    self._updateHeartbeat()
-  
-  def buildtimeout_set_event(self, event):
-    self._updateHeartbeat()
-  
-  def stream_status_event(self, event):
-    self._updateHeartbeat()
-  
-  def or_conn_status_event(self, event):
-    self._updateHeartbeat()
-  
-  def stream_bw_event(self, event):
-    self._updateHeartbeat()
-  
-  def bandwidth_event(self, event):
-    self._updateHeartbeat()
-  
-  def address_mapped_event(self, event):
-    self._updateHeartbeat()
-  
-  def unknown_event(self, event):
-    self._updateHeartbeat()
-  
-  def write(self, msg):
-    """
-    Tracks TorCtl events. Ugly hack since TorCtl/TorUtil.py expects a file.
-    """
-    
-    timestampStart, timestampEnd = msg.find("["), msg.find("]")
-    level = msg[:timestampStart]
-    msg = msg[timestampEnd + 2:].strip()
-    
-    # notifies listeners of TorCtl events
-    for callback in self.torctlListeners: callback(level, msg)
-    
-    # checks if TorCtl is providing a notice that control port is closed
-    if TOR_CTL_CLOSE_MSG in msg: self.close()
-  
-  def flush(self): pass
-  
-  def _updateHeartbeat(self):
-    """
-    Called on any event occurance to note the time it occured.
-    """
-    
-    self.lastHeartbeat = time.time()
-  
-  def _getRelayAttr(self, key, default, cacheUndefined = True):
-    """
-    Provides information associated with this relay, using the cached value if
-    available and otherwise looking it up.
-    
-    Arguments:
-      key            - parameter being queried (from CACHE_ARGS)
-      default        - value to be returned if undefined
-      cacheUndefined - caches when values are undefined, avoiding further
-                       lookups if true
-    """
-    
-    currentVal = self._cachedParam[key]
-    if currentVal:
-      if currentVal == UNKNOWN: return default
-      else: return currentVal
-    
-    self.connLock.acquire()
-    
-    currentVal, result = self._cachedParam[key], None
-    if not currentVal and self.isAlive():
-      # still unset - fetch value
-      if key in ("nsEntry", "descEntry"):
-        myFingerprint = self.getMyFingerprint()
-        
-        if myFingerprint:
-          queryType = "ns" if key == "nsEntry" else "desc"
-          queryResult = self.getInfo("%s/id/%s" % (queryType, myFingerprint))
-          if queryResult: result = queryResult.split("\n")
-      elif key == "bwRate":
-        # effective relayed bandwidth is the minimum of BandwidthRate,
-        # MaxAdvertisedBandwidth, and RelayBandwidthRate (if set)
-        effectiveRate = int(self.getOption("BandwidthRate"))
-        
-        relayRate = self.getOption("RelayBandwidthRate")
-        if relayRate and relayRate != "0":
-          effectiveRate = min(effectiveRate, int(relayRate))
-        
-        maxAdvertised = self.getOption("MaxAdvertisedBandwidth")
-        if maxAdvertised: effectiveRate = min(effectiveRate, int(maxAdvertised))
-        
-        result = effectiveRate
-      elif key == "bwBurst":
-        # effective burst (same for BandwidthBurst and RelayBandwidthBurst)
-        effectiveBurst = int(self.getOption("BandwidthBurst"))
-        
-        relayBurst = self.getOption("RelayBandwidthBurst")
-        if relayBurst and relayBurst != "0":
-          effectiveBurst = min(effectiveBurst, int(relayBurst))
-        
-        result = effectiveBurst
-      elif key == "bwObserved":
-        for line in self.getMyDescriptor([]):
-          if line.startswith("bandwidth"):
-            # line should look something like:
-            # bandwidth 40960 102400 47284
-            comp = line.split()
-            
-            if len(comp) == 4 and comp[-1].isdigit():
-              result = int(comp[-1])
-              break
-      elif key == "bwMeasured":
-        # TODO: Currently there's no client side indication of what type of
-        # measurement was used. Include this in results if it's ever available.
-        
-        for line in self.getMyNetworkStatus([]):
-          if line.startswith("w Bandwidth="):
-            bwValue = line[12:]
-            if bwValue.isdigit(): result = int(bwValue)
-            break
-      elif key == "fingerprint":
-        # Fingerprints are kept until sighup if set (most likely not even a
-        # setconf can change it since it's in the data directory). If orport is
-        # unset then no fingerprint will be set.
-        orPort = self.getOption("ORPort", "0")
-        if orPort == "0": result = UNKNOWN
-        else: result = self.getInfo("fingerprint")
-      elif key == "flags":
-        for line in self.getMyNetworkStatus([]):
-          if line.startswith("s "):
-            result = line[2:].split()
-            break
-      elif key == "pid":
-        result = getPid(int(self.getOption("ControlPort", 9051)), self.getOption("PidFile"))
-      
-      # cache value
-      if result: self._cachedParam[key] = result
-      elif cacheUndefined: self._cachedParam[key] = UNKNOWN
-    elif currentVal == UNKNOWN: result = currentVal
-    
-    self.connLock.release()
-    
-    if result: return result
-    else: return default
-  
-  def _notifyStatusListeners(self, eventType):
-    """
-    Sends a notice to all current listeners that a given change in tor's
-    controller status has occurred.
-    
-    Arguments:
-      eventType - enum representing tor's new status
-    """
-    
-    # resets cached getInfo parameters
-    self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
-    
-    for callback in self.statusListeners:
-      callback(self, eventType)
-

Copied: arm/trunk/src/util/torTools.py (from rev 22948, arm/trunk/util/torTools.py)
===================================================================
--- arm/trunk/src/util/torTools.py	                        (rev 0)
+++ arm/trunk/src/util/torTools.py	2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,1009 @@
+"""
+Helper for working with an active tor process. This both provides a wrapper for
+accessing TorCtl and notifications of state changes to subscribers. To quickly
+fetch a TorCtl instance to experiment with use the following:
+
+>>> import util.torTools
+>>> conn = util.torTools.connect()
+>>> conn.get_info("version")["version"]
+'0.2.1.24'
+"""
+
+import os
+import time
+import socket
+import getpass
+import thread
+import threading
+
+from TorCtl import TorCtl, TorUtil
+
+import log
+import sysTools
+
+# enums for tor's controller state:
+# TOR_INIT - attached to a new controller or restart/sighup signal received
+# TOR_CLOSED - control port closed
+TOR_INIT, TOR_CLOSED = range(1, 3)
+
+# message logged by default when a controller can't set an event type
+DEFAULT_FAILED_EVENT_MSG = "Unsupported event type: %s"
+
+# TODO: check version when reattaching to controller and if version changes, flush?
+# Skips attempting to set events we've failed to set before. This avoids
+# logging duplicate warnings but can be problematic if controllers belonging
+# to multiple versions of tor are attached, making this unreflective of the
+# controller's capabilites. However, this is a pretty bizarre edge case.
+DROP_FAILED_EVENTS = True
+FAILED_EVENTS = set()
+
+CONTROLLER = None # singleton Controller instance
+INCORRECT_PASSWORD_MSG = "Provided passphrase was incorrect"
+
+# valid keys for the controller's getInfo cache
+CACHE_ARGS = ("nsEntry", "descEntry", "bwRate", "bwBurst", "bwObserved",
+              "bwMeasured", "flags", "fingerprint", "pid")
+
+TOR_CTL_CLOSE_MSG = "Tor closed control connection. Exiting event thread."
+UNKNOWN = "UNKNOWN" # value used by cached information if undefined
+CONFIG = {"log.torCtlPortClosed": log.NOTICE, "log.torGetInfo": log.DEBUG, "log.torGetConf": log.DEBUG}
+
+# events used for controller functionality:
+# NOTICE - used to detect when tor is shut down
+# NEWDESC, NS, and NEWCONSENSUS - used for cache invalidation
+REQ_EVENTS = {"NOTICE": "this will be unable to detect when tor is shut down",
+              "NEWDESC": "information related to descriptors will grow stale",
+              "NS": "information related to the consensus will grow stale",
+              "NEWCONSENSUS": "information related to the consensus will grow stale"}
+
+def loadConfig(config):
+  config.update(CONFIG)
+
+def makeCtlConn(controlAddr="127.0.0.1", controlPort=9051):
+  """
+  Opens a socket to the tor controller and queries its authentication type,
+  raising an IOError if problems occur. The result of this function is a tuple
+  of the TorCtl connection and the authentication type, where the later is one
+  of the following:
+  "NONE"          - no authentication required
+  "PASSWORD"      - requires authentication via a hashed password
+  "COOKIE=<FILE>" - requires the specified authentication cookie
+  
+  Arguments:
+    controlAddr - ip address belonging to the controller
+    controlPort - port belonging to the controller
+  """
+  
+  try:
+    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    s.connect((controlAddr, controlPort))
+    conn = TorCtl.Connection(s)
+  except socket.error, exc:
+    if "Connection refused" in exc.args:
+      # most common case - tor control port isn't available
+      raise IOError("Connection refused. Is the ControlPort enabled?")
+    else: raise IOError("Failed to establish socket: %s" % exc)
+  
+  # check PROTOCOLINFO for authentication type
+  try:
+    authInfo = conn.sendAndRecv("PROTOCOLINFO\r\n")[1][1]
+  except TorCtl.ErrorReply, exc:
+    raise IOError("Unable to query PROTOCOLINFO for authentication type: %s" % exc)
+  
+  if authInfo.startswith("AUTH METHODS=NULL"):
+    # no authentication required
+    return (conn, "NONE")
+  elif authInfo.startswith("AUTH METHODS=HASHEDPASSWORD"):
+    # password authentication
+    return (conn, "PASSWORD")
+  elif authInfo.startswith("AUTH METHODS=COOKIE"):
+    # cookie authentication, parses authentication cookie path
+    start = authInfo.find("COOKIEFILE=\"") + 12
+    end = authInfo.find("\"", start)
+    return (conn, "COOKIE=%s" % authInfo[start:end])
+
+def initCtlConn(conn, authType="NONE", authVal=None):
+  """
+  Authenticates to a tor connection. The authentication type can be any of the
+  following strings:
+  NONE, PASSWORD, COOKIE
+  
+  if the authentication type is anything other than NONE then either a
+  passphrase or path to an authentication cookie is expected. If an issue
+  arises this raises either of the following:
+    - IOError for failures in reading an authentication cookie
+    - TorCtl.ErrorReply for authentication failures
+  
+  Argument:
+    conn     - unauthenticated TorCtl connection
+    authType - type of authentication method to use
+    authVal  - passphrase or path to authentication cookie
+  """
+  
+  # validates input
+  if authType not in ("NONE", "PASSWORD", "COOKIE"):
+    # authentication type unrecognized (possibly a new addition to the controlSpec?)
+    raise TorCtl.ErrorReply("Unrecognized authentication type: %s" % authType)
+  elif authType != "NONE" and authVal == None:
+    typeLabel = "passphrase" if authType == "PASSWORD" else "cookie"
+    raise TorCtl.ErrorReply("Unable to authenticate: no %s provided" % typeLabel)
+  
+  authCookie = None
+  try:
+    if authType == "NONE": conn.authenticate("")
+    elif authType == "PASSWORD": conn.authenticate(authVal)
+    else:
+      authCookie = open(authVal, "r")
+      conn.authenticate_cookie(authCookie)
+      authCookie.close()
+  except TorCtl.ErrorReply, exc:
+    if authCookie: authCookie.close()
+    issue = str(exc)
+    
+    # simplifies message if the wrong credentials were provided (common mistake)
+    if issue.startswith("515 Authentication failed: "):
+      if issue[27:].startswith("Password did not match"):
+        issue = "password incorrect"
+      elif issue[27:] == "Wrong length on authentication cookie.":
+        issue = "cookie value incorrect"
+    
+    raise TorCtl.ErrorReply("Unable to authenticate: %s" % issue)
+  except IOError, exc:
+    if authCookie: authCookie.close()
+    issue = None
+    
+    # cleaner message for common errors
+    if str(exc).startswith("[Errno 13] Permission denied"): issue = "permission denied"
+    elif str(exc).startswith("[Errno 2] No such file or directory"): issue = "file doesn't exist"
+    
+    # if problem's recognized give concise message, otherwise print exception string
+    if issue: raise IOError("Failed to read authentication cookie (%s): %s" % (issue, authVal))
+    else: raise IOError("Failed to read authentication cookie: %s" % exc)
+
+def connect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None):
+  """
+  Convenience method for quickly getting a TorCtl connection. This is very
+  handy for debugging or CLI setup, handling setup and prompting for a password
+  if necessary (if either none is provided as input or it fails). If any issues
+  arise this prints a description of the problem and returns None.
+  
+  Arguments:
+    controlAddr - ip address belonging to the controller
+    controlPort - port belonging to the controller
+    passphrase  - authentication passphrase (if defined this is used rather
+                  than prompting the user)
+  """
+  
+  try:
+    conn, authType = makeCtlConn(controlAddr, controlPort)
+    authValue = None
+    
+    if authType == "PASSWORD":
+      # password authentication, promting for the password if it wasn't provided
+      if passphrase: authValue = passphrase
+      else:
+        try: authValue = getpass.getpass()
+        except KeyboardInterrupt: return None
+    elif authType.startswith("COOKIE"):
+      authType, authValue = authType.split("=", 1)
+    
+    initCtlConn(conn, authType, authValue)
+    return conn
+  except Exception, exc:
+    if passphrase and str(exc) == "Unable to authenticate: password incorrect":
+      # provide a warning that the provided password didn't work, then try
+      # again prompting for the user to enter it
+      print INCORRECT_PASSWORD_MSG
+      return connect(controlAddr, controlPort)
+    else:
+      print exc
+      return None
+
+def getPid(controlPort=9051, pidFilePath=None):
+  """
+  Attempts to determine the process id for a running tor process, using the
+  following:
+  1. GETCONF PidFile
+  2. "pidof tor"
+  3. "netstat -npl | grep 127.0.0.1:%s" % <tor control port>
+  4. "ps -o pid -C tor"
+  
+  If pidof or ps provide multiple tor instances then their results are
+  discarded (since only netstat can differentiate using the control port). This
+  provides None if either no running process exists or it can't be determined.
+  
+  Arguments:
+    controlPort - control port of the tor process if multiple exist
+    pidFilePath - path to the pid file generated by tor
+  """
+  
+  # attempts to fetch via the PidFile, failing if:
+  # - the option is unset
+  # - unable to read the file (such as insufficient permissions)
+  
+  if pidFilePath:
+    try:
+      pidFile = open(pidFilePath, "r")
+      pidEntry = pidFile.readline().strip()
+      pidFile.close()
+      
+      if pidEntry.isdigit(): return pidEntry
+    except Exception: pass
+  
+  # attempts to resolve using pidof, failing if:
+  # - tor's running under a different name
+  # - there's multiple instances of tor
+  try:
+    results = sysTools.call("pidof tor")
+    if len(results) == 1 and len(results[0].split()) == 1:
+      pid = results[0].strip()
+      if pid.isdigit(): return pid
+  except IOError: pass
+  
+  # attempts to resolve using netstat, failing if:
+  # - tor's being run as a different user due to permissions
+  try:
+    results = sysTools.call("netstat -npl | grep 127.0.0.1:%i" % controlPort)
+    
+    if len(results) == 1:
+      results = results[0].split()[6] # process field (ex. "7184/tor")
+      pid = results[:results.find("/")]
+      if pid.isdigit(): return pid
+  except IOError: pass
+  
+  # attempts to resolve using ps, failing if:
+  # - tor's running under a different name
+  # - there's multiple instances of tor
+  try:
+    results = sysTools.call("ps -o pid -C tor")
+    if len(results) == 2:
+      pid = results[1].strip()
+      if pid.isdigit(): return pid
+  except IOError: pass
+  
+  return None
+
+def getConn():
+  """
+  Singleton constructor for a Controller. Be aware that this start
+  uninitialized, needing a TorCtl instance before it's fully functional.
+  """
+  
+  global CONTROLLER
+  if CONTROLLER == None: CONTROLLER = Controller()
+  return CONTROLLER
+
+class Controller(TorCtl.PostEventListener):
+  """
+  TorCtl wrapper providing convenience functions, listener functionality for
+  tor's state, and the capability for controller connections to be restarted
+  if closed.
+  """
+  
+  def __init__(self):
+    TorCtl.PostEventListener.__init__(self)
+    self.conn = None                    # None if uninitialized or controller's been closed
+    self.connLock = threading.RLock()
+    self.eventListeners = []            # instances listening for tor controller events
+    self.torctlListeners = []           # callback functions for TorCtl events
+    self.statusListeners = []           # callback functions for tor's state changes
+    self.controllerEvents = []          # list of successfully set controller events
+    self._isReset = False               # internal flag for tracking resets
+    self._status = TOR_CLOSED           # current status of the attached control port
+    self._statusTime = 0                # unix time-stamp for the duration of the status
+    self.lastHeartbeat = 0              # time of the last tor event
+    
+    # cached getInfo parameters (None if unset or possibly changed)
+    self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
+    
+    # directs TorCtl to notify us of events
+    TorUtil.loglevel = "DEBUG"
+    TorUtil.logfile = self
+  
+  def init(self, conn=None):
+    """
+    Uses the given TorCtl instance for future operations, notifying listeners
+    about the change.
+    
+    Arguments:
+      conn - TorCtl instance to be used, if None then a new instance is fetched
+             via the connect function
+    """
+    
+    if conn == None:
+      conn = connect()
+      
+      if conn == None: raise ValueError("Unable to initialize TorCtl instance.")
+    
+    if conn.is_live() and conn != self.conn:
+      self.connLock.acquire()
+      
+      if self.conn: self.close() # shut down current connection
+      self.conn = conn
+      self.conn.add_event_listener(self)
+      for listener in self.eventListeners: self.conn.add_event_listener(listener)
+      
+      # sets the events listened for by the new controller (incompatible events
+      # are dropped with a logged warning)
+      self.setControllerEvents(self.controllerEvents)
+      
+      self.connLock.release()
+      
+      self._status = TOR_INIT
+      self._statusTime = time.time()
+      
+      # notifies listeners that a new controller is available
+      thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,))
+  
+  def close(self):
+    """
+    Closes the current TorCtl instance and notifies listeners.
+    """
+    
+    self.connLock.acquire()
+    if self.conn:
+      self.conn.close()
+      self.conn = None
+      self.connLock.release()
+      
+      self._status = TOR_CLOSED
+      self._statusTime = time.time()
+      
+      # notifies listeners that the controller's been shut down
+      thread.start_new_thread(self._notifyStatusListeners, (TOR_CLOSED,))
+    else: self.connLock.release()
+  
+  def isAlive(self):
+    """
+    Returns True if this has been initialized with a working TorCtl instance,
+    False otherwise.
+    """
+    
+    self.connLock.acquire()
+    
+    result = False
+    if self.conn:
+      if self.conn.is_live(): result = True
+      else: self.close()
+    
+    self.connLock.release()
+    return result
+  
+  def getHeartbeat(self):
+    """
+    Provides the time of the last registered tor event (if listening for BW
+    events then this should occure every second if relay's still responsive).
+    This returns zero if this has never received an event.
+    """
+    
+    return self.lastHeartbeat
+  
+  def getTorCtl(self):
+    """
+    Provides the current TorCtl connection. If unset or closed then this
+    returns None.
+    """
+    
+    self.connLock.acquire()
+    result = None
+    if self.isAlive(): result = self.conn
+    self.connLock.release()
+    
+    return result
+  
+  def getInfo(self, param, default = None, suppressExc = True):
+    """
+    Queries the control port for the given GETINFO option, providing the
+    default if the response is undefined or fails for any reason (error
+    response, control port closed, initiated, etc).
+    
+    Arguments:
+      param       - GETINFO 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
+    """
+    
+    self.connLock.acquire()
+    
+    startTime = time.time()
+    result, raisedExc = default, None
+    if self.isAlive():
+      try:
+        getInfoVal = self.conn.get_info(param)[param]
+        if getInfoVal != None: result = getInfoVal
+      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
+        if type(exc) == TorCtl.TorCtlClosed: self.close()
+        raisedExc = exc
+    
+    msg = "tor control call: GETINFO %s (runtime: %0.4f)" % (param, time.time() - startTime)
+    log.log(CONFIG["log.torGetInfo"], msg)
+    
+    self.connLock.release()
+    
+    if not suppressExc and raisedExc: raise raisedExc
+    else: return result
+  
+  def getOption(self, param, default = None, multiple = False, suppressExc = True):
+    """
+    Queries the control port for the given configuration option, providing the
+    default if the response is undefined or fails for any reason. If multiple
+    values exist then this arbitrarily returns the first unless the multiple
+    flag is set.
+    
+    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
+      suppressExc - suppresses lookup errors (returning the default) if true,
+                    otherwise this raises the original exception
+    """
+    
+    self.connLock.acquire()
+    
+    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
+    
+    msg = "tor control call: GETCONF %s (runtime: %0.4f)" % (param, time.time() - startTime)
+    log.log(CONFIG["log.torGetConf"], msg)
+    
+    self.connLock.release()
+    
+    if not suppressExc and raisedExc: raise raisedExc
+    elif result == []: return default
+    else: return result
+  
+  def getMyNetworkStatus(self, default = None):
+    """
+    Provides the network status entry for this relay if available. This is
+    occasionally expanded so results may vary depending on tor's version. For
+    0.2.2.13 they contained entries like the following:
+    
+    r caerSidi p1aag7VwarGxqctS7/fS0y5FU+s 9On1TRGCEpljszPpJR1hKqlzaY8 2010-05-26 09:26:06 76.104.132.98 9001 0
+    s Fast HSDir Named Running Stable Valid
+    w Bandwidth=25300
+    p reject 1-65535
+    
+    Arguments:
+      default - result if the query fails
+    """
+    
+    return self._getRelayAttr("nsEntry", default)
+  
+  def getMyDescriptor(self, default = None):
+    """
+    Provides the descriptor entry for this relay if available.
+    
+    Arguments:
+      default - result if the query fails
+    """
+    
+    return self._getRelayAttr("descEntry", default)
+  
+  def getMyBandwidthRate(self, default = None):
+    """
+    Provides the effective relaying bandwidth rate of this relay. Currently
+    this doesn't account for SETCONF events.
+    
+    Arguments:
+      default - result if the query fails
+    """
+    
+    return self._getRelayAttr("bwRate", default)
+  
+  def getMyBandwidthBurst(self, default = None):
+    """
+    Provides the effective bandwidth burst rate of this relay. Currently this
+    doesn't account for SETCONF events.
+    
+    Arguments:
+      default - result if the query fails
+    """
+    
+    return self._getRelayAttr("bwBurst", default)
+  
+  def getMyBandwidthObserved(self, default = None):
+    """
+    Provides the relay's current observed bandwidth (the throughput determined
+    from historical measurements on the client side). This is used in the
+    heuristic used for path selection if the measured bandwidth is undefined.
+    This is fetched from the descriptors and hence will get stale if
+    descriptors aren't periodically updated.
+    
+    Arguments:
+      default - result if the query fails
+    """
+    
+    return self._getRelayAttr("bwObserved", default)
+  
+  def getMyBandwidthMeasured(self, default = None):
+    """
+    Provides the relay's current measured bandwidth (the throughput as noted by
+    the directory authorities and used by clients for relay selection). This is
+    undefined if not in the consensus or with older versions of Tor. Depending
+    on the circumstances this can be from a variety of things (observed,
+    measured, weighted measured, etc) as described by:
+    https://trac.torproject.org/projects/tor/ticket/1566
+    
+    Arguments:
+      default - result if the query fails
+    """
+    
+    return self._getRelayAttr("bwMeasured", default)
+  
+  def getMyFingerprint(self, default = None):
+    """
+    Provides the fingerprint for this relay.
+    
+    Arguments:
+      default - result if the query fails
+    """
+    
+    return self._getRelayAttr("fingerprint", default, False)
+  
+  def getMyFlags(self, default = None):
+    """
+    Provides the flags held by this relay.
+    
+    Arguments:
+      default - result if the query fails or this relay isn't a part of the consensus yet
+    """
+    
+    return self._getRelayAttr("flags", default)
+  
+  def getMyPid(self):
+    """
+    Provides the pid of the attached tor process (None if no controller exists
+    or this can't be determined).
+    """
+    
+    return self._getRelayAttr("pid", None)
+  
+  def getStatus(self):
+    """
+    Provides a tuple consisting of the control port's current status and unix
+    time-stamp for when it became this way (zero if no status has yet to be
+    set).
+    """
+    
+    return (self._status, self._statusTime)
+  
+  def addEventListener(self, listener):
+    """
+    Directs further tor controller events to callback functions of the
+    listener. If a new control connection is initialized then this listener is
+    reattached.
+    
+    Arguments:
+      listener - TorCtl.PostEventListener instance listening for events
+    """
+    
+    self.connLock.acquire()
+    self.eventListeners.append(listener)
+    if self.isAlive(): self.conn.add_event_listener(listener)
+    self.connLock.release()
+  
+  def addTorCtlListener(self, callback):
+    """
+    Directs further TorCtl events to the callback function. Events are composed
+    of a runlevel and message tuple.
+    
+    Arguments:
+      callback - functor that'll accept the events, expected to be of the form:
+                 myFunction(runlevel, msg)
+    """
+    
+    self.torctlListeners.append(callback)
+  
+  def addStatusListener(self, callback):
+    """
+    Directs further events related to tor's controller status to the callback
+    function.
+    
+    Arguments:
+      callback - functor that'll accept the events, expected to be of the form:
+                 myFunction(controller, eventType)
+    """
+    
+    self.statusListeners.append(callback)
+  
+  def removeStatusListener(self, callback):
+    """
+    Stops listener from being notified of further events. This returns true if a
+    listener's removed, false otherwise.
+    
+    Arguments:
+      callback - functor to be removed
+    """
+    
+    if callback in self.statusListeners:
+      self.statusListeners.remove(callback)
+      return True
+    else: return False
+  
+  def getControllerEvents(self):
+    """
+    Provides the events the controller's currently configured to listen for.
+    """
+    
+    return list(self.controllerEvents)
+  
+  def setControllerEvents(self, events):
+    """
+    Sets the events being requested from any attached tor instance, logging
+    warnings for event types that aren't supported (possibly due to version
+    issues). Events in REQ_EVENTS will also be included, logging at the error
+    level with an additional description in case of failure.
+    
+    This remembers the successfully set events and tries to request them from
+    any tor instance it attaches to in the future too (again logging and
+    dropping unsuccessful event types).
+    
+    This returns the listing of event types that were successfully set. If not
+    currently attached to a tor instance then all events are assumed to be ok,
+    then attempted when next attached to a control port.
+    
+    Arguments:
+      events - listing of events to be set
+    """
+    
+    self.connLock.acquire()
+    
+    returnVal = []
+    if self.isAlive():
+      events = set(events)
+      events = events.union(set(REQ_EVENTS.keys()))
+      unavailableEvents = set()
+      
+      # removes anything we've already failed to set
+      if DROP_FAILED_EVENTS:
+        unavailableEvents.update(events.intersection(FAILED_EVENTS))
+        events.difference_update(FAILED_EVENTS)
+      
+      # initial check for event availability, using the 'events/names' GETINFO
+      # option to detect invalid events
+      validEvents = self.getInfo("events/names")
+      
+      if validEvents:
+        validEvents = set(validEvents.split())
+        unavailableEvents.update(events.difference(validEvents))
+        events.intersection_update(validEvents)
+      
+      # attempt to set events via trial and error
+      isEventsSet, isAbandoned = False, False
+      
+      while not isEventsSet and not isAbandoned:
+        try:
+          self.conn.set_events(list(events))
+          isEventsSet = True
+        except TorCtl.ErrorReply, exc:
+          msg = str(exc)
+          
+          if "Unrecognized event" in msg:
+            # figure out type of event we failed to listen for
+            start = msg.find("event \"") + 7
+            end = msg.rfind("\"")
+            failedType = msg[start:end]
+            
+            unavailableEvents.add(failedType)
+            events.discard(failedType)
+          else:
+            # unexpected error, abandon attempt
+            isAbandoned = True
+        except TorCtl.TorCtlClosed:
+          self.close()
+          isAbandoned = True
+      
+      FAILED_EVENTS.update(unavailableEvents)
+      if not isAbandoned:
+        # logs warnings or errors for failed events
+        for eventType in unavailableEvents:
+          defaultMsg = DEFAULT_FAILED_EVENT_MSG % eventType
+          if eventType in REQ_EVENTS:
+            log.log(log.ERR, defaultMsg + " (%s)" % REQ_EVENTS[eventType])
+          else:
+            log.log(log.WARN, defaultMsg)
+        
+        self.controllerEvents = list(events)
+        returnVal = list(events)
+    else:
+      # attempts to set the events when next attached to a control port
+      self.controllerEvents = list(events)
+      returnVal = list(events)
+    
+    self.connLock.release()
+    return returnVal
+  
+  def reload(self, issueSighup = False):
+    """
+    This resets tor (sending a RELOAD signal to the control port) causing tor's
+    internal state to be reset and the torrc reloaded. This can either be done
+    by...
+      - the controller via a RELOAD signal (default and suggested)
+          conn.send_signal("RELOAD")
+      - system reload signal (hup)
+          pkill -sighup tor
+    
+    The later isn't really useful unless there's some reason the RELOAD signal
+    won't do the trick. Both methods raise an IOError in case of failure.
+    
+    Arguments:
+      issueSighup - issues a sighup rather than a controller RELOAD signal
+    """
+    
+    self.connLock.acquire()
+    
+    raisedException = None
+    if self.isAlive():
+      if not issueSighup:
+        try:
+          self.conn.send_signal("RELOAD")
+        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... :(
+          # http://bugs.noreply.org/flyspray/index.php?do=details&id=1329
+          raisedException = IOError(str(exc))
+      else:
+        try:
+          # Redirects stderr to stdout so we can check error status (output
+          # should be empty if successful). Example error:
+          # pkill: 5592 - Operation not permitted
+          #
+          # note that this may provide multiple errors, even if successful,
+          # hence this:
+          #   - only provide an error if Tor fails to log a sighup
+          #   - provide the error message associated with the tor pid (others
+          #     would be a red herring)
+          if not sysTools.isAvailable("pkill"):
+            raise IOError("pkill command is unavailable")
+          
+          self._isReset = False
+          pkillCall = os.popen("pkill -sighup ^tor$ 2> /dev/stdout")
+          pkillOutput = pkillCall.readlines()
+          pkillCall.close()
+          
+          # Give the sighupTracker a moment to detect the sighup signal. This
+          # is, of course, a possible concurrency bug. However I'm not sure
+          # of a better method for blocking on this...
+          waitStart = time.time()
+          while time.time() - waitStart < 1:
+            time.sleep(0.1)
+            if self._isReset: break
+          
+          if not self._isReset:
+            errorLine, torPid = "", self.getMyPid()
+            if torPid:
+              for line in pkillOutput:
+                if line.startswith("pkill: %s - " % torPid):
+                  errorLine = line
+                  break
+            
+            if errorLine: raise IOError(" ".join(errorLine.split()[3:]))
+            else: raise IOError("failed silently")
+        except IOError, exc:
+          raisedException = exc
+    
+    self.connLock.release()
+    
+    if raisedException: raise raisedException
+  
+  def msg_event(self, event):
+    """
+    Listens for reload signal (hup), which is either produced by:
+    causing the torrc and internal state to be reset.
+    """
+    
+    if event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)"):
+      self._isReset = True
+      
+      self._status = TOR_INIT
+      self._statusTime = time.time()
+      
+      thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,))
+  
+  def ns_event(self, event):
+    self._updateHeartbeat()
+    
+    myFingerprint = self.getMyFingerprint()
+    if myFingerprint:
+      for ns in event.nslist:
+        if ns.idhex == myFingerprint:
+          self._cachedParam["nsEntry"] = None
+          self._cachedParam["flags"] = None
+          self._cachedParam["bwMeasured"] = None
+          return
+    else:
+      self._cachedParam["nsEntry"] = None
+      self._cachedParam["flags"] = None
+      self._cachedParam["bwMeasured"] = None
+  
+  def new_consensus_event(self, event):
+    self._updateHeartbeat()
+    
+    self._cachedParam["nsEntry"] = None
+    self._cachedParam["flags"] = None
+    self._cachedParam["bwMeasured"] = None
+  
+  def new_desc_event(self, event):
+    self._updateHeartbeat()
+    
+    myFingerprint = self.getMyFingerprint()
+    if not myFingerprint or myFingerprint in event.idlist:
+      self._cachedParam["descEntry"] = None
+      self._cachedParam["bwObserved"] = None
+  
+  def circ_status_event(self, event):
+    self._updateHeartbeat()
+  
+  def buildtimeout_set_event(self, event):
+    self._updateHeartbeat()
+  
+  def stream_status_event(self, event):
+    self._updateHeartbeat()
+  
+  def or_conn_status_event(self, event):
+    self._updateHeartbeat()
+  
+  def stream_bw_event(self, event):
+    self._updateHeartbeat()
+  
+  def bandwidth_event(self, event):
+    self._updateHeartbeat()
+  
+  def address_mapped_event(self, event):
+    self._updateHeartbeat()
+  
+  def unknown_event(self, event):
+    self._updateHeartbeat()
+  
+  def write(self, msg):
+    """
+    Tracks TorCtl events. Ugly hack since TorCtl/TorUtil.py expects a file.
+    """
+    
+    timestampStart, timestampEnd = msg.find("["), msg.find("]")
+    level = msg[:timestampStart]
+    msg = msg[timestampEnd + 2:].strip()
+    
+    # notifies listeners of TorCtl events
+    for callback in self.torctlListeners: callback(level, msg)
+    
+    # checks if TorCtl is providing a notice that control port is closed
+    if TOR_CTL_CLOSE_MSG in msg: self.close()
+  
+  def flush(self): pass
+  
+  def _updateHeartbeat(self):
+    """
+    Called on any event occurance to note the time it occured.
+    """
+    
+    # alternative is to use the event's timestamp (via event.arrived_at)
+    self.lastHeartbeat = time.time()
+  
+  def _getRelayAttr(self, key, default, cacheUndefined = True):
+    """
+    Provides information associated with this relay, using the cached value if
+    available and otherwise looking it up.
+    
+    Arguments:
+      key            - parameter being queried (from CACHE_ARGS)
+      default        - value to be returned if undefined
+      cacheUndefined - caches when values are undefined, avoiding further
+                       lookups if true
+    """
+    
+    currentVal = self._cachedParam[key]
+    if currentVal:
+      if currentVal == UNKNOWN: return default
+      else: return currentVal
+    
+    self.connLock.acquire()
+    
+    currentVal, result = self._cachedParam[key], None
+    if not currentVal and self.isAlive():
+      # still unset - fetch value
+      if key in ("nsEntry", "descEntry"):
+        myFingerprint = self.getMyFingerprint()
+        
+        if myFingerprint:
+          queryType = "ns" if key == "nsEntry" else "desc"
+          queryResult = self.getInfo("%s/id/%s" % (queryType, myFingerprint))
+          if queryResult: result = queryResult.split("\n")
+      elif key == "bwRate":
+        # effective relayed bandwidth is the minimum of BandwidthRate,
+        # MaxAdvertisedBandwidth, and RelayBandwidthRate (if set)
+        effectiveRate = int(self.getOption("BandwidthRate"))
+        
+        relayRate = self.getOption("RelayBandwidthRate")
+        if relayRate and relayRate != "0":
+          effectiveRate = min(effectiveRate, int(relayRate))
+        
+        maxAdvertised = self.getOption("MaxAdvertisedBandwidth")
+        if maxAdvertised: effectiveRate = min(effectiveRate, int(maxAdvertised))
+        
+        result = effectiveRate
+      elif key == "bwBurst":
+        # effective burst (same for BandwidthBurst and RelayBandwidthBurst)
+        effectiveBurst = int(self.getOption("BandwidthBurst"))
+        
+        relayBurst = self.getOption("RelayBandwidthBurst")
+        if relayBurst and relayBurst != "0":
+          effectiveBurst = min(effectiveBurst, int(relayBurst))
+        
+        result = effectiveBurst
+      elif key == "bwObserved":
+        for line in self.getMyDescriptor([]):
+          if line.startswith("bandwidth"):
+            # line should look something like:
+            # bandwidth 40960 102400 47284
+            comp = line.split()
+            
+            if len(comp) == 4 and comp[-1].isdigit():
+              result = int(comp[-1])
+              break
+      elif key == "bwMeasured":
+        # TODO: Currently there's no client side indication of what type of
+        # measurement was used. Include this in results if it's ever available.
+        
+        for line in self.getMyNetworkStatus([]):
+          if line.startswith("w Bandwidth="):
+            bwValue = line[12:]
+            if bwValue.isdigit(): result = int(bwValue)
+            break
+      elif key == "fingerprint":
+        # Fingerprints are kept until sighup if set (most likely not even a
+        # setconf can change it since it's in the data directory). If orport is
+        # unset then no fingerprint will be set.
+        orPort = self.getOption("ORPort", "0")
+        if orPort == "0": result = UNKNOWN
+        else: result = self.getInfo("fingerprint")
+      elif key == "flags":
+        for line in self.getMyNetworkStatus([]):
+          if line.startswith("s "):
+            result = line[2:].split()
+            break
+      elif key == "pid":
+        result = getPid(int(self.getOption("ControlPort", 9051)), self.getOption("PidFile"))
+      
+      # cache value
+      if result: self._cachedParam[key] = result
+      elif cacheUndefined: self._cachedParam[key] = UNKNOWN
+    elif currentVal == UNKNOWN: result = currentVal
+    
+    self.connLock.release()
+    
+    if result: return result
+    else: return default
+  
+  def _notifyStatusListeners(self, eventType):
+    """
+    Sends a notice to all current listeners that a given change in tor's
+    controller status has occurred.
+    
+    Arguments:
+      eventType - enum representing tor's new status
+    """
+    
+    # resets cached getInfo parameters
+    self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
+    
+    # gives a notice that the control port has closed
+    if eventType == TOR_CLOSED:
+      log.log(CONFIG["log.torCtlPortClosed"], "Tor control port closed")
+    
+    for callback in self.statusListeners:
+      callback(self, eventType)
+



More information about the tor-commits mailing list