[or-cvs] r22549: {arm} Rewrote graph panel and related stats. Besides refactoring, (in arm/trunk: . init interface interface/graphing util)

Damian Johnson atagar1 at gmail.com
Thu Jun 24 16:26:17 UTC 2010


Author: atagar
Date: 2010-06-24 16:26:16 +0000 (Thu, 24 Jun 2010)
New Revision: 22549

Added:
   arm/trunk/interface/graphing/
   arm/trunk/interface/graphing/__init__.py
   arm/trunk/interface/graphing/bandwidthStats.py
   arm/trunk/interface/graphing/connStats.py
   arm/trunk/interface/graphing/graphPanel.py
   arm/trunk/interface/graphing/psStats.py
Removed:
   arm/trunk/interface/bandwidthMonitor.py
   arm/trunk/interface/connCountMonitor.py
   arm/trunk/interface/cpuMemMonitor.py
   arm/trunk/interface/graphPanel.py
Modified:
   arm/trunk/README
   arm/trunk/TODO
   arm/trunk/armrc.sample
   arm/trunk/init/starter.py
   arm/trunk/interface/__init__.py
   arm/trunk/interface/confPanel.py
   arm/trunk/interface/connPanel.py
   arm/trunk/interface/controller.py
   arm/trunk/interface/headerPanel.py
   arm/trunk/interface/logPanel.py
   arm/trunk/util/conf.py
   arm/trunk/util/connections.py
   arm/trunk/util/hostnames.py
   arm/trunk/util/log.py
   arm/trunk/util/panel.py
   arm/trunk/util/sysTools.py
   arm/trunk/util/torTools.py
   arm/trunk/util/uiTools.py
Log:
Rewrote graph panel and related stats. Besides refactoring, added configurability, etc this included the following:
added: graph can be configured to display any numeric ps stat
added: optional caching-only mode for ps graph, dramatically reducing call volume
added: third option for graphing bounds (restricting to both local minima and maxima)
change: substantially dropped rate at which graphing content is redrawn
fix: revised calculation for effective bandwidth rate to take MaxAdvertisedBandwidth into account



Modified: arm/trunk/README
===================================================================
--- arm/trunk/README	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/README	2010-06-24 16:26:16 UTC (rev 22549)
@@ -69,7 +69,7 @@
 status.
 
 That said, this is not a terribly big whoop. ISPs and anyone sniffing your
-connection already has this data - the only difference is that instead of
+connection already have this data - the only difference is that instead of
 saying "I am talking to x" you're saying "I'm talking to x, who's x?", meaning
 the resolver's also aware of who they are.
 

Modified: arm/trunk/TODO
===================================================================
--- arm/trunk/TODO	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/TODO	2010-06-24 16:26:16 UTC (rev 22549)
@@ -10,13 +10,8 @@
       bugs are being fixed while refactoring.
         [X] header panel
         [ ] graph panel
-          - revise effective bandwidth
-              Take into account 'MaxAdvertisedBandwidth', as per:
-              http://paste.debian.net/76254/
           - include observed bandwidth
               http://torstatus.blutmagie.de/router_detail.php?FP=a7569a83b5706ab1b1a9cb52eff7d2d32e4553eb
-          - graph for arm cpu/mem usage
-              Trivial to implement but not sure if this would be useful.
           - prepopulate bandwidth graph with contents of state file
               Open questions:
                 - How frequently are the bandwidth values in the state file
@@ -65,6 +60,9 @@
             submit at bugs.debian.org with subject "RFP: arm" and starting with a
             line "Package: wnpp". Also add to 'deb.torprojec.org'. (requested
             by helmut)
+              * http://www.debian.org/doc/maint-guide/
+              * http://www.debian.org/doc/packaging-manuals/python-policy/
+              * http://showmedo.com/videotutorials/video?name=linuxJensMakingDeb
   * release prep
     * check performance of this version vs last version (general screen refresh
         times)
@@ -72,6 +70,10 @@
     * double check __init__.py and README for changes
 
 - Bugs
+  * util are assuming that tor is running under the default command name
+      attempt to determine the command name at runtime (if the pid is available
+      then ps can do the mapping)
+  
   * log panel:
     * not catching events unexpected by arm
         Future tor and TorCtl revisions could provide new events - these should
@@ -112,6 +114,9 @@
     * connections aren't cleared when control port closes
 
 - Features / Site
+  * check if batch getInfo/getOption calls provide much performance benefit
+  * layout (css) bugs with site
+      Revise to use 'em' for measurements and somehow stretch image's y-margin?
   * page with details on client circuits, attempting to detect details like
       country, ISP, latency, exit policy for the circuit, traffic, etc
   * attempt to clear controller password from memory

Modified: arm/trunk/armrc.sample
===================================================================
--- arm/trunk/armrc.sample	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/armrc.sample	2010-06-24 16:26:16 UTC (rev 22549)
@@ -7,6 +7,29 @@
 
 features.colorInterface true
 
+# general graph parameters
+# interval: 0 -> each second,  1 -> 5 seconds,  2 -> 30 seconds,
+#           3 -> minutely,     4 -> half hour,  5 -> hourly,      6 -> daily
+# bound:    0 -> global maxima,        1 -> local maxima, 2 -> tight
+# type:     0 -> None, 1 -> Bandwidth, 2 -> Connections,  3 -> System Resources
+
+features.graph.interval 0
+features.graph.bound 1
+features.graph.type 1
+features.graph.maxSize 150
+
+# ps graph parameters
+# primary/secondaryStat: any numeric field provided by the ps command
+# cachedOnly: determines if the graph should query ps or rely on cached results
+#             (this lowers the call volume but limits the graph's granularity)
+
+features.graph.ps.primaryStat %cpu
+features.graph.ps.secondaryStat rss
+features.graph.ps.cachedOnly true
+
+features.graph.bw.showAccounting true
+features.graph.bw.isAccountingTimeLong false
+
 # seconds between querying information
 queries.ps.rate 5
 queries.connections.minRate 5
@@ -14,12 +37,14 @@
 # Thread pool size for hostname resolutions (determining the maximum number of
 # concurrent requests). Upping this to around thirty or so seems to be
 # problematic, causing intermittently seizing.
+
 queries.hostnames.poolSize 5
 
 # Uses python's internal "socket.gethostbyaddr" to resolve addresses rather
 # than the host command. This is ignored if the system's unable to make
 # parallel requests. Resolving this way seems to be much slower than host calls
 # in practice.
+
 queries.hostnames.useSocketModule false
 
 # caching parameters
@@ -37,6 +62,9 @@
 log.sysCallCached NONE
 log.sysCallFailed INFO
 log.sysCallCacheGrowing INFO
+log.panelRecreated DEBUG
+log.graph.ps.invalidStat WARN
+log.graph.ps.abandon WARN
 log.connLookupFailed INFO
 log.connLookupFailover NOTICE
 log.connLookupAbandon WARN

Modified: arm/trunk/init/starter.py
===================================================================
--- arm/trunk/init/starter.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/init/starter.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -19,6 +19,7 @@
 import util.connections
 import util.hostnames
 import util.log
+import util.panel
 import util.sysTools
 import util.torTools
 import util.uiTools
@@ -129,7 +130,7 @@
       config.update(DEFAULTS)
       
       # loads user preferences for utilities
