[or-cvs] r20493: {arm} Several substantial features (last tasks for arm's todo list (in arm/trunk: . interface)

atagar at seul.org atagar at seul.org
Mon Sep 7 05:21:43 UTC 2009


Author: atagar
Date: 2009-09-07 01:21:42 -0400 (Mon, 07 Sep 2009)
New Revision: 20493

Added:
   arm/trunk/interface/cpuMemMonitor.py
Modified:
   arm/trunk/interface/connCountMonitor.py
   arm/trunk/interface/connPanel.py
   arm/trunk/interface/controller.py
   arm/trunk/interface/headerPanel.py
   arm/trunk/interface/logPanel.py
   arm/trunk/interface/util.py
   arm/trunk/readme.txt
Log:
Several substantial features (last tasks for arm's todo list).
added: scroll bars for connections listing and event log
added: made log scrollable (feature request by StrangeCharm)
added: regular expression filtering for log (feature request by StrangeCharm)
added: connection uptimes (time since connection was first made)
added: identifying client from server connections and providing popup for client circuits
added: graph for system resource usage (cpu/memory)
change: removed cursor toggling option for connection page
fix: minor display issue when changing event types



Modified: arm/trunk/interface/connCountMonitor.py
===================================================================
--- arm/trunk/interface/connCountMonitor.py	2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/interface/connCountMonitor.py	2009-09-07 05:21:42 UTC (rev 20493)
@@ -12,13 +12,14 @@
 class ConnCountMonitor(graphPanel.GraphStats, TorCtl.PostEventListener):
   """
   Tracks number of connections, using cached values in connPanel if recent
-  enough (otherwise retrieved independently).
+  enough (otherwise retrieved independently). Client connections are counted
+  as outbound.
   """
   
   def __init__(self, connectionPanel):
     graphPanel.GraphStats.__init__(self)
     TorCtl.PostEventListener.__init__(self)
-    graphPanel.GraphStats.initialize(self, connPanel.TYPE_COLORS["inbound"], connPanel.TYPE_COLORS["outbound"], 10)
+    graphPanel.GraphStats.initialize(self, "green", "cyan", 10)
     self.connectionPanel = connectionPanel  # connection panel, used to limit netstat calls
   
   def bandwidth_event(self, event):
@@ -28,7 +29,7 @@
     if self.connectionPanel.lastUpdate + 1 >= time.time():
       # reuses netstat results if recent enough
       counts = self.connectionPanel.connectionCount
-      self._processEvent(counts[0], counts[1])
+      self._processEvent(counts[0], counts[1] + counts[2])
     else:
       # cached results stale - requery netstat
       inbound, outbound, control = 0, 0, 0

Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py	2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/interface/connPanel.py	2009-09-07 05:21:42 UTC (rev 20493)
@@ -17,15 +17,15 @@
 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", "localhost": "yellow"}
-TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "control": 2, "localhost": 3} # defines ordering
+TYPE_COLORS = {"inbound": "green", "outbound": "blue", "client": "cyan", "control": "red", "localhost": "yellow"}
+TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "client": 2, "control": 3, "localhost": 4} # 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)
+CONN_TYPE, CONN_L_IP, CONN_L_PORT, CONN_F_IP, CONN_F_PORT, CONN_COUNTRY, CONN_TIME = range(7)
 
 # 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)
+ORD_TYPE, ORD_FOREIGN_LISTING, ORD_SRC_LISTING, ORD_DST_LISTING, ORD_COUNTRY, ORD_FOREIGN_PORT, ORD_SRC_PORT, ORD_DST_PORT, ORD_TIME = range(9)
 SORT_TYPES = [(ORD_TYPE, "Connection Type",
                 lambda x, y: TYPE_WEIGHTS[x[CONN_TYPE]] - TYPE_WEIGHTS[y[CONN_TYPE]]),
               (ORD_FOREIGN_LISTING, "Listing (Foreign)", None),
@@ -38,7 +38,9 @@
               (ORD_SRC_PORT, "Port (Source)",
                 lambda x, y: int(x[CONN_F_PORT] if x[CONN_TYPE] == "inbound" else x[CONN_L_PORT]) - int(y[CONN_F_PORT] if y[CONN_TYPE] == "inbound" else y[CONN_L_PORT])),
               (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]))]
+                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])),
+              (ORD_TIME, "Connection Time",
+                lambda x, y: cmp(-x[CONN_TIME], -y[CONN_TIME]))]
 
 # provides bi-directional mapping of sorts with their associated labels
 def getSortLabel(sortType, withColor = False):
@@ -62,6 +64,7 @@
         elif label.startswith("Port"): color = "green"
         elif label == "Bandwidth": color = "cyan"
         elif label == "Country Code": color = "yellow"
