[or-cvs] r21580: {arm} Had enough of a siesta - getting back into development begin (in arm/trunk: . interface)

Damian Johnson atagar1 at gmail.com
Mon Feb 8 01:47:07 UTC 2010


Author: atagar
Date: 2010-02-08 01:47:07 +0000 (Mon, 08 Feb 2010)
New Revision: 21580

Modified:
   arm/trunk/ChangeLog
   arm/trunk/arm.py
   arm/trunk/interface/controller.py
   arm/trunk/interface/logPanel.py
Log:
Had enough of a siesta - getting back into development beginning with a rewrite if the starter.
added: made authentication a little smarter, using PROTOCOLINFO to autodetect authentication type and cookie location
change: made 'blind mode' (disables connection queries) a startup option rather than flag in source (request by Sebastian)
change: all log events (including arm) are now set via character flags, with TorCtl events log as their own toggleable type
change: starting log label with runlevel events, condensing if logging a range
change: simplifying command line parsing via getopt
fix: blind mode now prevents all netstats (including connection counts and halting resolver thread), improving performance



Modified: arm/trunk/ChangeLog
===================================================================
--- arm/trunk/ChangeLog	2010-02-07 15:19:23 UTC (rev 21579)
+++ arm/trunk/ChangeLog	2010-02-08 01:47:07 UTC (rev 21580)
@@ -1,6 +1,16 @@
 CHANGE LOG
 
-11/29/09 - version 1.3.0
+2/7/10 - version 1.3.1
+Had enough of a siesta - getting back into development beginning with a rewrite if the starter.
+
+    * added: made authentication a little smarter, using PROTOCOLINFO to autodetect authentication type and cookie location
+    * change: made 'blind mode' (disables connection queries) a startup option rather than flag in source (request by Sebastian)
+    * change: all log events (including arm) are now set via character flags, with TorCtl events log as their own toggleable type
+    * change: starting log label with runlevel events, condensing if logging a range
+    * change: simplifying command line parsing via getopt
+    * fix: blind mode now prevents all netstats (including connection counts and halting resolver thread), improving performance
+
+11/29/09 - version 1.3.0 (r21062)
 Weekend bugfix bundle.
 
     * added: most commands can be immediately executed from the help page (feature request by arma)

Modified: arm/trunk/arm.py
===================================================================
--- arm/trunk/arm.py	2010-02-07 15:19:23 UTC (rev 21579)
+++ arm/trunk/arm.py	2010-02-08 01:47:07 UTC (rev 21580)
@@ -9,8 +9,8 @@
 """
 
 import sys
-import os
 import socket
+import getopt
 import getpass
 
 from TorCtl import TorCtl
@@ -19,120 +19,31 @@
 from interface import controller
 from interface import logPanel
 
-VERSION = "1.3.0"
-LAST_MODIFIED = "Nov 29, 2009"
+VERSION = "1.3.1"
+LAST_MODIFIED = "Feb 7, 2010"
 
 DEFAULT_CONTROL_ADDR = "127.0.0.1"
 DEFAULT_CONTROL_PORT = 9051
-DEFAULT_AUTH_COOKIE = os.path.expanduser("~/.tor/control_auth_cookie") # TODO: Check if this is valid for macs
-DEFAULT_LOGGED_EVENTS = "nwe" # NOTICE, WARN, ERR
+DEFAULT_LOGGED_EVENTS = "N3" # tor and arm NOTICE, WARN, and ERR events
 
-NO_AUTH, COOKIE_AUTH, PASSWORD_AUTH = range(3) # enums for authentication type
-
+OPT = "i:p:be:vh"
+OPT_EXPANDED = ["interface=", "password=", "blind", "event=", "version", "help"]
 HELP_TEXT = """Usage arm [OPTION]
 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
-                                    '%s'
-  -p, --password[=PASSWORD]       authenticates using password, prompting
-                                    without terminal echo if not provided
-  -e, --event=[EVENT FLAGS]       event types in message log  (default: %s)
+  -p, --password PASSWORD         authenticate using password (skip prompt)
+  -b, --blind                     disable connection lookups
+  -e, --event EVENT_FLAGS         event types in message log  (default: %s)
 %s
   -v, --version                   provides version information
   -h, --help                      presents this help
 
 Example:
