[or-cvs] r20331: {arm} Work done over this last week. added: popup for raw consensu (in arm/trunk: . interface)

atagar at seul.org atagar at seul.org
Tue Aug 18 00:07:50 UTC 2009


Author: atagar
Date: 2009-08-17 20:07:49 -0400 (Mon, 17 Aug 2009)
New Revision: 20331

Added:
   arm/trunk/interface/descriptorPopup.py
Modified:
   arm/trunk/interface/bandwidthMonitor.py
   arm/trunk/interface/confPanel.py
   arm/trunk/interface/connCountMonitor.py
   arm/trunk/interface/connPanel.py
   arm/trunk/interface/controller.py
   arm/trunk/interface/graphPanel.py
   arm/trunk/interface/headerPanel.py
   arm/trunk/interface/hostnameResolver.py
   arm/trunk/interface/util.py
   arm/trunk/readme.txt
Log:
Work done over this last week.
added: popup for raw consensus description
added: total bandwidth measurement
added: connection entry for lookup of local consensus data
change: widened graphs to utilize full screen width (clever idea by StrangeCharm)
change: preserving runtime and pid when shutting down
change: few tweaks to the readme
fix: joining on worker daemon threads to exit gracefully (had a noisy race condition)
fix: using BW events to keep connection count graph in sync with bandwidth graph
fix: can now support graphs of multiple sizes



Modified: arm/trunk/interface/bandwidthMonitor.py
===================================================================
--- arm/trunk/interface/bandwidthMonitor.py	2009-08-17 18:22:24 UTC (rev 20330)
+++ arm/trunk/interface/bandwidthMonitor.py	2009-08-18 00:07:49 UTC (rev 20331)
@@ -73,8 +73,9 @@
     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)
+    total = self.primaryTotal if isPrimary else self.secondaryTotal
+    avg = total / max(1, self.tick)
+    return "avg: %s/sec, total: %s" % (util.getSizeLabel(avg * 1024), util.getSizeLabel(total * 1024))
   
   def _updateAccountingInfo(self):
     """

Modified: arm/trunk/interface/confPanel.py
===================================================================
--- arm/trunk/interface/confPanel.py	2009-08-17 18:22:24 UTC (rev 20330)
+++ arm/trunk/interface/confPanel.py	2009-08-18 00:07:49 UTC (rev 20331)
@@ -89,31 +89,12 @@
             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, xLoc = self.addstr_wrap(lineNum, xLoc, command, curses.A_BOLD | util.getColor("green"), numOffset)
+          lineNum, xLoc = self.addstr_wrap(lineNum, xLoc, argument, curses.A_BOLD | util.getColor("cyan"), numOffset)
+          lineNum, xLoc = self.addstr_wrap(lineNum, xLoc, comment, util.getColor("white"), numOffset)
           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/connCountMonitor.py
===================================================================
--- arm/trunk/interface/connCountMonitor.py	2009-08-17 18:22:24 UTC (rev 20330)
+++ arm/trunk/interface/connCountMonitor.py	2009-08-18 00:07:49 UTC (rev 20331)
@@ -4,14 +4,12 @@
 
 import os
 import time
-from threading import Thread
 from TorCtl import TorCtl
 
 import connPanel
 import graphPanel
-import util
 
-class ConnCountMonitor(graphPanel.GraphStats, Thread):
+class ConnCountMonitor(graphPanel.GraphStats, TorCtl.PostEventListener):
   """
   Tracks number of connections, using cached values in connPanel if recent
   enough (otherwise retrieved independently).
@@ -19,46 +17,39 @@
   
   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
+    TorCtl.PostEventListener.__init__(self)
+    graphPanel.GraphStats.initialize(self, connPanel.TYPE_COLORS["inbound"], connPanel.TYPE_COLORS["outbound"], 9)
     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")
+  def bandwidth_event(self, event):
+    # doesn't use events but this keeps it in sync with the bandwidth panel
+    # (and so it stops if Tor stops - used to use a separate thread but this
+    # is better)
+    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()
         
-        netstatCall.close()
-        self._processEvent(inbound, outbound)
+        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")
       