+        elif label == "Connection Time": color = "magenta"
       
       if color: return "<%s>%s</%s>" % (color, label, color)
       else: return label
@@ -106,6 +109,8 @@
     self.providedGeoipWarning = False
     self.orconnStatusCache = []           # cache for 'orconn-status' calls
     self.orconnStatusCacheValid = False   # indicates if cache has been invalidated
+    self.clientConnectionCache = None     # listing of nicknames for our client connections
+    self.clientConnectionLock = RLock()   # lock for clientConnectionCache
     
     self.isCursorEnabled = True
     self.cursorSelection = None
@@ -121,11 +126,17 @@
     self.connections = []
     self.connectionsLock = RLock()    # limits modifications of connections
     
-    # count of total inbound, outbound, and control connections
-    self.connectionCount = [0, 0, 0]
+    # count of total inbound, outbound, client, and control connections
+    self.connectionCount = [0, 0, 0, 0]
     
     self.reset()
   
+  # change in client circuits
+  def circ_status_event(self, event):
+    self.clientConnectionLock.acquire()
+    self.clientConnectionCache = None
+    self.clientConnectionLock.release()
+  
   # when consensus changes update fingerprint mappings
   def new_consensus_event(self, event):
     self.orconnStatusCacheValid = False
@@ -147,7 +158,9 @@
           if k in self.nicknameLookupCache.keys(): del self.nicknameLookupCache[k]
       
       # gets consensus data for the new description
-      nsData = self.conn.get_network_status("id/%s" % fingerprint)
+      try: nsData = self.conn.get_network_status("id/%s" % fingerprint)
+      except TorCtl.ErrorReply: return
+      
       if len(nsData) > 1:
         # multiple records for fingerprint (shouldn't happen)
         self.logger.monitor_event("WARN", "Multiple consensus entries for fingerprint: %s" % fingerprint)
@@ -178,9 +191,18 @@
     
     if self.isPaused or not self.pid: return
     self.connectionsLock.acquire()
+    self.clientConnectionLock.acquire()
     try:
+      if self.clientConnectionCache == None:
+        # client connection cache was invalidated
+        self.clientConnectionCache = _getClientConnections(self.conn)
+      
+      connTimes = {} # mapping of ip/port to connection time
+      for entry in self.connections:
+        connTimes[(entry[CONN_F_IP], entry[CONN_F_PORT])] = entry[CONN_TIME]
+      
       self.connections = []
-      self.connectionCount = [0, 0, 0]
+      self.connectionCount = [0, 0, 0, 0]
       
       # 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
@@ -200,10 +222,23 @@
             self.connectionCount[0] += 1
           elif localPort == self.controlPort:
             type = "control"
-            self.connectionCount[2] += 1
+            self.connectionCount[3] += 1
           else:
-            type = "outbound"
-            self.connectionCount[1] += 1
+            fingerprint = self.getFingerprint(foreignIP, foreignPort)
+            nickname = self.getNickname(foreignIP, foreignPort)
+            
+            isClient = False
+            for clientName in self.clientConnectionCache:
+              if nickname == clientName or (len(clientName) > 1 and clientName[0] == "$" and fingerprint == clientName[1:]):
+                isClient = True
+                break
+            
+            if isClient:
+              type = "client"
+              self.connectionCount[2] += 1
+            else:
+              type = "outbound"
+              self.connectionCount[1] += 1
           
           try:
             countryCodeQuery = "ip-to-country/%s" % foreign[:foreign.find(":")]
@@ -214,7 +249,10 @@
               self.logger.monitor_event("WARN", "Tor geoip database is unavailable.")
               self.providedGeoipWarning = True
           
-          self.connections.append((type, localIP, localPort, foreignIP, foreignPort, countryCode))
+          if (foreignIP, foreignPort) in connTimes: connTime = connTimes[(foreignIP, foreignPort)]
+          else: connTime = time.time()
+          
+          self.connections.append((type, localIP, localPort, foreignIP, foreignPort, countryCode, connTime))
       except IOError:
         # netstat call failed
         self.logger.monitor_event("WARN", "Unable to query netstat for new connections")
@@ -236,7 +274,10 @@
         except socket.error:
           selfCountryCode = "??"
         
-        self.localhostEntry = (("localhost", selfAddress, selfPort, selfAddress, selfPort, selfCountryCode), selfFingerprint)
+        if (selfAddress, selfPort) in connTimes: connTime = connTimes[(selfAddress, selfPort)]
+        else: connTime = time.time()
+        
+        self.localhostEntry = (("localhost", selfAddress, selfPort, selfAddress, selfPort, selfCountryCode, connTime), selfFingerprint)
         self.connections.append(self.localhostEntry[0])
       else:
         self.localhostEntry = None
