[or-cvs] r20233: {arm} Rewrote graph panel so it can handle any real time statistic (arm/trunk/interface)

atagar at seul.org atagar at seul.org
Sat Aug 8 07:02:42 UTC 2009


Author: atagar
Date: 2009-08-08 03:02:41 -0400 (Sat, 08 Aug 2009)
New Revision: 20233

Added:
   arm/trunk/interface/bandwidthMonitor.py
   arm/trunk/interface/connCountMonitor.py
   arm/trunk/interface/graphPanel.py
Removed:
   arm/trunk/interface/bandwidthPanel.py
Modified:
   arm/trunk/interface/connPanel.py
   arm/trunk/interface/controller.py
Log:
Rewrote graph panel so it can handle any real time statistics.
added: option to graph connection counts (feature request by phobos)
added: custom graph bounds (global or local maxima)



Added: arm/trunk/interface/bandwidthMonitor.py
===================================================================
--- arm/trunk/interface/bandwidthMonitor.py	                        (rev 0)
+++ arm/trunk/interface/bandwidthMonitor.py	2009-08-08 07:02:41 UTC (rev 20233)
@@ -0,0 +1,112 @@
+#!/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
+from TorCtl import TorCtl
+
+import graphPanel
+import util
+
+DL_COLOR = "green"  # download section color
+UL_COLOR = "cyan"   # upload section color
+
+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)
+    
+    if conn:
+      self.isAccounting = conn.get_info('accounting/enabled')['accounting/enabled'] == '1'
+      
+      # static limit stats for label
+      bwStats = conn.get_option(['BandwidthRate', 'BandwidthBurst'])
+      self.bwRate = util.getSizeLabel(int(bwStats[0][1]))
+      self.bwBurst = util.getSizeLabel(int(bwStats[1][1]))
+    else:
+      self.isAccounting = False
+      self.bwRate, self.bwBurst = -1, -1
+    
+    # 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 redraw(self, panel):
+    # 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>)" % (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"]), util.getColor(self.primaryColor))
+        panel.addstr(11, 37, "%s / %s" % (self.accountingInfo["written"], self.accountingInfo["writtenLimit"]), util.getColor(self.secondaryColor))
+      else:
+        panel.addfstr(10, 0, "<b>Accounting:</b> Shutting Down...")
+  
+  def getTitle(self, width):
+    # provides label, dropping stats if there's not enough room
+    labelContents = "Bandwidth (cap: %s, burst: %s):" % (self.bwRate, self.bwBurst)
+    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, isPrimary):
+    if isPrimary: return "Downloaded (%s/sec):" % util.getSizeLabel(self.lastPrimary * 1024)
+    else: return "Uploaded (%s/sec):" % util.getSizeLabel(self.lastSecondary * 1024)
+  
+  def getFooterLabel(self, isPrimary):
+    avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
+    return "avg: %s/sec" % util.getSizeLabel(avg * 1024)
+  
+  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"]
+      
+      # altzone subtraction converts from gmt to local with respect to DST
+      sec = time.mktime(time.strptime(accountingParams["accounting/interval-end"], "%Y-%m-%d %H:%M:%S")) - time.time() - time.altzone
+      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"] = util.getSizeLabel(read)
+      self.accountingInfo["written"] = util.getSizeLabel(written)
+      self.accountingInfo["readLimit"] = util.getSizeLabel(read + readLeft)
+      self.accountingInfo["writtenLimit"] = util.getSizeLabel(written + writtenLeft)
+    except TorCtl.TorCtlClosed:
+      self.accountingInfo = None
+

