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

[or-cvs] r23680: {arm} Revisions in preparation for a config editor panel - Using s (in arm/trunk: . src/interface src/util)



Author: atagar
Date: 2010-10-26 16:48:35 +0000 (Tue, 26 Oct 2010)
New Revision: 23680

Added:
   arm/trunk/src/interface/configFilePanel.py
Removed:
   arm/trunk/src/interface/confPanel.py
Modified:
   arm/trunk/armrc.sample
   arm/trunk/src/interface/controller.py
   arm/trunk/src/util/torTools.py
   arm/trunk/src/util/torrc.py
Log:
Revisions in preparation for a config editor panel

- Using singleton pattern for fetching the torrc, allowing the controller to 
  handle loading and validation logging independently of the the torrc display.
- Dropping tor/arm state support from the display for now, greatly simplifying
  the configFile panel. The next work will be to make an independent panel for
  showing/editing tor's current state.



Modified: arm/trunk/armrc.sample
===================================================================
--- arm/trunk/armrc.sample	2010-10-26 16:47:34 UTC (rev 23679)
+++ arm/trunk/armrc.sample	2010-10-26 16:48:35 UTC (rev 23680)
@@ -13,6 +13,9 @@
 # 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
@@ -55,15 +58,12 @@
 # ---------------------------
 # 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 8
 
@@ -169,9 +169,9 @@
 log.logPanel.logFileOpened NOTICE
 log.logPanel.logFileWriteFailed ERR
 log.logPanel.forceDoubleRedraw DEBUG
-log.confPanel.torrcReadFailed WARN
-log.torrcValidation.duplicateEntries NOTICE
-log.torrcValidation.torStateDiffers NOTICE
+log.torrc.readFailed WARN
+log.torrc.validation.duplicateEntries NOTICE
+log.torrc.validation.torStateDiffers NOTICE
 log.connLookupFailed INFO
 log.connLookupFailover NOTICE
 log.connLookupAbandon WARN