@@ -248,6 +289,7 @@
       if self.listingType != LIST_HOSTNAME: self.sortConnections()
     finally:
       self.connectionsLock.release()
+      self.clientConnectionLock.release()
   
   def handleKey(self, key):
     # cursor or scroll movement
@@ -281,8 +323,6 @@
         else: self.scroll = newLoc
       finally:
         self.connectionsLock.release()
-    elif key == ord('c') or key == ord('C'):
-      self.isCursorEnabled = not self.isCursorEnabled
     elif key == ord('r') or key == ord('R'):
       self.allowDNS = not self.allowDNS
       if not self.allowDNS: self.resolver.setPaused(True)
@@ -299,12 +339,20 @@
         if self.listingType == LIST_HOSTNAME: self.sortConnections()
         
         self.clear()
-        if self.showLabel: self.addstr(0, 0, "Connections (%i inbound, %i outbound, %i control):" % tuple(self.connectionCount), util.LABEL_ATTR)
+        clientCountLabel = "" if self.connectionCount[2] == 0 else "%i client, " % self.connectionCount[2]
+        if self.showLabel: self.addstr(0, 0, "Connections (%i inbound, %i outbound, %s%i control):" % (self.connectionCount[0], self.connectionCount[1], clientCountLabel, self.connectionCount[3]), util.LABEL_ATTR)
         
         if self.connections:
           listingHeight = self.maxY - 1
-          if self.showingDetails: listingHeight -= 8
+          currentTime = time.time()
           
+          if self.showingDetails:
+            listingHeight -= 8
+            isScrollBarVisible = len(self.connections) > self.maxY - 9
+          else:
+            isScrollBarVisible = len(self.connections) > self.maxY - 1
+          xOffset = 3 if isScrollBarVisible else 0 # content offset for scroll bar
+          
           # ensure cursor location and scroll top are within bounds
           self.cursorLoc = max(min(self.cursorLoc, len(self.connections) - 1), 0)
           self.scroll = max(min(self.scroll, len(self.connections) - listingHeight), 0)
@@ -345,19 +393,24 @@
                 else: dst = self.getFingerprint(entry[CONN_F_IP], entry[CONN_F_PORT])
                 dst = "%-41s" % dst
               else:
-                src = "%-11s" % self.nickname
+                src = "%-26s" % self.nickname
                 if entry[CONN_TYPE] == "control": dst = self.nickname
                 else: dst = self.getNickname(entry[CONN_F_IP], entry[CONN_F_PORT])
-                dst = "%-41s" % dst
+                dst = "%-26s" % dst
               
               if type == "inbound": src, dst = dst, src
-              lineEntry = "<%s>%s -->   %s   (<b>%s</b>)</%s>" % (color, src, dst, type.upper(), color)
+              lineEntry = "<%s>%s -->  %s %5s (<b>%s</b>)</%s>" % (color, src, dst, util.getTimeLabel(currentTime - entry[CONN_TIME], 1), type.upper(), color)
               if self.isCursorEnabled and entry == self.cursorSelection:
                 lineEntry = "<h>%s</h>" % lineEntry
               
-              offset = 0 if not self.showingDetails else 8
-              self.addfstr(lineNum + offset, 0, lineEntry)
+              yOffset = 0 if not self.showingDetails else 8
+              self.addfstr(lineNum + yOffset, xOffset, lineEntry)
             lineNum += 1
+          
+          if isScrollBarVisible:
+            topY = 9 if self.showingDetails else 1
+            bottomEntry = self.scroll + self.maxY - 9 if self.showingDetails else self.scroll + self.maxY - 1
+            util.drawScrollBar(self, topY, self.maxY - 1, self.scroll, bottomEntry, len(self.connections))
         
         self.refresh()
       finally:
@@ -382,7 +435,7 @@
       match = None
       
       # orconn-status provides a listing of Tor's current connections - used to
-      # eliminated ambiguity for inbound connections
+      # eliminated ambiguity for outbound connections
       if not self.orconnStatusCacheValid:
         self.orconnStatusCache, isOdd = [], True
         self.orconnStatusCacheValid = True
@@ -521,6 +574,7 @@
   if not nsList:
     try: nsList = conn.get_network_status()
     except TorCtl.TorCtlClosed: nsList = []
+    except TorCtl.ErrorReply: nsList = []
   
   for entry in nsList:
     if entry.ip in ipToFingerprint.keys(): ipToFingerprint[entry.ip].append((entry.orport, entry.idhex, entry.nickname))
@@ -528,3 +582,18 @@
   
   return ipToFingerprint
 