-arm -c                  authenticate using the default cookie
-arm -i 1643 -p          prompt for password using control port 1643
+arm -b -i 1643          hide connection data, attaching to control port 1643
 arm -e=we -p=nemesis    use password 'nemesis' with 'WARN'/'ERR' events
-""" % (DEFAULT_CONTROL_ADDR, DEFAULT_CONTROL_PORT, DEFAULT_AUTH_COOKIE, DEFAULT_LOGGED_EVENTS, logPanel.EVENT_LISTING)
+""" % (DEFAULT_CONTROL_ADDR, DEFAULT_CONTROL_PORT, DEFAULT_LOGGED_EVENTS, logPanel.EVENT_LISTING)
 
-class Input:
-  "Collection of the user's command line input"
-  
-  def __init__(self, args):
-    self.controlAddr = DEFAULT_CONTROL_ADDR     # controller interface IP address
-    self.controlPort = DEFAULT_CONTROL_PORT     # controller interface port
-    self.authType = NO_AUTH                     # type of authentication used
-    self.authCookieLoc = DEFAULT_AUTH_COOKIE    # location of authentication cookie
-    self.authPassword = ""                      # authentication password
-    self.loggedEvents = DEFAULT_LOGGED_EVENTS   # flags for event types in message log
-    self.isValid = True                         # determines if the program should run
-    self.printVersion = False                   # prints version then quits
-    self.printHelp = False                      # prints help then quits
-    self._parseArgs(args)
-  
-  def _parseArgs(self, args):
-    """
-    Recursively parses arguments, populating parameters and checking input 
-    validity. This does not check if options are defined multiple times.
-    """
-    
-    if len(args) == 0: return
-    elif args[0] == "-i" or args[0] == "--interface":
-      # defines control interface address/port
-      if len(args) >= 2:
-        interfaceArg = args[1]
-        
-        try:
-          divIndex = interfaceArg.find(":")
-          
-          if divIndex == -1:
-            self.controlAddr = DEFAULT_CONTROL_ADDR
-            self.controlPort = int(interfaceArg)
-          else:
-            self.controlAddr = interfaceArg[0:divIndex]
-            if not isValidIpAddr(self.controlAddr): raise AssertionError()
-            self.controlPort = int(interfaceArg[divIndex + 1:])
-          self._parseArgs(args[2:])
-        except ValueError:
-          print "'%s' isn't a valid interface" % interfaceArg
-          self.isValid = False
-        except AssertionError:
-          print "'%s' isn't a valid IP address" % self.controlAddr
-          self.isValid = False
-      else:
-        print "%s argument provided without defining an interface" % args[0]
-        self.isValid = False
-        
-    elif args[0] == "-c" or args[0].startswith("-c=") or args[0] == "--cookie" or args[0].startswith("--cookie="):
-      # set to use cookie authentication (and possibly define location)
-      self.authType = COOKIE_AUTH
-      
-      # sets authentication path if provided
-      if args[0].startswith("-c="):
-        self.authCookieLoc = args[0][3:]
-      elif args[0].startswith("--cookie="):
-        self.authCookieLoc = args[0][9:]
-      
-      self._parseArgs(args[1:])
-    elif args[0] == "-p" or args[0].startswith("-p=") or args[0] == "--password" or args[0].startswith("--password="):
-      # set to use password authentication
-      self.authType = PASSWORD_AUTH
-      
-      # sets authentication password if provided
-      if args[0].startswith("-p="):
-        self.authPassword = args[0][3:]
-      elif args[0].startswith("--password="):
-        self.authPassword = args[0][11:]
-      
-      self._parseArgs(args[1:])
-    elif args[0].startswith("-e=") or args[0].startswith("--event="):
-      # set event flags
-      if args[0].startswith("-e="): self.loggedEvents = args[0][3:]
-      else: self.loggedEvents = args[0][8:]
-      self._parseArgs(args[1:])
-    elif args[0] == "-v" or args[0] == "--version":
-      self.printVersion = True
-      self._parseArgs(args[1:])
-    elif args[0] == "-h" or args[0] == "--help":
-      self.printHelp = True
-      self._parseArgs(args[1:])
-    else:
-      print "Unrecognized command: " + args[0]
-      self.isValid = False
-
 def isValidIpAddr(ipStr):
   """
   Returns true if input is a valid IPv4 address, false otherwise.
@@ -157,61 +68,127 @@
   return True
 
 if __name__ == '__main__':
-  # parses user input, quitting if there's a problem
-  input = Input(sys.argv[1:])
-  if not input.isValid: sys.exit()
+  controlAddr = DEFAULT_CONTROL_ADDR     # controller interface IP address
+  controlPort = DEFAULT_CONTROL_PORT     # controller interface port
+  authPassword = ""                      # authentication password (prompts if unset and needed)
+  isBlindMode = False                    # allows connection lookups to be disabled
+  loggedEvents = DEFAULT_LOGGED_EVENTS   # flags for event types in message log
   
-  # if help or version flags are set then prints and quits
-  if input.printHelp:
-    print HELP_TEXT
+  # parses user input, noting any issues
+  try:
+    opts, args = getopt.getopt(sys.argv[1:], OPT, OPT_EXPANDED)
+  except getopt.GetoptError, exc:
+    print str(exc) + " (for usage provide --help)"
     sys.exit()
-  elif input.printVersion:
-    print "arm version %s (released %s)\n" % (VERSION, LAST_MODIFIED)
-    sys.exit()
   
-  # validates that cookie authentication path exists
-  if input.authType == COOKIE_AUTH and not os.path.exists(input.authCookieLoc):
-    print "Authentication cookie doesn't exist: %s" % input.authCookieLoc
-    sys.exit()
+  for opt, arg in opts:
+    if opt in ("-i", "--interface"):
+      # defines control interface address/port
+      try:
+        divIndex = arg.find(":")
+        
+        if divIndex == -1:
+          controlPort = int(arg)
+        else:
+          controlAddr = arg[0:divIndex]
+          controlPort = int(arg[divIndex + 1:])
+        
+        # validates that input is a valid ip address and port
+        if divIndex != -1 and not isValidIpAddr(controlAddr):
+          raise AssertionError("'%s' isn't a valid IP address" % controlAddr)
+        elif controlPort < 0 or controlPort > 65535:
+          raise AssertionError("'%s' isn't a valid port number (ports range 0-65535)" % controlPort)
+      except ValueError:
+        print "'%s' isn't a valid port number" % arg
+        sys.exit()
+      except AssertionError, exc:
+        print exc
+        sys.exit()
+    elif opt in ("-p", "--password"): authPassword = arg    # sets authentication password
+    elif opt in ("-b", "--blind"): isBlindMode = True       # prevents connection lookups
+    elif opt in ("-e", "--event"): loggedEvents = arg       # set event flags
+    elif opt in ("-v", "--version"):
+      print "arm version %s (released %s)\n" % (VERSION, LAST_MODIFIED)
+      sys.exit()
+    elif opt in ("-h", "--help"):
+      print HELP_TEXT
+      sys.exit()
   
-  # promts for password if not provided
-  if input.authType == PASSWORD_AUTH and input.authPassword == "":
-    input.authPassword = getpass.getpass()
-  
-  # validates and expands logged event flags
+  # validates and expands log event flags
   try:
-    expandedEvents = logPanel.expandEvents(input.loggedEvents)
+    expandedEvents = logPanel.expandEvents(loggedEvents)
   except ValueError, exc:
     for flag in str(exc):
       print "Unrecognized event flag: %s" % flag
     sys.exit()
   
-  # temporarily disables TorCtl logging to prevent issues from going to stdout when starting
+  # temporarily disables TorCtl logging to prevent issues from going to stdout while starting
   TorUtil.loglevel = "NONE"
   
   # attempts to open a socket to the tor server
-  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   try:
-    s.connect((input.controlAddr, input.controlPort))
+    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    s.connect((controlAddr, controlPort))
     conn = TorCtl.Connection(s)
+  except socket.error, exc:
+    if str(exc) == "[Errno 111] Connection refused":
+      # most common case - tor control port isn't available
+      print "Connection refused. Is the ControlPort enabled?"
+    else:
+      # less common issue - provide exc message
+      print "Failed to establish socket: %s" % exc
     
-    # provides authentication credentials to the control port
-    if input.authType == NO_AUTH:
+    sys.exit()
+  
+  # check PROTOCOLINFO for authentication type
+  try:
+    authInfo = conn.sendAndRecv("PROTOCOLINFO\r\n")[1][1]
+  except TorCtl.ErrorReply, exc:
+    print "Unable to query PROTOCOLINFO for authentication type: %s" % exc
+    sys.exit()
+  
+  try:
+    if authInfo.startswith("AUTH METHODS=NULL"):
+      # no authentication required
       conn.authenticate("")
-    elif input.authType == COOKIE_AUTH:
-      authCookie = open(input.authCookieLoc)
-      conn.authenticate_cookie(authCookie)
-      authCookie.close()
+    elif authInfo.startswith("AUTH METHODS=HASHEDPASSWORD"):
+      # password authentication, promts for password if it wasn't provided
+      if not authPassword: authPassword = getpass.getpass()
+      conn.authenticate(authPassword)
+    elif authInfo.startswith("AUTH METHODS=COOKIE"):
+      # cookie authtication, parses path to authentication cookie
+      start = authInfo.find("COOKIEFILE=\"") + 12
+      end = authInfo[start:].find("\"")
+      authCookiePath = authInfo[start:start + end]
+      
+      try:
+        authCookie = open(authCookiePath, "r")
+        conn.authenticate_cookie(authCookie)
+        authCookie.close()
+      except IOError, exc:
+        # cleaner message for common errors
+        issue = None
+        if str(exc).startswith("[Errno 13] Permission denied"): issue = "permission denied"
+        elif str(exc).startswith("[Errno 2] No such file or directory"): issue = "file doesn't exist"
+        
+        # if problem's recognized give concise message, otherwise print exception string
+        if issue: print "Failed to read authentication cookie (%s): %s" % (issue, authCookiePath)
+        else: print "Failed to read authentication cookie: %s" % exc
+        
+        sys.exit()
     else:
-      assert input.authType == PASSWORD_AUTH, "Invalid value in input.authType enum: " + str(input.authType)
-      conn.authenticate(input.authPassword)
-  except socket.error, exc:
-    print "Is the ControlPort enabled? Connection failed: %s" % exc
-    sys.exit()
+      # authentication type unrecognized (probably a new addition to the controlSpec)
+      print "Unrecognized authentication type: %s" % authInfo
+      sys.exit()
   except TorCtl.ErrorReply, exc:
-    print "Connection failed: %s" % exc
+    # authentication failed
+    issue = str(exc)
+    if str(exc).startswith("515 Authentication failed: Password did not match"): issue = "password incorrect"
+    if str(exc) == "515 Authentication failed: Wrong length on authentication cookie.": issue = "cookie value incorrect"
+    
+    print "Unable to authenticate: %s" % issue
     sys.exit()
   
-  controller.startTorMonitor(conn, expandedEvents)
+  controller.startTorMonitor(conn, expandedEvents, isBlindMode)
   conn.close()
 

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2010-02-07 15:19:23 UTC (rev 21579)
+++ arm/trunk/interface/controller.py	2010-02-08 01:47:07 UTC (rev 21580)
@@ -31,7 +31,6 @@
 import connCountMonitor
 
 CONFIRM_QUIT = True
-DISABLE_CONNECTIONS_PAGE = False
 REFRESH_RATE = 5        # seconds between redrawing screen
 cursesLock = RLock()    # global curses lock (curses isn't thread safe and
                         # concurrency bugs produce especially sinister glitches)
@@ -54,13 +53,14 @@
 class ControlPanel(util.Panel):
   """ Draws single line label for interface controls. """
   
-  def __init__(self, lock, resolver):
+  def __init__(self, lock, resolver, isBlindMode):
     util.Panel.__init__(self, lock, 1)
     self.msgText = CTL_HELP           # message text to be displyed
     self.msgAttr = curses.A_NORMAL    # formatting attributes
     self.page = 1                     # page number currently being displayed
     self.resolver = resolver          # dns resolution thread
-    self.resolvingCounter = -1         # count of resolver when starting (-1 if we aren't working on a batch)
+    self.resolvingCounter = -1        # count of resolver when starting (-1 if we aren't working on a batch)
+    self.isBlindMode = isBlindMode
   
   def setMsg(self, msgText, msgAttr=curses.A_NORMAL):
     """