-      for utilModule in (util.conf, util.connections, util.hostnames, util.log, util.sysTools, util.uiTools):
+      for utilModule in (util.conf, util.connections, util.hostnames, util.log, util.panel, util.sysTools, util.uiTools):
         utilModule.loadConfig(config)
     except IOError, exc:
       msg = "Failed to load configuration (using defaults): \"%s\"" % str(exc)

Modified: arm/trunk/interface/__init__.py
===================================================================
--- arm/trunk/interface/__init__.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/interface/__init__.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -2,5 +2,5 @@
 Panels, popups, and handlers comprising the arm user interface.
 """
 
-__all__ = ["bandwidthMonitor", "confPanel", "connCountMonitor", "connPanel", "controller", "cpuMemMonitor", "descriptorPopup", "fileDescriptorPopup", "graphPanel", "headerPanel", "logPanel"]
+__all__ = ["confPanel", "connPanel", "controller", "descriptorPopup", "fileDescriptorPopup", "headerPanel", "logPanel"]
 

Deleted: arm/trunk/interface/bandwidthMonitor.py
===================================================================
--- arm/trunk/interface/bandwidthMonitor.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/interface/bandwidthMonitor.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -1,185 +0,0 @@
-#!/usr/bin/env python
-# bandwidthMonitor.py -- Tracks stats concerning bandwidth usage.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import time
-import socket
-from TorCtl import TorCtl
-
-import graphPanel
-from util import uiTools
-
-DL_COLOR = "green"  # download section color
-UL_COLOR = "cyan"   # upload section color
-
-# width at which panel abandons placing optional stats (avg and total) with
-# header in favor of replacing the x-axis label
-COLLAPSE_WIDTH = 135
-
-class BandwidthMonitor(graphPanel.GraphStats, TorCtl.PostEventListener):
-  """
-  Tor event listener, taking bandwidth sampling to draw a bar graph. This is
-  updated every second by the BW events.
-  """
-  
-  def __init__(self, conn):
-    graphPanel.GraphStats.__init__(self)
-    TorCtl.PostEventListener.__init__(self)
-    self.conn = conn              # Tor control port connection
-    self.accountingInfo = None    # accounting data (set by _updateAccountingInfo method)
-    
-    # dummy values for static data
-    self.isAccounting = False
-    self.bwRate, self.bwBurst = None, None
-    self.resetOptions()
-  
-  def resetOptions(self):
-    """
-    Checks with tor for static bandwidth parameters (rates, accounting
-    information, etc).
-    """
-    
-    try:
-      if not self.conn: raise ValueError
-      self.isAccounting = self.conn.get_info('accounting/enabled')['accounting/enabled'] == '1'
-      
-      # static limit stats for label, uses relay stats if defined (internal behavior of tor)
-      bwStats = self.conn.get_option(['BandwidthRate', 'BandwidthBurst'])
-      relayStats = self.conn.get_option(['RelayBandwidthRate', 'RelayBandwidthBurst'])
-      
-      self.bwRate = uiTools.getSizeLabel(int(bwStats[0][1] if relayStats[0][1] == "0" else relayStats[0][1]), 1)
-      self.bwBurst = uiTools.getSizeLabel(int(bwStats[1][1] if relayStats[1][1] == "0" else relayStats[1][1]), 1)
-      
-      # if both are using rounded values then strip off the ".0" decimal
-      if ".0" in self.bwRate and ".0" in self.bwBurst:
-        self.bwRate = self.bwRate.replace(".0", "")
-        self.bwBurst = self.bwBurst.replace(".0", "")
-      
-    except (ValueError, socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
-      pass # keep old values
-    
-    # this doesn't track accounting stats when paused so doesn't need a custom pauseBuffer
-    contentHeight = 13 if self.isAccounting else 10
-    graphPanel.GraphStats.initialize(self, DL_COLOR, UL_COLOR, contentHeight)
-  
-  def bandwidth_event(self, event):
-    self._processEvent(event.read / 1024.0, event.written / 1024.0)
-  
-  def draw(self, panel):
-    # if display is narrow, overwrites x-axis labels with avg / total stats
-    if panel.maxX <= COLLAPSE_WIDTH:
-      # clears line
-      panel.addstr(8, 0, " " * 200)
-      graphCol = min((panel.maxX - 10) / 2, graphPanel.MAX_GRAPH_COL)
-      
-      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.primaryColor))
-      panel.addstr(8, graphCol + 6, secondaryFooter, uiTools.getColor(self.secondaryColor))
-    
-    # provides accounting stats if enabled
-    if self.isAccounting:
-      if not self.isPaused: self._updateAccountingInfo()
-      
-      if self.accountingInfo:
-        status = self.accountingInfo["status"]
-        hibernateColor = "green"
-        if status == "soft": hibernateColor = "yellow"
-        elif status == "hard": hibernateColor = "red"
-        
-        panel.addfstr(10, 0, "<b>Accounting (<%s>%s</%s>)</b>" % (hibernateColor, status, hibernateColor))
-        panel.addstr(10, 35, "Time to reset: %s" % self.accountingInfo["resetTime"])
-        panel.addstr(11, 2, "%s / %s" % (self.accountingInfo["read"], self.accountingInfo["readLimit"]), uiTools.getColor(self.primaryColor))
-        panel.addstr(11, 37, "%s / %s" % (self.accountingInfo["written"], self.accountingInfo["writtenLimit"]), uiTools.getColor(self.secondaryColor))
-      else:
-        panel.addfstr(10, 0, "<b>Accounting:</b> Connection Closed...")
-  
-  def getTitle(self, width):
-    # provides label, dropping stats if there's not enough room
-    capLabel = "cap: %s" % self.bwRate if self.bwRate else ""
-    burstLabel = "burst: %s" % self.bwBurst if self.bwBurst else ""
-    
-    if capLabel and burstLabel:
-      bwLabel = " (%s, %s)" % (capLabel, burstLabel)
-    elif capLabel or burstLabel:
-      # only one is set - use whatever's avaialble
-      bwLabel = " (%s%s)" % (capLabel, burstLabel)
-    else:
-      bwLabel = ""
-    
-    labelContents = "Bandwidth%s:" % bwLabel
-    if width < len(labelContents):
-      labelContents = "%s):" % labelContents[:labelContents.find(",")]  # removes burst measure
-      if width < len(labelContents): labelContents = "Bandwidth:"       # removes both
-    
-    return labelContents
-  
-  def getHeaderLabel(self, width, isPrimary):
-    graphType = "Downloaded" if isPrimary else "Uploaded"
-    stats = [""]
-    
-    # conditional is to avoid flickering as stats change size for tty terminals
-    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))
-    
-    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 _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
-    
-    Sets mapping to None if the Tor connection is closed.
-    """
-    
-    try:
-      self.accountingInfo = {}
-      
-      accountingParams = self.conn.get_info(["accounting/hibernating", "accounting/bytes", "accounting/bytes-left", "accounting/interval-end"])
-      self.accountingInfo["status"] = accountingParams["accounting/hibernating"]
-      
-      # 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(accountingParams["accounting/interval-end"], "%Y-%m-%d %H:%M:%S")) - time.time() - tz_offset
-      resetHours = sec / 3600
-      sec %= 3600
-      resetMin = sec / 60
-      sec %= 60
-      self.accountingInfo["resetTime"] = "%i:%02i:%02i" % (resetHours, resetMin, sec)
-      
-      read = int(accountingParams["accounting/bytes"].split(" ")[0])
-      written = int(accountingParams["accounting/bytes"].split(" ")[1])
-      readLeft = int(accountingParams["accounting/bytes-left"].split(" ")[0])
-      writtenLeft = int(accountingParams["accounting/bytes-left"].split(" ")[1])
-      
-      self.accountingInfo["read"] = uiTools.getSizeLabel(read)
-      self.accountingInfo["written"] = uiTools.getSizeLabel(written)
-      self.accountingInfo["readLimit"] = uiTools.getSizeLabel(read + readLeft)
-      self.accountingInfo["writtenLimit"] = uiTools.getSizeLabel(written + writtenLeft)
-    except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
-      self.accountingInfo = None
-