+# provides client relays we're currently attached to (first hops in circuits)
+# this consists of the nicknames and ${fingerprint} if unnamed
+def _getClientConnections(conn):
+  clients = []
+  
+  try:
+    for line in conn.get_info("circuit-status")["circuit-status"].split("\n"):
+      components = line.split()
+      if len(components) > 3: clients += [components[2].split(",")[0]]
+  except TorCtl.ErrorReply: pass
+  except TorCtl.TorCtlClosed: pass
+  except socket.error: pass
+  
+  return clients
+

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/interface/controller.py	2009-09-07 05:21:42 UTC (rev 20493)
@@ -6,6 +6,7 @@
 Curses (terminal) interface for the arm relay status monitor.
 """
 
+import re
 import os
 import time
 import curses
@@ -21,11 +22,13 @@
 
 import util
 import bandwidthMonitor
+import cpuMemMonitor
 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)
+MAX_REGEX_FILTERS = 5   # maximum number of previous regex filters that'll be remembered
 
 # enums for message in control label
 CTL_HELP, CTL_PAUSED = range(2)
@@ -39,7 +42,7 @@
 PAUSEABLE = ["header", "graph", "log", "conn"]
 
 # events needed for panels other than the event log
-REQ_EVENTS = ["BW", "NEWDESC", "NEWCONSENSUS"]
+REQ_EVENTS = ["BW", "NEWDESC", "NEWCONSENSUS", "CIRC"]
 
 class ControlPanel(util.Panel):
   """ Draws single line label for interface controls. """
@@ -265,6 +268,7 @@
   
   # statistical monitors for graph
   panels["graph"].addStats("bandwidth", bandwidthMonitor.BandwidthMonitor(conn))
+  panels["graph"].addStats("cpu / memory", cpuMemMonitor.CpuMemMonitor(panels["header"]))
   panels["graph"].addStats("connection count", connCountMonitor.ConnCountMonitor(panels["conn"]))
   panels["graph"].setStats("bandwidth")
   
@@ -272,6 +276,7 @@
   sighupTracker = sighupListener()
   conn.add_event_listener(panels["log"])
   conn.add_event_listener(panels["graph"].stats["bandwidth"])
+  conn.add_event_listener(panels["graph"].stats["cpu / memory"])
   conn.add_event_listener(panels["graph"].stats["connection count"])
   conn.add_event_listener(panels["conn"])
   conn.add_event_listener(sighupTracker)
@@ -284,6 +289,7 @@
   isPaused = False          # if true updates are frozen
   page = 0
   netstatRefresh = time.time()  # time of last netstat refresh
+  regexFilters = []             # previously used log regex filters
   
   while True:
     # tried only refreshing when the screen was resized but it caused a
@@ -329,7 +335,7 @@
       
       # if it's been at least five seconds since the last refresh of connection listing, update
       currentTime = time.time()
-      if not panels["conn"].isPaused and currentTime - netstatRefresh >= 5:
+      if not panels["conn"].isPaused and (currentTime - netstatRefresh >= 5):
         panels["conn"].reset()
         netstatRefresh = currentTime
       
@@ -392,6 +398,9 @@
           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")
+          
+          regexLabel = "enabled" if panels["log"].regexFilter else "disabled"
+          popup.addfstr(3, 2, "r: log regex filter (<b>%s</b>)" % regexLabel)
         if page == 1:
           popup.addstr(1, 2, "up arrow: scroll up a line")
           popup.addstr(1, 41, "down arrow: scroll down a line")
@@ -407,7 +416,8 @@
           popup.addfstr(4, 41, "r: permit DNS resolution (<b>%s</b>)" % allowDnsLabel)
           
           popup.addstr(5, 2, "s: sort ordering")
-          popup.addfstr(5, 41, "c: toggle cursor (<b>%s</b>)" % ("on" if panels["conn"].isCursorEnabled else "off"))
+          popup.addstr(5, 41, "c: client circuits")
+          #popup.addfstr(5, 41, "c: toggle cursor (<b>%s</b>)" % ("on" if panels["conn"].isCursorEnabled else "off"))
         elif page == 2:
           popup.addstr(1, 2, "up arrow: scroll up a line")
           popup.addstr(1, 41, "down arrow: scroll down a line")
@@ -421,7 +431,6 @@
           popup.addfstr(3, 41, "n: line numbering (<b>%s</b>)" % lineNumLabel)
         
         popup.addstr(7, 2, "Press any key...")
-        
         popup.refresh()
         
         curses.cbreak()
@@ -503,12 +512,16 @@
         
         # lists event types
         popup = panels["popup"]
+        popup.height = 10
+        popup.recreate(stdscr, popup.startY, 80)
+        
         popup.clear()
+        popup.win.box()
         popup.addstr(0, 0, "Event Types:", util.LABEL_ATTR)
         lineNum = 1
         for line in logPanel.EVENT_LISTING.split("\n"):
           line = line[6:]
-          popup.addstr(lineNum, 0, line)
+          popup.addstr(lineNum, 1, line)
           lineNum += 1
         popup.refresh()
         
@@ -533,10 +546,82 @@
             panels["control"].redraw()
             time.sleep(2)
         
+        # reverts popup dimensions
+        popup.height = 9
+        popup.recreate(stdscr, popup.startY, 80)
+        
         panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
         setPauseState(panels, isPaused, page)
       finally:
         cursesLock.release()
+    elif page == 0 and (key == ord('r') or key == ord('R')):
+      # provides menu to pick previous regular expression filters or to add a new one
+      # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
+      options = ["None"] + regexFilters + ["New..."]
+      initialSelection = 0 if not panels["log"].regexFilter else 1
+      
+      # hides top label of the graph panel and pauses panels
+      if panels["graph"].currentDisplay:
+        panels["graph"].showLabel = False
+        panels["graph"].redraw()
+      setPauseState(panels, isPaused, page, True)
+      
+      selection = showMenu(stdscr, panels["popup"], "Log Filter:", options, initialSelection)
+      
+      # applies new setting
+      if selection == 0:
+        panels["log"].regexFilter = None
+      elif selection == len(options) - 1:
+        # selected 'New...' option - prompt user to input regular expression
+        cursesLock.acquire()
+        try:
+          # provides prompt
+          panels["control"].setMsg("Regular expression: ")
+          panels["control"].redraw()
+          
+          # makes cursor and typing visible
+          try: curses.curs_set(1)
+          except curses.error: pass
+          curses.echo()
+          
+          # gets user input (this blocks monitor updates)
+          regexInput = panels["control"].win.getstr(0, 20)
+          
+          # reverts visability settings
+          try: curses.curs_set(0)
+          except curses.error: pass
+          curses.noecho()
+          curses.halfdelay(REFRESH_RATE * 10)
+          
+          if regexInput != "":
+            try:
+              panels["log"].regexFilter = re.compile(regexInput)
+              if regexInput in regexFilters: regexFilters.remove(regexInput)
+              regexFilters = [regexInput] + regexFilters
+            except re.error, exc:
+              panels["control"].setMsg("Unable to compile expression: %s" % str(exc), curses.A_STANDOUT)
+              panels["control"].redraw()
+              time.sleep(2)
+          panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+        finally:
+          cursesLock.release()
+      elif selection != -1:
+        try:
+          panels["log"].regexFilter = re.compile(regexFilters[selection - 1])
+          
+          # move selection to top
+          regexFilters = [regexFilters[selection - 1]] + regexFilters
+          del regexFilters[selection]
+        except re.error, exc:
+          # shouldn't happen since we've already checked validity
+          panels["log"].monitor_event("WARN", "Invalid regular expression ('%s': %s) - removing from listing" % (regexFilters[selection - 1], str(exc)))
+          del regexFilters[selection - 1]
+      
+      if len(regexFilters) > MAX_REGEX_FILTERS: del regexFilters[MAX_REGEX_FILTERS:]
+      
+      # reverts changes made for popup
+      panels["graph"].showLabel = True
+      setPauseState(panels, isPaused, page)
     elif key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingCounter != -1:
       # canceling hostname resolution (esc on any page)
       panels["conn"].listingType = connPanel.LIST_IP
@@ -797,6 +882,61 @@
         curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
       finally:
         cursesLock.release()
+    elif page == 1 and (key == ord('c') or key == ord('C')):
+      # displays popup with client circuits
+      clientCircuits = None
+      try:
+        clientCircuits = conn.get_info("circuit-status")["circuit-status"].split("\n")
+      except TorCtl.ErrorReply: pass
+      except TorCtl.TorCtlClosed: pass
+      except socket.error: pass
+      
+      maxEntryLength = 0
+      if clientCircuits:
+        for clientEntry in clientCircuits: maxEntryLength = max(len(clientEntry), maxEntryLength)
+      
+      cursesLock.acquire()
+      try:
+        setPauseState(panels, isPaused, page, True)
+        
+        # makes sure there's room for the longest entry
+        popup = panels["popup"]
+        popup._resetBounds()
+        if clientCircuits and maxEntryLength + 4 > popup.maxX:
+          popup.height = max(popup.height, len(clientCircuits) + 3)
+          popup.recreate(stdscr, popup.startY, maxEntryLength + 4)
+        
+        # lists commands
+        popup.clear()
+        popup.win.box()
+        popup.addstr(0, 0, "Client Circuits:", util.LABEL_ATTR)
+        
+        if clientCircuits == None:
+          popup.addstr(1, 2, "Unable to retireve current circuits")
+        elif len(clientCircuits) == 1 and clientCircuits[0] == "":
+          popup.addstr(1, 2, "No active client circuits")
+        else:
+          line = 1
+          for clientEntry in clientCircuits:
+            popup.addstr(line, 2, clientEntry)
+            line += 1
+            
+        popup.addstr(popup.height - 2, 2, "Press any key...")
+        popup.refresh()
+        
+        curses.cbreak()
+        stdscr.getch()
+        curses.halfdelay(REFRESH_RATE * 10)
+        
+        # reverts popup dimensions
+        popup.height = 9
+        popup.recreate(stdscr, popup.startY, 80)
+        
+        setPauseState(panels, isPaused, page)
+      finally:
+        cursesLock.release()
+    elif page == 0:
+      panels["log"].handleKey(key)
     elif page == 1:
       panels["conn"].handleKey(key)
     elif page == 2:

Added: arm/trunk/interface/cpuMemMonitor.py
===================================================================
--- arm/trunk/interface/cpuMemMonitor.py	                        (rev 0)
+++ arm/trunk/interface/cpuMemMonitor.py	2009-09-07 05:21:42 UTC (rev 20493)
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+# cpuMemMonitor.py -- Tracks cpu and memory usage of Tor.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import os
+import time
+from TorCtl import TorCtl
+
+import util
+import graphPanel
+
+class CpuMemMonitor(graphPanel.GraphStats, TorCtl.PostEventListener):
+  """
+  Tracks system resource usage (cpu and memory usage), using cached values in
+  headerPanel if recent enough (otherwise retrieved independently).
+  """
+  
+  def __init__(self, headerPanel):
+    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
+    # (and so it stops if Tor stops
+    if self.headerPanel.lastUpdate + 1 >= time.time():
+      # reuses ps results if recent enough
+      self._processEvent(float(self.headerPanel.vals["%cpu"]), float(self.headerPanel.vals["rss"]) / 1024.0)
+    else:
+      # cached results stale - requery ps
+      inbound, outbound, control = 0, 0, 0
+      psCall = os.popen('ps -p %s -o %s' % (self.headerPanel.vals["pid"], "%cpu,rss"))
+      try:
+        sampling = psCall.read().strip().split()[2:]
+        psCall.close()
+        
+        if len(sampling) < 2:
+          # either ps failed or returned no tor instance, register error
+          raise IOError()
+        else:
+          self._processEvent(float(sampling[0]), float(sampling[1]) / 1024.0)
+      except IOError:
+        # ps call failed
+        self.connectionPanel.monitor_event("WARN", "Unable to query ps for resource usage")
+  
+  def getTitle(self, width):
+    return "System Resources:"
+  
+  def getHeaderLabel(self, width, isPrimary):
+    avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
+    if isPrimary: return "CPU (%s%%, avg: %0.1f%%):" % (self.lastPrimary, avg)
+    else: return "Memory (%s, avg: %s):" % (util.getSizeLabel(self.lastSecondary * 1048576, 1), util.getSizeLabel(avg * 1048576, 1))
+

Modified: arm/trunk/interface/headerPanel.py
===================================================================
--- arm/trunk/interface/headerPanel.py	2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/interface/headerPanel.py	2009-09-07 05:21:42 UTC (rev 20493)
@@ -3,6 +3,7 @@
 # Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
 
 import os
+import time
 import socket
 from TorCtl import TorCtl
 
@@ -42,6 +43,7 @@
     self.conn = conn                # Tor control port connection
     self.isPaused = False
     self.isWide = False             # doubles up parameters to shorten section if room's available
+    self.lastUpdate = -1            # time last stats was retrived
     self._updateParams()
   
   def recreate(self, stdscr, startY, maxX=-1):
@@ -212,4 +214,6 @@
     
     for i in range(len(psParams)):
       self.vals[psParams[i]] = sampling[i]
+    
+    self.lastUpdate = time.time()
 

Modified: arm/trunk/interface/logPanel.py
===================================================================
--- arm/trunk/interface/logPanel.py	2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/interface/logPanel.py	2009-09-07 05:21:42 UTC (rev 20493)
@@ -2,13 +2,15 @@
 # logPanel.py -- Resources related to Tor event monitoring.
 # Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
 
+import re
 import time
+import curses
 from curses.ascii import isprint
 from TorCtl import TorCtl
 
 import util
 
-MAX_LOG_ENTRIES = 80                # size of log buffer (max number of entries)
+MAX_LOG_ENTRIES = 1000               # size of log buffer (max number of entries)
 RUNLEVEL_EVENT_COLOR = {"DEBUG": "magenta", "INFO": "blue", "NOTICE": "green", "WARN": "yellow", "ERR": "red"}
 
 EVENT_TYPES = {
@@ -27,7 +29,7 @@
                     k NEWCONSENSUS    u CLIENTS_SEEN
         Aliases:    A All Events      X No Events       U Unknown Events
                     DINWE Runlevel and higher severity"""
