[or-cvs] r22447: {arm} Full rewrite of the header panel, providing: - lightweight r (in arm/trunk: init interface util)

Damian Johnson atagar1 at gmail.com
Mon May 31 22:41:08 UTC 2010


Author: atagar
Date: 2010-05-31 22:41:07 +0000 (Mon, 31 May 2010)
New Revision: 22447

Modified:
   arm/trunk/init/starter.py
   arm/trunk/interface/controller.py
   arm/trunk/interface/cpuMemMonitor.py
   arm/trunk/interface/headerPanel.py
   arm/trunk/util/__init__.py
   arm/trunk/util/connections.py
   arm/trunk/util/hostnames.py
   arm/trunk/util/panel.py
   arm/trunk/util/torTools.py
   arm/trunk/util/uiTools.py
Log:
Full rewrite of the header panel, providing:
  - lightweight redrawing (smarter caching and moved updating into a daemon thread)
  - more graceful handling of tiny displays
  - configurable update rate
fix: revised sleep pattern used for threads, greatly reducing the time it takes to quit



Modified: arm/trunk/init/starter.py
===================================================================
--- arm/trunk/init/starter.py	2010-05-31 15:17:34 UTC (rev 22446)
+++ arm/trunk/init/starter.py	2010-05-31 22:41:07 UTC (rev 22447)
@@ -19,7 +19,7 @@
 import util.torTools
 import TorCtl.TorUtil
 
-VERSION = "1.3.5"
+VERSION = "1.3.5_dev"
 LAST_MODIFIED = "Apr 8, 2010"
 
 DEFAULT_CONTROL_ADDR = "127.0.0.1"
@@ -139,7 +139,7 @@
   # sending problems to stdout if they arise
   util.torTools.INCORRECT_PASSWORD_MSG = "Controller password found in '%s' was incorrect" % configPath
   authPassword = config.get(AUTH_CFG, None)
-  conn = util.torTools.makeConn(controlAddr, controlPort, authPassword)
+  conn = util.torTools.connect(controlAddr, controlPort, authPassword)
   if conn == None: sys.exit(1)
   
   controller = util.torTools.getConn()

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2010-05-31 15:17:34 UTC (rev 22446)
+++ arm/trunk/interface/controller.py	2010-05-31 22:41:07 UTC (rev 22447)
@@ -332,7 +332,7 @@
   connections.RESOLVER_FINAL_FAILURE_MSG += " (connection related portions of the monitor won't function)"
   
   panels = {
-    "header": headerPanel.HeaderPanel(stdscr, conn, torPid),
+    "header": headerPanel.HeaderPanel(stdscr),
     "popup": Popup(stdscr, 9),
     "graph": graphPanel.GraphPanel(stdscr),
     "log": logPanel.LogMonitor(stdscr, conn, loggedEvents)}
@@ -353,7 +353,7 @@
   
   # statistical monitors for graph
   panels["graph"].addStats("bandwidth", bandwidthMonitor.BandwidthMonitor(conn))
-  panels["graph"].addStats("system resources", cpuMemMonitor.CpuMemMonitor(panels["header"]))
+  panels["graph"].addStats("system resources", cpuMemMonitor.CpuMemMonitor())
   if not isBlindMode: panels["graph"].addStats("connections", connCountMonitor.ConnCountMonitor(conn))
   panels["graph"].setStats("bandwidth")
   
@@ -374,6 +374,9 @@
   TorUtil.loglevel = "DEBUG"
   TorUtil.logfile = panels["log"]
   
+  # tells revised panels to run as daemons
+  panels["header"].start()
+  
   # warns if tor isn't updating descriptors
   try:
     if conn.get_option("FetchUselessDescriptors")[0][1] == "0" and conn.get_option("DirPort")[0][1] == "0":
@@ -400,7 +403,7 @@
     try:
       # if sighup received then reload related information
       if sighupTracker.isReset:
-        panels["header"]._updateParams(True)
+        #panels["header"]._updateParams(True)
         
         # other panels that use torrc data
         panels["conn"].resetOptions()
@@ -409,7 +412,7 @@
         
         # if bandwidth graph is being shown then height might have changed
         if panels["graph"].currentDisplay == "bandwidth":
-          panels["graph"].height = panels["graph"].stats["bandwidth"].height
+          panels["graph"].setHeight(panels["graph"].stats["bandwidth"].height)
         
         panels["torrc"].reset()
         sighupTracker.isReset = False
@@ -420,7 +423,7 @@
       # resilient in case of funky changes (such as resizing during popups)
       
       # hack to make sure header picks layout before using the dimensions below
-      panels["header"].getPreferredSize()
+      #panels["header"].getPreferredSize()
       
       startY = 0
       for panelKey in PAGE_S[:2]:
@@ -428,7 +431,7 @@
         panels[panelKey].setParent(stdscr)
         panels[panelKey].setWidth(-1)
         panels[panelKey].setTop(startY)
-        startY += panels[panelKey].height
+        startY += panels[panelKey].getHeight()
       
       panels["popup"].recreate(stdscr, 80, startY)
       
@@ -440,7 +443,7 @@
           panels[panelKey].setParent(stdscr)
           panels[panelKey].setWidth(-1)
           panels[panelKey].setTop(tmpStartY)
-          tmpStartY += panels[panelKey].height
+          tmpStartY += panels[panelKey].getHeight()
       
       # if it's been at least ten seconds since the last BW event Tor's probably done
       if not isUnresponsive and not panels["log"].controlPortClosed and panels["log"].getHeartbeat() >= 10:
@@ -508,6 +511,10 @@
         hostnames.stop()              # halts and joins on hostname worker thread pool
         if resolver: resolver.join()  # joins on halted resolver
         
+        # stops panel daemons
+        panels["header"].stop()
+        panels["header"].join()
+        
         conn.close() # joins on TorCtl event thread
         break
     elif key == curses.KEY_LEFT or key == curses.KEY_RIGHT:

Modified: arm/trunk/interface/cpuMemMonitor.py
===================================================================
--- arm/trunk/interface/cpuMemMonitor.py	2010-05-31 15:17:34 UTC (rev 22446)
+++ arm/trunk/interface/cpuMemMonitor.py	2010-05-31 22:41:07 UTC (rev 22447)
@@ -5,7 +5,7 @@
 import time
 from TorCtl import TorCtl
 
-from util import sysTools, uiTools
+from util import sysTools, torTools, uiTools
 import graphPanel
 
 class CpuMemMonitor(graphPanel.GraphStats, TorCtl.PostEventListener):
@@ -14,11 +14,10 @@
   headerPanel if recent enough (otherwise retrieved independently).
   """
   
-  def __init__(self, headerPanel):
+  def __init__(self):
     graphPanel.GraphStats.__init__(self)
     TorCtl.PostEventListener.__init__(self)
     graphPanel.GraphStats.initialize(self, "green", "cyan", 10)
-    self.headerPanel = headerPanel  # header panel, used to limit ps calls
   
   def bandwidth_event(self, event):
     # doesn't use events but this keeps it in sync with the bandwidth panel
@@ -27,25 +26,28 @@
     # 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)
-    if self.headerPanel.lastUpdate + 5 >= time.time():
-      # reuses ps results if recent enough
-      self._processEvent(float(self.headerPanel.vals["%cpu"]), float(self.headerPanel.vals["rss"]) / 1024.0)
+    # 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"), 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:
-      # cached results stale - requery ps
-      sampling = []
-      psCall = None
-      if self.headerPanel.vals["pid"]:
-        psCall = sysTools.call("ps -p %s -o %s" % (self.headerPanel.vals["pid"], "%cpu,rss"), 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)
+      self._processEvent(float(sampling[0]), float(sampling[1]) / 1024.0)
   
   def getTitle(self, width):
     return "System Resources:"

Modified: arm/trunk/interface/headerPanel.py
===================================================================
--- arm/trunk/interface/headerPanel.py	2010-05-31 15:17:34 UTC (rev 22446)
+++ arm/trunk/interface/headerPanel.py	2010-05-31 22:41:07 UTC (rev 22447)
@@ -1,272 +1,340 @@
-#!/usr/bin/env python
-# summaryPanel.py -- Static system and Tor related information.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+"""
+Top panel for every page, containing basic system and tor related information.
+If there's room available then this expands to present its information in two
+columns, otherwise it's laid out as follows:
+  arm - <hostname> (<os> <sys/version>)         Tor <tor/version> (<new, old, recommended, etc>)
+  <nickname> - <address>:<orPort>, [Dir Port: <dirPort>, ]Control Port (<open, password, cookie>): <controlPort>
+  cpu: <cpu%> mem: <mem> (<mem%>) uid: <uid> uptime: <upmin>:<upsec>
+  fingerprint: <fingerprint>
 
+Example:
+  arm - odin (Linux 2.6.24-24-generic)         Tor 0.2.1.19 (recommended)
+  odin - 76.104.132.98:9001, Dir Port: 9030, Control Port (cookie): 9051
+  cpu: 14.6%    mem: 42 MB (4.2%)    pid: 20060   uptime: 48:27
+  fingerprint: BDAD31F6F318E0413833E8EBDA956F76E4D66788
+"""
+
 import os
 import time
-import socket
-from TorCtl import TorCtl
+import threading
 
-from util import panel, sysTools, uiTools
+from util import conf, log, panel, sysTools, torTools, uiTools
 
+# seconds between querying information
+DEFAULT_UPDATE_RATE = 5
+UPDATE_RATE_CFG = "updateRate.header"
+
 # minimum width for which panel attempts to double up contents (two columns to
 # better use screen real estate)
-MIN_DUAL_ROW_WIDTH = 140
+MIN_DUAL_COL_WIDTH = 141
 
 FLAG_COLORS = {"Authority": "white",  "BadExit": "red",     "BadDirectory": "red",    "Exit": "cyan",
                "Fast": "yellow",      "Guard": "green",     "HSDir": "magenta",       "Named": "blue",
                "Stable": "blue",      "Running": "yellow",  "Unnamed": "magenta",     "Valid": "green",
                "V2Dir": "cyan",       "V3Dir": "white"}
 
-VERSION_STATUS_COLORS = {"new": "blue",      "new in series": "blue",  "recommended": "green",  "old": "red",
-                         "obsolete": "red",  "unrecommended": "red",   "unknown": "cyan"}
+VERSION_STATUS_COLORS = {"new": "blue", "new in series": "blue", "obsolete": "red", "recommended": "green",  
+                         "old": "red",  "unrecommended": "red",  "unknown": "cyan"}
 
-class HeaderPanel(panel.Panel):
+class HeaderPanel(panel.Panel, threading.Thread):
   """
-  Draws top area containing static information.
+  Top area contenting tor settings and system information. Stats are stored in
+  the vals mapping, keys including:
+    tor/ version, versionStatus, nickname, orPort, dirPort, controlPort,
+         exitPolicy, isAuthPassword (bool), isAuthCookie (bool)
+         *address, *fingerprint, *flags
+    sys/ hostname, os, version
+    ps/  *%cpu, *rss, *%mem, pid, *etime
   
