[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[or-cvs] r23663: {arm} added: config panel can now show tor's state, the torrc, and (in arm/trunk: . src src/interface src/util)



Author: atagar
Date: 2010-10-24 00:10:16 +0000 (Sun, 24 Oct 2010)
New Revision: 23663

Modified:
   arm/trunk/armrc.sample
   arm/trunk/src/interface/confPanel.py
   arm/trunk/src/interface/controller.py
   arm/trunk/src/starter.py
   arm/trunk/src/util/conf.py
Log:
added: config panel can now show tor's state, the torrc, and arm's state/rc
added: INFO level logging for arm startup time
fix: extra line when line cap was reached



Modified: arm/trunk/armrc.sample
===================================================================
--- arm/trunk/armrc.sample	2010-10-23 22:03:33 UTC (rev 23662)
+++ arm/trunk/armrc.sample	2010-10-24 00:10:16 UTC (rev 23663)
@@ -13,9 +13,6 @@
 # Renders the interface with color if set and the terminal supports it
 features.colorInterface true
 
-# Checks the torrc for issues, warning and hilighting problems if true.
-features.torrc.validate true
-
 # Set this if you're running in a chroot jail or other environment where tor's
 # resources (log, state, etc) should have a prefix in their paths.
 features.pathPrefix
@@ -56,13 +53,19 @@
 
 # Paremters for the config panel
 # ---------------------------
+# type
+#   0 -> tor state, 1 -> torrc, 2 -> arm state, 3 -> armrc
+# validate
+#   checks torrc and armrcs for issues, warning and hilighting problems if true
 # showScrollbars
 #   displays scrollbars when the torrc content is longer than the display
 # maxLinesPerEntry
 #   max number of lines to display for a single entry in the torrc
 
+features.config.type 0
+features.config.validate true
 features.config.showScrollbars true
-features.config.maxLinesPerEntry 5
+features.config.maxLinesPerEntry 8
 
 # General graph parameters
 # ------------------------
@@ -142,6 +145,7 @@
 cache.armLog.trimSize 200
 
 # Runlevels at which arm logs its events
+log.startTime INFO
 log.refreshRate DEBUG
 log.configEntryNotFound NONE
 log.configEntryUndefined NOTICE

Modified: arm/trunk/src/interface/confPanel.py
===================================================================
--- arm/trunk/src/interface/confPanel.py	2010-10-23 22:03:33 UTC (rev 23662)
+++ arm/trunk/src/interface/confPanel.py	2010-10-24 00:10:16 UTC (rev 23663)
@@ -6,15 +6,20 @@
 import curses
 import threading
 
-from util import log, panel, torrc, uiTools
+from util import conf, log, panel, torrc, torTools, uiTools
 
-DEFAULT_CONFIG = {"features.torrc.validate": True,
+DEFAULT_CONFIG = {"features.config.type": 0,
+                  "features.config.validate": True,
                   "features.config.showScrollbars": True,
-                  "features.config.maxLinesPerEntry": 5,
+                  "features.config.maxLinesPerEntry": 8,
                   "log.confPanel.torrcReadFailed": log.WARN,
                   "log.torrcValidation.duplicateEntries": log.NOTICE,
                   "log.torrcValidation.torStateDiffers": log.NOTICE}
 
+# configurations that can be displayed
+TOR_STATE, TORRC, ARM_STATE, ARMRC = range(4)
+CONFIG_LABELS = {TORRC: "torrc", TOR_STATE: "tor state", ARMRC: "armrc", ARM_STATE: "arm state"}
+
 class ConfPanel(panel.Panel):
   """
   Presents torrc, armrc, or loaded settings with syntax highlighting in a
@@ -26,99 +31,150 @@
     
     self._config = dict(DEFAULT_CONFIG)
     if config:
-      config.update(self._config, {"features.config.maxLinesPerEntry": 1})
+      config.update(self._config, {
+        "features.config.type": (0, 3),
+        "features.config.maxLinesPerEntry": 1})
     
     self.valsLock = threading.RLock()
     self.scroll = 0
+    self.showLabel = True         # shows top label if true, hides otherwise
     self.showLineNum = True
     self.stripComments = False
-    self.confLocation = ""
-    self.confContents = None # read torrc, None if it failed to load
-    self.corrections = {}
     
+    # type of config currently being displayed
+    self.configType = self._config["features.config.type"]
+    
+    # Mappings of config types to tuples of:
+    # (contents, corrections, confLocation)
+    # This maps to None if they haven't been loaded yet or failed to load.
+    self.configs = {TORRC: None, TOR_STATE: None, ARMRC: None, ARM_STATE: None}
+    
     # height of the content when last rendered (the cached value is invalid if
     # _lastContentHeightArgs is None or differs from the current dimensions)
     self._lastContentHeight = 1
     self._lastContentHeightArgs = None
     
-    self.reset()
+    self.loadConfig(TOR_STATE)
+    self.loadConfig(TORRC)
   
-  def reset(self, logErrors = True):
+  def loadConfig(self, configType = None, logErrors = True):
     """
-    Reloads torrc contents and resets scroll height. Returns True if
-    successful, else false.
+    Reloads configuration or state contents and resets scroll height. Returns
+    True if successful, else false.
     
     Arguments:
-      logErrors - logs if unable to read the torrc or issues are found during
-                  validation
+      configType - configuration type to load (displayed config type if None)
+      logErrors  - logs if unable to read the torrc or issues are found during
+                   validation
     """
     
     self.valsLock.acquire()
+    if configType == None: configType = self.configType
+    confContents, corrections, confLocation = [], {}, None
     
-    try:
-      self.confLocation = torrc.getConfigLocation()
-      confFile = open(self.confLocation, "r")
-      self.confContents = confFile.readlines()
-      confFile.close()
-      self.scroll = 0
+    if configType in (TORRC, ARMRC):
+      # load configuration file
+      try:
+        if configType == TORRC: confLocation = torrc.getConfigLocation()
+        else:
+          confLocation = conf.getConfig("arm").path
+          if not confLocation: raise IOError("no armrc has been loaded")
+        
+        confFile = open(confLocation, "r")
+        confContents = confFile.readlines()
+        confFile.close()
+        self.scroll = 0
+      except IOError, exc:
+        self.configs[configType] = None
+        msg = "Unable to load torrc (%s)" % exc
+        if logErrors: log.log(self._config["log.confPanel.torrcReadFailed"], msg)
+        self.valsLock.release()
+        return False
       
-      # sets the content height to be something somewhat reasonable
-      self._lastContentHeight = len(self.confContents)
-      self._lastContentHeightArgs = None
-    except IOError, exc:
-      self.confContents = None
-      msg = "Unable to load torrc (%s)" % exc
-      if logErrors: log.log(self._config["log.confPanel.torrcReadFailed"], msg)
-      self.valsLock.release()
-      return False
-    
-    if self._config["features.torrc.validate"]:
-      self.corrections = torrc.validate(self.confContents)
-      
-      if self.corrections and logErrors:
-        # logs issues found during validation
-        irrelevantLines, mismatchLines = [], []
-        for lineNum in self.corrections:
-          problem = self.corrections[lineNum][0]
-          if problem == torrc.VAL_DUPLICATE: irrelevantLines.append(lineNum)
-          elif problem == torrc.VAL_MISMATCH: mismatchLines.append(lineNum)
+      if configType == TORRC and self._config["features.config.validate"]:
+        # TODO: add armrc validation
+        corrections = torrc.validate(confContents)
         
-        if irrelevantLines:
-          irrelevantLines.sort()
+        if corrections and logErrors:
+          # logs issues found during validation
+          irrelevantLines, mismatchLines = [], []
+          for lineNum in corrections:
+            problem = corrections[lineNum][0]
+            if problem == torrc.VAL_DUPLICATE: irrelevantLines.append(lineNum)
+            elif problem == torrc.VAL_MISMATCH: mismatchLines.append(lineNum)
           
-          if len(irrelevantLines) > 1: first, second, third = "Entries", "are", ", including lines"
-          else: first, second, third = "Entry", "is", " on line"
-          msgStart = "%s in your torrc %s ignored due to duplication%s" % (first, second, third)
-          msgLines = ", ".join([str(val + 1) for val in irrelevantLines])
-          msg = "%s: %s (highlighted in blue)" % (msgStart, msgLines)
-          log.log(self._config["log.torrcValidation.duplicateEntries"], msg)
+          if irrelevantLines:
+            irrelevantLines.sort()
+            
+            if len(irrelevantLines) > 1: first, second, third = "Entries", "are", ", including lines"
+            else: first, second, third = "Entry", "is", " on line"
+            msgStart = "%s in your torrc %s ignored due to duplication%s" % (first, second, third)
+            msgLines = ", ".join([str(val + 1) for val in irrelevantLines])
+            msg = "%s: %s (highlighted in blue)" % (msgStart, msgLines)
+            log.log(self._config["log.torrcValidation.duplicateEntries"], msg)
+          
+          if mismatchLines:
+            mismatchLines.sort()
+            msgStart = "Tor's state differs from loaded torrc on line%s" % ("s" if len(mismatchLines) > 1 else "")
+            msgLines = ", ".join([str(val + 1) for val in mismatchLines])
+            msg = "%s: %s" % (msgStart, msgLines)
+            log.log(self._config["log.torrcValidation.torStateDiffers"], msg)
+      
+      if confContents:
+        # Restricts contents to be displayable characters:
+        # - Tabs print as three spaces. Keeping them as tabs is problematic for
+        #   the layout since it's counted as a single character, but occupies
+        #   several cells.
+        # - Strips control and unprintable characters.
+        for lineNum in range(len(confContents)):
+          lineText = confContents[lineNum]
+          lineText = lineText.replace("\t", "   ")
+          lineText = "".join([char for char in lineText if curses.ascii.isprint(char)])
+          confContents[lineNum] = lineText
+    elif configType == TOR_STATE:
+      # for all recognized tor config options, provide their current value
+      conn = torTools.getConn()
+      configOptionQuery = conn.getInfo("config/names", "").strip().split("\n")
+      
+      for lineNum in range(len(configOptionQuery)):
+        # lines are of the form "<option> <type>", like:
+        # UseEntryGuards Boolean
+        line = configOptionQuery[lineNum]
+        confOption, confType = line.strip().split(" ", 1)
+        confValue = ", ".join(conn.getOption(confOption, [], True))
         
-        if mismatchLines:
-          mismatchLines.sort()
-          msgStart = "Tor's state differs from loaded torrc on line%s" % ("s" if len(mismatchLines) > 1 else "")
-          msgLines = ", ".join([str(val + 1) for val in mismatchLines])
-          msg = "%s: %s" % (msgStart, msgLines)
-          log.log(self._config["log.torrcValidation.torStateDiffers"], msg)
+        # provides nicer values for recognized types
+        if not confValue: confValue = "<none>"
+        elif confType == "Boolean" and confValue in ("0", "1"):
+          confValue = "False" if confValue == "0" else "True"
+        elif confType == "DataSize" and confValue.isdigit():
+          confValue = uiTools.getSizeLabel(int(confValue))
+        elif confType == "TimeInterval" and confValue.isdigit():
+          confValue = uiTools.getTimeLabel(int(confValue), isLong = True)
+        
+        confContents.append("%s %s\n" % (confOption, confValue))
+        
+        # hijacks the correction field to display the value's type
+        corrections[lineNum] = (None, confType)
+    elif configType == ARM_STATE:
+      # loaded via the conf utility
+      armConf = conf.getConfig("arm")
+      for key in armConf.getKeys():
+        confContents.append("%s %s\n" % (key, ", ".join(armConf.getValue(key, [], True))))
+      confContents.sort()
     
-    if self.confContents:
-      # Restricts contents to be displayable characters:
-      # - Tabs print as three spaces. Keeping them as tabs is problematic for
-      #   the layout since it's counted as a single character, but occupies
-      #   several cells.
-      # - Strips control and unprintable characters.
-      for lineNum in range(len(self.confContents)):
-        lineText = self.confContents[lineNum]
-        lineText = lineText.replace("\t", "   ")
-        lineText = "".join([char for char in lineText if curses.ascii.isprint(char)])
-        self.confContents[lineNum] = lineText
+    self.configs[configType] = (confContents, corrections, confLocation)
     
-    self.redraw(True)
+    # sets the content height to be something somewhat reasonable
+    self._lastContentHeight = len(confContents)
+    self._lastContentHeightArgs = None
+    
     self.valsLock.release()
     return True
   
   def handleKey(self, key):
     self.valsLock.acquire()
-    if uiTools.isScrollKey(key) and self.confContents != None:
+    if uiTools.isScrollKey(key) and self.configs[self.configType] != None:
       pageHeight = self.getPreferredSize()[0] - 1
       newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self._lastContentHeight)
       
@@ -131,12 +187,30 @@
       self.redraw(True)
     elif key == ord('s') or key == ord('S'):
       self.stripComments = not self.stripComments
-      self.scroll = 0
       self._lastContentHeightArgs = None
       self.redraw(True)
     
     self.valsLock.release()
   
+  def setConfigType(self, configType):
+    """
+    Sets the type of configuration to be displayed. If the configuration isn't
+    already loaded then this fetches it.
+    
+    Arguments
+      configType - enum representing the type of configuration to be loaded
+    """
+    
+    if self.configType != configType or not self.configs[configType]:
+      self.valsLock.acquire()
+      self.configType = configType
+      
+      if not self.configs[configType]: self.loadConfig()
+      
+      self._lastContentHeightArgs = None
+      self.redraw(True)
+      self.valsLock.release()
+  
   def draw(self, subwindow, width, height):
     self.valsLock.acquire()
     
@@ -147,18 +221,17 @@
     # height if not).
     trustLastContentHeight = self._lastContentHeightArgs == (width, height)
     
-    # draws the top label
-    locationLabel = " (%s)" % self.confLocation if self.confLocation else ""
-    self.addstr(0, 0, "Tor Config%s:" % locationLabel, curses.A_STANDOUT)
-    
     # restricts scroll location to valid bounds
     self.scroll = max(0, min(self.scroll, self._lastContentHeight - height + 1))
     
-    renderedContents = self.confContents
-    if self.confContents == None:
-      renderedContents = ["### Unable to load torrc ###"]
+    renderedContents, corrections, confLocation = None, {}, None
+    if self.configs[self.configType]:
+      renderedContents, corrections, confLocation = self.configs[self.configType]
+    
+    if renderedContents == None:
+      renderedContents = ["### Unable to load the %s ###" % CONFIG_LABELS[self.configType]]
     elif self.stripComments:
-      renderedContents = torrc.stripComments(self.confContents)
+      renderedContents = torrc.stripComments(renderedContents)
     
     # offset to make room for the line numbers
     lineNumOffset = int(math.log10(len(renderedContents))) + 2 if self.showLineNum else 0
@@ -171,12 +244,22 @@
     
     displayLine = -self.scroll + 1 # line we're drawing on
     
+    # draws the top label
+    if self.showLabel:
+      sourceLabel = "Tor" if self.configType in (TORRC, TOR_STATE) else "Arm"
+      typeLabel = "Config" if self.configType in (TORRC, ARMRC) else "State"
+      locationLabel = " (%s)" % confLocation if confLocation else ""
+      self.addstr(0, 0, "%s %s%s:" % (sourceLabel, typeLabel, locationLabel), curses.A_STANDOUT)
+    
     for lineNumber in range(0, len(renderedContents)):
       lineText = renderedContents[lineNumber]
       lineText = lineText.rstrip() # remove ending whitespace
       
-      # blank lines are hidden when stripping comments
-      hideLine = self.stripComments and not lineText
+      # blank lines are hidden when stripping comments, and undefined
+      # values are dropped if showing tor's state
+      if self.stripComments:
+        if not lineText: continue
+        elif self.configType == TOR_STATE and "<none>" in lineText: continue
       
       # splits the line into its component (msg, format) tuples
       lineComp = {"option": ["", curses.A_BOLD | uiTools.getColor("green")],
@@ -202,8 +285,8 @@
         lineComp["argument"][0] = lineText[optionEnd:]
       
       # gets the correction
-      if lineNumber in self.corrections:
-        lineIssue, lineIssueMsg = self.corrections[lineNumber]
+      if lineNumber in corrections:
+        lineIssue, lineIssueMsg = corrections[lineNumber]
         
         if lineIssue == torrc.VAL_DUPLICATE:
           lineComp["option"][1] = curses.A_BOLD | uiTools.getColor("blue")
@@ -211,9 +294,14 @@
         elif lineIssue == torrc.VAL_MISMATCH:
           lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("red")
           lineComp["correction"][0] = " (%s)" % lineIssueMsg
+        else:
+          # For some types of configs the correction field is simply used to
+          # provide extra data (for instance, the type for tor state fields).
+          lineComp["correction"][0] = " (%s)" % lineIssueMsg
+          lineComp["correction"][1] = curses.A_BOLD | uiTools.getColor("magenta")
       
       # draws the line number
-      if self.showLineNum and not hideLine and displayLine < height and displayLine >= 1:
+      if self.showLineNum and displayLine < height and displayLine >= 1:
         lineNumStr = ("%%%ii" % (lineNumOffset - 1)) % (lineNumber + 1)
         self.addstr(displayLine, scrollOffset, lineNumStr, curses.A_BOLD | uiTools.getColor("yellow"))
       
@@ -224,15 +312,14 @@
       
       while displayQueue:
         msg, format = displayQueue.pop(0)
-        if hideLine: break
         
         maxMsgSize, includeBreak = width - cursorLoc, False
         if len(msg) >= maxMsgSize:
           # message is too long - break it up
-          includeBreak = True
           if lineOffset == maxLinesPerEntry - 1:
             msg = uiTools.cropStr(msg, maxMsgSize)
           else:
+            includeBreak = True
             msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.END_WITH_HYPHEN, True)
             displayQueue.insert(0, (remainder.strip(), format))
         
@@ -240,12 +327,16 @@
         if msg and drawLine < height and drawLine >= 1:
           self.addstr(drawLine, cursorLoc, msg, format)
         
+        # If we're done, and have added content to this line, then start
+        # further content on the next line.
         cursorLoc += len(msg)
-        if includeBreak or not displayQueue:
+        includeBreak |= not displayQueue and cursorLoc != lineNumOffset + scrollOffset
+        
+        if includeBreak:
           lineOffset += 1
           cursorLoc = lineNumOffset + scrollOffset
       
-      displayLine += lineOffset
+      displayLine += max(lineOffset, 1)
       
       if trustLastContentHeight and displayLine >= height: break
     

Modified: arm/trunk/src/interface/controller.py
===================================================================
--- arm/trunk/src/interface/controller.py	2010-10-23 22:03:33 UTC (rev 23662)
+++ arm/trunk/src/interface/controller.py	2010-10-24 00:10:16 UTC (rev 23663)
@@ -46,6 +46,7 @@
           "queries.refreshRate.rate": 5,
           "log.torEventTypeUnrecognized": log.NOTICE,
           "features.graph.bw.prepopulate": True,
+          "log.startTime": log.INFO,
           "log.refreshRate": log.DEBUG,
           "log.configEntryUndefined": log.NOTICE}
 
@@ -304,7 +305,7 @@
   for panelKey in PAGES[page]:
     panels[panelKey].redraw(True)
 
-def drawTorMonitor(stdscr, loggedEvents, isBlindMode):
+def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
   """
   Starts arm interface reflecting information on provided control port.
   
@@ -456,6 +457,9 @@
   # TODO: popups need to force the panels it covers to redraw (or better, have
   # a global refresh function for after changing pages, popups, etc)
   
+  initTime = time.time() - startTime
+  log.log(CONFIG["log.startTime"], "arm started (initialization took %0.3f seconds)" % initTime)
+  
   # TODO: come up with a nice, clean method for other threads to immediately
   # terminate the draw loop and provide a stacktrace
   while True:
@@ -705,6 +709,8 @@
           
           popup.addfstr(4, 2, "<b>r</b>: reload torrc")
           popup.addfstr(4, 41, "<b>x</b>: reset tor (issue sighup)")
+          
+          popup.addfstr(5, 2, "<b>c</b>: displayed configuration (<b>%s</b>)" % confPanel.CONFIG_LABELS[panels["torrc"].configType])
         
         popup.addstr(7, 2, "Press any key...")
         popup.refresh()
@@ -1359,8 +1365,9 @@
         panel.CURSES_LOCK.release()
     elif page == 2 and key == ord('r') or key == ord('R'):
       # reloads torrc, providing a notice if successful or not
-      isSuccessful = panels["torrc"].reset(False)
-      resetMsg = "torrc reloaded" if isSuccessful else "failed to reload torrc"
+      isSuccessful = panels["torrc"].loadConfig(logErrors = False)
+      confTypeLabel = confPanel.CONFIG_LABELS[panels["torrc"].configType]
+      resetMsg = "%s reloaded" % confTypeLabel if isSuccessful else "failed to reload %s" % confTypeLabel
       if isSuccessful: panels["torrc"].redraw(True)
       
       panels["control"].setMsg(resetMsg, curses.A_STANDOUT)
@@ -1397,6 +1404,26 @@
         setPauseState(panels, isPaused, page)
       finally:
         panel.CURSES_LOCK.release()
+    elif page == 2 and (key == ord('c') or key == ord('C')):
+      # provides menu to pick config being displayed
+      options = [confPanel.CONFIG_LABELS[confType] for confType in range(4)]
+      initialSelection = panels["torrc"].configType
+      
+      # hides top label of the graph panel and pauses panels
+      panels["torrc"].showLabel = False
+      panels["torrc"].redraw(True)
+      setPauseState(panels, isPaused, page, True)
+      
+      selection = showMenu(stdscr, panels["popup"], "Configuration:", options, initialSelection)
+      
+      # reverts changes made for popup
+      panels["torrc"].showLabel = True
+      setPauseState(panels, isPaused, page)
+      
+      # applies new setting
+      if selection != -1: panels["torrc"].setConfigType(selection)
+      
+      selectiveRefresh(panels, page)
     elif page == 0:
       panels["log"].handleKey(key)
     elif page == 1:
@@ -1404,9 +1431,9 @@
     elif page == 2:
       panels["torrc"].handleKey(key)
 
-def startTorMonitor(loggedEvents, isBlindMode):
+def startTorMonitor(startTime, loggedEvents, isBlindMode):
   try:
-    curses.wrapper(drawTorMonitor, loggedEvents, isBlindMode)
+    curses.wrapper(drawTorMonitor, startTime, loggedEvents, isBlindMode)
   except KeyboardInterrupt:
     pass # skip printing stack trace in case of keyboard interrupt
 

Modified: arm/trunk/src/starter.py
===================================================================
--- arm/trunk/src/starter.py	2010-10-23 22:03:33 UTC (rev 23662)
+++ arm/trunk/src/starter.py	2010-10-24 00:10:16 UTC (rev 23663)
@@ -8,6 +8,7 @@
 
 import os
 import sys
+import time
 import getopt
 
 import version
@@ -75,6 +76,7 @@
   return True
 
 if __name__ == '__main__':
+  startTime = time.time()
   param = dict([(key, None) for key in DEFAULTS.keys()])
   configPath = DEFAULT_CONFIG            # path used for customized configuration
   
@@ -175,9 +177,12 @@
   conn = TorCtl.TorCtl.connect(controlAddr, controlPort, authPassword)
   if conn == None: sys.exit(1)
   
+  # initializing the connection may require user input (for the password)
+  # scewing the startup time results so this isn't counted
+  initTime = time.time() - startTime
   controller = util.torTools.getConn()
   controller.init(conn)
   
-  interface.controller.startTorMonitor(expandedEvents, param["startup.blindModeEnabled"])
+  interface.controller.startTorMonitor(time.time() - initTime, expandedEvents, param["startup.blindModeEnabled"])
   conn.close()
 

Modified: arm/trunk/src/util/conf.py
===================================================================
--- arm/trunk/src/util/conf.py	2010-10-23 22:03:33 UTC (rev 23662)
+++ arm/trunk/src/util/conf.py	2010-10-24 00:10:16 UTC (rev 23663)
@@ -55,6 +55,7 @@
     Creates a new configuration instance.
     """
     
+    self.path = None        # location last loaded from
     self.contents = {}      # configuration key/value pairs
     self.contentsLock = threading.RLock()
     self.requestedKeys = set()
@@ -240,6 +241,7 @@
         if key in self.contents: self.contents[key].append(value)
         else: self.contents[key] = [value]
     
+    self.path = path
     self.contentsLock.release()
   
   def save(self, saveBackup=True):