-      self.lastUpdate = time.time()
+      netstatCall.close()
+      self._processEvent(inbound, outbound)
   
   def getTitle(self, width):
     return "Connection Count:"

Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py	2009-08-17 18:22:24 UTC (rev 20330)
+++ arm/trunk/interface/connPanel.py	2009-08-18 00:07:49 UTC (rev 20331)
@@ -8,16 +8,16 @@
 import curses
 from TorCtl import TorCtl
 
+import hostnameResolver
 import util
-import hostnameResolver
 
 # enums for listing types
 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
+TYPE_COLORS = {"inbound": "green", "outbound": "blue", "control": "red", "localhost": "cyan"}
+TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "control": 2, "localhost": 3} # 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)
@@ -203,14 +203,30 @@
           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:
       # netstat call failed
       self.logger.monitor_event("WARN", "Unable to query netstat for new connections")
     
+    # appends localhost connection to allow user to look up their own consensus entry
+    selfAddress, selfPort, selfFingerprint = None, None, None
+    try:
+      selfAddress = self.conn.get_info("address")["address"]
+      selfPort = self.conn.get_option("ORPort")[0][1]
+      selfFingerprint = self.conn.get_info("fingerprint")["fingerprint"]
+    except TorCtl.ErrorReply: pass
+    except TorCtl.TorCtlClosed: pass
+    except socket.error: pass
+    
+    if selfAddress and selfPort and selfFingerprint:
+      try:
+        countryCodeQuery = "ip-to-country/%s" % selfAddress
+        selfCountryCode = self.conn.get_info(countryCodeQuery)[countryCodeQuery]
+      except socket.error:
+        selfCountryCode = "??"
+      self.connections.append(("localhost", selfAddress, selfPort, selfAddress, selfPort, selfCountryCode))
+    
     netstatCall.close()
     self.lastUpdate = time.time()
     

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2009-08-17 18:22:24 UTC (rev 20330)
+++ arm/trunk/interface/controller.py	2009-08-18 00:07:49 UTC (rev 20331)
@@ -17,6 +17,7 @@
 import logPanel
 import connPanel
 import confPanel
+import descriptorPopup
 
 import util
 import bandwidthMonitor
@@ -30,13 +31,12 @@
 CTL_HELP, CTL_PAUSED = range(2)
 
 # panel order per page
-PAGE_S = ["header", "control", "popup"]    # sticky (ie, always available) page
+PAGE_S = ["header", "control", "popup"] # sticky (ie, always available) page
 PAGES = [
   ["graph", "log"],
   ["conn"],
   ["torrc"]]
 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
 REQ_EVENTS = ["BW", "NEWDESC", "NEWCONSENSUS"]
@@ -86,7 +86,7 @@
             msgText = "Resolving hostnames (%i / %i, %i%%) - press esc %sto cancel" % (entryCount, batchSize, progress, additive)
         
         if self.resolvingCounter == -1:
-          msgText = "page %i / %i - q: quit, p: pause, h: page help" % (self.page, PAGE_COUNT)
+          msgText = "page %i / %i - q: quit, p: pause, h: page help" % (self.page, len(PAGES))
       elif msgText == CTL_PAUSED:
         msgText = "Paused"
         msgAttr = curses.A_STANDOUT
@@ -257,6 +257,7 @@
   # listeners that update bandwidth and log panels with Tor status
   conn.add_event_listener(panels["log"])
   conn.add_event_listener(panels["graph"].stats["bandwidth"])
+  conn.add_event_listener(panels["graph"].stats["connection count"])
   conn.add_event_listener(panels["conn"])
   
   # tells Tor to listen to the events we're interested
@@ -316,11 +317,20 @@
       cursesLock.release()
     
     key = stdscr.getch()
-    if key == ord('q') or key == ord('Q'): break # quits
+    if key == ord('q') or key == ord('Q'):
+      daemonThreads = panels["conn"].resolver.threadPool
+      
+      # sets halt flags for all worker daemon threads
+      for worker in daemonThreads: worker.halt = True
+      
+      # joins on workers (prevents noisy termination)
+      for worker in daemonThreads: worker.join()
+      
+      break
     elif key == curses.KEY_LEFT or key == curses.KEY_RIGHT:
       # switch page