-  arm - <System Name> (<OS> <Version>)         Tor <Tor Version>
-  <Relay Nickname> - <IP Addr>:<ORPort>, [Dir Port: <DirPort>, ]Control Port (<open, password, cookie>): <ControlPort>
-  cpu: <cpu%> mem: <mem> (<mem%>) uid: <uid> uptime: <upmin>:<upsec>
-  fingerprint: <Fingerprint>
-  
-  Example:
-  arm - odin (Linux 2.6.24-24-generic)         Tor 0.2.1.15-rc
-  odin - 76.104.132.98:9001, Dir Port: 9030, Control Port (cookie): 9051
-  cpu: 14.6%    mem: 42 MB (4.2%)    pid: 20060   uptime: 48:27
-  fingerprint: BDAD31F6F318E0413833E8EBDA956F76E4D66788
+  * volatile parameter that'll be reset on each update
   """
   
-  def __init__(self, stdscr, conn, torPid):
-    panel.Panel.__init__(self, stdscr, 0, 6)
-    self.vals = {"pid": torPid}     # mapping of information to be presented
-    self.conn = conn                # Tor control port connection
-    self.isPaused = False
-    self.isWide = False             # doubles up parameters to shorten section if room's available
-    self.rightParamX = 0            # offset used for doubled up parameters
-    self.lastUpdate = -1            # time last stats was retrived
-    self._updateParams()
-    self.getPreferredSize() # hack to force properly initialize size (when using wide version)
+  def __init__(self, stdscr):
+    panel.Panel.__init__(self, stdscr, 0)
+    threading.Thread.__init__(self)
+    self.setDaemon(True)
+    
+    # seconds between querying updates
+    try:
+      self._updateRate = int(conf.getConfig("arm").get(UPDATE_RATE_CFG, DEFAULT_UPDATE_RATE))
+    except ValueError:
+      # value wasn't an integer
+      log.log(log.WARN, "Config: %s is expected to be an integer (defaulting to %i)" % (UPDATE_RATE_CFG, DEFAULT_UPDATE_RATE))
+      self._updateRate = DEFAULT_UPDATE_RATE
+    
+    self._lastUpdate = -1       # time the content was last revised
+    self._isLastDrawWide = False
+    self._isChanged = False     # new stats to be drawn if true
+    self._isPaused = False      # prevents updates if true
+    self._halt = False          # terminates thread if true
+    self._cond = threading.Condition()  # used for pausing the thread
+    
+    self.vals = {}
+    self.valsLock = threading.RLock()
+    self._update(True)
+    
+    # listens for tor reload (sighup) events
+    torTools.getConn().addStatusListener(self.resetListener)
   
-  def getPreferredSize(self):
-    # width partially determines height (panel has two layouts)
-    panelHeight, panelWidth = panel.Panel.getPreferredSize(self)
-    self.isWide = panelWidth >= MIN_DUAL_ROW_WIDTH
-    self.rightParamX = max(panelWidth / 2, 75) if self.isWide else 0
-    self.setHeight(4 if self.isWide else 6)
-    return panel.Panel.getPreferredSize(self)
+  def getHeight(self):
+    """
+    Provides the height of the content, which is dynamically determined by the
+    panel's maximum width.
+    """
+    
+    isWide = self.getParent().getmaxyx()[1] >= MIN_DUAL_COL_WIDTH
+    return 4 if isWide else 6
   
   def draw(self, subwindow, width, height):
-    if not self.isPaused: self._updateParams()
+    self.valsLock.acquire()
+    isWide = width + 1 >= MIN_DUAL_COL_WIDTH
     
-    # TODO: remove after a few revisions if this issue can't be reproduced
-    #   (seemed to be a freak ui problem...)
+    # space available for content
+    if isWide:
+      leftWidth = max(width / 2, 77)
+      rightWidth = width - leftWidth
+    else: leftWidth = rightWidth = width
     
-    # extra erase/refresh is needed to avoid internal caching screwing up and
-    # refusing to redisplay content in the case of graphical glitches - probably
-    # an obscure curses bug...
-    #self.win.erase()
-    #self.win.refresh()
+    # Line 1 / Line 1 Left (system and tor version information)
+    sysNameLabel = "arm - %s" % self.vals["sys/hostname"]
+    contentSpace = min(leftWidth, 40)
     
-    #self.clear()
-    
-    # Line 1 (system and tor version information)
-    systemNameLabel = "arm - %s " % self.vals["sys-name"]
-    systemVersionLabel = "%s %s" % (self.vals["sys-os"], self.vals["sys-version"])
-    
-    # wraps systemVersionLabel in parentheses and truncates if too long
-    versionLabelMaxWidth = 40 - len(systemNameLabel)
-    if len(systemNameLabel) > 40:
-      # we only have room for the system name label
-      systemNameLabel = systemNameLabel[:39] + "..."
-      systemVersionLabel = ""
-    elif len(systemVersionLabel) > versionLabelMaxWidth:
-      # not enough room to show full version
-      systemVersionLabel = "(%s...)" % systemVersionLabel[:versionLabelMaxWidth - 3].strip()
+    if len(sysNameLabel) + 10 <= contentSpace:
+      sysTypeLabel = "%s %s" % (self.vals["sys/os"], self.vals["sys/version"])
+      sysTypeLabel = uiTools.cropStr(sysTypeLabel, contentSpace - len(sysNameLabel) - 3, 4)
+      self.addstr(0, 0, "%s (%s)" % (sysNameLabel, sysTypeLabel))
     else:
-      # enough room for everything
-      systemVersionLabel = "(%s)" % systemVersionLabel
+      self.addstr(0, 0, uiTools.cropStr(sysNameLabel, contentSpace))
     
-    self.addstr(0, 0, "%s%s" % (systemNameLabel, systemVersionLabel))
+    contentSpace = leftWidth - 43
+    if 7 + len(self.vals["tor/version"]) + len(self.vals["tor/versionStatus"]) <= contentSpace:
+      versionColor = VERSION_STATUS_COLORS[self.vals["tor/versionStatus"]] if \
+          self.vals["tor/versionStatus"] in VERSION_STATUS_COLORS else "white"
+      versionStatusMsg = "<%s>%s</%s>" % (versionColor, self.vals["tor/versionStatus"], versionColor)
+      self.addfstr(0, 43, "Tor %s (%s)" % (self.vals["tor/version"], versionStatusMsg))
+    elif 11 <= contentSpace:
+      self.addstr(0, 43, uiTools.cropStr("Tor %s" % self.vals["tor/version"], contentSpace, 4))
     
-    versionStatus = self.vals["status/version/current"]
-    versionColor = VERSION_STATUS_COLORS[versionStatus] if versionStatus in VERSION_STATUS_COLORS else "white"
+    # Line 2 / Line 2 Left (tor ip/port information)
+    entry = ""
+    dirPortLabel = ", Dir Port: %s" % self.vals["tor/dirPort"] if self.vals["tor/dirPort"] != "0" else ""
+    for label in (self.vals["tor/nickname"], " - " + self.vals["tor/address"], ":" + self.vals["tor/orPort"], dirPortLabel):
+      if len(entry) + len(label) <= leftWidth: entry += label
+      else: break
     
-    # truncates torVersionLabel if too long
-    torVersionLabel = self.vals["version"]
-    versionLabelMaxWidth =  (self.rightParamX if self.isWide else width) - 51 - len(versionStatus)
-    if len(torVersionLabel) > versionLabelMaxWidth:
-      torVersionLabel = torVersionLabel[:versionLabelMaxWidth - 1].strip() + "-"
+    if self.vals["tor/isAuthPassword"]: authType = "password"
+    elif self.vals["tor/isAuthCookie"]: authType = "cookie"
+    else: authType = "open"
     
-    self.addfstr(0, 43, "Tor %s (<%s>%s</%s>)" % (torVersionLabel, versionColor, versionStatus, versionColor))
+    if len(entry) + 19 + len(self.vals["tor/controlPort"]) + len(authType) <= leftWidth:
+      authColor = "red" if authType == "open" else "green"
+      authLabel = "<%s>%s</%s>" % (authColor, authType, authColor)
+      self.addfstr(1, 0, "%s, Control Port (%s): %s" % (entry, authLabel, self.vals["tor/controlPort"]))
+    elif len(entry) + 16 + len(self.vals["tor/controlPort"]) <= leftWidth:
+      self.addstr(1, 0, "%s, Control Port: %s" % (entry, self.vals["tor/controlPort"]))
+    else: self.addstr(1, 0, entry)
     
-    # Line 2 (authentication label red if open, green if credentials required)
-    dirPortLabel = "Dir Port: %s, " % self.vals["DirPort"] if self.vals["DirPort"] != "0" else ""
+    # Line 3 / Line 1 Right (system usage info)
+    y, x = (0, leftWidth) if isWide else (2, 0)
+    if self.vals["ps/rss"] != "0": memoryLabel = uiTools.getSizeLabel(int(self.vals["ps/rss"]) * 1024)
+    else: memoryLabel = "0"
     
-    if self.vals["IsPasswordAuthSet"]: controlPortAuthLabel = "password"
-    elif self.vals["IsCookieAuthSet"]: controlPortAuthLabel = "cookie"
-    else: controlPortAuthLabel = "open"
-    controlPortAuthColor = "red" if controlPortAuthLabel == "open" else "green"
+    sysFields = ((0, "cpu: %s%%" % self.vals["ps/%cpu"]),
+                 (13, "mem: %s (%s%%)" % (memoryLabel, self.vals["ps/%mem"])),
+                 (34, "pid: %s" % (self.vals["ps/pid"] if self.vals["ps/etime"] else "")),
+                 (47, "uptime: %s" % self.vals["ps/etime"]))
     
-    labelStart = "%s - %s:%s, %sControl Port (" % (self.vals["Nickname"], self.vals["address"], self.vals["ORPort"], dirPortLabel)
-    self.addfstr(1, 0, "%s<%s>%s</%s>): %s" % (labelStart, controlPortAuthColor, controlPortAuthLabel, controlPortAuthColor, self.vals["ControlPort"]))
+    for (start, label) in sysFields:
+      if start + len(label) <= rightWidth: self.addstr(y, x + start, label)
+      else: break
     
-    # Line 3 (system usage info) - line 1 right if wide
-    y, x = (0, self.rightParamX) if self.isWide else (2, 0)
-    self.addstr(y, x, "cpu: %s%%" % self.vals["%cpu"])
-    self.addstr(y, x + 13, "mem: %s (%s%%)" % (uiTools.getSizeLabel(int(self.vals["rss"]) * 1024), self.vals["%mem"]))
-    self.addstr(y, x + 34, "pid: %s" % (self.vals["pid"] if self.vals["etime"] else ""))
-    self.addstr(y, x + 47, "uptime: %s" % self.vals["etime"])
+    # Line 4 / Line 2 Right (fingerprint)
+    y, x = (1, leftWidth) if isWide else (3, 0)
+    self.addstr(y, x, "fingerprint: %s" % self.vals["tor/fingerprint"])
     
-    # Line 4 (fingerprint) - line 2 right if wide
-    y, x = (1, self.rightParamX) if self.isWide else (3, 0)
-    self.addstr(y, x, "fingerprint: %s" % self.vals["fingerprint"])
-    
-    # Line 5 (flags) - line 3 left if wide
+    # Line 5 / Line 3 Left (flags)
     flagLine = "flags: "
-    for flag in self.vals["flags"]:
+    for flag in self.vals["tor/flags"]:
       flagColor = FLAG_COLORS[flag] if flag in FLAG_COLORS.keys() else "white"
       flagLine += "<b><%s>%s</%s></b>, " % (flagColor, flag, flagColor)
     
-    if len(self.vals["flags"]) > 0: flagLine = flagLine[:-2]
-    self.addfstr(2 if self.isWide else 4, 0, flagLine)
+    if len(self.vals["tor/flags"]) > 0: flagLine = flagLine[:-2]
+    self.addfstr(2 if isWide else 4, 0, flagLine)
     
-    # Line 3 right (exit policy) - only present if wide
-    if self.isWide:
-      exitPolicy = self.vals["ExitPolicy"]
+    # Undisplayed / Line 3 Right (exit policy)
+    if isWide:
+      exitPolicy = self.vals["tor/exitPolicy"]
       
       # adds note when default exit policy is appended
-      # TODO: the following catch-all policies arne't quite exhaustive
       if exitPolicy == None: exitPolicy = "<default>"
-      elif not (exitPolicy.endswith("accept *:*") or exitPolicy.endswith("accept *")) and not (exitPolicy.endswith("reject *:*") or exitPolicy.endswith("reject *")):
-        exitPolicy += ", <default>"
+      elif not exitPolicy.endswith((" *:*", " *")): exitPolicy += ", <default>"
       
+      # color codes accepts to be green, rejects to be red, and default marker to be cyan
+      isSimple = len(exitPolicy) > rightWidth - 13
       policies = exitPolicy.split(", ")
-      
-      # color codes accepts to be green, rejects to be red, and default marker to be cyan
-      # TODO: instead base this on if there's space available for the full verbose version
-      isSimple = len(policies) <= 2 # if policy is short then it's kept verbose, otherwise 'accept' and 'reject' keywords removed
       for i in range(len(policies)):
         policy = policies[i].strip()
-        displayedPolicy = policy if isSimple else policy.replace("accept", "").replace("reject", "").strip()
+        displayedPolicy = policy.replace("accept", "").replace("reject", "").strip() if isSimple else policy
         if policy.startswith("accept"): policy = "<green><b>%s</b></green>" % displayedPolicy
         elif policy.startswith("reject"): policy = "<red><b>%s</b></red>" % displayedPolicy
         elif policy.startswith("<default>"): policy = "<cyan><b>%s</b></cyan>" % displayedPolicy
         policies[i] = policy
-      exitPolicy = ", ".join(policies)
       
-      self.addfstr(2, self.rightParamX, "exit policy: %s" % exitPolicy)
+      self.addfstr(2, leftWidth, "exit policy: %s" % ", ".join(policies))
+    
+    self._isLastDrawWide = isWide
+    self._isChanged = False
+    self.valsLock.release()
   
+  def redraw(self, forceRedraw=False, block=False):
+    # determines if the content needs to be redrawn or not
+    isWide = self.getParent().getmaxyx()[1] >= MIN_DUAL_COL_WIDTH
+    panel.Panel.redraw(self, forceRedraw or self._isChanged or isWide != self._isLastDrawWide, block)
+  
   def setPaused(self, isPause):
     """
     If true, prevents updates from being presented.
     """
     
-    self.isPaused = isPause
+    self._isPaused = isPause
   
-  def _updateParams(self, forceReload = False):
+  def run(self):
     """