@@ -109,7 +109,7 @@
           currentPage = self.page
           pageCount = len(PAGES)
           
-          if DISABLE_CONNECTIONS_PAGE:
+          if self.isBlindMode:
             if currentPage >= 2: currentPage -= 1
             pageCount -= 1
           
@@ -210,9 +210,13 @@
   # 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")
+  # removes special types only used in arm (UNKNOWN, TORCTL, ARM_DEBUG, etc)
+  toDiscard = []
+  for event in connEvents:
+    if event not in logPanel.TOR_EVENT_TYPES.values(): toDiscard += [event]
   
+  for event in toDiscard: connEvents.discard(event)
+  
   while not eventsSet:
     try:
       conn.set_events(connEvents)
@@ -231,7 +235,7 @@
         
         if eventType in REQ_EVENTS:
           if eventType == "BW": msg = "(bandwidth graph won't function)"
-          elif eventType in ("NEWDESC", "NEWCONSENSUS"): msg = "(connections listing can't register consensus changes)"
+          elif eventType in ("NEWDESC", "NEWCONSENSUS") and not isBlindMode: 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)
@@ -242,7 +246,7 @@
   loggedEvents.sort() # alphabetizes
   return loggedEvents
 
-def drawTorMonitor(stdscr, conn, loggedEvents):
+def drawTorMonitor(stdscr, conn, loggedEvents, isBlindMode):
   """
   Starts arm interface reflecting information on provided control port.
   
@@ -314,14 +318,14 @@
   
   # starts thread for processing netstat queries
   connResolutionThread = connResolver.ConnResolver(conn, torPid, panels["log"])
-  connResolutionThread.start()
+  if not isBlindMode: connResolutionThread.start()
   
   panels["conn"] = connPanel.ConnPanel(cursesLock, conn, connResolutionThread, panels["log"])
-  panels["control"] = ControlPanel(cursesLock, panels["conn"].resolver)
+  panels["control"] = ControlPanel(cursesLock, panels["conn"].resolver, isBlindMode)
   panels["torrc"] = confPanel.ConfPanel(cursesLock, confLocation, conn, panels["log"])
   
   # prevents netstat calls by connPanel if not being used
-  if DISABLE_CONNECTIONS_PAGE: panels["conn"].isDisabled = True
+  if isBlindMode: panels["conn"].isDisabled = True
   
   # provides error if pid coulnd't be determined (hopefully shouldn't happen...)
   if not torPid: panels["log"].monitor_event("WARN", "Unable to resolve tor pid, abandoning connection listing")
@@ -329,7 +333,7 @@
   # statistical monitors for graph
   panels["graph"].addStats("bandwidth", bandwidthMonitor.BandwidthMonitor(conn))
   panels["graph"].addStats("system resources", cpuMemMonitor.CpuMemMonitor(panels["header"]))
-  panels["graph"].addStats("connections", connCountMonitor.ConnCountMonitor(conn, connResolutionThread))
+  if not isBlindMode: panels["graph"].addStats("connections", connCountMonitor.ConnCountMonitor(conn, connResolutionThread))
   panels["graph"].setStats("bandwidth")
   
   # listeners that update bandwidth and log panels with Tor status
@@ -337,7 +341,7 @@
   conn.add_event_listener(panels["log"])
   conn.add_event_listener(panels["graph"].stats["bandwidth"])
   conn.add_event_listener(panels["graph"].stats["system resources"])
-  conn.add_event_listener(panels["graph"].stats["connections"])
+  if not isBlindMode: conn.add_event_listener(panels["graph"].stats["connections"])
   conn.add_event_listener(panels["conn"])
   conn.add_event_listener(sighupTracker)
   
@@ -382,7 +386,7 @@
         
         # other panels that use torrc data
         panels["conn"].resetOptions()
-        panels["graph"].stats["connections"].resetOptions(conn)
+        if not isBlindMode: panels["graph"].stats["connections"].resetOptions(conn)
         panels["graph"].stats["bandwidth"].resetOptions()
         
         panels["torrc"].reset()
@@ -474,7 +478,7 @@
       else: page = (page + 1) % len(PAGES)
       
       # skip connections listing if it's disabled
-      if page == 1 and DISABLE_CONNECTIONS_PAGE:
+      if page == 1 and isBlindMode:
         if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
         else: page = (page + 1) % len(PAGES)
       
@@ -516,13 +520,8 @@
           popup.addfstr(2, 41, "<b>d</b>: file descriptors")
           popup.addfstr(3, 2, "<b>e</b>: change logged events")
           
-          runlevelEventsLabel = "arm and tor"
-          if panels["log"].runlevelTypes == logPanel.RUNLEVEL_TOR_ONLY: runlevelEventsLabel = "tor only"
-          elif panels["log"].runlevelTypes == logPanel.RUNLEVEL_ARM_ONLY: runlevelEventsLabel = "arm only"
-          popup.addfstr(3, 41, "<b>r</b>: logged runlevels (<b>%s</b>)" % runlevelEventsLabel)
-          
           regexLabel = "enabled" if panels["log"].regexFilter else "disabled"
-          popup.addfstr(4, 2, "<b>f</b>: log regex filter (<b>%s</b>)" % regexLabel)
+          popup.addfstr(3, 41, "<b>f</b>: log regex filter (<b>%s</b>)" % regexLabel)
           
           pageOverrideKeys = (ord('s'), ord('i'), ord('d'), ord('e'), ord('r'), ord('f'))
         if page == 1:
@@ -765,25 +764,6 @@
       # reverts changes made for popup
       panels["graph"].showLabel = True
       setPauseState(panels, isPaused, page)
-    elif page == 0 and (key == ord('r') or key == ord('R')):
-      # provides menu to pick the type of runlevel events to log
-      options = ["tor only", "arm only", "arm and tor"]
-      initialSelection = panels["log"].runlevelTypes
-      
-      # 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"], "Logged Runlevels:", options, initialSelection)
-      
-      # reverts changes made for popup
-      panels["graph"].showLabel = True
-      setPauseState(panels, isPaused, page)
-      
-      # applies new setting
-      if selection != -1: panels["log"].runlevelTypes = selection
     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
@@ -1106,6 +1086,6 @@
     elif page == 2:
       panels["torrc"].handleKey(key)
 
-def startTorMonitor(conn, loggedEvents):
-  curses.wrapper(drawTorMonitor, conn, loggedEvents)
+def startTorMonitor(conn, loggedEvents, isBlindMode):
+  curses.wrapper(drawTorMonitor, conn, loggedEvents, isBlindMode)
 

Modified: arm/trunk/interface/logPanel.py
===================================================================
--- arm/trunk/interface/logPanel.py	2010-02-07 15:19:23 UTC (rev 21579)
+++ arm/trunk/interface/logPanel.py	2010-02-08 01:47:07 UTC (rev 21580)
@@ -17,13 +17,12 @@
 PRE_POPULATE_MAX_LIMIT = 5000             # limit for NOTICE - ERR (since most lines are skipped)
 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"}
-RUNLEVEL_TOR_ONLY, RUNLEVEL_ARM_ONLY, RUNLEVEL_BOTH = range(3)
 
-EVENT_TYPES = {
-  "d": "DEBUG",   "a": "ADDRMAP",       "l": "NEWDESC",     "v": "AUTHDIR_NEWDESCS",
-  "i": "INFO",    "b": "BW",            "m": "NS",          "x": "STATUS_GENERAL",
-  "n": "NOTICE",  "c": "CIRC",          "o": "ORCONN",      "y": "STATUS_CLIENT",
-  "w": "WARN",    "f": "DESCCHANGED",   "s": "STREAM",      "z": "STATUS_SERVER",
+TOR_EVENT_TYPES = {
+  "d": "DEBUG",   "a": "ADDRMAP",       "l": "NEWDESC",       "v": "AUTHDIR_NEWDESCS",
+  "i": "INFO",    "b": "BW",            "m": "NS",            "x": "STATUS_GENERAL",
+  "n": "NOTICE",  "c": "CIRC",          "o": "ORCONN",        "y": "STATUS_CLIENT",
+  "w": "WARN",    "f": "DESCCHANGED",   "s": "STREAM",        "z": "STATUS_SERVER",
   "e": "ERR",     "g": "GUARD",         "t": "STREAM_BW",
                   "k": "NEWCONSENSUS",  "u": "CLIENTS_SEEN"}
 
@@ -31,44 +30,54 @@
         i INFO      b BW              m NS              x STATUS_GENERAL
         n NOTICE    c CIRC            o ORCONN          y STATUS_CLIENT
         w WARN      f DESCCHANGED     s STREAM          z STATUS_SERVER
-        e ERR       g GUARD           t STREAM_BW
-                    k NEWCONSENSUS    u CLIENTS_SEEN
-        Aliases:    A All Events      X No Events       U Unknown Events
-                    DINWE Runlevel and higher severity"""
+        e ERR       g GUARD           t STREAM_BW       A All Events
+                    k NEWCONSENSUS    u CLIENTS_SEEN    X No Events
+          DINWE Runlevel and higher severity            C TorCtl Events
+          12345 ARM runlevel and higher severity        U Unknown Events"""
 
 TOR_CTL_CLOSE_MSG = "Tor closed control connection. Exiting event thread."
 
 def expandEvents(eventAbbr):
   """
   Expands event abbreviations to their full names. Beside mappings privided in
