[or-cvs] r20198: {arm} Work done over the trip. added: customizable update interval (in arm/trunk: . interface)

atagar at seul.org atagar at seul.org
Thu Jul 30 07:09:47 UTC 2009


Author: atagar
Date: 2009-07-30 03:09:47 -0400 (Thu, 30 Jul 2009)
New Revision: 20198

Added:
   arm/trunk/screenshot_page1.png
   arm/trunk/screenshot_page2.png
Removed:
   arm/trunk/arm-in-action.png
Modified:
   arm/trunk/arm.py
   arm/trunk/interface/bandwidthPanel.py
   arm/trunk/interface/confPanel.py
   arm/trunk/interface/connPanel.py
   arm/trunk/interface/controller.py
   arm/trunk/interface/headerPanel.py
   arm/trunk/interface/hostnameResolver.py
   arm/trunk/interface/logPanel.py
   arm/trunk/interface/util.py
   arm/trunk/readme.txt
Log:
Work done over the trip.
added: customizable update interval for bandwidth graph (feature request by StrangeCharm)
change: noted new project page in the readme (www.atagar.com/arm)
change: added word wrapping to conf panel
change: added function for custom popup menus
change: logs error message when required event types are unsupported rather than throwing an exception
change: using different screenshot images
fix: resolved issue that caused monitor to think tor was resumed when quit
fix: bug with panel utility's resize detection
fix: resorts connections after NEWDESC and NEWCONSENSUS events
fix: forgetting to to resume monitor at multiple points after a temporary pause
fix: minor refactoring based on suggestions from pylint (unused imports and such)



Deleted: arm/trunk/arm-in-action.png
===================================================================
(Binary files differ)

Modified: arm/trunk/arm.py
===================================================================
--- arm/trunk/arm.py	2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/arm.py	2009-07-30 07:09:47 UTC (rev 20198)
@@ -12,7 +12,6 @@
 import os
 import socket
 import getpass
-import binascii
 
 from TorCtl import TorCtl
 from TorCtl import TorUtil
@@ -28,7 +27,7 @@
 NO_AUTH, COOKIE_AUTH, PASSWORD_AUTH = range(3) # enums for authentication type
 
 HELP_TEXT = """Usage arm [OPTION]
-Terminal Tor relay status monitor.
+Terminal status monitor for Tor relays.
 
   -i, --interface [ADDRESS:]PORT  change control interface from %s:%i
   -c, --cookie[=PATH]             authenticates using cookie, PATH defaults to

Modified: arm/trunk/interface/bandwidthPanel.py
===================================================================
--- arm/trunk/interface/bandwidthPanel.py	2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/bandwidthPanel.py	2009-07-30 07:09:47 UTC (rev 20198)
@@ -3,21 +3,26 @@
 # 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_SAMPLES = 5         # seconds of data used for a bar in the graph
 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
-  BANDWIDTH_GRAPH_SAMPLES second intervals.
+  a timescale determined by the updateIntervalIndex.
   """
   
   def __init__(self, lock, conn):
@@ -32,17 +37,24 @@
     self.tick = 0                 # number of updates performed
     self.lastDownloadRate = 0     # most recently sampled rates
     self.lastUploadRate = 0
-    self.maxDownloadRate = 1      # max rates seen, used to determine graph bounds
-    self.maxUploadRate = 1
     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
-    self.downloadRates = [0] * (BANDWIDTH_GRAPH_COL + 1)
-    self.uploadRates = [0] * (BANDWIDTH_GRAPH_COL + 1)
+    # 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
@@ -60,21 +72,24 @@
       self.lastDownloadRate = event.read
       self.lastUploadRate = event.written
       
-      self.downloadRates[0] += event.read
-      self.uploadRates[0] += event.written
-      
       self.totalDownload += event.read
       self.totalUpload += event.written
       
+      # updates graphs for all time intervals
       self.tick += 1
-      if self.tick % BANDWIDTH_GRAPH_SAMPLES == 0:
-        self.maxDownloadRate = max(self.maxDownloadRate, self.downloadRates[0])
-        self.downloadRates.insert(0, 0)
-        del self.downloadRates[BANDWIDTH_GRAPH_COL + 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]
         