-    Updates mapping of static Tor settings and system information to their
-    corresponding string values. Keys include:
-    info - version, *address, *fingerprint, *flags, status/version/current
-    sys - sys-name, sys-os, sys-version
-    ps - *%cpu, *rss, *%mem, *pid, *etime
-    config - Nickname, ORPort, DirPort, ControlPort, ExitPolicy
-    config booleans - IsPasswordAuthSet, IsCookieAuthSet, IsAccountingEnabled
+    Keeps stats updated, querying new information at a set rate.
+    """
     
-    * volatile parameter that'll be reset (otherwise won't be checked if
-    already set)
+    while not self._halt:
+      timeSinceReset = time.time() - self._lastUpdate
+      
+      if self._isPaused or timeSinceReset < self._updateRate:
+        sleepTime = max(0.5, self._updateRate - timeSinceReset)
+        self._cond.acquire()
+        if not self._halt: self._cond.wait(sleepTime)
+        self._cond.release()
+      else:
+        self._update()
+        self.redraw()
+  
+  def stop(self):
     """
+    Halts further resolutions and terminates the thread.
+    """
     
-    infoFields = ["address", "fingerprint"] # keys for which get_info will be called
-    if len(self.vals) <= 1 or forceReload:
-      lookupFailed = False
+    self._cond.acquire()
+    self._halt = True
+    self._cond.notifyAll()
+    self._cond.release()
+  
+  def resetListener(self, conn, eventType):
+    """
+    Updates static parameters on tor reload (sighup) events.
+    
+    Arguments:
+      conn      - tor controller
+      eventType - type of event detected
+    """
+    
+    if eventType == torTools.TOR_RESET:
+      self._update(True)
+  
+  def _update(self, setStatic=False):
+    """
+    Updates stats in the vals mapping. By default this just revises volatile
+    attributes.
+    
+    Arguments:
+      setStatic - resets all parameters, including relatively static values
+    """
+    
+    self.valsLock.acquire()
+    conn = torTools.getConn()
+    
+    if setStatic:
+      # version is truncated to first part, for instance:
+      # 0.2.2.13-alpha (git-feb8c1b5f67f2c6f) -> 0.2.2.13-alpha
+      self.vals["tor/version"] = conn.getInfo("version", "Unknown").split()[0]
+      self.vals["tor/versionStatus"] = conn.getInfo("status/version/current", "Unknown")
+      self.vals["tor/nickname"] = conn.getOption("Nickname", "")
+      self.vals["tor/orPort"] = conn.getOption("ORPort", "")
+      self.vals["tor/dirPort"] = conn.getOption("DirPort", "0")
+      self.vals["tor/controlPort"] = conn.getOption("ControlPort", "")
+      self.vals["tor/isAuthPassword"] = conn.getOption("HashedControlPassword") != None
+      self.vals["tor/isAuthCookie"] = conn.getOption("CookieAuthentication") == "1"
       
-      # first call (only contasns 'pid' mapping) - retrieve static params
-      infoFields += ["version", "status/version/current"]
+      # fetch exit policy (might span over multiple lines)
+      policyEntries = []
+      for exitPolicy in conn.getOption("ExitPolicy", [], True):
+        policyEntries += [policy.strip() for policy in exitPolicy[1].split(",")]
+      self.vals["tor/exitPolicy"] = ", ".join(policyEntries)
       
-      # populates with some basic system information
+      # system information
       unameVals = os.uname()
-      self.vals["sys-name"] = unameVals[1]
-      self.vals["sys-os"] = unameVals[0]
-      self.vals["sys-version"] = unameVals[2]
+      self.vals["sys/hostname"] = unameVals[1]
+      self.vals["sys/os"] = unameVals[0]
+      self.vals["sys/version"] = unameVals[2]
       
-      try:
-        # parameters from the user's torrc
-        configFields = ["Nickname", "ORPort", "DirPort", "ControlPort"]
-        self.vals.update(dict([(key, self.conn.get_option(key)[0][1]) for key in configFields]))
-        
-        # fetch exit policy (might span over multiple lines)
-        exitPolicyEntries = []
-        for (key, value) in self.conn.get_option("ExitPolicy"):
-          if value: exitPolicyEntries.append(value)
-        
-        self.vals["ExitPolicy"] = ", ".join(exitPolicyEntries)
-        
-        # simply keeps booleans for if authentication info is set
-        self.vals["IsPasswordAuthSet"] = not self.conn.get_option("HashedControlPassword")[0][1] == None
-        self.vals["IsCookieAuthSet"] = self.conn.get_option("CookieAuthentication")[0][1] == "1"
-        self.vals["IsAccountingEnabled"] = self.conn.get_info('accounting/enabled')['accounting/enabled'] == "1"
-      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): lookupFailed = True
+      pid = conn.getPid()
+      self.vals["ps/pid"] = pid if pid else ""
       
-      if lookupFailed:
-        # tor connection closed or gave error - keep old values if available, otherwise set to empty string / false
-        for field in configFields:
-          if field not in self.vals: self.vals[field] = ""
-        
-        for field in ["IsPasswordAuthSet", "IsCookieAuthSet", "IsAccountingEnabled"]:
-          if field not in self.vals: self.vals[field] = False
-      
-    # gets parameters that throw errors if unavailable
-    for param in infoFields:
-      try: self.vals.update(self.conn.get_info(param))
-      except TorCtl.ErrorReply: self.vals[param] = "Unknown"
-      except (TorCtl.TorCtlClosed, socket.error):
-        # Tor shut down or crashed - keep last known values
-        if not param in self.vals.keys() or not self.vals[param]: self.vals[param] = "Unknown"
+      # reverts volatile parameters to defaults
+      self.vals["tor/address"] = "Unknown"
+      self.vals["tor/fingerprint"] = "Unknown"
+      self.vals["tor/flags"] = []
+      self.vals["ps/%cpu"] = "0"
+      self.vals["ps/rss"] = "0"
+      self.vals["ps/%mem"] = "0"
+      self.vals["ps/etime"] = ""
     
-    # if ORListenAddress is set overwrites 'address' (and possibly ORPort)
-    try:
-      listenAddr = self.conn.get_option("ORListenAddress")[0][1]
-      if listenAddr:
-        if ":" in listenAddr:
-          # both ip and port overwritten
-          self.vals["address"] = listenAddr[:listenAddr.find(":")]
-          self.vals["ORPort"] = listenAddr[listenAddr.find(":") + 1:]
-        else:
-          self.vals["address"] = listenAddr
-    except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
+    # sets volatile parameters
+    volatile = {}
+    volatile["tor/address"] = conn.getInfo("address", self.vals["tor/address"])
+    volatile["tor/fingerprint"] = conn.getInfo("fingerprint", self.vals["tor/fingerprint"])
     
-    # flags held by relay
-    self.vals["flags"] = []
-    if self.vals["fingerprint"] != "Unknown":
-      try:
-        nsCall = self.conn.get_network_status("id/%s" % self.vals["fingerprint"])
-        if nsCall: self.vals["flags"] = nsCall[0].flags
-        else: raise TorCtl.ErrorReply # network consensus couldn't be fetched
-      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
+    # overwrite address if ORListenAddress is set (and possibly orPort too)
+    listenAddr = conn.getOption("ORListenAddress")
+    if listenAddr:
+      if ":" in listenAddr:
+        # both ip and port overwritten
+        volatile["address"] = listenAddr[:listenAddr.find(":")]
+        volatile["orPort"] = listenAddr[listenAddr.find(":") + 1:]
+      else:
+        volatile["address"] = listenAddr
     
+    # sets flags
+    if self.vals["tor/fingerprint"] != "Unknown":
+      # network status contains a couple of lines, looking like:
+      # r caerSidi p1aag7VwarGxqctS7/fS0y5FU+s 9On1TRGCEpljszPpJR1hKqlzaY8 2010-05-26 09:26:06 76.104.132.98 9001 0
+      # s Fast HSDir Named Running Stable Valid
+      nsResults = conn.getInfo("ns/id/%s" % self.vals["tor/fingerprint"], "").split("\n")
+      if len(nsResults) >= 2: volatile["tor/flags"] = nsResults[1][2:].split()
+    
+    # ps derived stats
     psParams = ["%cpu", "rss", "%mem", "etime"]
-    sampling = []
-    if self.vals["pid"]:
-      # ps call provides header followed by params for tor
-      # this caches the results for five seconds and suppress any exceptions
-      # results are expected to look something like:
+    if self.vals["ps/pid"]:
+      # if call fails then everything except etime are zeroed out (most likely
+      # tor's no longer running)
+      volatile["ps/%cpu"] = "0"
+      volatile["ps/rss"] = "0"
+      volatile["ps/%mem"] = "0"
+      
+      # the ps call formats results as:
       # %CPU   RSS %MEM     ELAPSED
       # 0.3 14096  1.3       29:51
-      psCall = sysTools.call("ps -p %s -o %s" % (self.vals["pid"], ",".join(psParams)), 5, True)
-      if psCall and len(psCall) >= 2: sampling = psCall[1].strip().split()
-    
-    if len(sampling) < len(psParams):
-      # pid is unknown, ps call failed, or returned no tor instance - blank information except runtime
-      if "etime" in self.vals: sampling = [""] * (len(psParams) - 1) + [self.vals["etime"]]
-      else: sampling = [""] * len(psParams)
+      psCall = sysTools.call("ps -p %s -o %s" % (self.vals["ps/pid"], ",".join(psParams)), self._updateRate, True)
       
-      # %cpu, rss, and %mem are better zeroed out
-      for i in range(3): sampling[i] = "0"
+      if psCall and len(psCall) >= 2:
+        stats = psCall[1].strip().split()
+        
+        if len(stats) == len(psParams):
+          for i in range(len(psParams)):
+            volatile["ps/" + psParams[i]] = stats[i]
     
-    for i in range(len(psParams)):
-      self.vals[psParams[i]] = sampling[i]
+    # checks if any changes have been made and merges volatile into vals
+    self._isChanged |= setStatic
+    for key, val in volatile.items():
+      self._isChanged |= self.vals[key] != val
+      self.vals[key] = val
     
-    self.lastUpdate = time.time()
+    self._lastUpdate = time.time()
+    self.valsLock.release()
 

Modified: arm/trunk/util/__init__.py
===================================================================
--- arm/trunk/util/__init__.py	2010-05-31 15:17:34 UTC (rev 22446)
+++ arm/trunk/util/__init__.py	2010-05-31 22:41:07 UTC (rev 22447)
@@ -4,5 +4,5 @@
 and safely working with curses (hiding some of the gory details).
 """
 