-  EVENT_TYPES this recognizes:
-  A - alias for all events
-  U - "UNKNOWN" events
-  R - alias for runtime events (DEBUG, INFO, NOTICE, WARN, ERR)
+  TOR_EVENT_TYPES this recognizes the following special events and aliases:
+  C - TORCTL runlevel events
+  U - UKNOWN events
+  A - all events
+  X - no events
+  DINWE - runlevel and higher
+  12345 - arm runlevel and higher (ARM_DEBUG - ARM_ERR)
   Raises ValueError with invalid input if any part isn't recognized.
   
-  Example:
+  Examples:
   "inUt" -> ["INFO", "NOTICE", "UNKNOWN", "STREAM_BW"]
+  "N4" -> ["NOTICE", "WARN", "ERR", "ARM_WARN", "ARM_ERR"]
+  "cfX" -> []
   """
   
   expandedEvents = set()
   invalidFlags = ""
   for flag in eventAbbr:
     if flag == "A":
-      expandedEvents = set(EVENT_TYPES.values())
-      expandedEvents.add("UNKNOWN")
+      expandedEvents = set(TOR_EVENT_TYPES.values() + ["ARM_DEBUG", "ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"])
       break
     elif flag == "X":
       expandedEvents = set()
       break
+    elif flag == "C": expandedEvents.add("TORCTL")
     elif flag == "U": expandedEvents.add("UNKNOWN")
     elif flag == "D": expandedEvents = expandedEvents.union(set(["DEBUG", "INFO", "NOTICE", "WARN", "ERR"]))
     elif flag == "I": expandedEvents = expandedEvents.union(set(["INFO", "NOTICE", "WARN", "ERR"]))
     elif flag == "N": expandedEvents = expandedEvents.union(set(["NOTICE", "WARN", "ERR"]))
     elif flag == "W": expandedEvents = expandedEvents.union(set(["WARN", "ERR"]))
     elif flag == "E": expandedEvents.add("ERR")
-    elif flag in EVENT_TYPES:
-      expandedEvents.add(EVENT_TYPES[flag])
+    elif flag == "1": expandedEvents = expandedEvents.union(set(["ARM_DEBUG", "ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
+    elif flag == "2": expandedEvents = expandedEvents.union(set(["ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
+    elif flag == "3": expandedEvents = expandedEvents.union(set(["ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
+    elif flag == "4": expandedEvents = expandedEvents.union(set(["ARM_WARN", "ARM_ERR"]))
+    elif flag == "5": expandedEvents.add("ARM_ERR")
+    elif flag in TOR_EVENT_TYPES:
+      expandedEvents.add(TOR_EVENT_TYPES[flag])
     else:
       invalidFlags += flag
   
@@ -91,7 +100,6 @@
     self.lastHeartbeat = time.time()      # time of last event
     self.regexFilter = None               # filter for presented log events (no filtering if None)
     self.eventTimeOverwrite = None        # replaces time for further events with this (uses time it occures if None)
-    self.runlevelTypes = RUNLEVEL_BOTH    # types of runlevels to show (arm, tor, or both)
     self.controlPortClosed = False        # flag set if TorCtl provided notice that control port is closed
     
     # attempts to process events from log file
@@ -193,7 +201,6 @@
     if "BW" in self.loggedEvents: self.registerEvent("BW", "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
   
   def msg_event(self, event):
-    if not self.runlevelTypes in (RUNLEVEL_TOR_ONLY, RUNLEVEL_BOTH): return
     self.registerEvent(event.level, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
   
   def new_desc_event(self, event):
@@ -223,10 +230,13 @@
     if "UNKNOWN" in self.loggedEvents: self.registerEvent("UNKNOWN", event.event_string, "red")
   
   def monitor_event(self, level, msg):
-    # events provided by the arm monitor - types use the same as runlevel
-    if not self.runlevelTypes in (RUNLEVEL_ARM_ONLY, RUNLEVEL_BOTH): return
-    if level in self.loggedEvents: self.registerEvent("ARM-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
+    # events provided by the arm monitor
+    if "ARM_" + level in self.loggedEvents: self.registerEvent("ARM-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
   
+  def tor_ctl_event(self, level, msg):
+    # events provided by TorCtl
+    if "TORCTL" in self.loggedEvents: self.registerEvent("TORCTL-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
+  
   def write(self, msg):
     """
     Tracks TorCtl events. Ugly hack since TorCtl/TorUtil.py expects a file.