-      if key == curses.KEY_LEFT: page = (page - 1) % PAGE_COUNT
-      else: page = (page + 1) % PAGE_COUNT
+      if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
+      else: page = (page + 1) % len(PAGES)
       
       # pauses panels that aren't visible to prevent events from accumilating
       # (otherwise they'll wait on the curses lock which might get demanding)
@@ -362,7 +372,7 @@
           popup.addstr(2, 2, "page up: scroll up a page")
           popup.addstr(2, 41, "page down: scroll down a page")
           popup.addstr(3, 2, "enter: connection details")
-          popup.addfstr(3, 41, "c: toggle cursor (<b>%s</b>)" % ("on" if panels["conn"].isCursorEnabled else "off"))
+          popup.addstr(3, 41, "d: raw consensus descriptor")
           
           listingType = connPanel.LIST_LABEL[panels["conn"].listingType].lower()
           popup.addfstr(4, 2, "l: listed identity (<b>%s</b>)" % listingType)
@@ -371,6 +381,7 @@
           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"))
         elif page == 2:
           popup.addstr(1, 2, "up arrow: scroll up a line")
           popup.addstr(1, 41, "down arrow: scroll down a line")
@@ -507,39 +518,6 @@
       panels["control"].resolvingCounter = -1
       panels["conn"].resolver.setPaused(True)
       panels["conn"].sortConnections()
-    elif page == 1 and (key == ord('l') or key == ord('L')):
-      # provides menu to pick identification info listed for connections
-      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]
-        
-        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()
     elif page == 1 and panels["conn"].isCursorEnabled and key in (curses.KEY_ENTER, 10, ord(' ')):
       # provides details on selected connection
       cursesLock.acquire()
@@ -619,6 +597,7 @@
               # ns lookup fails... weird
               try: nsData = conn.get_network_status("id/%s" % fingerprint)
               except TorCtl.ErrorReply: break
+              except TorCtl.TorCtlClosed: break
               
               if len(nsData) > 1:
                 # multiple records for fingerprint (shouldn't happen)
@@ -662,6 +641,9 @@
           
           if key in (curses.KEY_DOWN, curses.KEY_UP, curses.KEY_PPAGE, curses.KEY_NPAGE):
             panels["conn"].handleKey(key)
+          elif key in (ord('d'), ord('D')):
+            descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, conn, panels["conn"])
+            panels["conn"].redraw()
         
         panels["conn"].showLabel = True
         panels["conn"].showingDetails = False
@@ -670,7 +652,55 @@
         curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
       finally:
         cursesLock.release()
+    elif page == 1 and panels["conn"].isCursorEnabled and key in (ord('d'), ord('D')):
+      # presents popup for raw consensus data
+      cursesLock.acquire()
+      try:
+        setPauseState(panels, isPaused, page, True)
+        curses.cbreak() # wait indefinitely for key presses (no timeout)
+        panels["conn"].showLabel = False
+        panels["conn"].redraw()
+        
+        descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, conn, panels["conn"])
+        
+        setPauseState(panels, isPaused, page)
+        curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+        panels["conn"].showLabel = True
+      finally:
+        cursesLock.release()
+    elif page == 1 and (key == ord('l') or key == ord('L')):
+      # provides menu to pick identification info listed for connections
+      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]
+        
+        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()
     elif page == 1 and (key == ord('s') or key == ord('S')):
       # set ordering for connection listing
       cursesLock.acquire()

