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 +