Deleted: arm/trunk/interface/bandwidthPanel.py
===================================================================
--- arm/trunk/interface/bandwidthPanel.py	2009-08-08 04:28:00 UTC (rev 20232)
+++ arm/trunk/interface/bandwidthPanel.py	2009-08-08 07:02:41 UTC (rev 20233)
@@ -1,262 +0,0 @@
-#!/usr/bin/env python
-# bandwidthPanel.py -- Resources related to monitoring Tor bandwidth usage.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import time
-import copy
-import curses
-from TorCtl import TorCtl
-
-import util
-
-BANDWIDTH_GRAPH_COL = 30            # columns of data in graph
-BANDWIDTH_GRAPH_COLOR_DL = "green"  # download section color
-BANDWIDTH_GRAPH_COLOR_UL = "cyan"   # upload section color
-
-# time intervals at which graphs can be updated
-DEFAULT_INTERVAL_INDEX = 1   # defaults to using five seconds of data per bar in the graph
-UPDATE_INTERVALS = [("each second", 1),     ("5 seconds", 5),   ("30 seconds", 30),   ("minutely", 60),
-                    ("half hour", 1800),    ("hourly", 3600),   ("daily", 86400)]
-
-class BandwidthMonitor(TorCtl.PostEventListener, util.Panel):
-  """
-  Tor event listener, taking bandwidth sampling and drawing bar graph. This is
-  updated every second by the BW events and graph samples are spaced at
-  a timescale determined by the updateIntervalIndex.
-  """
-  
-  def __init__(self, lock, conn):
-    TorCtl.PostEventListener.__init__(self)
-    if conn: self.isAccounting = conn.get_info('accounting/enabled')['accounting/enabled'] == '1'
-    else: self.isAccounting = False
-    
-    self.contentHeight = 13 if self.isAccounting else 10
-    util.Panel.__init__(self, lock, self.contentHeight)
-    
-    self.conn = conn              # Tor control port connection
-    self.tick = 0                 # number of updates performed
-    self.lastDownloadRate = 0     # most recently sampled rates
-    self.lastUploadRate = 0
-    self.accountingInfo = None    # accounting data (set by _updateAccountingInfo method)
-    self.isPaused = False
-    self.isVisible = True
-    self.showLabel = True         # shows top label if true, hides otherwise
-    self.pauseBuffer = None       # mirror instance used to track updates when paused
-    self.updateIntervalIndex = DEFAULT_INTERVAL_INDEX
-    
-    # graphed download (read) and upload (write) rates - first index accumulator
-    # iterative insert is to avoid issue with shallow copies (nasty, nasty gotcha)
-    self.downloadRates, self.uploadRates = [], []
-    for i in range(len(UPDATE_INTERVALS)):
-      self.downloadRates.insert(0, (BANDWIDTH_GRAPH_COL + 1) * [0])
-      self.uploadRates.insert(0, (BANDWIDTH_GRAPH_COL + 1) * [0])
-    
-    # max rates seen, used to determine graph bounds
-    self.maxDownloadRate = len(UPDATE_INTERVALS) * [1]
-    self.maxUploadRate = len(UPDATE_INTERVALS) * [1]
-    
-    # used to calculate averages, uses tick for time
-    self.totalDownload = 0
-    self.totalUpload = 0
-    
-    # retrieves static stats for label
-    if conn:
-      bwStats = conn.get_option(['BandwidthRate', 'BandwidthBurst'])
-      self.bwRate = util.getSizeLabel(int(bwStats[0][1]))
-      self.bwBurst = util.getSizeLabel(int(bwStats[1][1]))
-    else: self.bwRate, self.bwBurst = -1, -1
-  
-  def bandwidth_event(self, event):
-    if self.isPaused or not self.isVisible: self.pauseBuffer.bandwidth_event(event)
-    else:
-      self.lastDownloadRate = event.read
-      self.lastUploadRate = event.written
-      
-      self.totalDownload += event.read
-      self.totalUpload += event.written
-      
-      # updates graphs for all time intervals
-      self.tick += 1
-      for i in range(len(UPDATE_INTERVALS)):
-        self.downloadRates[i][0] += event.read
-        self.uploadRates[i][0] += event.written
-        interval = UPDATE_INTERVALS[i][1]
-        
-        if self.tick % interval == 0:
-          self.maxDownloadRate[i] = max(self.maxDownloadRate[i], self.downloadRates[i][0] / interval)
-          self.downloadRates[i].insert(0, 0)
-          del self.downloadRates[i][BANDWIDTH_GRAPH_COL + 1:]
-          
-          self.maxUploadRate[i] = max(self.maxUploadRate[i], self.uploadRates[i][0] / interval)
-          self.uploadRates[i].insert(0, 0)
-          del self.uploadRates[i][BANDWIDTH_GRAPH_COL + 1:]
-      
-      self.redraw()
-  
-  def redraw(self):
-    """ Redraws bandwidth panel. """
-    # doesn't draw if headless (indicating that the instance is for a pause buffer)
-    if self.win:
-      if not self.lock.acquire(False): return
-      try:
-        self.clear()
-        dlColor = util.getColor(BANDWIDTH_GRAPH_COLOR_DL)
-        ulColor = util.getColor(BANDWIDTH_GRAPH_COLOR_UL)
-        
-        # draws label, dropping stats if there's not enough room
-        labelContents = "Bandwidth (cap: %s, burst: %s):" % (self.bwRate, self.bwBurst)
-        if self.maxX < len(labelContents):
-          labelContents = "%s):" % labelContents[:labelContents.find(",")]  # removes burst measure
-          if self.maxX < len(labelContents): labelContents = "Bandwidth:"   # removes both
-        
-        if self.showLabel: self.addstr(0, 0, labelContents, util.LABEL_ATTR)
-        
-        # current numeric measures
-        self.addstr(1, 0, "Downloaded (%s/sec):" % util.getSizeLabel(self.lastDownloadRate), curses.A_BOLD | dlColor)
-        self.addstr(1, 35, "Uploaded (%s/sec):" % util.getSizeLabel(self.lastUploadRate), curses.A_BOLD | ulColor)
-        
-        # graph bounds in KB (uses highest recorded value as max)
-        self.addstr(2, 0, "%4s" % str(self.maxDownloadRate[self.updateIntervalIndex] / 1024), dlColor)
-        self.addstr(7, 0, "   0", dlColor)
-        
-        self.addstr(2, 35, "%4s" % str(self.maxUploadRate[self.updateIntervalIndex] / 1024), ulColor)
-        self.addstr(7, 35, "   0", ulColor)
-        
-        # creates bar graph of bandwidth usage over time
-        for col in range(BANDWIDTH_GRAPH_COL):
-          bytesDownloaded = self.downloadRates[self.updateIntervalIndex][col + 1] / UPDATE_INTERVALS[self.updateIntervalIndex][1]
-          colHeight = min(5, 5 * bytesDownloaded / self.maxDownloadRate[self.updateIntervalIndex])
-          for row in range(colHeight):
-            self.addstr(7 - row, col + 5, " ", curses.A_STANDOUT | dlColor)
-        
-        for col in range(BANDWIDTH_GRAPH_COL):
-          bytesUploaded = self.uploadRates[self.updateIntervalIndex][col + 1] / UPDATE_INTERVALS[self.updateIntervalIndex][1]
-          colHeight = min(5, 5 * bytesUploaded / self.maxUploadRate[self.updateIntervalIndex])
-          for row in range(colHeight):
-            self.addstr(7 - row, col + 40, " ", curses.A_STANDOUT | ulColor)
-        
-        # provides average dl/ul rates
-        if self.tick > 0:
-          avgDownload = self.totalDownload / self.tick
-          avgUpload = self.totalUpload / self.tick
-        else: avgDownload, avgUpload = 0, 0
-        self.addstr(8, 1, "avg: %s/sec" % util.getSizeLabel(avgDownload), dlColor)
-        self.addstr(8, 36, "avg: %s/sec" % util.getSizeLabel(avgUpload), ulColor)
-        
-        # accounting stats if enabled
-        if self.isAccounting:
-          if not self.isPaused and self.isVisible: self._updateAccountingInfo()
-          
-          if self.accountingInfo:
-            status = self.accountingInfo["status"]
-            hibernateColor = "green"
-            if status == "soft": hibernateColor = "yellow"
-            elif status == "hard": hibernateColor = "red"
-            
-            self.addfstr(10, 0, "<b>Accounting (<%s>%s</%s>)" % (hibernateColor, status, hibernateColor))
-            self.addstr(10, 35, "Time to reset: %s" % self.accountingInfo["resetTime"])
-            self.addstr(11, 2, "%s / %s" % (self.accountingInfo["read"], self.accountingInfo["readLimit"]), dlColor)
-            self.addstr(11, 37, "%s / %s" % (self.accountingInfo["written"], self.accountingInfo["writtenLimit"]), ulColor)
-          else:
-            self.addfstr(10, 0, "<b>Accounting:</b> Shutting Down...")
-        
-        self.refresh()
-      finally:
-        self.lock.release()
-  
-  def setUpdateInterval(self, intervalIndex):
-    """
-    Sets the timeframe at which the graph is updated. This throws a ValueError
-    if the index isn't within UPDATE_INTERVALS.
-    """
-    
-    if intervalIndex >= 0 and intervalIndex < len(UPDATE_INTERVALS):
-      self.updateIntervalIndex = intervalIndex
-    else: raise ValueError("%i out of bounds of UPDATE_INTERVALS" % intervalIndex)
-  
-  def setPaused(self, isPause):
-    """
-    If true, prevents bandwidth updates from being presented.
-    """
-    
-    if isPause == self.isPaused: return
-    self.isPaused = isPause
-    if self.isVisible: self._parameterSwap()
-  
-  def setVisible(self, isVisible):
-    """
-    Toggles panel visability, hiding if false.
-    """
-    
-    if isVisible == self.isVisible: return
-    self.isVisible = isVisible
-    
-    if self.isVisible: self.height = self.contentHeight
-    else: self.height = 0
-    
-    if not self.isPaused: self._parameterSwap()
-  
-  def _parameterSwap(self):
-    if self.isPaused or not self.isVisible:
-      if self.pauseBuffer == None: self.pauseBuffer = BandwidthMonitor(None, None)
-      
-      # TODO: use a more clever swap using 'active' and 'inactive' instances
-      self.pauseBuffer.tick = self.tick
-      self.pauseBuffer.lastDownloadRate = self.lastDownloadRate
-      self.pauseBuffer.lastUploadRate = self.lastUploadRate
-      self.pauseBuffer.maxDownloadRate = list(self.maxDownloadRate)
-      self.pauseBuffer.maxUploadRate = list(self.maxUploadRate)
-      self.pauseBuffer.downloadRates = copy.deepcopy(self.downloadRates)
-      self.pauseBuffer.uploadRates = copy.deepcopy(self.uploadRates)
-      self.pauseBuffer.totalDownload = self.totalDownload
-      self.pauseBuffer.totalUpload = self.totalUpload
-      self.pauseBuffer.bwRate = self.bwRate
-      self.pauseBuffer.bwBurst = self.bwBurst
-    else:
-      self.tick = self.pauseBuffer.tick
-      self.lastDownloadRate = self.pauseBuffer.lastDownloadRate
-      self.lastUploadRate = self.pauseBuffer.lastUploadRate
-      self.maxDownloadRate = self.pauseBuffer.maxDownloadRate
-      self.maxUploadRate = self.pauseBuffer.maxUploadRate
-      self.downloadRates = self.pauseBuffer.downloadRates
-      self.uploadRates = self.pauseBuffer.uploadRates
-      self.totalDownload = self.pauseBuffer.totalDownload
-      self.totalUpload = self.pauseBuffer.totalUpload
-      self.bwRate = self.pauseBuffer.bwRate
-      self.bwBurst = self.pauseBuffer.bwBurst
-      self.redraw()
-  
-  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"]
-      
-      # altzone subtraction converts from gmt to local with respect to DST
-      sec = time.mktime(time.strptime(accountingParams["accounting/interval-end"], "%Y-%m-%d %H:%M:%S")) - time.time() - time.altzone
-      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"] = util.getSizeLabel(read)
-      self.accountingInfo["written"] = util.getSizeLabel(written)
-      self.accountingInfo["readLimit"] = util.getSizeLabel(read + readLeft)
-      self.accountingInfo["writtenLimit"] = util.getSizeLabel(written + writtenLeft)
-    except TorCtl.TorCtlClosed:
-      self.accountingInfo = None
-