-  
+
 def expandEvents(eventAbbr):
   """
   Expands event abbreviations to their full names. Beside mappings privided in
@@ -73,19 +75,38 @@
   def __init__(self, lock, loggedEvents):
     TorCtl.PostEventListener.__init__(self)
     util.Panel.__init__(self, lock, -1)
+    self.scroll = 0
     self.msgLog = []                      # tuples of (logText, color)
     self.isPaused = False
     self.pauseBuffer = []                 # location where messages are buffered if paused
     self.loggedEvents = loggedEvents      # events we're listening to
     self.lastHeartbeat = time.time()      # time of last event
+    self.regexFilter = None               # filter for presented log events (no filtering if None)
   
+  def handleKey(self, key):
+    # scroll movement
+    if key in (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE):
+      self._resetBounds()
+      pageHeight, shift = self.maxY - 1, 0
+      
+      # location offset
+      if key == curses.KEY_UP: shift = -1
+      elif key == curses.KEY_DOWN: shift = 1
+      elif key == curses.KEY_PPAGE: shift = -pageHeight
+      elif key == curses.KEY_NPAGE: shift = pageHeight
+      
+      # restricts to valid bounds and applies
+      maxLoc = self.getLogDisplayLength() - pageHeight
+      self.scroll = max(0, min(self.scroll + shift, maxLoc))
+  
   # Listens for all event types and redirects to registerEvent
   def circ_status_event(self, event):
-    optionalParams = ""
-    if event.purpose: optionalParams += " PURPOSE: %s" % event.purpose
-    if event.reason: optionalParams += " REASON: %s" % event.reason
-    if event.remote_reason: optionalParams += " REMOTE_REASON: %s" % event.remote_reason
-    self.registerEvent("CIRC", "ID: %-3s STATUS: %-10s PATH: %s%s" % (event.circ_id, event.status, ", ".join(event.path), optionalParams), "yellow")
+    if "CIRC" in self.loggedEvents:
+      optionalParams = ""
+      if event.purpose: optionalParams += " PURPOSE: %s" % event.purpose
+      if event.reason: optionalParams += " REASON: %s" % event.reason
+      if event.remote_reason: optionalParams += " REMOTE_REASON: %s" % event.remote_reason
+      self.registerEvent("CIRC", "ID: %-3s STATUS: %-10s PATH: %s%s" % (event.circ_id, event.status, ", ".join(event.path), optionalParams), "yellow")
   
   def stream_status_event(self, event):
     # TODO: not sure how to stimulate event - needs sanity check
@@ -179,10 +200,14 @@
       try:
         self.clear()
         
+        isScrollBarVisible = self.getLogDisplayLength() > self.maxY - 1
+        xOffset = 3 if isScrollBarVisible else 0 # content offset for scroll bar
+        
         # draws label - uses ellipsis if too long, for instance:
         # Events (DEBUG, INFO, NOTICE, WARN...):
         eventsLabel = "Events"
         eventsListing = ", ".join(self.loggedEvents)
+        filterLabel = "" if not self.regexFilter else " - filter: %s" % self.regexFilter.pattern
         
         firstLabelLen = eventsListing.find(", ")
         if firstLabelLen == -1: firstLabelLen = len(eventsListing)
@@ -190,34 +215,59 @@
         
         if self.maxX > 10 + firstLabelLen:
           eventsLabel += " ("
+          
           if len(eventsListing) > self.maxX - 11:
             labelBreak = eventsListing[:self.maxX - 12].rfind(", ")
             eventsLabel += "%s..." % eventsListing[:labelBreak]
-          else: eventsLabel += eventsListing
+          elif len(eventsListing) + len(filterLabel) > self.maxX - 11:
+            eventsLabel += eventsListing
+          else: eventsLabel += eventsListing + filterLabel
           eventsLabel += ")"
         eventsLabel += ":"
         
         self.addstr(0, 0, eventsLabel, util.LABEL_ATTR)
         
         # log entries
-        lineCount = 1
+        maxLoc = self.getLogDisplayLength() - self.maxY + 1
+        self.scroll = max(0, min(self.scroll, maxLoc))
+        lineCount = 1 - self.scroll
         
         for (line, color) in self.msgLog:
+          if self.regexFilter and not self.regexFilter.search(line):
+            continue  # filter doesn't match log message - skip
+          
           # splits over too lines if too long
           if len(line) < self.maxX:
-            self.addstr(lineCount, 0, line, util.getColor(color))
+            if lineCount >= 1: self.addstr(lineCount, xOffset, line, util.getColor(color))
             lineCount += 1
           else:
-            (line1, line2) = splitLine(line, self.maxX)
-            self.addstr(lineCount, 0, line1, util.getColor(color))
-            self.addstr(lineCount + 1, 0, line2, util.getColor(color))
+            (line1, line2) = splitLine(line, self.maxX - xOffset)
+            if lineCount >= 1: self.addstr(lineCount, xOffset, line1, util.getColor(color))
+            if lineCount >= 0: self.addstr(lineCount + 1, xOffset, line2, util.getColor(color))
             lineCount += 2
           
           if lineCount >= self.maxY: break # further log messages wouldn't fit
+        
+        if isScrollBarVisible: util.drawScrollBar(self, 1, self.maxY - 1, self.scroll, self.scroll + self.maxY - 1, self.getLogDisplayLength())
         self.refresh()
       finally:
         self.lock.release()
   
+  def getLogDisplayLength(self):
+    """
+    Provides the number of lines the log would currently occupy.
+    """
+    
+    logLength = len(self.msgLog)
+    
+    # takes into account filtered and wrapped messages
+    self._resetBounds()
+    for (line, color) in self.msgLog:
+      if self.regexFilter and not self.regexFilter.search(line): logLength -= 1
+      elif len(line) >= self.maxX: logLength += 1
+    
+    return logLength
+  
   def setPaused(self, isPause):
     """
     If true, prevents message log from being updated with new events.

