commit d0ec1e72ef3d7745b68fad7a652f2773ea7bc2b0 Author: Damian Johnson atagar@torproject.org Date: Tue Jan 1 13:58:06 2013 -0800
Using stem's conf util
Ok, let's get this out of the way first. This is a major overhaul. Arm's conf util had its insidious clutch over just about every file in our codebase. Stem's conf util introduces a new pattern for managing configurations that turns out to be vastly cleaner.
I'll be amazed if this doesn't introduce some sort of regression but so far so good. In the long run this'll be a big step toward having a simpler, more manageable codebase. --- armrc.sample | 42 ++++-- src/cli/configPanel.py | 75 ++++---- src/cli/connections/connEntry.py | 21 +-- src/cli/connections/connPanel.py | 90 ++++++----- src/cli/controller.py | 70 ++++---- src/cli/graphing/bandwidthStats.py | 53 +++--- src/cli/graphing/graphPanel.py | 31 ++-- src/cli/headerPanel.py | 34 ++-- src/cli/logPanel.py | 137 +++++++-------- src/cli/torrcPanel.py | 30 ++-- src/starter.py | 73 ++++----- src/util/conf.py | 337 ------------------------------------ src/util/connections.py | 74 ++++---- src/util/hostnames.py | 30 ++-- src/util/log.py | 23 ++- src/util/panel.py | 9 +- src/util/procTools.py | 11 +- src/util/sysTools.py | 25 ++-- src/util/torConfig.py | 66 ++++---- src/util/torTools.py | 18 +- src/util/uiTools.py | 33 ++-- 21 files changed, 488 insertions(+), 794 deletions(-)
diff --git a/armrc.sample b/armrc.sample index 9072713..2c3a8ca 100644 --- a/armrc.sample +++ b/armrc.sample @@ -109,9 +109,17 @@ features.log.maxRefreshRate 300 # --------------------------- # order # three comma separated configuration attributes, options including: -# 0 -> Category, 1 -> Option Name, 2 -> Value, 3 -> Arg Type, -# 4 -> Arg Usage, 5 -> Summary, 6 -> Description, 7 -> Man Entry, -# 8 -> Is Default +# +# * CATEGORY +# * OPTION +# * VALUE +# * TYPE +# * ARG_USAGE +# * SUMMARY +# * DESCRIPTION +# * MAN_ENTRY +# * IS_DEFAULT +# # selectionDetails.height # rows of data for the panel showing details on the current selection, this # is disabled entirely if zero @@ -131,7 +139,7 @@ features.log.maxRefreshRate 300 # file.maxLinesPerEntry # max number of lines to display for a single entry in the torrc
-features.config.order 7, 1, 8 +features.config.order MAN_ENTRY, OPTION, IS_DEFAULT features.config.selectionDetails.height 6 features.config.prepopulateEditValues true features.config.state.colWidth.option 25 @@ -200,13 +208,25 @@ features.graph.bw.accounting.isTimeLong false # --------------------------------- # listingType # the primary category of information shown by default, options including: -# 0 -> IP Address / Port 1 -> Hostname -# 2 -> Fingerprint 3 -> Nickname +# +# * IP_ADDRESS +# * HOSTNAME +# * FINGERPRINT +# * NICKNAME +# # order # three comma separated configuration attributes, options including: -# 0 -> Category, 1 -> Uptime, 2 -> Listing, 3 -> IP Address, -# 4 -> Port, 5 -> Hostname, 6 -> Fingerprint, 7 -> Nickname, -# 8 -> Country +# +# * CATEGORY +# * UPTIME +# * LISTING +# * IP_ADDRESS +# * PORT +# * HOSTNAME +# * FINGERPRINT +# * NICKNAME +# * COUNTRY +# # refreshRate # rate at which the connection panel contents is redrawn (if higher than the # connection resolution rate then reducing this won't casue new data to @@ -227,8 +247,8 @@ features.graph.bw.accounting.isTimeLong false # showColumn.* # toggles the visability of the connection table columns
-features.connection.listingType 0 -features.connection.order 0, 2, 1 +features.connection.listingType IP_ADDRESS +features.connection.order CATEGORY, LISTING, UPTIME features.connection.refreshRate 5 features.connection.resolveApps true features.connection.markInitialConnections true diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py index 50b66b3..38df235 100644 --- a/src/cli/configPanel.py +++ b/src/cli/configPanel.py @@ -9,16 +9,9 @@ import threading import cli.controller import popups
-from util import conf, panel, sysTools, torConfig, torTools, uiTools +from util import panel, sysTools, torConfig, torTools, uiTools
-from stem.util import enum - -DEFAULT_CONFIG = {"features.config.selectionDetails.height": 6, - "features.config.prepopulateEditValues": True, - "features.config.state.showPrivateOptions": False, - "features.config.state.showVirtualOptions": False, - "features.config.state.colWidth.option": 25, - "features.config.state.colWidth.value": 15} +from stem.util import conf, enum
# TODO: The arm use cases are incomplete since they currently can't be # modified, have their descriptions fetched, or even get a complete listing @@ -38,7 +31,7 @@ CATEGORY_COLOR = {torConfig.Category.GENERAL: "green", # attributes of a ConfigEntry Field = enum.Enum("CATEGORY", "OPTION", "VALUE", "TYPE", "ARG_USAGE", "SUMMARY", "DESCRIPTION", "MAN_ENTRY", "IS_DEFAULT") -DEFAULT_SORT_ORDER = (Field.MAN_ENTRY, Field.OPTION, Field.IS_DEFAULT) + FIELD_ATTR = {Field.CATEGORY: ("Category", "red"), Field.OPTION: ("Option Name", "blue"), Field.VALUE: ("Value", "cyan"), @@ -49,6 +42,26 @@ FIELD_ATTR = {Field.CATEGORY: ("Category", "red"), Field.MAN_ENTRY: ("Man Page Entry", "blue"), Field.IS_DEFAULT: ("Is Default", "magenta")}
+def conf_handler(key, value): + if key == "features.config.selectionDetails.height": + return max(0, value) + elif key == "features.config.state.colWidth.option": + return max(5, value) + elif key == "features.config.state.colWidth.value": + return max(5, value) + elif key == "features.config.order": + return conf.parse_enum_csv(key, value[0], Field, 3) + +CONFIG = conf.config_dict("arm", { + "features.config.order": [Field.MAN_ENTRY, Field.OPTION, Field.IS_DEFAULT], + "features.config.selectionDetails.height": 6, + "features.config.prepopulateEditValues": True, + "features.config.state.showPrivateOptions": False, + "features.config.state.showVirtualOptions": False, + "features.config.state.colWidth.option": 25, + "features.config.state.colWidth.value": 15, +}, conf_handler) + def getFieldFromLabel(fieldLabel): """ Converts field labels back to their enumeration, raising a ValueError if it @@ -173,23 +186,9 @@ class ConfigPanel(panel.Panel): be selected and edited. """
- def __init__(self, stdscr, configType, config=None): + def __init__(self, stdscr, configType): panel.Panel.__init__(self, stdscr, "configuration", 0)
- self.sortOrdering = DEFAULT_SORT_ORDER - self._config = dict(DEFAULT_CONFIG) - if config: - config.update(self._config, { - "features.config.selectionDetails.height": 0, - "features.config.state.colWidth.option": 5, - "features.config.state.colWidth.value": 5}) - - sortFields = list(Field) - customOrdering = config.getIntCSV("features.config.order", None, 3, 0, len(sortFields)) - - if customOrdering: - self.sortOrdering = [sortFields[i] for i in customOrdering] - self.configType = configType self.confContents = [] self.confImportantContents = [] @@ -237,16 +236,16 @@ class ConfigPanel(panel.Panel): confOption, confType = lineComp[0], lineComp[1]
# skips private and virtual entries if not configured to show them - if not self._config["features.config.state.showPrivateOptions"] and confOption.startswith("__"): + if not CONFIG["features.config.state.showPrivateOptions"] and confOption.startswith("__"): continue - elif not self._config["features.config.state.showVirtualOptions"] and confType == "Virtual": + elif not CONFIG["features.config.state.showVirtualOptions"] and confType == "Virtual": continue
self.confContents.append(ConfigEntry(confOption, confType, not confOption in customOptions)) elif self.configType == State.ARM: # loaded via the conf utility - armConf = conf.getConfig("arm") - for key in armConf.getKeys(): + armConf = conf.get_config("arm") + for key in armConf.keys(): pass # TODO: implement
# mirror listing with only the important configuration options @@ -290,9 +289,9 @@ class ConfigPanel(panel.Panel): """
self.valsLock.acquire() - if ordering: self.sortOrdering = ordering - self.confContents.sort(key=lambda i: (i.getAll(self.sortOrdering))) - self.confImportantContents.sort(key=lambda i: (i.getAll(self.sortOrdering))) + if ordering: CONFIG["features.config.order"] = ordering + self.confContents.sort(key=lambda i: (i.getAll(CONFIG["features.config.order"]))) + self.confImportantContents.sort(key=lambda i: (i.getAll(CONFIG["features.config.order"]))) self.valsLock.release()
def showSortDialog(self): @@ -303,7 +302,7 @@ class ConfigPanel(panel.Panel): # set ordering for config options titleLabel = "Config Option Ordering:" options = [FIELD_ATTR[field][0] for field in Field] - oldSelection = [FIELD_ATTR[field][0] for field in self.sortOrdering] + oldSelection = [FIELD_ATTR[field][0] for field in CONFIG["features.config.order"]] optionColors = dict([FIELD_ATTR[field] for field in Field]) results = popups.showSortDialog(titleLabel, options, oldSelection, optionColors)
@@ -317,7 +316,7 @@ class ConfigPanel(panel.Panel): isKeystrokeConsumed = True if uiTools.isScrollKey(key): pageHeight = self.getPreferredSize()[0] - 1 - detailPanelHeight = self._config["features.config.selectionDetails.height"] + detailPanelHeight = CONFIG["features.config.selectionDetails.height"] if detailPanelHeight > 0 and detailPanelHeight + 2 <= pageHeight: pageHeight -= (detailPanelHeight + 1)
@@ -336,7 +335,7 @@ class ConfigPanel(panel.Panel): else: initialValue = selection.get(Field.VALUE)
promptMsg = "%s Value (esc to cancel): " % configOption - isPrepopulated = self._config["features.config.prepopulateEditValues"] + isPrepopulated = CONFIG["features.config.prepopulateEditValues"] newValue = popups.inputPrompt(promptMsg, initialValue if isPrepopulated else "")
if newValue != None and newValue != initialValue: @@ -494,7 +493,7 @@ class ConfigPanel(panel.Panel): self.valsLock.acquire()
# panel with details for the current selection - detailPanelHeight = self._config["features.config.selectionDetails.height"] + detailPanelHeight = CONFIG["features.config.selectionDetails.height"] isScrollbarVisible = False if detailPanelHeight == 0 or detailPanelHeight + 2 >= height: # no detail panel @@ -526,8 +525,8 @@ class ConfigPanel(panel.Panel): scrollOffset = 3 self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self._getConfigOptions()), 1 + detailPanelHeight)
- optionWidth = self._config["features.config.state.colWidth.option"] - valueWidth = self._config["features.config.state.colWidth.value"] + optionWidth = CONFIG["features.config.state.colWidth.option"] + valueWidth = CONFIG["features.config.state.colWidth.value"] descriptionWidth = max(0, width - scrollOffset - optionWidth - valueWidth - 2)
# if the description column is overly long then use its space for the diff --git a/src/cli/connections/connEntry.py b/src/cli/connections/connEntry.py index 2f57b51..73c3ece 100644 --- a/src/cli/connections/connEntry.py +++ b/src/cli/connections/connEntry.py @@ -9,7 +9,7 @@ import curses from util import connections, torTools, uiTools from cli.connections import entries
-from stem.util import enum +from stem.util import conf, enum
# Connection Categories: # Inbound Relay connection, coming to us. @@ -35,16 +35,15 @@ LABEL_MIN_PADDING = 2 # min space between listing label and following data # sort value for scrubbed ip addresses SCRUBBED_IP_VAL = 255 ** 4
-CONFIG = {"features.connection.markInitialConnections": True, - "features.connection.showIps": True, - "features.connection.showExitPort": True, - "features.connection.showColumn.fingerprint": True, - "features.connection.showColumn.nickname": True, - "features.connection.showColumn.destination": True, - "features.connection.showColumn.expandedIp": True} - -def loadConfig(config): - config.update(CONFIG) +CONFIG = conf.config_dict("arm", { + "features.connection.markInitialConnections": True, + "features.connection.showIps": True, + "features.connection.showExitPort": True, + "features.connection.showColumn.fingerprint": True, + "features.connection.showColumn.nickname": True, + "features.connection.showColumn.destination": True, + "features.connection.showColumn.expandedIp": True, +})
class Endpoint: """ diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py index 50ba2e8..9f3a5ed 100644 --- a/src/cli/connections/connPanel.py +++ b/src/cli/connections/connPanel.py @@ -12,12 +12,7 @@ import cli.popups from cli.connections import countPopup, descriptorPopup, entries, connEntry, circEntry from util import connections, panel, torTools, uiTools
-from stem.util import enum - -DEFAULT_CONFIG = {"features.connection.resolveApps": True, - "features.connection.listingType": 0, - "features.connection.refreshRate": 5, - "features.connection.showIps": True} +from stem.util import conf, enum
# height of the detail panel content, not counting top and bottom border DETAILS_HEIGHT = 7 @@ -25,7 +20,24 @@ DETAILS_HEIGHT = 7 # listing types Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
-DEFAULT_SORT_ORDER = (entries.SortAttr.CATEGORY, entries.SortAttr.LISTING, entries.SortAttr.UPTIME) +def conf_handler(key, value): + if key == "features.connection.listingType": + return conf.parse_enum(key, value, Listing) + elif key == "features.connection.refreshRate": + return max(1, value) + elif key == "features.connection.order": + return conf.parse_enum_csv(key, value[0], entries.SortAttr, 3) + +CONFIG = conf.config_dict("arm", { + "features.connection.resolveApps": True, + "features.connection.listingType": Listing.IP_ADDRESS, + "features.connection.order": [ + entries.SortAttr.CATEGORY, + entries.SortAttr.LISTING, + entries.SortAttr.UPTIME], + "features.connection.refreshRate": 5, + "features.connection.showIps": True, +}, conf_handler)
class ConnectionPanel(panel.Panel, threading.Thread): """ @@ -33,31 +45,21 @@ class ConnectionPanel(panel.Panel, threading.Thread): the current consensus and other data sources. """
- def __init__(self, stdscr, config=None): + def __init__(self, stdscr): panel.Panel.__init__(self, stdscr, "connections", 0) threading.Thread.__init__(self) self.setDaemon(True)
- self._sortOrdering = DEFAULT_SORT_ORDER - self._config = dict(DEFAULT_CONFIG) + # defaults our listing selection to fingerprints if ip address + # displaying is disabled + # + # TODO: This is a little sucky in that it won't work if showIps changes + # while we're running (... but arm doesn't allow for that atm)
- if config: - config.update(self._config, { - "features.connection.listingType": (0, len(list(Listing)) - 1), - "features.connection.refreshRate": 1}) - - # defaults our listing selection to fingerprints if ip address - # displaying is disabled - if not self._config["features.connection.showIps"] and self._config["features.connection.listingType"] == 0: - self._config["features.connection.listingType"] = 2 - - sortFields = list(entries.SortAttr) - customOrdering = config.getIntCSV("features.connection.order", None, 3, 0, len(sortFields)) - - if customOrdering: - self._sortOrdering = [sortFields[i] for i in customOrdering] + if not CONFIG["features.connection.showIps"] and CONFIG["features.connection.listingType"] == 0: + armConf = conf.get_config("arm") + armConf.set("features.connection.listingType", enumeration.keys()[Listing.index_of(Listing.FINGERPRINT)])
- self._listingType = list(Listing)[self._config["features.connection.listingType"]] self._scroller = uiTools.Scroller(True) self._title = "Connections:" # title line of the panel self._entries = [] # last fetched display entries @@ -150,8 +152,14 @@ class ConnectionPanel(panel.Panel, threading.Thread): """
self.valsLock.acquire() - if ordering: self._sortOrdering = ordering - self._entries.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType))) + + if ordering: + armConf = conf.get_config("arm") + + ordering_keys = [entries.SortAttr.keys()[entries.SortAttr.index_of(v)] for v in ordering] + armConf.set("features.connection.order", ", ".join(ordering_keys)) + + self._entries.sort(key=lambda i: (i.getSortValues(CONFIG["features.connection.order"], self.getListingType())))
self._entryLines = [] for entry in self._entries: @@ -163,7 +171,7 @@ class ConnectionPanel(panel.Panel, threading.Thread): Provides the priority content we list connections by. """
- return self._listingType + return CONFIG["features.connection.listingType"]
def setListingType(self, listingType): """ @@ -173,13 +181,15 @@ class ConnectionPanel(panel.Panel, threading.Thread): listingType - Listing instance for the primary information to be shown """
- if self._listingType == listingType: return + if self.getListingType() == listingType: return
self.valsLock.acquire() - self._listingType = listingType + + armConf = conf.get_config("arm") + armConf.set("features.connection.listingType", enumeration.keys()[Listing.index_of(listingType)])
# if we're sorting by the listing then we need to resort - if entries.SortAttr.LISTING in self._sortOrdering: + if entries.SortAttr.LISTING in CONFIG["features.connection.order"]: self.setSortOrder()
self.valsLock.release() @@ -208,7 +218,7 @@ class ConnectionPanel(panel.Panel, threading.Thread): # set ordering for connection options titleLabel = "Connection Ordering:" options = list(entries.SortAttr) - oldSelection = self._sortOrdering + oldSelection = CONFIG["features.connection.order"] optionColors = dict([(attr, entries.SORT_COLORS[attr]) for attr in options]) results = cli.popups.showSortDialog(titleLabel, options, oldSelection, optionColors) if results: self.setSortOrder(results) @@ -251,7 +261,7 @@ class ConnectionPanel(panel.Panel, threading.Thread): # dropping the HOSTNAME listing type until we support displaying that content options.remove(cli.connections.entries.ListingType.HOSTNAME)
- oldSelection = options.index(self._listingType) + oldSelection = options.index(self.getListingType()) selection = cli.popups.showMenu(title, options, oldSelection)
# applies new setting @@ -287,7 +297,7 @@ class ConnectionPanel(panel.Panel, threading.Thread): while not self._halt: currentTime = time.time()
- if self.isPaused() or not self._isTorRunning or currentTime - lastDraw < self._config["features.connection.refreshRate"]: + if self.isPaused() or not self._isTorRunning or currentTime - lastDraw < CONFIG["features.connection.refreshRate"]: self._cond.acquire() if not self._halt: self._cond.wait(0.2) self._cond.release() @@ -298,8 +308,8 @@ class ConnectionPanel(panel.Panel, threading.Thread):
# we may have missed multiple updates due to being paused, showing # another panel, etc so lastDraw might need to jump multiple ticks - drawTicks = (time.time() - lastDraw) / self._config["features.connection.refreshRate"] - lastDraw += self._config["features.connection.refreshRate"] * drawTicks + drawTicks = (time.time() - lastDraw) / CONFIG["features.connection.refreshRate"] + lastDraw += CONFIG["features.connection.refreshRate"] * drawTicks
def getHelp(self): resolverUtil = connections.getResolver("tor").overwriteResolver @@ -319,7 +329,7 @@ class ConnectionPanel(panel.Panel, threading.Thread): if self.isExitsAllowed(): options.append(("e", "exit port usage summary", None))
- options.append(("l", "listed identity", self._listingType.lower())) + options.append(("l", "listed identity", self.getListingType().lower())) options.append(("s", "sort ordering", None)) options.append(("u", "resolving utility", resolverUtil)) return options @@ -387,7 +397,7 @@ class ConnectionPanel(panel.Panel, threading.Thread): self.addch(drawLine, scrollOffset + i, prefix[i])
xOffset = scrollOffset + len(prefix) - drawEntry = entryLine.getListingEntry(width - scrollOffset - len(prefix), currentTime, self._listingType) + drawEntry = entryLine.getListingEntry(width - scrollOffset - len(prefix), currentTime, self.getListingType())
for msg, attr in drawEntry: attr |= extraFormat @@ -530,7 +540,7 @@ class ConnectionPanel(panel.Panel, threading.Thread): until the next update if true """
- if self.appResolveSinceUpdate or not self._config["features.connection.resolveApps"]: return + if self.appResolveSinceUpdate or not CONFIG["features.connection.resolveApps"]: return unresolvedLines = [l for l in self._entryLines if isinstance(l, connEntry.ConnectionLine) and l.isUnresolvedApp()]
# get the ports used for unresolved applications diff --git a/src/cli/controller.py b/src/cli/controller.py index af3fdac..a41cc83 100644 --- a/src/cli/controller.py +++ b/src/cli/controller.py @@ -22,29 +22,37 @@ import cli.connections.connPanel
from stem.control import Controller
-from util import connections, conf, hostnames, log, panel, sysTools, torConfig, torTools +from util import connections, hostnames, log, panel, sysTools, torConfig, torTools
-from stem.util import enum +from stem.util import conf, enum
ARM_CONTROLLER = None
-CONFIG = {"startup.events": "N3", - "startup.dataDirectory": "~/.arm", - "startup.blindModeEnabled": False, - "features.panels.show.graph": True, - "features.panels.show.log": True, - "features.panels.show.connection": True, - "features.panels.show.config": True, - "features.panels.show.torrc": True, - "features.redrawRate": 5, - "features.refreshRate": 5, - "features.confirmQuit": True, - "features.graph.type": 1, - "features.graph.bw.prepopulate": True, - "log.startTime": log.INFO, - "log.torEventTypeUnrecognized": log.INFO, - "log.configEntryUndefined": log.NOTICE, - "log.unknownTorPid": log.WARN} +def conf_handler(key, value): + if key == "features.redrawRate": + return max(1, value) + elif key == "features.refreshRate": + return max(0, value) + +CONFIG = conf.config_dict("arm", { + "startup.events": "N3", + "startup.dataDirectory": "~/.arm", + "startup.blindModeEnabled": False, + "features.panels.show.graph": True, + "features.panels.show.log": True, + "features.panels.show.connection": True, + "features.panels.show.config": True, + "features.panels.show.torrc": True, + "features.redrawRate": 5, + "features.refreshRate": 5, + "features.confirmQuit": True, + "features.graph.type": 1, + "features.graph.bw.prepopulate": True, + "log.startTime": log.INFO, + "log.torEventTypeUnrecognized": log.INFO, + "log.configEntryUndefined": log.NOTICE, + "log.unknownTorPid": log.WARN, +}, conf_handler)
GraphStat = enum.Enum("BANDWIDTH", "CONNECTIONS", "SYSTEM_RESOURCES")
@@ -67,10 +75,9 @@ def initController(stdscr, startTime): """
global ARM_CONTROLLER - config = conf.getConfig("arm")
# initializes the panels - stickyPanels = [cli.headerPanel.HeaderPanel(stdscr, startTime, config), + stickyPanels = [cli.headerPanel.HeaderPanel(stdscr, startTime), LabelPanel(stdscr)] pagePanels, firstPagePanels = [], []
@@ -80,21 +87,21 @@ def initController(stdscr, startTime):
if CONFIG["features.panels.show.log"]: expandedEvents = cli.logPanel.expandEvents(CONFIG["startup.events"]) - firstPagePanels.append(cli.logPanel.LogPanel(stdscr, expandedEvents, config)) + firstPagePanels.append(cli.logPanel.LogPanel(stdscr, expandedEvents))
if firstPagePanels: pagePanels.append(firstPagePanels)
# second page: connections if not CONFIG["startup.blindModeEnabled"] and CONFIG["features.panels.show.connection"]: - pagePanels.append([cli.connections.connPanel.ConnectionPanel(stdscr, config)]) + pagePanels.append([cli.connections.connPanel.ConnectionPanel(stdscr)])
# third page: config if CONFIG["features.panels.show.config"]: - pagePanels.append([cli.configPanel.ConfigPanel(stdscr, cli.configPanel.State.TOR, config)]) + pagePanels.append([cli.configPanel.ConfigPanel(stdscr, cli.configPanel.State.TOR)])
# fourth page: torrc if CONFIG["features.panels.show.torrc"]: - pagePanels.append([cli.torrcPanel.TorrcPanel(stdscr, cli.torrcPanel.Config.TORRC, config)]) + pagePanels.append([cli.torrcPanel.TorrcPanel(stdscr, cli.torrcPanel.Config.TORRC)])
# initializes the controller ARM_CONTROLLER = Controller(stdscr, stickyPanels, pagePanels) @@ -104,7 +111,7 @@ def initController(stdscr, startTime):
if graphPanel: # statistical monitors for graph - bwStats = cli.graphing.bandwidthStats.BandwidthStats(config) + bwStats = cli.graphing.bandwidthStats.BandwidthStats() graphPanel.addStats(GraphStat.BANDWIDTH, bwStats) graphPanel.addStats(GraphStat.SYSTEM_RESOURCES, cli.graphing.resourceStats.ResourceStats()) if not CONFIG["startup.blindModeEnabled"]: @@ -509,15 +516,6 @@ def startTorMonitor(startTime): startTime - unix time for when arm was started """
- # initializes interface configs - config = conf.getConfig("arm") - config.update(CONFIG, { - "features.redrawRate": 1, - "features.refreshRate": 0}) - - cli.graphing.graphPanel.loadConfig(config) - cli.connections.connEntry.loadConfig(config) - # attempts to fetch the tor pid, warning if unsuccessful (this is needed for # checking its resource usage, among other things) conn = torTools.getConn() @@ -592,7 +590,7 @@ def drawTorMonitor(stdscr, startTime): control = getController()
# provides notice about any unused config keys - for key in conf.getConfig("arm").getUnusedKeys(): + for key in conf.get_config("arm").unused_keys(): log.log(CONFIG["log.configEntryUndefined"], "Unused configuration entry: %s" % key)
# tells daemon panels to start diff --git a/src/cli/graphing/bandwidthStats.py b/src/cli/graphing/bandwidthStats.py index 2b60c7e..9aecf96 100644 --- a/src/cli/graphing/bandwidthStats.py +++ b/src/cli/graphing/bandwidthStats.py @@ -11,6 +11,21 @@ import cli.controller from cli.graphing import graphPanel from util import log, sysTools, torTools, uiTools
+from stem.util import conf + +def conf_handler(key, value): + if key == "features.graph.bw.accounting.rate": + return max(1, value) + +CONFIG = conf.config_dict("arm", { + "features.graph.bw.transferInBytes": False, + "features.graph.bw.accounting.show": True, + "features.graph.bw.accounting.rate": 10, + "features.graph.bw.accounting.isTimeLong": False, + "log.graph.bw.prepopulateSuccess": log.NOTICE, + "log.graph.bw.prepopulateFailure": log.NOTICE, +}, conf_handler) + DL_COLOR, UL_COLOR = "green", "cyan"
# width at which panel abandons placing optional stats (avg and total) with @@ -23,26 +38,14 @@ ACCOUNTING_ARGS = ("status", "resetTime", "read", "written", "readLimit", "writt PREPOPULATE_SUCCESS_MSG = "Read the last day of bandwidth history from the state file" PREPOPULATE_FAILURE_MSG = "Unable to prepopulate bandwidth information (%s)"
-DEFAULT_CONFIG = {"features.graph.bw.transferInBytes": False, - "features.graph.bw.accounting.show": True, - "features.graph.bw.accounting.rate": 10, - "features.graph.bw.accounting.isTimeLong": False, - "log.graph.bw.prepopulateSuccess": log.NOTICE, - "log.graph.bw.prepopulateFailure": log.NOTICE} - class BandwidthStats(graphPanel.GraphStats): """ Uses tor BW events to generate bandwidth usage graph. """
- def __init__(self, config=None, isPauseBuffer=False): + def __init__(self, isPauseBuffer=False): graphPanel.GraphStats.__init__(self)
- self.inputConfig = config - self._config = dict(DEFAULT_CONFIG) - if config: - config.update(self._config, {"features.graph.bw.accounting.rate": 1}) - # stats prepopulated from tor's state file self.prepopulatePrimaryTotal = 0 self.prepopulateSecondaryTotal = 0 @@ -78,7 +81,7 @@ class BandwidthStats(graphPanel.GraphStats): self.initialSecondaryTotal = int(writeTotal) / 1024 # Bytes -> KB
def clone(self, newCopy=None): - if not newCopy: newCopy = BandwidthStats(self.inputConfig, True) + if not newCopy: newCopy = BandwidthStats(True) newCopy.accountingLastUpdated = self.accountingLastUpdated newCopy.accountingInfo = self.accountingInfo
@@ -93,7 +96,7 @@ class BandwidthStats(graphPanel.GraphStats): self._titleStats = [] # force reset of title self.new_desc_event(None) # updates title params
- if eventType in (torTools.State.INIT, torTools.State.RESET) and self._config["features.graph.bw.accounting.show"]: + if eventType in (torTools.State.INIT, torTools.State.RESET) and CONFIG["features.graph.bw.accounting.show"]: isAccountingEnabled = conn.getInfo('accounting/enabled', None) == '1'
if isAccountingEnabled != self.isAccounting: @@ -136,21 +139,21 @@ class BandwidthStats(graphPanel.GraphStats): # results associated with this tor instance if not uptime or not "-" in uptime: msg = PREPOPULATE_FAILURE_MSG % "insufficient uptime" - log.log(self._config["log.graph.bw.prepopulateFailure"], msg) + log.log(CONFIG["log.graph.bw.prepopulateFailure"], msg) return False
# get the user's data directory (usually '~/.tor') dataDir = conn.getOption("DataDirectory", None) if not dataDir: msg = PREPOPULATE_FAILURE_MSG % "data directory not found" - log.log(self._config["log.graph.bw.prepopulateFailure"], msg) + log.log(CONFIG["log.graph.bw.prepopulateFailure"], msg) return False
# attempt to open the state file try: stateFile = open("%s%s/state" % (conn.getPathPrefix(), dataDir), "r") except IOError: msg = PREPOPULATE_FAILURE_MSG % "unable to read the state file" - log.log(self._config["log.graph.bw.prepopulateFailure"], msg) + log.log(CONFIG["log.graph.bw.prepopulateFailure"], msg) return False
# get the BWHistory entries (ordered oldest to newest) and number of @@ -189,7 +192,7 @@ class BandwidthStats(graphPanel.GraphStats):
if not bwReadEntries or not bwWriteEntries or not lastReadTime or not lastWriteTime: msg = PREPOPULATE_FAILURE_MSG % "bandwidth stats missing from state file" - log.log(self._config["log.graph.bw.prepopulateFailure"], msg) + log.log(CONFIG["log.graph.bw.prepopulateFailure"], msg) return False
# fills missing entries with the last value @@ -228,13 +231,13 @@ class BandwidthStats(graphPanel.GraphStats): msg = PREPOPULATE_SUCCESS_MSG missingSec = time.time() - min(lastReadTime, lastWriteTime) if missingSec: msg += " (%s is missing)" % uiTools.getTimeLabel(missingSec, 0, True) - log.log(self._config["log.graph.bw.prepopulateSuccess"], msg) + log.log(CONFIG["log.graph.bw.prepopulateSuccess"], msg)
return True
def bandwidth_event(self, event): if self.isAccounting and self.isNextTickRedraw(): - if time.time() - self.accountingLastUpdated >= self._config["features.graph.bw.accounting.rate"]: + if time.time() - self.accountingLastUpdated >= CONFIG["features.graph.bw.accounting.rate"]: self._updateAccountingInfo()
# scales units from B to KB for graphing @@ -309,7 +312,7 @@ class BandwidthStats(graphPanel.GraphStats): stats[1] = "- %s" % self._getAvgLabel(isPrimary) stats[2] = ", %s" % self._getTotalLabel(isPrimary)
- stats[0] = "%-14s" % ("%s/sec" % uiTools.getSizeLabel((self.lastPrimary if isPrimary else self.lastSecondary) * 1024, 1, False, self._config["features.graph.bw.transferInBytes"])) + stats[0] = "%-14s" % ("%s/sec" % uiTools.getSizeLabel((self.lastPrimary if isPrimary else self.lastSecondary) * 1024, 1, False, CONFIG["features.graph.bw.transferInBytes"]))
# drops label's components if there's not enough space labeling = graphType + " (" + "".join(stats).strip() + "):" @@ -342,7 +345,7 @@ class BandwidthStats(graphPanel.GraphStats): bwBurst = conn.getMyBandwidthBurst() bwObserved = conn.getMyBandwidthObserved() bwMeasured = conn.getMyBandwidthMeasured() - labelInBytes = self._config["features.graph.bw.transferInBytes"] + labelInBytes = CONFIG["features.graph.bw.transferInBytes"]
if bwRate and bwBurst: bwRateLabel = uiTools.getSizeLabel(bwRate, 1, False, labelInBytes) @@ -369,7 +372,7 @@ class BandwidthStats(graphPanel.GraphStats): def _getAvgLabel(self, isPrimary): total = self.primaryTotal if isPrimary else self.secondaryTotal total += self.prepopulatePrimaryTotal if isPrimary else self.prepopulateSecondaryTotal - return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick + self.prepopulateTicks)) * 1024, 1, False, self._config["features.graph.bw.transferInBytes"]) + return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick + self.prepopulateTicks)) * 1024, 1, False, CONFIG["features.graph.bw.transferInBytes"])
def _getTotalLabel(self, isPrimary): total = self.primaryTotal if isPrimary else self.secondaryTotal @@ -396,7 +399,7 @@ class BandwidthStats(graphPanel.GraphStats): else: tz_offset = time.timezone
sec = time.mktime(time.strptime(endInterval, "%Y-%m-%d %H:%M:%S")) - time.time() - tz_offset - if self._config["features.graph.bw.accounting.isTimeLong"]: + if CONFIG["features.graph.bw.accounting.isTimeLong"]: queried["resetTime"] = ", ".join(uiTools.getTimeLabels(sec, True)) else: days = sec / 86400 diff --git a/src/cli/graphing/graphPanel.py b/src/cli/graphing/graphPanel.py index 16efe8f..b989ef5 100644 --- a/src/cli/graphing/graphPanel.py +++ b/src/cli/graphing/graphPanel.py @@ -26,7 +26,7 @@ import stem.control
from util import panel, torTools, uiTools
-from stem.util import enum +from stem.util import conf, enum
# time intervals at which graphs can be updated UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5), ("30 seconds", 30), @@ -45,19 +45,24 @@ Bounds = enum.Enum("GLOBAL_MAX", "LOCAL_MAX", "TIGHT")
WIDE_LABELING_GRAPH_COL = 50 # minimum graph columns to use wide spacing for x-axis labels
-# used for setting defaults when initializing GraphStats and GraphPanel instances -CONFIG = {"features.graph.height": 7, - "features.graph.interval": 0, - "features.graph.bound": 1, - "features.graph.maxWidth": 150, - "features.graph.showIntermediateBounds": True} +def conf_handler(key, value): + if key == "features.graph.height": + return max(MIN_GRAPH_HEIGHT, value) + elif key == "features.graph.maxWidth": + return max(1, value) + elif key == "features.graph.interval": + return max(0, min(len(UPDATE_INTERVALS) - 1, value)) + elif key == "features.graph.bound": + return max(0, min(2, value))
-def loadConfig(config): - config.update(CONFIG, { - "features.graph.height": MIN_GRAPH_HEIGHT, - "features.graph.maxWidth": 1, - "features.graph.interval": (0, len(UPDATE_INTERVALS) - 1), - "features.graph.bound": (0, 2)}) +# used for setting defaults when initializing GraphStats and GraphPanel instances +CONFIG = conf.config_dict("arm", { + "features.graph.height": 7, + "features.graph.interval": 0, + "features.graph.bound": 1, + "features.graph.maxWidth": 150, + "features.graph.showIntermediateBounds": True, +}, conf_handler)
class GraphStats: """ diff --git a/src/cli/headerPanel.py b/src/cli/headerPanel.py index 4ab3daf..07937e0 100644 --- a/src/cli/headerPanel.py +++ b/src/cli/headerPanel.py @@ -23,6 +23,7 @@ import stem import stem.connection
from stem.control import Controller +from stem.util import conf
import starter import cli.popups @@ -42,12 +43,14 @@ FLAG_COLORS = {"Authority": "white", "BadExit": "red", "BadDirectory": "red VERSION_STATUS_COLORS = {"new": "blue", "new in series": "blue", "obsolete": "red", "recommended": "green", "old": "red", "unrecommended": "red", "unknown": "cyan"}
-DEFAULT_CONFIG = {"startup.interface.ipAddress": "127.0.0.1", - "startup.interface.port": 9051, - "startup.interface.socket": "/var/run/tor/control", - "features.showFdUsage": False, - "log.fdUsageSixtyPercent": log.NOTICE, - "log.fdUsageNinetyPercent": log.WARN} +CONFIG = conf.config_dict("arm", { + "startup.interface.ipAddress": "127.0.0.1", + "startup.interface.port": 9051, + "startup.interface.socket": "/var/run/tor/control", + "features.showFdUsage": False, + "log.fdUsageSixtyPercent": log.NOTICE, + "log.fdUsageNinetyPercent": log.WARN, +})
class HeaderPanel(panel.Panel, threading.Thread): """ @@ -63,14 +66,11 @@ class HeaderPanel(panel.Panel, threading.Thread): * volatile parameter that'll be reset on each update """
- def __init__(self, stdscr, startTime, config = None): + def __init__(self, stdscr, startTime): panel.Panel.__init__(self, stdscr, "header", 0) threading.Thread.__init__(self) self.setDaemon(True)
- self._config = dict(DEFAULT_CONFIG) - if config: config.update(self._config) - self._isTorConnected = torTools.getConn().isAlive() self._lastUpdate = -1 # time the content was last revised self._halt = False # terminates thread if true @@ -140,10 +140,10 @@ class HeaderPanel(panel.Panel, threading.Thread): controller = None allowPortConnection, allowSocketConnection, _ = starter.allowConnectionTypes()
- if os.path.exists(self._config["startup.interface.socket"]) and allowSocketConnection: + if os.path.exists(CONFIG["startup.interface.socket"]) and allowSocketConnection: try: # TODO: um... what about passwords? - controller = Controller.from_socket_file(self._config["startup.interface.socket"]) + controller = Controller.from_socket_file(CONFIG["startup.interface.socket"]) controller.authenticate() except (IOError, stem.SocketError), exc: controller = None @@ -151,7 +151,7 @@ class HeaderPanel(panel.Panel, threading.Thread): if not allowPortConnection: cli.popups.showMsg("Unable to reconnect (%s)" % exc, 3) elif not allowPortConnection: - cli.popups.showMsg("Unable to reconnect (socket '%s' doesn't exist)" % self._config["startup.interface.socket"], 3) + cli.popups.showMsg("Unable to reconnect (socket '%s' doesn't exist)" % CONFIG["startup.interface.socket"], 3)
if not controller and allowPortConnection: # TODO: This has diverged from starter.py's connection, for instance it @@ -161,7 +161,7 @@ class HeaderPanel(panel.Panel, threading.Thread): # manageable.
try: - ctlAddr, ctlPort = self._config["startup.interface.ipAddress"], self._config["startup.interface.port"] + ctlAddr, ctlPort = CONFIG["startup.interface.ipAddress"], CONFIG["startup.interface.port"] controller = Controller.from_port(ctlAddr, ctlPort)
try: @@ -297,7 +297,7 @@ class HeaderPanel(panel.Panel, threading.Thread):
fdPercent = 100 * self.vals["tor/fdUsed"] / self.vals["tor/fdLimit"]
- if fdPercent >= 60 or self._config["features.showFdUsage"]: + if fdPercent >= 60 or CONFIG["features.showFdUsage"]: fdPercentLabel, fdPercentFormat = "%i%%" % fdPercent, curses.A_NORMAL if fdPercent >= 95: fdPercentFormat = curses.A_BOLD | uiTools.getColor("red") @@ -559,10 +559,10 @@ class HeaderPanel(panel.Panel, threading.Thread): if fdPercent >= 90 and not self._isFdNinetyPercentWarned: self._isFdSixtyPercentWarned, self._isFdNinetyPercentWarned = True, True msg += " If you run out Tor will be unable to continue functioning." - log.log(self._config["log.fdUsageNinetyPercent"], msg) + log.log(CONFIG["log.fdUsageNinetyPercent"], msg) elif fdPercent >= 60 and not self._isFdSixtyPercentWarned: self._isFdSixtyPercentWarned = True - log.log(self._config["log.fdUsageSixtyPercent"], msg) + log.log(CONFIG["log.fdUsageSixtyPercent"], msg)
# ps or proc derived resource usage stats if self.vals["tor/pid"]: diff --git a/src/cli/logPanel.py b/src/cli/logPanel.py index d03973d..e196544 100644 --- a/src/cli/logPanel.py +++ b/src/cli/logPanel.py @@ -11,12 +11,13 @@ import curses import logging import threading
-import stem.util.log +import stem.util.log # move to a 'from' import when we've dropped the other log util from stem.response import events +from stem.util import conf
import popups from version import VERSION -from util import conf, log, panel, sysTools, torTools, uiTools +from util import log, panel, sysTools, torTools, uiTools
TOR_EVENT_TYPES = { "d": "DEBUG", "a": "ADDRMAP", "k": "DESCCHANGED", "s": "STREAM", @@ -42,22 +43,35 @@ DAYBREAK_EVENT = "DAYBREAK" # special event for marking when the date changes TIMEZONE_OFFSET = time.altzone if time.localtime()[8] else time.timezone
ENTRY_INDENT = 2 # spaces an entry's message is indented after the first line -DEFAULT_CONFIG = {"features.logFile": "", - "features.log.showDateDividers": True, - "features.log.showDuplicateEntries": False, - "features.log.entryDuration": 7, - "features.log.maxLinesPerEntry": 6, - "features.log.prepopulate": True, - "features.log.prepopulateReadLimit": 5000, - "features.log.maxRefreshRate": 300, - "features.log.regex": [], - "cache.logPanel.size": 1000, - "log.logPanel.prepopulateSuccess": log.INFO, - "log.logPanel.prepopulateFailed": log.WARN, - "log.logPanel.logFileOpened": log.NOTICE, - "log.logPanel.logFileWriteFailed": log.ERR, - "log.logPanel.forceDoubleRedraw": log.DEBUG, - "log.configEntryTypeError": log.NOTICE} + +def conf_handler(key, value): + if key == "features.log.maxLinesPerEntry": + return max(1, value) + elif key == "features.log.prepopulateReadLimit": + return max(0, value) + elif key == "features.log.maxRefreshRate": + return max(10, value) + elif key == "cache.logPanel.size": + return max(1000, value) + +CONFIG = conf.config_dict("arm", { + "features.logFile": "", + "features.log.showDateDividers": True, + "features.log.showDuplicateEntries": False, + "features.log.entryDuration": 7, + "features.log.maxLinesPerEntry": 6, + "features.log.prepopulate": True, + "features.log.prepopulateReadLimit": 5000, + "features.log.maxRefreshRate": 300, + "features.log.regex": [], + "cache.logPanel.size": 1000, + "log.logPanel.prepopulateSuccess": log.INFO, + "log.logPanel.prepopulateFailed": log.WARN, + "log.logPanel.logFileOpened": log.NOTICE, + "log.logPanel.logFileWriteFailed": log.ERR, + "log.logPanel.forceDoubleRedraw": log.DEBUG, + "log.configEntryTypeError": log.NOTICE, +}, conf_handler)
DUPLICATE_MSG = " [%i duplicate%s hidden]"
@@ -173,16 +187,16 @@ def loadLogMessages(): """
global COMMON_LOG_MESSAGES - armConf = conf.getConfig("arm") + armConf = conf.get_config("arm")
COMMON_LOG_MESSAGES = {} - for confKey in armConf.getKeys(): + for confKey in armConf.keys(): if confKey.startswith("msg."): eventType = confKey[4:].upper() messages = armConf.get(confKey, []) COMMON_LOG_MESSAGES[eventType] = messages
-def getLogFileEntries(runlevels, readLimit = None, addLimit = None, config = None): +def getLogFileEntries(runlevels, readLimit = None, addLimit = None): """ Parses tor's log file for past events matching the given runlevels, providing a list of log entries (ordered newest to oldest). Limiting the number of read @@ -193,15 +207,11 @@ def getLogFileEntries(runlevels, readLimit = None, addLimit = None, config = Non runlevels - event types (DEBUG - ERR) to be returned readLimit - max lines of the log file that'll be read (unlimited if None) addLimit - maximum entries to provide back (unlimited if None) - config - configuration parameters related to this panel, uses defaults - if left as None """
startTime = time.time() if not runlevels: return []
- if not config: config = DEFAULT_CONFIG - # checks tor's configuration for the log file's location (if any exists) loggingTypes, loggingLocation = None, None for loggingEntry in torTools.getConn().getOption("Log", [], True): @@ -252,7 +262,7 @@ def getLogFileEntries(runlevels, readLimit = None, addLimit = None, config = Non logFile.close() except IOError: msg = "Unable to read tor's log file: %s" % loggingLocation - log.log(config["log.logPanel.prepopulateFailed"], msg) + log.log(CONFIG["log.logPanel.prepopulateFailed"], msg)
if not lines: return []
@@ -308,7 +318,7 @@ def getLogFileEntries(runlevels, readLimit = None, addLimit = None, config = Non
if addLimit: loggedEvents = loggedEvents[:addLimit] msg = "Read %i entries from tor's log file: %s (read limit: %i, runtime: %0.3f)" % (len(loggedEvents), loggingLocation, readLimit, time.time() - startTime) - log.log(config["log.logPanel.prepopulateSuccess"], msg) + log.log(CONFIG["log.logPanel.prepopulateSuccess"], msg) return loggedEvents
def getDaybreaks(events, ignoreTimeForCache = False): @@ -474,7 +484,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): from tor's log file if it exists. """
- def __init__(self, stdscr, loggedEvents, config=None): + def __init__(self, stdscr, loggedEvents): panel.Panel.__init__(self, stdscr, "log", 0) logging.Handler.__init__(self, level = stem.util.log.logging_level(stem.util.log.DEBUG))
@@ -494,28 +504,16 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): # regex filters the user has defined self.filterOptions = []
- self._config = dict(DEFAULT_CONFIG) - - if config: - config.update(self._config, { - "features.log.maxLinesPerEntry": 1, - "features.log.prepopulateReadLimit": 0, - "features.log.maxRefreshRate": 10, - "cache.logPanel.size": 1000}) + for filter in CONFIG["features.log.regex"]: + # checks if we can't have more filters + if len(self.filterOptions) >= MAX_REGEX_FILTERS: break
- for filter in self._config["features.log.regex"]: - # checks if we can't have more filters - if len(self.filterOptions) >= MAX_REGEX_FILTERS: break - - try: - re.compile(filter) - self.filterOptions.append(filter) - except re.error, exc: - msg = "Invalid regular expression pattern (%s): %s" % (exc, filter) - log.log(self._config["log.configEntryTypeError"], msg) - - # collapses duplicate log entries if false, showing only the most recent - self.showDuplicates = self._config["features.log.showDuplicateEntries"] + try: + re.compile(filter) + self.filterOptions.append(filter) + except re.error, exc: + msg = "Invalid regular expression pattern (%s): %s" % (exc, filter) + log.log(CONFIG["log.configEntryTypeError"], msg)
self.loggedEvents = [] # needs to be set before we receive any events
@@ -563,8 +561,8 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): conn.addStatusListener(self._resetListener)
# opens log file if we'll be saving entries - if self._config["features.logFile"]: - logPath = self._config["features.logFile"] + if CONFIG["features.logFile"]: + logPath = CONFIG["features.logFile"]
try: # make dir if the path doesn't already exist @@ -572,9 +570,9 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): if not os.path.exists(baseDir): os.makedirs(baseDir)
self.logFile = open(logPath, "a") - log.log(self._config["log.logPanel.logFileOpened"], "arm %s opening log file (%s)" % (VERSION, logPath)) + log.log(CONFIG["log.logPanel.logFileOpened"], "arm %s opening log file (%s)" % (VERSION, logPath)) except (IOError, OSError), exc: - log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc)) + log.log(CONFIG["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc)) self.logFile = None
stem_logger = stem.util.log.get_logger() @@ -596,11 +594,11 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
# fetches past tor events from log file, if available torEventBacklog = [] - if self._config["features.log.prepopulate"]: + if CONFIG["features.log.prepopulate"]: setRunlevels = list(set.intersection(set(self.loggedEvents), set(list(log.Runlevel)))) - readLimit = self._config["features.log.prepopulateReadLimit"] - addLimit = self._config["cache.logPanel.size"] - torEventBacklog = getLogFileEntries(setRunlevels, readLimit, addLimit, self._config) + readLimit = CONFIG["features.log.prepopulateReadLimit"] + addLimit = CONFIG["cache.logPanel.size"] + torEventBacklog = getLogFileEntries(setRunlevels, readLimit, addLimit)
# gets the set of arm events we're logging setRunlevels = [] @@ -639,7 +637,8 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): deduplicated """
- self.showDuplicates = isVisible + armConf = conf.get_config("arm") + armConf.set("features.log.showDuplicateEntries", str(isVisible))
def registerTorEvent(self, event): """ @@ -687,7 +686,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): self.logFile.write(event.getDisplayMessage(True) + "\n") self.logFile.flush() except IOError, exc: - log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc)) + log.log(CONFIG["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc)) self.logFile = None
self.valsLock.acquire() @@ -883,7 +882,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): self.valsLock.release() elif key in (ord('u'), ord('U')): self.valsLock.acquire() - self.showDuplicates = not self.showDuplicates + self.setDuplicateVisability(not CONFIG["features.log.showDuplicateEntries"]) self.redraw(True) self.valsLock.release() elif key == ord('c') or key == ord('C'): @@ -929,7 +928,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): options.append(("a", "save snapshot of the log", None)) options.append(("e", "change logged events", None)) options.append(("f", "log regex filter", "enabled" if self.regexFilter else "disabled")) - options.append(("u", "duplicate log entries", "visible" if self.showDuplicates else "hidden")) + options.append(("u", "duplicate log entries", "visible" if CONFIG["features.log.showDuplicateEntries"] else "hidden")) options.append(("c", "clear event log", None)) return options
@@ -963,15 +962,15 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): seenFirstDateDivider = False dividerAttr, duplicateAttr = curses.A_BOLD | uiTools.getColor("yellow"), curses.A_BOLD | uiTools.getColor("green")
- isDatesShown = self.regexFilter == None and self._config["features.log.showDateDividers"] + isDatesShown = self.regexFilter == None and CONFIG["features.log.showDateDividers"] eventLog = getDaybreaks(currentLog, self.isPaused()) if isDatesShown else list(currentLog) - if not self.showDuplicates: + if not CONFIG["features.log.showDuplicateEntries"]: deduplicatedLog = getDuplicates(eventLog)
if deduplicatedLog == None: msg = "Deduplication took too long. Its current implementation has difficulty handling large logs so disabling it to keep the interface responsive." log.log(log.WARN, msg) - self.showDuplicates = True + self.setDuplicateVisability(True) deduplicatedLog = [(entry, 0) for entry in eventLog] else: deduplicatedLog = [(entry, 0) for entry in eventLog]
@@ -1024,7 +1023,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): displayQueue.append((duplicateMsg, duplicateAttr, False))
cursorLoc, lineOffset = msgIndent, 0 - maxEntriesPerLine = self._config["features.log.maxLinesPerEntry"] + maxEntriesPerLine = CONFIG["features.log.maxLinesPerEntry"] while displayQueue: msg, format, includeBreak = displayQueue.pop(0) drawLine = lineCount + lineOffset @@ -1085,7 +1084,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): self.lastContentHeight = newContentHeight if forceRedraw: forceRedrawReason = "redrawing the log panel with the corrected content height (%s)" % forceRedrawReason - log.log(self._config["log.logPanel.forceDoubleRedraw"], forceRedrawReason) + log.log(CONFIG["log.logPanel.forceDoubleRedraw"], forceRedrawReason) self.redraw(True)
self.valsLock.release() @@ -1105,7 +1104,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): while not self._halt: currentDay = daysSince() timeSinceReset = time.time() - self._lastUpdate - maxLogUpdateRate = self._config["features.log.maxRefreshRate"] / 1000.0 + maxLogUpdateRate = CONFIG["features.log.maxRefreshRate"] / 1000.0
sleepTime = 0 if (self.msgLog == self._lastLoggedEvents and lastDay == currentDay) or self.isPaused(): @@ -1281,10 +1280,10 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): eventListing - listing of log entries """
- cacheSize = self._config["cache.logPanel.size"] + cacheSize = CONFIG["cache.logPanel.size"] if len(eventListing) > cacheSize: del eventListing[cacheSize:]
- logTTL = self._config["features.log.entryDuration"] + logTTL = CONFIG["features.log.entryDuration"] if logTTL > 0: currentDay = daysSince()
diff --git a/src/cli/torrcPanel.py b/src/cli/torrcPanel.py index 983768f..635fc85 100644 --- a/src/cli/torrcPanel.py +++ b/src/cli/torrcPanel.py @@ -8,12 +8,18 @@ import threading
import popups
-from util import conf, panel, torConfig, torTools, uiTools +from util import panel, torConfig, torTools, uiTools
-from stem.util import enum +from stem.util import conf, enum
-DEFAULT_CONFIG = {"features.config.file.showScrollbars": True, - "features.config.file.maxLinesPerEntry": 8} +def conf_handler(key, value): + if key == "features.config.file.maxLinesPerEntry": + return max(1, value) + +CONFIG = conf.config_dict("arm", { + "features.config.file.showScrollbars": True, + "features.config.file.maxLinesPerEntry": 8, +}, conf_handler)
# TODO: The armrc use case is incomplete. There should be equivilant reloading # and validation capabilities to the torrc. @@ -25,13 +31,9 @@ class TorrcPanel(panel.Panel): area. """
- def __init__(self, stdscr, configType, config=None): + def __init__(self, stdscr, configType): panel.Panel.__init__(self, stdscr, "torrc", 0)
- self._config = dict(DEFAULT_CONFIG) - if config: - config.update(self._config, {"features.config.file.maxLinesPerEntry": 1}) - self.valsLock = threading.RLock() self.configType = configType self.scroll = 0 @@ -180,9 +182,9 @@ class TorrcPanel(panel.Panel):
loadedTorrc.getLock().release() else: - loadedArmrc = conf.getConfig("arm") - confLocation = loadedArmrc.path - renderedContents = list(loadedArmrc.rawContents) + loadedArmrc = conf.get_config("arm") + confLocation = loadedArmrc._path + renderedContents = list(loadedArmrc._raw_contents)
# offset to make room for the line numbers lineNumOffset = 0 @@ -192,7 +194,7 @@ class TorrcPanel(panel.Panel):
# draws left-hand scroll bar if content's longer than the height scrollOffset = 0 - if self._config["features.config.file.showScrollbars"] and self._lastContentHeight > height - 1: + if CONFIG["features.config.file.showScrollbars"] and self._lastContentHeight > height - 1: scrollOffset = 3 self.addScrollBar(self.scroll, self.scroll + height - 1, self._lastContentHeight, 1)
@@ -267,7 +269,7 @@ class TorrcPanel(panel.Panel):
# draws the rest of the components with line wrap cursorLoc, lineOffset = lineNumOffset + scrollOffset, 0 - maxLinesPerEntry = self._config["features.config.file.maxLinesPerEntry"] + maxLinesPerEntry = CONFIG["features.config.file.maxLinesPerEntry"] displayQueue = [lineComp[entry] for entry in ("option", "argument", "correction", "comment")]
while displayQueue: diff --git a/src/starter.py b/src/starter.py index ccf7aad..3cf4dff 100644 --- a/src/starter.py +++ b/src/starter.py @@ -17,12 +17,8 @@ import platform import version import cli.controller import cli.logPanel -import util.conf import util.connections -import util.hostnames import util.log -import util.panel -import util.procTools import util.sysTools import util.torConfig import util.torTools @@ -30,28 +26,32 @@ import util.uiTools
from stem.control import Controller import stem.connection +import stem.util.conf
LOG_DUMP_PATH = os.path.expanduser("~/.arm/log") DEFAULT_CONFIG = os.path.expanduser("~/.arm/armrc") -CONFIG = {"startup.controlPassword": None, - "startup.interface.ipAddress": "127.0.0.1", - "startup.interface.port": 9051, - "startup.interface.socket": "/var/run/tor/control", - "startup.blindModeEnabled": False, - "startup.events": "N3", - "startup.dataDirectory": "~/.arm", - "features.allowDetachedStartup": False, - "features.config.descriptions.enabled": True, - "features.config.descriptions.persist": True, - "log.configDescriptions.readManPageSuccess": util.log.INFO, - "log.configDescriptions.readManPageFailed": util.log.NOTICE, - "log.configDescriptions.internalLoadSuccess": util.log.NOTICE, - "log.configDescriptions.internalLoadFailed": util.log.ERR, - "log.configDescriptions.persistance.loadSuccess": util.log.INFO, - "log.configDescriptions.persistance.loadFailed": util.log.INFO, - "log.configDescriptions.persistance.saveSuccess": util.log.INFO, - "log.configDescriptions.persistance.saveFailed": util.log.NOTICE, - "log.savingDebugLog": util.log.NOTICE} + +CONFIG = stem.util.conf.config_dict("arm", { + "startup.controlPassword": None, + "startup.interface.ipAddress": "127.0.0.1", + "startup.interface.port": 9051, + "startup.interface.socket": "/var/run/tor/control", + "startup.blindModeEnabled": False, + "startup.events": "N3", + "startup.dataDirectory": "~/.arm", + "features.allowDetachedStartup": False, + "features.config.descriptions.enabled": True, + "features.config.descriptions.persist": True, + "log.configDescriptions.readManPageSuccess": util.log.INFO, + "log.configDescriptions.readManPageFailed": util.log.NOTICE, + "log.configDescriptions.internalLoadSuccess": util.log.NOTICE, + "log.configDescriptions.internalLoadFailed": util.log.ERR, + "log.configDescriptions.persistance.loadSuccess": util.log.INFO, + "log.configDescriptions.persistance.loadFailed": util.log.INFO, + "log.configDescriptions.persistance.saveSuccess": util.log.INFO, + "log.configDescriptions.persistance.saveFailed": util.log.NOTICE, + "log.savingDebugLog": util.log.NOTICE, +})
OPT = "gi:s:c:dbe:vh" OPT_EXPANDED = ["interface=", "socket=", "config=", "debug", "blind", "event=", "version", "help"] @@ -113,7 +113,7 @@ def allowConnectionTypes(): (allowPortConnection, allowSocketConnection, allowDetachedStart) """
- confKeys = util.conf.getConfig("arm").getKeys() + confKeys = stem.util.conf.get_config("arm").keys()
isPortArgPresent = "startup.interface.ipAddress" in confKeys or "startup.interface.port" in confKeys isSocketArgPresent = "startup.interface.socket" in confKeys @@ -242,12 +242,12 @@ def _dumpConfig(): check that I didn't miss anything. """
- config = util.conf.getConfig("arm") + config = stem.util.conf.get_config("arm") conn = util.torTools.getConn()
# dumps arm's configuration armConfigEntry = "" - armConfigKeys = list(config.getKeys()) + armConfigKeys = list(config.keys()) armConfigKeys.sort()
for configKey in armConfigKeys: @@ -256,7 +256,7 @@ def _dumpConfig(): # arm.
if not configKey.startswith("config.summary.") and not configKey.startswith("torrc.") and not configKey.startswith("msg."): - armConfigEntry += "%s -> %s\n" % (configKey, config.contents[configKey]) + armConfigEntry += "%s -> %s\n" % (configKey, config.get_value(configKey))
if armConfigEntry: armConfigEntry = "Arm Configuration:\n%s" % armConfigEntry else: armConfigEntry = "Arm Configuration: None" @@ -338,7 +338,7 @@ if __name__ == '__main__': except (OSError, IOError), exc: print "Unable to write to debug log file: %s" % util.sysTools.getFileErrorMsg(exc)
- config = util.conf.getConfig("arm") + config = stem.util.conf.get_config("arm")
# attempts to fetch attributes for parsing tor's logs, configuration, etc pathPrefix = os.path.dirname(sys.argv[0]) @@ -371,13 +371,6 @@ if __name__ == '__main__': if util.torTools.isTorRunning(): config.set("features.allowDetachedStartup", "false")
- # revises defaults to match user's configuration - config.update(CONFIG) - - # loads user preferences for utilities - for utilModule in (util.conf, util.connections, util.hostnames, util.log, util.panel, util.procTools, util.sysTools, util.torConfig, util.torTools, util.uiTools): - utilModule.loadConfig(config) - # syncs config and parameters, saving changed config options and overwriting # undefined parameters with defaults for key in param.keys(): @@ -435,17 +428,17 @@ if __name__ == '__main__': # (unfortunately python does allow for direct access to the memory so this # is the best we can do) del authPassword - if "startup.controlPassword" in config.contents: - del config.contents["startup.controlPassword"] + if "startup.controlPassword" in config._contents: + del config._contents["startup.controlPassword"]
pwLineNum = None - for i in range(len(config.rawContents)): - if config.rawContents[i].strip().startswith("startup.controlPassword"): + for i in range(len(config._raw_contents)): + if config._raw_contents[i].strip().startswith("startup.controlPassword"): pwLineNum = i break
if pwLineNum != None: - del config.rawContents[i] + del config._raw_contents[i]
if controller is None and not allowDetachedStart: sys.exit(1)
diff --git a/src/util/conf.py b/src/util/conf.py deleted file mode 100644 index a8ca6de..0000000 --- a/src/util/conf.py +++ /dev/null @@ -1,337 +0,0 @@ -""" -This provides handlers for specially formatted configuration files. Entries are -expected to consist of simple key/value pairs, and anything after "#" is -stripped as a comment. Excess whitespace is trimmed and empty lines are -ignored. For instance: -# This is my sample config - -user.name Galen -user.password yabba1234 # here's an inline comment -user.notes takes a fancy to pepperjack chese -blankEntry.example - -would be loaded as four entries (the last one's value being an empty string). -If a key's defined multiple times then the last instance of it is used. -""" - -import threading - -from util import log - -CONFS = {} # mapping of identifier to singleton instances of configs -CONFIG = {"log.configEntryNotFound": None, - "log.configEntryTypeError": log.NOTICE} - -def loadConfig(config): - config.update(CONFIG) - -def getConfig(handle): - """ - Singleton constructor for configuration file instances. If a configuration - already exists for the handle then it's returned. Otherwise a fresh instance - is constructed. - - Arguments: - handle - unique identifier used to access this config instance - """ - - if not handle in CONFS: CONFS[handle] = Config() - return CONFS[handle] - -class Config(): - """ - Handler for easily working with custom configurations, providing persistence - to and from files. All operations are thread safe. - - Parameters: - path - location from which configurations are saved and loaded - contents - mapping of current key/value pairs - rawContents - last read/written config (initialized to an empty string) - """ - - def __init__(self): - """ - 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() - self.rawContents = [] # raw contents read from configuration file - - def getValue(self, key, default=None, multiple=False): - """ - This provides the currently value associated with a given key. If no such - key exists then this provides the default. - - Arguments: - key - config setting to be fetched - default - value provided if no such key exists - multiple - provides back a list of all values if true, otherwise this - returns the last loaded configuration value - """ - - self.contentsLock.acquire() - - if key in self.contents: - val = self.contents[key] - if not multiple: val = val[-1] - self.requestedKeys.add(key) - else: - msg = "config entry '%s' not found, defaulting to '%s'" % (key, str(default)) - log.log(CONFIG["log.configEntryNotFound"], msg) - val = default - - self.contentsLock.release() - - return val - - def get(self, key, default=None): - """ - Fetches the given configuration, using the key and default value to hint - the type it should be. Recognized types are: - - logging runlevel if key starts with "log." - - boolean if default is a boolean (valid values are 'true' and 'false', - anything else provides the default) - - integer or float if default is a number (provides default if fails to - cast) - - list of all defined values default is a list - - mapping of all defined values (key/value split via "=>") if the default - is a dict - - Arguments: - key - config setting to be fetched - default - value provided if no such key exists - """ - - isMultivalue = isinstance(default, list) or isinstance(default, dict) - val = self.getValue(key, default, isMultivalue) - if val == default: return val - - if key.startswith("log."): - if val.upper() == "NONE": val = None - elif val.upper() in list(log.Runlevel): val = val.upper() - else: - msg = "Config entry '%s' is expected to be a runlevel" % key - if default != None: msg += ", defaulting to '%s'" % default - log.log(CONFIG["log.configEntryTypeError"], msg) - val = default - elif isinstance(default, bool): - if val.lower() == "true": val = True - elif val.lower() == "false": val = False - else: - msg = "Config entry '%s' is expected to be a boolean, defaulting to '%s'" % (key, str(default)) - log.log(CONFIG["log.configEntryTypeError"], msg) - val = default - elif isinstance(default, int): - try: val = int(val) - except ValueError: - msg = "Config entry '%s' is expected to be an integer, defaulting to '%i'" % (key, default) - log.log(CONFIG["log.configEntryTypeError"], msg) - val = default - elif isinstance(default, float): - try: val = float(val) - except ValueError: - msg = "Config entry '%s' is expected to be a float, defaulting to '%f'" % (key, default) - log.log(CONFIG["log.configEntryTypeError"], msg) - val = default - elif isinstance(default, list): - pass # nothing special to do (already a list) - elif isinstance(default, dict): - valMap = {} - for entry in val: - if "=>" in entry: - entryKey, entryVal = entry.split("=>", 1) - valMap[entryKey.strip()] = entryVal.strip() - else: - msg = "Ignoring invalid %s config entry (expected a mapping, but "%s" was missing "=>")" % (key, entry) - log.log(CONFIG["log.configEntryTypeError"], msg) - val = valMap - - return val - - def getStrCSV(self, key, default = None, count = None): - """ - Fetches the given key as a comma separated value. This provides back a list - with the stripped values. - - Arguments: - key - config setting to be fetched - default - value provided if no such key exists or doesn't match the count - count - if set, then a TypeError is logged (and default returned) if - the number of elements doesn't match the count - """ - - confValue = self.getValue(key) - if confValue == None: return default - else: - confComp = [entry.strip() for entry in confValue.split(",")] - - # check if the count doesn't match - if count != None and len(confComp) != count: - msg = "Config entry '%s' is expected to be %i comma separated values" % (key, count) - if default != None and (isinstance(default, list) or isinstance(default, tuple)): - defaultStr = ", ".join([str(i) for i in default]) - msg += ", defaulting to '%s'" % defaultStr - - log.log(CONFIG["log.configEntryTypeError"], msg) - return default - - return confComp - - def getIntCSV(self, key, default = None, count = None, minValue = None, maxValue = None): - """ - Fetches the given comma separated value, logging a TypeError (and returning - the default) if the values arne't ints or aren't constrained to the given - bounds. - - Arguments: - key - config setting to be fetched - default - value provided if no such key exists, doesn't match the count, - values aren't all integers, or doesn't match the bounds - count - checks that the number of values matches this if set - minValue - checks that all values are over this if set - maxValue - checks that all values are less than this if set - """ - - confComp = self.getStrCSV(key, default, count) - if confComp == default: return default - - # validates the input, setting the errorMsg if there's a problem - errorMsg = None - baseErrorMsg = "Config entry '%s' is expected to %%s" % key - if default != None and (isinstance(default, list) or isinstance(default, tuple)): - defaultStr = ", ".join([str(i) for i in default]) - baseErrorMsg += ", defaulting to '%s'" % defaultStr - - for val in confComp: - if not val.isdigit(): - errorMsg = baseErrorMsg % "only have integer values" - break - else: - if minValue != None and int(val) < minValue: - errorMsg = baseErrorMsg % "only have values over %i" % minValue - break - elif maxValue != None and int(val) > maxValue: - errorMsg = baseErrorMsg % "only have values less than %i" % maxValue - break - - if errorMsg: - log.log(CONFIG["log.configEntryTypeError"], errorMsg) - return default - else: return [int(val) for val in confComp] - - def update(self, confMappings, limits = {}): - """ - Revises a set of key/value mappings to reflect the current configuration. - Undefined values are left with their current values. - - Arguments: - confMappings - configuration key/value mappings to be revised - limits - mappings of limits on numeric values, expected to be of - the form "configKey -> min" or "configKey -> (min, max)" - """ - - for entry in confMappings.keys(): - val = self.get(entry, confMappings[entry]) - - if entry in limits and (isinstance(val, int) or isinstance(val, float)): - if isinstance(limits[entry], tuple): - val = max(val, limits[entry][0]) - val = min(val, limits[entry][1]) - else: val = max(val, limits[entry]) - - confMappings[entry] = val - - def getKeys(self): - """ - Provides all keys in the currently loaded configuration. - """ - - return self.contents.keys() - - def getUnusedKeys(self): - """ - Provides the set of keys that have never been requested. - """ - - return set(self.getKeys()).difference(self.requestedKeys) - - def set(self, key, value): - """ - Stores the given configuration value. - - Arguments: - key - config key to be set - value - config value to be set - """ - - self.contentsLock.acquire() - self.contents[key] = [value] - self.contentsLock.release() - - def clear(self): - """ - Drops all current key/value mappings. - """ - - self.contentsLock.acquire() - self.contents.clear() - self.contentsLock.release() - - def load(self, path): - """ - Reads in the contents of the given path, adding its configuration values - and overwriting any that already exist. If the file's empty then this - doesn't do anything. Other issues (like having insufficient permissions or - if the file doesn't exist) result in an IOError. - - Arguments: - path - file path to be loaded - """ - - configFile = open(path, "r") - self.rawContents = configFile.readlines() - configFile.close() - - self.contentsLock.acquire() - - for line in self.rawContents: - # strips any commenting or excess whitespace - commentStart = line.find("#") - if commentStart != -1: line = line[:commentStart] - line = line.strip() - - # parse the key/value pair - if line and " " in line: - key, value = line.split(" ", 1) - value = value.strip() - - 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): - """ - Writes the contents of the current configuration. If a configuration file - already exists then merges as follows: - - comments and file contents not in this config are left unchanged - - lines with duplicate keys are stripped (first instance is kept) - - existing entries are overwritten with their new values, preserving the - positioning of in-line comments if able - - config entries not in the file are appended to the end in alphabetical - order - - If problems arise in writing (such as an unset path or insufficient - permissions) result in an IOError. - - Arguments: - saveBackup - if true and a file already exists then it's saved (with - '.backup' appended to its filename) - """ - - pass # TODO: implement when persistence is needed - diff --git a/src/util/connections.py b/src/util/connections.py index de128c2..5b3c9bb 100644 --- a/src/util/connections.py +++ b/src/util/connections.py @@ -23,7 +23,7 @@ import threading
from util import log, procTools, sysTools
-from stem.util import enum +from stem.util import conf, enum
# enums for connection resolution utilities Resolver = enum.Enum(("PROC", "proc"), @@ -76,45 +76,43 @@ RESOLVERS = [] # connection resolvers available via the sin RESOLVER_FAILURE_TOLERANCE = 3 # number of subsequent failures before moving on to another resolver RESOLVER_SERIAL_FAILURE_MSG = "Unable to query connections with %s, trying %s" RESOLVER_FINAL_FAILURE_MSG = "All connection resolvers failed" -CONFIG = {"queries.connections.minRate": 5, - "log.connResolverOptions": log.INFO, - "log.connLookupFailed": log.INFO, - "log.connLookupFailover": log.NOTICE, - "log.connLookupAbandon": log.NOTICE, - "log.connLookupRateGrowing": None, - "log.configEntryTypeError": log.NOTICE}
-PORT_USAGE = {} - -def loadConfig(config): - config.update(CONFIG) - - for configKey in config.getKeys(): - # fetches any port.label.* values - if configKey.startswith("port.label."): - portEntry = configKey[11:] - purpose = config.get(configKey) - - divIndex = portEntry.find("-") - if divIndex == -1: - # single port - if portEntry.isdigit(): - PORT_USAGE[portEntry] = purpose - else: - msg = "Port value isn't numeric for entry: %s" % configKey - log.log(CONFIG["log.configEntryTypeError"], msg) +def conf_handler(key, value): + if key.startswith("port.label."): + portEntry = key[11:] + + divIndex = portEntry.find("-") + if divIndex == -1: + # single port + if portEntry.isdigit(): + PORT_USAGE[portEntry] = value else: - try: - # range of ports (inclusive) - minPort = int(portEntry[:divIndex]) - maxPort = int(portEntry[divIndex + 1:]) - if minPort > maxPort: raise ValueError() - - for port in range(minPort, maxPort + 1): - PORT_USAGE[str(port)] = purpose - except ValueError: - msg = "Unable to parse port range for entry: %s" % configKey - log.log(CONFIG["log.configEntryTypeError"], msg) + msg = "Port value isn't numeric for entry: %s" % key + log.log(CONFIG["log.configEntryTypeError"], msg) + else: + try: + # range of ports (inclusive) + minPort = int(portEntry[:divIndex]) + maxPort = int(portEntry[divIndex + 1:]) + if minPort > maxPort: raise ValueError() + + for port in range(minPort, maxPort + 1): + PORT_USAGE[str(port)] = value + except ValueError: + msg = "Unable to parse port range for entry: %s" % key + log.log(CONFIG["log.configEntryTypeError"], msg) + +CONFIG = conf.config_dict("arm", { + "queries.connections.minRate": 5, + "log.connResolverOptions": log.INFO, + "log.connLookupFailed": log.INFO, + "log.connLookupFailover": log.NOTICE, + "log.connLookupAbandon": log.NOTICE, + "log.connLookupRateGrowing": None, + "log.configEntryTypeError": log.NOTICE, +}, conf_handler) + +PORT_USAGE = {}
def isValidIpAddress(ipStr): """ diff --git a/src/util/hostnames.py b/src/util/hostnames.py index 9ae856a..d12f5cb 100644 --- a/src/util/hostnames.py +++ b/src/util/hostnames.py @@ -34,25 +34,31 @@ import distutils.sysconfig
from util import log, sysTools
+from stem.util import conf + RESOLVER = None # hostname resolver (service is stopped if None) RESOLVER_LOCK = threading.RLock() # regulates assignment to the RESOLVER RESOLVER_COUNTER = itertools.count() # atomic counter, providing the age for new entries (for trimming) DNS_ERROR_CODES = ("1(FORMERR)", "2(SERVFAIL)", "3(NXDOMAIN)", "4(NOTIMP)", "5(REFUSED)", "6(YXDOMAIN)", "7(YXRRSET)", "8(NXRRSET)", "9(NOTAUTH)", "10(NOTZONE)", "16(BADVERS)")
-CONFIG = {"queries.hostnames.poolSize": 5, - "queries.hostnames.useSocketModule": False, - "cache.hostnames.size": 700000, - "cache.hostnames.trimSize": 200000, - "log.hostnameCacheTrimmed": log.INFO} +def conf_handler(key, value): + if key == "queries.hostnames.poolSize": + return max(1, value) + elif key == "cache.hostnames.size": + return max(100, value) + elif key == "cache.hostnames.trimSize": + return max(10, value) + elif key == "cache.hostnames.trimSize": + return min(value, CONFIG["cache.hostnames.size"] / 2)
-def loadConfig(config): - config.update(CONFIG, { - "queries.hostnames.poolSize": 1, - "cache.hostnames.size": 100, - "cache.hostnames.trimSize": 10}) - - CONFIG["cache.hostnames.trimSize"] = min(CONFIG["cache.hostnames.trimSize"], CONFIG["cache.hostnames.size"] / 2) +CONFIG = conf.config_dict("arm", { + "queries.hostnames.poolSize": 5, + "queries.hostnames.useSocketModule": False, + "cache.hostnames.size": 700000, + "cache.hostnames.trimSize": 200000, + "log.hostnameCacheTrimmed": log.INFO, +}, conf_handler)
def start(): """ diff --git a/src/util/log.py b/src/util/log.py index 56a6636..60572d5 100644 --- a/src/util/log.py +++ b/src/util/log.py @@ -11,7 +11,7 @@ import time from sys import maxint from threading import RLock
-from stem.util import enum +from stem.util import conf, enum
# Logging runlevels. These are *very* commonly used so including shorter # aliases (so they can be referenced as log.DEBUG, log.WARN, etc). @@ -28,17 +28,20 @@ _backlog = dict([(level, []) for level in Runlevel]) # mapping of runlevels to the listeners interested in receiving events from it _listeners = dict([(level, []) for level in Runlevel])
-CONFIG = {"cache.armLog.size": 1000, - "cache.armLog.trimSize": 200} +def conf_handler(key, value): + if key == "cache.armLog.size": + return max(10, value) + elif key == "cache.armLog.trimSize": + return max(5, value) + elif key == "cache.armLog.trimSize": + return min(value, CONFIG["cache.armLog.size"] / 2)
-DUMP_FILE = None +CONFIG = conf.config_dict("arm", { + "cache.armLog.size": 1000, + "cache.armLog.trimSize": 200, +}, conf_handler)
-def loadConfig(config): - config.update(CONFIG, { - "cache.armLog.size": 10, - "cache.armLog.trimSize": 5}) - - CONFIG["cache.armLog.trimSize"] = min(CONFIG["cache.armLog.trimSize"], CONFIG["cache.armLog.size"] / 2) +DUMP_FILE = None
def setDumpFile(logPath): """ diff --git a/src/util/panel.py b/src/util/panel.py index 50ae2c0..6f69881 100644 --- a/src/util/panel.py +++ b/src/util/panel.py @@ -11,6 +11,8 @@ from threading import RLock
from util import log, textInput, uiTools
+from stem.util import conf + # global ui lock governing all panel instances (curses isn't thread save and # concurrency bugs produce especially sinister glitches) CURSES_LOCK = RLock() @@ -23,14 +25,13 @@ FORMAT_TAGS = {"<b>": (_noOp, curses.A_BOLD), "<h>": (_noOp, curses.A_STANDOUT)} for colorLabel in uiTools.COLOR_LIST: FORMAT_TAGS["<%s>" % colorLabel] = (uiTools.getColor, colorLabel)
-CONFIG = {"log.panelRecreated": log.DEBUG} +CONFIG = conf.config_dict("arm", { + "log.panelRecreated": log.DEBUG, +})
# prevents curses redraws if set HALT_ACTIVITY = False
-def loadConfig(config): - config.update(CONFIG) - class Panel(): """ Wrapper for curses subwindows. This hides most of the ugliness in common diff --git a/src/util/procTools.py b/src/util/procTools.py index 835814e..6f3c31a 100644 --- a/src/util/procTools.py +++ b/src/util/procTools.py @@ -22,18 +22,17 @@ import base64
from util import log
-from stem.util import enum +from stem.util import conf, enum
# cached system values SYS_START_TIME, SYS_PHYSICAL_MEMORY = None, None CLOCK_TICKS = os.sysconf(os.sysconf_names["SC_CLK_TCK"]) Stat = enum.Enum("COMMAND", "CPU_UTIME", "CPU_STIME", "START_TIME")
-CONFIG = {"queries.useProc": True, - "log.procCallMade": log.DEBUG} - -def loadConfig(config): - config.update(CONFIG) +CONFIG = conf.config_dict("arm", { + "queries.useProc": True, + "log.procCallMade": log.DEBUG, +})
def isProcAvailable(): """ diff --git a/src/util/sysTools.py b/src/util/sysTools.py index 1c84462..e8acf86 100644 --- a/src/util/sysTools.py +++ b/src/util/sysTools.py @@ -8,6 +8,8 @@ import threading
from util import log, procTools, uiTools
+from stem.util import conf + # Mapping of commands to if they're available or not. This isn't always # reliable, failing for some special commands. For these the cache is # prepopulated to skip lookups. @@ -28,18 +30,17 @@ RESOURCE_TRACKERS = {} # mapping of pids to their resource tracker instances RUNTIMES = [] SAMPLING_PERIOD = 5 # time of the sampling period
-CONFIG = {"queries.resourceUsage.rate": 5, - "cache.sysCalls.size": 600, - "log.sysCallMade": log.DEBUG, - "log.sysCallCached": None, - "log.sysCallFailed": log.INFO, - "log.sysCallCacheGrowing": log.INFO, - "log.stats.failedProcResolution": log.DEBUG, - "log.stats.procResolutionFailover": log.INFO, - "log.stats.failedPsResolution": log.INFO} - -def loadConfig(config): - config.update(CONFIG) +CONFIG = conf.config_dict("arm", { + "queries.resourceUsage.rate": 5, + "cache.sysCalls.size": 600, + "log.sysCallMade": log.DEBUG, + "log.sysCallCached": None, + "log.sysCallFailed": log.INFO, + "log.sysCallCacheGrowing": log.INFO, + "log.stats.failedProcResolution": log.DEBUG, + "log.stats.procResolutionFailover": log.INFO, + "log.stats.failedPsResolution": log.INFO, +})
def getSysCpuUsage(): """ diff --git a/src/util/torConfig.py b/src/util/torConfig.py index 5566e9d..27636b3 100644 --- a/src/util/torConfig.py +++ b/src/util/torConfig.py @@ -11,25 +11,38 @@ import stem.version
from util import log, sysTools, torTools, uiTools
-from stem.util import enum +from stem.util import conf, enum
-CONFIG = {"features.torrc.validate": True, - "config.important": [], - "torrc.alias": {}, - "torrc.label.size.b": [], - "torrc.label.size.kb": [], - "torrc.label.size.mb": [], - "torrc.label.size.gb": [], - "torrc.label.size.tb": [], - "torrc.label.time.sec": [], - "torrc.label.time.min": [], - "torrc.label.time.hour": [], - "torrc.label.time.day": [], - "torrc.label.time.week": [], - "log.torrc.readFailed": log.WARN, - "log.configDescriptions.unrecognizedCategory": log.NOTICE, - "log.torrc.validation.unnecessaryTorrcEntries": log.NOTICE, - "log.torrc.validation.torStateDiffers": log.WARN} +def conf_handler(key, value): + if key == "config.important": + # stores lowercase entries to drop case sensitivity + return [entry.lower() for entry in value] + elif key.startswith("config.summary."): + # we'll look for summary keys with a lowercase config name + CONFIG[key.lower()] = value + elif key.startswith("torrc.label.") and value: + # all the torrc.label.* values are comma separated lists + return [entry.strip() for entry in value[0].split(",")] + +CONFIG = conf.config_dict("arm", { + "features.torrc.validate": True, + "config.important": [], + "torrc.alias": {}, + "torrc.label.size.b": [], + "torrc.label.size.kb": [], + "torrc.label.size.mb": [], + "torrc.label.size.gb": [], + "torrc.label.size.tb": [], + "torrc.label.time.sec": [], + "torrc.label.time.min": [], + "torrc.label.time.hour": [], + "torrc.label.time.day": [], + "torrc.label.time.week": [], + "log.torrc.readFailed": log.WARN, + "log.configDescriptions.unrecognizedCategory": log.NOTICE, + "log.torrc.validation.unnecessaryTorrcEntries": log.NOTICE, + "log.torrc.validation.torStateDiffers": log.WARN, +}, conf_handler)
# enums and values for numeric torrc entries ValueType = enum.Enum("UNRECOGNIZED", "SIZE", "TIME") @@ -59,23 +72,6 @@ MULTILINE_PARAM = None # cached multiline parameters (lazily loaded) # torrc options that bind to ports PORT_OPT = ("SocksPort", "ORPort", "DirPort", "ControlPort", "TransPort")
-def loadConfig(config): - config.update(CONFIG) - - # stores lowercase entries to drop case sensitivity - CONFIG["config.important"] = [entry.lower() for entry in CONFIG["config.important"]] - - for configKey in config.getKeys(): - # fetches any config.summary.* values - if configKey.startswith("config.summary."): - CONFIG[configKey.lower()] = config.get(configKey) - - # all the torrc.label.* values are comma separated lists - for configKey in CONFIG.keys(): - if configKey.startswith("torrc.label."): - configValues = config.get(configKey, "").split(",") - if configValues: CONFIG[configKey] = [val.strip() for val in configValues] - class ManPageEntry: """ Information provided about a tor configuration option in its man page entry. diff --git a/src/util/torTools.py b/src/util/torTools.py index 7aea8fa..ff85496 100644 --- a/src/util/torTools.py +++ b/src/util/torTools.py @@ -18,7 +18,7 @@ import stem.descriptor
from util import connections, log, procTools, sysTools, uiTools
-from stem.util import enum +from stem.util import conf, enum
# enums for tor's controller state: # INIT - attached to a new controller @@ -46,11 +46,14 @@ CONTROLLER = None # singleton Controller instance UNDEFINED = "<Undefined_ >"
UNKNOWN = "UNKNOWN" # value used by cached information if undefined -CONFIG = {"features.pathPrefix": "", - "log.stemPortClosed": log.NOTICE, - "log.torPrefixPathInvalid": log.NOTICE, - "log.bsdJailFound": log.INFO, - "log.unknownBsdJailId": log.WARN} + +CONFIG = conf.config_dict("arm", { + "features.pathPrefix": "", + "log.stemPortClosed": log.NOTICE, + "log.torPrefixPathInvalid": log.NOTICE, + "log.bsdJailFound": log.INFO, + "log.unknownBsdJailId": log.WARN, +})
# events used for controller functionality: # NOTICE - used to detect when tor is shut down @@ -73,9 +76,6 @@ NO_SPAWN = False # startup performance so we don't introduce a sleep while initializing. IS_STARTUP_SIGNAL = True
-def loadConfig(config): - config.update(CONFIG) - def getPid(controlPort=9051, pidFilePath=None): """ Attempts to determine the process id for a running tor process, using the diff --git a/src/util/uiTools.py b/src/util/uiTools.py index c1f1a86..10f922b 100644 --- a/src/util/uiTools.py +++ b/src/util/uiTools.py @@ -12,7 +12,7 @@ import curses from curses.ascii import isprint from util import log
-from stem.util import enum +from stem.util import conf, enum
# colors curses can handle COLOR_LIST = {"red": curses.COLOR_RED, "green": curses.COLOR_GREEN, @@ -40,27 +40,26 @@ TIME_UNITS = [(86400.0, "d", " day"), (3600.0, "h", " hour"),
Ending = enum.Enum("ELLIPSE", "HYPHEN") SCROLL_KEYS = (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE, curses.KEY_HOME, curses.KEY_END) -CONFIG = {"features.colorInterface": True, - "features.acsSupport": True, - "features.printUnicode": True, - "log.cursesColorSupport": log.INFO, - "log.configEntryTypeError": log.NOTICE} + +def conf_handler(key, value): + if key == "features.colorOverride" and value != "none": + try: setColorOverride(value) + except ValueError, exc: + log.log(CONFIG["log.configEntryTypeError"], exc) + +CONFIG = conf.config_dict("arm", { + "features.colorOverride": "none", + "features.colorInterface": True, + "features.acsSupport": True, + "features.printUnicode": True, + "log.cursesColorSupport": log.INFO, + "log.configEntryTypeError": log.NOTICE, +}, conf_handler)
# Flag indicating if unicode is supported by curses. If None then this has yet # to be determined. IS_UNICODE_SUPPORTED = None
-def loadConfig(config): - config.update(CONFIG) - - CONFIG["features.colorOverride"] = "none" - colorOverride = config.get("features.colorOverride", "none") - - if colorOverride != "none": - try: setColorOverride(colorOverride) - except ValueError, exc: - log.log(CONFIG["log.configEntryTypeError"], exc) - def demoGlyphs(): """ Displays all ACS options with their corresponding representation. These are
tor-commits@lists.torproject.org