Deleted: arm/trunk/src/interface/confPanel.py
===================================================================
--- arm/trunk/src/interface/confPanel.py	2010-10-26 16:47:34 UTC (rev 23679)
+++ arm/trunk/src/interface/confPanel.py	2010-10-26 16:48:35 UTC (rev 23680)
@@ -1,366 +0,0 @@
-"""
-Panel displaying the torrc and validation done against it.
-"""
-
-import math
-import curses
-import threading
-
-from util import conf, log, panel, torrc, torTools, uiTools
-
-DEFAULT_CONFIG = {"features.config.type": 0,
-                  "features.config.validate": True,
-                  "features.config.showScrollbars": True,
-                  "features.config.maxLinesPerEntry": 8,
-                  "log.confPanel.torrcReadFailed": log.WARN,
-                  "log.torrcValidation.duplicateEntries": log.NOTICE,
-                  "log.torrcValidation.torStateDiffers": log.NOTICE,
-                  "torrc.map": {}}
-
-# 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
-  scrollable area.
-  """
-  
-  def __init__(self, stdscr, config=None):
-    panel.Panel.__init__(self, stdscr, "conf", 0)
-    
-    self._config = dict(DEFAULT_CONFIG)
-    if config:
-      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
-    
-    # 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.loadConfig(TOR_STATE)
-    self.loadConfig(TORRC)
-  
-  def loadConfig(self, configType = None, logErrors = True):
-    """
-    Reloads configuration or state contents and resets scroll height. Returns
-    True if successful, else false.
-    
-    Arguments:
-      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
-    
-    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
-      
-      if configType == TORRC and self._config["features.config.validate"]:
-        # TODO: add armrc validation
-        corrections = torrc.validate(confContents)
-        
-        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 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 = None
-        if confOption in self._config["torrc.map"]:
-          confMappings = conn.getOptionMap(self._config["torrc.map"][confOption], {})
-          if confOption in confMappings: confValue = confMappings[confOption]
-          fetchConfOption = self._config["torrc.map"][confOption]
-        else:
-          confValue = ", ".join(conn.getOption(confOption, [], True))
-        
-        # 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()
-    
-    self.configs[configType] = (confContents, corrections, confLocation)
-    
-    # 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.configs[self.configType] != None:
-      pageHeight = self.getPreferredSize()[0] - 1
-      newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self._lastContentHeight)
-      
-      if self.scroll != newScroll:
-        self.scroll = newScroll
-        self.redraw(True)
-    elif key == ord('n') or key == ord('N'):
-      self.showLineNum = not self.showLineNum
-      self._lastContentHeightArgs = None
-      self.redraw(True)
-    elif key == ord('s') or key == ord('S'):
-      self.stripComments = not self.stripComments
-      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()
-    
-    # If true, we assume that the cached value in self._lastContentHeight is
-    # still accurate, and stop drawing when there's nothing more to display.
-    # Otherwise the self._lastContentHeight is suspect, and we'll process all
-    # the content to check if it's right (and redraw again with the corrected
-    # height if not).
-    trustLastContentHeight = self._lastContentHeightArgs == (width, height)
-    
-    # restricts scroll location to valid bounds
-    self.scroll = max(0, min(self.scroll, self._lastContentHeight - height + 1))
-    
-    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(renderedContents)
-    
-    # offset to make room for the line numbers
-    lineNumOffset = 0
-    if self.showLineNum:
-      if len(renderedContents) == 0: lineNumOffset = 2
-      else: lineNumOffset = int(math.log10(len(renderedContents))) + 2
-    
-    # draws left-hand scroll bar if content's longer than the height
-    scrollOffset = 0
-    if self._config["features.config.showScrollbars"] and self._lastContentHeight > height - 1:
-      scrollOffset = 3
-      self.addScrollBar(self.scroll, self.scroll + height - 1, self._lastContentHeight, 1)
-    
-    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, 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")],
-                  "argument": ["", curses.A_BOLD | uiTools.getColor("cyan")],
-                  "correction": ["", curses.A_BOLD | uiTools.getColor("cyan")],
-                  "comment": ["", uiTools.getColor("white")]}
-      
-      # parses the comment
-      commentIndex = lineText.find("#")
-      if commentIndex != -1:
-        lineComp["comment"][0] = lineText[commentIndex:]
-        lineText = lineText[:commentIndex]
-      
-      # splits the option and argument, preserving any whitespace around them
-      strippedLine = lineText.strip()
-      optionIndex = strippedLine.find(" ")
-      if optionIndex == -1:
-        lineComp["option"][0] = lineText # no argument provided
-      else:
-        optionText = strippedLine[:optionIndex]
-        optionEnd = lineText.find(optionText) + len(optionText)
-        lineComp["option"][0] = lineText[:optionEnd]
-        lineComp["argument"][0] = lineText[optionEnd:]
-      
-      # gets the correction
-      if lineNumber in corrections:
-        lineIssue, lineIssueMsg = corrections[lineNumber]
-        
-        if lineIssue == torrc.VAL_DUPLICATE:
-          lineComp["option"][1] = curses.A_BOLD | uiTools.getColor("blue")
-          lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("blue")
-        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 displayLine < height and displayLine >= 1:
-        lineNumStr = ("%%%ii" % (lineNumOffset - 1)) % (lineNumber + 1)
-        self.addstr(displayLine, scrollOffset, lineNumStr, curses.A_BOLD | uiTools.getColor("yellow"))
-      
-      # draws the rest of the components with line wrap
-      cursorLoc, lineOffset = lineNumOffset + scrollOffset, 0
-      maxLinesPerEntry = self._config["features.config.maxLinesPerEntry"]
-      displayQueue = [lineComp[entry] for entry in ("option", "argument", "correction", "comment")]
-      
-      while displayQueue:
-        msg, format = displayQueue.pop(0)
-        
-        maxMsgSize, includeBreak = width - cursorLoc, False
-        if len(msg) >= maxMsgSize:
-          # message is too long - break it up
-          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))
-        
-        drawLine = displayLine + lineOffset
-        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)
-        includeBreak |= not displayQueue and cursorLoc != lineNumOffset + scrollOffset
-        
-        if includeBreak:
-          lineOffset += 1
-          cursorLoc = lineNumOffset + scrollOffset
-      
-      displayLine += max(lineOffset, 1)
-      
-      if trustLastContentHeight and displayLine >= height: break
-    
-    if not trustLastContentHeight:
-      self._lastContentHeightArgs = (width, height)
-      newContentHeight = displayLine + self.scroll - 1
-      
-      if self._lastContentHeight != newContentHeight:
-        self._lastContentHeight = newContentHeight
-        self.redraw(True)
-    
-    self.valsLock.release()
-  
-  def redraw(self, forceRedraw=False, block=False):
-    panel.Panel.redraw(self, forceRedraw, block)
-