Added: arm/trunk/interface/descriptorPopup.py
===================================================================
--- arm/trunk/interface/descriptorPopup.py	                        (rev 0)
+++ arm/trunk/interface/descriptorPopup.py	2009-08-18 00:07:49 UTC (rev 20331)
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+# descriptorPopup.py -- popup panel used to show raw consensus data
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import math
+import curses
+from TorCtl import TorCtl
+
+import connPanel
+import util
+
+# field keywords used to identify areas for coloring
+LINE_NUM_COLOR = "yellow"
+HEADER_COLOR = "cyan"
+HEADER_PREFIX = ["ns/id/", "desc/id/"]
+
+SIG_COLOR = "red"
+SIG_START_KEYS = ["-----BEGIN RSA PUBLIC KEY-----", "-----BEGIN SIGNATURE-----"]
+SIG_END_KEYS = ["-----END RSA PUBLIC KEY-----", "-----END SIGNATURE-----"]
+
+UNRESOLVED_MSG = "No consensus data available"
+ERROR_MSG = "Unable to retrieve consensus data"
+
+class PopupProperties:
+  """
+  State attributes of popup window for consensus descriptions.
+  """
+  
+  def __init__(self, conn):
+    self.conn = conn
+    self.fingerprint = ""
+    self.entryColor = "white"
+    self.text = []
+    self.scroll = 0
+    self.showLineNum = True
+  
+  def reset(self, fingerprint, entryColor):
+    self.fingerprint = fingerprint
+    self.entryColor = entryColor
+    self.text = []
+    self.scroll = 0
+    
+    if fingerprint == "UNKNOWN":
+      self.fingerprint = None
+      self.showLineNum = False
+      self.text.append(UNRESOLVED_MSG)
+    else:
+      try:
+        self.showLineNum = True
+        nsCommand = "ns/id/%s" % fingerprint
+        self.text.append(nsCommand)
+        self.text = self.text + self.conn.get_info(nsCommand)[nsCommand].split("\n")
+        
+        descCommand = "desc/id/%s" % fingerprint
+        self.text.append(descCommand)
+        self.text = self.text + self.conn.get_info(descCommand)[descCommand].split("\n")
+      except TorCtl.ErrorReply:
+        self.fingerprint = None
+        self.showLineNum = False
+        self.text.append(ERROR_MSG)
+  
+  def handleKey(self, key, height):
+    if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
+    elif key == curses.KEY_DOWN: self.scroll = max(0, min(self.scroll + 1, len(self.text) - height))
+    elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - height, 0)
+    elif key == curses.KEY_NPAGE: self.scroll = max(0, min(self.scroll + height, len(self.text) - height))
+
+def showDescriptorPopup(popup, stdscr, conn, connectionPanel):
+  """
+  Presents consensus descriptor in popup window with the following controls:
+  Up, Down, Page Up, Page Down - scroll descriptor
+  Right, Left - next / previous connection
+  Enter, Space, d, D - close popup
+  """
+  
+  properties = PopupProperties(conn)
+  isVisible = True
+  
+  if not popup.lock.acquire(False): return
+  try:
+    while isVisible:
+      selection = connectionPanel.cursorSelection
+      if not selection: break
+      fingerprint = connectionPanel.getFingerprint(selection[connPanel.CONN_F_IP], selection[connPanel.CONN_F_PORT])
+      entryColor = connPanel.TYPE_COLORS[selection[connPanel.CONN_TYPE]]
+      properties.reset(fingerprint, entryColor)
+      
+      # constrains popup size to match text
+      width, height = 0, 0
+      for line in properties.text:
+        # width includes content, line number field, and border
+        lineWidth = len(line) + 5
+        if properties.showLineNum: lineWidth += int(math.log10(len(properties.text))) + 1
+        width = max(width, lineWidth)
+        
+        # tracks number of extra lines that will be taken due to text wrap
+        height += (lineWidth - 2) / connectionPanel.maxX
+      
+      popup._resetBounds()
+      popup.height = min(len(properties.text) + height + 2, connectionPanel.maxY)
+      popup.recreate(stdscr, popup.startY, width)
+      
+      while isVisible:
+        draw(popup, properties)
+        key = stdscr.getch()
+        
+        if key in (curses.KEY_ENTER, 10, ord(' '), ord('d'), ord('D')):
+          # closes popup
+          isVisible = False
+        elif key in (curses.KEY_LEFT, curses.KEY_RIGHT):
+          # navigation - pass on to connPanel and recreate popup
+          connectionPanel.handleKey(curses.KEY_UP if key == curses.KEY_LEFT else curses.KEY_DOWN)
+          break
+        else: properties.handleKey(key, popup.height - 2)
+    
+    popup.height = 9
+    popup.recreate(stdscr, popup.startY, 80)
+  finally:
+    popup.lock.release()
+
+def draw(popup, properties):
+  popup.clear()
+  popup.win.box()
+  xOffset = 2
+  
+  if properties.text:
+    if properties.fingerprint: popup.addstr(0, 0, "Consensus Descriptor (%s):" % properties.fingerprint, util.LABEL_ATTR)
+    else: popup.addstr(0, 0, "Consensus Descriptor:", util.LABEL_ATTR)
+    
+    isEncryption = False          # true if line is part of an encryption block
+    pageHeight = popup.maxY - 2
+    numFieldWidth = int(math.log10(len(properties.text))) + 1
+    lineNum = 1
+    for i in range(properties.scroll, min(len(properties.text), properties.scroll + pageHeight)):
+      lineText = properties.text[i].strip()
+      
+      numOffset = 0     # offset for line numbering
+      if properties.showLineNum:
+        popup.addstr(lineNum, xOffset, ("%%%ii" % numFieldWidth) % (i + 1), curses.A_BOLD | util.getColor(LINE_NUM_COLOR))
+        numOffset = numFieldWidth + 1
+      
+      if lineText:
+        keyword = lineText.split()[0]   # first word of line
+        remainder = lineText[len(keyword):]
+        keywordFormat = curses.A_BOLD | util.getColor(properties.entryColor)
+        remainderFormat = util.getColor(properties.entryColor)
+        
+        if lineText.startswith(HEADER_PREFIX[0]) or lineText.startswith(HEADER_PREFIX[1]):
+          keyword, remainder = lineText, ""
+          keywordFormat = curses.A_BOLD | util.getColor(HEADER_COLOR)
+        if lineText == UNRESOLVED_MSG or lineText == ERROR_MSG:
+          keyword, remainder = lineText, ""
+        if lineText in SIG_START_KEYS:
+          keyword, remainder = lineText, ""
+          isEncryption = True
+          keywordFormat = curses.A_BOLD | util.getColor(SIG_COLOR)
+        elif lineText in SIG_END_KEYS:
+          keyword, remainder = lineText, ""
+          isEncryption = False
+          keywordFormat = curses.A_BOLD | util.getColor(SIG_COLOR)
+        elif isEncryption:
+          keyword, remainder = lineText, ""
+          keywordFormat = util.getColor(SIG_COLOR)
+        
+        lineNum, xLoc = popup.addstr_wrap(lineNum, 0, keyword, keywordFormat, xOffset + numOffset, popup.maxX - 1, popup.maxY - 1)
+        lineNum, xLoc = popup.addstr_wrap(lineNum, xLoc, remainder, remainderFormat, xOffset + numOffset, popup.maxX - 1, popup.maxY - 1)
+      
+      lineNum += 1
+      if lineNum > pageHeight: break
+      
+  popup.refresh()
+