-        self.maxUploadRate = max(self.maxUploadRate, self.uploadRates[0])
-        self.uploadRates.insert(0, 0)
-        del self.uploadRates[BANDWIDTH_GRAPH_COL + 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()
   
@@ -94,29 +109,29 @@
           labelContents = "%s):" % labelContents[:labelContents.find(",")]  # removes burst measure
           if self.maxX < len(labelContents): labelContents = "Bandwidth:"   # removes both
         
-        self.addstr(0, 0, labelContents, util.LABEL_ATTR)
+        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 / 1024 / BANDWIDTH_GRAPH_SAMPLES), dlColor)
+        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 / 1024 / BANDWIDTH_GRAPH_SAMPLES), ulColor)
+        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[col + 1]
-          colHeight = min(5, 5 * bytesDownloaded / self.maxDownloadRate)
+          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[col + 1]
-          colHeight = min(5, 5 * bytesUploaded / self.maxUploadRate)
+          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)
         
@@ -149,6 +164,16 @@
       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.
@@ -175,13 +200,14 @@
     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 = self.maxDownloadRate
-      self.pauseBuffer.maxUploadRate = self.maxUploadRate
-      self.pauseBuffer.downloadRates = list(self.downloadRates)
-      self.pauseBuffer.uploadRates = list(self.uploadRates)
+      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
@@ -189,7 +215,7 @@
     else:
       self.tick = self.pauseBuffer.tick
       self.lastDownloadRate = self.pauseBuffer.lastDownloadRate
-      self.lastUploadRate = self.pauseBuffer.lastuploadRate
+      self.lastUploadRate = self.pauseBuffer.lastUploadRate
       self.maxDownloadRate = self.pauseBuffer.maxDownloadRate
       self.maxUploadRate = self.pauseBuffer.maxUploadRate
       self.downloadRates = self.pauseBuffer.downloadRates

Modified: arm/trunk/interface/confPanel.py
===================================================================
--- arm/trunk/interface/confPanel.py	2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/confPanel.py	2009-07-30 07:09:47 UTC (rev 20198)
@@ -4,7 +4,6 @@
 
 import math
 import curses
-from TorCtl import TorCtl
 
 import util
 
@@ -18,6 +17,8 @@
     self.confLocation = confLocation
     self.showLineNum = True
     self.stripComments = False
+    self.confContents = []
+    self.scroll = 0
     self.reset()
   
   def reset(self):
@@ -65,26 +66,54 @@
         
         pageHeight = self.maxY - 1
         numFieldWidth = int(math.log10(len(displayText))) + 1
+        lineNum = 1
         for i in range(self.scroll, min(len(displayText), self.scroll + pageHeight)):
           lineText = displayText[i].strip()
-          endBreak = 0
           
+          numOffset = 0     # offset for line numbering
           if self.showLineNum:
-            self.addstr(i - self.scroll + 1, 0, ("%%%ii" % numFieldWidth) % (i + 1), curses.A_BOLD | util.getColor("yellow"))
+            self.addstr(lineNum, 0, ("%%%ii" % numFieldWidth) % (i + 1), curses.A_BOLD | util.getColor("yellow"))
             numOffset = numFieldWidth + 1
-          else: numOffset = 0
           
-          if not lineText: continue
-          elif not lineText[0] == "#":
-            ctlBreak = lineText.find(" ")
-            endBreak = lineText.find("#")
-            if endBreak == -1: endBreak = len(lineText)
+          command, argument, comment = "", "", ""
+          if not lineText: continue # no text
+          elif lineText[0] == "#":
+            # whole line is commented out
+            comment = lineText
+          else:
+            # parse out command, argument, and possible comment
+            ctlEnd = lineText.find(" ")   # end of command
+            argEnd = lineText.find("#")   # end of argument (start of comment or end of line)
+            if argEnd == -1: argEnd = len(lineText)
             
-            self.addstr(i - self.scroll + 1, numOffset, lineText[:ctlBreak], curses.A_BOLD | util.getColor("green"))
-            self.addstr(i - self.scroll + 1, numOffset + ctlBreak, lineText[ctlBreak:endBreak], curses.A_BOLD | util.getColor("cyan"))
-          self.addstr(i - self.scroll + 1, numOffset + endBreak, lineText[endBreak:], util.getColor("white"))
-        
+            command, argument, comment = lineText[:ctlEnd], lineText[ctlEnd:argEnd], lineText[argEnd:]
+          
+          xLoc = 0
+          lineNum, xLoc = self.addstr_wrap(lineNum, xLoc, numOffset, command, curses.A_BOLD | util.getColor("green"))
+          lineNum, xLoc = self.addstr_wrap(lineNum, xLoc, numOffset, argument, curses.A_BOLD | util.getColor("cyan"))
+          lineNum, xLoc = self.addstr_wrap(lineNum, xLoc, numOffset, comment, util.getColor("white"))
+          lineNum += 1
+          
         self.refresh()
       finally:
         self.lock.release()