Modified: arm/trunk/interface/confPanel.py
===================================================================
--- arm/trunk/interface/confPanel.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/interface/confPanel.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -37,7 +37,7 @@
   """
   
   def __init__(self, stdscr, confLocation, conn):
-    panel.Panel.__init__(self, stdscr, 0)
+    panel.Panel.__init__(self, stdscr, "conf", 0)
     self.confLocation = confLocation
     self.showLineNum = True
     self.stripComments = False
@@ -176,7 +176,7 @@
     elif key == ord('s') or key == ord('S'):
       self.stripComments = not self.stripComments
       self.scroll = 0
-    self.redraw()
+    self.redraw(True)
   
   def draw(self, subwindow, width, height):
     self.addstr(0, 0, "Tor Config (%s):" % self.confLocation, curses.A_STANDOUT)

Deleted: arm/trunk/interface/connCountMonitor.py
===================================================================
--- arm/trunk/interface/connCountMonitor.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/interface/connCountMonitor.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -1,57 +0,0 @@
-#!/usr/bin/env python
-# connCountMonitor.py -- Tracks the number of connections made by Tor.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import socket
-from TorCtl import TorCtl
-
-import graphPanel
-from util import connections
-
-class ConnCountMonitor(graphPanel.GraphStats, TorCtl.PostEventListener):
-  """
-  Tracks number of connections, counting client and directory connections as 
-  outbound.
-  """
-  
-  def __init__(self, conn):
-    graphPanel.GraphStats.__init__(self)
-    TorCtl.PostEventListener.__init__(self)
-    graphPanel.GraphStats.initialize(self, "green", "cyan", 10)
-    
-    self.orPort = "0"
-    self.dirPort = "0"
-    self.controlPort = "0"
-    self.resetOptions(conn)
-  
-  def resetOptions(self, conn):
-    try:
-      self.orPort = conn.get_option("ORPort")[0][1]
-      self.dirPort = conn.get_option("DirPort")[0][1]
-      self.controlPort = conn.get_option("ControlPort")[0][1]
-    except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
-      self.orPort = "0"
-      self.dirPort = "0"
-      self.controlPort = "0"
-  
-  def bandwidth_event(self, event):
-    # doesn't use events but this keeps it in sync with the bandwidth panel
-    # (and so it stops if Tor stops - used to use a separate thread but this
-    # is better)
-    inbound, outbound, control = 0, 0, 0
-    
-    for lIp, lPort, fIp, fPort in connections.getResolver("tor").getConnections():
-      if lPort in (self.orPort, self.dirPort): inbound += 1
-      elif lPort == self.controlPort: control += 1
-      else: outbound += 1
-    
-    self._processEvent(inbound, outbound)
-  
-  def getTitle(self, width):
-    return "Connection Count:"
-  
-  def getHeaderLabel(self, width, isPrimary):
-    avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
-    if isPrimary: return "Inbound (%s, avg: %s):" % (self.lastPrimary, avg)
-    else: return "Outbound (%s, avg: %s):" % (self.lastSecondary, avg)
-

Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/interface/connPanel.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -111,7 +111,7 @@
   
   def __init__(self, stdscr, conn, isDisabled):
     TorCtl.PostEventListener.__init__(self)
-    panel.Panel.__init__(self, stdscr, 0)
+    panel.Panel.__init__(self, stdscr, "conn", 0)
     self.scroll = 0
     self.conn = conn                  # tor connection for querrying country codes
     self.listingType = LIST_IP        # information used in listing entries
@@ -452,7 +452,7 @@
       if not self.allowDNS: hostnames.setPaused(True)
       elif self.listingType == LIST_HOSTNAME: hostnames.setPaused(False)
     else: return # skip following redraw
-    self.redraw()
+    self.redraw(True)
   
   def draw(self, subwindow, width, height):
     self.connectionsLock.acquire()

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/interface/controller.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -15,7 +15,7 @@
 from TorCtl import TorUtil
 
 import headerPanel
-import graphPanel
+import graphing.graphPanel
 import logPanel
 import connPanel
 import confPanel
@@ -23,9 +23,9 @@
 import fileDescriptorPopup
 
 from util import conf, log, connections, hostnames, panel, sysTools, torTools, uiTools
-import bandwidthMonitor
-import cpuMemMonitor
-import connCountMonitor
+import graphing.bandwidthStats
+import graphing.connStats
+import graphing.psStats
 
 CONFIRM_QUIT = True
 REFRESH_RATE = 5        # seconds between redrawing screen
@@ -42,14 +42,13 @@
   ["torrc"]]
 PAUSEABLE = ["header", "graph", "log", "conn"]
 
-# user customizable parameters
-CONFIG = {"log.configEntryUndefined": log.NOTICE}
+CONFIG = {"features.graph.type": 1, "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, 0, 1)
+    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
@@ -125,7 +124,7 @@
   """
   
   def __init__(self, stdscr, height):