-__all__ = ["connections", "hostnames", "log", "panel", "sysTools", "torTools", "uiTools"]
+__all__ = ["conf", "connections", "hostnames", "log", "panel", "sysTools", "torTools", "uiTools"]
 

Modified: arm/trunk/util/connections.py
===================================================================
--- arm/trunk/util/connections.py	2010-05-31 15:17:34 UTC (rev 22446)
+++ arm/trunk/util/connections.py	2010-05-31 22:41:07 UTC (rev 22447)
@@ -7,7 +7,7 @@
 - lsof      lsof -nPi | grep "<process>\s*<pid>.*(ESTABLISHED)"
 
 all queries dump its stderr (directing it to /dev/null). Unfortunately FreeBSD
-lacks support for the needed netstat flags, and has a completely different
+lacks support for the needed netstat flags and has a completely different
 program for 'ss', so this is quite likely to fail there.
 """
 
@@ -139,7 +139,7 @@
 
 if __name__ == '__main__':
   # quick method for testing connection resolution
-  userInput = raw_input("Enter query (RESOLVER PROCESS_NAME [PID]: ").split()
+  userInput = raw_input("Enter query (<ss, netstat, lsof> PROCESS_NAME [PID]): ").split()
   
   # checks if there's enough arguments
   if len(userInput) == 0: sys.exit(0)
@@ -243,15 +243,22 @@
     self._connections = []        # connection cache (latest results)
     self._isPaused = False
     self._halt = False            # terminates thread if true
+    self._cond = threading.Condition()  # used for pausing the thread
     self._subsiquentFailures = 0  # number of failed resolutions with the default in a row
     self._resolverBlacklist = []  # resolvers that have failed to resolve
   
   def run(self):
     while not self._halt:
       minWait = self.resolveRate if self.resolveRate else self.defaultRate
+      timeSinceReset = time.time() - self.lastLookup
       
-      if self._isPaused or time.time() - self.lastLookup < minWait:
-        time.sleep(RESOLVER_SLEEP_INTERVAL)
+      if self._isPaused or timeSinceReset < minWait:
+        sleepTime = max(0.2, minWait - timeSinceReset)
+        
+        self._cond.acquire()
+        if not self._halt: self._cond.wait(sleepTime)
+        self._cond.release()
+        
         continue # done waiting, try again
       
       isDefault = self.overwriteResolver == None
@@ -331,5 +338,8 @@
     Halts further resolutions and terminates the thread.
     """
     