+  
+  def addstr_wrap(self, y, x, indent, text, formatting):
+    """
+    Writes text with word wrapping, returning the ending y/x coordinate.
+    """
+    
+    if not text: return (y, x)        # nothing to write
+    lineWidth = self.maxX - indent    # room for text
+    while True:
+      if len(text) > lineWidth - x - 1:
+        chunkSize = text.rfind(" ", 0, lineWidth - x)
+        writeText = text[:chunkSize]
+        text = text[chunkSize:].strip()
+        
+        self.addstr(y, x + indent, writeText, formatting)
+        y, x = y + 1, 0
+      else:
+        self.addstr(y, x + indent, text, formatting)
+        return (y, x + len(text))
 

Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py	2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/connPanel.py	2009-07-30 07:09:47 UTC (rev 20198)
@@ -14,6 +14,13 @@
 LIST_IP, LIST_HOSTNAME, LIST_FINGERPRINT, LIST_NICKNAME = range(4)
 LIST_LABEL = {LIST_IP: "IP Address", LIST_HOSTNAME: "Hostname", LIST_FINGERPRINT: "Fingerprint", LIST_NICKNAME: "Nickname"}
 
+# attributes for connection types
+TYPE_COLORS = {"inbound": "green", "outbound": "blue", "control": "red"}
+TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "control": 2} # defines ordering
+
+# enums for indexes of ConnPanel 'connections' fields
+CONN_TYPE, CONN_L_IP, CONN_L_PORT, CONN_F_IP, CONN_F_PORT, CONN_COUNTRY = range(6)
+
 # enums for sorting types (note: ordering corresponds to SORT_TYPES for easy lookup)
 # TODO: add ORD_BANDWIDTH -> (ORD_BANDWIDTH, "Bandwidth", lambda x, y: ???)
 ORD_TYPE, ORD_FOREIGN_LISTING, ORD_SRC_LISTING, ORD_DST_LISTING, ORD_COUNTRY, ORD_FOREIGN_PORT, ORD_SRC_PORT, ORD_DST_PORT = range(8)
@@ -31,12 +38,6 @@
               (ORD_DST_PORT, "Port (Dest.)",
                 lambda x, y: int(x[CONN_L_PORT] if x[CONN_TYPE] == "inbound" else x[CONN_F_PORT]) - int(y[CONN_L_PORT] if y[CONN_TYPE] == "inbound" else y[CONN_F_PORT]))]
 
-TYPE_COLORS = {"inbound": "green", "outbound": "blue", "control": "red"}
-TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "control": 2}
-
-# enums for indexes of ConnPanel 'connections' fields
-CONN_TYPE, CONN_L_IP, CONN_L_PORT, CONN_F_IP, CONN_F_PORT, CONN_COUNTRY = range(6)
-
 # provides bi-directional mapping of sorts with their associated labels
 def getSortLabel(sortType, withColor = False):
   """
@@ -123,6 +124,7 @@
     self.fingerprintLookupCache.clear()
     self.nicknameLookupCache.clear()
     self.fingerprintMappings = _getFingerprintMappings(self.conn, event.nslist)
+    if self.listingType != LIST_HOSTNAME: self.sortConnections()
   
   def new_desc_event(self, event):
     for fingerprint in event.idlist:
@@ -157,6 +159,7 @@
         self.fingerprintMappings[nsEntry.ip].append((nsEntry.orport, nsEntry.idhex))
       else:
         self.fingerprintMappings[nsEntry.ip] = [(nsEntry.orport, nsEntry.idhex)]
+    if self.listingType != LIST_HOSTNAME: self.sortConnections()
   
   def reset(self):
     """

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/controller.py	2009-07-30 07:09:47 UTC (rev 20198)
@@ -34,7 +34,6 @@
   ["torrc"]]
 PAUSEABLE = ["header", "bandwidth", "log", "conn"]
 PAGE_COUNT = 3 # all page numbering is internally represented as 0-indexed
-# TODO: page for configuration information
 
 # events needed for panels other than the event log
 REQ_EVENTS = ["BW", "NEWDESC", "NEWCONSENSUS"]