Modified: arm/trunk/interface/util.py
===================================================================
--- arm/trunk/interface/util.py	2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/interface/util.py	2009-09-07 05:21:42 UTC (rev 20493)
@@ -75,7 +75,7 @@
 def getTimeLabel(seconds, decimal = 0):
   """
   Concerts seconds into a time label truncated to its most significant units,
-  for instance 7500 seconds would return "". Units go up through days.
+  for instance 7500 seconds would return "2h". Units go up through days.
   """
   
   format = "%%.%if" % decimal
@@ -84,6 +84,34 @@
   elif seconds >= 60: return (format + "m") % (seconds / 60.0)
   else: return "%is" % seconds
 
+def drawScrollBar(panel, drawTop, drawBottom, top, bottom, size):
+  """
+  Draws scroll bar reflecting position within a vertical listing. This is
+  squared off at the bottom, having a layout like:
+   | 
+  *|
+  *|
+  *|
+   |
+  -+
+  """
+  
+  barTop = (drawBottom - drawTop) * top / size
+  barSize = (drawBottom - drawTop) * (bottom - top) / size
+  
+  # makes sure bar isn't at top or bottom unless really at those extreme bounds
+  if top > 0: barTop = max(barTop, 1)
+  if bottom != size: barTop = min(barTop, drawBottom - drawTop - barSize - 2)
+  
+  for i in range(drawBottom - drawTop):
+    if i >= barTop and i <= barTop + barSize:
+      panel.addstr(i + drawTop, 0, " ", curses.A_STANDOUT)
+  
+  # draws box around scroll bar
+  panel.win.vline(drawTop, 1, curses.ACS_VLINE, panel.maxY - 2)
+  panel.win.vline(drawBottom, 1, curses.ACS_LRCORNER, 1)
+  panel.win.hline(drawBottom, 0, curses.ACS_HLINE, 1)
+
 class Panel():
   """
   Wrapper for curses subwindows. This provides safe proxies to common methods

Modified: arm/trunk/readme.txt
===================================================================
--- arm/trunk/readme.txt	2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/readme.txt	2009-09-07 05:21:42 UTC (rev 20493)
@@ -12,9 +12,10 @@
 status. Releases should be stable so if you manage to make it crash (or have a 
 feature request) then please let me know!
 
-The project was originally proposed in 2008 by Jacob and Karsten 
-(http://archives.seul.org/or/dev/Jan-2008/msg00005.html). An interview by 
-Brenno Winter discussing the project is available at:
+The project was originally proposed in 2008 by Jacob and Karsten:
+  http://archives.seul.org/or/dev/Jan-2008/msg00005.html
+
+An interview by Brenno Winter discussing the project is available at:
   http://www.atagar.com/arm/HFM_INT_0001.mp3
 
 Requirements:



More information about the tor-commits mailing list