Modified: arm/trunk/interface/graphPanel.py
===================================================================
--- arm/trunk/interface/graphPanel.py	2009-08-17 18:22:24 UTC (rev 20330)
+++ arm/trunk/interface/graphPanel.py	2009-08-18 00:07:49 UTC (rev 20331)
@@ -7,7 +7,7 @@
 
 import util
 
-GRAPH_COL = 30  # columns of data in graph
+MAX_GRAPH_COL = 150  # max columns of data in graph
 
 # enums for graph bounds:
 #   BOUNDS_MAX - global maximum (highest value ever seen)
@@ -58,8 +58,8 @@
       
       # 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]
+      self.primaryCounts[label] = (MAX_GRAPH_COL + 1) * [0]
+      self.secondaryCounts[label] = (MAX_GRAPH_COL + 1) * [0]
   
   def initialize(self, primaryColor, secondaryColor, height, pauseBuffer=None):
     """
@@ -69,7 +69,7 @@
     # 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
+    self.height = height
     
     # mirror instance used to track updates when paused
     if not pauseBuffer: self.pauseBuffer = GraphStats()
@@ -154,12 +154,12 @@
           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:]
+          del self.primaryCounts[label][MAX_GRAPH_COL + 1:]
           
           self.maxSecondary[label] = max(self.maxSecondary[label], self.secondaryCounts[label][0] / timescale)
           self.secondaryCounts[label][0] /= timescale
           self.secondaryCounts[label].insert(0, 0)
-          del self.secondaryCounts[label][GRAPH_COL + 1:]
+          del self.secondaryCounts[label][MAX_GRAPH_COL + 1:]
       
       if self.graphPanel: self.graphPanel.redraw()
 
@@ -174,7 +174,7 @@
     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.bounds = BOUNDS_TIGHT    # determines bounds on graph
     self.currentDisplay = None    # label of the stats currently being displayed
     self.stats = {}               # available stats (mappings of label -> instance)
   
@@ -184,6 +184,7 @@
       if not self.lock.acquire(False): return
       try:
         self.clear()
+        graphCol = min(self.maxX / 2, MAX_GRAPH_COL)
         
         if self.currentDisplay:
           param = self.stats[self.currentDisplay]
@@ -195,7 +196,7 @@
           # 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)
+          if right: self.addstr(1, graphCol + 5, right, curses.A_BOLD | secondaryColor)
           
           # determines max value on the graph
           primaryBound, secondaryBound = -1, -1
@@ -211,22 +212,22 @@
           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)
+          self.addstr(2, graphCol + 5, "%4s" % str(int(secondaryBound)), secondaryColor)
+          self.addstr(7, graphCol + 5, "   0", secondaryColor)
           
           # creates bar graph of bandwidth usage over time
-          for col in range(GRAPH_COL):
-            colHeight = min(5, 5 * param.primaryCounts[self.updateInterval][col + 1] / primaryBound)
+          for col in range(graphCol):
+            colHeight = min(5, 5 * param.primaryCounts[self.updateInterval][col + 1] / max(1, primaryBound))
             for row in range(colHeight): self.addstr(7 - row, col + 5, " ", curses.A_STANDOUT | primaryColor)
           
-          for col in range(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)
+          for col in range(graphCol):
+            colHeight = min(5, 5 * param.secondaryCounts[self.updateInterval][col + 1] / max(1, secondaryBound))
+            for row in range(colHeight): self.addstr(7 - row, col + graphCol + 10, " ", curses.A_STANDOUT | secondaryColor)
           
           # bottom 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)
+          if right: self.addstr(8, graphCol + 6, right, secondaryColor)
           
           # allows for finishing touches by monitor
           param.redraw(self)
@@ -257,16 +258,8 @@
         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
-        
+        self.height = newStats.height
         newStats.setPaused(self.isPaused)
       else: raise ValueError("Unrecognized stats label: %s" % label)
   

Modified: arm/trunk/interface/headerPanel.py
===================================================================
--- arm/trunk/interface/headerPanel.py	2009-08-17 18:22:24 UTC (rev 20330)
+++ arm/trunk/interface/headerPanel.py	2009-08-18 00:07:49 UTC (rev 20331)
@@ -162,8 +162,9 @@
       sampling = [] # no pid known - blank fields
     
     if len(sampling) < 4:
-      # either ps failed or returned no tor instance
-      sampling = [""] * len(psParams)
+      # either ps failed or returned no tor instance, blank information except runtime
+      if "etime" in self.vals: sampling = [""] * (len(psParams) - 1) + [self.vals["etime"]]
+      else: sampling = [""] * len(psParams)
       
       # %cpu, rss, and %mem are better zeroed out
       for i in range(3): sampling[i] = "0"

Modified: arm/trunk/interface/hostnameResolver.py
===================================================================
--- arm/trunk/interface/hostnameResolver.py	2009-08-17 18:22:24 UTC (rev 20330)
+++ arm/trunk/interface/hostnameResolver.py	2009-08-18 00:07:49 UTC (rev 20331)
@@ -105,12 +105,18 @@
     self.unresolvedQueue = unresolvedQueue
     self.counter = counter
     self.isPaused = False
+    self.halt = False         # terminates thread if true
   
   def run(self):
-    while True:
-      while self.isPaused: time.sleep(1)
+    while not self.halt:
+      while self.isPaused and not self.halt: time.sleep(0.25)
+      if self.halt: break
       
-      ipAddr = self.unresolvedQueue.get() # snag next available ip
+      # snags next available ip, timeout is because queue can't be woken up
+      # when 'halt' is set
+      try: ipAddr = self.unresolvedQueue.get(True, 0.25)
+      except Queue.Empty: continue
+      
       resolutionFailed = False            # if true don't cache results
       hostCall = os.popen("host %s" % ipAddr)
       

Modified: arm/trunk/interface/util.py
===================================================================
--- arm/trunk/interface/util.py	2009-08-17 18:22:24 UTC (rev 20330)
+++ arm/trunk/interface/util.py	2009-08-18 00:07:49 UTC (rev 20331)
@@ -115,8 +115,8 @@
     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):
-      # window growing or moving - recreate
+    if self.startY != startY or newHeight != self.maxY or self.isDisplaced or (self.maxX != maxX and maxX != -1):
+      # window resized or moving - recreate
       self.startY = startY
       startY = min(startY, y - 1) # better create a displaced window than leave it as None
       if maxX != -1: x = min(x, maxX)
@@ -209,6 +209,32 @@
         
         x += len(msgSegment)
   
+  def addstr_wrap(self, y, x, text, formatting, startX = 0, endX = -1, maxY = -1):
+    """
+    Writes text with word wrapping, returning the ending y/x coordinate.
+    y: starting write line
+    x: column offset from startX
+    text / formatting: content to be written
+    startX / endX: column bounds in which text may be written
+    """
+    
+    if not text: return (y, x)          # nothing to write
+    if endX == -1: endX = self.maxX     # defaults to writing to end of panel
+    if maxY == -1: maxY = self.maxY + 1 # defaults to writing to bottom of panel
+    lineWidth = endX - startX           # 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 + startX, writeText, formatting)
+        y, x = y + 1, 0
+        if y >= maxY: return (y, x)
+      else:
+        self.addstr(y, x + startX, text, formatting)
+        return (y, x + len(text))
+  
   def _resetBounds(self):
     if self.win: self.maxY, self.maxX = self.win.getmaxyx()
     else: self.maxY, self.maxX = -1, -1

Modified: arm/trunk/readme.txt
===================================================================
--- arm/trunk/readme.txt	2009-08-17 18:22:24 UTC (rev 20330)
+++ arm/trunk/readme.txt	2009-08-18 00:07:49 UTC (rev 20331)
@@ -4,8 +4,11 @@
 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, connections, etc. This uses a curses interface much like 'top' does for system usage.
+Command line application for monitoring Tor relays, providing real time status information such as the current configuration, bandwidth usage, message log, connections, etc. This uses a curses interface much like 'top' does for system usage. The application is intended for command-line aficionados, ssh connections, and anyone stuck with a tty terminal for checking their relay's 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:
+  http://www.atagar.com/arm/HFM_INT_0001.mp3
+
 Requirements:
 Python 2.5
 TorCtl (retrieved in svn checkout)
@@ -16,8 +19,6 @@
 
 This is started via 'arm' (use the '--help' argument for usage).
 
-Warning: The second page (connections) provides the hostnames of Tor relays you're connected to. This means reverse DNS lookups which, if monitored, could leak your current connections to an eavesdropper. You can disable lookups with 'r' (see the page's help for the current status).
-
 FAQ:
 > Why is it called 'arm'?
 
@@ -33,6 +34,10 @@
 
 Not really, but it's discouraged. The original plan for arm included a special emphasis that it wouldn't log any data. The reason is that if a large number of relay operators published the details of their connections then correlation attacks could break Tor user's anonymity. Just show some moderation in what you share and it should be fine.
 
+> Is there any chance that arm will leak data?
+
+Yes - arm is a passive listener with one exception. The second page (connections) provides the hostnames of Tor relays you're connected to. This means reverse DNS lookups which, if monitored, could leak your current connections to an eavesdropper. However, lookups are only made upon request (when showing connection details or listing connections by hostname) and you can disable lookups entirely with 'r' - see the page's help for the current status.
+
 > When arm starts it gives "Unable to resolve tor pid, abandoning connection listing"... why?
 
 If you're running multiple instances of tor then arm needs to figure out which pid belongs to the open control port. If it's running as a different user (such as being in a chroot jail) then it's probably failing due to permission issues. Arm still runs, just no connection listing or ps stats.



More information about the tor-commits mailing list