@@ -92,6 +91,64 @@
       self.addstr(0, 0, msgText, msgAttr)
       self.refresh()
 
+def setPauseState(panels, monitorIsPaused, currentPage, overwrite=False):
+  """
+  Resets the isPaused state of panels. If overwrite is True then this pauses
+  reguardless of the monitor is paused or not.
+  """
+  
+  for key in PAUSEABLE: panels[key].setPaused(overwrite or monitorIsPaused or (key not in PAGES[currentPage] and key not in PAGE_S))
+
+def showMenu(stdscr, popup, title, options, initialSelection):
+  """
+  Provides menu with options laid out in a single column. User can cancel
+  selection with the escape key, in which case this proives -1. Otherwise this
+  returns the index of the selection. If initialSelection is -1 then the first
+  option is used and the carrot indicating past selection is ommitted.
+  """
+  
+  selection = initialSelection if initialSelection != -1 else 0
+  
+  if popup.win:
+    if not popup.lock.acquire(False): return -1
+    try:
+      curses.cbreak() # wait indefinitely for key presses (no timeout)
+      
+      # uses smaller dimentions more fitting for small content
+      popup.height = len(options) + 2
+      
+      newWidth = max([len(label) for label in options]) + 9
+      popup.recreate(stdscr, popup.startY, newWidth)
+      
+      key = 0
+      while key not in (curses.KEY_ENTER, 10, ord(' ')):
+        popup.clear()
+        popup.win.box()
+        popup.addstr(0, 0, title, util.LABEL_ATTR)
+        
+        for i in range(len(options)):
+          label = options[i]
+          format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
+          tab = "> " if i == initialSelection else "  "
+          popup.addstr(i + 1, 2, tab)
+          popup.addstr(i + 1, 4, " %s " % label, format)
+        
+        popup.refresh()
+        key = stdscr.getch()
+        if key == curses.KEY_UP: selection = max(0, selection - 1)
+        elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1)
+        elif key == 27: selection, key = -1, curses.KEY_ENTER # esc - cancel
+      
+      # reverts popup dimensions and conn panel label
+      popup.height = 9
+      popup.recreate(stdscr, popup.startY, 80)
+      
+      curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+    finally:
+      cursesLock.release()
+  
+  return selection
+
 def setEventListening(loggedEvents, conn, logListener):
   """
   Tries to set events being listened for, displaying error for any event
@@ -100,12 +157,14 @@
   """
   eventsSet = False
   
+  # adds events used for panels to function if not already included
+  connEvents = loggedEvents.union(set(REQ_EVENTS))
+  
+  # removes UNKNOWN since not an actual event type
+  connEvents.discard("UNKNOWN")
+  
   while not eventsSet:
     try:
-      # adds BW events if not already included (so bandwidth monitor will work)
-      # removes UNKNOWN since not an actual event type
-      connEvents = loggedEvents.union(set(REQ_EVENTS))
-      connEvents.discard("UNKNOWN")
       conn.set_events(connEvents)
       eventsSet = True
     except TorCtl.ErrorReply, exc:
@@ -115,13 +174,17 @@
         start = msg.find("event \"") + 7
         end = msg.rfind("\"")
         eventType = msg[start:end]
-        if eventType == "BW": raise exc # bandwidth monitoring won't work - best to crash
         
         # removes and notes problem
-        loggedEvents.remove(eventType)
-        logListener.monitor_event("WARN", "Unsupported event type: %s" % eventType)
-      else:
-        raise exc
+        connEvents.discard(eventType)
+        if eventType in loggedEvents: loggedEvents.remove(eventType)
+        
+        if eventType in REQ_EVENTS:
+          if eventType == "BW": msg = "(bandwidth panel 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))
+        else: logListener.monitor_event("WARN", "Unsupported event type: %s" % eventType)
     except TorCtl.TorCtlClosed:
       return []
   
@@ -226,10 +289,10 @@
       if not isUnresponsive and panels["log"].getHeartbeat() >= 10:
         isUnresponsive = True
         panels["log"].monitor_event("NOTICE", "Relay unresponsive (last heartbeat: %s)" % time.ctime(panels["log"].lastHeartbeat))
-      elif isUnresponsive and panels["log"].getHeartbeat() < 5:
-        # this really shouldn't happen - BW events happen every second...
+      elif isUnresponsive and panels["log"].getHeartbeat() < 10:
+        # shouldn't happen unless Tor freezes for a bit - BW events happen every second...
         isUnresponsive = False