@@ -242,8 +252,7 @@
       # TorCtl providing notice that control port is closed
       self.controlPortClosed = True
       self.monitor_event("NOTICE", "Tor control port closed")
-    else:
-      self.monitor_event(level, "TorCtl: " + msg)
+    self.tor_ctl_event(level, msg)
   
   def flush(self): pass
   
@@ -295,7 +304,16 @@
         # draws label - uses ellipsis if too long, for instance:
         # Events (DEBUG, INFO, NOTICE, WARN...):
         eventsLabel = "Events"
-        eventsListing = ", ".join(self.loggedEvents)
+        
+        # separates tor and arm runlevels (might be able to show as range)
+        eventsList = list(self.loggedEvents)
+        torRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, ""))
+        armRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, "ARM_"))
+        
+        if armRunlevelLabel: eventsList = ["ARM " + armRunlevelLabel] + eventsList
+        if torRunlevelLabel: eventsList = [torRunlevelLabel] + eventsList
+        
+        eventsListing = ", ".join(eventsList)
         filterLabel = "" if not self.regexFilter else " - filter: %s" % self.regexFilter.pattern
         
         firstLabelLen = eventsListing.find(", ")
@@ -379,6 +397,48 @@
     
     return time.time() - self.lastHeartbeat
 
+def parseRunlevelRanges(eventsList, searchPrefix):
+  """
+  This parses a list of events to provide an ordered list of runlevels, 
+  condensed if three or more are in a contiguous range. This removes parsed 
+  runlevels from the eventsList. For instance:
+  
+  eventsList = ["BW", "ARM_WARN", "ERR", "ARM_ERR", "ARM_DEBUG", "ARM_NOTICE"]
+  searchPrefix = "ARM_"
+  
+  results in:
+  eventsList = ["BW", "ERR"]
+  return value is ["DEBUG", "NOTICE - ERR"]
+  
+  """
+  
+  # blank ending runlevel forces the break condition to be reached at the end
+  runlevels = ["DEBUG", "INFO", "NOTICE", "WARN", "ERR", ""]
+  runlevelLabels = []
+  start, end = "", ""
+  rangeLength = 0
+  
+  for level in runlevels:
+    if searchPrefix + level in eventsList:
+      eventsList.remove(searchPrefix + level)
+      
+      if start:
+        end = level
+        rangeLength += 1
+      else:
+        start = level
+        rangeLength = 1
+    elif rangeLength > 0:
+      # reached a break in the runlevels
+      if rangeLength == 1: runlevelLabels += [start]
+      elif rangeLength == 2: runlevelLabels += [start, end]
+      else: runlevelLabels += ["%s - %s" % (start, end)]
+      
+      start, end = "", ""
+      rangeLength = 0
+  
+  return runlevelLabels
+
 def splitLine(message, x):
   """
   Divides message into two lines, attempting to do it on a wordbreak.



More information about the tor-commits mailing list