+    self._cond.acquire()
     self._halt = True
+    self._cond.notifyAll()
+    self._cond.release()
 

Modified: arm/trunk/util/hostnames.py
===================================================================
--- arm/trunk/util/hostnames.py	2010-05-31 15:17:34 UTC (rev 22446)
+++ arm/trunk/util/hostnames.py	2010-05-31 22:41:07 UTC (rev 22447)
@@ -75,7 +75,7 @@
     resolverRef, RESOLVER = RESOLVER, None
     
     # joins on its worker thread pool
-    resolverRef.halt = True
+    resolverRef.stop()
     for t in resolverRef.threadPool: t.join()
   RESOLVER_LOCK.release()
 
@@ -251,6 +251,7 @@
     self.totalResolves = 0                # counter for the total number of addresses queried to be resolved
     self.isPaused = False                 # prevents further resolutions if true
     self.halt = False                     # if true, tells workers to stop
+    self.cond = threading.Condition()     # used for pausing threads
     
     # Determines if resolutions are made using os 'host' calls or python's
     # 'socket.gethostbyaddr'. The following checks if the system has the
@@ -312,6 +313,16 @@
     
     return None # timeout reached without resolution
   
+  def stop(self):
+    """
+    Halts further resolutions and terminates the thread.
+    """
+    
+    self.cond.acquire()
+    self.halt = True
+    self.cond.notifyAll()
+    self.cond.release()
+  
   def _workerLoop(self):
     """
     Simple producer-consumer loop followed by worker threads. This takes
@@ -322,13 +333,21 @@
     
     while not self.halt:
       # if resolver is paused then put a hold on further resolutions
-      while self.isPaused and not self.halt: time.sleep(0.25)
-      if self.halt: break
+      if self.isPaused:
+        self.cond.acquire()
+        if not self.halt: self.cond.wait(1)
+        self.cond.release()
+        continue
       
       # snags next available ip, timeout is because queue can't be woken up
       # when 'halt' is set
-      try: ipAddr = self.unresolvedQueue.get(True, 0.25)
-      except Queue.Empty: continue
+      try: ipAddr = self.unresolvedQueue.get_nowait()
+      except Queue.Empty:
+        # no elements ready, wait a little while and try again
+        self.cond.acquire()
+        if not self.halt: self.cond.wait(1)
+        self.cond.release()
+        continue
       if self.halt: break
       
       try:

Modified: arm/trunk/util/panel.py
===================================================================
--- arm/trunk/util/panel.py	2010-05-31 15:17:34 UTC (rev 22446)
+++ arm/trunk/util/panel.py	2010-05-31 22:41:07 UTC (rev 22447)
@@ -148,15 +148,18 @@
     """
     
     newHeight, newWidth = self.parent.getmaxyx()