Copied: arm/trunk/src/interface/configFilePanel.py (from rev 23670, arm/trunk/src/interface/confPanel.py)
===================================================================
--- arm/trunk/src/interface/configFilePanel.py	                        (rev 0)
+++ arm/trunk/src/interface/configFilePanel.py	2010-10-26 16:48:35 UTC (rev 23680)
@@ -0,0 +1,212 @@
+"""
+Panel displaying the torrc or armrc with the validation done against it.
+"""
+
+import math
+import curses
+import threading
+
+from util import conf, panel, torrc, uiTools
+
+DEFAULT_CONFIG = {"features.config.showScrollbars": True,
+                  "features.config.maxLinesPerEntry": 8}
+
+TORRC, ARMRC = range(1, 3) # configuration file types that can  be displayed
+
+class ConfigFilePanel(panel.Panel):
+  """
+  Renders the current torrc or armrc with syntax highlighting in a scrollable
+  area.
+  """
+  
+  def __init__(self, stdscr, configType, config=None):
+    panel.Panel.__init__(self, stdscr, "conf", 0)
+    
+    self._config = dict(DEFAULT_CONFIG)
+    if config:
+      config.update(self._config, {"features.config.maxLinesPerEntry": 1})
+    
+    self.valsLock = threading.RLock()
+    self.configType = configType
+    self.scroll = 0
+    self.showLabel = True       # shows top label (hides otherwise)
+    self.showLineNum = True     # shows left aligned line numbers
+    self.stripComments = False  # drops comments and extra whitespace
+    
+    # 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
+  
+  def handleKey(self, key):
+    self.valsLock.acquire()
+    if uiTools.isScrollKey(key):
+      pageHeight = self.getPreferredSize()[0] - 1
+      newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self._lastContentHeight)
+      
+      if self.scroll != newScroll:
+        self.scroll = newScroll
+        self.redraw(True)
+    elif key == ord('n') or key == ord('N'):
+      self.showLineNum = not self.showLineNum
+      self._lastContentHeightArgs = None
+      self.redraw(True)
+    elif key == ord('s') or key == ord('S'):
+      self.stripComments = not self.stripComments
+      self._lastContentHeightArgs = None
+      self.redraw(True)
+    
+    self.valsLock.release()
+  
+  def draw(self, subwindow, width, height):
+    self.valsLock.acquire()
+    
+    # If true, we assume that the cached value in self._lastContentHeight is
+    # still accurate, and stop drawing when there's nothing more to display.
+    # Otherwise the self._lastContentHeight is suspect, and we'll process all
+    # the content to check if it's right (and redraw again with the corrected
+    # height if not).
+    trustLastContentHeight = self._lastContentHeightArgs == (width, height)
+    
+    # restricts scroll location to valid bounds
+    self.scroll = max(0, min(self.scroll, self._lastContentHeight - height + 1))
+    
+    renderedContents, corrections, confLocation = None, {}, None
+    if self.configType == TORRC:
+      loadedTorrc = torrc.getTorrc()
+      loadedTorrc.getLock().acquire()
+      confLocation = loadedTorrc.getConfigLocation()
+      
+      if not loadedTorrc.isLoaded():
+        renderedContents = ["### Unable to load the torrc ###"]
+      else:
+        renderedContents = loadedTorrc.getDisplayContents(self.stripComments)
+        corrections = loadedTorrc.getCorrections()
+      
+      loadedTorrc.getLock().release()
+    else:
+      # TODO: The armrc use case is incomplete. There should be equivilant
+      # reloading and validation capabilities to the torrc.
+      loadedArmrc = conf.getConfig("arm")
+      confLocation = loadedArmrc.path
+      renderedContents = list(loadedArmrc.rawContents)
+    
+    # offset to make room for the line numbers
+    lineNumOffset = 0
+    if self.showLineNum:
+      if len(renderedContents) == 0: lineNumOffset = 2
+      else: lineNumOffset = int(math.log10(len(renderedContents))) + 2
+    
+    # draws left-hand scroll bar if content's longer than the height
+    scrollOffset = 0
+    if self._config["features.config.showScrollbars"] and self._lastContentHeight > height - 1:
+      scrollOffset = 3
+      self.addScrollBar(self.scroll, self.scroll + height - 1, self._lastContentHeight, 1)
+    
+    displayLine = -self.scroll + 1 # line we're drawing on
+    
+    # draws the top label
+    if self.showLabel:
+      sourceLabel = "Tor" if self.configType == TORRC else "Arm"
+      locationLabel = " (%s)" % confLocation if confLocation else ""
+      self.addstr(0, 0, "%s Config%s:" % (sourceLabel, 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
+      if self.stripComments and not lineText: continue
+      
+      # splits the line into its component (msg, format) tuples
+      lineComp = {"option": ["", curses.A_BOLD | uiTools.getColor("green")],
+                  "argument": ["", curses.A_BOLD | uiTools.getColor("cyan")],
+                  "correction": ["", curses.A_BOLD | uiTools.getColor("cyan")],
+                  "comment": ["", uiTools.getColor("white")]}
+      
+      # parses the comment
+      commentIndex = lineText.find("#")
+      if commentIndex != -1:
+        lineComp["comment"][0] = lineText[commentIndex:]
+        lineText = lineText[:commentIndex]
+      
+      # splits the option and argument, preserving any whitespace around them
+      strippedLine = lineText.strip()
+      optionIndex = strippedLine.find(" ")
+      if optionIndex == -1:
+        lineComp["option"][0] = lineText # no argument provided
+      else:
+        optionText = strippedLine[:optionIndex]
+        optionEnd = lineText.find(optionText) + len(optionText)
+        lineComp["option"][0] = lineText[:optionEnd]
+        lineComp["argument"][0] = lineText[optionEnd:]
+      
+      # gets the correction
+      if lineNumber in corrections:
+        lineIssue, lineIssueMsg = corrections[lineNumber]
+        
+        if lineIssue == torrc.VAL_DUPLICATE:
+          lineComp["option"][1] = curses.A_BOLD | uiTools.getColor("blue")
+          lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("blue")
+        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 displayLine < height and displayLine >= 1:
+        lineNumStr = ("%%%ii" % (lineNumOffset - 1)) % (lineNumber + 1)
+        self.addstr(displayLine, scrollOffset, lineNumStr, curses.A_BOLD | uiTools.getColor("yellow"))
+      
+      # draws the rest of the components with line wrap
+      cursorLoc, lineOffset = lineNumOffset + scrollOffset, 0
+      maxLinesPerEntry = self._config["features.config.maxLinesPerEntry"]
+      displayQueue = [lineComp[entry] for entry in ("option", "argument", "correction", "comment")]
+      
+      while displayQueue:
+        msg, format = displayQueue.pop(0)
+        
+        maxMsgSize, includeBreak = width - cursorLoc, False
+        if len(msg) >= maxMsgSize:
+          # message is too long - break it up
+          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))
+        
+        drawLine = displayLine + lineOffset
+        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)
+        includeBreak |= not displayQueue and cursorLoc != lineNumOffset + scrollOffset
+        
+        if includeBreak:
+          lineOffset += 1
+          cursorLoc = lineNumOffset + scrollOffset
+      
+      displayLine += max(lineOffset, 1)
+      
+      if trustLastContentHeight and displayLine >= height: break
+    
+    if not trustLastContentHeight:
+      self._lastContentHeightArgs = (width, height)
+      newContentHeight = displayLine + self.scroll - 1
+      
+      if self._lastContentHeight != newContentHeight:
+        self._lastContentHeight = newContentHeight
+        self.redraw(True)
+    
+    self.valsLock.release()
+  
+  def redraw(self, forceRedraw=False, block=False):
+    panel.Panel.redraw(self, forceRedraw, block)
+