-        panels["log"].monitor_event("WARN", "Relay resumed")
+        panels["log"].monitor_event("NOTICE", "Relay resumed")
       
       # if it's been at least five seconds since the last refresh of connection listing, update
       currentTime = time.time()
@@ -253,7 +316,7 @@
       
       # pauses panels that aren't visible to prevent events from accumilating
       # (otherwise they'll wait on the curses lock which might get demanding)
-      for key in PAUSEABLE: panels[key].setPaused(isPaused or (key not in PAGES[page] and key not in PAGE_S))
+      setPauseState(panels, isPaused, page)
       
       panels["control"].page = page + 1
       panels["control"].refresh()
@@ -262,7 +325,7 @@
       cursesLock.acquire()
       try:
         isPaused = not isPaused
-        for key in PAUSEABLE: panels[key].setPaused(isPaused or key not in PAGES[page])
+        setPauseState(panels, isPaused, page)
         panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
       finally:
         cursesLock.release()
@@ -270,7 +333,7 @@
       # displays popup for current page's controls
       cursesLock.acquire()
       try:
-        for key in PAUSEABLE: panels[key].setPaused(True)
+        setPauseState(panels, isPaused, page, True)
         
         # lists commands
         popup = panels["popup"]
@@ -281,7 +344,11 @@
         if page == 0:
           bwVisibleLabel = "visible" if panels["bandwidth"].isVisible else "hidden"
           popup.addfstr(1, 2, "b: toggle bandwidth panel (<b>%s</b>)" % bwVisibleLabel)
-          popup.addstr(1, 41, "e: change logged events")
+          
+          # 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")
         if page == 1:
           popup.addstr(1, 2, "up arrow: scroll up a line")
           popup.addstr(1, 41, "down arrow: scroll down a line")
@@ -317,18 +384,37 @@
         stdscr.getch()
         curses.halfdelay(REFRESH_RATE * 10)
         