+    setHeight, setWidth = self.getHeight(), self.getWidth()
     newHeight = max(0, newHeight - self.top)
-    if self.height != -1: newHeight = min(self.height, newHeight)
-    if self.width != -1: newWidth = min(self.width, newWidth)
+    if setHeight != -1: newHeight = min(newHeight, setHeight)
+    if setWidth != -1: newWidth = min(newWidth, setWidth)
     return (newHeight, newWidth)
   
   def draw(self, subwindow, width, height):
     """
     Draws display's content. This is meant to be overwritten by 
-    implementations and not called directly (use redraw() instead).
+    implementations and not called directly (use redraw() instead). The
+    dimensions provided are the drawable dimensions, which in terms of width is
+    a column less than the actual space.
     
     Arguments:
       sudwindow - panel's current subwindow instance, providing raw access to
@@ -167,15 +170,16 @@
     
     pass
   
-  def redraw(self, refresh=False, block=False):
+  def redraw(self, forceRedraw=True, block=False):
     """
-    Clears display and redraws its content.
+    Clears display and redraws its content. This can skip redrawing content if
+    able (ie, the subwindow's unchanged), instead just refreshing the display.
     
     Arguments:
-      refresh - skips redrawing content if able (ie, the subwindow's 
-                unchanged), instead just refreshing the display
-      block   - if drawing concurrently with other panels this determines if
-                the request is willing to wait its turn or should be abandoned
+      forceRedraw - forces the content to be cleared and redrawn if true
+      block       - if drawing concurrently with other panels this determines
+                    if the request is willing to wait its turn or should be
+                    abandoned
     """
     
     # if the panel's completely outside its parent then this is a no-op
@@ -195,13 +199,14 @@
     
     subwinMaxY, subwinMaxX = self.win.getmaxyx()
     if isNewWindow or subwinMaxY != self.maxY or subwinMaxX != self.maxX:
-      refresh = False
+      forceRedraw = True
     
     self.maxY, self.maxX = subwinMaxY, subwinMaxX
     if not CURSES_LOCK.acquire(block): return
     try:
-      self.win.erase() # clears any old contents
-      if not refresh: self.draw(self.win, self.maxX, self.maxY)
+      if forceRedraw:
+        self.win.erase() # clears any old contents
+        self.draw(self.win, self.maxX - 1, self.maxY)
       self.win.refresh()
     finally:
       CURSES_LOCK.release()

Modified: arm/trunk/util/torTools.py
===================================================================
--- arm/trunk/util/torTools.py	2010-05-31 15:17:34 UTC (rev 22446)
+++ arm/trunk/util/torTools.py	2010-05-31 22:41:07 UTC (rev 22447)
@@ -4,7 +4,7 @@
 fetch a TorCtl instance to experiment with use the following:
 
 >>> import util.torTools
->>> conn = util.torTools.makeConn()
+>>> conn = util.torTools.connect()
 >>> conn.get_info("version")["version"]
 '0.2.1.24'
 """
@@ -143,7 +143,7 @@
     if issue: raise IOError("Failed to read authentication cookie (%s): %s" % (issue, authVal))
     else: raise IOError("Failed to read authentication cookie: %s" % exc)
 
-def makeConn(controlAddr="127.0.0.1", controlPort=9051, passphrase=None):
+def connect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None):
   """
   Convenience method for quickly getting a TorCtl connection. This is very
   handy for debugging or CLI setup, handling setup and prompting for a password
@@ -271,9 +271,9 @@
     """
     
     if conn.is_live() and conn != self.conn:
+      self.connLock.acquire()
+      
       if self.conn: self.close() # shut down current connection
-      
-      self.connLock.acquire()
       self.conn = conn
       self.conn.add_event_listener(self)
       

Modified: arm/trunk/util/uiTools.py
===================================================================
--- arm/trunk/util/uiTools.py	2010-05-31 15:17:34 UTC (rev 22446)
+++ arm/trunk/util/uiTools.py	2010-05-31 22:41:07 UTC (rev 22447)
@@ -5,6 +5,7 @@
 - unit conversion for labels
 """
 
+import sys
 import curses
 
 # colors curses can handle
@@ -42,6 +43,59 @@
   if not COLOR_ATTR_INITIALIZED: _initColors()
   return COLOR_ATTR[color]
 
+def cropStr(msg, size, minWordLen = 4, addEllipse = True):
+  """
+  Provides the msg constrained to the given length, truncating on word breaks.
+  If the last words is long this truncates mid-word with an ellipse. If there
+  isn't room for even a truncated single word (or one word plus the ellipse if
+  inlcuding those) then this provides an empty string. Examples:
+  
+  cropStr("This is a looooong message", 17)
+  "This is a looo..."
+  
+  cropStr("This is a looooong message", 12)
+  "This is a..."
+  
+  cropStr("This is a looooong message", 3)
+  ""
+  
+  Arguments:
+    msg        - source text
+    size       - room available for text
+    minWordLen - minimum characters before which a word is dropped, requires
+                 whole word if -1
+    addEllipse - includes an ellipse when truncating if true (dropped if size
+                 size is 
+  """
+  
+  if minWordLen < 0: minWordLen = sys.maxint
+  
+  if len(msg) <= size: return msg
+  else:
+    msgWords = msg.split(" ")
+    msgWords.reverse()
+    
+    returnWords = []
+    sizeLeft = size - 3 if addEllipse else size
+    
+    # checks that there's room for at least one word
+    if min(minWordLen, len(msgWords[-1])) > sizeLeft: return ""
+    
+    while sizeLeft > 0:
+      nextWord = msgWords.pop()
+      
+      if len(nextWord) <= sizeLeft:
+        returnWords.append(nextWord)
+        sizeLeft -= (len(nextWord) + 1)
+      elif minWordLen <= sizeLeft:
+        returnWords.append(nextWord[:sizeLeft])
+        sizeLeft = 0
+      else: sizeLeft = 0
+    
+    returnMsg = " ".join(returnWords)
+    if addEllipse: returnMsg += "..."
+    return returnMsg
+
 def getSizeLabel(bytes, decimal = 0, isLong = False):
   """
   Converts byte count into label in its most significant units, for instance



More information about the tor-commits mailing list