Modified: arm/trunk/src/interface/controller.py
===================================================================
--- arm/trunk/src/interface/controller.py	2010-10-26 16:47:34 UTC (rev 23679)
+++ arm/trunk/src/interface/controller.py	2010-10-26 16:48:35 UTC (rev 23680)
@@ -18,11 +18,11 @@
 import graphing.graphPanel
 import logPanel
 import connPanel
-import confPanel
+import configFilePanel
 import descriptorPopup
 import fileDescriptorPopup
 
-from util import conf, log, connections, hostnames, panel, sysTools, torTools, uiTools
+from util import conf, log, connections, hostnames, panel, sysTools, torrc, torTools, uiTools
 import graphing.bandwidthStats
 import graphing.connStats
 import graphing.psStats
@@ -42,13 +42,16 @@
   ["torrc"]]
 PAUSEABLE = ["header", "graph", "log", "conn"]
 
-CONFIG = {"features.graph.type": 1,
+CONFIG = {"log.torrc.readFailed": log.WARN,
+          "features.graph.type": 1,
           "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}
+          "log.configEntryUndefined": log.NOTICE,
+          "log.torrc.validation.duplicateEntries": log.NOTICE,
+          "log.torrc.validation.torStateDiffers": log.NOTICE}
 
 class ControlPanel(panel.Panel):
   """ Draws single line label for interface controls. """
@@ -356,6 +359,46 @@
   #except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
   #  confLocation = ""
   
+  # loads the torrc and provides warnings in case of validation errors
+  loadedTorrc = torrc.getTorrc()
+  loadedTorrc.getLock().acquire()
+  
+  try:
+    loadedTorrc.load()
+  except IOError, exc:
+    excMsg = str(exc)
+    if excMsg.startswith("[Errno "): excMsg = excMsg[10:]
+    msg = "Unable to load torrc (%s)" % excMsg
+    log.log(CONFIG["log.torrc.readFailed"], msg)
+  
+  if loadedTorrc.isLoaded():
+    corrections = loadedTorrc.getCorrections()
+    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 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(CONFIG["log.torrc.validation.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(CONFIG["log.torrc.validation.torStateDiffers"], msg)
+  
+  loadedTorrc.getLock().release()
+  
   # minor refinements for connection resolver
   if not isBlindMode:
     resolver = connections.getResolver("tor")
@@ -379,7 +422,7 @@
   
   panels["conn"] = connPanel.ConnPanel(stdscr, conn, isBlindMode)
   panels["control"] = ControlPanel(stdscr, isBlindMode)
-  panels["torrc"] = confPanel.ConfPanel(stdscr, config)
+  panels["torrc"] = configFilePanel.ConfigFilePanel(stdscr, configFilePanel.TORRC, config)
   
   # provides error if pid coulnd't be determined (hopefully shouldn't happen...)
   if not torPid: log.log(log.WARN, "Unable to resolve tor pid, abandoning connection listing")
@@ -710,7 +753,7 @@
           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.addfstr(5, 2, "<b>c</b>: displayed configuration (<b>%s</b>)" % configFilePanel.CONFIG_LABELS[panels["torrc"].configType])
         
         popup.addstr(7, 2, "Press any key...")
         popup.refresh()
@@ -1365,11 +1408,24 @@
         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"].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)
+      loadedTorrc = torrc.getTorrc()
+      loadedTorrc.getLock().acquire()
       
+      try:
+        loadedTorrc.load()
+        isSuccessful = True
+      except IOError:
+        isSuccessful = False
+      
+      loadedTorrc.getLock().release()
+      
+      #isSuccessful = panels["torrc"].loadConfig(logErrors = False)
+      #confTypeLabel = confPanel.CONFIG_LABELS[panels["torrc"].configType]
+      resetMsg = "torrc reloaded" if isSuccessful else "failed to reload torrc"
+      if isSuccessful:
+        panels["torrc"]._lastContentHeightArgs = None
+        panels["torrc"].redraw(True)
+      
       panels["control"].setMsg(resetMsg, curses.A_STANDOUT)
       panels["control"].redraw(True)
       time.sleep(1)
@@ -1404,9 +1460,11 @@
         setPauseState(panels, isPaused, page)
       finally:
         panel.CURSES_LOCK.release()
-    elif page == 2 and (key == ord('c') or key == ord('C')):
+    elif page == 2 and (key == ord('c') or key == ord('C')) and False:
+      # TODO: disabled for now (pending the ConfigStatePanel implementation)
       # provides menu to pick config being displayed
-      options = [confPanel.CONFIG_LABELS[confType] for confType in range(4)]
+      #options = [confPanel.CONFIG_LABELS[confType] for confType in range(4)]
+      options = []
       initialSelection = panels["torrc"].configType
       
       # hides top label of the graph panel and pauses panels

Modified: arm/trunk/src/util/torTools.py
===================================================================
--- arm/trunk/src/util/torTools.py	2010-10-26 16:47:34 UTC (rev 23679)
+++ arm/trunk/src/util/torTools.py	2010-10-26 16:48:35 UTC (rev 23680)
@@ -152,7 +152,7 @@
 
 def getConn():
   """
-  Singleton constructor for a Controller. Be aware that this start
+  Singleton constructor for a Controller. Be aware that this starts as being
   uninitialized, needing a TorCtl instance before it's fully functional.
   """
   

Modified: arm/trunk/src/util/torrc.py
===================================================================
--- arm/trunk/src/util/torrc.py	2010-10-26 16:47:34 UTC (rev 23679)
+++ arm/trunk/src/util/torrc.py	2010-10-26 16:48:35 UTC (rev 23680)
@@ -2,9 +2,13 @@
 Helper functions for working with tor's configuration file.
 """
 
+import curses
+import threading
+
 from util import sysTools, torTools, uiTools
 
-CONFIG = {"torrc.map": {},
+CONFIG = {"features.torrc.validate": True,
+          "torrc.map": {},
           "torrc.multiline": [],
           "torrc.alias": {},
           "torrc.label.size.b": [],
@@ -28,8 +32,7 @@
 # VAL_MISMATCH  - the value doesn't match tor's current state
 VAL_DUPLICATE, VAL_MISMATCH = range(1, 3)
 
-# cached results for the stripComments function
-STRIP_COMMENTS_ARG, STRIP_COMMENTS_RESULT = None, None
+TORRC = None # singleton torrc instance
 
 def loadConfig(config):
   CONFIG["torrc.map"] = config.get("torrc.map", {})
@@ -42,6 +45,16 @@
       configValues = config.get(configKey, "").split(",")
       if configValues: CONFIG[configKey] = [val.strip() for val in configValues]
 
+def getTorrc():
+  """
+  Singleton constructor for a Controller. Be aware that this starts as being
+  unloaded, needing the torrc contents to be loaded before being functional.
+  """
+  
+  global TORRC
+  if TORRC == None: TORRC = Torrc()
+  return TORRC
+
 def getConfigLocation():
   """
   Provides the location of the torrc, raising an IOError with the reason if the
@@ -77,33 +90,8 @@
   
   return torTools.getPathPrefix() + configLocation
 
-def stripComments(contents):
+def validate(contents = None):
   """
-  Provides the torrc contents back with comments and extra whitespace stripped.
-  
-  Arguments:
-    contents - torrc contents
-  """
-  
-  global STRIP_COMMENTS_ARG, STRIP_COMMENTS_RESULT
-  
-  if contents == STRIP_COMMENTS_ARG:
-    return list(STRIP_COMMENTS_RESULT)
-  
-  strippedContents = []
-  for line in contents:
-    # strips off comment
-    if line and "#" in line:
-      line = line[:line.find("#")]
-    
-    strippedContents.append(line.strip())
-  
-  STRIP_COMMENTS_ARG = list(contents)
-  STRIP_COMMENTS_RESULT = list(strippedContents)
-  return strippedContents
-
-def validate(contents):
-  """
   Performs validation on the given torrc contents, providing back a mapping of
   line numbers to tuples of the (issue, msg) found on them.
   
@@ -112,7 +100,7 @@
   """
   
   conn = torTools.getConn()
-  contents = stripComments(contents)
+  contents = _stripComments(contents)
   issuesFound, seenOptions = {}, []
   for lineNumber in range(len(contents) - 1, -1, -1):
     lineText = contents[lineNumber]
@@ -215,3 +203,146 @@
   
   return None, UNRECOGNIZED
 
+def _stripComments(contents):
+  """
+  Removes comments and extra whitespace from the given torrc contents.
+  
+  Arguments:
+    contents - torrc contents
+  """
+  
+  strippedContents = []
+  for line in contents:
+    if line and "#" in line: line = line[:line.find("#")]
+    strippedContents.append(line.strip())
+  return strippedContents
+
+class Torrc():
+  """
+  Wrapper for the torrc. All getters provide None if the contents are unloaded.
+  """
+  
+  def __init__(self):
+    self.contents = None
+    self.configLocation = None
+    self.valsLock = threading.RLock()
+    
+    # cached results for the current contents
+    self.displayableContents = None
+    self.strippedContents = None
+    self.corrections = None
+  
+  def load(self):
+    """
+    Loads or reloads the torrc contents, raising an IOError if there's a
+    problem.
+    """
+    
+    self.valsLock.acquire()
+    
+    # clears contents and caches
+    self.contents, self.configLocation = None, None
+    self.displayableContents = None
+    self.strippedContents = None
+    self.corrections = None
+    
+    try:
+      self.configLocation = getConfigLocation()
+      configFile = open(self.configLocation, "r")
+      self.contents = configFile.readlines()
+      configFile.close()
+    except IOError, exc:
+      self.valsLock.release()
+      raise exc
+    
+    self.valsLock.release()
+  
+  def isLoaded(self):
+    """
+    Provides true if there's loaded contents, false otherwise.
+    """
+    
+    return self.contents != None
+  
+  def getConfigLocation(self):
+    """
+    Provides the location of the loaded configuration contents. This may be
+    available, even if the torrc failed to be loaded.
+    """
+    
+    return self.configLocation
+  
+  def getContents(self):
+    """
+    Provides the contents of the configuration file.
+    """
+    
+    self.valsLock.acquire()
+    returnVal = list(self.contents) if self.contents else None
+    self.valsLock.relese()
+    return returnVal
+  
+  def getDisplayContents(self, strip = False):
+    """
+    Provides the contents of the configuration file, formatted in a rendering
+    frindly fashion:
+    - Tabs print as three spaces. Keeping them as tabs is problematic for
+      layouts since it's counted as a single character, but occupies several
+      cells.
+    - Strips control and unprintable characters.
+    
+    Arguments:
+      strip - removes comments and extra whitespace if true
+    """
+    
+    self.valsLock.acquire()
+    
+    if not self.isLoaded(): returnVal = None
+    else:
+      if self.displayableContents == None:
+        # restricts contents to displayable characters
+        self.displayableContents = []
+        
+        for lineNum in range(len(self.contents)):
+          lineText = self.contents[lineNum]
+          lineText = lineText.replace("\t", "   ")
+          lineText = "".join([char for char in lineText if curses.ascii.isprint(char)])
+          self.displayableContents.append(lineText)
+      
+      if strip:
+        if self.strippedContents == None:
+          self.strippedContents = _stripComments(self.displayableContents)
+        
+        returnVal = list(self.strippedContents)
+      else: returnVal = list(self.displayableContents)
+    
+    self.valsLock.release()
+    return returnVal
+  
+  def getCorrections(self):
+    """
+    Performs validation on the loaded contents and provides back the
+    corrections. If validation is disabled then this won't provide any
+    results.
+    """
+    
+    self.valsLock.acquire()
+    
+    if not self.isLoaded(): returnVal = None
+    elif not CONFIG["features.torrc.validate"]: returnVal = {}
+    else:
+      if self.corrections == None:
+        self.corrections = validate(self.contents)
+      
+      returnVal = dict(self.corrections)
+    
+    self.valsLock.release()
+    return returnVal
+  
+  def getLock(self):
+    """
+    Provides the lock governing concurrent access to the contents.
+    """
+    
+    return self.valsLock
+