-        for key in PAUSEABLE: panels[key].setPaused(isPaused or key not in PAGES[page])
+        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('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
+      
+      # hides top label of bandwidth panel and pauses panels
+      panels["bandwidth"].showLabel = False
+      panels["bandwidth"].redraw()
+      setPauseState(panels, isPaused, page, True)
+      
+      selection = showMenu(stdscr, panels["popup"], "Update Interval:", options, initialSelection)
+      
+      # reverts changes made for popup
+      panels["bandwidth"].showLabel = True
+      setPauseState(panels, isPaused, page)
+      
+      # applies new setting
+      if selection != -1:
+        panels["bandwidth"].setUpdateInterval(selection)
     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()
       try:
-        for key in PAUSEABLE: panels[key].setPaused(True)
+        setPauseState(panels, isPaused, page, True)
         
         # provides prompt
         panels["control"].setMsg("Events to log: ")
@@ -360,7 +446,7 @@
         curses.noecho()
         curses.halfdelay(REFRESH_RATE * 10) # evidenlty previous tweaks reset this...
         
-        # TODO: it would be nice to quit on esc, but looks like this might not be possible...
+        # it would be nice to quit on esc, but looks like this might not be possible...
         if eventsInput != "":
           try:
             expandedEvents = logPanel.expandEvents(eventsInput)
@@ -372,7 +458,7 @@
             time.sleep(2)
         
         panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-        for key in PAUSEABLE: panels[key].setPaused(isPaused or key not in PAGES[page])
+        setPauseState(panels, isPaused, page)
       finally:
         cursesLock.release()
     elif key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingCounter != -1:
@@ -383,80 +469,42 @@
       panels["conn"].sortConnections()
     elif page == 1 and (key == ord('l') or key == ord('L')):
       # provides menu to pick identification info listed for connections
-      cursesLock.acquire()
-      try:
-        for key in PAUSEABLE: panels[key].setPaused(True)
-        curses.cbreak() # wait indefinitely for key presses (no timeout)
-        popup = panels["popup"]
+      optionTypes = [connPanel.LIST_IP, connPanel.LIST_HOSTNAME, connPanel.LIST_FINGERPRINT, connPanel.LIST_NICKNAME]
+      options = [connPanel.LIST_LABEL[sortType] for sortType in optionTypes]
+      initialSelection = panels["conn"].listingType   # enums correspond to index
+      
+      # hides top label of conn panel and pauses panels
+      panels["conn"].showLabel = False
+      panels["conn"].redraw()
+      setPauseState(panels, isPaused, page, True)
+      
+      selection = showMenu(stdscr, panels["popup"], "List By:", options, initialSelection)
+      
+      # reverts changes made for popup
+      panels["conn"].showLabel = True
+      setPauseState(panels, isPaused, page)
+      
+      # applies new setting
+      if selection != -1 and optionTypes[selection] != panels["conn"].listingType:
+        panels["conn"].listingType = optionTypes[selection]
         
-        # uses smaller dimentions more fitting for small content
-        panels["popup"].height = 6
-        panels["popup"].recreate(stdscr, startY, 20)
-        
-        # hides top label of conn panel
-        panels["conn"].showLabel = False
-        panels["conn"].redraw()
-        
-        selection = panels["conn"].listingType    # starts with current option selected
-        options = [connPanel.LIST_IP, connPanel.LIST_HOSTNAME, connPanel.LIST_FINGERPRINT, connPanel.LIST_NICKNAME]
-        key = 0
-        
-        while key not in (curses.KEY_ENTER, 10, ord(' ')):
-          popup.clear()
-          popup.win.box()
-          popup.addstr(0, 0, "List By:", util.LABEL_ATTR)
+        if panels["conn"].listingType == connPanel.LIST_HOSTNAME:
+          curses.halfdelay(10) # refreshes display every second until done resolving
+          panels["control"].resolvingCounter = panels["conn"].resolver.totalResolves - panels["conn"].resolver.unresolvedQueue.qsize()
           
-          for i in range(len(options)):
-            sortType = options[i]
-            format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
-            
-            if panels["conn"].listingType == sortType: tab = "> "
-            else: tab = "  "
-            sortLabel = connPanel.LIST_LABEL[sortType]
-            
-            popup.addstr(i + 1, 2, tab)
-            popup.addstr(i + 1, 4, sortLabel, format)
-          
-          popup.refresh()
-          key = stdscr.getch()
-          if key == curses.KEY_UP: selection = max(0, selection - 1)
-          elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1)
-          elif key == 27:
-            # esc - cancel
-            selection = panels["conn"].listingType
-            key = curses.KEY_ENTER
+          resolver = panels["conn"].resolver
+          resolver.setPaused(not panels["conn"].allowDNS)
+          for connEntry in panels["conn"].connections: resolver.resolve(connEntry[connPanel.CONN_F_IP])
+        else:
+          panels["control"].resolvingCounter = -1
+          panels["conn"].resolver.setPaused(True)
         
-        # reverts popup dimensions and conn panel label
-        panels["popup"].height = 9
-        panels["popup"].recreate(stdscr, startY, 80)
-        panels["conn"].showLabel = True
-        
-        # applies new setting
-        pickedOption = options[selection]
-        if pickedOption != panels["conn"].listingType:
-          panels["conn"].listingType = pickedOption
-          
-          if panels["conn"].listingType == connPanel.LIST_HOSTNAME:
-            curses.halfdelay(10) # refreshes display every second until done resolving
-            panels["control"].resolvingCounter = panels["conn"].resolver.totalResolves - panels["conn"].resolver.unresolvedQueue.qsize()
-            
-            resolver = panels["conn"].resolver
-            resolver.setPaused(not panels["conn"].allowDNS)
-            for connEntry in panels["conn"].connections: resolver.resolve(connEntry[connPanel.CONN_F_IP])
-          else:
-            panels["control"].resolvingCounter = -1
-            panels["conn"].resolver.setPaused(True)
-          
-          panels["conn"].sortConnections()
-        
-        curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
-      finally:
-        cursesLock.release()
+        panels["conn"].sortConnections()
     elif page == 1 and panels["conn"].isCursorEnabled and key in (curses.KEY_ENTER, 10, ord(' ')):
       # provides details on selected connection
       cursesLock.acquire()
       try:
-        for key in PAUSEABLE: panels[key].setPaused(True)
+        setPauseState(panels, isPaused, page, True)
         popup = panels["popup"]
         
         # reconfigures connection panel to accomidate details dialog
@@ -468,9 +516,10 @@
         resolver.setPaused(not panels["conn"].allowDNS)
         relayLookupCache = {} # temporary cache of entry -> (ns data, desc data)
         