Added: arm/trunk/interface/connCountMonitor.py
===================================================================
--- arm/trunk/interface/connCountMonitor.py	                        (rev 0)
+++ arm/trunk/interface/connCountMonitor.py	2009-08-08 07:02:41 UTC (rev 20233)
@@ -0,0 +1,70 @@
+#!/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 os
+import time
+from threading import Thread
+from TorCtl import TorCtl
+
+import connPanel
+import graphPanel
+import util
+
+class ConnCountMonitor(graphPanel.GraphStats, Thread):
+  """
+  Tracks number of connections, using cached values in connPanel if recent
+  enough (otherwise retrieved independently).
+  """
+  
+  def __init__(self, connectionPanel):
+    graphPanel.GraphStats.__init__(self)
+    Thread.__init__(self)
+    graphPanel.GraphStats.initialize(self, connPanel.TYPE_COLORS["inbound"], connPanel.TYPE_COLORS["outbound"], 10)
+    
+    self.lastUpdate = -1                    # time last stats was retrived
+    self.connectionPanel = connectionPanel  # connection panel, used to limit netstat calls
+    
+    self.setDaemon(True)
+    self.start()
+  
+  def run(self):
+    while True:
+      while self.lastUpdate + 1 > time.time(): time.sleep(0.5)
+      
+      if self.connectionPanel.lastUpdate + 1 >= time.time():
+        # reuses netstat results if recent enough
+        counts = self.connectionPanel.connectionCount
+        self._processEvent(counts[0], counts[1])
+      else:
+        # cached results stale - requery netstat
+        inbound, outbound, control = 0, 0, 0
+        netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor 2> /dev/null" % self.connectionPanel.pid)
+        try:
+          results = netstatCall.readlines()
+          
+          for line in results:
+            if not line.startswith("tcp"): continue
+            param = line.split()
+            localPort = param[3][param[3].find(":") + 1:]
+            
+            if localPort in (self.connectionPanel.orPort, self.connectionPanel.dirPort): inbound += 1
+            elif localPort == self.connectionPanel.controlPort: control += 1
+            else: outbound += 1
+        except IOError:
+          # netstat call failed
+          self.connectionPanel.monitor_event("WARN", "Unable to query netstat for connection counts")
+        
+        netstatCall.close()
+        self._processEvent(inbound, outbound)
+      
+      self.lastUpdate = time.time()
+  
+  def getTitle(self, width):
+    return "Connection Count:"
+  
+  def getHeaderLabel(self, 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	2009-08-08 04:28:00 UTC (rev 20232)
+++ arm/trunk/interface/connPanel.py	2009-08-08 07:02:41 UTC (rev 20233)
@@ -3,6 +3,7 @@
 # Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
 
 import os
+import time
 import socket
 import curses
 from TorCtl import TorCtl
@@ -92,6 +93,7 @@
     self.allowDNS = True            # permits hostname resolutions if true
     self.showLabel = True           # shows top label if true, hides otherwise
     self.showingDetails = False     # augments display to accomidate details window if true
+    self.lastUpdate = -1            # time last stats was retrived
     self.sortOrdering = [ORD_TYPE, ORD_FOREIGN_LISTING, ORD_FOREIGN_PORT]
     self.isPaused = False
     self.resolver = hostnameResolver.HostnameResolver()
@@ -172,7 +174,7 @@
     
     # looks at netstat for tor with stderr redirected to /dev/null, options are:
     # n = prevents dns lookups, p = include process (say if it's tor), t = tcp only
-    netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor" % self.pid)
+    netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor 2> /dev/null" % self.pid)
     try:
       results = netstatCall.readlines()
       
@@ -201,6 +203,8 @@
           if not self.providedGeoipWarning:
             self.logger.monitor_event("WARN", "Tor geoip database is unavailable.")
             self.providedGeoipWarning = True
+        except error:
+          countryCode = "??"
         
         self.connections.append((type, localIP, localPort, foreignIP, foreignPort, countryCode))
     except IOError:
@@ -208,6 +212,7 @@
       self.logger.monitor_event("WARN", "Unable to query netstat for new connections")
     
     netstatCall.close()
+    self.lastUpdate = time.time()
     
     # hostnames are sorted at redraw - otherwise now's a good time
     if self.listingType != LIST_HOSTNAME: self.sortConnections()

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2009-08-08 04:28:00 UTC (rev 20232)
+++ arm/trunk/interface/controller.py	2009-08-08 07:02:41 UTC (rev 20233)
@@ -12,13 +12,16 @@
 from threading import RLock
 from TorCtl import TorCtl
 
-import util
 import headerPanel
-import bandwidthPanel
+import graphPanel
 import logPanel
 import connPanel
 import confPanel
 
+import util
+import bandwidthMonitor
+import connCountMonitor
+
 REFRESH_RATE = 5        # seconds between redrawing screen
 cursesLock = RLock()    # global curses lock (curses isn't thread safe and
                         # concurrency bugs produce especially sinister glitches)
@@ -29,10 +32,10 @@
 # panel order per page
 PAGE_S = ["header", "control", "popup"]    # sticky (ie, always available) page
 PAGES = [
-  ["bandwidth", "log"],
+  ["graph", "log"],
   ["conn"],
   ["torrc"]]
-PAUSEABLE = ["header", "bandwidth", "log", "conn"]
+PAUSEABLE = ["header", "graph", "log", "conn"]
 PAGE_COUNT = 3 # all page numbering is internally represented as 0-indexed
 
 # events needed for panels other than the event log
@@ -180,7 +183,7 @@
         if eventType in loggedEvents: loggedEvents.remove(eventType)
         
         if eventType in REQ_EVENTS:
-          if eventType == "BW": msg = "(bandwidth panel won't function)"
+          if eventType == "BW": msg = "(bandwidth graph won't function)"
           elif eventType in ("NEWDESC", "NEWCONSENSUS"): msg = "(connections listing can't register consensus changes)"
           else: msg = ""
           logListener.monitor_event("ERR", "Unsupported event type: %s %s" % (eventType, msg))
@@ -237,7 +240,7 @@
   panels = {
     "header": headerPanel.HeaderPanel(cursesLock, conn, torPid),
     "popup": util.Panel(cursesLock, 9),
-    "bandwidth": bandwidthPanel.BandwidthMonitor(cursesLock, conn),
+    "graph": graphPanel.GraphPanel(cursesLock),
     "log": logPanel.LogMonitor(cursesLock, loggedEvents),
     "torrc": confPanel.ConfPanel(cursesLock, conn.get_info("config-file")["config-file"])}
   panels["conn"] = connPanel.ConnPanel(cursesLock, conn, torPid, panels["log"])
@@ -246,9 +249,14 @@
   # provides error if pid coulnd't be determined (hopefully shouldn't happen...)
   if not torPid: panels["log"].monitor_event("WARN", "Unable to resolve tor pid, abandoning connection listing")
   
+  # statistical monitors for graph
+  panels["graph"].addStats("bandwidth", bandwidthMonitor.BandwidthMonitor(conn))
+  panels["graph"].addStats("connection count", connCountMonitor.ConnCountMonitor(panels["conn"]))
+  panels["graph"].setStats("bandwidth")
+  
   # listeners that update bandwidth and log panels with Tor status
   conn.add_event_listener(panels["log"])
-  conn.add_event_listener(panels["bandwidth"])
+  conn.add_event_listener(panels["graph"].stats["bandwidth"])
   conn.add_event_listener(panels["conn"])
   
   # tells Tor to listen to the events we're interested
@@ -342,13 +350,12 @@
         popup.addstr(0, 0, "Page %i Commands:" % (page + 1), util.LABEL_ATTR)
         
         if page == 0:
-          bwVisibleLabel = "visible" if panels["bandwidth"].isVisible else "hidden"
-          popup.addfstr(1, 2, "b: toggle bandwidth panel (<b>%s</b>)" % bwVisibleLabel)
-          
-          # matches timescale used by bandwith panel to recognized labeling
-          intervalLabel = bandwidthPanel.UPDATE_INTERVALS[panels["bandwidth"].updateIntervalIndex][0]
-          popup.addfstr(1, 41, "i: graph update interval (<b>%s</b>)" % intervalLabel)
-          popup.addstr(2, 2, "e: change logged events")
+          graphedStats = panels["graph"].currentDisplay
+          if not graphedStats: graphedStats = "none"
+          popup.addfstr(1, 2, "s: graphed stats (<b>%s</b>)" % graphedStats)
+          popup.addfstr(1, 41, "i: graph update interval (<b>%s</b>)" % panels["graph"].updateInterval)
+          popup.addfstr(2, 2, "b: graph bounds (<b>%s</b>)" % graphPanel.BOUND_LABELS[panels["graph"].bounds])
+          popup.addstr(2, 41, "e: change logged events")
         if page == 1:
           popup.addstr(1, 2, "up arrow: scroll up a line")
           popup.addstr(1, 41, "down arrow: scroll down a line")
@@ -387,29 +394,62 @@
         setPauseState(panels, isPaused, page)
       finally:
         cursesLock.release()
-    elif page == 0 and (key == ord('b') or key == ord('B')):
-      # toggles bandwidth panel visability
-      panels["bandwidth"].setVisible(not panels["bandwidth"].isVisible)
-      oldY = -1 # force resize event
+    elif page == 0 and (key == ord('s') or key == ord('S')):
+      # provides menu to pick stats to be graphed
+      #options = ["None"] + [label for label in panels["graph"].stats.keys()]
+      options = ["None"]
+      
+      # appends stats labels with first letters of each word capitalized
+      initialSelection, i = -1, 1
+      if not panels["graph"].currentDisplay: initialSelection = 0
+      for label in panels["graph"].stats.keys():
+        if label == panels["graph"].currentDisplay: initialSelection = i
+        words = label.split()
+        options.append(" ".join(word[0].upper() + word[1:] for word in words))
+        i += 1
+      
+      # hides top label of the graph panel and pauses panels
+      if panels["graph"].currentDisplay:
+        panels["graph"].showLabel = False
+        panels["graph"].redraw()
+      setPauseState(panels, isPaused, page, True)
+      
+      selection = showMenu(stdscr, panels["popup"], "Graphed Stats:", options, initialSelection)
+      
+      # reverts changes made for popup
+      panels["graph"].showLabel = True
+      setPauseState(panels, isPaused, page)
+      
+      # applies new setting
+      if selection != -1 and selection != initialSelection:
+        if selection == 0: panels["graph"].setStats(None)
+        else: panels["graph"].setStats(options[selection].lower())
+        oldY = -1 # force resize event
     elif page == 0 and (key == ord('i') or key == ord('I')):
-      # provides menu to pick bandwidth graph update interval
-      options = [label for (label, intervalTime) in bandwidthPanel.UPDATE_INTERVALS]
-      initialSelection = panels["bandwidth"].updateIntervalIndex
+      # provides menu to pick graph panel update interval
+      options = [label for (label, intervalTime) in graphPanel.UPDATE_INTERVALS]
       
-      # hides top label of bandwidth panel and pauses panels
-      panels["bandwidth"].showLabel = False
-      panels["bandwidth"].redraw()
+      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()
       setPauseState(panels, isPaused, page, True)
       
       selection = showMenu(stdscr, panels["popup"], "Update Interval:", options, initialSelection)
       
       # reverts changes made for popup
-      panels["bandwidth"].showLabel = True
+      panels["graph"].showLabel = True
       setPauseState(panels, isPaused, page)
       
       # applies new setting
-      if selection != -1:
-        panels["bandwidth"].setUpdateInterval(selection)
+      if selection != -1: panels["graph"].updateInterval = options[selection]
+    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
     elif page == 0 and (key == ord('e') or key == ord('E')):
       # allow user to enter new types of events to log - unchanged if left blank
       cursesLock.acquire()

Added: arm/trunk/interface/graphPanel.py
===================================================================
--- arm/trunk/interface/graphPanel.py	                        (rev 0)
+++ arm/trunk/interface/graphPanel.py	2009-08-08 07:02:41 UTC (rev 20233)
@@ -0,0 +1,281 @@
+#!/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
+
+import util
+
+GRAPH_COL = 30  # columns of data in graph
+
+# 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] = (GRAPH_COL + 1) * [0]
+      self.secondaryCounts[label] = (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                    # vertical size of content
+    
+    # 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, isPrimary):
+    """
+    Provides labeling presented at the top of the graph.
+    """
+    
+    return ""
+  
+  def getFooterLabel(self, isPrimary):
+    """
+    Provides labeling present at the bottom of the graph.
+    """
+    
+    return ""
+  
+  def redraw(self, panel):
+    """
+    Allows for any custom redrawing 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][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][GRAPH_COL + 1:]
+      
+      if self.graphPanel: self.graphPanel.redraw()
+
+class GraphPanel(util.Panel):
+  """
+  Panel displaying a graph, drawing statistics from custom GraphStats
+  implementations.
+  """
+  
+  def __init__(self, lock):
+    util.Panel.__init__(self, lock, 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_MAX      # determines bounds on graph
+    self.currentDisplay = None    # label of the stats currently being displayed
+    self.stats = {}               # available stats (mappings of label -> instance)
+  
+  def redraw(self):
+    """ Redraws graph panel """
+    if self.win:
+      if not self.lock.acquire(False): return
+      try:
+        self.clear()
+        
+        if self.currentDisplay:
+          param = self.stats[self.currentDisplay]
+          primaryColor = util.getColor(param.primaryColor)
+          secondaryColor = util.getColor(param.secondaryColor)
+          
+          if self.showLabel: self.addstr(0, 0, param.getTitle(self.maxX), util.LABEL_ATTR)
+          
+          # top labels
+          left, right = param.getHeaderLabel(True), param.getHeaderLabel(False)
+          if left: self.addstr(1, 0, left, curses.A_BOLD | primaryColor)
+          if right: self.addstr(1, 35, 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:]: primaryBound = max(value, primaryBound)
+            for value in param.secondaryCounts[self.updateInterval][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, 35, "%4s" % str(int(secondaryBound)), secondaryColor)
+          self.addstr(7, 35, "   0", secondaryColor)
+          
+          # creates bar graph of bandwidth usage over time
+          for col in range(GRAPH_COL):
+            colHeight = min(5, 5 * param.primaryCounts[self.updateInterval][col + 1] / primaryBound)
+            for row in range(colHeight): self.addstr(7 - row, col + 5, " ", curses.A_STANDOUT | primaryColor)
+          
+          for col in range(GRAPH_COL):
+            colHeight = min(5, 5 * param.secondaryCounts[self.updateInterval][col + 1] / secondaryBound)
+            for row in range(colHeight): self.addstr(7 - row, col + 40, " ", curses.A_STANDOUT | secondaryColor)
+          
+          # bottom labels
+          left, right = param.getFooterLabel(True), param.getFooterLabel(False)
+          if left: self.addstr(8, 1, left, primaryColor)
+          if right: self.addstr(8, 36, right, secondaryColor)
+          
+          # allows for finishing touches by monitor
+          param.redraw(self)
+          
+        self.refresh()
+      finally:
+        self.lock.release()
+  
+  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]
+        
+        # TODO: BUG - log panel's partly overwritten if showing a smaller panel
+        # (simple workaround is to use max size, but fix would be preferable)
+        #self.height = newStats.height
+        maxHeight = 0
+        for panel in self.stats.values(): maxHeight = max(panel.height, maxHeight)
+        self.height = maxHeight
+        
+        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)
+



More information about the tor-commits mailing list