-    panel.Panel.__init__(self, stdscr, 0, 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
@@ -301,6 +300,15 @@
     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.
@@ -314,6 +322,7 @@
   # loads config for various interface components
   config = conf.getConfig("arm")
   config.update(CONFIG)
+  config.update(graphing.graphPanel.CONFIG)
   
   # pauses/unpauses connection resolution according to if tor's connected or not
   torTools.getConn().addStatusListener(connResetListener)
@@ -354,7 +363,7 @@
   panels = {
     "header": headerPanel.HeaderPanel(stdscr, config),
     "popup": Popup(stdscr, 9),
-    "graph": graphPanel.GraphPanel(stdscr),
+    "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, 
@@ -372,11 +381,17 @@
   if not torPid: log.log(log.WARN, "Unable to resolve tor pid, abandoning connection listing")
   
   # statistical monitors for graph
-  panels["graph"].addStats("bandwidth", bandwidthMonitor.BandwidthMonitor(conn))
-  panels["graph"].addStats("system resources", cpuMemMonitor.CpuMemMonitor())
-  if not isBlindMode: panels["graph"].addStats("connections", connCountMonitor.ConnCountMonitor(conn))
-  panels["graph"].setStats("bandwidth")
+  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"])
@@ -412,12 +427,14 @@
   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()      # hack to make sure popup has a window instance (not entirely sure why...)
+  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"], "config entry '%s' is unrecognized" % key)
+    log.log(CONFIG["log.configEntryUndefined"], "unrecognized configuration entry: %s" % key)
   
+  # 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
@@ -431,12 +448,12 @@
         
         # other panels that use torrc data
         panels["conn"].resetOptions()
-        if not isBlindMode: panels["graph"].stats["connections"].resetOptions(conn)
+        #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"].height)
+          panels["graph"].setHeight(panels["graph"].stats["bandwidth"].getPreferredHeight())
         
         panels["torrc"].reset()
         sighupTracker.isReset = False
@@ -486,7 +503,12 @@
       # 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": panels[panelKey].redraw()
+        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()
     finally:
@@ -510,7 +532,7 @@
           
           # provides prompt
           panels["control"].setMsg("Are you sure (q again to confirm)?", curses.A_BOLD)
-          panels["control"].redraw()
+          panels["control"].redraw(True)
           
           curses.cbreak()
           confirmationKey = stdscr.getch()
@@ -559,8 +581,9 @@
       
       # TODO: this redraw doesn't seem necessary (redraws anyway after this
       # loop) - look into this when refactoring
-      panels["control"].redraw()
+      panels["control"].redraw(True)
       
+      selectiveRefresh(panels, page)
     elif key == ord('p') or key == ord('P'):
       # toggles update freezing
       panel.CURSES_LOCK.acquire()
@@ -570,6 +593,8 @@
         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()
@@ -588,8 +613,8 @@
           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>)" % panels["graph"].updateInterval)
-          popup.addfstr(2, 2, "<b>b</b>: graph bounds (<b>%s</b>)" % graphPanel.BOUND_LABELS[panels["graph"].bounds])
+          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")
           
@@ -647,6 +672,7 @@
         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')):
@@ -668,7 +694,7 @@
       # hides top label of the graph panel and pauses panels
       if panels["graph"].currentDisplay:
         panels["graph"].showLabel = False
-        panels["graph"].redraw()
+        panels["graph"].redraw(True)
       setPauseState(panels, isPaused, page, True)
       
       selection = showMenu(stdscr, panels["popup"], "Graphed Stats:", options, initialSelection)
@@ -681,18 +707,22 @@
       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 graphPanel.UPDATE_INTERVALS]
+      options = [label for (label, intervalTime) in graphing.graphPanel.UPDATE_INTERVALS]
       
-      initialSelection = -1
-      for i in range(len(options)):
-        if options[i] == panels["graph"].updateInterval: initialSelection = i
+      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()
+        panels["graph"].redraw(True)
       setPauseState(panels, isPaused, page, True)
       
       selection = showMenu(stdscr, panels["popup"], "Update Interval:", options, initialSelection)
@@ -702,10 +732,14 @@
       setPauseState(panels, isPaused, page)
       
       # applies new setting
-      if selection != -1: panels["graph"].updateInterval = options[selection]
+      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) % 2
+      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()
@@ -727,7 +761,7 @@
         
         # provides prompt
         panels["control"].setMsg("Events to log: ")
-        panels["control"].redraw()
+        panels["control"].redraw(True)
         
         # makes cursor and typing visible
         try: curses.curs_set(1)
@@ -767,7 +801,7 @@
             panels["log"].loggedEvents = loggedEvents
           except ValueError, exc:
             panels["control"].setMsg("Invalid flags: %s" % str(exc), curses.A_STANDOUT)
-            panels["control"].redraw()
+            panels["control"].redraw(True)
             time.sleep(2)
         
         # reverts popup dimensions
@@ -787,7 +821,7 @@
       # hides top label of the graph panel and pauses panels
       if panels["graph"].currentDisplay:
         panels["graph"].showLabel = False
-        panels["graph"].redraw()
+        panels["graph"].redraw(True)
       setPauseState(panels, isPaused, page, True)
       
       selection = showMenu(stdscr, panels["popup"], "Log Filter:", options, initialSelection)
@@ -801,7 +835,7 @@
         try:
           # provides prompt
           panels["control"].setMsg("Regular expression: ")
-          panels["control"].redraw()
+          panels["control"].redraw(True)
           
           # makes cursor and typing visible
           try: curses.curs_set(1)
@@ -824,7 +858,7 @@
               regexFilters = [regexInput] + regexFilters
             except re.error, exc:
               panels["control"].setMsg("Unable to compile expression: %s" % str(exc), curses.A_STANDOUT)
-              panels["control"].redraw()
+              panels["control"].redraw(True)
               time.sleep(2)
           panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
         finally:
@@ -862,7 +896,7 @@
         # reconfigures connection panel to accomidate details dialog
         panels["conn"].showLabel = False
         panels["conn"].showingDetails = True
-        panels["conn"].redraw()
+        panels["conn"].redraw(True)
         
         hostnames.setPaused(not panels["conn"].allowDNS)
         relayLookupCache = {} # temporary cache of entry -> (ns data, desc data)
@@ -997,7 +1031,7 @@
             panels["conn"].handleKey(key)
           elif key in (ord('d'), ord('D')):
             descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, conn, panels["conn"])
-            panels["conn"].redraw()
+            panels["conn"].redraw(True)
         
         panels["conn"].showLabel = True
         panels["conn"].showingDetails = False
@@ -1013,7 +1047,7 @@
         setPauseState(panels, isPaused, page, True)
         curses.cbreak() # wait indefinitely for key presses (no timeout)
         panels["conn"].showLabel = False
-        panels["conn"].redraw()
+        panels["conn"].redraw(True)
         
         descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, conn, panels["conn"])
         
@@ -1030,7 +1064,7 @@
       
       # hides top label of conn panel and pauses panels
       panels["conn"].showLabel = False
-      panels["conn"].redraw()
+      panels["conn"].redraw(True)
       setPauseState(panels, isPaused, page, True)
       
       selection = showMenu(stdscr, panels["popup"], "List By:", options, initialSelection)
@@ -1066,7 +1100,7 @@
       
       # hides top label of conn panel and pauses panels
       panels["conn"].showLabel = False
-      panels["conn"].redraw()
+      panels["conn"].redraw(True)
       setPauseState(panels, isPaused, page, True)
       
       selection = showMenu(stdscr, panels["popup"], "Resolver Util:", options, initialSelection)
@@ -1199,10 +1233,10 @@
       # 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()
+      if isSuccessful: panels["torrc"].redraw(True)
       
       panels["control"].setMsg(resetMsg, curses.A_STANDOUT)
-      panels["control"].redraw()
+      panels["control"].redraw(True)
       time.sleep(1)
       
       panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
@@ -1214,7 +1248,7 @@
         
         # 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()
+        panels["control"].redraw(True)
         
         curses.cbreak()
         confirmationKey = stdscr.getch()
@@ -1226,7 +1260,7 @@
             
             #errorMsg = " (%s)" % str(err) if str(err) else ""
             #panels["control"].setMsg("Sighup failed%s" % errorMsg, curses.A_STANDOUT)
-            #panels["control"].redraw()
+            #panels["control"].redraw(True)
             #time.sleep(2)
         
         # reverts display settings