+        curses.cbreak() # wait indefinitely for key presses (no timeout)
+        key = 0
+        
         while key not in (curses.KEY_ENTER, 10, ord(' ')):
-          key = 0
-          curses.cbreak() # wait indefinitely for key presses (no timeout)
           popup.clear()
           popup.win.box()
           popup.addstr(0, 0, "Connection Details:", util.LABEL_ATTR)
@@ -577,6 +626,7 @@
         panels["conn"].showLabel = True
         panels["conn"].showingDetails = False
         resolver.setPaused(not panels["conn"].allowDNS and panels["conn"].listingType == connPanel.LIST_HOSTNAME)
+        setPauseState(panels, isPaused, page)
         curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
       finally:
         cursesLock.release()
@@ -585,7 +635,7 @@
       # set ordering for connection listing
       cursesLock.acquire()
       try:
-        for key in PAUSEABLE: panels[key].setPaused(True)
+        setPauseState(panels, isPaused, page, True)
         curses.cbreak() # wait indefinitely for key presses (no timeout)
         
         # lists event types
@@ -644,6 +694,7 @@
         if len(selections) == 3:
           panels["conn"].sortOrdering = selections
           panels["conn"].sortConnections()
+        setPauseState(panels, isPaused, page)
         curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
       finally:
         cursesLock.release()

Modified: arm/trunk/interface/headerPanel.py
===================================================================
--- arm/trunk/interface/headerPanel.py	2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/headerPanel.py	2009-07-30 07:09:47 UTC (rev 20198)
@@ -3,7 +3,6 @@
 # Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
 
 import os
-import curses
 import socket
 from TorCtl import TorCtl
 
@@ -62,7 +61,6 @@
       # Line 2 (authentication label red if open, green if credentials required)
       dirPortLabel = "Dir Port: %s, " % self.vals["DirPort"] if self.vals["DirPort"] != "0" else ""
       
-      # TODO: if both cookie and password are set then which takes priority?
       if self.vals["IsPasswordAuthSet"]: controlPortAuthLabel = "password"
       elif self.vals["IsCookieAuthSet"]: controlPortAuthLabel = "cookie"
       else: controlPortAuthLabel = "open"

Modified: arm/trunk/interface/hostnameResolver.py
===================================================================
--- arm/trunk/interface/hostnameResolver.py	2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/hostnameResolver.py	2009-07-30 07:09:47 UTC (rev 20198)
@@ -13,11 +13,11 @@
 RESOLVER_CACHE_TRIM_SIZE = 2000   # entries removed when max cache size reached
 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)")
 
-class HostnameResolver(Thread):
+class HostnameResolver():
   """
-  Background thread that quietly performs reverse DNS lookup of address with
-  caching. This is non-blocking, providing None in the case of errors or
-  new requests.
+  Provides background threads that quietly performs reverse DNS lookup of
+  address with caching. This is non-blocking, providing None in the case of
+  errors or new requests.
   """
   
   # Resolutions are made using os 'host' calls as opposed to 'gethostbyaddr' in
@@ -30,7 +30,6 @@
   # however, I didn't find this to be the case. As always, suggestions welcome!
   
   def __init__(self):
-    Thread.__init__(self)
     self.resolvedCache = {}           # IP Address => (hostname, age) (None if couldn't be resolved)
     self.unresolvedQueue = Queue.Queue()
     self.recentQueries = []           # recent resolution requests to prevent duplicate requests

Modified: arm/trunk/interface/logPanel.py
===================================================================
--- arm/trunk/interface/logPanel.py	2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/logPanel.py	2009-07-30 07:09:47 UTC (rev 20198)
@@ -3,7 +3,6 @@
 # Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
 
 import time
-import curses
 from curses.ascii import isprint
 from TorCtl import TorCtl
 
@@ -153,7 +152,7 @@
     Notes event and redraws log. If paused it's held in a temporary buffer.
     """
     
-    self.lastHeartbeat = time.time()
+    if not type.startswith("ARM"): self.lastHeartbeat = time.time()
     
     # strips control characters to avoid screwing up the terminal
     msg = "".join([char for char in msg if isprint(char)])
@@ -209,7 +208,7 @@
             self.addstr(lineCount, 0, line, util.getColor(color))
             lineCount += 1
           else:
-            (line1, line2) = self._splitLine(line, self.maxX)
+            (line1, line2) = splitLine(line, self.maxX)
             self.addstr(lineCount, 0, line1, util.getColor(color))
             self.addstr(lineCount + 1, 0, line2, util.getColor(color))
             lineCount += 2
@@ -240,27 +239,29 @@
     """
     
     return time.time() - self.lastHeartbeat
+
+def splitLine(message, x):
+  """
+  Divides message into two lines, attempting to do it on a wordbreak.
+  """
   
-  # divides long message to cover two lines
-  def _splitLine(self, message, x):
-    # divides message into two lines, attempting to do it on a wordbreak
-    lastWordbreak = message[:x].rfind(" ")
-    if x - lastWordbreak < 10:
-      line1 = message[:lastWordbreak]
-      line2 = "  %s" % message[lastWordbreak:].strip()
-    else:
-      # over ten characters until the last word - dividing
-      line1 = "%s-" % message[:x - 2]
-      line2 = "  %s" % message[x - 2:].strip()
+  lastWordbreak = message[:x].rfind(" ")
+  if x - lastWordbreak < 10:
+    line1 = message[:lastWordbreak]
+    line2 = "  %s" % message[lastWordbreak:].strip()
+  else:
+    # over ten characters until the last word - dividing
+    line1 = "%s-" % message[:x - 2]
+    line2 = "  %s" % message[x - 2:].strip()
+  
+  # ends line with ellipsis if too long
+  if len(line2) > x:
+    lastWordbreak = line2[:x - 4].rfind(" ")
     
-    # ends line with ellipsis if too long
-    if len(line2) > x:
-      lastWordbreak = line2[:x - 4].rfind(" ")
-      
-      # doesn't use wordbreak if it's a long word or the whole line is one 
-      # word (picking up on two space indent to have index 1)
-      if x - lastWordbreak > 10 or lastWordbreak == 1: lastWordbreak = x - 4
-      line2 = "%s..." % line2[:lastWordbreak]
-    
-    return (line1, line2)
+    # doesn't use wordbreak if it's a long word or the whole line is one 
+    # word (picking up on two space indent to have index 1)
+    if x - lastWordbreak > 10 or lastWordbreak == 1: lastWordbreak = x - 4
+    line2 = "%s..." % line2[:lastWordbreak]
+  
+  return (line1, line2)
 

Modified: arm/trunk/interface/util.py
===================================================================
--- arm/trunk/interface/util.py	2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/util.py	2009-07-30 07:09:47 UTC (rev 20198)
@@ -83,6 +83,7 @@
     self.startY = -1          # top in parent window when created
     self.height = height      # preferred (max) height of panel, -1 if infinite
     self.isDisplaced = False  # window isn't in the right location - don't redraw
+    self.maxY, self.maxX = -1, -1
     self._resetBounds()       # sets last known dimensions of win (maxX and maxY)
   
   def redraw(self):
@@ -114,7 +115,7 @@
     newHeight = max(0, y - startY)
     if self.height != -1: newHeight = min(newHeight, self.height)
     
-    if self.startY != startY or newHeight > self.maxY or self.isDisplaced or (self.maxX > maxX and maxX != -1):
+    if self.startY != startY or newHeight > self.maxY or self.isDisplaced or (self.maxX != maxX and maxX != -1):
       # window growing or moving - recreate
       self.startY = startY
       startY = min(startY, y - 1) # better create a displaced window than leave it as None

Modified: arm/trunk/readme.txt
===================================================================
--- arm/trunk/readme.txt	2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/readme.txt	2009-07-30 07:09:47 UTC (rev 20198)
@@ -1,6 +1,7 @@
-arm (arm relay monitor) - Terminal status monitor for Tor relays.
+arm (anonymizing relay monitor) - Terminal status monitor for Tor relays.
 Developed by Damian Johnson (www.atagar.com - atagar1 at gmail.com)
 All code under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+Project page: www.atagar.com/arm
 
 Description:
 Command line application for monitoring Tor relays, providing real time status information such as the current configuration, bandwidth usage, message log, current connections, etc. This uses a curses interface much like 'top' does for system usage.

Added: arm/trunk/screenshot_page1.png
===================================================================
(Binary files differ)


Property changes on: arm/trunk/screenshot_page1.png
___________________________________________________________________
Added: svn:mime-type
   + application/octet-stream

Added: arm/trunk/screenshot_page2.png
===================================================================
(Binary files differ)


Property changes on: arm/trunk/screenshot_page2.png
___________________________________________________________________
Added: svn:mime-type
   + application/octet-stream



More information about the tor-commits mailing list