Deleted: arm/trunk/interface/cpuMemMonitor.py
===================================================================
--- arm/trunk/interface/cpuMemMonitor.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/interface/cpuMemMonitor.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -1,59 +0,0 @@
-#!/usr/bin/env python
-# cpuMemMonitor.py -- Tracks cpu and memory usage of Tor.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import time
-from TorCtl import TorCtl
-
-from util import sysTools, torTools, uiTools
-import graphPanel
-
-class CpuMemMonitor(graphPanel.GraphStats, TorCtl.PostEventListener):
-  """
-  Tracks system resource usage (cpu and memory usage), using cached values in
-  headerPanel if recent enough (otherwise retrieved independently).
-  """
-  
-  def __init__(self):
-    graphPanel.GraphStats.__init__(self)
-    TorCtl.PostEventListener.__init__(self)
-    graphPanel.GraphStats.initialize(self, "green", "cyan", 10)
-  
-  def bandwidth_event(self, event):
-    # doesn't use events but this keeps it in sync with the bandwidth panel
-    # (and so it stops if Tor stops
-    # TODO: ok, screw it - the number of ps calls this makes is ridicuous
-    # compared to how frequently it changes - now caching for five seconds
-    # (note this during the rewrite that its fidelity isn't at the second
-    # level)
-    # TODO: when rewritten raise fidelity to second level if being actively
-    # looked at (or has been recently)
-    # TODO: dropped header requirement so any documentation will, of course,
-    # need to be revised
-    torPid = torTools.getConn().getPid()
-    
-    # cached results stale - requery ps
-    # TODO: issue the same request as header panel to take advantage of cached results
-    sampling = []
-    psCall = None
-    if torPid:
-      psCall = sysTools.call("ps -p %s -o %s" % (torPid, "%cpu,rss,%mem,etime"), 5, True)
-    if psCall and len(psCall) >= 2: sampling = psCall[1].strip().split()
-    
-    if len(sampling) < 2:
-      # either ps failed or returned no tor instance, register error
-      # ps call failed (returned no tor instance or registered an  error) -
-      # we need to register something (otherwise timescale would be thrown
-      # off) so keep old results
-      self._processEvent(self.lastPrimary, self.lastSecondary)
-    else:
-      self._processEvent(float(sampling[0]), float(sampling[1]) / 1024.0)
-  
-  def getTitle(self, width):
-    return "System Resources:"
-  
-  def getHeaderLabel(self, width, isPrimary):
-    avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
-    if isPrimary: return "CPU (%s%%, avg: %0.1f%%):" % (self.lastPrimary, avg)
-    else: return "Memory (%s, avg: %s):" % (uiTools.getSizeLabel(self.lastSecondary * 1048576, 1), uiTools.getSizeLabel(avg * 1048576, 1))
-

Deleted: arm/trunk/interface/graphPanel.py
===================================================================
--- arm/trunk/interface/graphPanel.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/interface/graphPanel.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -1,279 +0,0 @@
-#!/usr/bin/env python
-# graphPanel.py -- Graph providing a variety of statistics.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import copy
-import curses
-
-from util import panel, uiTools
-
-MAX_GRAPH_COL = 150  # max columns of data in graph
-WIDE_LABELING_GRAPH_COL = 50  # minimum graph columns to use wide spacing for x-axis labels
-
-# enums for graph bounds:
-#   BOUNDS_MAX - global maximum (highest value ever seen)
-#   BOUNDS_TIGHT - local maximum (highest value currently on the graph)
-BOUNDS_MAX, BOUNDS_TIGHT = range(2)
-BOUND_LABELS = {BOUNDS_MAX: "max", BOUNDS_TIGHT: "tight"}
-
-# time intervals at which graphs can be updated
-DEFAULT_UPDATE_INTERVAL = "5 seconds"
-UPDATE_INTERVALS = [("each second", 1),     ("5 seconds", 5),   ("30 seconds", 30),   ("minutely", 60),
-                    ("half hour", 1800),    ("hourly", 3600),   ("daily", 86400)]
-
-class GraphStats:
-  """
-  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):
-    """
-    Initializes all parameters to dummy values.
-    """
-    
-    self.primaryColor = None    # colors used to draw stats/graphs
-    self.secondaryColor = None
-    self.height = None          # vertical size of content
-    self.graphPanel = None      # panel where stats are drawn (set when added to GraphPanel)
-    
-    self.isPaused = False
-    self.pauseBuffer = None     # mirror instance used to track updates when pauses - 
-                                # this is a pauseBuffer instance itself if None
-    
-    # tracked stats
-    self.tick = 0               # number of events processed
-    self.lastPrimary = 0        # most recent registered stats
-    self.lastSecondary = 0
-    self.primaryTotal = 0       # sum of all stats seen
-    self.secondaryTotal = 0
-    
-    # timescale dependent stats
-    self.maxPrimary, self.maxSecondary = {}, {}
-    self.primaryCounts, self.secondaryCounts = {}, {}
-    for (label, timescale) in UPDATE_INTERVALS:
-      # recent rates for graph
-      self.maxPrimary[label] = 1
-      self.maxSecondary[label] = 1
-      
-      # historic stats for graph, first is accumulator
-      # iterative insert needed to avoid making shallow copies (nasty, nasty gotcha)
-      self.primaryCounts[label] = (MAX_GRAPH_COL + 1) * [0]
-      self.secondaryCounts[label] = (MAX_GRAPH_COL + 1) * [0]
-  
-  def initialize(self, primaryColor, secondaryColor, height, pauseBuffer=None):
-    """
-    Initializes newly constructed GraphPanel instance.
-    """
-    
-    # used because of python's inability to have overloaded constructors
-    self.primaryColor = primaryColor        # colors used to draw stats/graphs
-    self.secondaryColor = secondaryColor
-    self.height = height
-    
-    # mirror instance used to track updates when paused
-    if not pauseBuffer: self.pauseBuffer = GraphStats()
-    else: self.pauseBuffer = pauseBuffer
-  
-  def getTitle(self, width):
-    """
-    Provides top label.
-    """
-    
-    return ""
-  
-  def getHeaderLabel(self, width, isPrimary):
-    """
-    Provides labeling presented at the top of the graph.
-    """
-    
-    return ""
-  
-  def draw(self, panel):
-    """
-    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 not self.pauseBuffer: return
-    self.isPaused = isPause
-    
-    if self.isPaused: active, inactive = self.pauseBuffer, self
-    else: active, inactive = self, self.pauseBuffer
-    self._parameterSwap(active, inactive)
-  
-  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.
-    """
-    
-    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 GraphPanel of changes.
-    """
-    
-    if self.isPaused: self.pauseBuffer._processEvent(primary, secondary)
-    else:
-      self.lastPrimary, self.lastSecondary = primary, secondary
-      self.primaryTotal += primary
-      self.secondaryTotal += secondary
-      
-      # updates for all time intervals
-      self.tick += 1
-      for (label, timescale) in UPDATE_INTERVALS:
-        self.primaryCounts[label][0] += primary
-        self.secondaryCounts[label][0] += secondary
-        
-        if self.tick % timescale == 0:
-          self.maxPrimary[label] = max(self.maxPrimary[label], self.primaryCounts[label][0] / timescale)
-          self.primaryCounts[label][0] /= timescale
-          self.primaryCounts[label].insert(0, 0)
-          del self.primaryCounts[label][MAX_GRAPH_COL + 1:]
-          
-          self.maxSecondary[label] = max(self.maxSecondary[label], self.secondaryCounts[label][0] / timescale)
-          self.secondaryCounts[label][0] /= timescale
-          self.secondaryCounts[label].insert(0, 0)
-          del self.secondaryCounts[label][MAX_GRAPH_COL + 1:]
-      
-      if self.graphPanel: self.graphPanel.redraw()
-
-class GraphPanel(panel.Panel):
-  """
-  Panel displaying a graph, drawing statistics from custom GraphStats
-  implementations.
-  """
-  
-  def __init__(self, stdscr):
-    panel.Panel.__init__(self, stdscr, 0, 0) # height is overwritten with current module
-    self.updateInterval = DEFAULT_UPDATE_INTERVAL
-    self.isPaused = False
-    self.showLabel = True         # shows top label if true, hides otherwise
-    self.bounds = BOUNDS_TIGHT    # determines bounds on graph
-    self.currentDisplay = None    # label of the stats currently being displayed
-    self.stats = {}               # available stats (mappings of label -> instance)
-  
-  def draw(self, subwindow, width, height):
-    """ Redraws graph panel """
-    
-    graphCol = min((width - 10) / 2, MAX_GRAPH_COL)
-    
-    if self.currentDisplay:
-      param = self.stats[self.currentDisplay]
-      primaryColor = uiTools.getColor(param.primaryColor)
-      secondaryColor = uiTools.getColor(param.secondaryColor)
-      
-      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 value on the graph
-      primaryBound, secondaryBound = -1, -1
-      
-      if self.bounds == BOUNDS_MAX:
-        primaryBound = param.maxPrimary[self.updateInterval]
-        secondaryBound = param.maxSecondary[self.updateInterval]
-      elif self.bounds == BOUNDS_TIGHT:
-        for value in param.primaryCounts[self.updateInterval][1:graphCol + 1]: primaryBound = max(value, primaryBound)
-        for value in param.secondaryCounts[self.updateInterval][1:graphCol + 1]: secondaryBound = max(value, secondaryBound)
-      
-      # displays bound
-      self.addstr(2, 0, "%4s" % str(int(primaryBound)), primaryColor)
-      self.addstr(7, 0, "   0", primaryColor)
-      
-      self.addstr(2, graphCol + 5, "%4s" % str(int(secondaryBound)), secondaryColor)
-      self.addstr(7, graphCol + 5, "   0", secondaryColor)
-      
-      # creates bar graph of bandwidth usage over time
-      for col in range(graphCol):
-        colHeight = min(5, 5 * param.primaryCounts[self.updateInterval][col + 1] / max(1, primaryBound))
-        for row in range(colHeight): self.addstr(7 - row, col + 5, " ", curses.A_STANDOUT | primaryColor)
-      
-      for col in range(graphCol):
-        colHeight = min(5, 5 * param.secondaryCounts[self.updateInterval][col + 1] / max(1, secondaryBound))
-        for row in range(colHeight): self.addstr(7 - row, col + graphCol + 10, " ", curses.A_STANDOUT | secondaryColor)
-      
-      # bottom labeling of x-axis
-      intervalSec = 1
-      for (label, timescale) in UPDATE_INTERVALS:
-        if label == self.updateInterval: intervalSec = timescale
-      
-      intervalSpacing = 10 if graphCol >= WIDE_LABELING_GRAPH_COL else 5
-      unitsLabel, decimalPrecision = None, 0
-      for i in range(1, (graphCol + intervalSpacing - 4) / intervalSpacing):
-        loc = i * 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)
-        
-      # allows for finishing touches by monitor
-      param.draw(self)
-  
-  def addStats(self, label, stats):
-    """
-    Makes GraphStats instance available in the panel.
-    """
-    
-    stats.graphPanel = self
-    self.stats[label] = stats
-    stats.isPaused = True
-  
-  def setStats(self, label):
-    """
-    Sets the current 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
-        self.height = 0
-      elif label in self.stats.keys():
-        self.currentDisplay = label
-        newStats = self.stats[label]
-        self.height = newStats.height
-        newStats.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)
-

Added: arm/trunk/interface/graphing/__init__.py
===================================================================
--- arm/trunk/interface/graphing/__init__.py	                        (rev 0)
+++ arm/trunk/interface/graphing/__init__.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -0,0 +1,6 @@
+"""
+Panels, popups, and handlers comprising the arm user interface.
+"""
+
+__all__ = ["graphPanel.py", "bandwidthStats", "connStats", "psStats"]
+

Added: arm/trunk/interface/graphing/bandwidthStats.py
===================================================================
--- arm/trunk/interface/graphing/bandwidthStats.py	                        (rev 0)
+++ arm/trunk/interface/graphing/bandwidthStats.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -0,0 +1,227 @@
+"""
+Tracks bandwidth usage of the tor process, expanding to include accounting
+stats if they're set.
+"""
+
+import time
+
+import graphPanel
+from util import 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")
+
+DEFAULT_CONFIG = {"features.graph.bw.showAccounting": True, "features.graph.bw.isAccountingTimeLong": False}
+
+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)
+    
+    # accounting data (set by _updateAccountingInfo method)
+    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.isAccounting, self.bwRate, self.bwBurst = False, None, None
+    self.resetListener(conn, torTools.TOR_INIT) # initializes values
+    conn.addStatusListener(self.resetListener)
+  
+  def resetListener(self, conn, eventType):
+    # queries for rate, burst, and accounting status if it might have changed
+    if eventType == torTools.TOR_INIT:
+      if self._config["features.graph.bw.showAccounting"]:
+        self.isAccounting = conn.getInfo('accounting/enabled') == '1'
+      
+      # effective relayed bandwidth is the minimum of BandwidthRate,
+      # MaxAdvertisedBandwidth, and RelayBandwidthRate (if set)
+      effectiveRate = int(conn.getOption("BandwidthRate"))
+      
+      relayRate = conn.getOption("RelayBandwidthRate")
+      if relayRate and relayRate != "0":
+        effectiveRate = min(effectiveRate, int(relayRate))
+      
+      maxAdvertised = conn.getOption("MaxAdvertisedBandwidth")
+      if maxAdvertised: effectiveRate = min(effectiveRate, int(maxAdvertised))
+      
+      # effective burst (same for BandwidthBurst and RelayBandwidthBurst)
+      effectiveBurst = int(conn.getOption("BandwidthBurst"))
+      
+      relayBurst = conn.getOption("RelayBandwidthBurst")
+      if relayBurst and relayBurst != "0":
+        effectiveBurst = min(effectiveBurst, int(relayBurst))
+      
+      self.bwRate = uiTools.getSizeLabel(effectiveRate, 1)
+      self.bwBurst = uiTools.getSizeLabel(effectiveBurst, 1)
+      
+      # if both are using rounded values then strip off the ".0" decimal
+      if ".0" in self.bwRate and ".0" in self.bwBurst:
+        self.bwRate = self.bwRate.replace(".0", "")
+        self.bwBurst = self.bwBurst.replace(".0", "")
+  
+  def bandwidth_event(self, event):
+    if self.isNextTickRedraw(): 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):
+    # provides label, dropping stats if there's not enough room
+    capLabel = "cap: %s" % self.bwRate if self.bwRate else ""
+    burstLabel = "burst: %s" % self.bwBurst if self.bwBurst else ""
+    
+    if capLabel and burstLabel:
+      bwLabel = " (%s, %s)" % (capLabel, burstLabel)
+    elif capLabel or burstLabel:
+      # only one is set - use whatever's avaialble
+      bwLabel = " (%s%s)" % (capLabel, burstLabel)
+    else:
+      bwLabel = ""
+    
+    labelContents = "Bandwidth%s:" % bwLabel
+    if width < len(labelContents):
+      labelContents = "%s):" % labelContents[:labelContents.find(",")]  # removes burst measure
+      if width < len(labelContents): labelContents = "Bandwidth:"       # removes both
+    
+    return labelContents
+  
+  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 _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.isAccountingTimeLong"]:
+        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
+

Added: arm/trunk/interface/graphing/connStats.py
===================================================================
--- arm/trunk/interface/graphing/connStats.py	                        (rev 0)
+++ arm/trunk/interface/graphing/connStats.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -0,0 +1,51 @@
+"""
+Tracks stats concerning tor's current connections.
+"""
+
+import graphPanel
+from util import connections, torTools
+
+class ConnStats(graphPanel.GraphStats):
+  """
+  Tracks number of connections, counting client and directory connections as 
+  outbound. Control connections are excluded from counts.
+  """
+  
+  def __init__(self):
+    graphPanel.GraphStats.__init__(self)
+    
+    # listens for tor reload (sighup) events which can reset the ports tor uses
+    conn = torTools.getConn()
+    self.orPort, self.dirPort, self.controlPort = "0", "0", "0"
+    self.resetListener(conn, torTools.TOR_INIT) # initialize port values
+    conn.addStatusListener(self.resetListener)
+  
+  def resetListener(self, conn, eventType):
+    if eventType == torTools.TOR_INIT:
+      self.orPort = conn.getOption("ORPort", "0")
+      self.dirPort = conn.getOption("DirPort", "0")
+      self.controlPort = conn.getOption("ControlPort", "0")
+  
+  def eventTick(self):
+    """
+    Fetches connection stats from cached information.
+    """
+    
+    inboundCount, outboundCount = 0, 0
+    
+    for entry in connections.getResolver("tor").getConnections():
+      localPort = entry[1]
+      if localPort in (self.orPort, self.dirPort): inboundCount += 1
+      elif localPort == self.controlPort: pass # control connection
+      else: outboundCount += 1
+    
+    self._processEvent(inboundCount, outboundCount)
+  
+  def getTitle(self, width):
+    return "Connection Count:"
+  
+  def getHeaderLabel(self, width, isPrimary):
+    avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
+    if isPrimary: return "Inbound (%s, avg: %s):" % (self.lastPrimary, avg)
+    else: return "Outbound (%s, avg: %s):" % (self.lastSecondary, avg)
+

Copied: arm/trunk/interface/graphing/graphPanel.py (from rev 22468, arm/trunk/interface/graphPanel.py)
===================================================================
--- arm/trunk/interface/graphing/graphPanel.py	                        (rev 0)
+++ arm/trunk/interface/graphing/graphPanel.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -0,0 +1,356 @@
+"""
+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),
+                    ("half hour", 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}
+
+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.isPaused:
+      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
+        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)
+

Added: arm/trunk/interface/graphing/psStats.py
===================================================================
--- arm/trunk/interface/graphing/psStats.py	                        (rev 0)
+++ arm/trunk/interface/graphing/psStats.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -0,0 +1,129 @@
+"""
+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 subsiquent 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 subsiquent failed queries
+    
+    self._config = dict(DEFAULT_CONFIG)
+    if config: config.update(self._config)
+    
+    self.queryPid = torTools.getConn().getPid()
+    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 resonable 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)
+

Modified: arm/trunk/interface/headerPanel.py
===================================================================
--- arm/trunk/interface/headerPanel.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/interface/headerPanel.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -32,7 +32,6 @@
 VERSION_STATUS_COLORS = {"new": "blue", "new in series": "blue", "obsolete": "red", "recommended": "green",  
                          "old": "red",  "unrecommended": "red",  "unknown": "cyan"}
 
-# user customizable parameters
 DEFAULT_CONFIG = {"queries.ps.rate": 5}
 
 class HeaderPanel(panel.Panel, threading.Thread):
@@ -49,7 +48,7 @@
   """
   
   def __init__(self, stdscr, config=None):
-    panel.Panel.__init__(self, stdscr, 0)
+    panel.Panel.__init__(self, stdscr, "header", 0)
     threading.Thread.__init__(self)
     self.setDaemon(True)
     

Modified: arm/trunk/interface/logPanel.py
===================================================================
--- arm/trunk/interface/logPanel.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/interface/logPanel.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -90,7 +90,7 @@
   
   def __init__(self, stdscr, conn, loggedEvents):
     TorCtl.PostEventListener.__init__(self)
-    panel.Panel.__init__(self, stdscr, 0)
+    panel.Panel.__init__(self, stdscr, "log", 0)
     self.scroll = 0
     self.msgLog = []                      # tuples of (logText, color)
     self.isPaused = False
@@ -293,7 +293,7 @@
     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()
+      self.redraw(True)
   
   def draw(self, subwindow, width, height):
     """
@@ -385,7 +385,7 @@
     if self.isPaused: self.pauseBuffer = []
     else:
       self.msgLog = (self.pauseBuffer + self.msgLog)[:MAX_LOG_ENTRIES]
-      if self.win: self.redraw() # hack to avoid redrawing during init
+      if self.win: self.redraw(True) # hack to avoid redrawing during init
   
   def getHeartbeat(self):
     """

Modified: arm/trunk/util/conf.py
===================================================================
--- arm/trunk/util/conf.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/util/conf.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -20,8 +20,6 @@
 import log
 
 CONFS = {}  # mapping of identifier to singleton instances of configs
-
-# user customizable parameters
 CONFIG = {"log.configEntryNotFound": None, "log.configEntryTypeError": log.INFO}
 
 def loadConfig(config):

Modified: arm/trunk/util/connections.py
===================================================================
--- arm/trunk/util/connections.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/util/connections.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -22,6 +22,11 @@
 CMD_NETSTAT, CMD_SS, CMD_LSOF = range(1, 4)
 CMD_STR = {CMD_NETSTAT: "netstat", CMD_SS: "ss", CMD_LSOF: "lsof"}
 
+# If true this provides new instantiations for resolvers if the old one has
+# been stopped. This can make it difficult ensure all threads are terminated
+# when accessed concurrently.
+RECREATE_HALTED_RESOLVERS = False
+
 # formatted strings for the commands to be executed with the various resolvers
 # options are:
 # n = prevents dns lookups, p = include process, t = tcp only
@@ -46,8 +51,6 @@
 RESOLVER_FAILURE_TOLERANCE = 3      # number of subsequent failures before moving on to another resolver
 RESOLVER_SERIAL_FAILURE_MSG = "Querying connections with %s failed, trying %s"
 RESOLVER_FINAL_FAILURE_MSG = "All connection resolvers failed"
-
-# user customizable parameters
 CONFIG = {"queries.connections.minRate": 5, "log.connLookupFailed": log.INFO, "log.connLookupFailover": log.NOTICE, "log.connLookupAbandon": log.WARN, "log.connLookupRateGrowing": None}
 
 def loadConfig(config):
@@ -129,7 +132,7 @@
   for i in range(len(RESOLVERS)):
     resolver = RESOLVERS[i]
     if resolver.processName == processName and (not processPid or resolver.processPid == processPid):
-      if resolver._halt: haltedIndex = i
+      if resolver._halt and RECREATE_HALTED_RESOLVERS: haltedIndex = i
       else: return resolver
   
   # make a new resolver

Modified: arm/trunk/util/hostnames.py
===================================================================
--- arm/trunk/util/hostnames.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/util/hostnames.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -41,7 +41,6 @@
 DNS_ERROR_CODES = ("1(FORMERR)", "2(SERVFAIL)", "3(NXDOMAIN)", "4(NOTIMP)", "5(REFUSED)", "6(YXDOMAIN)",
                    "7(YXRRSET)", "8(NXRRSET)", "9(NOTAUTH)", "10(NOTZONE)", "16(BADVERS)")
 
-# user customizable parameters
 CONFIG = {"queries.hostnames.poolSize": 5, "queries.hostnames.useSocketModule": False, "cache.hostnames.size": 700000, "cache.hostnames.trimSize": 200000, "log.hostnameCacheTrimmed": log.INFO}
 
 def loadConfig(config):

Modified: arm/trunk/util/log.py
===================================================================
--- arm/trunk/util/log.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/util/log.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -17,9 +17,6 @@
 # provides thread safety for logging operations
 LOG_LOCK = RLock()
 
-# user customizable parameters (caching limits are per-runlevel)
-CONFIG = {"cache.armLog.size": 1000, "cache.armLog.trimSize": 200}
-
 # chronologically ordered records of events for each runlevel, stored as tuples
 # consisting of: (time, message)
 _backlog = dict([(level, []) for level in range(1, 6)])
@@ -27,6 +24,8 @@
 # mapping of runlevels to the listeners interested in receiving events from it
 _listeners = dict([(level, []) for level in range(1, 6)])
 
+CONFIG = {"cache.armLog.size": 1000, "cache.armLog.trimSize": 200}
+
 def loadConfig(config):
   config.update(CONFIG)
   

Modified: arm/trunk/util/panel.py
===================================================================
--- arm/trunk/util/panel.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/util/panel.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -5,7 +5,7 @@
 import curses
 from threading import RLock
 
-import uiTools
+import log, uiTools
 
 # global ui lock governing all panel instances (curses isn't thread save and 
 # concurrency bugs produce especially sinister glitches)
@@ -19,6 +19,11 @@
                "<h>": (_noOp, curses.A_STANDOUT)}
 for colorLabel in uiTools.COLOR_LIST: FORMAT_TAGS["<%s>" % colorLabel] = (uiTools.getColor, colorLabel)
 
+CONFIG = {"log.panelRecreated": log.DEBUG}
+
+def loadConfig(config):
+  config.update(CONFIG)
+
 class Panel():
   """
   Wrapper for curses subwindows. This hides most of the ugliness in common
@@ -33,12 +38,13 @@
   redraw().
   """
   
-  def __init__(self, parent, top, height=-1, width=-1):
+  def __init__(self, parent, name, top, height=-1, width=-1):
     """
     Creates a durable wrapper for a curses subwindow in the given parent.
     
     Arguments:
       parent - parent curses window
+      name   - identifier for the panel
       top    - positioning of top within parent
       height - maximum height of panel (uses all available space if -1)
       width  - maximum width of panel (uses all available space if -1)
@@ -49,6 +55,7 @@
     # might chose their height based on its parent's current width).
     
     self.parent = parent
+    self.panelName = name
     self.top = top
     self.height = height
     self.width = width
@@ -64,6 +71,13 @@
     
     self.maxY, self.maxX = -1, -1 # subwindow dimensions when last redrawn
   
+  def getName(self):
+    """
+    Provides panel's identifier.
+    """
+    
+    return self.name
+  
   def getParent(self):
     """
     Provides the parent used to create subwindows.
@@ -170,7 +184,7 @@
     
     pass
   
-  def redraw(self, forceRedraw=True, block=False):
+  def redraw(self, forceRedraw=False, block=False):
     """
     Clears display and redraws its content. This can skip redrawing content if
     able (ie, the subwindow's unchanged), instead just refreshing the display.
@@ -371,6 +385,7 @@
       manifests if the terminal's shrank then re-expanded. Displaced
       subwindows are never restored to their proper position, resulting in
       graphical glitches if we draw to them.
+    - The preferred size is smaller than the actual size (should shrink).
     
     This returns True if a new subwindow instance was created, False otherwise.
     """
@@ -384,6 +399,7 @@
       subwinMaxY, subwinMaxX = self.win.getmaxyx()
       recreate |= subwinMaxY < newHeight              # check for vertical growth
       recreate |= self.top > self.win.getparyx()[0]   # check for displacement
+      recreate |= subwinMaxX > newWidth or subwinMaxY > newHeight # shrinking
     
     # I'm not sure if recreating subwindows is some sort of memory leak but the
     # Python curses bindings seem to lack all of the following:
@@ -392,6 +408,11 @@
     # so this is the only option (besides removing subwindows entirely which 
     # would mean far more complicated code and no more selective refreshing)
     
-    if recreate: self.win = self.parent.subwin(newHeight, newWidth, self.top, 0)
+    if recreate:
+      self.win = self.parent.subwin(newHeight, newWidth, self.top, 0)
+      
+      # note: doing this log before setting win produces an infinite loop
+      msg = "recreating panel '%s' with the dimensions of %i/%i" % (self.panelName, newHeight, newWidth)
+      log.log(CONFIG["log.panelRecreated"], msg)
     return recreate
   

Modified: arm/trunk/util/sysTools.py
===================================================================
--- arm/trunk/util/sysTools.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/util/sysTools.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -16,7 +16,6 @@
 IS_FAILURES_CACHED = True           # caches both successful and failed results if true
 CALL_CACHE_LOCK = threading.RLock() # governs concurrent modifications of CALL_CACHE
 
-# user customizable parameters
 CONFIG = {"cache.sysCalls.size": 600, "log.sysCallMade": log.DEBUG, "log.sysCallCached": None, "log.sysCallFailed": log.INFO, "log.sysCallCacheGrowing": log.INFO}
 
 def loadConfig(config):

Modified: arm/trunk/util/torTools.py
===================================================================
--- arm/trunk/util/torTools.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/util/torTools.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -240,8 +240,7 @@
   if CONTROLLER == None: CONTROLLER = Controller()
   return CONTROLLER
 
-# TODO: sighup notification (and replacement in controller!)
-class Controller (TorCtl.PostEventListener):
+class Controller(TorCtl.PostEventListener):
   """
   TorCtl wrapper providing convenience functions, listener functionality for
   tor's state, and the capability for controller connections to be restarted

Modified: arm/trunk/util/uiTools.py
===================================================================
--- arm/trunk/util/uiTools.py	2010-06-23 15:53:17 UTC (rev 22548)
+++ arm/trunk/util/uiTools.py	2010-06-24 16:26:16 UTC (rev 22549)
@@ -28,7 +28,6 @@
 TIME_UNITS = [(86400.0, "d", " day"),                   (3600.0, "h", " hour"),
               (60.0, "m", " minute"),                   (1.0, "s", " second")]
 
-# user customizable parameters
 CONFIG = {"features.colorInterface": True, "log.cursesColorSupport": log.INFO}
 
 def loadConfig(config):



More information about the tor-commits mailing list