commit 058dee1d2ae6570d2ab1eb7254783c276d32c838 Author: Damian Johnson atagar@torproject.org Date: Mon Sep 16 14:51:48 2013 -0700
Removing all trailing whitespace
We'll be swapping over to stem's conventions which are closer to PEP8. Starting by removing all trailing whitespace. --- arm/configPanel.py | 216 ++++++++--------- arm/connections/circEntry.py | 78 +++---- arm/connections/connEntry.py | 338 +++++++++++++-------------- arm/connections/connPanel.py | 252 ++++++++++---------- arm/connections/countPopup.py | 40 ++-- arm/connections/descriptorPopup.py | 82 +++---- arm/connections/entries.py | 66 +++--- arm/controller.py | 262 ++++++++++----------- arm/graphing/bandwidthStats.py | 160 ++++++------- arm/graphing/connStats.py | 24 +- arm/graphing/graphPanel.py | 190 +++++++-------- arm/graphing/resourceStats.py | 18 +- arm/headerPanel.py | 192 +++++++-------- arm/logPanel.py | 450 ++++++++++++++++++------------------ arm/menu/actions.py | 122 +++++----- arm/menu/item.py | 84 +++---- arm/menu/menu.py | 58 ++--- arm/popups.py | 110 ++++----- arm/prereq.py | 46 ++-- arm/torrcPanel.py | 104 ++++----- arm/util/__init__.py | 4 +- arm/util/connections.py | 250 ++++++++++---------- arm/util/hostnames.py | 96 ++++---- arm/util/panel.py | 292 +++++++++++------------ arm/util/sysTools.py | 88 +++---- arm/util/textInput.py | 68 +++--- arm/util/torConfig.py | 390 +++++++++++++++---------------- arm/util/torTools.py | 448 +++++++++++++++++------------------ arm/util/uiTools.py | 172 +++++++------- setup.py | 24 +- 30 files changed, 2362 insertions(+), 2362 deletions(-)
diff --git a/arm/configPanel.py b/arm/configPanel.py index 9ae4fa8..c0fb974 100644 --- a/arm/configPanel.py +++ b/arm/configPanel.py @@ -69,7 +69,7 @@ def getFieldFromLabel(fieldLabel): Converts field labels back to their enumeration, raising a ValueError if it doesn't exist. """ - + for entryEnum in FIELD_ATTR: if fieldLabel == FIELD_ATTR[entryEnum][0]: return entryEnum @@ -78,18 +78,18 @@ class ConfigEntry(): """ Configuration option in the panel. """ - + def __init__(self, option, type, isDefault): self.fields = {} self.fields[Field.OPTION] = option self.fields[Field.TYPE] = type self.fields[Field.IS_DEFAULT] = isDefault - + # Fetches extra infromation from external sources (the arm config and tor # man page). These are None if unavailable for this config option. summary = torConfig.getConfigSummary(option) manEntry = torConfig.getConfigDescription(option) - + if manEntry: self.fields[Field.MAN_ENTRY] = manEntry.index self.fields[Field.CATEGORY] = manEntry.category @@ -100,49 +100,49 @@ class ConfigEntry(): self.fields[Field.CATEGORY] = torConfig.Category.UNKNOWN self.fields[Field.ARG_USAGE] = "" self.fields[Field.DESCRIPTION] = "" - + # uses the full man page description if a summary is unavailable self.fields[Field.SUMMARY] = summary if summary != None else self.fields[Field.DESCRIPTION] - + # cache of what's displayed for this configuration option self.labelCache = None self.labelCacheArgs = None - + def get(self, field): """ Provides back the value in the given field. - + Arguments: field - enum for the field to be provided back """ - + if field == Field.VALUE: return self._getValue() else: return self.fields[field] - + def getAll(self, fields): """ Provides back a list with the given field values. - + Arguments: field - enums for the fields to be provided back """ - + return [self.get(field) for field in fields] - + def getLabel(self, optionWidth, valueWidth, summaryWidth): """ Provides display string of the configuration entry with the given constraints on the width of the contents. - + Arguments: optionWidth - width of the option column valueWidth - width of the value column summaryWidth - width of the summary column """ - + # Fetching the display entries is very common so this caches the values. # Doing this substantially drops cpu usage when scrolling (by around 40%). - + argSet = (optionWidth, valueWidth, summaryWidth) if not self.labelCache or self.labelCacheArgs != argSet: optionLabel = uiTools.cropStr(self.get(Field.OPTION), optionWidth) @@ -151,26 +151,26 @@ class ConfigEntry(): lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, summaryWidth) self.labelCache = lineTextLayout % (optionLabel, valueLabel, summaryLabel) self.labelCacheArgs = argSet - + return self.labelCache - + def isUnset(self): """ True if we have no value, false otherwise. """ - + confValue = torTools.getConn().getOption(self.get(Field.OPTION), [], True) return not bool(confValue) - + def _getValue(self): """ Provides the current value of the configuration entry, taking advantage of the torTools caching to effectively query the accurate value. This uses the value's type to provide a user friendly representation if able. """ - + confValue = ", ".join(torTools.getConn().getOption(self.get(Field.OPTION), [], True)) - + # provides nicer values for recognized types if not confValue: confValue = "<none>" elif self.get(Field.TYPE) == "Boolean" and confValue in ("0", "1"): @@ -179,7 +179,7 @@ class ConfigEntry(): confValue = str_tools.get_size_label(int(confValue)) elif self.get(Field.TYPE) == "TimeInterval" and confValue.isdigit(): confValue = str_tools.get_time_label(int(confValue), is_long = True) - + return confValue
class ConfigPanel(panel.Panel): @@ -187,48 +187,48 @@ class ConfigPanel(panel.Panel): Renders a listing of the tor or arm configuration state, allowing options to be selected and edited. """ - + def __init__(self, stdscr, configType): panel.Panel.__init__(self, stdscr, "configuration", 0) - + self.configType = configType self.confContents = [] self.confImportantContents = [] self.scroller = uiTools.Scroller(True) self.valsLock = threading.RLock() - + # shows all configuration options if true, otherwise only the ones with # the 'important' flag are shown self.showAll = False - + # initializes config contents if we're connected conn = torTools.getConn() conn.addStatusListener(self.resetListener) if conn.isAlive(): self.resetListener(None, stem.control.State.INIT, None) - + def resetListener(self, controller, eventType, _): # fetches configuration options if a new instance, otherewise keeps our # current contents - + if eventType == stem.control.State.INIT: self._loadConfigOptions() - + def _loadConfigOptions(self): """ Fetches the configuration options available from tor or arm. """ - + self.confContents = [] self.confImportantContents = [] - + if self.configType == State.TOR: conn, configOptionLines = torTools.getConn(), [] customOptions = torConfig.getCustomOptions() configOptionQuery = conn.getInfo("config/names", None) - + if configOptionQuery: configOptionLines = configOptionQuery.strip().split("\n") - + for line in configOptionLines: # lines are of the form "<option> <type>[ <documentation>]", like: # UseEntryGuards Boolean @@ -236,83 +236,83 @@ class ConfigPanel(panel.Panel): # 0.2.1.25) lineComp = line.strip().split(" ") confOption, confType = lineComp[0], lineComp[1] - + # skips private and virtual entries if not configured to show them if not CONFIG["features.config.state.showPrivateOptions"] and confOption.startswith("__"): continue 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.get_config("arm") for key in armConf.keys(): pass # TODO: implement - + # mirror listing with only the important configuration options self.confImportantContents = [] for entry in self.confContents: if torConfig.isImportant(entry.get(Field.OPTION)): self.confImportantContents.append(entry) - + # if there aren't any important options then show everything if not self.confImportantContents: self.confImportantContents = self.confContents - + self.setSortOrder() # initial sorting of the contents - + def getSelection(self): """ Provides the currently selected entry. """ - + return self.scroller.getCursorSelection(self._getConfigOptions()) - + def setFiltering(self, isFiltered): """ Sets if configuration options are filtered or not. - + Arguments: isFiltered - if true then only relatively important options will be shown, otherwise everything is shown """ - + self.showAll = not isFiltered - + def setSortOrder(self, ordering = None): """ Sets the configuration attributes we're sorting by and resorts the contents. - + Arguments: ordering - new ordering, if undefined then this resorts with the last set ordering """ - + self.valsLock.acquire() 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): """ Provides the sort dialog for our configuration options. """ - + # 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 CONFIG["features.config.order"]] optionColors = dict([FIELD_ATTR[field] for field in Field]) results = popups.showSortDialog(titleLabel, options, oldSelection, optionColors) - + if results: # converts labels back to enums resultEnums = [getFieldFromLabel(label) for label in results] self.setSortOrder(resultEnums) - + def handleKey(self, key): self.valsLock.acquire() isKeystrokeConsumed = True @@ -321,25 +321,25 @@ class ConfigPanel(panel.Panel): detailPanelHeight = CONFIG["features.config.selectionDetails.height"] if detailPanelHeight > 0 and detailPanelHeight + 2 <= pageHeight: pageHeight -= (detailPanelHeight + 1) - + isChanged = self.scroller.handleKey(key, self._getConfigOptions(), pageHeight) if isChanged: self.redraw(True) elif uiTools.isSelectionKey(key) and self._getConfigOptions(): # Prompts the user to edit the selected configuration value. The # interface is locked to prevent updates between setting the value # and showing any errors. - + panel.CURSES_LOCK.acquire() try: selection = self.getSelection() configOption = selection.get(Field.OPTION) if selection.isUnset(): initialValue = "" else: initialValue = selection.get(Field.VALUE) - + promptMsg = "%s Value (esc to cancel): " % configOption isPrepopulated = CONFIG["features.config.prepopulateEditValues"] newValue = popups.inputPrompt(promptMsg, initialValue if isPrepopulated else "") - + if newValue != None and newValue != initialValue: try: if selection.get(Field.TYPE) == "Boolean": @@ -349,16 +349,16 @@ class ConfigPanel(panel.Panel): elif selection.get(Field.TYPE) == "LineList": # setOption accepts list inputs when there's multiple values newValue = newValue.split(",") - + torTools.getConn().setOption(configOption, newValue) - + # forces the label to be remade with the new value selection.labelCache = None - + # resets the isDefault flag customOptions = torConfig.getCustomOptions() selection.fields[Field.IS_DEFAULT] = not configOption in customOptions - + self.redraw(True) except Exception, exc: popups.showMsg("%s (press any key)" % exc) @@ -372,41 +372,41 @@ class ConfigPanel(panel.Panel): elif key == ord('v') or key == ord('V'): self.showWriteDialog() else: isKeystrokeConsumed = False - + self.valsLock.release() return isKeystrokeConsumed - + def showWriteDialog(self): """ Provies an interface to confirm if the configuration is saved and, if so, where. """ - + # display a popup for saving the current configuration configLines = torConfig.getCustomOptions(True) popup, width, height = popups.init(len(configLines) + 2) if not popup: return - + try: # displayed options (truncating the labels if there's limited room) if width >= 30: selectionOptions = ("Save", "Save As...", "Cancel") else: selectionOptions = ("Save", "Save As", "X") - + # checks if we can show options beside the last line of visible content isOptionLineSeparate = False lastIndex = min(height - 2, len(configLines) - 1) - + # if we don't have room to display the selection options and room to # grow then display the selection options on its own line if width < (30 + len(configLines[lastIndex])): popup.setHeight(height + 1) popup.redraw(True) # recreates the window instance newHeight, _ = popup.getPreferredSize() - + if newHeight > height: height = newHeight isOptionLineSeparate = True - + key, selection = 0, 2 while not uiTools.isSelectionKey(key): # if the popup has been resized then recreate it (needed for the @@ -415,70 +415,70 @@ class ConfigPanel(panel.Panel): if (height, width) != (newHeight, newWidth): height, width = newHeight, newWidth popup.redraw(True) - + # if there isn't room to display the popup then cancel it if height <= 2: selection = 2 break - + popup.win.erase() popup.win.box() popup.addstr(0, 0, "Configuration being saved:", curses.A_STANDOUT) - + visibleConfigLines = height - 3 if isOptionLineSeparate else height - 2 for i in range(visibleConfigLines): line = uiTools.cropStr(configLines[i], width - 2) - + if " " in line: option, arg = line.split(" ", 1) popup.addstr(i + 1, 1, option, curses.A_BOLD | uiTools.getColor("green")) popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD | uiTools.getColor("cyan")) else: popup.addstr(i + 1, 1, line, curses.A_BOLD | uiTools.getColor("green")) - + # draws selection options (drawn right to left) drawX = width - 1 for i in range(len(selectionOptions) - 1, -1, -1): optionLabel = selectionOptions[i] drawX -= (len(optionLabel) + 2) - + # if we've run out of room then drop the option (this will only # occure on tiny displays) if drawX < 1: break - + selectionFormat = curses.A_STANDOUT if i == selection else curses.A_NORMAL popup.addstr(height - 2, drawX, "[") popup.addstr(height - 2, drawX + 1, optionLabel, selectionFormat | curses.A_BOLD) popup.addstr(height - 2, drawX + len(optionLabel) + 1, "]") - + drawX -= 1 # space gap between the options - + popup.win.refresh() - + key = arm.controller.getController().getScreen().getch() if key == curses.KEY_LEFT: selection = max(0, selection - 1) elif key == curses.KEY_RIGHT: selection = min(len(selectionOptions) - 1, selection + 1) - + if selection in (0, 1): loadedTorrc, promptCanceled = torConfig.getTorrc(), False try: configLocation = loadedTorrc.getConfigLocation() except IOError: configLocation = "" - + if selection == 1: # prompts user for a configuration location configLocation = popups.inputPrompt("Save to (esc to cancel): ", configLocation) if not configLocation: promptCanceled = True - + if not promptCanceled: try: torConfig.saveConf(configLocation, configLines) msg = "Saved configuration to %s" % configLocation except IOError, exc: msg = "Unable to save configuration (%s)" % exc.strerror - + popups.showMsg(msg, 2) finally: popups.finalize() - + def getHelp(self): options = [] options.append(("up arrow", "scroll up a line", None)) @@ -490,10 +490,10 @@ class ConfigPanel(panel.Panel): options.append(("a", "toggle option filtering", None)) options.append(("s", "sort ordering", None)) return options - + def draw(self, width, height): self.valsLock.acquire() - + # panel with details for the current selection detailPanelHeight = CONFIG["features.config.selectionDetails.height"] isScrollbarVisible = False @@ -510,68 +510,68 @@ class ConfigPanel(panel.Panel): scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1 - detailPanelHeight) cursorSelection = self.getSelection() isScrollbarVisible = len(self._getConfigOptions()) > height - detailPanelHeight - 1 - + if cursorSelection != None: self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, isScrollbarVisible) - + # draws the top label if self.isTitleVisible(): configType = "Tor" if self.configType == State.TOR else "Arm" hiddenMsg = "press 'a' to hide most options" if self.showAll else "press 'a' to show all options" titleLabel = "%s Configuration (%s):" % (configType, hiddenMsg) self.addstr(0, 0, titleLabel, curses.A_STANDOUT) - + # draws left-hand scroll bar if content's longer than the height scrollOffset = 1 if isScrollbarVisible: scrollOffset = 3 self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self._getConfigOptions()), 1 + detailPanelHeight) - + 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 # value instead if descriptionWidth > 80: valueWidth += descriptionWidth - 80 descriptionWidth = 80 - + for lineNum in range(scrollLoc, len(self._getConfigOptions())): entry = self._getConfigOptions()[lineNum] drawLine = lineNum + detailPanelHeight + 1 - scrollLoc - + lineFormat = curses.A_NORMAL if entry.get(Field.IS_DEFAULT) else curses.A_BOLD if entry.get(Field.CATEGORY): lineFormat |= uiTools.getColor(CATEGORY_COLOR[entry.get(Field.CATEGORY)]) if entry == cursorSelection: lineFormat |= curses.A_STANDOUT - + lineText = entry.getLabel(optionWidth, valueWidth, descriptionWidth) self.addstr(drawLine, scrollOffset, lineText, lineFormat) - + if drawLine >= height: break - + self.valsLock.release() - + def _getConfigOptions(self): return self.confContents if self.showAll else self.confImportantContents - + def _drawSelectionPanel(self, selection, width, detailPanelHeight, isScrollbarVisible): """ Renders a panel for the selected configuration option. """ - + # This is a solid border unless the scrollbar is visible, in which case a # 'T' pipe connects the border to the bar. uiTools.drawBox(self, 0, 0, width, detailPanelHeight + 1) if isScrollbarVisible: self.addch(detailPanelHeight, 1, curses.ACS_TTEE) - + selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[selection.get(Field.CATEGORY)]) - + # first entry: # <option> (<category> Option) optionLabel =" (%s Option)" % selection.get(Field.CATEGORY) self.addstr(1, 2, selection.get(Field.OPTION) + optionLabel, selectionFormat) - + # second entry: # Value: <value> ([default|custom], <type>, usage: <argument usage>) if detailPanelHeight >= 3: @@ -580,28 +580,28 @@ class ConfigPanel(panel.Panel): valueAttr.append(selection.get(Field.TYPE)) valueAttr.append("usage: %s" % (selection.get(Field.ARG_USAGE))) valueAttrLabel = ", ".join(valueAttr) - + valueLabelWidth = width - 12 - len(valueAttrLabel) valueLabel = uiTools.cropStr(selection.get(Field.VALUE), valueLabelWidth) - + self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat) - + # remainder is filled with the man page description descriptionHeight = max(0, detailPanelHeight - 3) descriptionContent = "Description: " + selection.get(Field.DESCRIPTION) - + for i in range(descriptionHeight): # checks if we're done writing the description if not descriptionContent: break - + # there's a leading indent after the first line if i > 0: descriptionContent = " " + descriptionContent - + # we only want to work with content up until the next newline if "\n" in descriptionContent: lineContent, descriptionContent = descriptionContent.split("\n", 1) else: lineContent, descriptionContent = descriptionContent, "" - + if i != descriptionHeight - 1: # there's more lines to display msg, remainder = uiTools.cropStr(lineContent, width - 3, 4, 4, uiTools.Ending.HYPHEN, True) @@ -609,6 +609,6 @@ class ConfigPanel(panel.Panel): else: # this is the last line, end it with an ellipse msg = uiTools.cropStr(lineContent, width - 3, 4, 4) - + self.addstr(3 + i, 2, msg, selectionFormat)
diff --git a/arm/connections/circEntry.py b/arm/connections/circEntry.py index 6c809b1..cef6820 100644 --- a/arm/connections/circEntry.py +++ b/arm/connections/circEntry.py @@ -16,56 +16,56 @@ from arm.util import torTools, uiTools class CircEntry(connEntry.ConnectionEntry): def __init__(self, circuitID, status, purpose, path): connEntry.ConnectionEntry.__init__(self, "127.0.0.1", "0", "127.0.0.1", "0") - + self.circuitID = circuitID self.status = status - + # drops to lowercase except the first letter if len(purpose) >= 2: purpose = purpose[0].upper() + purpose[1:].lower() - + self.lines = [CircHeaderLine(self.circuitID, purpose)] - + # Overwrites attributes of the initial line to make it more fitting as the # header for our listing. - + self.lines[0].baseType = connEntry.Category.CIRCUIT - + self.update(status, path) - + def update(self, status, path): """ Our status and path can change over time if the circuit is still in the process of being built. Updates these attributes of our relay. - + Arguments: status - new status of the circuit path - list of fingerprints for the series of relays involved in the circuit """ - + self.status = status self.lines = [self.lines[0]] conn = torTools.getConn() - + if status == "BUILT" and not self.lines[0].isBuilt: exitIp, exitORPort = conn.getRelayAddress(path[-1], ("192.168.0.1", "0")) self.lines[0].setExit(exitIp, exitORPort, path[-1]) - + for i in range(len(path)): relayFingerprint = path[i] relayIp, relayOrPort = conn.getRelayAddress(relayFingerprint, ("192.168.0.1", "0")) - + if i == len(path) - 1: if status == "BUILT": placementType = "Exit" else: placementType = "Extending" elif i == 0: placementType = "Guard" else: placementType = "Middle" - + placementLabel = "%i / %s" % (i + 1, placementType) - + self.lines.append(CircLine(relayIp, relayOrPort, relayFingerprint, placementLabel)) - + self.lines[-1].isLast = True
class CircHeaderLine(connEntry.ConnectionLine): @@ -73,40 +73,40 @@ class CircHeaderLine(connEntry.ConnectionLine): Initial line of a client entry. This has the same basic format as connection lines except that its etc field has circuit attributes. """ - + def __init__(self, circuitID, purpose): connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", "0.0.0.0", "0", False, False) self.circuitID = circuitID self.purpose = purpose self.isBuilt = False - + def setExit(self, exitIpAddr, exitPort, exitFingerprint): connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", exitIpAddr, exitPort, False, False) self.isBuilt = True self.foreign.fingerprintOverwrite = exitFingerprint - + def getType(self): return connEntry.Category.CIRCUIT - + def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False): if not self.isBuilt: return "Building..." return connEntry.ConnectionLine.getDestinationLabel(self, maxLength, includeLocale, includeHostname) - + def getEtcContent(self, width, listingType): """ Attempts to provide all circuit related stats. Anything that can't be shown completely (not enough room) is dropped. """ - + etcAttr = ["Purpose: %s" % self.purpose, "Circuit ID: %i" % self.circuitID] - + for i in range(len(etcAttr), -1, -1): etcLabel = ", ".join(etcAttr[:i]) if len(etcLabel) <= width: return ("%%-%is" % width) % etcLabel - + return "" - + def getDetails(self, width): if not self.isBuilt: detailFormat = curses.A_BOLD | uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()]) @@ -119,60 +119,60 @@ class CircLine(connEntry.ConnectionLine): otherwise makes use of the ConnectionLine attributes (for the detail display, caching, etc). """ - + def __init__(self, fIpAddr, fPort, fFingerprint, placementLabel): connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", fIpAddr, fPort) self.foreign.fingerprintOverwrite = fFingerprint self.placementLabel = placementLabel self.includePort = False - + # determines the sort of left hand bracketing we use self.isLast = False - + def getType(self): return connEntry.Category.CIRCUIT - + def getListingPrefix(self): if self.isLast: return (ord(' '), curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' ')) else: return (ord(' '), curses.ACS_VLINE, ord(' '), ord(' ')) - + def getListingEntry(self, width, currentTime, listingType): """ Provides the [(msg, attr)...] listing for this relay in the circuilt listing. Lines are composed of the following components: <bracket> <dst> <etc> <placement label> - + The dst and etc entries largely match their ConnectionEntry counterparts. - + Arguments: width - maximum length of the line currentTime - the current unix time (ignored) listingType - primary attribute we're listing connections by """ - + return entries.ConnectionPanelLine.getListingEntry(self, width, currentTime, listingType) - + def _getListingEntry(self, width, currentTime, listingType): lineFormat = uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()]) - + # The required widths are the sum of the following: # initial space (1 character) # bracketing (3 characters) # placementLabel (14 characters) # gap between etc and placement label (5 characters) - + baselineSpace = 14 + 5 - + dst, etc = "", "" if listingType == entries.ListingType.IP_ADDRESS: # TODO: include hostname when that's available # dst width is derived as: # src (21) + dst (26) + divider (7) + right gap (2) - bracket (3) = 53 char dst = "%-53s" % self.getDestinationLabel(53, includeLocale = True) - + # fills the nickname into the empty space here dst = "%s%-25s " % (dst[:25], uiTools.cropStr(self.foreign.getNickname(), 25, 0)) - + etc = self.getEtcContent(width - baselineSpace - len(dst), listingType) elif listingType == entries.ListingType.HOSTNAME: # min space for the hostname is 40 characters @@ -189,7 +189,7 @@ class CircLine(connEntry.ConnectionLine): etc = self.getEtcContent(width - baselineSpace - 56, listingType) dstLayout = "%%-%is" % (width - baselineSpace - len(etc)) dst = dstLayout % self.foreign.getNickname() - + return ((dst + etc, lineFormat), (" " * (width - baselineSpace - len(dst) - len(etc) + 5), lineFormat), ("%-14s" % self.placementLabel, lineFormat)) diff --git a/arm/connections/connEntry.py b/arm/connections/connEntry.py index 8ade8e5..bd83cca 100644 --- a/arm/connections/connEntry.py +++ b/arm/connections/connEntry.py @@ -51,43 +51,43 @@ class Endpoint: thin wrapper for torUtil functions, making use of its caching for performance. """ - + def __init__(self, ipAddr, port): self.ipAddr = ipAddr self.port = port - + # if true, we treat the port as an definitely not being an ORPort when # searching for matching fingerprints (otherwise we use it to possably # narrow results when unknown) self.isNotORPort = True - + # if set then this overwrites fingerprint lookups self.fingerprintOverwrite = None - + def getIpAddr(self): """ Provides the IP address of the endpoint. """ - + return self.ipAddr - + def getPort(self): """ Provides the port of the endpoint. """ - + return self.port - + def getHostname(self, default = None): """ Provides the hostname associated with the relay's address. This is a non-blocking call and returns None if the address either can't be resolved or hasn't been resolved yet. - + Arguments: default - return value if no hostname is available """ - + # TODO: skipping all hostname resolution to be safe for now #try: # myHostname = hostnames.resolve(self.ipAddr) @@ -97,52 +97,52 @@ class Endpoint: # #if not myHostname: return default #else: return myHostname - + return default - + def getLocale(self, default=None): """ Provides the two letter country code for the IP address' locale. - + Arguments: default - return value if no locale information is available """ - + conn = torTools.getConn() return conn.getInfo("ip-to-country/%s" % self.ipAddr, default) - + def getFingerprint(self): """ Provides the fingerprint of the relay, returning "UNKNOWN" if it can't be determined. """ - + if self.fingerprintOverwrite: return self.fingerprintOverwrite - + conn = torTools.getConn() myFingerprint = conn.getRelayFingerprint(self.ipAddr) - + # If there were multiple matches and our port is likely the ORPort then # try again with that to narrow the results. if not myFingerprint and not self.isNotORPort: myFingerprint = conn.getRelayFingerprint(self.ipAddr, self.port) - + if myFingerprint: return myFingerprint else: return "UNKNOWN" - + def getNickname(self): """ Provides the nickname of the relay, retuning "UNKNOWN" if it can't be determined. """ - + myFingerprint = self.getFingerprint() - + if myFingerprint != "UNKNOWN": conn = torTools.getConn() myNickname = conn.getRelayNickname(myFingerprint) - + if myNickname: return myNickname else: return "UNKNOWN" else: return "UNKNOWN" @@ -153,16 +153,16 @@ class ConnectionEntry(entries.ConnectionPanelEntry): concern real connections so it includes the inbound, outbound, directory, application, and controller categories. """ - + def __init__(self, lIpAddr, lPort, fIpAddr, fPort): entries.ConnectionPanelEntry.__init__(self) self.lines = [ConnectionLine(lIpAddr, lPort, fIpAddr, fPort)] - + def getSortValue(self, attr, listingType): """ Provides the value of a single attribute used for sorting purposes. """ - + connLine = self.lines[0] if attr == entries.SortAttr.IP_ADDRESS: if connLine.isPrivate(): return SCRUBBED_IP_VAL # orders at the end @@ -192,44 +192,44 @@ class ConnectionLine(entries.ConnectionPanelLine): """ Display component of the ConnectionEntry. """ - + def __init__(self, lIpAddr, lPort, fIpAddr, fPort, includePort=True, includeExpandedIpAddr=True): entries.ConnectionPanelLine.__init__(self) - + self.local = Endpoint(lIpAddr, lPort) self.foreign = Endpoint(fIpAddr, fPort) self.startTime = time.time() self.isInitialConnection = False - + # overwrite the local fingerprint with ours conn = torTools.getConn() self.local.fingerprintOverwrite = conn.getInfo("fingerprint", None) - + # True if the connection has matched the properties of a client/directory # connection every time we've checked. The criteria we check is... # client - first hop in an established circuit # directory - matches an established single-hop circuit (probably a # directory mirror) - + self._possibleClient = True self._possibleDirectory = True - + # attributes for SOCKS, HIDDEN, and CONTROL connections self.appName = None self.appPid = None self.isAppResolving = False - + myOrPort = conn.getOption("ORPort", None) myDirPort = conn.getOption("DirPort", None) mySocksPort = conn.getOption("SocksPort", "9050") myCtlPort = conn.getOption("ControlPort", None) myHiddenServicePorts = conn.getHiddenServicePorts() - + # the ORListenAddress can overwrite the ORPort listenAddr = conn.getOption("ORListenAddress", None) if listenAddr and ":" in listenAddr: myOrPort = listenAddr[listenAddr.find(":") + 1:] - + if lPort in (myOrPort, myDirPort): self.baseType = Category.INBOUND self.local.isNotORPort = False @@ -242,74 +242,74 @@ class ConnectionLine(entries.ConnectionPanelLine): else: self.baseType = Category.OUTBOUND self.foreign.isNotORPort = False - + self.cachedType = None - + # includes the port or expanded ip address field when displaying listing # information if true self.includePort = includePort self.includeExpandedIpAddr = includeExpandedIpAddr - + # cached immutable values used for sorting self.sortIpAddr = connections.ipToInt(self.foreign.getIpAddr()) self.sortPort = int(self.foreign.getPort()) - + def getListingEntry(self, width, currentTime, listingType): """ Provides the tuple list for this connection's listing. Lines are composed of the following components: <src> --> <dst> <etc> <uptime> (<type>) - + ListingType.IP_ADDRESS: src - <internal addr:port> --> <external addr:port> dst - <destination addr:port> etc - <fingerprint> <nickname> - + ListingType.HOSTNAME: src - localhost:<port> dst - <destination hostname:port> etc - <destination addr:port> <fingerprint> <nickname> - + ListingType.FINGERPRINT: src - localhost dst - <destination fingerprint> etc - <nickname> <destination addr:port> - + ListingType.NICKNAME: src - <source nickname> dst - <destination nickname> etc - <fingerprint> <destination addr:port> - + Arguments: width - maximum length of the line currentTime - unix timestamp for what the results should consider to be the current time listingType - primary attribute we're listing connections by """ - + # fetch our (most likely cached) display entry for the listing myListing = entries.ConnectionPanelLine.getListingEntry(self, width, currentTime, listingType) - + # fill in the current uptime and return the results if CONFIG["features.connection.markInitialConnections"]: timePrefix = "+" if self.isInitialConnection else " " else: timePrefix = "" - + timeLabel = timePrefix + "%5s" % str_tools.get_time_label(currentTime - self.startTime, 1) myListing[2] = (timeLabel, myListing[2][1]) - + return myListing - + def isUnresolvedApp(self): """ True if our display uses application information that hasn't yet been resolved. """ - + return self.appName == None and self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL) - + def _getListingEntry(self, width, currentTime, listingType): entryType = self.getType() - + # Lines are split into the following components in reverse: # init gap - " " # content - "<src> --> <dst> <etc> " @@ -317,10 +317,10 @@ class ConnectionLine(entries.ConnectionPanelLine): # preType - " (" # category - "<type>" # postType - ") " - + lineFormat = uiTools.getColor(CATEGORY_COLOR[entryType]) timeWidth = 6 if CONFIG["features.connection.markInitialConnections"] else 5 - + drawEntry = [(" ", lineFormat), (self._getListingContent(width - (12 + timeWidth) - 1, listingType), lineFormat), (" " * timeWidth, lineFormat), @@ -328,41 +328,41 @@ class ConnectionLine(entries.ConnectionPanelLine): (entryType.upper(), lineFormat | curses.A_BOLD), (")" + " " * (9 - len(entryType)), lineFormat)] return drawEntry - + def _getDetails(self, width): """ Provides details on the connection, correlated against available consensus data. - + Arguments: width - available space to display in """ - + detailFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[self.getType()]) return [(line, detailFormat) for line in self._getDetailContent(width)] - + def resetDisplay(self): entries.ConnectionPanelLine.resetDisplay(self) self.cachedType = None - + def isPrivate(self): """ Returns true if the endpoint is private, possibly belonging to a client connection or exit traffic. """ - + if not CONFIG["features.connection.showIps"]: return True - + # This is used to scrub private information from the interface. Relaying # etiquette (and wiretapping laws) say these are bad things to look at so # DON'T CHANGE THIS UNLESS YOU HAVE A DAMN GOOD REASON! - + myType = self.getType() - + if myType == Category.INBOUND: # if we're a guard or bridge and the connection doesn't belong to a # known relay then it might be client traffic - + conn = torTools.getConn() if "Guard" in conn.getMyFlags([]) or conn.getOption("BridgeRelay", None) == "1": allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True) @@ -370,23 +370,23 @@ class ConnectionLine(entries.ConnectionPanelLine): elif myType == Category.EXIT: # DNS connections exiting us aren't private (since they're hitting our # resolvers). Everything else, however, is. - + # TODO: Ideally this would also double check that it's a UDP connection # (since DNS is the only UDP connections Tor will relay), however this # will take a bit more work to propagate the information up from the # connection resolver. return self.foreign.getPort() != "53" - + # for everything else this isn't a concern return False - + def getType(self): """ Provides our best guess at the current type of the connection. This depends on consensus results, our current client circuits, etc. Results are cached until this entry's display is reset. """ - + # caches both to simplify the calls and to keep the type consistent until # we want to reflect changes if not self.cachedType: @@ -395,79 +395,79 @@ class ConnectionLine(entries.ConnectionPanelLine): # - EXIT since this depends on the current consensus # - CIRCUIT if this is likely to belong to our guard usage # - DIRECTORY if this is a single-hop circuit (directory mirror?) - # + # # The exitability, circuits, and fingerprints are all cached by the # torTools util keeping this a quick lookup. - + conn = torTools.getConn() destFingerprint = self.foreign.getFingerprint() - + if destFingerprint == "UNKNOWN": # Not a known relay. This might be an exit connection. - + if conn.isExitingAllowed(self.foreign.getIpAddr(), self.foreign.getPort()): self.cachedType = Category.EXIT elif self._possibleClient or self._possibleDirectory: # This belongs to a known relay. If we haven't eliminated ourselves as # a possible client or directory connection then check if it still # holds true. - + myCircuits = conn.getCircuits() - + if self._possibleClient: # Checks that this belongs to the first hop in a circuit that's # either unestablished or longer than a single hop (ie, anything but # a built 1-hop connection since those are most likely a directory # mirror). - + for _, status, _, path in myCircuits: if path and path[0] == destFingerprint and (status != "BUILT" or len(path) > 1): self.cachedType = Category.CIRCUIT # matched a probable guard connection - + # if we fell through, we can eliminate ourselves as a guard in the future if not self.cachedType: self._possibleClient = False - + if self._possibleDirectory: # Checks if we match a built, single hop circuit. - + for _, status, _, path in myCircuits: if path and path[0] == destFingerprint and status == "BUILT" and len(path) == 1: self.cachedType = Category.DIRECTORY - + # if we fell through, eliminate ourselves as a directory connection if not self.cachedType: self._possibleDirectory = False - + if not self.cachedType: self.cachedType = self.baseType - + return self.cachedType - + def getEtcContent(self, width, listingType): """ Provides the optional content for the connection. - + Arguments: width - maximum length of the line listingType - primary attribute we're listing connections by """ - + # for applications show the command/pid if self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL): displayLabel = "" - + if self.appName: if self.appPid: displayLabel = "%s (%s)" % (self.appName, self.appPid) else: displayLabel = self.appName elif self.isAppResolving: displayLabel = "resolving..." else: displayLabel = "UNKNOWN" - + if len(displayLabel) < width: return ("%%-%is" % width) % displayLabel else: return "" - + # for everything else display connection/consensus information dstAddress = self.getDestinationLabel(26, includeLocale = True) etc, usedSpace = "", 0 @@ -476,7 +476,7 @@ class ConnectionLine(entries.ConnectionPanelLine): # show fingerprint (column width: 42 characters) etc += "%-40s " % self.foreign.getFingerprint() usedSpace += 42 - + if width > usedSpace + 10 and CONFIG["features.connection.showColumn.nickname"]: # show nickname (column width: remainder) nicknameSpace = width - usedSpace @@ -488,12 +488,12 @@ class ConnectionLine(entries.ConnectionPanelLine): # show destination ip/port/locale (column width: 28 characters) etc += "%-26s " % dstAddress usedSpace += 28 - + if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: # show fingerprint (column width: 42 characters) etc += "%-40s " % self.foreign.getFingerprint() usedSpace += 42 - + if width > usedSpace + 17 and CONFIG["features.connection.showColumn.nickname"]: # show nickname (column width: min 17 characters, uses half of the remainder) nicknameSpace = 15 + (width - (usedSpace + 17)) / 2 @@ -504,18 +504,18 @@ class ConnectionLine(entries.ConnectionPanelLine): if width > usedSpace + 17: # show nickname (column width: min 17 characters, consumes any remaining space) nicknameSpace = width - usedSpace - 2 - + # if there's room then also show a column with the destination # ip/port/locale (column width: 28 characters) isIpLocaleIncluded = width > usedSpace + 45 isIpLocaleIncluded &= CONFIG["features.connection.showColumn.destination"] if isIpLocaleIncluded: nicknameSpace -= 28 - + if CONFIG["features.connection.showColumn.nickname"]: nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) etc += ("%%-%is " % nicknameSpace) % nicknameLabel usedSpace += nicknameSpace + 2 - + if isIpLocaleIncluded: etc += "%-26s " % dstAddress usedSpace += 28 @@ -524,40 +524,40 @@ class ConnectionLine(entries.ConnectionPanelLine): # show fingerprint (column width: 42 characters) etc += "%-40s " % self.foreign.getFingerprint() usedSpace += 42 - + if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]: # show destination ip/port/locale (column width: 28 characters) etc += "%-26s " % dstAddress usedSpace += 28 - + return ("%%-%is" % width) % etc - + def _getListingContent(self, width, listingType): """ Provides the source, destination, and extra info for our listing. - + Arguments: width - maximum length of the line listingType - primary attribute we're listing connections by """ - + conn = torTools.getConn() myType = self.getType() dstAddress = self.getDestinationLabel(26, includeLocale = True) - + # The required widths are the sum of the following: # - room for LABEL_FORMAT and LABEL_MIN_PADDING (11 characters) # - base data for the listing # - that extra field plus any previous - + usedSpace = len(LABEL_FORMAT % tuple([""] * 4)) + LABEL_MIN_PADDING localPort = ":%s" % self.local.getPort() if self.includePort else "" - + src, dst, etc = "", "", "" if listingType == entries.ListingType.IP_ADDRESS: myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr()) addrDiffer = myExternalIpAddr != self.local.getIpAddr() - + # Expanding doesn't make sense, if the connection isn't actually # going through Tor's external IP address. As there isn't a known # method for checking if it is, we're checking the type instead. @@ -565,47 +565,47 @@ class ConnectionLine(entries.ConnectionPanelLine): # This isn't entirely correct. It might be a better idea to check if # the source and destination addresses are both private, but that might # not be perfectly reliable either. - + isExpansionType = not myType in (Category.SOCKS, Category.HIDDEN, Category.CONTROL) - + if isExpansionType: srcAddress = myExternalIpAddr + localPort else: srcAddress = self.local.getIpAddr() + localPort - + if myType in (Category.SOCKS, Category.CONTROL): # Like inbound connections these need their source and destination to # be swapped. However, this only applies when listing by IP or hostname # (their fingerprint and nickname are both for us). Reversing the # fields here to keep the same column alignments. - + src = "%-21s" % dstAddress dst = "%-26s" % srcAddress else: src = "%-21s" % srcAddress # ip:port = max of 21 characters dst = "%-26s" % dstAddress # ip:port (xx) = max of 26 characters - + usedSpace += len(src) + len(dst) # base data requires 47 characters - + # Showing the fingerprint (which has the width of 42) has priority over # an expanded address field. Hence check if we either have space for # both or wouldn't be showing the fingerprint regardless. - + isExpandedAddrVisible = width > usedSpace + 28 if isExpandedAddrVisible and CONFIG["features.connection.showColumn.fingerprint"]: isExpandedAddrVisible = width < usedSpace + 42 or width > usedSpace + 70 - + if addrDiffer and isExpansionType and isExpandedAddrVisible and self.includeExpandedIpAddr and CONFIG["features.connection.showColumn.expandedIp"]: # include the internal address in the src (extra 28 characters) internalAddress = self.local.getIpAddr() + localPort - + # If this is an inbound connection then reverse ordering so it's: # <foreign> --> <external> --> <internal> # when the src and dst are swapped later - + if myType == Category.INBOUND: src = "%-21s --> %s" % (src, internalAddress) else: src = "%-21s --> %s" % (internalAddress, src) - + usedSpace += 28 - + etc = self.getEtcContent(width - usedSpace, listingType) usedSpace += len(etc) elif listingType == entries.ListingType.HOSTNAME: @@ -615,10 +615,10 @@ class ConnectionLine(entries.ConnectionPanelLine): src = "localhost%-6s" % localPort usedSpace += len(src) minHostnameSpace = 40 - + etc = self.getEtcContent(width - usedSpace - minHostnameSpace, listingType) usedSpace += len(etc) - + hostnameSpace = width - usedSpace usedSpace = width # prevents padding at the end if self.isPrivate(): @@ -626,7 +626,7 @@ class ConnectionLine(entries.ConnectionPanelLine): else: hostname = self.foreign.getHostname(self.foreign.getIpAddr()) portLabel = ":%-5s" % self.foreign.getPort() if self.includePort else "" - + # truncates long hostnames and sets dst to <hostname>:<port> hostname = uiTools.cropStr(hostname, hostnameSpace, 0) dst = ("%%-%is" % hostnameSpace) % (hostname + portLabel) @@ -635,9 +635,9 @@ class ConnectionLine(entries.ConnectionPanelLine): if myType == Category.CONTROL: dst = "localhost" else: dst = self.foreign.getFingerprint() dst = "%-40s" % dst - + usedSpace += len(src) + len(dst) # base data requires 49 characters - + etc = self.getEtcContent(width - usedSpace, listingType) usedSpace += len(etc) else: @@ -646,204 +646,204 @@ class ConnectionLine(entries.ConnectionPanelLine): if myType == Category.CONTROL: dst = self.local.getNickname() else: dst = self.foreign.getNickname() minBaseSpace = 50 - + etc = self.getEtcContent(width - usedSpace - minBaseSpace, listingType) usedSpace += len(etc) - + baseSpace = width - usedSpace usedSpace = width # prevents padding at the end - + if len(src) + len(dst) > baseSpace: src = uiTools.cropStr(src, baseSpace / 3) dst = uiTools.cropStr(dst, baseSpace - len(src)) - + # pads dst entry to its max space dst = ("%%-%is" % (baseSpace - len(src))) % dst - + if myType == Category.INBOUND: src, dst = dst, src padding = " " * (width - usedSpace + LABEL_MIN_PADDING) return LABEL_FORMAT % (src, dst, etc, padding) - + def _getDetailContent(self, width): """ Provides a list with detailed information for this connection. - + Arguments: width - max length of lines """ - + lines = [""] * 7 lines[0] = "address: %s" % self.getDestinationLabel(width - 11) lines[1] = "locale: %s" % ("??" if self.isPrivate() else self.foreign.getLocale("??")) - + # Remaining data concerns the consensus results, with three possible cases: # - if there's a single match then display its details # - if there's multiple potential relays then list all of the combinations # of ORPorts / Fingerprints # - if no consensus data is available then say so (probably a client or # exit connection) - + fingerprint = self.foreign.getFingerprint() conn = torTools.getConn() - + if fingerprint != "UNKNOWN": # single match - display information available about it nsEntry = conn.getConsensusEntry(fingerprint) descEntry = conn.getDescriptorEntry(fingerprint) - + # append the fingerprint to the second line lines[1] = "%-13sfingerprint: %s" % (lines[1], fingerprint) - + if nsEntry: # example consensus entry: # r murble R8sCM1ar1sS2GulQYFVmvN95xsk RJr6q+wkTFG+ng5v2bdCbVVFfA4 2011-02-21 00:25:32 195.43.157.85 443 0 # s Exit Fast Guard Named Running Stable Valid # w Bandwidth=2540 # p accept 20-23,43,53,79-81,88,110,143,194,443 - + nsLines = nsEntry.split("\n") - + firstLineComp = nsLines[0].split(" ") if len(firstLineComp) >= 9: _, nickname, _, _, pubDate, pubTime, _, orPort, dirPort = firstLineComp[:9] else: nickname, pubDate, pubTime, orPort, dirPort = "", "", "", "", "" - + flags = "unknown" if len(nsLines) >= 2 and nsLines[1].startswith("s "): flags = nsLines[1][2:] - + exitPolicy = conn.getRelayExitPolicy(fingerprint) - + if exitPolicy: policyLabel = exitPolicy.summary() else: policyLabel = "unknown" - + dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel) lines[3] = "published: %s %s" % (pubTime, pubDate) lines[4] = "flags: %s" % flags.replace(" ", ", ") lines[5] = "exit policy: %s" % policyLabel - + if descEntry: torVersion, platform, contact = "", "", "" - + for descLine in descEntry.split("\n"): if descLine.startswith("platform"): # has the tor version and platform, ex: # platform Tor 0.2.1.29 (r318f470bc5f2ad43) on Linux x86_64 - + torVersion = descLine[13:descLine.find(" ", 13)] platform = descLine[descLine.rfind(" on ") + 4:] elif descLine.startswith("contact"): contact = descLine[8:] - + # clears up some highly common obscuring for alias in (" at ", " AT "): contact = contact.replace(alias, "@") for alias in (" dot ", " DOT "): contact = contact.replace(alias, ".") - + break # contact lines come after the platform - + lines[3] = "%-35s os: %-14s version: %s" % (lines[3], platform, torVersion) - + # contact information is an optional field if contact: lines[6] = "contact: %s" % contact else: allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True) - + if allMatches: # multiple matches lines[2] = "Multiple matches, possible fingerprints are:" - + for i in range(len(allMatches)): isLastLine = i == 3 - + relayPort, relayFingerprint = allMatches[i] lineText = "%i. or port: %-5s fingerprint: %s" % (i, relayPort, relayFingerprint) - + # if there's multiple lines remaining at the end then give a count remainingRelays = len(allMatches) - i if isLastLine and remainingRelays > 1: lineText = "... %i more" % remainingRelays - + lines[3 + i] = lineText - + if isLastLine: break else: # no consensus entry for this ip address lines[2] = "No consensus data found" - + # crops any lines that are too long for i in range(len(lines)): lines[i] = uiTools.cropStr(lines[i], width - 2) - + return lines - + def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False): """ Provides a short description of the destination. This is made up of two components, the base <ip addr>:<port> and an extra piece of information in parentheses. The IP address is scrubbed from private connections. - + Extra information is... - the port's purpose for exit connections - the locale and/or hostname if set to do so, the address isn't private, and isn't on the local network - nothing otherwise - + Arguments: maxLength - maximum length of the string returned includeLocale - possibly includes the locale includeHostname - possibly includes the hostname """ - + # the port and port derived data can be hidden by config or without includePort includePort = self.includePort and (CONFIG["features.connection.showExitPort"] or self.getType() != Category.EXIT) - + # destination of the connection ipLabel = "<scrubbed>" if self.isPrivate() else self.foreign.getIpAddr() portLabel = ":%s" % self.foreign.getPort() if includePort else "" dstAddress = ipLabel + portLabel - + # Only append the extra info if there's at least a couple characters of # space (this is what's needed for the country codes). if len(dstAddress) + 5 <= maxLength: spaceAvailable = maxLength - len(dstAddress) - 3 - + if self.getType() == Category.EXIT and includePort: purpose = connections.getPortUsage(self.foreign.getPort()) - + if purpose: # BitTorrent is a common protocol to truncate, so just use "Torrent" # if there's not enough room. if len(purpose) > spaceAvailable and purpose == "BitTorrent": purpose = "Torrent" - + # crops with a hyphen if too long purpose = uiTools.cropStr(purpose, spaceAvailable, endType = uiTools.Ending.HYPHEN) - + dstAddress += " (%s)" % purpose elif not connections.isIpAddressPrivate(self.foreign.getIpAddr()): extraInfo = [] conn = torTools.getConn() - + if includeLocale and not conn.isGeoipUnavailable(): foreignLocale = self.foreign.getLocale("??") extraInfo.append(foreignLocale) spaceAvailable -= len(foreignLocale) + 2 - + if includeHostname: dstHostname = self.foreign.getHostname() - + if dstHostname: # determines the full space available, taking into account the ", " # dividers if there's multiple pieces of extra data - + maxHostnameSpace = spaceAvailable - 2 * len(extraInfo) dstHostname = uiTools.cropStr(dstHostname, maxHostnameSpace) extraInfo.append(dstHostname) spaceAvailable -= len(dstHostname) - + if extraInfo: dstAddress += " (%s)" % ", ".join(extraInfo) - + return dstAddress[:maxLength]
diff --git a/arm/connections/connPanel.py b/arm/connections/connPanel.py index 6dec45a..7d7864e 100644 --- a/arm/connections/connPanel.py +++ b/arm/connections/connPanel.py @@ -45,176 +45,176 @@ class ConnectionPanel(panel.Panel, threading.Thread): Listing of connections tor is making, with information correlated against the current consensus and other data sources. """ - + def __init__(self, stdscr): panel.Panel.__init__(self, stdscr, "connections", 0) threading.Thread.__init__(self) self.setDaemon(True) - + # 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 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._scroller = uiTools.Scroller(True) self._title = "Connections:" # title line of the panel self._entries = [] # last fetched display entries self._entryLines = [] # individual lines rendered from the entries listing self._showDetails = False # presents the details panel if true - + self._lastUpdate = -1 # time the content was last revised self._isTorRunning = True # indicates if tor is currently running or not self._haltTime = None # time when tor was stopped self._halt = False # terminates thread if true self._cond = threading.Condition() # used for pausing the thread self.valsLock = threading.RLock() - + # Tracks exiting port and client country statistics self._clientLocaleUsage = {} self._exitPortUsage = {} - + # If we're a bridge and been running over a day then prepopulates with the # last day's clients. - + conn = torTools.getConn() bridgeClients = conn.getInfo("status/clients-seen", None) - + if bridgeClients: # Response has a couple arguments... # TimeStarted="2011-08-17 15:50:49" CountrySummary=us=16,de=8,uk=8 - + countrySummary = None for arg in bridgeClients.split(): if arg.startswith("CountrySummary="): countrySummary = arg[15:] break - + if countrySummary: for entry in countrySummary.split(","): if re.match("^..=[0-9]+$", entry): locale, count = entry.split("=", 1) self._clientLocaleUsage[locale] = int(count) - + # Last sampling received from the ConnectionResolver, used to detect when # it changes. self._lastResourceFetch = -1 - + # resolver for the command/pid associated with SOCKS, HIDDEN, and CONTROL connections self._appResolver = connections.AppResolver("arm") - + # rate limits appResolver queries to once per update self.appResolveSinceUpdate = False - + # mark the initially exitsing connection uptimes as being estimates for entry in self._entries: if isinstance(entry, connEntry.ConnectionEntry): entry.getLines()[0].isInitialConnection = True - + # listens for when tor stops so we know to stop reflecting changes conn.addStatusListener(self.torStateListener) - + def torStateListener(self, controller, eventType, _): """ Freezes the connection contents when Tor stops. """ - + self._isTorRunning = eventType in (State.INIT, State.RESET) - + if self._isTorRunning: self._haltTime = None else: self._haltTime = time.time() - + self.redraw(True) - + def getPauseTime(self): """ Provides the time Tor stopped if it isn't running. Otherwise this is the time we were last paused. """ - + if self._haltTime: return self._haltTime else: return panel.Panel.getPauseTime(self) - + def setSortOrder(self, ordering = None): """ Sets the connection attributes we're sorting by and resorts the contents. - + Arguments: ordering - new ordering, if undefined then this resorts with the last set ordering """ - + self.valsLock.acquire() - + 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: self._entryLines += entry.getLines() self.valsLock.release() - + def getListingType(self): """ Provides the priority content we list connections by. """ - + return CONFIG["features.connection.listingType"] - + def setListingType(self, listingType): """ Sets the priority information presented by the panel. - + Arguments: listingType - Listing instance for the primary information to be shown """ - + if self.getListingType() == listingType: return - + self.valsLock.acquire() - + armConf = conf.get_config("arm") armConf.set("features.connection.listingType", Listing.keys()[Listing.index_of(listingType)]) - + # if we're sorting by the listing then we need to resort if entries.SortAttr.LISTING in CONFIG["features.connection.order"]: self.setSortOrder() - + self.valsLock.release() - + def isClientsAllowed(self): """ True if client connections are permissable, false otherwise. """ - + conn = torTools.getConn() return "Guard" in conn.getMyFlags([]) or conn.getOption("BridgeRelay", None) == "1" - + def isExitsAllowed(self): """ True if exit connections are permissable, false otherwise. """ - + if not torTools.getConn().getOption("ORPort", None): return False # no ORPort - + policy = torTools.getConn().getExitPolicy() return policy and policy.is_exiting_allowed() - + def showSortDialog(self): """ Provides the sort dialog for our connections. """ - + # set ordering for connection options titleLabel = "Connection Ordering:" options = list(entries.SortAttr) @@ -222,10 +222,10 @@ class ConnectionPanel(panel.Panel, threading.Thread): optionColors = dict([(attr, entries.SORT_COLORS[attr]) for attr in options]) results = arm.popups.showSortDialog(titleLabel, options, oldSelection, optionColors) if results: self.setSortOrder(results) - + def handleKey(self, key): self.valsLock.acquire() - + isKeystrokeConsumed = True if uiTools.isScrollKey(key): pageHeight = self.getPreferredSize()[0] - 1 @@ -242,13 +242,13 @@ class ConnectionPanel(panel.Panel, threading.Thread): title = "Resolver Util:" options = ["auto"] + list(connections.Resolver) connResolver = connections.getResolver("tor") - + currentOverwrite = connResolver.overwriteResolver if currentOverwrite == None: oldSelection = 0 else: oldSelection = options.index(currentOverwrite) - + selection = arm.popups.showMenu(title, options, oldSelection) - + # applies new setting if selection != -1: selectedOption = options[selection] if selection != 0 else None @@ -257,13 +257,13 @@ class ConnectionPanel(panel.Panel, threading.Thread): # provides a menu to pick the primary information we list connections by title = "List By:" options = list(entries.ListingType) - + # dropping the HOSTNAME listing type until we support displaying that content options.remove(arm.connections.entries.ListingType.HOSTNAME) - + oldSelection = options.index(self.getListingType()) selection = arm.popups.showMenu(title, options, oldSelection) - + # applies new setting if selection != -1: self.setListingType(options[selection]) elif key == ord('d') or key == ord('D'): @@ -274,17 +274,17 @@ class ConnectionPanel(panel.Panel, threading.Thread): elif (key == ord('e') or key == ord('E')) and self.isExitsAllowed(): countPopup.showCountDialog(countPopup.CountType.EXIT_PORT, self._exitPortUsage) else: isKeystrokeConsumed = False - + self.valsLock.release() return isKeystrokeConsumed - + def run(self): """ Keeps connections listing updated, checking for new entries at a set rate. """ - + lastDraw = time.time() - 1 - + # Fetches out initial connection results. The wait is so this doesn't # run during arm's interface initialization (otherwise there's a # noticeable pause before the first redraw). @@ -293,10 +293,10 @@ class ConnectionPanel(panel.Panel, threading.Thread): self._cond.release() self._update() # populates initial entries self._resolveApps(False) # resolves initial applications - + while not self._halt: currentTime = time.time() - + 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) @@ -305,16 +305,16 @@ class ConnectionPanel(panel.Panel, threading.Thread): # updates content if their's new results, otherwise just redraws self._update() self.redraw(True) - + # 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) / CONFIG["features.connection.refreshRate"] lastDraw += CONFIG["features.connection.refreshRate"] * drawTicks - + def getHelp(self): resolverUtil = connections.getResolver("tor").overwriteResolver if resolverUtil == None: resolverUtil = "auto" - + options = [] options.append(("up arrow", "scroll up a line", None)) options.append(("down arrow", "scroll down a line", None)) @@ -322,141 +322,141 @@ class ConnectionPanel(panel.Panel, threading.Thread): options.append(("page down", "scroll down a page", None)) options.append(("enter", "show connection details", None)) options.append(("d", "raw consensus descriptor", None)) - + if self.isClientsAllowed(): options.append(("c", "client locale usage summary", None)) - + if self.isExitsAllowed(): options.append(("e", "exit port usage summary", None)) - + options.append(("l", "listed identity", self.getListingType().lower())) options.append(("s", "sort ordering", None)) options.append(("u", "resolving utility", resolverUtil)) return options - + def getSelection(self): """ Provides the currently selected connection entry. """ - + return self._scroller.getCursorSelection(self._entryLines) - + def draw(self, width, height): self.valsLock.acquire() - + # if we don't have any contents then refuse to show details if not self._entries: self._showDetails = False - + # extra line when showing the detail panel is for the bottom border detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0 isScrollbarVisible = len(self._entryLines) > height - detailPanelOffset - 1 - + scrollLoc = self._scroller.getScrollLoc(self._entryLines, height - detailPanelOffset - 1) cursorSelection = self.getSelection() - + # draws the detail panel if currently displaying it if self._showDetails and cursorSelection: # This is a solid border unless the scrollbar is visible, in which case a # 'T' pipe connects the border to the bar. uiTools.drawBox(self, 0, 0, width, DETAILS_HEIGHT + 2) if isScrollbarVisible: self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE) - + drawEntries = cursorSelection.getDetails(width) for i in range(min(len(drawEntries), DETAILS_HEIGHT)): self.addstr(1 + i, 2, drawEntries[i][0], drawEntries[i][1]) - + # title label with connection counts if self.isTitleVisible(): title = "Connection Details:" if self._showDetails else self._title self.addstr(0, 0, title, curses.A_STANDOUT) - + scrollOffset = 0 if isScrollbarVisible: scrollOffset = 2 self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._entryLines), 1 + detailPanelOffset) - + if self.isPaused() or not self._isTorRunning: currentTime = self.getPauseTime() else: currentTime = time.time() - + for lineNum in range(scrollLoc, len(self._entryLines)): entryLine = self._entryLines[lineNum] - + # if this is an unresolved SOCKS, HIDDEN, or CONTROL entry then queue up # resolution for the applicaitions they belong to if isinstance(entryLine, connEntry.ConnectionLine) and entryLine.isUnresolvedApp(): self._resolveApps() - + # hilighting if this is the selected line extraFormat = curses.A_STANDOUT if entryLine == cursorSelection else curses.A_NORMAL - + drawLine = lineNum + detailPanelOffset + 1 - scrollLoc - + prefix = entryLine.getListingPrefix() for i in range(len(prefix)): self.addch(drawLine, scrollOffset + i, prefix[i]) - + xOffset = scrollOffset + len(prefix) drawEntry = entryLine.getListingEntry(width - scrollOffset - len(prefix), currentTime, self.getListingType()) - + for msg, attr in drawEntry: attr |= extraFormat self.addstr(drawLine, xOffset, msg, attr) xOffset += len(msg) - + if drawLine >= height: break - + self.valsLock.release() - + def stop(self): """ Halts further resolutions and terminates the thread. """ - + self._cond.acquire() self._halt = True self._cond.notifyAll() self._cond.release() - + def _update(self): """ Fetches the newest resolved connections. """ - + self.appResolveSinceUpdate = False - + # if we don't have an initialized resolver then this is a no-op if not connections.isResolverAlive("tor"): return - + connResolver = connections.getResolver("tor") currentResolutionCount = connResolver.getResolutionCount() - + self.valsLock.acquire() - + newEntries = [] # the new results we'll display - + # Fetches new connections and client circuits... # newConnections [(local ip, local port, foreign ip, foreign port)...] # newCircuits {circuitID => (status, purpose, path)...} - + newConnections = connResolver.getConnections() newCircuits = {} - + for circuitID, status, purpose, path in torTools.getConn().getCircuits(): # Skips established single-hop circuits (these are for directory # fetches, not client circuits) if not (status == "BUILT" and len(path) == 1): newCircuits[circuitID] = (status, purpose, path) - + # Populates newEntries with any of our old entries that still exist. # This is both for performance and to keep from resetting the uptime # attributes. Note that CircEntries are a ConnectionEntry subclass so # we need to check for them first. - + for oldEntry in self._entries: if isinstance(oldEntry, circEntry.CircEntry): newEntry = newCircuits.get(oldEntry.circuitID) - + if newEntry: oldEntry.update(newEntry[0], newEntry[2]) newEntries.append(oldEntry) @@ -465,42 +465,42 @@ class ConnectionPanel(panel.Panel, threading.Thread): connLine = oldEntry.getLines()[0] connAttr = (connLine.local.getIpAddr(), connLine.local.getPort(), connLine.foreign.getIpAddr(), connLine.foreign.getPort()) - + if connAttr in newConnections: newEntries.append(oldEntry) newConnections.remove(connAttr) - + # Reset any display attributes for the entries we're keeping for entry in newEntries: entry.resetDisplay() - + # Adds any new connection and circuit entries. for lIp, lPort, fIp, fPort in newConnections: newConnEntry = connEntry.ConnectionEntry(lIp, lPort, fIp, fPort) newConnLine = newConnEntry.getLines()[0] - + if newConnLine.getType() != connEntry.Category.CIRCUIT: newEntries.append(newConnEntry) - + # updates exit port and client locale usage information if newConnLine.isPrivate(): if newConnLine.getType() == connEntry.Category.INBOUND: # client connection, update locale information clientLocale = newConnLine.foreign.getLocale() - + if clientLocale: self._clientLocaleUsage[clientLocale] = self._clientLocaleUsage.get(clientLocale, 0) + 1 elif newConnLine.getType() == connEntry.Category.EXIT: exitPort = newConnLine.foreign.getPort() self._exitPortUsage[exitPort] = self._exitPortUsage.get(exitPort, 0) + 1 - + for circuitID in newCircuits: status, purpose, path = newCircuits[circuitID] newEntries.append(circEntry.CircEntry(circuitID, status, purpose, path)) - + # Counts the relays in each of the categories. This also flushes the # type cache for all of the connections (in case its changed since last # fetched). - + categoryTypes = list(connEntry.Category) typeCounts = dict((type, 0) for type in categoryTypes) for entry in newEntries: @@ -508,53 +508,53 @@ class ConnectionPanel(panel.Panel, threading.Thread): typeCounts[entry.getLines()[0].getType()] += 1 elif isinstance(entry, circEntry.CircEntry): typeCounts[connEntry.Category.CIRCUIT] += 1 - + # makes labels for all the categories with connections (ie, # "21 outbound", "1 control", etc) countLabels = [] - + for category in categoryTypes: if typeCounts[category] > 0: countLabels.append("%i %s" % (typeCounts[category], category.lower())) - + if countLabels: self._title = "Connections (%s):" % ", ".join(countLabels) else: self._title = "Connections:" - + self._entries = newEntries - + self._entryLines = [] for entry in self._entries: self._entryLines += entry.getLines() - + self.setSortOrder() self._lastResourceFetch = currentResolutionCount self.valsLock.release() - + def _resolveApps(self, flagQuery = True): """ Triggers an asynchronous query for all unresolved SOCKS, HIDDEN, and CONTROL entries. - + Arguments: flagQuery - sets a flag to prevent further call from being respected until the next update if true """ - + 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 appPorts = [] - + for line in unresolvedLines: appConn = line.local if line.getType() == connEntry.Category.HIDDEN else line.foreign appPorts.append(appConn.getPort()) - + # Queue up resolution for the unresolved ports (skips if it's still working # on the last query). if appPorts and not self._appResolver.isResolving: self._appResolver.resolve(appPorts) - + # Fetches results. If the query finishes quickly then this is what we just # asked for, otherwise these belong to an earlier resolution. # @@ -562,26 +562,26 @@ class ConnectionPanel(panel.Panel, threading.Thread): # the lsof lookups aren't working on this platform or lacks permissions). # The isAppResolving flag lets the unresolved entries indicate if there's # a lookup in progress for them or not. - + appResults = self._appResolver.getResults(0.2) - + for line in unresolvedLines: isLocal = line.getType() == connEntry.Category.HIDDEN linePort = line.local.getPort() if isLocal else line.foreign.getPort() - + if linePort in appResults: # sets application attributes if there's a result with this as the # inbound port for inboundPort, outboundPort, cmd, pid in appResults[linePort]: appPort = outboundPort if isLocal else inboundPort - + if linePort == appPort: line.appName = cmd line.appPid = pid line.isAppResolving = False else: line.isAppResolving = self._appResolver.isResolving - + if flagQuery: self.appResolveSinceUpdate = True
diff --git a/arm/connections/countPopup.py b/arm/connections/countPopup.py index 34a779e..0c6d14a 100644 --- a/arm/connections/countPopup.py +++ b/arm/connections/countPopup.py @@ -19,26 +19,26 @@ def showCountDialog(countType, counts): """ Provides a dialog with bar graphs and percentages for the given set of counts. Pressing any key closes the dialog. - + Arguments: countType - type of counts being presented counts - mapping of labels to counts """ - + isNoStats = not counts noStatsMsg = "Usage stats aren't available yet, press any key..." - + if isNoStats: popup, width, height = arm.popups.init(3, len(noStatsMsg) + 4) else: popup, width, height = arm.popups.init(4 + max(1, len(counts)), 80) if not popup: return - + try: control = arm.controller.getController() - + popup.win.box() - + # dialog title if countType == CountType.CLIENT_LOCALE: title = "Client Locales" @@ -47,55 +47,55 @@ def showCountDialog(countType, counts): else: title = "" log.warn("Unrecognized count type: %s" % countType) - + popup.addstr(0, 0, title, curses.A_STANDOUT) - + if isNoStats: popup.addstr(1, 2, noStatsMsg, curses.A_BOLD | uiTools.getColor("cyan")) else: sortedCounts = sorted(counts.iteritems(), key=operator.itemgetter(1)) sortedCounts.reverse() - + # constructs string formatting for the max key and value display width keyWidth, valWidth, valueTotal = 3, 1, 0 for k, v in sortedCounts: keyWidth = max(keyWidth, len(k)) valWidth = max(valWidth, len(str(v))) valueTotal += v - + # extra space since we're adding usage informaion if countType == CountType.EXIT_PORT: keyWidth += EXIT_USAGE_WIDTH - + labelFormat = "%%-%is %%%ii (%%%%%%-2i)" % (keyWidth, valWidth) - + for i in range(height - 4): k, v = sortedCounts[i] - + # includes a port usage column if countType == CountType.EXIT_PORT: usage = connections.getPortUsage(k) - + if usage: keyFormat = "%%-%is %%s" % (keyWidth - EXIT_USAGE_WIDTH) k = keyFormat % (k, usage[:EXIT_USAGE_WIDTH - 3]) - + label = labelFormat % (k, v, v * 100 / valueTotal) popup.addstr(i + 1, 2, label, curses.A_BOLD | uiTools.getColor("green")) - + # All labels have the same size since they're based on the max widths. # If this changes then this'll need to be the max label width. labelWidth = len(label) - + # draws simple bar graph for percentages fillWidth = v * (width - 4 - labelWidth) / valueTotal for j in range(fillWidth): popup.addstr(i + 1, 3 + labelWidth + j, " ", curses.A_STANDOUT | uiTools.getColor("red")) - + popup.addstr(height - 2, 2, "Press any key...") - + popup.win.refresh() - + curses.cbreak() control.getScreen().getch() finally: arm.popups.finalize() diff --git a/arm/connections/descriptorPopup.py b/arm/connections/descriptorPopup.py index a156280..d4ab918 100644 --- a/arm/connections/descriptorPopup.py +++ b/arm/connections/descriptorPopup.py @@ -28,46 +28,46 @@ def showDescriptorPopup(connPanel): Up, Down, Page Up, Page Down - scroll descriptor Right, Left - next / previous connection Enter, Space, d, D - close popup - + Arguments: connPanel - connection panel providing the dialog """ - + # hides the title of the connection panel connPanel.setTitleVisible(False) connPanel.redraw(True) - + control = arm.controller.getController() panel.CURSES_LOCK.acquire() isDone = False - + try: while not isDone: selection = connPanel.getSelection() if not selection: break - + fingerprint = selection.foreign.getFingerprint() if fingerprint == "UNKNOWN": fingerprint = None - + displayText = getDisplayText(fingerprint) displayColor = arm.connections.connEntry.CATEGORY_COLOR[selection.getType()] showLineNumber = fingerprint != None - + # determines the maximum popup size the displayText can fill pHeight, pWidth = getPreferredSize(displayText, connPanel.maxX, showLineNumber) - + popup, _, height = arm.popups.init(pHeight, pWidth) if not popup: break scroll, isChanged = 0, True - + try: while not isDone: if isChanged: draw(popup, fingerprint, displayText, displayColor, scroll, showLineNumber) isChanged = False - + key = control.getScreen().getch() - + if uiTools.isScrollKey(key): # TODO: This is a bit buggy in that scrolling is by displayText # lines rather than the displayed lines, causing issues when @@ -76,9 +76,9 @@ def showDescriptorPopup(connPanel): # displayed. However, trying to correct this introduces a big can # of worms and after hours decided that this isn't worth the # effort... - + newScroll = uiTools.getScrollPosition(key, scroll, height - 2, len(displayText)) - + if scroll != newScroll: scroll, isChanged = newScroll, True elif uiTools.isSelectionKey(key) or key in (ord('d'), ord('D')): @@ -98,29 +98,29 @@ def getDisplayText(fingerprint): Provides the descriptor and consensus entry for a relay. This is a list of lines to be displayed by the dialog. """ - + if not fingerprint: return [UNRESOLVED_MSG] conn, description = torTools.getConn(), [] - + description.append("ns/id/%s" % fingerprint) consensusEntry = conn.getConsensusEntry(fingerprint) - + if consensusEntry: description += consensusEntry.split("\n") else: description += [ERROR_MSG, ""] - + description.append("desc/id/%s" % fingerprint) descriptorEntry = conn.getDescriptorEntry(fingerprint) - + if descriptorEntry: description += descriptorEntry.split("\n") else: description += [ERROR_MSG] - + return description
def getPreferredSize(text, maxWidth, showLineNumber): """ Provides the (height, width) tuple for the preferred size of the given text. """ - + width, height = 0, len(text) + 2 lineNumWidth = int(math.log10(len(text))) + 1 for line in text: @@ -128,48 +128,48 @@ def getPreferredSize(text, maxWidth, showLineNumber): lineWidth = len(line) + 5 if showLineNumber: lineWidth += lineNumWidth width = max(width, lineWidth) - + # tracks number of extra lines that will be taken due to text wrap height += (lineWidth - 2) / maxWidth - + return (height, width)
def draw(popup, fingerprint, displayText, displayColor, scroll, showLineNumber): popup.win.erase() popup.win.box() xOffset = 2 - + if fingerprint: title = "Consensus Descriptor (%s):" % fingerprint else: title = "Consensus Descriptor:" popup.addstr(0, 0, title, curses.A_STANDOUT) - + lineNumWidth = int(math.log10(len(displayText))) + 1 isEncryptionBlock = False # flag indicating if we're currently displaying a key - + # checks if first line is in an encryption block for i in range(0, scroll): lineText = displayText[i].strip() if lineText in SIG_START_KEYS: isEncryptionBlock = True elif lineText in SIG_END_KEYS: isEncryptionBlock = False - + drawLine, pageHeight = 1, popup.maxY - 2 for i in range(scroll, scroll + pageHeight): lineText = displayText[i].strip() xOffset = 2 - + if showLineNumber: lineNumLabel = ("%%%ii" % lineNumWidth) % (i + 1) lineNumFormat = curses.A_BOLD | uiTools.getColor(LINE_NUM_COLOR) - + popup.addstr(drawLine, xOffset, lineNumLabel, lineNumFormat) xOffset += lineNumWidth + 1 - + # Most consensus and descriptor lines are keyword/value pairs. Both are # shown with the same color, but the keyword is bolded. - + keyword, value = lineText, "" drawFormat = uiTools.getColor(displayColor) - + if lineText.startswith(HEADER_PREFIX[0]) or lineText.startswith(HEADER_PREFIX[1]): keyword, value = lineText, "" drawFormat = uiTools.getColor(HEADER_COLOR) @@ -189,41 +189,41 @@ def draw(popup, fingerprint, displayText, displayColor, scroll, showLineNumber): elif " " in lineText: divIndex = lineText.find(" ") keyword, value = lineText[:divIndex], lineText[divIndex:] - + displayQueue = [(keyword, drawFormat | curses.A_BOLD), (value, drawFormat)] cursorLoc = xOffset - + while displayQueue: msg, format = displayQueue.pop(0) if not msg: continue - + maxMsgSize = popup.maxX - 1 - cursorLoc if len(msg) >= maxMsgSize: # needs to split up the line msg, remainder = uiTools.cropStr(msg, maxMsgSize, None, endType = None, getRemainder = True) - + if xOffset == cursorLoc and msg == "": # first word is longer than the line msg = uiTools.cropStr(remainder, maxMsgSize) - + if " " in remainder: remainder = remainder.split(" ", 1)[1] else: remainder = "" - + popup.addstr(drawLine, cursorLoc, msg, format) cursorLoc = xOffset - + if remainder: displayQueue.insert(0, (remainder.strip(), format)) drawLine += 1 else: popup.addstr(drawLine, cursorLoc, msg, format) cursorLoc += len(msg) - + if drawLine > pageHeight: break - + drawLine += 1 if drawLine > pageHeight: break - + popup.win.refresh()
diff --git a/arm/connections/entries.py b/arm/connections/entries.py index d5085aa..bf319d8 100644 --- a/arm/connections/entries.py +++ b/arm/connections/entries.py @@ -27,53 +27,53 @@ class ConnectionPanelEntry: in the panel listing. This caches results until the display indicates that they should be flushed. """ - + def __init__(self): self.lines = [] self.flushCache = True - + def getLines(self): """ Provides the individual lines in the connection listing. """ - + if self.flushCache: self.lines = self._getLines(self.lines) self.flushCache = False - + return self.lines - + def _getLines(self, oldResults): # implementation of getLines - + for line in oldResults: line.resetDisplay() - + return oldResults - + def getSortValues(self, sortAttrs, listingType): """ Provides the value used in comparisons to sort based on the given attribute. - + Arguments: sortAttrs - list of SortAttr values for the field being sorted on listingType - ListingType enumeration for the attribute we're listing entries by """ - + return [self.getSortValue(attr, listingType) for attr in sortAttrs] - + def getSortValue(self, attr, listingType): """ Provides the value of a single attribute used for sorting purposes. - + Arguments: attr - list of SortAttr values for the field being sorted on listingType - ListingType enumeration for the attribute we're listing entries by """ - + if attr == SortAttr.LISTING: if listingType == ListingType.IP_ADDRESS: # uses the IP address as the primary value, and port as secondary @@ -86,44 +86,44 @@ class ConnectionPanelEntry: return self.getSortValue(SortAttr.FINGERPRINT, listingType) elif listingType == ListingType.NICKNAME: return self.getSortValue(SortAttr.NICKNAME, listingType) - + return "" - + def resetDisplay(self): """ Flushes cached display results. """ - + self.flushCache = True
class ConnectionPanelLine: """ Individual line in the connection panel listing. """ - + def __init__(self): # cache for displayed information self._listingCache = None self._listingCacheArgs = (None, None) - + self._detailsCache = None self._detailsCacheArgs = None - + self._descriptorCache = None self._descriptorCacheArgs = None - + def getListingPrefix(self): """ Provides a list of characters to be appended before the listing entry. """ - + return () - + def getListingEntry(self, width, currentTime, listingType): """ Provides a [(msg, attr)...] tuple list for contents to be displayed in the connection panel listing. - + Arguments: width - available space to display in currentTime - unix timestamp for what the results should consider to be @@ -131,41 +131,41 @@ class ConnectionPanelLine: listingType - ListingType enumeration for the highest priority content to be displayed """ - + if self._listingCacheArgs != (width, listingType): self._listingCache = self._getListingEntry(width, currentTime, listingType) self._listingCacheArgs = (width, listingType) - + return self._listingCache - + def _getListingEntry(self, width, currentTime, listingType): # implementation of getListingEntry return None - + def getDetails(self, width): """ Provides a list of [(msg, attr)...] tuple listings with detailed information for this connection. - + Arguments: width - available space to display in """ - + if self._detailsCacheArgs != width: self._detailsCache = self._getDetails(width) self._detailsCacheArgs = width - + return self._detailsCache - + def _getDetails(self, width): # implementation of getDetails return [] - + def resetDisplay(self): """ Flushes cached display results. """ - + self._listingCacheArgs = (None, None) self._detailsCacheArgs = None
diff --git a/arm/controller.py b/arm/controller.py index a72c634..2fa0027 100644 --- a/arm/controller.py +++ b/arm/controller.py @@ -60,52 +60,52 @@ def getController(): """ Provides the arm controller instance. """ - + return ARM_CONTROLLER
def initController(stdscr, startTime): """ Spawns the controller, and related panels for it. - + Arguments: stdscr - curses window """ - + global ARM_CONTROLLER - + # initializes the panels stickyPanels = [arm.headerPanel.HeaderPanel(stdscr, startTime), LabelPanel(stdscr)] pagePanels, firstPagePanels = [], [] - + # first page: graph and log if CONFIG["features.panels.show.graph"]: firstPagePanels.append(arm.graphing.graphPanel.GraphPanel(stdscr)) - + if CONFIG["features.panels.show.log"]: expandedEvents = arm.logPanel.expandEvents(CONFIG["startup.events"]) firstPagePanels.append(arm.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([arm.connections.connPanel.ConnectionPanel(stdscr)]) - + # third page: config if CONFIG["features.panels.show.config"]: pagePanels.append([arm.configPanel.ConfigPanel(stdscr, arm.configPanel.State.TOR)]) - + # fourth page: torrc if CONFIG["features.panels.show.torrc"]: pagePanels.append([arm.torrcPanel.TorrcPanel(stdscr, arm.torrcPanel.Config.TORRC)]) - + # initializes the controller ARM_CONTROLLER = Controller(stdscr, stickyPanels, pagePanels) - + # additional configuration for the graph panel graphPanel = ARM_CONTROLLER.getPanel("graph") - + if graphPanel: # statistical monitors for graph bwStats = arm.graphing.bandwidthStats.BandwidthStats() @@ -113,13 +113,13 @@ def initController(stdscr, startTime): graphPanel.addStats(GraphStat.SYSTEM_RESOURCES, arm.graphing.resourceStats.ResourceStats()) if not CONFIG["startup.blindModeEnabled"]: graphPanel.addStats(GraphStat.CONNECTIONS, arm.graphing.connStats.ConnStats()) - + # sets graph based on config parameter try: initialStats = GRAPH_INIT_STATS.get(CONFIG["features.graph.type"]) graphPanel.setStats(initialStats) except ValueError: pass # invalid stats, maybe connections when in blind mode - + # prepopulates bandwidth values from state file if CONFIG["features.graph.bw.prepopulate"] and torTools.getConn().isAlive(): isSuccessful = bwStats.prepopulateFromState() @@ -129,25 +129,25 @@ class LabelPanel(panel.Panel): """ Panel that just displays a single line of text. """ - + def __init__(self, stdscr): panel.Panel.__init__(self, stdscr, "msg", 0, height=1) self.msgText = "" self.msgAttr = curses.A_NORMAL - + def setMessage(self, msg, attr = None): """ Sets the message being displayed by the panel. - + Arguments: msg - string to be displayed attr - attribute for the label, normal text if undefined """ - + if attr == None: attr = curses.A_NORMAL self.msgText = msg self.msgAttr = attr - + def draw(self, width, height): self.addstr(0, 0, self.msgText, self.msgAttr)
@@ -155,18 +155,18 @@ class Controller: """ Tracks the global state of the interface """ - + def __init__(self, stdscr, stickyPanels, pagePanels): """ Creates a new controller instance. Panel lists are ordered as they appear, top to bottom on the page. - + Arguments: stdscr - curses window stickyPanels - panels shown at the top of each page pagePanels - list of pages, each being a list of the panels on it """ - + self._screen = stdscr self._stickyPanels = stickyPanels self._pagePanels = pagePanels @@ -176,208 +176,208 @@ class Controller: self._isDone = False self._lastDrawn = 0 self.setMsg() # initializes our control message - + def getScreen(self): """ Provides our curses window. """ - + return self._screen - + def getPageCount(self): """ Provides the number of pages the interface has. This may be zero if all page panels have been disabled. """ - + return len(self._pagePanels) - + def getPage(self): """ Provides the number belonging to this page. Page numbers start at zero. """ - + return self._page - + def setPage(self, pageNumber): """ Sets the selected page, raising a ValueError if the page number is invalid. - + Arguments: pageNumber - page number to be selected """ - + if pageNumber < 0 or pageNumber >= self.getPageCount(): raise ValueError("Invalid page number: %i" % pageNumber) - + if pageNumber != self._page: self._page = pageNumber self._forceRedraw = True self.setMsg() - + def nextPage(self): """ Increments the page number. """ - + self.setPage((self._page + 1) % len(self._pagePanels)) - + def prevPage(self): """ Decrements the page number. """ - + self.setPage((self._page - 1) % len(self._pagePanels)) - + def isPaused(self): """ True if the interface is paused, false otherwise. """ - + return self._isPaused - + def setPaused(self, isPause): """ Sets the interface to be paused or unpaused. """ - + if isPause != self._isPaused: self._isPaused = isPause self._forceRedraw = True self.setMsg() - + for panelImpl in self.getAllPanels(): panelImpl.setPaused(isPause) - + def getPanel(self, name): """ Provides the panel with the given identifier. This returns None if no such panel exists. - + Arguments: name - name of the panel to be fetched """ - + for panelImpl in self.getAllPanels(): if panelImpl.getName() == name: return panelImpl - + return None - + def getStickyPanels(self): """ Provides the panels visibile at the top of every page. """ - + return list(self._stickyPanels) - + def getDisplayPanels(self, pageNumber = None, includeSticky = True): """ Provides all panels belonging to a page and sticky content above it. This is ordered they way they are presented (top to bottom) on the page. - + Arguments: pageNumber - page number of the panels to be returned, the current page if None includeSticky - includes sticky panels in the results if true """ - + returnPage = self._page if pageNumber == None else pageNumber - + if self._pagePanels: if includeSticky: return self._stickyPanels + self._pagePanels[returnPage] else: return list(self._pagePanels[returnPage]) else: return self._stickyPanels if includeSticky else [] - + def getDaemonPanels(self): """ Provides thread panels. """ - + threadPanels = [] for panelImpl in self.getAllPanels(): if isinstance(panelImpl, threading.Thread): threadPanels.append(panelImpl) - + return threadPanels - + def getAllPanels(self): """ Provides all panels in the interface. """ - + allPanels = list(self._stickyPanels) - + for page in self._pagePanels: allPanels += list(page) - + return allPanels - + def redraw(self, force = True): """ Redraws the displayed panel content. - + Arguments: force - redraws reguardless of if it's needed if true, otherwise ignores the request when there arne't changes to be displayed """ - + force |= self._forceRedraw self._forceRedraw = False - + currentTime = time.time() if CONFIG["features.refreshRate"] != 0: if self._lastDrawn + CONFIG["features.refreshRate"] <= currentTime: force = True - + displayPanels = self.getDisplayPanels() - + occupiedContent = 0 for panelImpl in displayPanels: panelImpl.setTop(occupiedContent) occupiedContent += panelImpl.getHeight() - + # apparently curses may cache display contents unless we explicitely # request a redraw here... # https://trac.torproject.org/projects/tor/ticket/2830#comment:9 if force: self._screen.clear() - + for panelImpl in displayPanels: panelImpl.redraw(force) - + if force: self._lastDrawn = currentTime - + def requestRedraw(self): """ Requests that all content is redrawn when the interface is next rendered. """ - + self._forceRedraw = True - + def getLastRedrawTime(self): """ Provides the time when the content was last redrawn, zero if the content has never been drawn. """ - + return self._lastDrawn - + def setMsg(self, msg = None, attr = None, redraw = False): """ Sets the message displayed in the interfaces control panel. This uses our default prompt if no arguments are provided. - + Arguments: msg - string to be displayed attr - attribute for the label, normal text if undefined redraw - redraws right away if true, otherwise redraws when display content is next normally drawn """ - + if msg == None: msg = "" - + if attr == None: if not self._isPaused: msg = "page %i / %i - m: menu, p: pause, h: page help, q: quit" % (self._page + 1, len(self._pagePanels)) @@ -385,52 +385,52 @@ class Controller: else: msg = "Paused" attr = curses.A_STANDOUT - + controlPanel = self.getPanel("msg") controlPanel.setMessage(msg, attr) - + if redraw: controlPanel.redraw(True) else: self._forceRedraw = True - + def getDataDirectory(self): """ Provides the path where arm's resources are being placed. The path ends with a slash and is created if it doesn't already exist. """ - + dataDir = os.path.expanduser(CONFIG["startup.dataDirectory"]) if not dataDir.endswith("/"): dataDir += "/" if not os.path.exists(dataDir): os.makedirs(dataDir) return dataDir - + def isDone(self): """ True if arm should be terminated, false otherwise. """ - + return self._isDone - + def quit(self): """ Terminates arm after the input is processed. Optionally if we're connected to a arm generated tor instance then this may check if that should be shut down too. """ - + self._isDone = True - + # check if the torrc has a "ARM_SHUTDOWN" comment flag, if so then shut # down the instance - + isShutdownFlagPresent = False torrcContents = torConfig.getTorrc().getContents() - + if torrcContents: for line in torrcContents: if "# ARM_SHUTDOWN" in line: isShutdownFlagPresent = True break - + if isShutdownFlagPresent: try: torTools.getConn().shutdown() except IOError, exc: arm.popups.showMsg(str(exc), 3, curses.A_BOLD) @@ -439,20 +439,20 @@ def shutdownDaemons(): """ Stops and joins on worker threads. """ - + # prevents further worker threads from being spawned torTools.NO_SPAWN = True - + # stops panel daemons control = getController()
if control: for panelImpl in control.getDaemonPanels(): panelImpl.stop() for panelImpl in control.getDaemonPanels(): panelImpl.join() - + # joins on stem threads torTools.getConn().close() - + # joins on utility daemon threads - this might take a moment since the # internal threadpools being joined might be sleeping hostnames.stop() @@ -466,11 +466,11 @@ def shutdownDaemons(): def heartbeatCheck(isUnresponsive): """ Logs if its been ten seconds since the last BW event. - + Arguments: isUnresponsive - flag for if we've indicated to be responsive or not """ - + conn = torTools.getConn() lastHeartbeat = conn.controller.get_latest_heartbeat() if conn.isAlive(): @@ -481,7 +481,7 @@ def heartbeatCheck(isUnresponsive): # really shouldn't happen (meant Tor froze for a bit) isUnresponsive = False log.notice("Relay resumed") - + return isUnresponsive
def connResetListener(controller, eventType, _): @@ -489,19 +489,19 @@ def connResetListener(controller, eventType, _): Pauses connection resolution when tor's shut down, and resumes with the new pid if started again. """ - + if connections.isResolverAlive("tor"): resolver = connections.getResolver("tor") resolver.setPaused(eventType == State.CLOSED) - + if eventType in (State.INIT, State.RESET): # Reload the torrc contents. If the torrc panel is present then it will # do this instead since it wants to do validation and redraw _after_ the # new contents are loaded. - + if getController().getPanel("torrc") == None: torConfig.getTorrc().load(True) - + try: resolver.setPid(controller.get_pid()) except ValueError: @@ -510,63 +510,63 @@ def connResetListener(controller, eventType, _): def startTorMonitor(startTime): """ Initializes the interface and starts the main draw loop. - + Arguments: startTime - unix time for when arm was started """ - + # attempts to fetch the tor pid, warning if unsuccessful (this is needed for # checking its resource usage, among other things) conn = torTools.getConn() torPid = conn.controller.get_pid(None) - + if not torPid and conn.isAlive(): log.warn("Unable to determine Tor's pid. Some information, like its resource usage will be unavailable.") - + # adds events needed for arm functionality to the torTools REQ_EVENTS # mapping (they're then included with any setControllerEvents call, and log # a more helpful error if unavailable) - + torTools.REQ_EVENTS["BW"] = "bandwidth graph won't function" - + if not CONFIG["startup.blindModeEnabled"]: # The DisableDebuggerAttachment will prevent our connection panel from really # functioning. It'll have circuits, but little else. If this is the case then # notify the user and tell them what they can do to fix it. - + if conn.getOption("DisableDebuggerAttachment", None) == "1": log.notice("Tor is preventing system utilities like netstat and lsof from working. This means that arm can't provide you with connection information. You can change this by adding 'DisableDebuggerAttachment 0' to your torrc and restarting tor. For more information see...\nhttps://trac.torproject.org/3313") connections.getResolver("tor").setPaused(True) else: torTools.REQ_EVENTS["CIRC"] = "may cause issues in identifying client connections" - + # Configures connection resoultions. This is paused/unpaused according to # if Tor's connected or not. conn.addStatusListener(connResetListener) - + if torPid: # use the tor pid to help narrow connection results torCmdName = system.get_name_by_pid(torPid) - + if torCmdName is None: torCmdName = "tor" - + connections.getResolver(torCmdName, torPid, "tor") else: # constructs singleton resolver and, if tor isn't connected, initizes # it to be paused connections.getResolver("tor").setPaused(not conn.isAlive()) - + # hack to display a better (arm specific) notice if all resolvers fail connections.RESOLVER_FINAL_FAILURE_MSG = "We were unable to use any of your system's resolvers to get tor's connections. This is fine, but means that the connections page will be empty. This is usually permissions related so if you would like to fix this then run arm with the same user as tor (ie, "sudo -u <tor user> arm")." - + # provides a notice about any event types tor supports but arm doesn't missingEventTypes = arm.logPanel.getMissingEventTypes() - + if missingEventTypes: pluralLabel = "s" if len(missingEventTypes) > 1 else "" log.info("arm doesn't recognize the following event type%s: %s (log 'UNKNOWN' events to see them)" % (pluralLabel, ", ".join(missingEventTypes))) - + try: curses.wrapper(drawTorMonitor, startTime) except UnboundLocalError, exc: @@ -583,63 +583,63 @@ def startTorMonitor(startTime): # (which would leave the user's terminal in a screwed up state). There is # still a tiny timing issue here (after the exception but before the flag # is set) but I've never seen it happen in practice. - + panel.HALT_ACTIVITY = True shutdownDaemons()
def drawTorMonitor(stdscr, startTime): """ Main draw loop context. - + Arguments: stdscr - curses window startTime - unix time for when arm was started """ - + initController(stdscr, startTime) control = getController() - + # provides notice about any unused config keys for key in conf.get_config("arm").unused_keys(): log.notice("Unused configuration entry: %s" % key) - + # tells daemon panels to start for panelImpl in control.getDaemonPanels(): panelImpl.start() - + # allows for background transparency try: curses.use_default_colors() except curses.error: pass - + # makes the cursor invisible try: curses.curs_set(0) except curses.error: pass - + # logs the initialization time log.info("arm started (initialization took %0.3f seconds)" % (time.time() - startTime)) - + # main draw loop overrideKey = None # uses this rather than waiting on user input isUnresponsive = False # flag for heartbeat responsiveness check - + while not control.isDone(): displayPanels = control.getDisplayPanels() isUnresponsive = heartbeatCheck(isUnresponsive) - + # sets panel visability for panelImpl in control.getAllPanels(): panelImpl.setVisible(panelImpl in displayPanels) - + # redraws the interface if it's needed control.redraw(False) stdscr.refresh() - + # wait for user keyboard input until timeout, unless an override was set if overrideKey: key, overrideKey = overrideKey, None else: curses.halfdelay(CONFIG["features.redrawRate"] * 10) key = stdscr.getch() - + if key == curses.KEY_RIGHT: control.nextPage() elif key == curses.KEY_LEFT: @@ -655,13 +655,13 @@ def drawTorMonitor(stdscr, startTime): confirmationKey = arm.popups.showMsg(msg, attr = curses.A_BOLD) quitConfirmed = confirmationKey in (ord('q'), ord('Q')) else: quitConfirmed = True - + if quitConfirmed: control.quit() elif key == ord('x') or key == ord('X'): # provides prompt to confirm that arm should issue a sighup msg = "This will reset Tor's internal state. Are you sure (x again to confirm)?" confirmationKey = arm.popups.showMsg(msg, attr = curses.A_BOLD) - + if confirmationKey in (ord('x'), ord('X')): try: torTools.getConn().reload() except IOError, exc: @@ -675,6 +675,6 @@ def drawTorMonitor(stdscr, startTime): for panelImpl in displayPanels: isKeystrokeConsumed = panelImpl.handleKey(key) if isKeystrokeConsumed: break - + shutdownDaemons()
diff --git a/arm/graphing/bandwidthStats.py b/arm/graphing/bandwidthStats.py index 9d05c72..e074716 100644 --- a/arm/graphing/bandwidthStats.py +++ b/arm/graphing/bandwidthStats.py @@ -41,85 +41,85 @@ class BandwidthStats(graphPanel.GraphStats): """ Uses tor BW events to generate bandwidth usage graph. """ - + def __init__(self, isPauseBuffer=False): graphPanel.GraphStats.__init__(self) - + # stats prepopulated from tor's state file self.prepopulatePrimaryTotal = 0 self.prepopulateSecondaryTotal = 0 self.prepopulateTicks = 0 - + # accounting data (set by _updateAccountingInfo method) self.accountingLastUpdated = 0 self.accountingInfo = dict([(arg, "") for arg in ACCOUNTING_ARGS]) - + # listens for tor reload (sighup) events which can reset the bandwidth # rate/burst and if tor's using accounting conn = torTools.getConn() self._titleStats, self.isAccounting = [], False if not isPauseBuffer: self.resetListener(conn.getController(), State.INIT, None) # initializes values conn.addStatusListener(self.resetListener) - + # Initialized the bandwidth totals to the values reported by Tor. This # uses a controller options introduced in ticket 2345: # https://trac.torproject.org/projects/tor/ticket/2345 - # + # # further updates are still handled via BW events to avoid unnecessary # GETINFO requests. - + self.initialPrimaryTotal = 0 self.initialSecondaryTotal = 0 - + readTotal = conn.getInfo("traffic/read", None) if readTotal and readTotal.isdigit(): self.initialPrimaryTotal = int(readTotal) / 1024 # Bytes -> KB - + writeTotal = conn.getInfo("traffic/written", None) if writeTotal and writeTotal.isdigit(): self.initialSecondaryTotal = int(writeTotal) / 1024 # Bytes -> KB - + def clone(self, newCopy=None): if not newCopy: newCopy = BandwidthStats(True) newCopy.accountingLastUpdated = self.accountingLastUpdated newCopy.accountingInfo = self.accountingInfo - + # attributes that would have been initialized from calling the resetListener newCopy.isAccounting = self.isAccounting newCopy._titleStats = self._titleStats - + return graphPanel.GraphStats.clone(self, newCopy) - + def resetListener(self, controller, eventType, _): # updates title parameters and accounting status if they changed self._titleStats = [] # force reset of title self.new_desc_event(None) # updates title params - + if eventType in (State.INIT, State.RESET) and CONFIG["features.graph.bw.accounting.show"]: isAccountingEnabled = controller.get_info('accounting/enabled', None) == '1' - + if isAccountingEnabled != self.isAccounting: self.isAccounting = isAccountingEnabled - + # redraws the whole screen since our height changed arm.controller.getController().redraw() - + # redraws to reflect changes (this especially noticeable when we have # accounting and shut down since it then gives notice of the shutdown) if self._graphPanel and self.isSelected: self._graphPanel.redraw(True) - + def prepopulateFromState(self): """ Attempts to use tor's state file to prepopulate values for the 15 minute interval via the BWHistoryReadValues/BWHistoryWriteValues values. This returns True if successful and False otherwise. """ - + # checks that this is a relay (if ORPort is unset, then skip) conn = torTools.getConn() orPort = conn.getOption("ORPort", None) if orPort == "0": return - + # gets the uptime (using the same parameters as the header panel to take # advantage of caching) # TODO: stem dropped system caching support so we'll need to think of @@ -130,11 +130,11 @@ class BandwidthStats(graphPanel.GraphStats): queryParam = ["%cpu", "rss", "%mem", "etime"] queryCmd = "ps -p %s -o %s" % (queryPid, ",".join(queryParam)) psCall = system.call(queryCmd, None) - + if psCall and len(psCall) == 2: stats = psCall[1].strip().split() if len(stats) == 4: uptime = stats[3] - + # checks if tor has been running for at least a day, the reason being that # the state tracks a day's worth of data and this should only prepopulate # results associated with this tor instance @@ -142,38 +142,38 @@ class BandwidthStats(graphPanel.GraphStats): msg = PREPOPULATE_FAILURE_MSG % "insufficient uptime" log.notice(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.notice(msg) return False - + # attempt to open the state file try: stateFile = open("%s%s/state" % (torTools.get_chroot(), dataDir), "r") except IOError: msg = PREPOPULATE_FAILURE_MSG % "unable to read the state file" log.notice(msg) return False - + # get the BWHistory entries (ordered oldest to newest) and number of # intervals since last recorded bwReadEntries, bwWriteEntries = None, None missingReadEntries, missingWriteEntries = None, None - + # converts from gmt to local with respect to DST tz_offset = time.altzone if time.localtime()[8] else time.timezone - + for line in stateFile: line = line.strip() - + # According to the rep_hist_update_state() function the BWHistory*Ends # correspond to the start of the following sampling period. Also, the # most recent values of BWHistory*Values appear to be an incremental # counter for the current sampling period. Hence, offsets are added to # account for both. - + if line.startswith("BWHistoryReadValues"): bwReadEntries = line[20:].split(",") bwReadEntries = [int(entry) / 1024.0 / 900 for entry in bwReadEntries] @@ -190,131 +190,131 @@ class BandwidthStats(graphPanel.GraphStats): lastWriteTime = time.mktime(time.strptime(line[19:], "%Y-%m-%d %H:%M:%S")) - tz_offset lastWriteTime -= 900 missingWriteEntries = int((time.time() - lastWriteTime) / 900) - + if not bwReadEntries or not bwWriteEntries or not lastReadTime or not lastWriteTime: msg = PREPOPULATE_FAILURE_MSG % "bandwidth stats missing from state file" log.notice(msg) return False - + # fills missing entries with the last value bwReadEntries += [bwReadEntries[-1]] * missingReadEntries bwWriteEntries += [bwWriteEntries[-1]] * missingWriteEntries - + # crops starting entries so they're the same size entryCount = min(len(bwReadEntries), len(bwWriteEntries), self.maxCol) bwReadEntries = bwReadEntries[len(bwReadEntries) - entryCount:] bwWriteEntries = bwWriteEntries[len(bwWriteEntries) - entryCount:] - + # gets index for 15-minute interval intervalIndex = 0 for indexEntry in graphPanel.UPDATE_INTERVALS: if indexEntry[1] == 900: break else: intervalIndex += 1 - + # fills the graphing parameters with state information for i in range(entryCount): readVal, writeVal = bwReadEntries[i], bwWriteEntries[i] - + self.lastPrimary, self.lastSecondary = readVal, writeVal - + self.prepopulatePrimaryTotal += readVal * 900 self.prepopulateSecondaryTotal += writeVal * 900 self.prepopulateTicks += 900 - + self.primaryCounts[intervalIndex].insert(0, readVal) self.secondaryCounts[intervalIndex].insert(0, writeVal) - + self.maxPrimary[intervalIndex] = max(self.primaryCounts) self.maxSecondary[intervalIndex] = max(self.secondaryCounts) del self.primaryCounts[intervalIndex][self.maxCol + 1:] del self.secondaryCounts[intervalIndex][self.maxCol + 1:] - + msg = PREPOPULATE_SUCCESS_MSG missingSec = time.time() - min(lastReadTime, lastWriteTime) if missingSec: msg += " (%s is missing)" % str_tools.get_time_label(missingSec, 0, True) log.notice(msg) - + return True - + def bandwidth_event(self, event): if self.isAccounting and self.isNextTickRedraw(): if time.time() - self.accountingLastUpdated >= CONFIG["features.graph.bw.accounting.rate"]: self._updateAccountingInfo() - + # scales units from B to KB for graphing self._processEvent(event.read / 1024.0, event.written / 1024.0) - + def draw(self, panel, width, height): # line of the graph's x-axis labeling labelingLine = graphPanel.GraphStats.getContentHeight(self) + panel.graphHeight - 2 - + # if display is narrow, overwrites x-axis labels with avg / total stats if width <= COLLAPSE_WIDTH: # clears line panel.addstr(labelingLine, 0, " " * width) graphCol = min((width - 10) / 2, self.maxCol) - + primaryFooter = "%s, %s" % (self._getAvgLabel(True), self._getTotalLabel(True)) secondaryFooter = "%s, %s" % (self._getAvgLabel(False), self._getTotalLabel(False)) - + panel.addstr(labelingLine, 1, primaryFooter, uiTools.getColor(self.getColor(True))) panel.addstr(labelingLine, graphCol + 6, secondaryFooter, uiTools.getColor(self.getColor(False))) - + # provides accounting stats if enabled if self.isAccounting: if torTools.getConn().isAlive(): status = self.accountingInfo["status"] - + hibernateColor = "green" if status == "soft": hibernateColor = "yellow" elif status == "hard": hibernateColor = "red" elif status == "": # failed to be queried status, hibernateColor = "unknown", "red" - + panel.addstr(labelingLine + 2, 0, "Accounting (", curses.A_BOLD) panel.addstr(labelingLine + 2, 12, status, curses.A_BOLD | uiTools.getColor(hibernateColor)) panel.addstr(labelingLine + 2, 12 + len(status), ")", curses.A_BOLD) - + resetTime = self.accountingInfo["resetTime"] if not resetTime: resetTime = "unknown" panel.addstr(labelingLine + 2, 35, "Time to reset: %s" % resetTime) - + used, total = self.accountingInfo["read"], self.accountingInfo["readLimit"] if used and total: panel.addstr(labelingLine + 3, 2, "%s / %s" % (used, total), uiTools.getColor(self.getColor(True))) - + used, total = self.accountingInfo["written"], self.accountingInfo["writtenLimit"] if used and total: panel.addstr(labelingLine + 3, 37, "%s / %s" % (used, total), uiTools.getColor(self.getColor(False))) else: panel.addstr(labelingLine + 2, 0, "Accounting:", curses.A_BOLD) panel.addstr(labelingLine + 2, 12, "Connection Closed...") - + def getTitle(self, width): stats = list(self._titleStats) - + while True: if not stats: return "Bandwidth:" else: label = "Bandwidth (%s):" % ", ".join(stats) - + if len(label) > width: del stats[-1] else: return label - + def getHeaderLabel(self, width, isPrimary): graphType = "Download" if isPrimary else "Upload" stats = [""] - + # if wide then avg and total are part of the header, otherwise they're on # the x-axis if width * 2 > COLLAPSE_WIDTH: stats = [""] * 3 stats[1] = "- %s" % self._getAvgLabel(isPrimary) stats[2] = ", %s" % self._getTotalLabel(isPrimary) - + stats[0] = "%-14s" % ("%s/sec" % str_tools.get_size_label((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() + "):" while len(labeling) >= width: @@ -324,21 +324,21 @@ class BandwidthStats(graphPanel.GraphStats): else: labeling = graphType + ":" break - + return labeling - + def getColor(self, isPrimary): return DL_COLOR if isPrimary else UL_COLOR - + def getContentHeight(self): baseHeight = graphPanel.GraphStats.getContentHeight(self) return baseHeight + 3 if self.isAccounting else baseHeight - + def new_desc_event(self, event): # updates self._titleStats with updated values conn = torTools.getConn() if not conn.isAlive(): return # keep old values - + myFingerprint = conn.getInfo("fingerprint", None) if not self._titleStats or not myFingerprint or (event and myFingerprint in event.idlist): stats = [] @@ -347,19 +347,19 @@ class BandwidthStats(graphPanel.GraphStats): bwObserved = conn.getMyBandwidthObserved() bwMeasured = conn.getMyBandwidthMeasured() labelInBytes = CONFIG["features.graph.bw.transferInBytes"] - + if bwRate and bwBurst: bwRateLabel = str_tools.get_size_label(bwRate, 1, False, labelInBytes) bwBurstLabel = str_tools.get_size_label(bwBurst, 1, False, labelInBytes) - + # if both are using rounded values then strip off the ".0" decimal if ".0" in bwRateLabel and ".0" in bwBurstLabel: bwRateLabel = bwRateLabel.replace(".0", "") bwBurstLabel = bwBurstLabel.replace(".0", "") - + stats.append("limit: %s/s" % bwRateLabel) stats.append("burst: %s/s" % bwBurstLabel) - + # Provide the observed bandwidth either if the measured bandwidth isn't # available or if the measured bandwidth is the observed (this happens # if there isn't yet enough bandwidth measurements). @@ -367,38 +367,38 @@ class BandwidthStats(graphPanel.GraphStats): stats.append("observed: %s/s" % str_tools.get_size_label(bwObserved, 1, False, labelInBytes)) elif bwMeasured: stats.append("measured: %s/s" % str_tools.get_size_label(bwMeasured, 1, False, labelInBytes)) - + self._titleStats = stats - + def _getAvgLabel(self, isPrimary): total = self.primaryTotal if isPrimary else self.secondaryTotal total += self.prepopulatePrimaryTotal if isPrimary else self.prepopulateSecondaryTotal return "avg: %s/sec" % str_tools.get_size_label((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 total += self.initialPrimaryTotal if isPrimary else self.initialSecondaryTotal return "total: %s" % str_tools.get_size_label(total * 1024, 1) - + def _updateAccountingInfo(self): """ Updates mapping used for accounting info. This includes the following keys: status, resetTime, read, written, readLimit, writtenLimit - + Any failed lookups result in a mapping to an empty string. """ - + conn = torTools.getConn() queried = dict([(arg, "") for arg in ACCOUNTING_ARGS]) queried["status"] = conn.getInfo("accounting/hibernating", None) - + # provides a nicely formatted reset time endInterval = conn.getInfo("accounting/interval-end", None) if endInterval: # converts from gmt to local with respect to DST if time.localtime()[8]: tz_offset = time.altzone else: tz_offset = time.timezone - + sec = time.mktime(time.strptime(endInterval, "%Y-%m-%d %H:%M:%S")) - time.time() - tz_offset if CONFIG["features.graph.bw.accounting.isTimeLong"]: queried["resetTime"] = ", ".join(str_tools.get_time_labels(sec, True)) @@ -410,21 +410,21 @@ class BandwidthStats(graphPanel.GraphStats): minutes = sec / 60 sec %= 60 queried["resetTime"] = "%i:%02i:%02i:%02i" % (days, hours, minutes, sec) - + # number of bytes used and in total for the accounting period used = conn.getInfo("accounting/bytes", None) left = conn.getInfo("accounting/bytes-left", None) - + if used and left: usedComp, leftComp = used.split(" "), left.split(" ") read, written = int(usedComp[0]), int(usedComp[1]) readLeft, writtenLeft = int(leftComp[0]), int(leftComp[1]) - + queried["read"] = str_tools.get_size_label(read) queried["written"] = str_tools.get_size_label(written) queried["readLimit"] = str_tools.get_size_label(read + readLeft) queried["writtenLimit"] = str_tools.get_size_label(written + writtenLeft) - + self.accountingInfo = queried self.accountingLastUpdated = time.time()
diff --git a/arm/graphing/connStats.py b/arm/graphing/connStats.py index 69d3489..2b1b188 100644 --- a/arm/graphing/connStats.py +++ b/arm/graphing/connStats.py @@ -9,52 +9,52 @@ from stem.control import State
class ConnStats(graphPanel.GraphStats): """ - Tracks number of connections, counting client and directory connections as + Tracks number of connections, counting client and directory connections as outbound. Control connections are excluded from counts. """ - + def __init__(self): graphPanel.GraphStats.__init__(self) - + # listens for tor reload (sighup) events which can reset the ports tor uses conn = torTools.getConn() self.orPort, self.dirPort, self.controlPort = "0", "0", "0" self.resetListener(conn.getController(), State.INIT, None) # initialize port values conn.addStatusListener(self.resetListener) - + def clone(self, newCopy=None): if not newCopy: newCopy = ConnStats() return graphPanel.GraphStats.clone(self, newCopy) - + def resetListener(self, controller, eventType, _): if eventType in (State.INIT, State.RESET): self.orPort = controller.get_conf("ORPort", "0") self.dirPort = controller.get_conf("DirPort", "0") self.controlPort = controller.get_conf("ControlPort", "0") - + def eventTick(self): """ Fetches connection stats from cached information. """ - + inboundCount, outboundCount = 0, 0 - + for entry in connections.getResolver("tor").getConnections(): localPort = entry[1] if localPort in (self.orPort, self.dirPort): inboundCount += 1 elif localPort == self.controlPort: pass # control connection else: outboundCount += 1 - + self._processEvent(inboundCount, outboundCount) - + def getTitle(self, width): return "Connection Count:" - + def getHeaderLabel(self, width, isPrimary): avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick) if isPrimary: return "Inbound (%s, avg: %s):" % (self.lastPrimary, avg) else: return "Outbound (%s, avg: %s):" % (self.lastSecondary, avg) - + def getRefreshRate(self): return 5
diff --git a/arm/graphing/graphPanel.py b/arm/graphing/graphPanel.py index c95adda..50b755d 100644 --- a/arm/graphing/graphPanel.py +++ b/arm/graphing/graphPanel.py @@ -70,48 +70,48 @@ class GraphStats: graphed. Up to two graphs (a 'primary' and 'secondary') can be displayed at a time and timescale parameters use the labels defined in UPDATE_INTERVALS. """ - + def __init__(self): """ Initializes parameters needed to present a graph. """ - + # panel to be redrawn when updated (set when added to GraphPanel) self._graphPanel = None self.isSelected = False self.isPauseBuffer = False - + # tracked stats self.tick = 0 # number of processed events self.lastPrimary, self.lastSecondary = 0, 0 # most recent registered stats self.primaryTotal, self.secondaryTotal = 0, 0 # sum of all stats seen - + # timescale dependent stats self.maxCol = CONFIG["features.graph.maxWidth"] self.maxPrimary, self.maxSecondary = {}, {} self.primaryCounts, self.secondaryCounts = {}, {} - + for i in range(len(UPDATE_INTERVALS)): # recent rates for graph self.maxPrimary[i] = 0 self.maxSecondary[i] = 0 - + # historic stats for graph, first is accumulator # iterative insert needed to avoid making shallow copies (nasty, nasty gotcha) self.primaryCounts[i] = (self.maxCol + 1) * [0] self.secondaryCounts[i] = (self.maxCol + 1) * [0] - + # tracks BW events torTools.getConn().addEventListener(self.bandwidth_event, stem.control.EventType.BW) - + def clone(self, newCopy=None): """ Provides a deep copy of this instance. - + Arguments: newCopy - base instance to build copy off of """ - + if not newCopy: newCopy = GraphStats() newCopy.tick = self.tick newCopy.lastPrimary = self.lastPrimary @@ -124,110 +124,110 @@ class GraphStats: newCopy.secondaryCounts = copy.deepcopy(self.secondaryCounts) newCopy.isPauseBuffer = True return newCopy - + def eventTick(self): """ Called when it's time to process another event. All graphs use tor BW events to keep in sync with each other (this happens once a second). """ - + pass - + def isNextTickRedraw(self): """ Provides true if the following tick (call to _processEvent) will result in being redrawn. """ - + if self._graphPanel and self.isSelected and not self._graphPanel.isPaused(): # use the minimum of the current refresh rate and the panel's updateRate = UPDATE_INTERVALS[self._graphPanel.updateInterval][1] return (self.tick + 1) % min(updateRate, self.getRefreshRate()) == 0 else: return False - + def getTitle(self, width): """ Provides top label. """ - + return "" - + def getHeaderLabel(self, width, isPrimary): """ Provides labeling presented at the top of the graph. """ - + return "" - + def getColor(self, isPrimary): """ Provides the color to be used for the graph and stats. """ - + return DEFAULT_COLOR_PRIMARY if isPrimary else DEFAULT_COLOR_SECONDARY - + def getContentHeight(self): """ Provides the height content should take up (not including the graph). """ - + return DEFAULT_CONTENT_HEIGHT - + def getRefreshRate(self): """ Provides the number of ticks between when the stats have new values to be redrawn. """ - + return 1 - + def isVisible(self): """ True if the stat has content to present, false if it should be hidden. """ - + return True - + def draw(self, panel, width, height): """ Allows for any custom drawing monitor wishes to append. """ - + pass - + def bandwidth_event(self, event): if not self.isPauseBuffer: self.eventTick() - + def _processEvent(self, primary, secondary): """ Includes new stats in graphs and notifies associated GraphPanel of changes. """ - + isRedraw = self.isNextTickRedraw() - + self.lastPrimary, self.lastSecondary = primary, secondary self.primaryTotal += primary self.secondaryTotal += secondary - + # updates for all time intervals self.tick += 1 for i in range(len(UPDATE_INTERVALS)): lable, timescale = UPDATE_INTERVALS[i] - + self.primaryCounts[i][0] += primary self.secondaryCounts[i][0] += secondary - + if self.tick % timescale == 0: self.maxPrimary[i] = max(self.maxPrimary[i], self.primaryCounts[i][0] / timescale) self.primaryCounts[i][0] /= timescale self.primaryCounts[i].insert(0, 0) del self.primaryCounts[i][self.maxCol + 1:] - + self.maxSecondary[i] = max(self.maxSecondary[i], self.secondaryCounts[i][0] / timescale) self.secondaryCounts[i][0] /= timescale self.secondaryCounts[i].insert(0, 0) del self.secondaryCounts[i][self.maxCol + 1:] - + if isRedraw and self._graphPanel: self._graphPanel.redraw(True)
class GraphPanel(panel.Panel): @@ -235,7 +235,7 @@ class GraphPanel(panel.Panel): Panel displaying a graph, drawing statistics from custom GraphStats implementations. """ - + def __init__(self, stdscr): panel.Panel.__init__(self, stdscr, "graph", 0) self.updateInterval = CONFIG["features.graph.interval"] @@ -244,62 +244,62 @@ class GraphPanel(panel.Panel): self.currentDisplay = None # label of the stats currently being displayed self.stats = {} # available stats (mappings of label -> instance) self.setPauseAttr("stats") - + def getUpdateInterval(self): """ Provides the rate that we update the graph at. """ - + return self.updateInterval - + def setUpdateInterval(self, updateInterval): """ Sets the rate that we update the graph at. - + Arguments: updateInterval - update time enum """ - + self.updateInterval = updateInterval - + def getBoundsType(self): """ Provides the type of graph bounds used. """ - + return self.bounds - + def setBoundsType(self, boundsType): """ Sets the type of graph boundaries we use. - + Arguments: boundsType - graph bounds enum """ - + self.bounds = boundsType - + def getHeight(self): """ Provides the height requested by the currently displayed GraphStats (zero if hidden). """ - + if self.currentDisplay and self.stats[self.currentDisplay].isVisible(): return self.stats[self.currentDisplay].getContentHeight() + self.graphHeight else: return 0 - + def setGraphHeight(self, newGraphHeight): """ Sets the preferred height used for the graph (restricted to the MIN_GRAPH_HEIGHT minimum). - + Arguments: newGraphHeight - new height for the graph """ - + self.graphHeight = max(MIN_GRAPH_HEIGHT, newGraphHeight) - + def resizeGraph(self): """ Prompts for user input to resize the graph panel. Options include... @@ -307,9 +307,9 @@ class GraphPanel(panel.Panel): up arrow - shrink graph enter / space - set size """ - + control = arm.controller.getController() - + panel.CURSES_LOCK.acquire() try: while True: @@ -317,24 +317,24 @@ class GraphPanel(panel.Panel): control.setMsg(msg, curses.A_BOLD, True) curses.cbreak() key = control.getScreen().getch() - + if key == curses.KEY_DOWN: # don't grow the graph if it's already consuming the whole display # (plus an extra line for the graph/log gap) maxHeight = self.parent.getmaxyx()[0] - self.top currentHeight = self.getHeight() - + if currentHeight < maxHeight + 1: self.setGraphHeight(self.graphHeight + 1) elif key == curses.KEY_UP: self.setGraphHeight(self.graphHeight - 1) elif uiTools.isSelectionKey(key): break - + control.redraw() finally: control.setMsg() panel.CURSES_LOCK.release() - + def handleKey(self, key): isKeystrokeConsumed = True if key == ord('r') or key == ord('R'): @@ -347,19 +347,19 @@ class GraphPanel(panel.Panel): # provides a menu to pick the graphed stats availableStats = self.stats.keys() availableStats.sort() - + # uses sorted, camel cased labels for the options options = ["None"] for label in availableStats: words = label.split() options.append(" ".join(word[0].upper() + word[1:] for word in words)) - + if self.currentDisplay: initialSelection = availableStats.index(self.currentDisplay) + 1 else: initialSelection = 0 - + selection = arm.popups.showMenu("Graphed Stats:", options, initialSelection) - + # applies new setting if selection == 0: self.setStats(None) elif selection != -1: self.setStats(availableStats[selection - 1]) @@ -369,37 +369,37 @@ class GraphPanel(panel.Panel): selection = arm.popups.showMenu("Update Interval:", options, self.updateInterval) if selection != -1: self.updateInterval = selection else: isKeystrokeConsumed = False - + return isKeystrokeConsumed - + def getHelp(self): if self.currentDisplay: graphedStats = self.currentDisplay else: graphedStats = "none" - + options = [] options.append(("r", "resize graph", None)) options.append(("s", "graphed stats", graphedStats)) options.append(("b", "graph bounds", self.bounds.lower())) options.append(("i", "graph update interval", UPDATE_INTERVALS[self.updateInterval][0])) return options - + def draw(self, width, height): """ Redraws graph panel """ - + if self.currentDisplay: param = self.getAttr("stats")[self.currentDisplay] graphCol = min((width - 10) / 2, param.maxCol) - + primaryColor = uiTools.getColor(param.getColor(True)) secondaryColor = uiTools.getColor(param.getColor(False)) - + if self.isTitleVisible(): self.addstr(0, 0, param.getTitle(width), curses.A_STANDOUT) - + # top labels left, right = param.getHeaderLabel(width / 2, True), param.getHeaderLabel(width / 2, False) if left: self.addstr(1, 0, left, curses.A_BOLD | primaryColor) if right: self.addstr(1, graphCol + 5, right, curses.A_BOLD | secondaryColor) - + # determines max/min value on the graph if self.bounds == Bounds.GLOBAL_MAX: primaryMaxBound = int(param.maxPrimary[self.updateInterval]) @@ -412,60 +412,60 @@ class GraphPanel(panel.Panel): else: primaryMaxBound = int(max(param.primaryCounts[self.updateInterval][1:graphCol + 1])) secondaryMaxBound = int(max(param.secondaryCounts[self.updateInterval][1:graphCol + 1])) - + primaryMinBound = secondaryMinBound = 0 if self.bounds == Bounds.TIGHT: primaryMinBound = int(min(param.primaryCounts[self.updateInterval][1:graphCol + 1])) secondaryMinBound = int(min(param.secondaryCounts[self.updateInterval][1:graphCol + 1])) - + # if the max = min (ie, all values are the same) then use zero lower # bound so a graph is still displayed if primaryMinBound == primaryMaxBound: primaryMinBound = 0 if secondaryMinBound == secondaryMaxBound: secondaryMinBound = 0 - + # displays upper and lower bounds self.addstr(2, 0, "%4i" % primaryMaxBound, primaryColor) self.addstr(self.graphHeight + 1, 0, "%4i" % primaryMinBound, primaryColor) - + self.addstr(2, graphCol + 5, "%4i" % secondaryMaxBound, secondaryColor) self.addstr(self.graphHeight + 1, graphCol + 5, "%4i" % secondaryMinBound, secondaryColor) - + # displays intermediate bounds on every other row if CONFIG["features.graph.showIntermediateBounds"]: ticks = (self.graphHeight - 3) / 2 for i in range(ticks): row = self.graphHeight - (2 * i) - 3 if self.graphHeight % 2 == 0 and i >= (ticks / 2): row -= 1 - + if primaryMinBound != primaryMaxBound: primaryVal = (primaryMaxBound - primaryMinBound) * (self.graphHeight - row - 1) / (self.graphHeight - 1) if not primaryVal in (primaryMinBound, primaryMaxBound): self.addstr(row + 2, 0, "%4i" % primaryVal, primaryColor) - + if secondaryMinBound != secondaryMaxBound: secondaryVal = (secondaryMaxBound - secondaryMinBound) * (self.graphHeight - row - 1) / (self.graphHeight - 1) if not secondaryVal in (secondaryMinBound, secondaryMaxBound): self.addstr(row + 2, graphCol + 5, "%4i" % secondaryVal, secondaryColor) - + # creates bar graph (both primary and secondary) for col in range(graphCol): colCount = int(param.primaryCounts[self.updateInterval][col + 1]) - primaryMinBound colHeight = min(self.graphHeight, self.graphHeight * colCount / (max(1, primaryMaxBound) - primaryMinBound)) for row in range(colHeight): self.addstr(self.graphHeight + 1 - row, col + 5, " ", curses.A_STANDOUT | primaryColor) - + colCount = int(param.secondaryCounts[self.updateInterval][col + 1]) - secondaryMinBound colHeight = min(self.graphHeight, self.graphHeight * colCount / (max(1, secondaryMaxBound) - secondaryMinBound)) for row in range(colHeight): self.addstr(self.graphHeight + 1 - row, col + graphCol + 10, " ", curses.A_STANDOUT | secondaryColor) - + # bottom labeling of x-axis intervalSec = 1 # seconds per labeling for i in range(len(UPDATE_INTERVALS)): if i == self.updateInterval: intervalSec = UPDATE_INTERVALS[i][1] - + intervalSpacing = 10 if graphCol >= WIDE_LABELING_GRAPH_COL else 5 unitsLabel, decimalPrecision = None, 0 for i in range((graphCol - 4) / intervalSpacing): loc = (i + 1) * intervalSpacing timeLabel = str_tools.get_time_label(loc * intervalSec, decimalPrecision) - + if not unitsLabel: unitsLabel = timeLabel[-1] elif unitsLabel != timeLabel[-1]: # upped scale so also up precision of future measurements @@ -474,42 +474,42 @@ class GraphPanel(panel.Panel): else: # if constrained on space then strips labeling since already provided timeLabel = timeLabel[:-1] - + self.addstr(self.graphHeight + 2, 4 + loc, timeLabel, primaryColor) self.addstr(self.graphHeight + 2, graphCol + 10 + loc, timeLabel, secondaryColor) - + param.draw(self, width, height) # allows current stats to modify the display - + def addStats(self, label, stats): """ Makes GraphStats instance available in the panel. """ - + stats._graphPanel = self self.stats[label] = stats - + def getStats(self): """ Provides the currently selected stats label. """ - + return self.currentDisplay - + def setStats(self, label): """ Sets the currently displayed stats instance, hiding panel if None. """ - + if label != self.currentDisplay: if self.currentDisplay: self.stats[self.currentDisplay].isSelected = False - + if not label: self.currentDisplay = None elif label in self.stats.keys(): self.currentDisplay = label self.stats[self.currentDisplay].isSelected = True else: raise ValueError("Unrecognized stats label: %s" % label) - + def copyAttr(self, attr): if attr == "stats": # uses custom clone method to copy GraphStats instances diff --git a/arm/graphing/resourceStats.py b/arm/graphing/resourceStats.py index 80d23bc..d4d71c4 100644 --- a/arm/graphing/resourceStats.py +++ b/arm/graphing/resourceStats.py @@ -11,22 +11,22 @@ class ResourceStats(graphPanel.GraphStats): """ System resource usage tracker. """ - + def __init__(self): graphPanel.GraphStats.__init__(self) self.queryPid = torTools.getConn().controller.get_pid(None) - + def clone(self, newCopy=None): if not newCopy: newCopy = ResourceStats() return graphPanel.GraphStats.clone(self, newCopy) - + def getTitle(self, width): return "System Resources:" - + def getHeaderLabel(self, width, isPrimary): avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick) lastAmount = self.lastPrimary if isPrimary else self.lastSecondary - + if isPrimary: return "CPU (%0.1f%%, avg: %0.1f%%):" % (lastAmount, avg) else: @@ -34,20 +34,20 @@ class ResourceStats(graphPanel.GraphStats): usageLabel = str_tools.get_size_label(lastAmount * 1048576, 1) avgLabel = str_tools.get_size_label(avg * 1048576, 1) return "Memory (%s, avg: %s):" % (usageLabel, avgLabel) - + def eventTick(self): """ Fetch the cached measurement of resource usage from the ResourceTracker. """ - + primary, secondary = 0, 0 if self.queryPid: resourceTracker = sysTools.getResourceTracker(self.queryPid, True) - + if resourceTracker and not resourceTracker.lastQueryFailed(): primary, _, secondary, _ = resourceTracker.getResourceUsage() primary *= 100 # decimal percentage to whole numbers secondary /= 1048576 # translate size to MB so axis labels are short - + self._processEvent(primary, secondary)
diff --git a/arm/headerPanel.py b/arm/headerPanel.py index f494025..55f1727 100644 --- a/arm/headerPanel.py +++ b/arm/headerPanel.py @@ -42,7 +42,7 @@ FLAG_COLORS = {"Authority": "white", "BadExit": "red", "BadDirectory": "red "Stable": "blue", "Running": "yellow", "Unnamed": "magenta", "Valid": "green", "V2Dir": "cyan", "V3Dir": "white"}
-VERSION_STATUS_COLORS = {"new": "blue", "new in series": "blue", "obsolete": "red", "recommended": "green", +VERSION_STATUS_COLORS = {"new": "blue", "new in series": "blue", "obsolete": "red", "recommended": "green", "old": "red", "unrecommended": "red", "unknown": "cyan"}
CONFIG = conf.config_dict("arm", { @@ -59,78 +59,78 @@ class HeaderPanel(panel.Panel, threading.Thread): *fdUsed, fdLimit, isFdLimitEstimate sys/ hostname, os, version stat/ *%torCpu, *%armCpu, *rss, *%mem - + * volatile parameter that'll be reset on each update """ - + def __init__(self, stdscr, startTime): panel.Panel.__init__(self, stdscr, "header", 0) threading.Thread.__init__(self) self.setDaemon(True) - + self._isTorConnected = torTools.getConn().isAlive() self._lastUpdate = -1 # time the content was last revised self._halt = False # terminates thread if true self._cond = threading.Condition() # used for pausing the thread - + # Time when the panel was paused or tor was stopped. This is used to # freeze the uptime statistic (uptime increments normally when None). self._haltTime = None - + # The last arm cpu usage sampling taken. This is a tuple of the form: # (total arm cpu time, sampling timestamp) - # + # # The initial cpu total should be zero. However, at startup the cpu time # in practice is often greater than the real time causing the initially # reported cpu usage to be over 100% (which shouldn't be possible on # single core systems). - # + # # Setting the initial cpu total to the value at this panel's init tends to # give smoother results (staying in the same ballpark as the second # sampling) so fudging the numbers this way for now. - + self._armCpuSampling = (sum(os.times()[:3]), startTime) - + # Last sampling received from the ResourceTracker, used to detect when it # changes. self._lastResourceFetch = -1 - + # flag to indicate if we've already given file descriptor warnings self._isFdSixtyPercentWarned = False self._isFdNinetyPercentWarned = False - + self.vals = {} self.valsLock = threading.RLock() self._update(True) - + # listens for tor reload (sighup) events torTools.getConn().addStatusListener(self.resetListener) - + def getHeight(self): """ Provides the height of the content, which is dynamically determined by the panel's maximum width. """ - + isWide = self.getParent().getmaxyx()[1] >= MIN_DUAL_COL_WIDTH if self.vals["tor/orPort"]: return 4 if isWide else 6 else: return 3 if isWide else 4 - + def sendNewnym(self): """ Requests a new identity and provides a visual queue. """ - + torTools.getConn().sendNewnym() - + # If we're wide then the newnym label in this panel will give an # indication that the signal was sent. Otherwise use a msg. isWide = self.getParent().getmaxyx()[1] >= MIN_DUAL_COL_WIDTH if not isWide: arm.popups.showMsg("Requesting a new identity", 1) - + def handleKey(self, key): isKeystrokeConsumed = True - + if key in (ord('n'), ord('N')) and torTools.getConn().isNewnymAvailable(): self.sendNewnym() elif key in (ord('r'), ord('R')) and not self._isTorConnected: @@ -146,7 +146,7 @@ class HeaderPanel(panel.Panel, threading.Thread): # controller.authenticate() # except (IOError, stem.SocketError), exc: # controller = None - # + # # if not allowPortConnection: # arm.popups.showMsg("Unable to reconnect (%s)" % exc, 3) #elif not allowPortConnection: @@ -158,11 +158,11 @@ class HeaderPanel(panel.Panel, threading.Thread): # # methods. We can't use the starter.py's connection function directly # # due to password prompts, but we could certainly make this mess more # # manageable. - # + # # try: # ctlAddr, ctlPort = CONFIG["startup.interface.ipAddress"], CONFIG["startup.interface.port"] # controller = Controller.from_port(ctlAddr, ctlPort) - # + # # try: # controller.authenticate() # except stem.connection.MissingPassword: @@ -175,30 +175,30 @@ class HeaderPanel(panel.Panel, threading.Thread): # log.notice("Reconnected to Tor's control port") # arm.popups.showMsg("Tor reconnected", 1) else: isKeystrokeConsumed = False - + return isKeystrokeConsumed - + def draw(self, width, height): self.valsLock.acquire() isWide = width + 1 >= MIN_DUAL_COL_WIDTH - + # space available for content if isWide: leftWidth = max(width / 2, 77) rightWidth = width - leftWidth else: leftWidth = rightWidth = width - + # Line 1 / Line 1 Left (system and tor version information) sysNameLabel = "arm - %s" % self.vals["sys/hostname"] contentSpace = min(leftWidth, 40) - + if len(sysNameLabel) + 10 <= contentSpace: sysTypeLabel = "%s %s" % (self.vals["sys/os"], self.vals["sys/version"]) sysTypeLabel = uiTools.cropStr(sysTypeLabel, contentSpace - len(sysNameLabel) - 3, 4) self.addstr(0, 0, "%s (%s)" % (sysNameLabel, sysTypeLabel)) else: self.addstr(0, 0, uiTools.cropStr(sysNameLabel, contentSpace)) - + contentSpace = leftWidth - 43 if 7 + len(self.vals["tor/version"]) + len(self.vals["tor/versionStatus"]) <= contentSpace: if self.vals["tor/version"] != "Unknown": @@ -210,14 +210,14 @@ class HeaderPanel(panel.Panel, threading.Thread): self.addstr(0, 43 + len(labelPrefix) + len(self.vals["tor/versionStatus"]), ")") elif 11 <= contentSpace: self.addstr(0, 43, uiTools.cropStr("Tor %s" % self.vals["tor/version"], contentSpace, 4)) - + # Line 2 / Line 2 Left (tor ip/port information) x, includeControlPort = 0, True if self.vals["tor/orPort"]: myAddress = "Unknown" if self.vals["tor/orListenAddr"]: myAddress = self.vals["tor/orListenAddr"] elif self.vals["tor/address"]: myAddress = self.vals["tor/address"] - + # acting as a relay (we can assume certain parameters are set dirPortLabel = ", Dir Port: %s" % self.vals["tor/dirPort"] if self.vals["tor/dirPort"] != "0" else "" for label in (self.vals["tor/nickname"], " - " + myAddress, ":" + self.vals["tor/orPort"], dirPortLabel): @@ -232,16 +232,16 @@ class HeaderPanel(panel.Panel, threading.Thread): x += 17 else: statusTime = torTools.getConn().controller.get_latest_heartbeat() - + if statusTime: statusTimeLabel = time.strftime("%H:%M %m/%d/%Y, ", time.localtime(statusTime)) else: statusTimeLabel = "" # never connected to tor - + self.addstr(1, x, "Tor Disconnected", curses.A_BOLD | uiTools.getColor("red")) self.addstr(1, x + 16, " (%spress r to reconnect)" % statusTimeLabel) x += 39 + len(statusTimeLabel) includeControlPort = False - + if includeControlPort: if self.vals["tor/controlPort"] == "0": # connected via a control socket @@ -250,7 +250,7 @@ class HeaderPanel(panel.Panel, threading.Thread): if self.vals["tor/isAuthPassword"]: authType = "password" elif self.vals["tor/isAuthCookie"]: authType = "cookie" else: authType = "open" - + if x + 19 + len(self.vals["tor/controlPort"]) + len(authType) <= leftWidth: authColor = "red" if authType == "open" else "green" self.addstr(1, x, ", Control Port (") @@ -258,12 +258,12 @@ class HeaderPanel(panel.Panel, threading.Thread): self.addstr(1, x + 16 + len(authType), "): %s" % self.vals["tor/controlPort"]) elif x + 16 + len(self.vals["tor/controlPort"]) <= leftWidth: self.addstr(1, 0, ", Control Port: %s" % self.vals["tor/controlPort"]) - + # Line 3 / Line 1 Right (system usage info) y, x = (0, leftWidth) if isWide else (2, 0) if self.vals["stat/rss"] != "0": memoryLabel = str_tools.get_size_label(int(self.vals["stat/rss"])) else: memoryLabel = "0" - + uptimeLabel = "" if self.vals["tor/startTime"]: if self.isPaused() or not self._isTorConnected: @@ -271,31 +271,31 @@ class HeaderPanel(panel.Panel, threading.Thread): uptimeLabel = str_tools.get_short_time_label(self.getPauseTime() - self.vals["tor/startTime"]) else: uptimeLabel = str_tools.get_short_time_label(time.time() - self.vals["tor/startTime"]) - + sysFields = ((0, "cpu: %s%% tor, %s%% arm" % (self.vals["stat/%torCpu"], self.vals["stat/%armCpu"])), (27, "mem: %s (%s%%)" % (memoryLabel, self.vals["stat/%mem"])), (47, "pid: %s" % (self.vals["tor/pid"] if self._isTorConnected else "")), (59, "uptime: %s" % uptimeLabel)) - + for (start, label) in sysFields: if start + len(label) <= rightWidth: self.addstr(y, x + start, label) else: break - + if self.vals["tor/orPort"]: # Line 4 / Line 2 Right (fingerprint, and possibly file descriptor usage) y, x = (1, leftWidth) if isWide else (3, 0) - + fingerprintLabel = uiTools.cropStr("fingerprint: %s" % self.vals["tor/fingerprint"], width) self.addstr(y, x, fingerprintLabel) - + # if there's room and we're able to retrieve both the file descriptor # usage and limit then it might be presented if width - x - 59 >= 20 and self.vals["tor/fdUsed"] and self.vals["tor/fdLimit"]: # display file descriptor usage if we're either configured to do so or # running out - + fdPercent = 100 * self.vals["tor/fdUsed"] / self.vals["tor/fdLimit"] - + if fdPercent >= 60 or CONFIG["features.showFdUsage"]: fdPercentLabel, fdPercentFormat = "%i%%" % fdPercent, curses.A_NORMAL if fdPercent >= 95: @@ -304,28 +304,28 @@ class HeaderPanel(panel.Panel, threading.Thread): fdPercentFormat = uiTools.getColor("red") elif fdPercent >= 60: fdPercentFormat = uiTools.getColor("yellow") - + estimateChar = "?" if self.vals["tor/isFdLimitEstimate"] else "" baseLabel = "file desc: %i / %i%s (" % (self.vals["tor/fdUsed"], self.vals["tor/fdLimit"], estimateChar) - + self.addstr(y, x + 59, baseLabel) self.addstr(y, x + 59 + len(baseLabel), fdPercentLabel, fdPercentFormat) self.addstr(y, x + 59 + len(baseLabel) + len(fdPercentLabel), ")") - + # Line 5 / Line 3 Left (flags) if self._isTorConnected: y, x = (2 if isWide else 4, 0) self.addstr(y, x, "flags: ") x += 7 - + if len(self.vals["tor/flags"]) > 0: for i in range(len(self.vals["tor/flags"])): flag = self.vals["tor/flags"][i] flagColor = FLAG_COLORS[flag] if flag in FLAG_COLORS.keys() else "white" - + self.addstr(y, x, flag, curses.A_BOLD | uiTools.getColor(flagColor)) x += len(flag) - + if i < len(self.vals["tor/flags"]) - 1: self.addstr(y, x, ", ") x += 2 @@ -337,33 +337,33 @@ class HeaderPanel(panel.Panel, threading.Thread): statusTimeLabel = time.strftime("%H:%M %m/%d/%Y", time.localtime(statusTime)) self.addstr(y, 0, "Tor Disconnected", curses.A_BOLD | uiTools.getColor("red")) self.addstr(y, 16, " (%s) - press r to reconnect" % statusTimeLabel) - + # Undisplayed / Line 3 Right (exit policy) if isWide: exitPolicy = self.vals["tor/exitPolicy"] - + # adds note when default exit policy is appended if exitPolicy == "": exitPolicy = "<default>" elif not exitPolicy.endswith((" *:*", " *")): exitPolicy += ", <default>" - + self.addstr(2, leftWidth, "exit policy: ") x = leftWidth + 13 - + # color codes accepts to be green, rejects to be red, and default marker to be cyan isSimple = len(exitPolicy) > rightWidth - 13 policies = exitPolicy.split(", ") for i in range(len(policies)): policy = policies[i].strip() policyLabel = policy.replace("accept", "").replace("reject", "").strip() if isSimple else policy - + policyColor = "white" if policy.startswith("accept"): policyColor = "green" elif policy.startswith("reject"): policyColor = "red" elif policy.startswith("<default>"): policyColor = "cyan" - + self.addstr(2, x, policyLabel, curses.A_BOLD | uiTools.getColor(policyColor)) x += len(policyLabel) - + if i < len(policies) - 1: self.addstr(2, x, ", ") x += 2 @@ -372,34 +372,34 @@ class HeaderPanel(panel.Panel, threading.Thread): if isWide: conn = torTools.getConn() newnymWait = conn.getNewnymWait() - + msg = "press 'n' for a new identity" if newnymWait > 0: pluralLabel = "s" if newnymWait > 1 else "" msg = "building circuits, available again in %i second%s" % (newnymWait, pluralLabel) - + self.addstr(1, leftWidth, msg) - + self.valsLock.release() - + def getPauseTime(self): """ Provides the time Tor stopped if it isn't running. Otherwise this is the time we were last paused. """ - + if self._haltTime: return self._haltTime else: return panel.Panel.getPauseTime(self) - + def run(self): """ Keeps stats updated, checking for new information at a set rate. """ - + lastDraw = time.time() - 1 while not self._halt: currentTime = time.time() - + if self.isPaused() or currentTime - lastDraw < 1 or not self._isTorConnected: self._cond.acquire() if not self._halt: self._cond.wait(0.2) @@ -409,41 +409,41 @@ class HeaderPanel(panel.Panel, threading.Thread): # a new resource usage sampling (the most dynamic stat) or its been # twenty seconds since last fetched (so we still refresh occasionally # when resource fetches fail). - # + # # Otherwise, just redraw the panel to change the uptime field. - + isChanged = False if self.vals["tor/pid"]: resourceTracker = sysTools.getResourceTracker(self.vals["tor/pid"]) isChanged = self._lastResourceFetch != resourceTracker.getRunCount() - + if isChanged or currentTime - self._lastUpdate >= 20: self._update() - + self.redraw(True) lastDraw += 1 - + def stop(self): """ Halts further resolutions and terminates the thread. """ - + self._cond.acquire() self._halt = True self._cond.notifyAll() self._cond.release() - + def resetListener(self, controller, eventType, _): """ Updates static parameters on tor reload (sighup) events. """ - + if eventType in (State.INIT, State.RESET): initialHeight = self.getHeight() self._isTorConnected = True self._haltTime = None self._update(True) - + if self.getHeight() != initialHeight: # We're toggling between being a relay and client, causing the height # of this panel to change. Redraw all content so we don't get @@ -457,19 +457,19 @@ class HeaderPanel(panel.Panel, threading.Thread): self._haltTime = time.time() self._update() self.redraw(True) - + def _update(self, setStatic=False): """ Updates stats in the vals mapping. By default this just revises volatile attributes. - + Arguments: setStatic - resets all parameters, including relatively static values """ - + self.valsLock.acquire() conn = torTools.getConn() - + if setStatic: # version is truncated to first part, for instance: # 0.2.2.13-alpha (git-feb8c1b5f67f2c6f) -> 0.2.2.13-alpha @@ -482,10 +482,10 @@ class HeaderPanel(panel.Panel, threading.Thread): self.vals["tor/socketPath"] = conn.getOption("ControlSocket", "") self.vals["tor/isAuthPassword"] = conn.getOption("HashedControlPassword", None) != None self.vals["tor/isAuthCookie"] = conn.getOption("CookieAuthentication", None) == "1" - + # orport is reported as zero if unset if self.vals["tor/orPort"] == "0": self.vals["tor/orPort"] = "" - + # overwrite address if ORListenAddress is set (and possibly orPort too) self.vals["tor/orListenAddr"] = "" listenAddr = conn.getOption("ORListenAddress", None) @@ -496,30 +496,30 @@ class HeaderPanel(panel.Panel, threading.Thread): self.vals["tor/orPort"] = listenAddr[listenAddr.find(":") + 1:] else: self.vals["tor/orListenAddr"] = listenAddr - + # fetch exit policy (might span over multiple lines) policyEntries = [] for exitPolicy in conn.getOption("ExitPolicy", [], True): policyEntries += [policy.strip() for policy in exitPolicy.split(",")] self.vals["tor/exitPolicy"] = ", ".join(policyEntries) - + # file descriptor limit for the process, if this can't be determined # then the limit is None fdLimit, fdIsEstimate = conn.getMyFileDescriptorLimit() self.vals["tor/fdLimit"] = fdLimit self.vals["tor/isFdLimitEstimate"] = fdIsEstimate - + # system information unameVals = os.uname() self.vals["sys/hostname"] = unameVals[1] self.vals["sys/os"] = unameVals[0] self.vals["sys/version"] = unameVals[2] - + self.vals["tor/pid"] = conn.controller.get_pid("") - + startTime = conn.getStartTime() self.vals["tor/startTime"] = startTime if startTime else "" - + # reverts volatile parameters to defaults self.vals["tor/fingerprint"] = "Unknown" self.vals["tor/flags"] = [] @@ -528,15 +528,15 @@ class HeaderPanel(panel.Panel, threading.Thread): self.vals["stat/%armCpu"] = "0" self.vals["stat/rss"] = "0" self.vals["stat/%mem"] = "0" - + # sets volatile parameters # TODO: This can change, being reported by STATUS_SERVER -> EXTERNAL_ADDRESS # events. Introduce caching via torTools? self.vals["tor/address"] = conn.getInfo("address", "") - + self.vals["tor/fingerprint"] = conn.getInfo("fingerprint", self.vals["tor/fingerprint"]) self.vals["tor/flags"] = conn.getMyFlags(self.vals["tor/flags"]) - + # Updates file descriptor usage and logs if the usage is high. If we don't # have a known limit or it's obviously faulty (being lower than our # current usage) then omit file descriptor functionality. @@ -544,12 +544,12 @@ class HeaderPanel(panel.Panel, threading.Thread): fdUsed = conn.getMyFileDescriptorUsage() if fdUsed and fdUsed <= self.vals["tor/fdLimit"]: self.vals["tor/fdUsed"] = fdUsed else: self.vals["tor/fdUsed"] = 0 - + if self.vals["tor/fdUsed"] and self.vals["tor/fdLimit"]: fdPercent = 100 * self.vals["tor/fdUsed"] / self.vals["tor/fdLimit"] estimatedLabel = " estimated" if self.vals["tor/isFdLimitEstimate"] else "" msg = "Tor's%s file descriptor usage is at %i%%." % (estimatedLabel, fdPercent) - + 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." @@ -557,11 +557,11 @@ class HeaderPanel(panel.Panel, threading.Thread): elif fdPercent >= 60 and not self._isFdSixtyPercentWarned: self._isFdSixtyPercentWarned = True log.notice(msg) - + # ps or proc derived resource usage stats if self.vals["tor/pid"]: resourceTracker = sysTools.getResourceTracker(self.vals["tor/pid"]) - + if resourceTracker.lastQueryFailed(): self.vals["stat/%torCpu"] = "0" self.vals["stat/rss"] = "0" @@ -572,10 +572,10 @@ class HeaderPanel(panel.Panel, threading.Thread): self.vals["stat/%torCpu"] = "%0.1f" % (100 * cpuUsage) self.vals["stat/rss"] = str(memUsage) self.vals["stat/%mem"] = "%0.1f" % (100 * memUsagePercent) - + # determines the cpu time for the arm process (including user and system # time of both the primary and child processes) - + totalArmCpuTime, currentTime = sum(os.times()[:3]), time.time() armCpuDelta = totalArmCpuTime - self._armCpuSampling[0] armTimeDelta = currentTime - self._armCpuSampling[1] @@ -583,7 +583,7 @@ class HeaderPanel(panel.Panel, threading.Thread): sysCallCpuTime = sysTools.getSysCpuUsage() self.vals["stat/%armCpu"] = "%0.1f" % (100 * (pythonCpuTime + sysCallCpuTime)) self._armCpuSampling = (totalArmCpuTime, currentTime) - + self._lastUpdate = currentTime self.valsLock.release()
diff --git a/arm/logPanel.py b/arm/logPanel.py index e9b3d06..101a7e8 100644 --- a/arm/logPanel.py +++ b/arm/logPanel.py @@ -97,11 +97,11 @@ def daysSince(timestamp=None): """ Provides the number of days since the epoch converted to local time (rounded down). - + Arguments: timestamp - unix timestamp to convert, current time if undefined """ - + if timestamp == None: timestamp = time.time() return int((timestamp - TIMEZONE_OFFSET) / 86400)
@@ -115,18 +115,18 @@ def expandEvents(eventAbbr): DINWE - runlevel and higher 12345 - arm/stem runlevel and higher (ARM_DEBUG - ARM_ERR) Raises ValueError with invalid input if any part isn't recognized. - + Examples: "inUt" -> ["INFO", "NOTICE", "UNKNOWN", "STREAM_BW"] "N4" -> ["NOTICE", "WARN", "ERR", "ARM_WARN", "ARM_ERR"] "cfX" -> [] - + Arguments: eventAbbr - flags to be parsed to event types """ - + expandedEvents, invalidFlags = set(), "" - + for flag in eventAbbr: if flag == "A": armRunlevels = ["ARM_" + runlevel for runlevel in log.Runlevel] @@ -142,7 +142,7 @@ def expandEvents(eventAbbr): elif flag in "N3": runlevelIndex = 3 elif flag in "W4": runlevelIndex = 4 elif flag in "E5": runlevelIndex = 5 - + if flag in "DINWE": runlevelSet = [runlevel for runlevel in list(log.Runlevel)[runlevelIndex:]] expandedEvents = expandedEvents.union(set(runlevelSet)) @@ -155,7 +155,7 @@ def expandEvents(eventAbbr): expandedEvents.add(TOR_EVENT_TYPES[flag]) else: invalidFlags += flag - + if invalidFlags: raise ValueError(invalidFlags) else: return expandedEvents
@@ -165,9 +165,9 @@ def getMissingEventTypes(): doesn't. This provides an empty list if no event types are missing, and None if the GETINFO query fails. """ - + torEventTypes = torTools.getConn().getInfo("events/names", None) - + if torEventTypes: torEventTypes = torEventTypes.split(" ") armEventTypes = TOR_EVENT_TYPES.values() @@ -178,10 +178,10 @@ def loadLogMessages(): """ Fetches a mapping of common log messages to their runlevels from the config. """ - + global COMMON_LOG_MESSAGES armConf = conf.get_config("arm") - + COMMON_LOG_MESSAGES = {} for confKey in armConf.keys(): if confKey.startswith("msg."): @@ -195,31 +195,31 @@ def getLogFileEntries(runlevels, readLimit = None, addLimit = None): a list of log entries (ordered newest to oldest). Limiting the number of read entries is suggested to avoid parsing everything from logs in the GB and TB range. - + Arguments: 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) """ - + startTime = time.time() if not runlevels: return [] - + # 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): # looks for an entry like: notice file /var/log/tor/notices.log entryComp = loggingEntry.split() - + if entryComp[1] == "file": loggingTypes, loggingLocation = entryComp[0], entryComp[2] break - + if not loggingLocation: return [] - + # includes the prefix for tor paths loggingLocation = torTools.get_chroot() + loggingLocation - + # if the runlevels argument is a superset of the log file then we can # limit the read contents to the addLimit runlevels = list(log.Runlevel) @@ -233,16 +233,16 @@ def getLogFileEntries(runlevels, readLimit = None, addLimit = None): else: sIndex = runlevels.index(loggingTypes) logFileRunlevels = runlevels[sIndex:] - + # checks if runlevels we're reporting are a superset of the file's contents isFileSubset = True for runlevelType in logFileRunlevels: if runlevelType not in runlevels: isFileSubset = False break - + if isFileSubset: readLimit = addLimit - + # tries opening the log file, cropping results to avoid choking on huge logs lines = [] try: @@ -255,33 +255,33 @@ def getLogFileEntries(runlevels, readLimit = None, addLimit = None): logFile.close() except IOError: log.warn("Unable to read tor's log file: %s" % loggingLocation) - + if not lines: return [] - + loggedEvents = [] currentUnixTime, currentLocalTime = time.time(), time.localtime() for i in range(len(lines) - 1, -1, -1): line = lines[i] - + # entries look like: # Jul 15 18:29:48.806 [notice] Parsing GEOIP file. lineComp = line.split() - + # Checks that we have all the components we expect. This could happen if # we're either not parsing a tor log or in weird edge cases (like being # out of disk space) - + if len(lineComp) < 4: continue - + eventType = lineComp[3][1:-1].upper() - + if eventType in runlevels: # converts timestamp to unix time timestamp = " ".join(lineComp[:3]) - + # strips the decimal seconds if "." in timestamp: timestamp = timestamp[:timestamp.find(".")] - + # Ignoring wday and yday since they aren't used. # # Pretend the year is 2012, because 2012 is a leap year, and parsing a @@ -290,24 +290,24 @@ def getLogFileEntries(runlevels, readLimit = None, addLimit = None): # might be parsing old logs which didn't get rotated. # # https://trac.torproject.org/projects/tor/ticket/5265 - + timestamp = "2012 " + timestamp eventTimeComp = list(time.strptime(timestamp, "%Y %b %d %H:%M:%S")) eventTimeComp[8] = currentLocalTime.tm_isdst eventTime = time.mktime(eventTimeComp) # converts local to unix time - + # The above is gonna be wrong if the logs are for the previous year. If # the event's in the future then correct for this. if eventTime > currentUnixTime + 60: eventTimeComp[0] -= 1 eventTime = time.mktime(eventTimeComp) - + eventMsg = " ".join(lineComp[4:]) loggedEvents.append(LogEntry(eventTime, eventType, eventMsg, RUNLEVEL_EVENT_COLOR[eventType])) - + if "opening log file" in line: break # this entry marks the start of this tor instance - + if addLimit: loggedEvents = loggedEvents[:addLimit] log.info("Read %i entries from tor's log file: %s (read limit: %i, runtime: %0.3f)" % (len(loggedEvents), loggingLocation, readLimit, time.time() - startTime)) return loggedEvents @@ -318,36 +318,36 @@ def getDaybreaks(events, ignoreTimeForCache = False): whenever the date changed between log entries (or since the most recent event). The timestamp matches the beginning of the day for the following entry. - + Arguments: events - chronologically ordered listing of events ignoreTimeForCache - skips taking the day into consideration for providing cached results if true """ - + global CACHED_DAYBREAKS_ARGUMENTS, CACHED_DAYBREAKS_RESULT if not events: return [] - + newListing = [] currentDay = daysSince() lastDay = currentDay - + if CACHED_DAYBREAKS_ARGUMENTS[0] == events and \ (ignoreTimeForCache or CACHED_DAYBREAKS_ARGUMENTS[1] == currentDay): return list(CACHED_DAYBREAKS_RESULT) - + for entry in events: eventDay = daysSince(entry.timestamp) if eventDay != lastDay: markerTimestamp = (eventDay * 86400) + TIMEZONE_OFFSET newListing.append(LogEntry(markerTimestamp, DAYBREAK_EVENT, "", "white")) - + newListing.append(entry) lastDay = eventDay - + CACHED_DAYBREAKS_ARGUMENTS = (list(events), currentDay) CACHED_DAYBREAKS_RESULT = list(newListing) - + return newListing
def getDuplicates(events): @@ -356,39 +356,39 @@ def getDuplicates(events): log entry and count of duplicates following it. Entries in different days are not considered to be duplicates. This times out, returning None if it takes longer than DEDUPLICATION_TIMEOUT. - + Arguments: events - chronologically ordered listing of events """ - + global CACHED_DUPLICATES_ARGUMENTS, CACHED_DUPLICATES_RESULT if CACHED_DUPLICATES_ARGUMENTS == events: return list(CACHED_DUPLICATES_RESULT) - + # loads common log entries from the config if they haven't been if COMMON_LOG_MESSAGES == None: loadLogMessages() - + startTime = time.time() eventsRemaining = list(events) returnEvents = [] - + while eventsRemaining: entry = eventsRemaining.pop(0) duplicateIndices = isDuplicate(entry, eventsRemaining, True) - + # checks if the call timeout has been reached if (time.time() - startTime) > DEDUPLICATION_TIMEOUT / 1000.0: return None - + # drops duplicate entries duplicateIndices.reverse() for i in duplicateIndices: del eventsRemaining[i] - + returnEvents.append((entry, len(duplicateIndices))) - + CACHED_DUPLICATES_ARGUMENTS = list(events) CACHED_DUPLICATES_RESULT = list(returnEvents) - + return returnEvents
def isDuplicate(event, eventSet, getDuplicates = False): @@ -396,22 +396,22 @@ def isDuplicate(event, eventSet, getDuplicates = False): True if the event is a duplicate for something in the eventSet, false otherwise. If the getDuplicates flag is set this provides the indices of the duplicates instead. - + Arguments: event - event to search for duplicates of eventSet - set to look for the event in getDuplicates - instead of providing back a boolean this gives a list of the duplicate indices in the eventSet """ - + duplicateIndices = [] for i in range(len(eventSet)): forwardEntry = eventSet[i] - + # if showing dates then do duplicate detection for each day, rather # than globally if forwardEntry.type == DAYBREAK_EVENT: break - + if event.type == forwardEntry.type: isDuplicate = False if event.msg == forwardEntry.msg: isDuplicate = True @@ -423,13 +423,13 @@ def isDuplicate(event, eventSet, getDuplicates = False): isDuplicate = commonMsg[1:] in event.msg and commonMsg[1:] in forwardEntry.msg else: isDuplicate = event.msg.startswith(commonMsg) and forwardEntry.msg.startswith(commonMsg) - + if isDuplicate: break - + if isDuplicate: if getDuplicates: duplicateIndices.append(i) else: return True - + if getDuplicates: return duplicateIndices else: return False
@@ -441,32 +441,32 @@ class LogEntry(): msg - message that was logged color - color of the log entry """ - + def __init__(self, timestamp, eventType, msg, color): self.timestamp = timestamp self.type = eventType self.msg = msg self.color = color self._displayMessage = None - + def getDisplayMessage(self, includeDate = False): """ Provides the entry's message for the log. - + Arguments: includeDate - appends the event's date to the start of the message """ - + if includeDate: # not the common case so skip caching entryTime = time.localtime(self.timestamp) timeLabel = "%i/%i/%i %02i:%02i:%02i" % (entryTime[1], entryTime[2], entryTime[0], entryTime[3], entryTime[4], entryTime[5]) return "%s [%s] %s" % (timeLabel, self.type, self.msg) - + if not self._displayMessage: entryTime = time.localtime(self.timestamp) self._displayMessage = "%02i:%02i:%02i [%s] %s" % (entryTime[3], entryTime[4], entryTime[5], self.type, self.msg) - + return self._displayMessage
class LogPanel(panel.Panel, threading.Thread, logging.Handler): @@ -474,85 +474,85 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): Listens for and displays tor, arm, and stem events. This can prepopulate from tor's log file if it exists. """ - + def __init__(self, stdscr, loggedEvents): panel.Panel.__init__(self, stdscr, "log", 0) logging.Handler.__init__(self, level = log.logging_level(log.DEBUG)) - + self.setFormatter(logging.Formatter( fmt = '%(asctime)s [%(levelname)s] %(message)s', datefmt = '%m/%d/%Y %H:%M:%S'), ) - + threading.Thread.__init__(self) self.setDaemon(True) - + # Make sure that the msg.* messages are loaded. Lazy loading it later is # fine, but this way we're sure it happens before warning about unused # config options. loadLogMessages() - + # regex filters the user has defined self.filterOptions = [] - + for filter in 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: log.notice("Invalid regular expression pattern (%s): %s" % (exc, filter)) - + self.loggedEvents = [] # needs to be set before we receive any events - + # restricts the input to the set of events we can listen to, and # configures the controller to liten to them self.loggedEvents = self.setEventListening(loggedEvents) - + self.setPauseAttr("msgLog") # tracks the message log when we're paused self.msgLog = [] # log entries, sorted by the timestamp self.regexFilter = None # filter for presented log events (no filtering if None) self.lastContentHeight = 0 # height of the rendered content when last drawn self.logFile = None # file log messages are saved to (skipped if None) self.scroll = 0 - + self._lastUpdate = -1 # time the content was last revised self._halt = False # terminates thread if true self._cond = threading.Condition() # used for pausing/resuming the thread - + # restricts concurrent write access to attributes used to draw the display # and pausing: # msgLog, loggedEvents, regexFilter, scroll self.valsLock = threading.RLock() - + # cached parameters (invalidated if arguments for them change) # last set of events we've drawn with self._lastLoggedEvents = [] - + # _getTitle (args: loggedEvents, regexFilter pattern, width) self._titleCache = None self._titleArgs = (None, None, None) - + self.reprepopulateEvents() - + # leaving lastContentHeight as being too low causes initialization problems self.lastContentHeight = len(self.msgLog) - + # adds listeners for tor and stem events conn = torTools.getConn() conn.addStatusListener(self._resetListener) - + # opens log file if we'll be saving entries if CONFIG["features.logFile"]: logPath = CONFIG["features.logFile"] - + try: # make dir if the path doesn't already exist baseDir = os.path.dirname(logPath) if not os.path.exists(baseDir): os.makedirs(baseDir) - + self.logFile = open(logPath, "a") log.notice("arm %s opening log file (%s)" % (__version__, logPath)) except IOError, exc: @@ -561,29 +561,29 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): except OSError, exc: log.error("Unable to write to log file: %s" % exc) self.logFile = None - + stem_logger = log.get_logger() stem_logger.addHandler(self) - + def emit(self, record): if record.levelname == "ERROR": record.levelname = "ERR" elif record.levelname == "WARNING": record.levelname = "WARN" - + eventColor = RUNLEVEL_EVENT_COLOR[record.levelname] self.registerEvent(LogEntry(int(record.created), "ARM_%s" % record.levelname, record.msg, eventColor)) - + def reprepopulateEvents(self): """ Clears the event log and repopulates it from the arm and tor backlogs. """ - + self.valsLock.acquire() - + # clears the event log self.msgLog = [] - + # fetches past tor events from log file, if available if CONFIG["features.log.prepopulate"]: setRunlevels = list(set.intersection(set(self.loggedEvents), set(list(log.Runlevel)))) @@ -591,32 +591,32 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): addLimit = CONFIG["cache.logPanel.size"] for entry in getLogFileEntries(setRunlevels, readLimit, addLimit): self.msgLog.append(entry) - + # crops events that are either too old, or more numerous than the caching size self._trimEvents(self.msgLog) - + self.valsLock.release() - + def setDuplicateVisability(self, isVisible): """ Sets if duplicate log entries are collaped or expanded. - + Arguments: isVisible - if true all log entries are shown, otherwise they're deduplicated """ - + armConf = conf.get_config("arm") armConf.set("features.log.showDuplicateEntries", str(isVisible)) - + def registerTorEvent(self, event): """ Translates a stem.response.event.Event instance into a LogEvent, and calls registerEvent(). """ - + msg, color = ' '.join(str(event).split(' ')[1:]), "white" - + if isinstance(event, events.CircuitEvent): color = "yellow" elif isinstance(event, events.BandwidthEvent): @@ -633,22 +633,22 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): color = "yellow" elif not event.type in TOR_EVENT_TYPES.values(): color = "red" # unknown event type - + self.registerEvent(LogEntry(event.arrived_at, event.type, msg, color)) - + def registerEvent(self, event): """ Notes event and redraws log. If paused it's held in a temporary buffer. - + Arguments: event - LogEntry for the event that occurred """ - + if not event.type in self.loggedEvents: return - + # strips control characters to avoid screwing up the terminal event.msg = uiTools.getPrintable(event.msg) - + # note event in the log file if we're saving them if self.logFile: try: @@ -657,74 +657,74 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): except IOError, exc: log.error("Unable to write to log file: %s" % exc.strerror) self.logFile = None - + self.valsLock.acquire() self.msgLog.insert(0, event) self._trimEvents(self.msgLog) - + # notifies the display that it has new content if not self.regexFilter or self.regexFilter.search(event.getDisplayMessage()): self._cond.acquire() self._cond.notifyAll() self._cond.release() - + self.valsLock.release() - + def setLoggedEvents(self, eventTypes): """ Sets the event types recognized by the panel. - + Arguments: eventTypes - event types to be logged """ - + if eventTypes == self.loggedEvents: return self.valsLock.acquire() - + # configures the controller to listen for these tor events, and provides # back a subset without anything we're failing to listen to setTypes = self.setEventListening(eventTypes) self.loggedEvents = setTypes self.redraw(True) self.valsLock.release() - + def getFilter(self): """ Provides our currently selected regex filter. """ - + return self.filterOptions[0] if self.regexFilter else None - + def setFilter(self, logFilter): """ Filters log entries according to the given regular expression. - + Arguments: logFilter - regular expression used to determine which messages are shown, None if no filter should be applied """ - + if logFilter == self.regexFilter: return - + self.valsLock.acquire() self.regexFilter = logFilter self.redraw(True) self.valsLock.release() - + def makeFilterSelection(self, selectedOption): """ Makes the given filter selection, applying it to the log and reorganizing our filter selection. - + Arguments: selectedOption - regex filter we've already added, None if no filter should be applied """ - + if selectedOption: try: self.setFilter(re.compile(selectedOption)) - + # move selection to top self.filterOptions.remove(selectedOption) self.filterOptions.insert(0, selectedOption) @@ -733,14 +733,14 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): log.warn("Invalid regular expression ('%s': %s) - removing from listing" % (selectedOption, exc)) self.filterOptions.remove(selectedOption) else: self.setFilter(None) - + def showFilterPrompt(self): """ Prompts the user to add a new regex filter. """ - + regexInput = popups.inputPrompt("Regular expression: ") - + if regexInput: try: self.setFilter(re.compile(regexInput)) @@ -748,27 +748,27 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): self.filterOptions.insert(0, regexInput) except re.error, exc: popups.showMsg("Unable to compile expression: %s" % exc, 2) - + def showEventSelectionPrompt(self): """ Prompts the user to select the events being listened for. """ - + # allow user to enter new types of events to log - unchanged if left blank popup, width, height = popups.init(11, 80) - + if popup: try: # displays the available flags popup.win.box() popup.addstr(0, 0, "Event Types:", curses.A_STANDOUT) eventLines = EVENT_LISTING.split("\n") - + for i in range(len(eventLines)): popup.addstr(i + 1, 1, eventLines[i][6:]) - + popup.win.refresh() - + userInput = popups.inputPrompt("Events to log: ") if userInput: userInput = userInput.replace(' ', '') # strips spaces @@ -776,69 +776,69 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): except ValueError, exc: popups.showMsg("Invalid flags: %s" % str(exc), 2) finally: popups.finalize() - + def showSnapshotPrompt(self): """ Lets user enter a path to take a snapshot, canceling if left blank. """ - + pathInput = popups.inputPrompt("Path to save log snapshot: ") - + if pathInput: try: self.saveSnapshot(pathInput) popups.showMsg("Saved: %s" % pathInput, 2) except IOError, exc: popups.showMsg("Unable to save snapshot: %s" % exc.strerror, 2) - + def clear(self): """ Clears the contents of the event log. """ - + self.valsLock.acquire() self.msgLog = [] self.redraw(True) self.valsLock.release() - + def saveSnapshot(self, path): """ Saves the log events currently being displayed to the given path. This takes filers into account. This overwrites the file if it already exists, and raises an IOError if there's a problem. - + Arguments: path - path where to save the log snapshot """ - + path = os.path.abspath(os.path.expanduser(path)) - + # make dir if the path doesn't already exist baseDir = os.path.dirname(path) - + try: if not os.path.exists(baseDir): os.makedirs(baseDir) except OSError, exc: raise IOError("unable to make directory '%s'" % baseDir) - + snapshotFile = open(path, "w") self.valsLock.acquire() try: for entry in self.msgLog: isVisible = not self.regexFilter or self.regexFilter.search(entry.getDisplayMessage()) if isVisible: snapshotFile.write(entry.getDisplayMessage(True) + "\n") - + self.valsLock.release() except Exception, exc: self.valsLock.release() raise exc - + def handleKey(self, key): isKeystrokeConsumed = True if uiTools.isScrollKey(key): pageHeight = self.getPreferredSize()[0] - 1 newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self.lastContentHeight) - + if self.scroll != newScroll: self.valsLock.acquire() self.scroll = newScroll @@ -858,13 +858,13 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax options = ["None"] + self.filterOptions + ["New..."] oldSelection = 0 if not self.regexFilter else 1 - + # does all activity under a curses lock to prevent redraws when adding # new filters panel.CURSES_LOCK.acquire() try: selection = popups.showMenu("Log Filter:", options, oldSelection) - + # applies new setting if selection == 0: self.setFilter(None) @@ -875,16 +875,16 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): self.makeFilterSelection(self.filterOptions[selection - 1]) finally: panel.CURSES_LOCK.release() - + if len(self.filterOptions) > MAX_REGEX_FILTERS: del self.filterOptions[MAX_REGEX_FILTERS:] elif key == ord('e') or key == ord('E'): self.showEventSelectionPrompt() elif key == ord('a') or key == ord('A'): self.showSnapshotPrompt() else: isKeystrokeConsumed = False - + return isKeystrokeConsumed - + def getHelp(self): options = [] options.append(("up arrow", "scroll log up a line", None)) @@ -895,57 +895,57 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): options.append(("u", "duplicate log entries", "visible" if CONFIG["features.log.showDuplicateEntries"] else "hidden")) options.append(("c", "clear event log", None)) return options - + def draw(self, width, height): """ Redraws message log. Entries stretch to use available space and may contain up to two lines. Starts with newest entries. """ - + currentLog = self.getAttr("msgLog") - + self.valsLock.acquire() self._lastLoggedEvents, self._lastUpdate = list(currentLog), time.time() - + # draws the top label if self.isTitleVisible(): self.addstr(0, 0, self._getTitle(width), curses.A_STANDOUT) - + # restricts scroll location to valid bounds self.scroll = max(0, min(self.scroll, self.lastContentHeight - height + 1)) - + # draws left-hand scroll bar if content's longer than the height msgIndent, dividerIndent = 1, 0 # offsets for scroll bar isScrollBarVisible = self.lastContentHeight > height - 1 if isScrollBarVisible: msgIndent, dividerIndent = 3, 2 self.addScrollBar(self.scroll, self.scroll + height - 1, self.lastContentHeight, 1) - + # draws log entries lineCount = 1 - self.scroll seenFirstDateDivider = False dividerAttr, duplicateAttr = curses.A_BOLD | uiTools.getColor("yellow"), curses.A_BOLD | uiTools.getColor("green") - + isDatesShown = self.regexFilter == None and CONFIG["features.log.showDateDividers"] eventLog = getDaybreaks(currentLog, self.isPaused()) if isDatesShown else list(currentLog) if not CONFIG["features.log.showDuplicateEntries"]: deduplicatedLog = getDuplicates(eventLog) - + if deduplicatedLog == None: log.warn("Deduplication took too long. Its current implementation has difficulty handling large logs so disabling it to keep the interface responsive.") self.setDuplicateVisability(True) deduplicatedLog = [(entry, 0) for entry in eventLog] else: deduplicatedLog = [(entry, 0) for entry in eventLog] - + # determines if we have the minimum width to show date dividers showDaybreaks = width - dividerIndent >= 3 - + while deduplicatedLog: entry, duplicateCount = deduplicatedLog.pop(0) - + if self.regexFilter and not self.regexFilter.search(entry.getDisplayMessage()): continue # filter doesn't match log message - skip - + # checks if we should be showing a divider with the date if entry.type == DAYBREAK_EVENT: # bottom of the divider @@ -954,44 +954,44 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER, dividerAttr) self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 2, dividerAttr) self.addch(lineCount, width - 1, curses.ACS_LRCORNER, dividerAttr) - + lineCount += 1 - + # top of the divider if lineCount >= 1 and lineCount < height and showDaybreaks: timeLabel = time.strftime(" %B %d, %Y ", time.localtime(entry.timestamp)) self.addch(lineCount, dividerIndent, curses.ACS_ULCORNER, dividerAttr) self.addch(lineCount, dividerIndent + 1, curses.ACS_HLINE, dividerAttr) self.addstr(lineCount, dividerIndent + 2, timeLabel, curses.A_BOLD | dividerAttr) - + lineLength = width - dividerIndent - len(timeLabel) - 3 self.hline(lineCount, dividerIndent + len(timeLabel) + 2, lineLength, dividerAttr) self.addch(lineCount, dividerIndent + len(timeLabel) + 2 + lineLength, curses.ACS_URCORNER, dividerAttr) - + seenFirstDateDivider = True lineCount += 1 else: # entry contents to be displayed, tuples of the form: # (msg, formatting, includeLinebreak) displayQueue = [] - + msgComp = entry.getDisplayMessage().split("\n") for i in range(len(msgComp)): font = curses.A_BOLD if "ERR" in entry.type else curses.A_NORMAL # emphasizes ERR messages displayQueue.append((msgComp[i].strip(), font | uiTools.getColor(entry.color), i != len(msgComp) - 1)) - + if duplicateCount: pluralLabel = "s" if duplicateCount > 1 else "" duplicateMsg = DUPLICATE_MSG % (duplicateCount, pluralLabel) displayQueue.append((duplicateMsg, duplicateAttr, False)) - + cursorLoc, lineOffset = msgIndent, 0 maxEntriesPerLine = CONFIG["features.log.maxLinesPerEntry"] while displayQueue: msg, format, includeBreak = displayQueue.pop(0) drawLine = lineCount + lineOffset if lineOffset == maxEntriesPerLine: break - + maxMsgSize = width - cursorLoc - 1 if len(msg) > maxMsgSize: # message is too long - break it up @@ -1000,40 +1000,40 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): else: msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.Ending.HYPHEN, True) displayQueue.insert(0, (remainder.strip(), format, includeBreak)) - + includeBreak = True - + if drawLine < height and drawLine >= 1: if seenFirstDateDivider and width - dividerIndent >= 3 and showDaybreaks: self.addch(drawLine, dividerIndent, curses.ACS_VLINE, dividerAttr) self.addch(drawLine, width - 1, curses.ACS_VLINE, dividerAttr) - + self.addstr(drawLine, cursorLoc, msg, format) - + cursorLoc += len(msg) - + if includeBreak or not displayQueue: lineOffset += 1 cursorLoc = msgIndent + ENTRY_INDENT - + lineCount += lineOffset - + # if this is the last line and there's room, then draw the bottom of the divider if not deduplicatedLog and seenFirstDateDivider: if lineCount < height and showDaybreaks: self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER, dividerAttr) self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 2, dividerAttr) self.addch(lineCount, width - 1, curses.ACS_LRCORNER, dividerAttr) - + lineCount += 1 - + # redraw the display if... # - lastContentHeight was off by too much # - we're off the bottom of the page newContentHeight = lineCount + self.scroll - 1 contentHeightDelta = abs(self.lastContentHeight - newContentHeight) forceRedraw, forceRedrawReason = True, "" - + if contentHeightDelta >= CONTENT_HEIGHT_REDRAW_THRESHOLD: forceRedrawReason = "estimate was off by %i" % contentHeightDelta elif newContentHeight > height and self.scroll + height - 1 > newContentHeight: @@ -1043,37 +1043,37 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): elif isScrollBarVisible and newContentHeight <= height - 1: forceRedrawReason = "scroll bar shouldn't be visible" else: forceRedraw = False - + self.lastContentHeight = newContentHeight if forceRedraw: log.debug("redrawing the log panel with the corrected content height (%s)" % forceRedrawReason) self.redraw(True) - + self.valsLock.release() - + def redraw(self, forceRedraw=False, block=False): # determines if the content needs to be redrawn or not panel.Panel.redraw(self, forceRedraw, block) - + def run(self): """ Redraws the display, coalescing updates if events are rapidly logged (for instance running at the DEBUG runlevel) while also being immediately responsive if additions are less frequent. """ - + lastDay = daysSince() # used to determine if the date has changed while not self._halt: currentDay = daysSince() timeSinceReset = time.time() - self._lastUpdate maxLogUpdateRate = CONFIG["features.log.maxRefreshRate"] / 1000.0 - + sleepTime = 0 if (self.msgLog == self._lastLoggedEvents and lastDay == currentDay) or self.isPaused(): sleepTime = 5 elif timeSinceReset < maxLogUpdateRate: sleepTime = max(0.05, maxLogUpdateRate - timeSinceReset) - + if sleepTime: self._cond.acquire() if not self._halt: self._cond.wait(sleepTime) @@ -1081,84 +1081,84 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): else: lastDay = currentDay self.redraw(True) - + # makes sure that we register this as an update, otherwise lacking the # curses lock can cause a busy wait here self._lastUpdate = time.time() - + def stop(self): """ Halts further resolutions and terminates the thread. """ - + self._cond.acquire() self._halt = True self._cond.notifyAll() self._cond.release() - + def setEventListening(self, events): """ Configures the events Tor listens for, filtering non-tor events from what we request from the controller. This returns a sorted list of the events we successfully set. - + Arguments: events - event types to attempt to set """ - + events = set(events) # drops duplicates - + # accounts for runlevel naming difference if "ERROR" in events: events.add("ERR") events.remove("ERROR") - + if "WARNING" in events: events.add("WARN") events.remove("WARNING") - + torEvents = events.intersection(set(TOR_EVENT_TYPES.values())) armEvents = events.intersection(set(["ARM_%s" % runlevel for runlevel in log.Runlevel.keys()])) - + # adds events unrecognized by arm if we're listening to the 'UNKNOWN' type if "UNKNOWN" in events: torEvents.update(set(getMissingEventTypes())) - + torConn = torTools.getConn() torConn.removeEventListener(self.registerTorEvent) - + for eventType in list(torEvents): try: torConn.addEventListener(self.registerTorEvent, eventType) except stem.ProtocolError: torEvents.remove(eventType) - + # provides back the input set minus events we failed to set return sorted(torEvents.union(armEvents)) - + def _resetListener(self, controller, eventType, _): # if we're attaching to a new tor instance then clears the log and # prepopulates it with the content belonging to this instance - + if eventType == State.INIT: self.reprepopulateEvents() self.redraw(True) elif eventType == State.CLOSED: log.notice("Tor control port closed") - + def _getTitle(self, width): """ Provides the label used for the panel, looking like: Events (ARM NOTICE - ERR, BW - filter: prepopulate): - + This truncates the attributes (with an ellipse) if too long, and condenses runlevel ranges if there's three or more in a row (for instance ARM_INFO, ARM_NOTICE, and ARM_WARN becomes "ARM_INFO - WARN"). - + Arguments: width - width constraint the label needs to fix in """ - + # usually the attributes used to make the label are decently static, so # provide cached results if they're unchanged self.valsLock.acquire() @@ -1169,7 +1169,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): if isUnchanged: self.valsLock.release() return self._titleCache - + eventsList = list(self.loggedEvents) if not eventsList: if not currentPattern: @@ -1185,7 +1185,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): # types (ex. "NOTICE - ERR, ARM_NOTICE - ERR" becomes "TOR/ARM NOTICE - ERR") tmpRunlevels = [] # runlevels pulled from the list (just the runlevel part) runlevelRanges = [] # tuple of type, startLevel, endLevel for ranges to be consensed - + # reverses runlevels and types so they're appended in the right order reversedRunlevels = list(log.Runlevel) reversedRunlevels.reverse() @@ -1206,68 +1206,68 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): # adds runlevels individaully for tmpRunlevel in tmpRunlevels: eventsList.insert(0, prefix + tmpRunlevel) - + tmpRunlevels = [] - + # adds runlevel ranges, condensing if there's identical ranges for i in range(len(runlevelRanges)): if runlevelRanges[i]: prefix, startLevel, endLevel = runlevelRanges[i] - + # check for matching ranges matches = [] for j in range(i + 1, len(runlevelRanges)): if runlevelRanges[j] and runlevelRanges[j][1] == startLevel and runlevelRanges[j][2] == endLevel: matches.append(runlevelRanges[j]) runlevelRanges[j] = None - + if matches: # strips underscores and replaces empty entries with "TOR" prefixes = [entry[0] for entry in matches] + [prefix] for k in range(len(prefixes)): if prefixes[k] == "": prefixes[k] = "TOR" else: prefixes[k] = prefixes[k].replace("_", "") - + eventsList.insert(0, "%s %s - %s" % ("/".join(prefixes), startLevel, endLevel)) else: eventsList.insert(0, "%s%s - %s" % (prefix, startLevel, endLevel)) - + # truncates to use an ellipsis if too long, for instance: attrLabel = ", ".join(eventsList) if currentPattern: attrLabel += " - filter: %s" % currentPattern attrLabel = uiTools.cropStr(attrLabel, width - 10, 1) if attrLabel: attrLabel = " (%s)" % attrLabel panelLabel = "Events%s:" % attrLabel - + # cache results and return self._titleCache = panelLabel self._titleArgs = (list(self.loggedEvents), currentPattern, width) self.valsLock.release() return panelLabel - + def _trimEvents(self, eventListing): """ Crops events that have either: - grown beyond the cache limit - outlived the configured log duration - + Argument: eventListing - listing of log entries """ - + cacheSize = CONFIG["cache.logPanel.size"] if len(eventListing) > cacheSize: del eventListing[cacheSize:] - + logTTL = CONFIG["features.log.entryDuration"] if logTTL > 0: currentDay = daysSince() - + breakpoint = None # index at which to crop from for i in range(len(eventListing) - 1, -1, -1): daysSinceEvent = currentDay - daysSince(eventListing[i].timestamp) if daysSinceEvent > logTTL: breakpoint = i # older than the ttl else: break - + # removes entries older than the ttl if breakpoint != None: del eventListing[breakpoint:]
diff --git a/arm/menu/actions.py b/arm/menu/actions.py index ce58608..052b249 100644 --- a/arm/menu/actions.py +++ b/arm/menu/actions.py @@ -21,13 +21,13 @@ def makeMenu(): """ Constructs the base menu and all of its contents. """ - + baseMenu = arm.menu.item.Submenu("") baseMenu.add(makeActionsMenu()) baseMenu.add(makeViewMenu()) - + control = arm.controller.getController() - + for pagePanel in control.getDisplayPanels(includeSticky = False): if pagePanel.getName() == "graph": baseMenu.add(makeGraphMenu(pagePanel)) @@ -39,9 +39,9 @@ def makeMenu(): baseMenu.add(makeConfigurationMenu(pagePanel)) elif pagePanel.getName() == "torrc": baseMenu.add(makeTorrcMenu(pagePanel)) - + baseMenu.add(makeHelpMenu()) - + return baseMenu
def makeActionsMenu(): @@ -53,23 +53,23 @@ def makeActionsMenu(): Reset Tor Exit """ - + control = arm.controller.getController() conn = torTools.getConn() headerPanel = control.getPanel("header") actionsMenu = arm.menu.item.Submenu("Actions") actionsMenu.add(arm.menu.item.MenuItem("Close Menu", None)) actionsMenu.add(arm.menu.item.MenuItem("New Identity", headerPanel.sendNewnym)) - + if conn.isAlive(): actionsMenu.add(arm.menu.item.MenuItem("Stop Tor", conn.shutdown)) - + actionsMenu.add(arm.menu.item.MenuItem("Reset Tor", conn.reload)) - + if control.isPaused(): label, arg = "Unpause", False else: label, arg = "Pause", True actionsMenu.add(arm.menu.item.MenuItem(label, functools.partial(control.setPaused, arg))) - + actionsMenu.add(arm.menu.item.MenuItem("Exit", control.quit)) return actionsMenu
@@ -81,30 +81,30 @@ def makeViewMenu(): [ ] etc... Color (Submenu) """ - + viewMenu = arm.menu.item.Submenu("View") control = arm.controller.getController() - + if control.getPageCount() > 0: pageGroup = arm.menu.item.SelectionGroup(control.setPage, control.getPage()) - + for i in range(control.getPageCount()): pagePanels = control.getDisplayPanels(pageNumber = i, includeSticky = False) label = " / ".join([str_tools._to_camel_case(panel.getName()) for panel in pagePanels]) - + viewMenu.add(arm.menu.item.SelectionMenuItem(label, pageGroup, i)) - + if uiTools.isColorSupported(): colorMenu = arm.menu.item.Submenu("Color") colorGroup = arm.menu.item.SelectionGroup(uiTools.setColorOverride, uiTools.getColorOverride()) - + colorMenu.add(arm.menu.item.SelectionMenuItem("All", colorGroup, None)) - + for color in uiTools.COLOR_LIST: colorMenu.add(arm.menu.item.SelectionMenuItem(str_tools._to_camel_case(color), colorGroup, color)) - + viewMenu.add(colorMenu) - + return viewMenu
def makeHelpMenu(): @@ -113,7 +113,7 @@ def makeHelpMenu(): Hotkeys About """ - + helpMenu = arm.menu.item.Submenu("Help") helpMenu.add(arm.menu.item.MenuItem("Hotkeys", arm.popups.showHelpPopup)) helpMenu.add(arm.menu.item.MenuItem("About", arm.popups.showAboutPopup)) @@ -128,46 +128,46 @@ def makeGraphMenu(graphPanel): Resize... Interval (Submenu) Bounds (Submenu) - + Arguments: graphPanel - instance of the graph panel """ - + graphMenu = arm.menu.item.Submenu("Graph") - + # stats options statGroup = arm.menu.item.SelectionGroup(graphPanel.setStats, graphPanel.getStats()) availableStats = graphPanel.stats.keys() availableStats.sort() - + for statKey in ["None"] + availableStats: label = str_tools._to_camel_case(statKey, divider = " ") statKey = None if statKey == "None" else statKey graphMenu.add(arm.menu.item.SelectionMenuItem(label, statGroup, statKey)) - + # resizing option graphMenu.add(arm.menu.item.MenuItem("Resize...", graphPanel.resizeGraph)) - + # interval submenu intervalMenu = arm.menu.item.Submenu("Interval") intervalGroup = arm.menu.item.SelectionGroup(graphPanel.setUpdateInterval, graphPanel.getUpdateInterval()) - + for i in range(len(arm.graphing.graphPanel.UPDATE_INTERVALS)): label = arm.graphing.graphPanel.UPDATE_INTERVALS[i][0] label = str_tools._to_camel_case(label, divider = " ") intervalMenu.add(arm.menu.item.SelectionMenuItem(label, intervalGroup, i)) - + graphMenu.add(intervalMenu) - + # bounds submenu boundsMenu = arm.menu.item.Submenu("Bounds") boundsGroup = arm.menu.item.SelectionGroup(graphPanel.setBoundsType, graphPanel.getBoundsType()) - + for boundsType in arm.graphing.graphPanel.Bounds: boundsMenu.add(arm.menu.item.SelectionMenuItem(boundsType, boundsGroup, boundsType)) - + graphMenu.add(boundsMenu) - + return graphMenu
def makeLogMenu(logPanel): @@ -178,34 +178,34 @@ def makeLogMenu(logPanel): Clear Show / Hide Duplicates Filter (Submenu) - + Arguments: logPanel - instance of the log panel """ - + logMenu = arm.menu.item.Submenu("Log") - + logMenu.add(arm.menu.item.MenuItem("Events...", logPanel.showEventSelectionPrompt)) logMenu.add(arm.menu.item.MenuItem("Snapshot...", logPanel.showSnapshotPrompt)) logMenu.add(arm.menu.item.MenuItem("Clear", logPanel.clear)) - + if CONFIG["features.log.showDuplicateEntries"]: label, arg = "Hide", False else: label, arg = "Show", True logMenu.add(arm.menu.item.MenuItem("%s Duplicates" % label, functools.partial(logPanel.setDuplicateVisability, arg))) - + # filter submenu filterMenu = arm.menu.item.Submenu("Filter") filterGroup = arm.menu.item.SelectionGroup(logPanel.makeFilterSelection, logPanel.getFilter()) - + filterMenu.add(arm.menu.item.SelectionMenuItem("None", filterGroup, None)) - + for option in logPanel.filterOptions: filterMenu.add(arm.menu.item.SelectionMenuItem(option, filterGroup, option)) - + filterMenu.add(arm.menu.item.MenuItem("New...", logPanel.showFilterPrompt)) logMenu.add(filterMenu) - + return logMenu
def makeConnectionsMenu(connPanel): @@ -216,37 +216,37 @@ def makeConnectionsMenu(connPanel): [ ] Nickname Sorting... Resolver (Submenu) - + Arguments: connPanel - instance of the connections panel """ - + connectionsMenu = arm.menu.item.Submenu("Connections") - + # listing options listingGroup = arm.menu.item.SelectionGroup(connPanel.setListingType, connPanel.getListingType()) - + listingOptions = list(arm.connections.entries.ListingType) listingOptions.remove(arm.connections.entries.ListingType.HOSTNAME) - + for option in listingOptions: connectionsMenu.add(arm.menu.item.SelectionMenuItem(option, listingGroup, option)) - + # sorting option connectionsMenu.add(arm.menu.item.MenuItem("Sorting...", connPanel.showSortDialog)) - + # resolver submenu connResolver = connections.getResolver("tor") resolverMenu = arm.menu.item.Submenu("Resolver") resolverGroup = arm.menu.item.SelectionGroup(connResolver.setOverwriteResolver, connResolver.getOverwriteResolver()) - + resolverMenu.add(arm.menu.item.SelectionMenuItem("auto", resolverGroup, None)) - + for option in connections.Resolver: resolverMenu.add(arm.menu.item.SelectionMenuItem(option, resolverGroup, option)) - + connectionsMenu.add(resolverMenu) - + return connectionsMenu
def makeConfigurationMenu(configPanel): @@ -255,19 +255,19 @@ def makeConfigurationMenu(configPanel): Save Config... Sorting... Filter / Unfilter Options - + Arguments: configPanel - instance of the configuration panel """ - + configMenu = arm.menu.item.Submenu("Configuration") configMenu.add(arm.menu.item.MenuItem("Save Config...", configPanel.showWriteDialog)) configMenu.add(arm.menu.item.MenuItem("Sorting...", configPanel.showSortDialog)) - + if configPanel.showAll: label, arg = "Filter", True else: label, arg = "Unfilter", False configMenu.add(arm.menu.item.MenuItem("%s Options" % label, functools.partial(configPanel.setFiltering, arg))) - + return configMenu
def makeTorrcMenu(torrcPanel): @@ -276,21 +276,21 @@ def makeTorrcMenu(torrcPanel): Reload Show / Hide Comments Show / Hide Line Numbers - + Arguments: torrcPanel - instance of the torrc panel """ - + torrcMenu = arm.menu.item.Submenu("Torrc") torrcMenu.add(arm.menu.item.MenuItem("Reload", torrcPanel.reloadTorrc)) - + if torrcPanel.stripComments: label, arg = "Show", True else: label, arg = "Hide", False torrcMenu.add(arm.menu.item.MenuItem("%s Comments" % label, functools.partial(torrcPanel.setCommentsVisible, arg))) - + if torrcPanel.showLineNum: label, arg = "Hide", False else: label, arg = "Show", True torrcMenu.add(arm.menu.item.MenuItem("%s Line Numbers" % label, functools.partial(torrcPanel.setLineNumberVisible, arg))) - + return torrcMenu
diff --git a/arm/menu/item.py b/arm/menu/item.py index 4e66b2b..ffd46a5 100644 --- a/arm/menu/item.py +++ b/arm/menu/item.py @@ -8,99 +8,99 @@ class MenuItem(): """ Option in a drop-down menu. """ - + def __init__(self, label, callback): self._label = label self._callback = callback self._parent = None - + def getLabel(self): """ Provides a tuple of three strings representing the prefix, label, and suffix for this item. """ - + return ("", self._label, "") - + def getParent(self): """ Provides the Submenu we're contained within. """ - + return self._parent - + def getHierarchy(self): """ Provides a list with all of our parents, up to the root. """ - + myHierarchy = [self] while myHierarchy[-1].getParent(): myHierarchy.append(myHierarchy[-1].getParent()) - + myHierarchy.reverse() return myHierarchy - + def getRoot(self): """ Provides the base submenu we belong to. """ - + if self._parent: return self._parent.getRoot() else: return self - + def select(self): """ Performs the callback for the menu item, returning true if we should close the menu and false otherwise. """ - + if self._callback: control = arm.controller.getController() control.setMsg() control.redraw() self._callback() return True - + def next(self): """ Provides the next option for the submenu we're in, raising a ValueError if we don't have a parent. """ - + return self._getSibling(1) - + def prev(self): """ Provides the previous option for the submenu we're in, raising a ValueError if we don't have a parent. """ - + return self._getSibling(-1) - + def _getSibling(self, offset): """ Provides our sibling with a given index offset from us, raising a ValueError if we don't have a parent. - + Arguments: offset - index offset for the sibling to be returned """ - + if self._parent: mySiblings = self._parent.getChildren() - + try: myIndex = mySiblings.index(self) return mySiblings[(myIndex + offset) % len(mySiblings)] except ValueError: # We expect a bidirectional references between submenus and their # children. If we don't have this then our menu's screwed up. - + msg = "The '%s' submenu doesn't contain '%s' (children: '%s')" % (self, self._parent, "', '".join(mySiblings)) raise ValueError(msg) else: raise ValueError("Menu option '%s' doesn't have a parent" % self) - + def __str__(self): return self._label
@@ -108,48 +108,48 @@ class Submenu(MenuItem): """ Menu item that lists other menu options. """ - + def __init__(self, label): MenuItem.__init__(self, label, None) self._children = [] - + def getLabel(self): """ Provides our label with a ">" suffix to indicate that we have suboptions. """ - + myLabel = MenuItem.getLabel(self)[1] return ("", myLabel, " >") - + def add(self, menuItem): """ Adds the given menu item to our listing. This raises a ValueError if the item already has a parent. - + Arguments: menuItem - menu option to be added """ - + if menuItem.getParent(): raise ValueError("Menu option '%s' already has a parent" % menuItem) else: menuItem._parent = self self._children.append(menuItem) - + def getChildren(self): """ Provides the menu and submenus we contain. """ - + return list(self._children) - + def isEmpty(self): """ True if we have no children, false otherwise. """ - + return not bool(self._children) - + def select(self): return False
@@ -157,7 +157,7 @@ class SelectionGroup(): """ Radio button groups that SelectionMenuItems can belong to. """ - + def __init__(self, action, selectedArg): self.action = action self.selectedArg = selectedArg @@ -167,35 +167,35 @@ class SelectionMenuItem(MenuItem): Menu item with an associated group which determines the selection. This is for the common single argument getter/setter pattern. """ - + def __init__(self, label, group, arg): MenuItem.__init__(self, label, None) self._group = group self._arg = arg - + def isSelected(self): """ True if we're the selected item, false otherwise. """ - + return self._arg == self._group.selectedArg - + def getLabel(self): """ Provides our label with a "[X]" prefix if selected and "[ ]" if not. """ - + myLabel = MenuItem.getLabel(self)[1] myPrefix = "[X] " if self.isSelected() else "[ ] " return (myPrefix, myLabel, "") - + def select(self): """ Performs the group's setter action with our argument. """ - + if not self.isSelected(): self._group.action(self._arg) - + return True
diff --git a/arm/menu/menu.py b/arm/menu/menu.py index d8cb514..3edb0a7 100644 --- a/arm/menu/menu.py +++ b/arm/menu/menu.py @@ -15,30 +15,30 @@ class MenuCursor: """ Tracks selection and key handling in the menu. """ - + def __init__(self, initialSelection): self._selection = initialSelection self._isDone = False - + def isDone(self): """ Provides true if a selection has indicated that we should close the menu. False otherwise. """ - + return self._isDone - + def getSelection(self): """ Provides the currently selected menu item. """ - + return self._selection - + def handleKey(self, key): isSelectionSubmenu = isinstance(self._selection, arm.menu.item.Submenu) selectionHierarchy = self._selection.getHierarchy() - + if uiTools.isSelectionKey(key): if isSelectionSubmenu: if not self._selection.isEmpty(): @@ -73,46 +73,46 @@ def showMenu(): popup, _, _ = arm.popups.init(1, belowStatic = False) if not popup: return control = arm.controller.getController() - + try: # generates the menu and uses the initial selection of the first item in # the file menu menu = arm.menu.actions.makeMenu() cursor = MenuCursor(menu.getChildren()[0].getChildren()[0]) - + while not cursor.isDone(): # sets the background color popup.win.clear() popup.win.bkgd(' ', curses.A_STANDOUT | uiTools.getColor("red")) selectionHierarchy = cursor.getSelection().getHierarchy() - + # provide a message saying how to close the menu control.setMsg("Press m or esc to close the menu.", curses.A_BOLD, True) - + # renders the menu bar, noting where the open submenu is positioned drawLeft, selectionLeft = 0, 0 - + for topLevelItem in menu.getChildren(): drawFormat = curses.A_BOLD if topLevelItem == selectionHierarchy[1]: drawFormat |= curses.A_UNDERLINE selectionLeft = drawLeft - + drawLabel = " %s " % topLevelItem.getLabel()[1] popup.addstr(0, drawLeft, drawLabel, drawFormat) popup.addch(0, drawLeft + len(drawLabel), curses.ACS_VLINE) - + drawLeft += len(drawLabel) + 1 - + # recursively shows opened submenus _drawSubmenu(cursor, 1, 1, selectionLeft) - + popup.win.refresh() - + curses.cbreak() key = control.getScreen().getch() cursor.handleKey(key) - + # redraws the rest of the interface if we're rendering on it again if not cursor.isDone(): control.redraw() finally: @@ -121,44 +121,44 @@ def showMenu():
def _drawSubmenu(cursor, level, top, left): selectionHierarchy = cursor.getSelection().getHierarchy() - + # checks if there's nothing to display if len(selectionHierarchy) < level + 2: return - + # fetches the submenu and selection we're displaying submenu = selectionHierarchy[level] selection = selectionHierarchy[level + 1] - + # gets the size of the prefix, middle, and suffix columns allLabelSets = [entry.getLabel() for entry in submenu.getChildren()] prefixColSize = max([len(entry[0]) for entry in allLabelSets]) middleColSize = max([len(entry[1]) for entry in allLabelSets]) suffixColSize = max([len(entry[2]) for entry in allLabelSets]) - + # formatted string so we can display aligned menu entries labelFormat = " %%-%is%%-%is%%-%is " % (prefixColSize, middleColSize, suffixColSize) menuWidth = len(labelFormat % ("", "", "")) - + popup, _, _ = arm.popups.init(len(submenu.getChildren()), menuWidth, top, left, belowStatic = False) if not popup: return - + try: # sets the background color popup.win.bkgd(' ', curses.A_STANDOUT | uiTools.getColor("red")) - + drawTop, selectionTop = 0, 0 for menuItem in submenu.getChildren(): if menuItem == selection: drawFormat = curses.A_BOLD | uiTools.getColor("white") selectionTop = drawTop else: drawFormat = curses.A_NORMAL - + popup.addstr(drawTop, 0, labelFormat % menuItem.getLabel(), drawFormat) drawTop += 1 - + popup.win.refresh() - + # shows the next submenu _drawSubmenu(cursor, level + 1, top + selectionTop, left + menuWidth) finally: arm.popups.finalize() - + diff --git a/arm/popups.py b/arm/popups.py index 2b40558..c4aed89 100644 --- a/arm/popups.py +++ b/arm/popups.py @@ -16,7 +16,7 @@ def init(height = -1, width = -1, top = 0, left = 0, belowStatic = True): and this returns a tuple of the... (popup, draw width, draw height) Otherwise this leaves curses unlocked and returns None. - + Arguments: height - maximum height of the popup width - maximum width of the popup @@ -24,15 +24,15 @@ def init(height = -1, width = -1, top = 0, left = 0, belowStatic = True): left - left position from the screen belowStatic - positions popup below static content if true """ - + control = arm.controller.getController() if belowStatic: stickyHeight = sum([stickyPanel.getHeight() for stickyPanel in control.getStickyPanels()]) else: stickyHeight = 0 - + popup = panel.Panel(control.getScreen(), "popup", top + stickyHeight, left, height, width) popup.setVisible(True) - + # Redraws the popup to prepare a subwindow instance. If none is spawned then # the panel can't be drawn (for instance, due to not being visible). popup.redraw(True) @@ -46,7 +46,7 @@ def finalize(): Cleans up after displaying a popup, releasing the cureses lock and redrawing the rest of the display. """ - + arm.controller.getController().requestRedraw() panel.CURSES_LOCK.release()
@@ -54,12 +54,12 @@ def inputPrompt(msg, initialValue = ""): """ Prompts the user to enter a string on the control line (which usually displays the page number and basic controls). - + Arguments: msg - message to prompt the user for input with initialValue - initial value of the field """ - + panel.CURSES_LOCK.acquire() control = arm.controller.getController() msgPanel = control.getPanel("msg") @@ -74,23 +74,23 @@ def showMsg(msg, maxWait = -1, attr = curses.A_STANDOUT): """ Displays a single line message on the control line for a set time. Pressing any key will end the message. This returns the key pressed. - + Arguments: msg - message to be displayed to the user maxWait - time to show the message, indefinite if -1 attr - attributes with which to draw the message """ - + panel.CURSES_LOCK.acquire() control = arm.controller.getController() control.setMsg(msg, attr, True) - + if maxWait == -1: curses.cbreak() else: curses.halfdelay(maxWait * 10) keyPress = control.getScreen().getch() control.setMsg() panel.CURSES_LOCK.release() - + return keyPress
def showHelpPopup(): @@ -99,30 +99,30 @@ def showHelpPopup(): returns the user input used to close the popup. If the popup didn't close properly, this is an arrow, enter, or scroll key then this returns None. """ - + popup, _, height = init(9, 80) if not popup: return - + exitKey = None try: control = arm.controller.getController() pagePanels = control.getDisplayPanels() - + # the first page is the only one with multiple panels, and it looks better # with the log entries first, so reversing the order pagePanels.reverse() - + helpOptions = [] for entry in pagePanels: helpOptions += entry.getHelp() - + # test doing afterward in case of overwriting popup.win.box() popup.addstr(0, 0, "Page %i Commands:" % (control.getPage() + 1), curses.A_STANDOUT) - + for i in range(len(helpOptions)): if i / 2 >= height - 2: break - + # draws entries in the form '<key>: <description>[ (<selection>)]', for # instance... # u: duplicate log entries (hidden) @@ -130,26 +130,26 @@ def showHelpPopup(): if key: description = ": " + description row = (i / 2) + 1 col = 2 if i % 2 == 0 else 41 - + popup.addstr(row, col, key, curses.A_BOLD) col += len(key) popup.addstr(row, col, description) col += len(description) - + if selection: popup.addstr(row, col, " (") popup.addstr(row, col + 2, selection, curses.A_BOLD) popup.addstr(row, col + 2 + len(selection), ")") - + # tells user to press a key if the lower left is unoccupied if len(helpOptions) < 13 and height == 9: popup.addstr(7, 2, "Press any key...") - + popup.win.refresh() curses.cbreak() exitKey = control.getScreen().getch() finally: finalize() - + if not uiTools.isSelectionKey(exitKey) and \ not uiTools.isScrollKey(exitKey) and \ not exitKey in (curses.KEY_LEFT, curses.KEY_RIGHT): @@ -160,13 +160,13 @@ def showAboutPopup(): """ Presents a popup with author and version information. """ - + popup, _, height = init(9, 80) if not popup: return - + try: control = arm.controller.getController() - + popup.win.box() popup.addstr(0, 0, "About:", curses.A_STANDOUT) popup.addstr(1, 2, "arm, version %s (released %s)" % (__version__, __release_date__), curses.A_BOLD) @@ -175,7 +175,7 @@ def showAboutPopup(): popup.addstr(5, 2, "Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)") popup.addstr(7, 2, "Press any key...") popup.win.refresh() - + curses.cbreak() control.getScreen().getch() finally: finalize() @@ -183,42 +183,42 @@ def showAboutPopup(): def showSortDialog(title, options, oldSelection, optionColors): """ Displays a sorting dialog of the form: - + Current Order: <previous selection> New Order: <selections made> - + <option 1> <option 2> <option 3> Cancel - + Options are colored when among the "Current Order" or "New Order", but not when an option below them. If cancel is selected or the user presses escape then this returns None. Otherwise, the new ordering is provided. - + Arguments: title - title displayed for the popup window options - ordered listing of option labels oldSelection - current ordering optionColors - mappings of options to their color """ - + popup, _, _ = init(9, 80) if not popup: return newSelections = [] # new ordering - + try: cursorLoc = 0 # index of highlighted option curses.cbreak() # wait indefinitely for key presses (no timeout) - + selectionOptions = list(options) selectionOptions.append("Cancel") - + while len(newSelections) < len(oldSelection): popup.win.erase() popup.win.box() popup.addstr(0, 0, title, curses.A_STANDOUT) - + _drawSortSelection(popup, 1, 2, "Current Order: ", oldSelection, optionColors) _drawSortSelection(popup, 2, 2, "New Order: ", newSelections, optionColors) - + # presents remaining options, each row having up to four options with # spacing of nineteen cells row, col = 4, 0 @@ -227,9 +227,9 @@ def showSortDialog(title, options, oldSelection, optionColors): popup.addstr(row, col * 19 + 2, selectionOptions[i], optionFormat) col += 1 if col == 4: row, col = row + 1, 0 - + popup.win.refresh() - + key = arm.controller.getController().getScreen().getch() if key == curses.KEY_LEFT: cursorLoc = max(0, cursorLoc - 1) @@ -241,7 +241,7 @@ def showSortDialog(title, options, oldSelection, optionColors): cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 4) elif uiTools.isSelectionKey(key): selection = selectionOptions[cursorLoc] - + if selection == "Cancel": break else: newSelections.append(selection) @@ -249,7 +249,7 @@ def showSortDialog(title, options, oldSelection, optionColors): cursorLoc = min(cursorLoc, len(selectionOptions) - 1) elif key == 27: break # esc - cancel finally: finalize() - + if len(newSelections) == len(oldSelection): return newSelections else: return None @@ -258,9 +258,9 @@ def _drawSortSelection(popup, y, x, prefix, options, optionColors): """ Draws a series of comma separated sort selections. The whole line is bold and sort options also have their specified color. Example: - + Current Order: Man Page Entry, Option Name, Is Default - + Arguments: popup - panel in which to draw sort selection y - vertical location @@ -269,16 +269,16 @@ def _drawSortSelection(popup, y, x, prefix, options, optionColors): options - sort options to be shown optionColors - mappings of options to their color """ - + popup.addstr(y, x, prefix, curses.A_BOLD) x += len(prefix) - + for i in range(len(options)): sortType = options[i] sortColor = uiTools.getColor(optionColors.get(sortType, "white")) popup.addstr(y, x, sortType, sortColor | curses.A_BOLD) x += len(sortType) - + # comma divider between options, if this isn't the last if i < len(options) - 1: popup.addstr(y, x, ", ", curses.A_BOLD) @@ -289,42 +289,42 @@ def showMenu(title, options, oldSelection): Provides menu with options laid out in a single column. User can cancel selection with the escape key, in which case this proives -1. Otherwise this returns the index of the selection. - + Arguments: title - title displayed for the popup window options - ordered listing of options to display oldSelection - index of the initially selected option (uses the first selection without a carrot if -1) """ - + maxWidth = max(map(len, options)) + 9 popup, _, _ = init(len(options) + 2, maxWidth) if not popup: return key, selection = 0, oldSelection if oldSelection != -1 else 0 - + try: # hides the title of the first panel on the page control = arm.controller.getController() topPanel = control.getDisplayPanels(includeSticky = False)[0] topPanel.setTitleVisible(False) topPanel.redraw(True) - + curses.cbreak() # wait indefinitely for key presses (no timeout) - + while not uiTools.isSelectionKey(key): popup.win.erase() popup.win.box() popup.addstr(0, 0, title, curses.A_STANDOUT) - + for i in range(len(options)): label = options[i] format = curses.A_STANDOUT if i == selection else curses.A_NORMAL tab = "> " if i == oldSelection else " " popup.addstr(i + 1, 2, tab) popup.addstr(i + 1, 4, " %s " % label, format) - + popup.win.refresh() - + key = control.getScreen().getch() if key == curses.KEY_UP: selection = max(0, selection - 1) elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1) @@ -332,6 +332,6 @@ def showMenu(title, options, oldSelection): finally: topPanel.setTitleVisible(True) finalize() - + return selection
diff --git a/arm/prereq.py b/arm/prereq.py index c01dfbd..1813662 100644 --- a/arm/prereq.py +++ b/arm/prereq.py @@ -22,7 +22,7 @@ def isStemAvailable(): """ True if stem is already available on the platform, false otherwise. """ - + try: import stem return True @@ -34,20 +34,20 @@ def promptStemInstall(): Asks the user to install stem. This returns True if it was installed and False otherwise (if it was either declined or failed to be fetched). """ - + userInput = raw_input("Arm requires stem to run, but it's unavailable. Would you like to install it? (y/n): ") - + # if user says no then terminate if not userInput.lower() in ("y", "yes"): return False - + # attempt to install stem, printing the issue if unsuccessful try: #fetchLibrary(STEM_ARCHIVE, STEM_SIG) installStem() - + if not isStemAvailable(): raise IOError("Unable to install stem, sorry") - + print "Stem successfully installed" return True except IOError, exc: @@ -58,36 +58,36 @@ def fetchLibrary(url, sig): """ Downloads the given archive, verifies its signature, then installs the library. This raises an IOError if any of these steps fail. - + Arguments: url - url from which to fetch the gzipped tarball sig - sha256 signature for the archive """ - + tmpDir = tempfile.mkdtemp() destination = tmpDir + "/" + url.split("/")[-1] urllib.urlretrieve(url, destination) - + # checks the signature, reading the archive in 256-byte chunks m = hashlib.sha256() fd = open(destination, "rb") - + while True: data = fd.read(256) if not data: break m.update(data) - + fd.close() actualSig = m.hexdigest() - + if sig != actualSig: raise IOError("Signature of the library is incorrect (got '%s' rather than '%s')" % (actualSig, sig)) - + # extracts the tarball tarFd = tarfile.open(destination, 'r:gz') tarFd.extractall("src/") tarFd.close() - + # clean up the temporary contents (fails quietly if unsuccessful) shutil.rmtree(destination, ignore_errors=True)
@@ -96,24 +96,24 @@ def installStem(): Checks out the current git head release for stem and bundles it with arm. This raises an IOError if unsuccessful. """ - + if isStemAvailable(): return - + # temporary destination for stem's git clone, guarenteed to be unoccupied # (to avoid conflicting with files that are already there) tmpFilename = tempfile.mktemp("/stem") - + # fetches stem exitStatus = os.system("git clone --quiet %s %s > /dev/null" % (STEM_REPO, tmpFilename)) if exitStatus: raise IOError("Unable to get stem from %s. Is git installed?" % STEM_REPO) - + # the destination for stem will be our directory ourDir = os.path.dirname(os.path.realpath(__file__)) - + # exports stem to our location exitStatus = os.system("(cd %s && git archive --format=tar master stem) | (cd %s && tar xf - 2> /dev/null)" % (tmpFilename, ourDir)) if exitStatus: raise IOError("Unable to install stem to %s" % ourDir) - + # Clean up the temporary contents. This isn't vital so quietly fails in case # of errors. shutil.rmtree(tmpFilename, ignore_errors=True) @@ -121,18 +121,18 @@ def installStem(): if __name__ == '__main__': majorVersion = sys.version_info[0] minorVersion = sys.version_info[1] - + if majorVersion > 2: print("arm isn't compatible beyond the python 2.x series\n") sys.exit(1) elif majorVersion < 2 or minorVersion < 5: print("arm requires python version 2.5 or greater\n") sys.exit(1) - + if not isStemAvailable(): isInstalled = promptStemInstall() if not isInstalled: sys.exit(1) - + try: import curses except ImportError: diff --git a/arm/torrcPanel.py b/arm/torrcPanel.py index 7cf34d1..d6d8123 100644 --- a/arm/torrcPanel.py +++ b/arm/torrcPanel.py @@ -31,31 +31,31 @@ class TorrcPanel(panel.Panel): Renders the current torrc or armrc with syntax highlighting in a scrollable area. """ - + def __init__(self, stdscr, configType): panel.Panel.__init__(self, stdscr, "torrc", 0) - + self.valsLock = threading.RLock() self.configType = configType self.scroll = 0 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 - + # listens for tor reload (sighup) events conn = torTools.getConn() conn.addStatusListener(self.resetListener) if conn.isAlive(): self.resetListener(None, State.INIT, None) - + def resetListener(self, controller, eventType, _): """ Reloads and displays the torrc on tor reload (sighup) events. """ - + if eventType == State.INIT: # loads the torrc and provides warnings in case of validation errors try: @@ -69,36 +69,36 @@ class TorrcPanel(panel.Panel): torConfig.getTorrc().load(True) self.redraw(True) except: pass - + def setCommentsVisible(self, isVisible): """ Sets if comments and blank lines are shown or stripped. - + Arguments: isVisible - displayed comments and blank lines if true, strips otherwise """ - + self.stripComments = not isVisible self._lastContentHeightArgs = None self.redraw(True) - + def setLineNumberVisible(self, isVisible): """ Sets if line numbers are shown or hidden. - + Arguments: isVisible - displays line numbers if true, hides otherwise """ - + self.showLineNum = isVisible self._lastContentHeightArgs = None self.redraw(True) - + def reloadTorrc(self): """ Reloads the torrc, displaying an indicator of success or failure. """ - + try: torConfig.getTorrc().load() self._lastContentHeightArgs = None @@ -106,18 +106,18 @@ class TorrcPanel(panel.Panel): resultMsg = "torrc reloaded" except IOError: resultMsg = "failed to reload torrc" - + self._lastContentHeightArgs = None self.redraw(True) popups.showMsg(resultMsg, 1) - + def handleKey(self, key): self.valsLock.acquire() isKeystrokeConsumed = True 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) @@ -128,16 +128,16 @@ class TorrcPanel(panel.Panel): elif key == ord('r') or key == ord('R'): self.reloadTorrc() else: isKeystrokeConsumed = False - + self.valsLock.release() return isKeystrokeConsumed - + def setVisible(self, isVisible): if not isVisible: self._lastContentHeightArgs = None # redraws when next displayed - + panel.Panel.setVisible(self, isVisible) - + def getHelp(self): options = [] options.append(("up arrow", "scroll up a line", None)) @@ -149,80 +149,80 @@ class TorrcPanel(panel.Panel): options.append(("r", "reload torrc", None)) options.append(("x", "reset tor (issue sighup)", None)) return options - + def draw(self, 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 == Config.TORRC: loadedTorrc = torConfig.getTorrc() loadedTorrc.getLock().acquire() confLocation = loadedTorrc.getConfigLocation() - + if not loadedTorrc.isLoaded(): renderedContents = ["### Unable to load the torrc ###"] else: renderedContents = loadedTorrc.getDisplayContents(self.stripComments) - + # constructs a mapping of line numbers to the issue on it corrections = dict((lineNum, (issue, msg)) for lineNum, issue, msg in loadedTorrc.getCorrections()) - + loadedTorrc.getLock().release() else: loadedArmrc = conf.get_config("arm") confLocation = loadedArmrc._path renderedContents = list(loadedArmrc._raw_contents) - + # 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 CONFIG["features.config.file.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.isTitleVisible(): sourceLabel = "Tor" if self.configType == Config.TORRC else "Arm" locationLabel = " (%s)" % confLocation if confLocation else "" self.addstr(0, 0, "%s Configuration File%s:" % (sourceLabel, locationLabel), curses.A_STANDOUT) - + isMultiline = False # true if we're in the middle of a multiline torrc entry 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(" ") @@ -238,15 +238,15 @@ class TorrcPanel(panel.Panel): optionEnd = lineText.find(optionText) + len(optionText) lineComp["option"][0] = lineText[:optionEnd] lineComp["argument"][0] = lineText[optionEnd:] - + # flags following lines as belonging to this multiline entry if it ends # with a slash if strippedLine: isMultiline = strippedLine.endswith("\") - + # gets the correction if lineNumber in corrections: lineIssue, lineIssueMsg = corrections[lineNumber] - + if lineIssue in (torConfig.ValidationError.DUPLICATE, torConfig.ValidationError.IS_DEFAULT): lineComp["option"][1] = curses.A_BOLD | uiTools.getColor("blue") lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("blue") @@ -258,20 +258,20 @@ class TorrcPanel(panel.Panel): # 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 = CONFIG["features.config.file.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 @@ -281,31 +281,31 @@ class TorrcPanel(panel.Panel): includeBreak = True msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.Ending.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()
diff --git a/arm/util/__init__.py b/arm/util/__init__.py index 3e21520..33897a1 100644 --- a/arm/util/__init__.py +++ b/arm/util/__init__.py @@ -1,6 +1,6 @@ """ -General purpose utilities for a variety of tasks including logging the -application's status, making cross platform system calls, parsing tor data, +General purpose utilities for a variety of tasks including logging the +application's status, making cross platform system calls, parsing tor data, and safely working with curses (hiding some of the gory details). """
diff --git a/arm/util/connections.py b/arm/util/connections.py index aa2aa2e..423a555 100644 --- a/arm/util/connections.py +++ b/arm/util/connections.py @@ -57,7 +57,7 @@ RUN_SS = "ss -nptu" # -w = no warnings # output: # tor 3873 atagar 45u IPv4 40994 0t0 TCP 10.243.55.20:45724->194.154.227.109:9001 (ESTABLISHED) -# +# # oddly, using the -p flag via: # lsof lsof -nPi -p <pid> | grep "^<process>.*(ESTABLISHED)" # is much slower (11-28% in tests I ran) @@ -79,7 +79,7 @@ RESOLVER_FINAL_FAILURE_MSG = "All connection resolvers failed" def conf_handler(key, value): if key.startswith("port.label."): portEntry = key[11:] - + divIndex = portEntry.find("-") if divIndex == -1: # single port @@ -94,7 +94,7 @@ def conf_handler(key, value): 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: @@ -111,15 +111,15 @@ def isValidIpAddress(ipStr): """ Returns true if input is a valid IPv4 address, false otherwise. """ - + # checks if theres four period separated values if not ipStr.count(".") == 3: return False - + # checks that each value in the octet are decimal values between 0-255 for ipComp in ipStr.split("."): if not ipComp.isdigit() or int(ipComp) < 0 or int(ipComp) > 255: return False - + return True
def isIpAddressPrivate(ipAddr): @@ -128,49 +128,49 @@ def isIpAddressPrivate(ipAddr): loopback, false otherwise. These include: Private ranges: 10.*, 172.16.* - 172.31.*, 192.168.* Loopback: 127.* - + Arguments: ipAddr - IP address to be checked """ - + # checks for any of the simple wildcard ranges if ipAddr.startswith("10.") or ipAddr.startswith("192.168.") or ipAddr.startswith("127."): return True - + # checks for the 172.16.* - 172.31.* range if ipAddr.startswith("172.") and ipAddr.count(".") == 3: secondOctet = ipAddr[4:ipAddr.find(".", 4)] - + if secondOctet.isdigit() and int(secondOctet) >= 16 and int(secondOctet) <= 31: return True - + return False
def ipToInt(ipAddr): """ Provides an integer representation of the ip address, suitable for sorting. - + Arguments: ipAddr - ip address to be converted """ - + total = 0 - + for comp in ipAddr.split("."): total *= 255 total += int(comp) - + return total
def getPortUsage(port): """ Provides the common use of a given port. If no useage is known then this provides None. - + Arguments: port - port number to look up """ - + return PORT_USAGE.get(port)
def getResolverCommand(resolutionCmd, processName, processPid = ""): @@ -178,23 +178,23 @@ def getResolverCommand(resolutionCmd, processName, processPid = ""): Provides the command and line filter that would be processed for the given resolver type. This raises a ValueError if either the resolutionCmd isn't recognized or a pid was requited but not provided. - + Arguments: resolutionCmd - command to use in resolving the address processName - name of the process for which connections are fetched processPid - process ID (this helps improve accuracy) """ - + if not processPid: # the pid is required for procstat resolution if resolutionCmd == Resolver.BSD_PROCSTAT: raise ValueError("procstat resolution requires a pid") - + # if the pid was undefined then match any in that field processPid = "[0-9]*" - + no_op_filter = lambda line: True - + if resolutionCmd == Resolver.PROC: return ("", no_op_filter) elif resolutionCmd == Resolver.NETSTAT: return ( @@ -238,18 +238,18 @@ def getConnections(resolutionCmd, processName, processPid = ""): - insufficient permissions - resolution command is unavailable - usage of the command is non-standard (particularly an issue for BSD) - + Arguments: resolutionCmd - command to use in resolving the address processName - name of the process for which connections are fetched processPid - process ID (this helps improve accuracy) """ - + if resolutionCmd == Resolver.PROC: # Attempts resolution via checking the proc contents. if not processPid: raise ValueError("proc resolution requires a pid") - + try: return proc.get_connections(processPid) except Exception, exc: @@ -260,9 +260,9 @@ def getConnections(resolutionCmd, processName, processPid = ""): cmd, cmd_filter = getResolverCommand(resolutionCmd, processName, processPid) results = system.call(cmd) results = filter(cmd_filter, results) - + if not results: raise IOError("No results found using: %s" % cmd) - + # parses results for the resolution command conn = [] for line in results: @@ -272,7 +272,7 @@ def getConnections(resolutionCmd, processName, processPid = ""): # the last one. comp = line.replace("(ESTABLISHED)", "").strip().split() else: comp = line.split() - + if resolutionCmd == Resolver.NETSTAT: localIp, localPort = comp[3].split(":") foreignIp, foreignPort = comp[4].split(":") @@ -292,40 +292,40 @@ def getConnections(resolutionCmd, processName, processPid = ""): elif resolutionCmd == Resolver.BSD_PROCSTAT: localIp, localPort = comp[9].split(":") foreignIp, foreignPort = comp[10].split(":") - + conn.append((localIp, localPort, foreignIp, foreignPort)) - + return conn
def isResolverAlive(processName, processPid = ""): """ This provides true if a singleton resolver instance exists for the given process/pid combination, false otherwise. - + Arguments: processName - name of the process being checked processPid - pid of the process being checked, if undefined this matches against any resolver with the process name """ - + for resolver in RESOLVERS: if not resolver._halt and resolver.processName == processName and (not processPid or resolver.processPid == processPid): return True - + return False
def getResolver(processName, processPid = "", alias=None): """ Singleton constructor for resolver instances. If a resolver already exists for the process then it's returned. Otherwise one is created and started. - + Arguments: processName - name of the process being resolved processPid - pid of the process being resolved, if undefined this matches against any resolver with the process name alias - alternative handle under which the resolver can be requested """ - + # check if one's already been created requestHandle = alias if alias else processName haltedIndex = -1 # old instance of this resolver with the _halt flag set @@ -334,11 +334,11 @@ def getResolver(processName, processPid = "", alias=None): if resolver.handle == requestHandle and (not processPid or resolver.processPid == processPid): if resolver._halt and RECREATE_HALTED_RESOLVERS: haltedIndex = i else: return resolver - + # make a new resolver r = ConnectionResolver(processName, processPid, handle = requestHandle) r.start() - + # overwrites halted instance of this resolver if it exists, otherwise append if haltedIndex == -1: RESOLVERS.append(r) else: RESOLVERS[haltedIndex] = r @@ -348,24 +348,24 @@ def getSystemResolvers(osType = None): """ Provides the types of connection resolvers available on this operating system. - + Arguments: osType - operating system type, fetched from the os module if undefined """ - + if osType == None: osType = os.uname()[0] - + if osType == "FreeBSD": resolvers = [Resolver.BSD_SOCKSTAT, Resolver.BSD_PROCSTAT, Resolver.LSOF] elif osType in ("OpenBSD", "Darwin"): resolvers = [Resolver.LSOF] else: resolvers = [Resolver.NETSTAT, Resolver.SOCKSTAT, Resolver.LSOF, Resolver.SS] - + # proc resolution, by far, outperforms the others so defaults to this is able if proc.is_available(): resolvers = [Resolver.PROC] + resolvers - + return resolvers
class ConnectionResolver(threading.Thread): @@ -376,24 +376,24 @@ class ConnectionResolver(threading.Thread): - falls back to use different resolution methods in case of repeated failures - avoids overly frequent querying of connection data, which can be demanding in terms of system resources - + Unless an overriding method of resolution is requested this defaults to choosing a resolver the following way: - + - Checks the current PATH to determine which resolvers are available. This uses the first of the following that's available: netstat, ss, lsof (picks netstat if none are found) - + - Attempts to resolve using the selection. Single failures are logged at the INFO level, and a series of failures at NOTICE. In the later case this blacklists the resolver, moving on to the next. If all resolvers fail this way then resolution's abandoned and logs a WARN message. - + The time between resolving connections, unless overwritten, is set to be either five seconds or ten times the runtime of the resolver (whichever is larger). This is to prevent systems either strapped for resources or with a vast number of connections from being burdened too heavily by this daemon. - + Parameters: processName - name of the process being resolved processPid - pid of the process being resolved @@ -406,15 +406,15 @@ class ConnectionResolver(threading.Thread): * defaultResolver - resolver used by default (None if all resolution methods have been exhausted) resolverOptions - resolvers to be cycled through (differ by os) - + * read-only """ - + def __init__(self, processName, processPid = "", resolveRate = None, handle = None): """ Initializes a new resolver daemon. When no longer needed it's suggested that this is stopped. - + Arguments: processName - name of the process being resolved processPid - pid of the process being resolved @@ -423,10 +423,10 @@ class ConnectionResolver(threading.Thread): handle - name used to query this resolver, this is the processName if undefined """ - + threading.Thread.__init__(self) self.setDaemon(True) - + self.processName = processName self.processPid = processPid self.resolveRate = resolveRate @@ -435,23 +435,23 @@ class ConnectionResolver(threading.Thread): self.lastLookup = -1 self.overwriteResolver = None self.defaultResolver = Resolver.PROC - + osType = os.uname()[0] self.resolverOptions = getSystemResolvers(osType) - + log.info("Operating System: %s, Connection Resolvers: %s" % (osType, ", ".join(self.resolverOptions))) - + # sets the default resolver to be the first found in the system's PATH # (left as netstat if none are found) for resolver in self.resolverOptions: # Resolver strings correspond to their command with the exception of bsd # resolvers. resolverCmd = resolver.replace(" (bsd)", "") - + if resolver == Resolver.PROC or system.is_available(resolverCmd): self.defaultResolver = resolver break - + self._connections = [] # connection cache (latest results) self._resolutionCounter = 0 # number of successful connection resolutions self._isPaused = False @@ -459,70 +459,70 @@ class ConnectionResolver(threading.Thread): self._cond = threading.Condition() # used for pausing the thread self._subsiquentFailures = 0 # number of failed resolutions with the default in a row self._resolverBlacklist = [] # resolvers that have failed to resolve - + # Number of sequential times the threshold rate's been too low. This is to # avoid having stray spikes up the rate. self._rateThresholdBroken = 0 - + def getOverwriteResolver(self): """ Provides the resolver connection resolution is forced to use. This returns None if it's dynamically determined. """ - + return self.overwriteResolver - + def setOverwriteResolver(self, overwriteResolver): """ Sets the resolver used for connection resolution, if None then this is automatically determined based on what is available. - + Arguments: overwriteResolver - connection resolver to be used """ - + self.overwriteResolver = overwriteResolver - + def run(self): while not self._halt: minWait = self.resolveRate if self.resolveRate else self.defaultRate timeSinceReset = time.time() - self.lastLookup - + if self._isPaused or timeSinceReset < minWait: sleepTime = max(0.2, minWait - timeSinceReset) - + self._cond.acquire() if not self._halt: self._cond.wait(sleepTime) self._cond.release() - + continue # done waiting, try again - + isDefault = self.overwriteResolver == None resolver = self.defaultResolver if isDefault else self.overwriteResolver - + # checks if there's nothing to resolve with if not resolver: self.lastLookup = time.time() # avoids a busy wait in this case continue - + try: resolveStart = time.time() connResults = getConnections(resolver, self.processName, self.processPid) lookupTime = time.time() - resolveStart - + self._connections = connResults self._resolutionCounter += 1 - + newMinDefaultRate = 100 * lookupTime if self.defaultRate < newMinDefaultRate: if self._rateThresholdBroken >= 3: # adding extra to keep the rate from frequently changing self.defaultRate = newMinDefaultRate + 0.5 - + log.trace("connection lookup time increasing to %0.1f seconds per call" % self.defaultRate) else: self._rateThresholdBroken += 1 else: self._rateThresholdBroken = 0 - + if isDefault: self._subsiquentFailures = 0 except (ValueError, IOError), exc: # this logs in a couple of cases: @@ -531,86 +531,86 @@ class ConnectionResolver(threading.Thread): # - note fail-overs for default resolution methods if str(exc).startswith("No results found using:"): log.info(exc) - + if isDefault: self._subsiquentFailures += 1 - + if self._subsiquentFailures >= RESOLVER_FAILURE_TOLERANCE: # failed several times in a row - abandon resolver and move on to another self._resolverBlacklist.append(resolver) self._subsiquentFailures = 0 - + # pick another (non-blacklisted) resolver newResolver = None for r in self.resolverOptions: if not r in self._resolverBlacklist: newResolver = r break - + if newResolver: # provide notice that failures have occurred and resolver is changing log.notice(RESOLVER_SERIAL_FAILURE_MSG % (resolver, newResolver)) else: # exhausted all resolvers, give warning log.notice(RESOLVER_FINAL_FAILURE_MSG) - + self.defaultResolver = newResolver finally: self.lastLookup = time.time() - + def getConnections(self): """ Provides the last queried connection results, an empty list if resolver has been halted. """ - + if self._halt: return [] else: return list(self._connections) - + def getResolutionCount(self): """ Provides the number of successful resolutions so far. This can be used to determine if the connection results are new for the caller or not. """ - + return self._resolutionCounter - + def getPid(self): """ Provides the pid used to narrow down connection resolution. This is an empty string if undefined. """ - + return self.processPid - + def setPid(self, processPid): """ Sets the pid used to narrow down connection resultions. - + Arguments: processPid - pid for the process we're fetching connections for """ - + self.processPid = processPid - + def setPaused(self, isPause): """ Allows or prevents further connection resolutions (this still makes use of cached results). - + Arguments: isPause - puts a freeze on further resolutions if true, allows them to continue otherwise """ - + if isPause == self._isPaused: return self._isPaused = isPause - + def stop(self): """ Halts further resolutions and terminates the thread. """ - + self._cond.acquire() self._halt = True self._cond.notifyAll() @@ -622,89 +622,89 @@ class AppResolver: stops attempting to query if it fails three times without successfully getting lsof results. """ - + def __init__(self, scriptName = "python"): """ Constructs a resolver instance. - + Arguments: scriptName - name by which to all our own entries """ - + self.scriptName = scriptName self.queryResults = {} self.resultsLock = threading.RLock() self._cond = threading.Condition() # used for pausing when waiting for results self.isResolving = False # flag set if we're in the process of making a query self.failureCount = 0 # -1 if we've made a successful query - + def getResults(self, maxWait=0): """ Provides the last queried results. If we're in the process of making a query then we can optionally block for a time to see if it finishes. - + Arguments: maxWait - maximum second duration to block on getting results before returning """ - + self._cond.acquire() if self.isResolving and maxWait > 0: self._cond.wait(maxWait) self._cond.release() - + self.resultsLock.acquire() results = dict(self.queryResults) self.resultsLock.release() - + return results - + def resolve(self, ports): """ Queues the given listing of ports to be resolved. This clears the last set of results when completed. - + Arguments: ports - list of ports to be resolved to applications """ - + if self.failureCount < 3: self.isResolving = True t = threading.Thread(target = self._queryApplications, kwargs = {"ports": ports}) t.setDaemon(True) t.start() - + def _queryApplications(self, ports=[]): """ Performs an lsof lookup on the given ports to get the command/pid tuples. - + Arguments: ports - list of ports to be resolved to applications """ - + # atagar@fenrir:~/Desktop/arm$ lsof -i tcp:51849 -i tcp:37277 # COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME # tor 2001 atagar 14u IPv4 14048 0t0 TCP localhost:9051->localhost:37277 (ESTABLISHED) # tor 2001 atagar 15u IPv4 22024 0t0 TCP localhost:9051->localhost:51849 (ESTABLISHED) # python 2462 atagar 3u IPv4 14047 0t0 TCP localhost:37277->localhost:9051 (ESTABLISHED) # python 3444 atagar 3u IPv4 22023 0t0 TCP localhost:51849->localhost:9051 (ESTABLISHED) - + if not ports: self.resultsLock.acquire() self.queryResults = {} self.isResolving = False self.resultsLock.release() - + # wakes threads waiting on results self._cond.acquire() self._cond.notifyAll() self._cond.release() - + return - + results = {} lsofArgs = [] - + # Uses results from the last query if we have any, otherwise appends the # port to the lsof command. This has the potential for persisting dirty # results but if we're querying by the dynamic port on the local tcp @@ -714,11 +714,11 @@ class AppResolver: if port in self.queryResults: results[port] = self.queryResults[port] else: lsofArgs.append("-i tcp:%s" % port) - + if lsofArgs: lsofResults = system.call("lsof -nP " + " ".join(lsofArgs)) else: lsofResults = None - + if not lsofResults and self.failureCount != -1: # lsof query failed and we aren't yet sure if it's possible to # successfully get results on this platform @@ -728,26 +728,26 @@ class AppResolver: elif lsofResults: # (iPort, oPort) tuple for our own process, if it was fetched ourConnection = None - + for line in lsofResults: lineComp = line.split() - + if len(lineComp) == 10 and lineComp[9] == "(ESTABLISHED)": cmd, pid, _, _, _, _, _, _, portMap, _ = lineComp - + if "->" in portMap: iPort, oPort = portMap.split("->") iPort = iPort.split(":")[1] oPort = oPort.split(":")[1] - + # entry belongs to our own process if pid == str(os.getpid()): cmd = self.scriptName ourConnection = (iPort, oPort) - + if iPort.isdigit() and oPort.isdigit(): newEntry = (iPort, oPort, cmd, pid) - + # adds the entry under the key of whatever we queried it with # (this might be both the inbound _and_ outbound ports) for portMatch in (iPort, oPort): @@ -755,27 +755,27 @@ class AppResolver: if portMatch in results: results[portMatch].append(newEntry) else: results[portMatch] = [newEntry] - + # making the lsof call generated an extraneous sh entry for our own connection if ourConnection: for ourPort in ourConnection: if ourPort in results: shIndex = None - + for i in range(len(results[ourPort])): if results[ourPort][i][2] == "sh": shIndex = i break - + if shIndex != None: del results[ourPort][shIndex] - + self.resultsLock.acquire() self.failureCount = -1 self.queryResults = results self.isResolving = False self.resultsLock.release() - + # wakes threads waiting on results self._cond.acquire() self._cond.notifyAll() diff --git a/arm/util/hostnames.py b/arm/util/hostnames.py index a58eb94..a829f9f 100644 --- a/arm/util/hostnames.py +++ b/arm/util/hostnames.py @@ -63,7 +63,7 @@ def start(): not necessary since resolving any address will start the service if it isn't already running. """ - + global RESOLVER RESOLVER_LOCK.acquire() if not isRunning(): RESOLVER = _Resolver() @@ -74,7 +74,7 @@ def stop(): Halts further resolutions and stops the service. This joins on the resolver's thread pool and clears its lookup cache. """ - + global RESOLVER RESOLVER_LOCK.acquire() if isRunning(): @@ -83,7 +83,7 @@ def stop(): # all calls currently in progress can still proceed on the RESOLVER's local # references. resolverRef, RESOLVER = RESOLVER, None - + # joins on its worker thread pool resolverRef.stop() for t in resolverRef.threadPool: t.join() @@ -94,12 +94,12 @@ def setPaused(isPause): Allows or prevents further hostname resolutions (resolutions still make use of cached entries if available). This starts the service if it isn't already running. - + Arguments: isPause - puts a freeze on further resolutions if true, allows them to continue otherwise """ - + # makes sure a running resolver is set with the pausing setting RESOLVER_LOCK.acquire() start() @@ -110,14 +110,14 @@ def isRunning(): """ Returns True if the service is currently running, False otherwise. """ - + return bool(RESOLVER)
def isPaused(): """ Returns True if the resolver is paused, False otherwise. """ - + resolverRef = RESOLVER if resolverRef: return resolverRef.isPaused else: return False @@ -127,7 +127,7 @@ def isResolving(): Returns True if addresses are currently waiting to be resolved, False otherwise. """ - + resolverRef = RESOLVER if resolverRef: return not resolverRef.unresolvedQueue.empty() else: return False @@ -139,14 +139,14 @@ def resolve(ipAddr, timeout = 0, suppressIOExc = True): lookup if not. This provides None if the lookup fails (with a suppressed exception) or timeout is reached without resolution. This starts the service if it isn't already running. - + If paused this simply returns the cached reply (no request is queued and returns immediately regardless of the timeout argument). - + Requests may raise the following exceptions: - ValueError - address was unresolvable (includes the DNS error response) - IOError - lookup failed due to os or network issues (suppressed by default) - + Arguments: ipAddr - ip address to be resolved timeout - maximum duration to wait for a resolution (blocks to @@ -154,7 +154,7 @@ def resolve(ipAddr, timeout = 0, suppressIOExc = True): suppressIOExc - suppresses lookup errors and re-runs failed calls if true, raises otherwise """ - + # starts the service if it isn't already running (making sure we have an # instance in a thread safe fashion before continuing) resolverRef = RESOLVER @@ -163,11 +163,11 @@ def resolve(ipAddr, timeout = 0, suppressIOExc = True): start() resolverRef = RESOLVER RESOLVER_LOCK.release() - + if resolverRef.isPaused: # get cache entry, raising if an exception and returning if a hostname cacheRef = resolverRef.resolvedCache - + if ipAddr in cacheRef: entry = cacheRef[ipAddr][0] if suppressIOExc and type(entry) == IOError: return None @@ -179,7 +179,7 @@ def resolve(ipAddr, timeout = 0, suppressIOExc = True): # suppression since these error may be transient) cacheRef = resolverRef.resolvedCache flush = ipAddr in cacheRef and type(cacheRef[ipAddr]) == IOError - + try: return resolverRef.getHostname(ipAddr, timeout, flush) except IOError: return None else: return resolverRef.getHostname(ipAddr, timeout) @@ -189,7 +189,7 @@ def getPendingCount(): Provides an approximate count of the number of addresses still pending resolution. """ - + resolverRef = RESOLVER if resolverRef: return resolverRef.unresolvedQueue.qsize() else: return 0 @@ -198,7 +198,7 @@ def getRequestCount(): """ Provides the number of resolutions requested since starting the service. """ - + resolverRef = RESOLVER if resolverRef: return resolverRef.totalResolves else: return 0 @@ -208,11 +208,11 @@ def _resolveViaSocket(ipAddr): Performs hostname lookup via the socket module's gethostbyaddr function. This raises an IOError if the lookup fails (network issue) and a ValueError in case of DNS errors (address unresolvable). - + Arguments: ipAddr - ip address to be resolved """ - + try: # provides tuple like: ('localhost', [], ['127.0.0.1']) return socket.gethostbyaddr(ipAddr)[0] @@ -226,13 +226,13 @@ def _resolveViaHost(ipAddr): Performs a host lookup for the given IP, returning the resolved hostname. This raises an IOError if the lookup fails (os or network issue), and a ValueError in the case of DNS errors (address is unresolvable). - + Arguments: ipAddr - ip address to be resolved """ - + hostname = system.call("host %s" % ipAddr)[0].split()[-1:][0] - + if hostname == "reached": # got message: ";; connection timed out; no servers could be reached" raise IOError("lookup timed out") @@ -248,12 +248,12 @@ class _Resolver(): Performs reverse DNS resolutions. Lookups are a network bound operation so this spawns a pool of worker threads to do several at a time in parallel. """ - + def __init__(self): # IP Address => (hostname/error, age), resolution failures result in a # ValueError with the lookup's status self.resolvedCache = {} - + self.resolvedLock = threading.RLock() # governs concurrent access when modifying resolvedCache self.unresolvedQueue = Queue.Queue() # unprocessed lookup requests self.recentQueries = [] # recent resolution requests to prevent duplicate requests @@ -262,42 +262,42 @@ class _Resolver(): self.isPaused = False # prevents further resolutions if true self.halt = False # if true, tells workers to stop self.cond = threading.Condition() # used for pausing threads - + # Determines if resolutions are made using os 'host' calls or python's # 'socket.gethostbyaddr'. The following checks if the system has the # gethostbyname_r function, which determines if python resolutions can be # done in parallel or not. If so, this is preferable. isSocketResolutionParallel = distutils.sysconfig.get_config_var("HAVE_GETHOSTBYNAME_R") self.useSocketResolution = CONFIG["queries.hostnames.useSocketModule"] and isSocketResolutionParallel - + for _ in range(CONFIG["queries.hostnames.poolSize"]): t = threading.Thread(target = self._workerLoop) t.setDaemon(True) t.start() self.threadPool.append(t) - + def getHostname(self, ipAddr, timeout, flushCache = False): """ Provides the hostname, queuing the request and returning None if the timeout is reached before resolution. If a problem's encountered then this either raises an IOError (for os and network issues) or ValueError (for DNS resolution errors). - + Arguments: ipAddr - ip address to be resolved timeout - maximum duration to wait for a resolution (blocks to completion if None) flushCache - if true the cache is skipped and address re-resolved """ - + # if outstanding requests are done then clear recentQueries to allow # entries removed from the cache to be re-run if self.unresolvedQueue.empty(): self.recentQueries = [] - + # copies reference cache (this is important in case the cache is trimmed # during this call) cacheRef = self.resolvedCache - + if not flushCache and ipAddr in cacheRef: # cached response is available - raise if an error, return if a hostname response = cacheRef[ipAddr][0] @@ -308,11 +308,11 @@ class _Resolver(): self.totalResolves += 1 self.recentQueries.append(ipAddr) self.unresolvedQueue.put(ipAddr) - + # periodically check cache if requester is willing to wait if timeout == None or timeout > 0: startTime = time.time() - + while timeout == None or time.time() - startTime < timeout: if ipAddr in cacheRef: # address was resolved - raise if an error, return if a hostname @@ -320,19 +320,19 @@ class _Resolver(): if isinstance(response, Exception): raise response else: return response else: time.sleep(0.1) - + return None # timeout reached without resolution - + def stop(self): """ Halts further resolutions and terminates the thread. """ - + self.cond.acquire() self.halt = True self.cond.notifyAll() self.cond.release() - + def _workerLoop(self): """ Simple producer-consumer loop followed by worker threads. This takes @@ -340,7 +340,7 @@ class _Resolver(): adds its results or the error to the resolved cache. Resolver reference provides shared resources used by the thread pool. """ - + while not self.halt: # if resolver is paused then put a hold on further resolutions if self.isPaused: @@ -348,7 +348,7 @@ class _Resolver(): if not self.halt: self.cond.wait(1) self.cond.release() continue - + # snags next available ip, timeout is because queue can't be woken up # when 'halt' is set try: ipAddr = self.unresolvedQueue.get_nowait() @@ -359,36 +359,36 @@ class _Resolver(): self.cond.release() continue if self.halt: break - + try: if self.useSocketResolution: result = _resolveViaSocket(ipAddr) else: result = _resolveViaHost(ipAddr) except IOError, exc: result = exc # lookup failed except ValueError, exc: result = exc # dns error - + self.resolvedLock.acquire() self.resolvedCache[ipAddr] = (result, RESOLVER_COUNTER.next()) - + # trim cache if excessively large (clearing out oldest entries) if len(self.resolvedCache) > CONFIG["cache.hostnames.size"]: # Providing for concurrent, non-blocking calls require that entries are # never removed from the cache, so this creates a new, trimmed version # instead. - + # determines minimum age of entries to be kept currentCount = RESOLVER_COUNTER.next() newCacheSize = CONFIG["cache.hostnames.size"] - CONFIG["cache.hostnames.trimSize"] threshold = currentCount - newCacheSize newCache = {} - + msg = "trimming hostname cache from %i entries to %i" % (len(self.resolvedCache), newCacheSize) log.info(msg) - + # checks age of each entry, adding to toDelete if too old for ipAddr, entry in self.resolvedCache.iteritems(): if entry[1] >= threshold: newCache[ipAddr] = entry - + self.resolvedCache = newCache - + self.resolvedLock.release() - + diff --git a/arm/util/panel.py b/arm/util/panel.py index 17b99b7..fb33c7a 100644 --- a/arm/util/panel.py +++ b/arm/util/panel.py @@ -13,7 +13,7 @@ from arm.util import textInput, uiTools
from stem.util import log
-# global ui lock governing all panel instances (curses isn't thread save and +# global ui lock governing all panel instances (curses isn't thread save and # concurrency bugs produce especially sinister glitches) CURSES_LOCK = RLock()
@@ -36,16 +36,16 @@ class Panel(): - gracefully handle terminal resizing - clip text that falls outside the panel - convenience methods for word wrap, in-line formatting, etc - + This uses a design akin to Swing where panel instances provide their display implementation by overwriting the draw() method, and are redrawn with redraw(). """ - + def __init__(self, parent, name, top, left=0, height=-1, width=-1): """ Creates a durable wrapper for a curses subwindow in the given parent. - + Arguments: parent - parent curses window name - identifier for the panel @@ -54,112 +54,112 @@ class Panel(): height - maximum height of panel (uses all available space if -1) width - maximum width of panel (uses all available space if -1) """ - + # The not-so-pythonic getters for these parameters are because some # implementations aren't entirely deterministic (for instance panels # might chose their height based on its parent's current width). - + self.panelName = name self.parent = parent self.visible = False self.titleVisible = True - + # Attributes for pausing. The pauseAttr contains variables our getAttr # method is tracking, and the pause buffer has copies of the values from # when we were last unpaused (unused unless we're paused). - + self.paused = False self.pauseAttr = [] self.pauseBuffer = {} self.pauseTime = -1 - + self.top = top self.left = left self.height = height self.width = width - + # The panel's subwindow instance. This is made available to implementors # via their draw method and shouldn't be accessed directly. - # + # # This is None if either the subwindow failed to be created or needs to be # remade before it's used. The later could be for a couple reasons: # - The subwindow was never initialized. # - Any of the parameters used for subwindow initialization have changed. self.win = None - + self.maxY, self.maxX = -1, -1 # subwindow dimensions when last redrawn - + def getName(self): """ Provides panel's identifier. """ - + return self.panelName - + def isTitleVisible(self): """ True if the title is configured to be visible, False otherwise. """ - + return self.titleVisible - + def setTitleVisible(self, isVisible): """ Configures the panel's title to be visible or not when it's next redrawn. This is not guarenteed to be respected (not all panels have a title). """ - + self.titleVisible = isVisible - + def getParent(self): """ Provides the parent used to create subwindows. """ - + return self.parent - + def setParent(self, parent): """ Changes the parent used to create subwindows. - + Arguments: parent - parent curses window """ - + if self.parent != parent: self.parent = parent self.win = None - + def isVisible(self): """ Provides if the panel's configured to be visible or not. """ - + return self.visible - + def setVisible(self, isVisible): """ Toggles if the panel is visible or not. - + Arguments: isVisible - panel is redrawn when requested if true, skipped otherwise """ - + self.visible = isVisible - + def isPaused(self): """ Provides if the panel's configured to be paused or not. """ - + return self.paused - + def setPauseAttr(self, attr): """ Configures the panel to track the given attribute so that getAttr provides the value when it was last unpaused (or its current value if we're currently unpaused). For instance... - + > self.setPauseAttr("myVar") > self.myVar = 5 > self.myVar = 6 # self.getAttr("myVar") -> 6 @@ -167,160 +167,160 @@ class Panel(): > self.myVar = 7 # self.getAttr("myVar") -> 6 > self.setPaused(False) > self.myVar = 7 # self.getAttr("myVar") -> 7 - + Arguments: attr - parameter to be tracked for getAttr """ - + self.pauseAttr.append(attr) self.pauseBuffer[attr] = self.copyAttr(attr) - + def getAttr(self, attr): """ Provides the value of the given attribute when we were last unpaused. If we're currently unpaused then this is the current value. If untracked this returns None. - + Arguments: attr - local variable to be returned """ - + if not attr in self.pauseAttr: return None elif self.paused: return self.pauseBuffer[attr] else: return self.__dict__.get(attr) - + def copyAttr(self, attr): """ Provides a duplicate of the given configuration value, suitable for the pause buffer. - + Arguments: attr - parameter to be provided back """ - + currentValue = self.__dict__.get(attr) return copy.copy(currentValue) - + def setPaused(self, isPause, suppressRedraw = False): """ Toggles if the panel is paused or not. This causes the panel to be redrawn when toggling is pause state unless told to do otherwise. This is important when pausing since otherwise the panel's display could change when redrawn for other reasons. - + This returns True if the panel's pause state was changed, False otherwise. - + Arguments: isPause - freezes the state of the pause attributes if true, makes them editable otherwise suppressRedraw - if true then this will never redraw the panel """ - + if isPause != self.paused: if isPause: self.pauseTime = time.time() self.paused = isPause - + if isPause: # copies tracked attributes so we know what they were before pausing for attr in self.pauseAttr: self.pauseBuffer[attr] = self.copyAttr(attr) - + if not suppressRedraw: self.redraw(True) return True else: return False - + def getPauseTime(self): """ Provides the time that we were last paused, returning -1 if we've never been paused. """ - + return self.pauseTime - + def getTop(self): """ Provides the position subwindows are placed at within its parent. """ - + return self.top - + def setTop(self, top): """ Changes the position where subwindows are placed within its parent. - + Arguments: top - positioning of top within parent """ - + if self.top != top: self.top = top self.win = None - + def getLeft(self): """ Provides the left position where this subwindow is placed within its parent. """ - + return self.left - + def setLeft(self, left): """ Changes the left position where this subwindow is placed within its parent. - + Arguments: left - positioning of top within parent """ - + if self.left != left: self.left = left self.win = None - + def getHeight(self): """ Provides the height used for subwindows (-1 if it isn't limited). """ - + return self.height - + def setHeight(self, height): """ Changes the height used for subwindows. This uses all available space if -1. - + Arguments: height - maximum height of panel (uses all available space if -1) """ - + if self.height != height: self.height = height self.win = None - + def getWidth(self): """ Provides the width used for subwindows (-1 if it isn't limited). """ - + return self.width - + def setWidth(self, width): """ Changes the width used for subwindows. This uses all available space if -1. - + Arguments: width - maximum width of panel (uses all available space if -1) """ - + if self.width != width: self.width = width self.win = None - + def getPreferredSize(self): """ Provides the dimensions the subwindow would use when next redrawn, given that none of the properties of the panel or parent change before then. This returns a tuple of (height, width). """ - + newHeight, newWidth = self.parent.getmaxyx() setHeight, setWidth = self.getHeight(), self.getWidth() newHeight = max(0, newHeight - self.top) @@ -328,75 +328,75 @@ class Panel(): if setHeight != -1: newHeight = min(newHeight, setHeight) if setWidth != -1: newWidth = min(newWidth, setWidth) return (newHeight, newWidth) - + def handleKey(self, key): """ Handler for user input. This returns true if the key press was consumed, false otherwise. - + Arguments: key - keycode for the key pressed """ - + return False - + def getHelp(self): """ Provides help information for the controls this page provides. This is a list of tuples of the form... (control, description, status) """ - + return [] - + def draw(self, width, height): """ - Draws display's content. This is meant to be overwritten by + Draws display's content. This is meant to be overwritten by implementations and not called directly (use redraw() instead). The dimensions provided are the drawable dimensions, which in terms of width is a column less than the actual space. - + Arguments: width - horizontal space available for content height - vertical space available for content """ - + pass - + def redraw(self, forceRedraw=False, block=False): """ Clears display and redraws its content. This can skip redrawing content if able (ie, the subwindow's unchanged), instead just refreshing the display. - + Arguments: forceRedraw - forces the content to be cleared and redrawn if true block - if drawing concurrently with other panels this determines if the request is willing to wait its turn or should be abandoned """ - + # skipped if not currently visible or activity has been halted if not self.isVisible() or HALT_ACTIVITY: return - + # if the panel's completely outside its parent then this is a no-op newHeight, newWidth = self.getPreferredSize() if newHeight == 0 or newWidth == 0: self.win = None return - + # recreates the subwindow if necessary isNewWindow = self._resetSubwindow() - + # The reset argument is disregarded in a couple of situations: # - The subwindow's been recreated (obviously it then doesn't have the old # content to refresh). # - The subwindow's dimensions have changed since last drawn (this will # likely change the content's layout) - + subwinMaxY, subwinMaxX = self.win.getmaxyx() if isNewWindow or subwinMaxY != self.maxY or subwinMaxX != self.maxX: forceRedraw = True - + self.maxY, self.maxX = subwinMaxY, subwinMaxX if not CURSES_LOCK.acquire(block): return try: @@ -406,19 +406,19 @@ class Panel(): self.win.refresh() finally: CURSES_LOCK.release() - + def hline(self, y, x, length, attr=curses.A_NORMAL): """ Draws a horizontal line. This should only be called from the context of a panel's draw method. - + Arguments: y - vertical location x - horizontal location length - length the line spans attr - text attributes """ - + if self.win and self.maxX > x and self.maxY > y: try: drawLength = min(length, self.maxX - x) @@ -426,19 +426,19 @@ class Panel(): except: # in edge cases drawing could cause a _curses.error pass - + def vline(self, y, x, length, attr=curses.A_NORMAL): """ Draws a vertical line. This should only be called from the context of a panel's draw method. - + Arguments: y - vertical location x - horizontal location length - length the line spans attr - text attributes """ - + if self.win and self.maxX > x and self.maxY > y: try: drawLength = min(length, self.maxY - y) @@ -446,40 +446,40 @@ class Panel(): except: # in edge cases drawing could cause a _curses.error pass - + def addch(self, y, x, char, attr=curses.A_NORMAL): """ Draws a single character. This should only be called from the context of a panel's draw method. - + Arguments: y - vertical location x - horizontal location char - character to be drawn attr - text attributes """ - + if self.win and self.maxX > x and self.maxY > y: try: self.win.addch(y, x, char, attr) except: # in edge cases drawing could cause a _curses.error pass - + def addstr(self, y, x, msg, attr=curses.A_NORMAL): """ Writes string to subwindow if able. This takes into account screen bounds to avoid making curses upset. This should only be called from the context of a panel's draw method. - + Arguments: y - vertical location x - horizontal location msg - text to be added attr - text attributes """ - - # subwindows need a single character buffer (either in the x or y + + # subwindows need a single character buffer (either in the x or y # direction) from actual content to prevent crash when shrank if self.win and self.maxX > x and self.maxY > y: try: @@ -488,7 +488,7 @@ class Panel(): # this might produce a _curses.error during edge cases, for instance # when resizing with visible popups pass - + def addfstr(self, y, x, msg): """ Writes string to subwindow. The message can contain xhtml-style tags for @@ -497,36 +497,36 @@ class Panel(): <u>text</u> underline <h>text</h> highlight <[color]>text</[color]> use color (see uiTools.getColor() for constants) - + Tag nesting is supported and tag closing is strictly enforced (raising an exception for invalid formatting). Unrecognized tags are treated as normal text. This should only be called from the context of a panel's draw method. - + Text in multiple color tags (for instance "<blue><red>hello</red></blue>") uses the bitwise OR of those flags (hint: that's probably not what you want). - + Arguments: y - vertical location x - horizontal location msg - formatted text to be added """ - + if self.win and self.maxY > y: formatting = [curses.A_NORMAL] expectedCloseTags = [] unusedMsg = msg - + while self.maxX > x and len(unusedMsg) > 0: # finds next consumeable tag (left as None if there aren't any left) nextTag, tagStart, tagEnd = None, -1, -1 - + tmpChecked = 0 # portion of the message cleared for having any valid tags expectedTags = FORMAT_TAGS.keys() + expectedCloseTags while nextTag == None: tagStart = unusedMsg.find("<", tmpChecked) tagEnd = unusedMsg.find(">", tagStart) + 1 if tagStart != -1 else -1 - + if tagStart == -1 or tagEnd == -1: break # no more tags to consume else: # check if the tag we've found matches anything being expected @@ -536,7 +536,7 @@ class Panel(): else: # not a valid tag - narrow search to everything after it tmpChecked = tagEnd - + # splits into text before and after tag if nextTag: msgSegment = unusedMsg[:tagStart] @@ -544,18 +544,18 @@ class Panel(): else: msgSegment = unusedMsg unusedMsg = "" - + # adds text before tag with current formatting attr = 0 for format in formatting: attr |= format self.win.addstr(y, x, msgSegment[:self.maxX - x - 1], attr) x += len(msgSegment) - + # applies tag attributes for future text if nextTag: formatTag = "<" + nextTag[2:] if nextTag.startswith("</") else nextTag formatMatch = FORMAT_TAGS[formatTag][0](FORMAT_TAGS[formatTag][1]) - + if not nextTag.startswith("</"): # open tag - add formatting expectedCloseTags.append("</" + nextTag[1:]) @@ -564,25 +564,25 @@ class Panel(): # close tag - remove formatting expectedCloseTags.remove(nextTag) formatting.remove(formatMatch) - + # only check for unclosed tags if we processed the whole message (if we # stopped processing prematurely it might still be valid) if expectedCloseTags and not unusedMsg: # if we're done then raise an exception for any unclosed tags (tisk, tisk) baseMsg = "Unclosed formatting tag%s:" % ("s" if len(expectedCloseTags) > 1 else "") raise ValueError("%s: '%s'\n "%s"" % (baseMsg, "', '".join(expectedCloseTags), msg)) - + def getstr(self, y, x, initialText = "", format = None, maxWidth = None, validator = None): """ Provides a text field where the user can input a string, blocking until they've done so and returning the result. If the user presses escape then this terminates and provides back None. This should only be called from the context of a panel's draw method. - + This blanks any content within the space that the input field is rendered (otherwise stray characters would be interpreted as part of the initial input). - + Arguments: y - vertical location x - horizontal location @@ -591,60 +591,60 @@ class Panel(): maxWidth - maximum width for the text field validator - custom TextInputValidator for handling keybindings """ - + if not format: format = curses.A_NORMAL - + # makes cursor visible try: previousCursorState = curses.curs_set(1) except curses.error: previousCursorState = 0 - + # temporary subwindow for user input displayWidth = self.getPreferredSize()[1] if maxWidth: displayWidth = min(displayWidth, maxWidth + x) inputSubwindow = self.parent.subwin(1, displayWidth - x, self.top + y, self.left + x) - + # blanks the field's area, filling it with the font in case it's hilighting inputSubwindow.clear() inputSubwindow.bkgd(' ', format) - + # prepopulates the initial text if initialText: inputSubwindow.addstr(0, 0, initialText[:displayWidth - x - 1], format) - + # Displays the text field, blocking until the user's done. This closes the # text panel and returns userInput to the initial text if the user presses # escape. - + textbox = curses.textpad.Textbox(inputSubwindow) - + if not validator: validator = textInput.BasicValidator() - + textbox.win.attron(format) userInput = textbox.edit(lambda key: validator.validate(key, textbox)).strip() textbox.win.attroff(format) if textbox.lastcmd == curses.ascii.BEL: userInput = None - + # reverts visability settings try: curses.curs_set(previousCursorState) except curses.error: pass - + return userInput - + def addScrollBar(self, top, bottom, size, drawTop = 0, drawBottom = -1, drawLeft = 0): """ Draws a left justified scroll bar reflecting position within a vertical listing. This is shorted if necessary, and left undrawn if no space is available. The bottom is squared off, having a layout like: - | + | *| *| *| | -+ - + This should only be called from the context of a panel's draw method. - + Arguments: top - list index for the top-most visible element bottom - list index for the bottom-most visible element @@ -654,38 +654,38 @@ class Panel(): span to the bottom of the panel drawLeft - left offset at which to draw the scroll bar """ - + if (self.maxY - drawTop) < 2: return # not enough room - + # sets drawBottom to be the actual row on which the scrollbar should end if drawBottom == -1: drawBottom = self.maxY - 1 else: drawBottom = min(drawBottom, self.maxY - 1) - + # determines scrollbar dimensions scrollbarHeight = drawBottom - drawTop sliderTop = scrollbarHeight * top / size sliderSize = scrollbarHeight * (bottom - top) / size - + # ensures slider isn't at top or bottom unless really at those extreme bounds if top > 0: sliderTop = max(sliderTop, 1) if bottom != size: sliderTop = min(sliderTop, scrollbarHeight - sliderSize - 2) - + # avoids a rounding error that causes the scrollbar to be too low when at # the bottom if bottom == size: sliderTop = scrollbarHeight - sliderSize - 1 - + # draws scrollbar slider for i in range(scrollbarHeight): if i >= sliderTop and i <= sliderTop + sliderSize: self.addstr(i + drawTop, drawLeft, " ", curses.A_STANDOUT) else: self.addstr(i + drawTop, drawLeft, " ") - + # draws box around the scroll bar self.vline(drawTop, drawLeft + 1, drawBottom - 1) self.addch(drawBottom, drawLeft + 1, curses.ACS_LRCORNER) self.addch(drawBottom, drawLeft, curses.ACS_HLINE) - + def _resetSubwindow(self): """ Create a new subwindow instance for the panel if: @@ -698,13 +698,13 @@ class Panel(): subwindows are never restored to their proper position, resulting in graphical glitches if we draw to them. - The preferred size is smaller than the actual size (should shrink). - + This returns True if a new subwindow instance was created, False otherwise. """ - + newHeight, newWidth = self.getPreferredSize() if newHeight == 0: return False # subwindow would be outside its parent - + # determines if a new subwindow should be recreated recreate = self.win == None if self.win: @@ -712,17 +712,17 @@ class Panel(): recreate |= subwinMaxY < newHeight # check for vertical growth recreate |= self.top > self.win.getparyx()[0] # check for displacement recreate |= subwinMaxX > newWidth or subwinMaxY > newHeight # shrinking - + # I'm not sure if recreating subwindows is some sort of memory leak but the # Python curses bindings seem to lack all of the following: # - subwindow deletion (to tell curses to free the memory) # - subwindow moving/resizing (to restore the displaced windows) - # so this is the only option (besides removing subwindows entirely which + # so this is the only option (besides removing subwindows entirely which # would mean far more complicated code and no more selective refreshing) - + if recreate: self.win = self.parent.subwin(newHeight, newWidth, self.top, self.left) - + # note: doing this log before setting win produces an infinite loop log.debug("recreating panel '%s' with the dimensions of %i/%i" % (self.getName(), newHeight, newWidth)) return recreate diff --git a/arm/util/sysTools.py b/arm/util/sysTools.py index 80e5c12..cd5814d 100644 --- a/arm/util/sysTools.py +++ b/arm/util/sysTools.py @@ -30,30 +30,30 @@ def getSysCpuUsage(): unfortunately, doesn't seem to take popen calls into account. This returns a float representing the percentage used. """ - + currentTime = time.time() - + # removes any runtimes outside of our sampling period while RUNTIMES and currentTime - RUNTIMES[0][0] > SAMPLING_PERIOD: RUNTIMES.pop(0) - + runtimeSum = sum([entry[1] for entry in RUNTIMES]) return runtimeSum / SAMPLING_PERIOD
def getResourceTracker(pid, noSpawn = False): """ Provides a running singleton ResourceTracker instance for the given pid. - + Arguments: pid - pid of the process being tracked noSpawn - returns None rather than generating a singleton instance if True """ - + if pid in RESOURCE_TRACKERS: tracker = RESOURCE_TRACKERS[pid] if tracker.isAlive(): return tracker else: del RESOURCE_TRACKERS[pid] - + if noSpawn: return None tracker = ResourceTracker(pid, CONFIG["queries.resourceUsage.rate"]) RESOURCE_TRACKERS[pid] = tracker @@ -65,93 +65,93 @@ class ResourceTracker(threading.Thread): Periodically fetches the resource usage (cpu and memory usage) for a given process. """ - + def __init__(self, processPid, resolveRate): """ Initializes a new resolver daemon. When no longer needed it's suggested that this is stopped. - + Arguments: processPid - pid of the process being tracked resolveRate - time between resolving resource usage, resolution is disabled if zero """ - + threading.Thread.__init__(self) self.setDaemon(True) - + self.processPid = processPid self.resolveRate = resolveRate - + self.cpuSampling = 0.0 # latest cpu usage sampling self.cpuAvg = 0.0 # total average cpu usage self.memUsage = 0 # last sampled memory usage in bytes self.memUsagePercentage = 0.0 # percentage cpu usage - + # resolves usage via proc results if true, ps otherwise self._useProc = proc.is_available() - + # used to get the deltas when querying cpu time self._lastCpuTotal = 0 - + self.lastLookup = -1 self._halt = False # terminates thread if true self._valLock = threading.RLock() self._cond = threading.Condition() # used for pausing the thread - + # number of successful calls we've made self._runCount = 0 - + # sequential times we've failed with this method of resolution self._failureCount = 0 - + def getResourceUsage(self): """ Provides the last cached resource usage as a tuple of the form: (cpuUsage_sampling, cpuUsage_avg, memUsage_bytes, memUsage_percent) """ - + self._valLock.acquire() results = (self.cpuSampling, self.cpuAvg, self.memUsage, self.memUsagePercentage) self._valLock.release() - + return results - + def getRunCount(self): """ Provides the number of times we've successfully fetched the resource usages. """ - + return self._runCount - + def lastQueryFailed(self): """ Provides true if, since we fetched the currently cached results, we've failed to get new results. False otherwise. """ - + return self._failureCount != 0 - + def run(self): while not self._halt: timeSinceReset = time.time() - self.lastLookup - + if self.resolveRate == 0: self._cond.acquire() if not self._halt: self._cond.wait(0.2) self._cond.release() - + continue elif timeSinceReset < self.resolveRate: sleepTime = max(0.2, self.resolveRate - timeSinceReset) - + self._cond.acquire() if not self._halt: self._cond.wait(sleepTime) self._cond.release() - + continue # done waiting, try again - + newValues = {} try: if self._useProc: @@ -161,28 +161,28 @@ class ResourceTracker(threading.Thread): newValues["cpuSampling"] = cpuDelta / timeSinceReset newValues["cpuAvg"] = totalCpuTime / (time.time() - float(startTime)) newValues["_lastCpuTotal"] = totalCpuTime - + memUsage = int(proc.get_memory_usage(self.processPid)[0]) totalMemory = proc.get_physical_memory() newValues["memUsage"] = memUsage newValues["memUsagePercentage"] = float(memUsage) / totalMemory else: # the ps call formats results as: - # + # # TIME ELAPSED RSS %MEM # 3-08:06:32 21-00:00:12 121844 23.5 - # + # # or if Tor has only recently been started: - # + # # TIME ELAPSED RSS %MEM # 0:04.40 37:57 18772 0.9 - + psCall = system.call("ps -p %s -o cputime,etime,rss,%%mem" % self.processPid) - + isSuccessful = False if psCall and len(psCall) >= 2: stats = psCall[1].strip().split() - + if len(stats) == 4: try: totalCpuTime = str_tools.parse_short_time_label(stats[0]) @@ -191,24 +191,24 @@ class ResourceTracker(threading.Thread): newValues["cpuSampling"] = cpuDelta / timeSinceReset newValues["cpuAvg"] = totalCpuTime / uptime newValues["_lastCpuTotal"] = totalCpuTime - + newValues["memUsage"] = int(stats[2]) * 1024 # ps size is in kb newValues["memUsagePercentage"] = float(stats[3]) / 100.0 isSuccessful = True except ValueError, exc: pass - + if not isSuccessful: raise IOError("unrecognized output from ps: %s" % psCall) except IOError, exc: newValues = {} self._failureCount += 1 - + if self._useProc: if self._failureCount >= 3: # We've failed three times resolving via proc. Warn, and fall back # to ps resolutions. log.info("Failed three attempts to get process resource usage from proc, falling back to ps (%s)" % exc) - + self._useProc = False self._failureCount = 1 # prevents lastQueryFailed() from thinking that we succeeded else: @@ -224,7 +224,7 @@ class ResourceTracker(threading.Thread): self._cond.acquire() if not self._halt: self._cond.wait(sleepTime) self._cond.release() - + # sets the new values if newValues: # If this is the first run then the cpuSampling stat is meaningless @@ -232,7 +232,7 @@ class ResourceTracker(threading.Thread): # point). Setting it to the average, which is a fairer estimate. if self.lastLookup == -1: newValues["cpuSampling"] = newValues["cpuAvg"] - + self._valLock.acquire() self.cpuSampling = newValues["cpuSampling"] self.cpuAvg = newValues["cpuAvg"] @@ -243,12 +243,12 @@ class ResourceTracker(threading.Thread): self._runCount += 1 self._failureCount = 0 self._valLock.release() - + def stop(self): """ Halts further resolutions and terminates the thread. """ - + self._cond.acquire() self._halt = True self._cond.notifyAll() diff --git a/arm/util/textInput.py b/arm/util/textInput.py index c0f01ce..34a66a0 100644 --- a/arm/util/textInput.py +++ b/arm/util/textInput.py @@ -14,40 +14,40 @@ class TextInputValidator: Basic interface for validators. Implementations should override the handleKey method. """ - + def __init__(self, nextValidator = None): self.nextValidator = nextValidator - + def validate(self, key, textbox): """ Processes the given key input for the textbox. This may modify the textbox's content, cursor position, etc depending on the functionality of the validator. This returns the key that the textbox should interpret, PASS if this validator doesn't want to take any action. - + Arguments: key - key code input from the user textbox - curses Textbox instance the input came from """ - + result = self.handleKey(key, textbox) - + if result != PASS: return result elif self.nextValidator: return self.nextValidator.validate(key, textbox) else: return key - + def handleKey(self, key, textbox): """ Process the given keycode with this validator, returning the keycode for the textbox to process, and PASS if this doesn't want to modify it. - + Arguments: key - key code input from the user textbox - curses Textbox instance the input came from """ - + return PASS
class BasicValidator(TextInputValidator): @@ -58,10 +58,10 @@ class BasicValidator(TextInputValidator): arrow - home and end keys move to the start/end of the line """ - + def handleKey(self, key, textbox): y, x = textbox.win.getyx() - + if curses.ascii.isprint(key) and x < textbox.maxx: # Shifts the existing text forward so input is an insert method rather # than replacement. The curses.textpad accepts an insert mode flag but @@ -72,7 +72,7 @@ class BasicValidator(TextInputValidator): # - The textpad doesn't shift text that has text attributes. This is # because keycodes read by textbox.win.inch() includes formatting, # causing the curses.ascii.isprint() check it does to fail. - + currentInput = textbox.gather() textbox.win.addstr(y, x + 1, currentInput[x:textbox.maxx - 1]) textbox.win.move(y, x) # reverts cursor movement during gather call @@ -85,7 +85,7 @@ class BasicValidator(TextInputValidator): elif key in (curses.KEY_END, curses.KEY_RIGHT): msgLen = len(textbox.gather()) textbox.win.move(y, x) # reverts cursor movement during gather call - + if key == curses.KEY_END and msgLen > 0 and x < msgLen - 1: # if we're in the content then move to the end textbox.win.move(y, msgLen - 1) @@ -97,7 +97,7 @@ class BasicValidator(TextInputValidator): # if we're resizing the display during text entry then cancel it # (otherwise the input field is filled with nonprintable characters) return curses.ascii.BEL - + return PASS
class HistoryValidator(TextInputValidator): @@ -105,48 +105,48 @@ class HistoryValidator(TextInputValidator): This intercepts the up and down arrow keys to scroll through a backlog of previous commands. """ - + def __init__(self, commandBacklog = [], nextValidator = None): TextInputValidator.__init__(self, nextValidator) - + # contents that can be scrolled back through, newest to oldest self.commandBacklog = commandBacklog - + # selected item from the backlog, -1 if we're not on a backlog item self.selectionIndex = -1 - + # the fields input prior to selecting a backlog item self.customInput = "" - + def handleKey(self, key, textbox): if key in (curses.KEY_UP, curses.KEY_DOWN): offset = 1 if key == curses.KEY_UP else -1 newSelection = self.selectionIndex + offset - + # constrains the new selection to valid bounds newSelection = max(-1, newSelection) newSelection = min(len(self.commandBacklog) - 1, newSelection) - + # skips if this is a no-op if self.selectionIndex == newSelection: return None - + # saves the previous input if we weren't on the backlog if self.selectionIndex == -1: self.customInput = textbox.gather().strip() - + if newSelection == -1: newInput = self.customInput else: newInput = self.commandBacklog[newSelection] - + y, _ = textbox.win.getyx() _, maxX = textbox.win.getmaxyx() textbox.win.clear() textbox.win.addstr(y, 0, newInput[:maxX - 1]) textbox.win.move(y, min(len(newInput), maxX - 1)) - + self.selectionIndex = newSelection return None - + return PASS
class TabCompleter(TextInputValidator): @@ -155,13 +155,13 @@ class TabCompleter(TextInputValidator): a single match. This expects a functor that accepts the current input and provides matches. """ - + def __init__(self, completer, nextValidator = None): TextInputValidator.__init__(self, nextValidator) - + # functor that accepts a string and gives a list of matches self.completer = completer - + def handleKey(self, key, textbox): # Matches against the tab key. The ord('\t') is nine, though strangely none # of the curses.KEY_*TAB constants match this... @@ -169,27 +169,27 @@ class TabCompleter(TextInputValidator): currentContents = textbox.gather().strip() matches = self.completer(currentContents) newInput = None - + if len(matches) == 1: # only a single match, fill it in newInput = matches[0] elif len(matches) > 1: # looks for a common prefix we can complete commonPrefix = os.path.commonprefix(matches) # weird that this comes from path... - + if commonPrefix != currentContents: newInput = commonPrefix - + # TODO: somehow display matches... this is not gonna be fun - + if newInput: y, _ = textbox.win.getyx() _, maxX = textbox.win.getmaxyx() textbox.win.clear() textbox.win.addstr(y, 0, newInput[:maxX - 1]) textbox.win.move(y, min(len(newInput), maxX - 1)) - + return None - + return PASS
diff --git a/arm/util/torConfig.py b/arm/util/torConfig.py index 978f44c..f6e693c 100644 --- a/arm/util/torConfig.py +++ b/arm/util/torConfig.py @@ -52,7 +52,7 @@ CONFIG = conf.config_dict("arm", {
def general_conf_handler(config, key): value = config.get(key) - + if key.startswith("config.summary."): # we'll look for summary keys with a lowercase config name CONFIG[key.lower()] = value @@ -94,7 +94,7 @@ class ManPageEntry: """ Information provided about a tor configuration option in its man page entry. """ - + def __init__(self, option, index, category, argUsage, description): self.option = option self.index = index @@ -107,7 +107,7 @@ 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 @@ -118,21 +118,21 @@ def loadOptionDescriptions(loadPath = None, checkVersion = True): page. This can be a somewhat lengthy call, and raises an IOError if issues occure. When successful loading from a file this returns the version for the contents loaded. - + If available, this can load the configuration descriptions from a file where they were previously persisted to cut down on the load time (latency for this is around 200ms). - + Arguments: loadPath - if set, this attempts to fetch the configuration descriptions from the given path instead of the man page checkVersion - discards the results if true and tor's version doens't match the cached descriptors, otherwise accepts anyway """ - + CONFIG_DESCRIPTIONS_LOCK.acquire() CONFIG_DESCRIPTIONS.clear() - + raisedExc = None loadedVersion = "" try: @@ -145,58 +145,58 @@ def loadOptionDescriptions(loadPath = None, checkVersion = True): inputFile = open(loadPath, "r") inputFileContents = inputFile.readlines() inputFile.close() - + try: versionLine = inputFileContents.pop(0).rstrip() - + if versionLine.startswith("Tor Version "): fileVersion = versionLine[12:] loadedVersion = fileVersion torVersion = torTools.getConn().getInfo("version", "") - + if checkVersion and fileVersion != torVersion: msg = "wrong version, tor is %s but the file's from %s" % (torVersion, fileVersion) raise IOError(msg) else: raise IOError("unable to parse version") - + while inputFileContents: # gets category enum, failing if it doesn't exist category = inputFileContents.pop(0).rstrip() if not category in Category: baseMsg = "invalid category in input file: '%s'" raise IOError(baseMsg % category) - + # gets the position in the man page indexArg, indexStr = -1, inputFileContents.pop(0).rstrip() - + if indexStr.startswith("index: "): indexStr = indexStr[7:] - + if indexStr.isdigit(): indexArg = int(indexStr) else: raise IOError("non-numeric index value: %s" % indexStr) else: raise IOError("malformed index argument: %s"% indexStr) - + option = inputFileContents.pop(0).rstrip() argument = inputFileContents.pop(0).rstrip() - + description, loadedLine = "", inputFileContents.pop(0) while loadedLine != PERSIST_ENTRY_DIVIDER: description += loadedLine - + if inputFileContents: loadedLine = inputFileContents.pop(0) else: break - + CONFIG_DESCRIPTIONS[option.lower()] = ManPageEntry(option, indexArg, category, argument, description.rstrip()) except IndexError: CONFIG_DESCRIPTIONS.clear() raise IOError("input file format is invalid") else: manCallResults = system.call("man tor", None) - + if not manCallResults: raise IOError("man page not found") - + # Fetches all options available with this tor instance. This isn't # vital, and the validOptions are left empty if the call fails. conn, validOptions = torTools.getConn(), [] @@ -204,21 +204,21 @@ def loadOptionDescriptions(loadPath = None, checkVersion = True): if configOptionQuery: for line in configOptionQuery.strip().split("\n"): validOptions.append(line[:line.find(" ")].lower()) - + optionCount, lastOption, lastArg = 0, None, None lastCategory, lastDescription = Category.GENERAL, "" for line in manCallResults: line = uiTools.getPrintable(line) strippedLine = line.strip() - + # we have content, but an indent less than an option (ignore line) #if strippedLine and not line.startswith(" " * MAN_OPT_INDENT): continue - + # line starts with an indent equivilant to a new config option isOptIndent = line.startswith(" " * MAN_OPT_INDENT) and line[MAN_OPT_INDENT] != " " - + isCategoryLine = not line.startswith(" ") and "OPTIONS" in line - + # if this is a category header or a new option, add an entry using the # buffered results if isOptIndent or isCategoryLine: @@ -231,13 +231,13 @@ def loadOptionDescriptions(loadPath = None, checkVersion = True): CONFIG_DESCRIPTIONS[lastOption.lower()] = ManPageEntry(lastOption, optionCount, lastCategory, lastArg, strippedDescription) optionCount += 1 lastDescription = "" - + # parses the option and argument line = line.strip() divIndex = line.find(" ") if divIndex != -1: lastOption, lastArg = line[:divIndex], line[divIndex + 1:] - + # if this is a category header then switch it if isCategoryLine: if line.startswith("OPTIONS"): lastCategory = Category.GENERAL @@ -255,7 +255,7 @@ def loadOptionDescriptions(loadPath = None, checkVersion = True): # instance the ExitPolicy and TestingTorNetwork entries. if lastDescription and lastDescription[-1] != "\n": lastDescription += " " - + if not strippedLine: lastDescription += "\n\n" elif line.startswith(" " * MAN_EX_INDENT): @@ -263,7 +263,7 @@ def loadOptionDescriptions(loadPath = None, checkVersion = True): else: lastDescription += strippedLine except IOError, exc: raisedExc = exc - + CONFIG_DESCRIPTIONS_LOCK.release() if raisedExc: raise raisedExc else: return loadedVersion @@ -272,27 +272,27 @@ def saveOptionDescriptions(path): """ Preserves the current configuration descriptors to the given path. This raises an IOError or OSError if unable to do so. - + Arguments: path - location to persist configuration descriptors """ - + # make dir if the path doesn't already exist baseDir = os.path.dirname(path) if not os.path.exists(baseDir): os.makedirs(baseDir) outputFile = open(path, "w") - + CONFIG_DESCRIPTIONS_LOCK.acquire() sortedOptions = CONFIG_DESCRIPTIONS.keys() sortedOptions.sort() - + torVersion = torTools.getConn().getInfo("version", "") outputFile.write("Tor Version %s\n" % torVersion) for i in range(len(sortedOptions)): manEntry = getConfigDescription(sortedOptions[i]) outputFile.write("%s\nindex: %i\n%s\n%s\n%s\n" % (manEntry.category, manEntry.index, manEntry.option, manEntry.argUsage, manEntry.description)) if i != len(sortedOptions) - 1: outputFile.write(PERSIST_ENTRY_DIVIDER) - + outputFile.close() CONFIG_DESCRIPTIONS_LOCK.release()
@@ -300,22 +300,22 @@ def getConfigSummary(option): """ Provides a short summary description of the configuration option. If none is known then this proivdes None. - + Arguments: option - tor config option """ - + return CONFIG.get("config.summary.%s" % option.lower())
def isImportant(option): """ Provides True if the option has the 'important' flag in the configuration, False otherwise. - + Arguments: option - tor config option """ - + return option.lower() in CONFIG["config.important"]
def getConfigDescription(option): @@ -324,17 +324,17 @@ def getConfigDescription(option): tor man page. This provides None if no such option has been loaded. If the man page is in the process of being loaded then this call blocks until it finishes. - + Arguments: option - tor config option """ - + CONFIG_DESCRIPTIONS_LOCK.acquire() - + if option.lower() in CONFIG_DESCRIPTIONS: returnVal = CONFIG_DESCRIPTIONS[option.lower()] else: returnVal = None - + CONFIG_DESCRIPTIONS_LOCK.release() return returnVal
@@ -343,11 +343,11 @@ def getConfigOptions(): Provides the configuration options from the loaded man page. This is an empty list if no man page has been loaded. """ - + CONFIG_DESCRIPTIONS_LOCK.acquire() - + returnVal = [CONFIG_DESCRIPTIONS[opt].option for opt in CONFIG_DESCRIPTIONS] - + CONFIG_DESCRIPTIONS_LOCK.release() return returnVal
@@ -356,12 +356,12 @@ def getConfigLocation(): Provides the location of the torrc, raising an IOError with the reason if the path can't be determined. """ - + conn = torTools.getConn() configLocation = conn.getInfo("config-file", None) torPid, torPrefix = conn.controller.get_pid(None), torTools.get_chroot() if not configLocation: raise IOError("unable to query the torrc location") - + try: torCwd = system.get_cwd(torPid) return torPrefix + system.expand_path(configLocation, torCwd) @@ -373,13 +373,13 @@ def getMultilineParameters(): Provides parameters that can be defined multiple times in the torrc without overwriting the value. """ - + # fetches config options with the LINELIST (aka 'LineList'), LINELIST_S (aka # 'Dependent'), and LINELIST_V (aka 'Virtual') types global MULTILINE_PARAM if MULTILINE_PARAM == None: conn, multilineEntries = torTools.getConn(), [] - + configOptionQuery = conn.getInfo("config/names", None) if configOptionQuery: for line in configOptionQuery.strip().split("\n"): @@ -389,26 +389,26 @@ def getMultilineParameters(): else: # unable to query tor connection, so not caching results return () - + MULTILINE_PARAM = multilineEntries - + return tuple(MULTILINE_PARAM)
def getCustomOptions(includeValue = False): """ Provides the torrc parameters that differ from their defaults. - + Arguments: includeValue - provides the current value with results if true, otherwise this just contains the options """ - + configText = torTools.getConn().getInfo("config-text", "").strip() configLines = configText.split("\n") - + # removes any duplicates configLines = list(set(configLines)) - + # The "GETINFO config-text" query only provides options that differ # from Tor's defaults with the exception of its Log and Nickname entries # which, even if undefined, returns "Log notice stdout" as per: @@ -417,16 +417,16 @@ def getCustomOptions(includeValue = False): # If this is from the deb then it will be "Log notice file /var/log/tor/log" # due to special patching applied to it, as per: # https://trac.torproject.org/projects/tor/ticket/4602 - + try: configLines.remove("Log notice stdout") except ValueError: pass - + try: configLines.remove("Log notice file /var/log/tor/log") except ValueError: pass - + try: configLines.remove("Nickname %s" % socket.gethostname()) except ValueError: pass - + if includeValue: return configLines else: return [line[:line.find(" ")] for line in configLines]
@@ -436,77 +436,77 @@ def saveConf(destination = None, contents = None): issuing a SAVECONF (the contents and destination match what tor's using) then that's done. Otherwise, this writes the contents directly. This raises an IOError if unsuccessful. - + Arguments: destination - path to be saved to, the current config location if None contents - configuration to be saved, the current config if None """ - + if destination: destination = os.path.abspath(destination) - + # fills default config values, and sets isSaveconf to false if they differ # from the arguments isSaveconf, startTime = True, time.time() - + currentConfig = getCustomOptions(True) if not contents: contents = currentConfig else: isSaveconf &= contents == currentConfig - + # The "GETINFO config-text" option was introduced in Tor version 0.2.2.7. If # we're writing custom contents then this is fine, but if we're trying to # save the current configuration then we need to fail if it's unavailable. # Otherwise we'd write a blank torrc as per... # https://trac.torproject.org/projects/tor/ticket/3614 - + if contents == ['']: # double check that "GETINFO config-text" is unavailable rather than just # giving an empty result - + if torTools.getConn().getInfo("config-text", None) == None: raise IOError("determining the torrc requires Tor version 0.2.2.7") - + currentLocation = None try: currentLocation = getConfigLocation() if not destination: destination = currentLocation else: isSaveconf &= destination == currentLocation except IOError: pass - + if not destination: raise IOError("unable to determine the torrc's path") logMsg = "Saved config by %%s to %s (runtime: %%0.4f)" % destination - + # attempts SAVECONF if we're updating our torrc with the current state if isSaveconf: try: torTools.getConn().saveConf() - + try: getTorrc().load() except IOError: pass - + log.debug(logMsg % ("SAVECONF", time.time() - startTime)) return # if successful then we're done except: pass - + # if the SAVECONF fails or this is a custom save then write contents directly try: # make dir if the path doesn't already exist baseDir = os.path.dirname(destination) if not os.path.exists(baseDir): os.makedirs(baseDir) - + # saves the configuration to the file configFile = open(destination, "w") configFile.write("\n".join(contents)) configFile.close() except (IOError, OSError), exc: raise IOError(exc) - + # reloads the cached torrc if overwriting it if destination == currentLocation: try: getTorrc().load() except IOError: pass - + log.debug(logMsg % ("directly writing", time.time() - startTime))
def validate(contents = None): @@ -514,15 +514,15 @@ def validate(contents = None): Performs validation on the given torrc contents, providing back a listing of (line number, issue, msg) tuples for issues found. If the issue occures on a multiline torrc entry then the line number is for the last line of the entry. - + Arguments: contents - torrc contents """ - + conn = torTools.getConn() customOptions = getCustomOptions() issuesFound, seenOptions = [], [] - + # Strips comments and collapses multiline multi-line entries, for more # information see: # https://trac.torproject.org/projects/tor/ticket/1929 @@ -532,21 +532,21 @@ def validate(contents = None): else: line = multilineBuffer + line multilineBuffer = "" - + if line.endswith("\"): multilineBuffer = line[:-1] strippedContents.append("") else: strippedContents.append(line.strip()) - + for lineNumber in range(len(strippedContents) - 1, -1, -1): lineText = strippedContents[lineNumber] if not lineText: continue - + lineComp = lineText.split(None, 1) if len(lineComp) == 2: option, value = lineComp else: option, value = lineText, "" - + # Tor is case insensetive when parsing its torrc. This poses a bit of an # issue for us because we want all of our checks to be case insensetive # too but also want messages to match the normal camel-case conventions. @@ -556,56 +556,56 @@ def validate(contents = None): # value check will fail. Hence using that hash to correct the case. # # TODO: when refactoring for stem make this less confusing... - + for customOpt in customOptions: if customOpt.lower() == option.lower(): option = customOpt break - + # if an aliased option then use its real name if option in CONFIG["torrc.alias"]: option = CONFIG["torrc.alias"][option] - + # most parameters are overwritten if defined multiple times if option in seenOptions and not option in getMultilineParameters(): issuesFound.append((lineNumber, ValidationError.DUPLICATE, option)) continue else: seenOptions.append(option) - + # checks if the value isn't necessary due to matching the defaults if not option in customOptions: issuesFound.append((lineNumber, ValidationError.IS_DEFAULT, option)) - + # replace aliases with their recognized representation if option in CONFIG["torrc.alias"]: option = CONFIG["torrc.alias"][option] - + # tor appears to replace tabs with a space, for instance: # "accept\t*:563" is read back as "accept *:563" value = value.replace("\t", " ") - + # parse value if it's a size or time, expanding the units value, valueType = _parseConfValue(value) - + # issues GETCONF to get the values tor's currently configured to use torValues = conn.getOption(option, [], True) - + # multiline entries can be comma separated values (for both tor and conf) valueList = [value] if option in getMultilineParameters(): valueList = [val.strip() for val in value.split(",")] - + fetchedValues, torValues = torValues, [] for fetchedValue in fetchedValues: for fetchedEntry in fetchedValue.split(","): fetchedEntry = fetchedEntry.strip() if not fetchedEntry in torValues: torValues.append(fetchedEntry) - + for val in valueList: # checks if both the argument and tor's value are empty isBlankMatch = not val and not torValues - + if not isBlankMatch and not val in torValues: # converts corrections to reader friedly size values displayValues = torValues @@ -613,9 +613,9 @@ def validate(contents = None): displayValues = [str_tools.get_size_label(int(val)) for val in torValues] elif valueType == ValueType.TIME: displayValues = [str_tools.get_time_label(int(val)) for val in torValues] - + issuesFound.append((lineNumber, ValidationError.MISMATCH, ", ".join(displayValues))) - + # checks if any custom options are missing from the torrc for option in customOptions: # In new versions the 'DirReqStatistics' option is true by default and @@ -623,12 +623,12 @@ def validate(contents = None): # missing then that's most likely the reason. # # https://trac.torproject.org/projects/tor/ticket/4237 - + if option == "DirReqStatistics": continue - + if not option in seenOptions: issuesFound.append((None, ValidationError.MISSING, option)) - + return issuesFound
def _parseConfValue(confArg): @@ -636,48 +636,48 @@ def _parseConfValue(confArg): Converts size or time values to their lowest units (bytes or seconds) which is what GETCONF calls provide. The returned is a tuple of the value and unit type. - + Arguments: confArg - torrc argument """ - + if confArg.count(" ") == 1: val, unit = confArg.lower().split(" ", 1) if not val.isdigit(): return confArg, ValueType.UNRECOGNIZED mult, multType = _getUnitType(unit) - + if mult != None: return str(int(val) * mult), multType - + return confArg, ValueType.UNRECOGNIZED
def _getUnitType(unit): """ Provides the type and multiplier for an argument's unit. The multiplier is None if the unit isn't recognized. - + Arguments: unit - string representation of a unit """ - + for label in SIZE_MULT: if unit in CONFIG["torrc.label.size." + label]: return SIZE_MULT[label], ValueType.SIZE - + for label in TIME_MULT: if unit in CONFIG["torrc.label.time." + label]: return TIME_MULT[label], ValueType.TIME - + return None, ValueType.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("#")] @@ -688,38 +688,38 @@ 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 - + # flag to indicate if we've given a load failure warning before self.isLoadFailWarned = False - + def load(self, logFailure = False): """ Loads or reloads the torrc contents, raising an IOError if there's a problem. - + Arguments: logFailure - if the torrc fails to load and we've never provided a warning for this before then logs a warning """ - + 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") @@ -729,37 +729,37 @@ class Torrc(): if logFailure and not self.isLoadFailWarned: log.warn("Unable to load torrc (%s)" % exc.strerror) self.isLoadFailWarned = True - + 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.release() return returnVal - + def getDisplayContents(self, strip = False): """ Provides the contents of the configuration file, formatted in a rendering @@ -768,80 +768,80 @@ class Torrc(): 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 = uiTools.getPrintable(lineText) 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 else: torVersion = torTools.getConn().getVersion() skipValidation = not CONFIG["features.torrc.validate"] skipValidation |= (torVersion is None or not torVersion >= stem.version.Requirement.GETINFO_CONFIG_TEXT) - + if skipValidation: log.info("Skipping torrc validation (requires tor 0.2.2.7-alpha)") returnVal = {} else: if self.corrections == None: self.corrections = validate(self.contents) - + returnVal = list(self.corrections) - + self.valsLock.release() return returnVal - + def getLock(self): """ Provides the lock governing concurrent access to the contents. """ - + return self.valsLock - + def logValidationIssues(self): """ Performs validation on the loaded contents, and logs warnings for issues that are found. """ - + corrections = self.getCorrections() - + if corrections: duplicateOptions, defaultOptions, mismatchLines, missingOptions = [], [], [], [] - + for lineNum, issue, msg in corrections: if issue == ValidationError.DUPLICATE: duplicateOptions.append("%s (line %i)" % (msg, lineNum + 1)) @@ -849,51 +849,51 @@ class Torrc(): defaultOptions.append("%s (line %i)" % (msg, lineNum + 1)) elif issue == ValidationError.MISMATCH: mismatchLines.append(lineNum + 1) elif issue == ValidationError.MISSING: missingOptions.append(msg) - + if duplicateOptions or defaultOptions: msg = "Unneeded torrc entries found. They've been highlighted in blue on the torrc page." - + if duplicateOptions: if len(duplicateOptions) > 1: msg += "\n- entries ignored due to having duplicates: " else: msg += "\n- entry ignored due to having a duplicate: " - + duplicateOptions.sort() msg += ", ".join(duplicateOptions) - + if defaultOptions: if len(defaultOptions) > 1: msg += "\n- entries match their default values: " else: msg += "\n- entry matches its default value: " - + defaultOptions.sort() msg += ", ".join(defaultOptions) - + log.notice(msg) - + if mismatchLines or missingOptions: msg = "The torrc differs from what tor's using. You can issue a sighup to reload the torrc values by pressing x." - + if mismatchLines: if len(mismatchLines) > 1: msg += "\n- torrc values differ on lines: " else: msg += "\n- torrc value differs on line: " - + mismatchLines.sort() msg += ", ".join([str(val + 1) for val in mismatchLines]) - + if missingOptions: if len(missingOptions) > 1: msg += "\n- configuration values are missing from the torrc: " else: msg += "\n- configuration value is missing from the torrc: " - + missingOptions.sort() msg += ", ".join(missingOptions) - + log.warn(msg)
def _testConfigDescriptions(): @@ -901,17 +901,17 @@ def _testConfigDescriptions(): Tester for the loadOptionDescriptions function, fetching the man page contents and dumping its parsed results. """ - + loadOptionDescriptions() sortedOptions = CONFIG_DESCRIPTIONS.keys() sortedOptions.sort() - + for i in range(len(sortedOptions)): option = sortedOptions[i] argument, description = getConfigDescription(option) optLabel = "OPTION: "%s"" % option argLabel = "ARGUMENT: "%s"" % argument - + print " %-45s %s" % (optLabel, argLabel) print ""%s"" % description if i != len(sortedOptions) - 1: print "-" * 80 @@ -920,31 +920,31 @@ def isRootNeeded(torrcPath): """ Returns True if the given torrc needs root permissions to be ran, False otherwise. This raises an IOError if the torrc can't be read. - + Arguments: torrcPath - torrc to be checked """ - + try: torrcFile = open(torrcPath, "r") torrcLines = torrcFile.readlines() torrcFile.close() - + for line in torrcLines: line = line.strip() - + isPortOpt = False for opt in PORT_OPT: if line.startswith(opt): isPortOpt = True break - + if isPortOpt and " " in line: arg = line.split(" ")[1] - + if arg.isdigit() and int(arg) <= 1024 and int(arg) != 0: return True - + return False except Exception, exc: raise IOError(exc) @@ -959,26 +959,26 @@ def renderTorrc(template, options, commentIndent = 30): [IF <opt1> | <opt2>] # logical or of the options [ELSE] # if the prior conditional evaluated to false [END IF] # ends the control block - + [<option>] # inputs the option value, omitting the line if it maps # to a boolean or empty string [NEWLINE] # empty line, otherwise templating white space is ignored - + Arguments: template - torrc template lines used to generate the results options - mapping of keywords to their given values, with values being booleans or strings (possibly multi-line) commentIndent - minimum column that comments align on """ - + results = [] templateIter = iter(template) commentLineFormat = "%%-%is%%s" % commentIndent - + try: while True: line = templateIter.next().strip() - + if line.startswith("[IF ") and line.endswith("]"): # checks if any of the conditional options are true or a non-empty string evaluatesTrue = False @@ -987,30 +987,30 @@ def renderTorrc(template, options, commentIndent = 30): if cond.startswith("NOT "): isInverse = True cond = cond[4:] - + if isInverse != bool(options.get(cond.strip())): evaluatesTrue = True break - + if evaluatesTrue: continue else: # skips lines until we come to an else or the end of the block depth = 0 - + while depth != -1: line = templateIter.next().strip() - + if line.startswith("[IF ") and line.endswith("]"): depth += 1 elif line == "[END IF]": depth -= 1 elif depth == 0 and line == "[ELSE]": depth -= 1 elif line == "[ELSE]": # an else block we aren't using - skip to the end of it depth = 0 - + while depth != -1: line = templateIter.next().strip() - + if line.startswith("[IF "): depth += 1 elif line == "[END IF]": depth -= 1 elif line == "[NEWLINE]": @@ -1027,12 +1027,12 @@ def renderTorrc(template, options, commentIndent = 30): # torrc option line option, arg, comment = "", "", "" parsedLine = line - + if "#" in parsedLine: parsedLine, comment = parsedLine.split("#", 1) parsedLine = parsedLine.strip() comment = "# %s" % comment.strip() - + # parses the argument from the option if " " in parsedLine.strip(): option, arg = parsedLine.split(" ", 1) @@ -1040,19 +1040,19 @@ def renderTorrc(template, options, commentIndent = 30): else: log.info("torrc template option lacks an argument: '%s'" % line) continue - + # inputs dynamic arguments if arg.startswith("[") and arg.endswith("]"): arg = options.get(arg[1:-1]) - + # skips argument if it's false or an empty string if not arg: continue - + torrcEntry = "%s %s" % (option, arg) if comment: results.append(commentLineFormat % (torrcEntry + " ", comment)) else: results.append(torrcEntry) except StopIteration: pass - + return "\n".join(results)
def loadConfigurationDescriptions(pathPrefix): @@ -1060,46 +1060,46 @@ def loadConfigurationDescriptions(pathPrefix): Attempts to load descriptions for tor's configuration options, fetching them from the man page and persisting them to a file to speed future startups. """ - + # It is important that this is loaded before entering the curses context, # otherwise the man call pegs the cpu for around a minute (I'm not sure # why... curses must mess the terminal in a way that's important to man). - + if CONFIG["features.config.descriptions.enabled"]: isConfigDescriptionsLoaded = False - + # determines the path where cached descriptions should be persisted (left # undefined if caching is disabled) descriptorPath = None if CONFIG["features.config.descriptions.persist"]: dataDir = CONFIG["startup.dataDirectory"] if not dataDir.endswith("/"): dataDir += "/" - + descriptorPath = os.path.expanduser(dataDir + "cache/") + CONFIG_DESC_FILENAME - + # attempts to load configuration descriptions cached in the data directory if descriptorPath: try: loadStartTime = time.time() loadOptionDescriptions(descriptorPath) isConfigDescriptionsLoaded = True - + log.info(DESC_LOAD_SUCCESS_MSG % (descriptorPath, time.time() - loadStartTime)) except IOError, exc: log.info(DESC_LOAD_FAILED_MSG % exc.strerror) - + # fetches configuration options from the man page if not isConfigDescriptionsLoaded: try: loadStartTime = time.time() loadOptionDescriptions() isConfigDescriptionsLoaded = True - + log.info(DESC_READ_MAN_SUCCESS_MSG % (time.time() - loadStartTime)) except IOError, exc: log.notice(DESC_READ_MAN_FAILED_MSG % exc.strerror) - - # persists configuration descriptions + + # persists configuration descriptions if isConfigDescriptionsLoaded and descriptorPath: try: loadStartTime = time.time() @@ -1109,7 +1109,7 @@ def loadConfigurationDescriptions(pathPrefix): log.notice(DESC_SAVE_FAILED_MSG % exc.strerror) except OSError, exc: log.notice(DESC_SAVE_FAILED_MSG % exc) - + # finally fall back to the cached descriptors provided with arm (this is # often the case for tbb and manual builds) if not isConfigDescriptionsLoaded: diff --git a/arm/util/torTools.py b/arm/util/torTools.py index 63dc257..8c7631e 100644 --- a/arm/util/torTools.py +++ b/arm/util/torTools.py @@ -43,7 +43,7 @@ def getConn(): Singleton constructor for a Controller. Be aware that this starts as being uninitialized, needing a stem Controller before it's fully functional. """ - + global CONTROLLER if CONTROLLER == None: CONTROLLER = Controller() return CONTROLLER @@ -90,7 +90,7 @@ class Controller: TorCtl), listener functionality for tor's state, and the capability for controller connections to be restarted if closed. """ - + def __init__(self): self.controller = None self.connLock = threading.RLock() @@ -101,30 +101,30 @@ class Controller: self._consensusLookupCache = {} # lookup cache with network status entries self._descriptorLookupCache = {} # lookup cache with relay descriptors self._lastNewnym = 0 # time we last sent a NEWNYM signal - + def init(self, controller): """ Uses the given stem instance for future operations, notifying listeners about the change. - + Arguments: controller - stem based Controller instance """ - + # TODO: We should reuse our controller instance so event listeners will be # re-attached. This is a point of regression until we do... :( - + if controller.is_alive() and controller != self.controller: self.connLock.acquire() - + if self.controller: self.close() # shut down current connection self.controller = controller log.info("Stem connected to tor version %s" % self.controller.get_version()) - + self.controller.add_event_listener(self.ns_event, stem.control.EventType.NS) self.controller.add_event_listener(self.new_consensus_event, stem.control.EventType.NEWCONSENSUS) self.controller.add_event_listener(self.new_desc_event, stem.control.EventType.NEWDESC) - + # reset caches for ip -> fingerprint lookups self._fingerprintMappings = None self._fingerprintLookupCache = {} @@ -132,22 +132,22 @@ class Controller: self._addressLookupCache = {} self._consensusLookupCache = {} self._descriptorLookupCache = {} - + # time that we sent our last newnym signal self._lastNewnym = 0 - + self.connLock.release() - + def close(self): """ Closes the current stem instance and notifies listeners. """ - + self.connLock.acquire() if self.controller: self.controller.close() self.connLock.release() - + def getController(self): return self.controller
@@ -156,37 +156,37 @@ class Controller: Returns True if this has been initialized with a working stem instance, False otherwise. """ - + self.connLock.acquire() - + result = False if self.controller: if self.controller.is_alive(): result = True else: self.close() - + self.connLock.release() return result - + def getInfo(self, param, default = UNDEFINED): """ Queries the control port for the given GETINFO option, providing the default if the response is undefined or fails for any reason (error response, control port closed, initiated, etc). - + Arguments: param - GETINFO option to be queried default - result if the query fails """ - + self.connLock.acquire() - + try: if not self.isAlive(): if default != UNDEFINED: return default else: raise stem.SocketClosed() - + if default != UNDEFINED: return self.controller.get_info(param, default) else: @@ -196,30 +196,30 @@ class Controller: raise exc finally: self.connLock.release() - + def getOption(self, param, default = UNDEFINED, multiple = False): """ Queries the control port for the given configuration option, providing the default if the response is undefined or fails for any reason. If multiple values exist then this arbitrarily returns the first unless the multiple flag is set. - + Arguments: param - configuration option to be queried default - result if the query fails multiple - provides a list with all returned values if true, otherwise this just provides the first result """ - + self.connLock.acquire() - + try: if not self.isAlive(): if default != UNDEFINED: return default else: raise stem.SocketClosed() - + if default != UNDEFINED: return self.controller.get_conf(param, default, multiple) else: @@ -229,129 +229,129 @@ class Controller: raise exc finally: self.connLock.release() - + def setOption(self, param, value = None): """ Issues a SETCONF to set the given option/value pair. An exeptions raised if it fails to be set. If no value is provided then this sets the option to 0 or NULL. - + Arguments: param - configuration option to be set value - value to set the parameter to (this can be either a string or a list of strings) """ - + self.connLock.acquire() - + try: if not self.isAlive(): raise stem.SocketClosed() - + self.controller.set_conf(param, value) except stem.SocketClosed, exc: self.close() raise exc finally: self.connLock.release() - + def saveConf(self): """ Calls tor's SAVECONF method. """ - + self.connLock.acquire() - + if self.isAlive(): self.controller.save_conf() - + self.connLock.release() - + def sendNewnym(self): """ Sends a newnym request to Tor. These are rate limited so if it occures more than once within a ten second window then the second is delayed. """ - + self.connLock.acquire() - + if self.isAlive(): self._lastNewnym = time.time() self.controller.signal(stem.Signal.NEWNYM) - + self.connLock.release() - + def isNewnymAvailable(self): """ True if Tor will immediately respect a newnym request, false otherwise. """ - + if self.isAlive(): return self.getNewnymWait() == 0 else: return False - + def getNewnymWait(self): """ Provides the number of seconds until a newnym signal would be respected. """ - + # newnym signals can occure at the rate of one every ten seconds # TODO: this can't take other controllers into account :( return max(0, math.ceil(self._lastNewnym + 10 - time.time())) - + def getCircuits(self, default = []): """ This provides a list with tuples of the form: (circuitID, status, purpose, (fingerprint1, fingerprint2...)) - + Arguments: default - value provided back if unable to query the circuit-status """ - + # TODO: We're losing caching around this. We should check to see the call # volume of this and probably add it to stem. - + results = [] - + for entry in self.controller.get_circuits(): fingerprints = [] - + for fp, nickname in entry.path: if not fp: consensusEntry = self.controller.get_network_status(nickname, None) - + if consensusEntry: fp = consensusEntry.fingerprint - + # It shouldn't be possible for this lookup to fail, but we # need to fill something (callers won't expect our own client # paths to have unknown relays). If this turns out to be wrong # then log a warning. - + if not fp: log.warn("Unable to determine the fingerprint for a relay in our own circuit: %s" % nickname) fp = "0" * 40 - + fingerprints.append(fp) - + results.append((int(entry.id), entry.status, entry.purpose, fingerprints)) - + if results: return results else: return default - + def getHiddenServicePorts(self, default = []): """ Provides the target ports hidden services are configured to use. - + Arguments: default - value provided back if unable to query the hidden service ports """ - + result = [] hs_options = self.controller.get_conf_map("HiddenServiceOptions", {}) - + for entry in hs_options.get("HiddenServicePort", []): # HiddenServicePort entries are of the form... # @@ -359,12 +359,12 @@ class Controller: # # ... with the TARGET being an address, port, or address:port. If the # target port isn't defined then uses the VIRTPORT. - + hs_port = None - + if ' ' in entry: virtport, target = entry.split(' ', 1) - + if ':' in target: hs_port = target.split(':', 1)[1] # target is an address:port elif target.isdigit(): @@ -373,62 +373,62 @@ class Controller: hs_port = virtport # target is an address else: hs_port = entry # just has the virtual port - + if hs_port.isdigit(): result.append(hsPort) - + if result: return result else: return default - + def getMyBandwidthRate(self, default = None): """ Provides the effective relaying bandwidth rate of this relay. Currently this doesn't account for SETCONF events. - + Arguments: default - result if the query fails """ - + # effective relayed bandwidth is the minimum of BandwidthRate, # MaxAdvertisedBandwidth, and RelayBandwidthRate (if set) effectiveRate = int(self.getOption("BandwidthRate", None)) - + relayRate = self.getOption("RelayBandwidthRate", None) if relayRate and relayRate != "0": effectiveRate = min(effectiveRate, int(relayRate)) - + maxAdvertised = self.getOption("MaxAdvertisedBandwidth", None) if maxAdvertised: effectiveRate = min(effectiveRate, int(maxAdvertised)) - + if effectiveRate is not None: return effectiveRate else: return default - + def getMyBandwidthBurst(self, default = None): """ Provides the effective bandwidth burst rate of this relay. Currently this doesn't account for SETCONF events. - + Arguments: default - result if the query fails """ - + # effective burst (same for BandwidthBurst and RelayBandwidthBurst) effectiveBurst = int(self.getOption("BandwidthBurst", None)) - + relayBurst = self.getOption("RelayBandwidthBurst", None) - + if relayBurst and relayBurst != "0": effectiveBurst = min(effectiveBurst, int(relayBurst)) - + if effectiveBurst is not None: return effectiveBurst else: return default - + def getMyBandwidthObserved(self, default = None): """ Provides the relay's current observed bandwidth (the throughput determined @@ -436,21 +436,21 @@ class Controller: heuristic used for path selection if the measured bandwidth is undefined. This is fetched from the descriptors and hence will get stale if descriptors aren't periodically updated. - + Arguments: default - result if the query fails """ - + myFingerprint = self.getInfo("fingerprint", None) - + if myFingerprint: myDescriptor = self.controller.get_server_descriptor(myFingerprint) - + if myDescriptor: result = myDescriptor.observed_bandwidth - + return default - + def getMyBandwidthMeasured(self, default = None): """ Provides the relay's current measured bandwidth (the throughput as noted by @@ -459,51 +459,51 @@ class Controller: on the circumstances this can be from a variety of things (observed, measured, weighted measured, etc) as described by: https://trac.torproject.org/projects/tor/ticket/1566 - + Arguments: default - result if the query fails """ - + # TODO: Tor is documented as providing v2 router status entries but # actually looks to be v3. This needs to be sorted out between stem # and tor. - + myFingerprint = self.getInfo("fingerprint", None) - + if myFingerprint: myStatusEntry = self.controller.get_network_status(myFingerprint) - + if myStatusEntry and hasattr(myStatusEntry, 'bandwidth'): return myStatusEntry.bandwidth - + return default - + def getMyFlags(self, default = None): """ Provides the flags held by this relay. - + Arguments: default - result if the query fails or this relay isn't a part of the consensus yet """ - + myFingerprint = self.getInfo("fingerprint", None) - + if myFingerprint: myStatusEntry = self.controller.get_network_status(myFingerprint) - + if myStatusEntry: return myStatusEntry.flags
return default - + def getVersion(self): """ Provides the version of our tor instance, this is None if we don't have a connection. """ - + self.connLock.acquire() - + try: return self.controller.get_version() except stem.SocketClosed, exc: @@ -513,108 +513,108 @@ class Controller: return None finally: self.connLock.release() - + def isGeoipUnavailable(self): """ Provides true if we've concluded that our geoip database is unavailable, false otherwise. """ - + if self.isAlive(): return self.controller.is_geoip_unavailable() else: return False - + def getMyUser(self): """ Provides the user this process is running under. If unavailable this provides None. """ - + return self.controller.get_user(None) - + def getMyFileDescriptorUsage(self): """ Provides the number of file descriptors currently being used by this process. This returns None if this can't be determined. """ - + # The file descriptor usage is the size of the '/proc/<pid>/fd' contents # http://linuxshellaccount.blogspot.com/2008/06/finding-number-of-open-file-de... # I'm not sure about other platforms (like BSD) so erroring out there. - + self.connLock.acquire() - + result = None if self.isAlive() and proc.is_available(): myPid = self.controller.get_pid(None) - + if myPid: try: result = len(os.listdir("/proc/%s/fd" % myPid)) except: pass - + self.connLock.release() - + return result - + def getMyFileDescriptorLimit(self): """ Provides the maximum number of file descriptors this process can have. Only the Tor process itself reliably knows this value, and the option for getting this was added in Tor 0.2.3.x-final. If that's unavailable then we can only estimate the file descriptor limit based on other factors. - + The return result is a tuple of the form: (fileDescLimit, isEstimate) and if all methods fail then both values are None. """ - + # provides -1 if the query fails queriedLimit = self.getInfo("process/descriptor-limit", None) - + if queriedLimit != None and queriedLimit != "-1": return (int(queriedLimit), False) - + torUser = self.getMyUser() - + # This is guessing the open file limit. Unfortunately there's no way # (other than "/usr/proc/bin/pfiles pid | grep rlimit" under Solaris) # to get the file descriptor limit for an arbitrary process. - + if torUser == "debian-tor": # probably loaded via /etc/init.d/tor which changes descriptor limit return (8192, True) else: # uses ulimit to estimate (-H is for hard limit, which is what tor uses) ulimitResults = system.call("ulimit -Hn") - + if ulimitResults: ulimit = ulimitResults[0].strip() - + if ulimit.isdigit(): return (int(ulimit), True)
return (None, None) - + def getStartTime(self): """ Provides the unix time for when the tor process first started. If this can't be determined then this provides None. """ - + try: return system.get_start_time(self.controller.get_pid()) except: return None - + def isExitingAllowed(self, ipAddress, port): """ Checks if the given destination can be exited to by this relay, returning True if so and False otherwise. """ - + self.connLock.acquire() - + result = False if self.isAlive(): # If we allow any exiting then this could be relayed DNS queries, @@ -622,82 +622,82 @@ class Controller: # test when exiting isn't allowed, but nothing is relayed over them. # I'm registering these as non-exiting to avoid likely user confusion: # https://trac.torproject.org/projects/tor/ticket/965 - + our_policy = self.getExitPolicy() - + if our_policy and our_policy.is_exiting_allowed() and port == "53": result = True else: result = our_policy and our_policy.can_exit_to(ipAddress, port) - + self.connLock.release() - + return result - + def getExitPolicy(self): """ Provides an ExitPolicy instance for the head of this relay's exit policy chain. If there's no active connection then this provides None. """ - + self.connLock.acquire() - + result = None if self.isAlive(): try: result = self.controller.get_exit_policy(param) except: pass - + self.connLock.release() - + return result - + def getConsensusEntry(self, relayFingerprint): """ Provides the most recently available consensus information for the given relay. This is none if no such information exists. - + Arguments: relayFingerprint - fingerprint of the relay """ - + self.connLock.acquire() - + result = None if self.isAlive(): if not relayFingerprint in self._consensusLookupCache: nsEntry = self.getInfo("ns/id/%s" % relayFingerprint, None) self._consensusLookupCache[relayFingerprint] = nsEntry - + result = self._consensusLookupCache[relayFingerprint] - + self.connLock.release() - + return result - + def getDescriptorEntry(self, relayFingerprint): """ Provides the most recently available descriptor information for the given relay. Unless FetchUselessDescriptors is set this may frequently be unavailable. If no such descriptor is available then this returns None. - + Arguments: relayFingerprint - fingerprint of the relay """ - + self.connLock.acquire() - + result = None if self.isAlive(): if not relayFingerprint in self._descriptorLookupCache: descEntry = self.getInfo("desc/id/%s" % relayFingerprint, None) self._descriptorLookupCache[relayFingerprint] = descEntry - + result = self._descriptorLookupCache[relayFingerprint] - + self.connLock.release() - + return result - + def getRelayFingerprint(self, relayAddress, relayPort = None, getAllMatches = False): """ Provides the fingerprint associated with the given address. If there's @@ -705,7 +705,7 @@ class Controller: None. This disambiguates the fingerprint if there's multiple relays on the same ip address by several methods, one of them being to pick relays we have a connection with. - + Arguments: relayAddress - address of relay to be returned relayPort - orport of relay (to further narrow the results) @@ -713,16 +713,16 @@ class Controller: (port, fingerprint) tuples matching the given address """ - + self.connLock.acquire() - + result = None if self.isAlive(): if getAllMatches: # populates the ip -> fingerprint mappings if not yet available if self._fingerprintMappings == None: self._fingerprintMappings = self._getFingerprintMappings() - + if relayAddress in self._fingerprintMappings: result = self._fingerprintMappings[relayAddress] else: result = [] @@ -731,24 +731,24 @@ class Controller: if not (relayAddress, relayPort) in self._fingerprintLookupCache: relayFingerprint = self._getRelayFingerprint(relayAddress, relayPort) self._fingerprintLookupCache[(relayAddress, relayPort)] = relayFingerprint - + result = self._fingerprintLookupCache[(relayAddress, relayPort)] - + self.connLock.release() - + return result - + def getRelayNickname(self, relayFingerprint): """ Provides the nickname associated with the given relay. This provides None if no such relay exists, and "Unnamed" if the name hasn't been set. - + Arguments: relayFingerprint - fingerprint of the relay """ - + self.connLock.acquire() - + result = None if self.isAlive(): # query the nickname if it isn't yet cached @@ -759,16 +759,16 @@ class Controller: self._nicknameLookupCache[relayFingerprint] = myNickname else: nsEntry = self.controller.get_network_status(relayFingerprint, None) - + if nsEntry: self._nicknameLookupCache[relayFingerprint] = nsEntry.nickname - + result = self._nicknameLookupCache[relayFingerprint] - + self.connLock.release() - + return result - + def getRelayExitPolicy(self, relayFingerprint): """ Provides the ExitPolicy instance associated with the given relay. The tor @@ -776,36 +776,36 @@ class Controller: address-specific policies, so this is only used as a fallback if a recent descriptor is unavailable. This returns None if unable to determine the policy. - + Arguments: relayFingerprint - fingerprint of the relay """ - + self.connLock.acquire() - + result = None if self.isAlive(): # attempts to fetch the policy via the descriptor descriptor = self.controller.get_server_descriptor(relayFingerprint, None) - + if descriptor: result = descriptor.exit_policy - + self.connLock.release() - + return result - + def getRelayAddress(self, relayFingerprint, default = None): """ Provides the (IP Address, ORPort) tuple for a given relay. If the lookup fails then this returns the default. - + Arguments: relayFingerprint - fingerprint of the relay """ - + self.connLock.acquire() - + result = default if self.isAlive(): # query the address if it isn't yet cached @@ -814,65 +814,65 @@ class Controller: # this is us, simply check the config myAddress = self.getInfo("address", None) myOrPort = self.getOption("ORPort", None) - + if myAddress and myOrPort: self._addressLookupCache[relayFingerprint] = (myAddress, myOrPort) else: # check the consensus for the relay nsEntry = self.getConsensusEntry(relayFingerprint) - + if nsEntry: nsLineComp = nsEntry.split("\n")[0].split(" ") - + if len(nsLineComp) >= 8: self._addressLookupCache[relayFingerprint] = (nsLineComp[6], nsLineComp[7]) - + result = self._addressLookupCache.get(relayFingerprint, default) - + self.connLock.release() - + return result - + def addEventListener(self, listener, *eventTypes): """ Directs further tor controller events to callback functions of the listener. If a new control connection is initialized then this listener is reattached. """ - + self.connLock.acquire() if self.isAlive(): self.controller.add_event_listener(listener, *eventTypes) self.connLock.release() - + def removeEventListener(self, listener): """ Stops the given event listener from being notified of further events. """ - + self.connLock.acquire() if self.isAlive(): self.controller.remove_event_listener(listener) self.connLock.release() - + def addStatusListener(self, callback): """ Directs further events related to tor's controller status to the callback function. - + Arguments: callback - functor that'll accept the events, expected to be of the form: myFunction(controller, eventType) """ - + self.controller.add_status_listener(callback) - + def reload(self): """ This resets tor (sending a RELOAD signal to the control port) causing tor's internal state to be reset and the torrc reloaded. """ - + self.connLock.acquire() - + try: if self.isAlive(): try: @@ -882,72 +882,72 @@ class Controller: raise IOError(str(exc)) finally: self.connLock.release() - + def shutdown(self, force = False): """ Sends a shutdown signal to the attached tor instance. For relays the actual shutdown is delayed for thirty seconds unless the force flag is given. This raises an IOError if a signal is sent but fails. - + Arguments: force - triggers an immediate shutdown for relays if True """ - + self.connLock.acquire() - + raisedException = None if self.isAlive(): try: isRelay = self.getOption("ORPort", None) != None - + if force: self.controller.signal(stem.Signal.HALT) else: self.controller.signal(stem.Signal.SHUTDOWN) - + # shuts down control connection if we aren't making a delayed shutdown if force or not isRelay: self.close() except Exception, exc: raisedException = IOError(str(exc)) - + self.connLock.release() - + if raisedException: raise raisedException - + def ns_event(self, event): self._consensusLookupCache = {} - + def new_consensus_event(self, event): self.connLock.acquire() - + # reconstructs consensus based mappings self._fingerprintLookupCache = {} self._nicknameLookupCache = {} self._addressLookupCache = {} self._consensusLookupCache = {} - + if self._fingerprintMappings != None: self._fingerprintMappings = self._getFingerprintMappings(event.desc) - + self.connLock.release() - + def new_desc_event(self, event): self.connLock.acquire() - + myFingerprint = self.getInfo("fingerprint", None) desc_fingerprints = [fingerprint for (fingerprint, nickname) in event.relays] - + # If we're tracking ip address -> fingerprint mappings then update with # the new relays. self._fingerprintLookupCache = {} self._descriptorLookupCache = {} - + if self._fingerprintMappings != None: for fingerprint in desc_fingerprints: # gets consensus data for the new descriptor try: desc = self.controller.get_network_status(fingerprint) except stem.ControllerError: continue - + # updates fingerprintMappings with new data if desc.address in self._fingerprintMappings: # if entry already exists with the same orport, remove it @@ -956,68 +956,68 @@ class Controller: if entryPort == desc.or_port: orportMatch = (entryPort, entryFingerprint) break - + if orportMatch: self._fingerprintMappings[desc.address].remove(orportMatch) - + # add the new entry self._fingerprintMappings[desc.address].append((desc.or_port, desc.fingerprint)) else: self._fingerprintMappings[desc.address] = [(desc.or_port, desc.fingerprint)] - + self.connLock.release() - + def _getFingerprintMappings(self, descriptors = None): """ Provides IP address to (port, fingerprint) tuple mappings for all of the currently cached relays. - + Arguments: descriptors - router status entries (fetched if not provided) """ - + results = {} if self.isAlive(): # fetch the current network status if not provided if not descriptors: try: descriptors = self.controller.get_network_statuses() except stem.ControllerError: descriptors = [] - + # construct mappings of ips to relay data for desc in descriptors: results.setdefault(desc.address, []).append((desc.or_port, desc.fingerprint)) - + return results - + def _getRelayFingerprint(self, relayAddress, relayPort): """ Provides the fingerprint associated with the address/port combination. - + Arguments: relayAddress - address of relay to be returned relayPort - orport of relay (to further narrow the results) """ - + # If we were provided with a string port then convert to an int (so # lookups won't mismatch based on type). if isinstance(relayPort, str): relayPort = int(relayPort) - + # checks if this matches us if relayAddress == self.getInfo("address", None): if not relayPort or relayPort == self.getOption("ORPort", None): return self.getInfo("fingerprint", None) - + # if we haven't yet populated the ip -> fingerprint mappings then do so if self._fingerprintMappings == None: self._fingerprintMappings = self._getFingerprintMappings() - + potentialMatches = self._fingerprintMappings.get(relayAddress) if not potentialMatches: return None # no relay matches this ip address - + if len(potentialMatches) == 1: # There's only one relay belonging to this ip address. If the port # matches then we're done. match = potentialMatches[0] - + if relayPort and match[0] != relayPort: return None else: return match[1] elif relayPort: @@ -1025,6 +1025,6 @@ class Controller: for entryPort, entryFingerprint in potentialMatches: if entryPort == relayPort: return entryFingerprint - + return None
diff --git a/arm/util/uiTools.py b/arm/util/uiTools.py index 2aac55a..999186c 100644 --- a/arm/util/uiTools.py +++ b/arm/util/uiTools.py @@ -53,7 +53,7 @@ def demoGlyphs(): undocumented in the pydocs. For more information see the following man page: http://www.mkssoftware.com/docs/man5/terminfo.5.asp """ - + try: curses.wrapper(_showGlyphs) except KeyboardInterrupt: pass # quit
@@ -61,37 +61,37 @@ def _showGlyphs(stdscr): """ Renders a chart with the ACS glyphs. """ - + # allows things like semi-transparent backgrounds try: curses.use_default_colors() except curses.error: pass - + # attempts to make the cursor invisible try: curses.curs_set(0) except curses.error: pass - + acsOptions = [item for item in curses.__dict__.items() if item[0].startswith("ACS_")] acsOptions.sort(key=lambda i: (i[1])) # order by character codes - + # displays a chart with all the glyphs and their representations height, width = stdscr.getmaxyx() if width < 30: return # not enough room to show a column columns = width / 30 - + # display title stdscr.addstr(0, 0, "Curses Glyphs:", curses.A_STANDOUT) - + x, y = 0, 1 while acsOptions: name, keycode = acsOptions.pop(0) stdscr.addstr(y, x * 30, "%s (%i)" % (name, keycode)) stdscr.addch(y, (x * 30) + 25, keycode) - + x += 1 if x >= columns: x, y = 0, y + 1 if y >= height: break - + stdscr.getch() # quit on keyboard input
def isUnicodeAvailable(): @@ -99,31 +99,31 @@ def isUnicodeAvailable(): True if curses has wide character support, false otherwise or if it can't be determined. """ - + global IS_UNICODE_SUPPORTED if IS_UNICODE_SUPPORTED == None: if CONFIG["features.printUnicode"]: # Checks if our LANG variable is unicode. This is what will be respected # when printing multi-byte characters after calling... # locale.setlocale(locale.LC_ALL, '') - # + # # so if the LANG isn't unicode then setting this would be pointless. - + isLangUnicode = "utf-" in os.environ.get("LANG", "").lower() IS_UNICODE_SUPPORTED = isLangUnicode and _isWideCharactersAvailable() else: IS_UNICODE_SUPPORTED = False - + return IS_UNICODE_SUPPORTED
def getPrintable(line, keepNewlines = True): """ Provides the line back with non-printable characters stripped. - + Arguments: line - string to be processed stripNewlines - retains newlines if true, stripped otherwise """ - + line = line.replace('\xc2', "'") line = "".join([char for char in line if (isprint(char) or (keepNewlines and char == "\n"))]) return line @@ -132,7 +132,7 @@ def isColorSupported(): """ True if the display supports showing color, false otherwise. """ - + if COLOR_IS_SUPPORTED == None: _initColors() return COLOR_IS_SUPPORTED
@@ -142,14 +142,14 @@ def getColor(color): include: red green yellow blue cyan magenta black white - - If color support isn't available or colors can't be initialized then this uses the + + If color support isn't available or colors can't be initialized then this uses the terminal's default coloring scheme. - + Arguments: color - name of the foreground color to be returned """ - + colorOverride = getColorOverride() if colorOverride: color = colorOverride if not COLOR_ATTR_INITIALIZED: _initColors() @@ -159,12 +159,12 @@ def setColorOverride(color = None): """ Overwrites all requests for color with the given color instead. This raises a ValueError if the color is invalid. - + Arguments: color - name of the color to overwrite requests with, None to use normal coloring """ - + if color == None: CONFIG["features.colorOverride"] = "none" elif color in COLOR_LIST.keys(): @@ -175,7 +175,7 @@ def getColorOverride(): """ Provides the override color used by the interface, None if it isn't set. """ - + colorOverride = CONFIG.get("features.colorOverride", "none") if colorOverride == "none": return None else: return colorOverride @@ -188,16 +188,16 @@ def cropStr(msg, size, minWordLen = 4, minCrop = 0, endType = Ending.ELLIPSE, ge including those) then this provides an empty string. If a cropped string ends with a comma or period then it's stripped (unless we're providing the remainder back). Examples: - + cropStr("This is a looooong message", 17) "This is a looo..." - + cropStr("This is a looooong message", 12) "This is a..." - + cropStr("This is a looooong message", 3) "" - + Arguments: msg - source text size - room available for text @@ -211,81 +211,81 @@ def cropStr(msg, size, minWordLen = 4, minCrop = 0, endType = Ending.ELLIPSE, ge getRemainder - returns a tuple instead, with the second part being the cropped portion of the message """ - + # checks if there's room for the whole message if len(msg) <= size: if getRemainder: return (msg, "") else: return msg - + # avoids negative input size = max(0, size) if minWordLen != None: minWordLen = max(0, minWordLen) minCrop = max(0, minCrop) - + # since we're cropping, the effective space available is less with an # ellipse, and cropping words requires an extra space for hyphens if endType == Ending.ELLIPSE: size -= 3 elif endType == Ending.HYPHEN and minWordLen != None: minWordLen += 1 - + # checks if there isn't the minimum space needed to include anything lastWordbreak = msg.rfind(" ", 0, size + 1) - + if lastWordbreak == -1: # we're splitting the first word if minWordLen == None or size < minWordLen: if getRemainder: return ("", msg) else: return "" - + includeCrop = True else: lastWordbreak = len(msg[:lastWordbreak].rstrip()) # drops extra ending whitespaces if (minWordLen != None and size < minWordLen) or (minWordLen == None and lastWordbreak < 1): if getRemainder: return ("", msg) else: return "" - + if minWordLen == None: minWordLen = sys.maxint includeCrop = size - lastWordbreak - 1 >= minWordLen - + # if there's a max crop size then make sure we're cropping at least that many characters if includeCrop and minCrop: nextWordbreak = msg.find(" ", size) if nextWordbreak == -1: nextWordbreak = len(msg) includeCrop = nextWordbreak - size + 1 >= minCrop - + if includeCrop: returnMsg, remainder = msg[:size], msg[size:] if endType == Ending.HYPHEN: remainder = returnMsg[-1] + remainder returnMsg = returnMsg[:-1].rstrip() + "-" else: returnMsg, remainder = msg[:lastWordbreak], msg[lastWordbreak:] - + # if this is ending with a comma or period then strip it off if not getRemainder and returnMsg and returnMsg[-1] in (",", "."): returnMsg = returnMsg[:-1] - + if endType == Ending.ELLIPSE: returnMsg = returnMsg.rstrip() + "..." - + if getRemainder: return (returnMsg, remainder) else: return returnMsg
def padStr(msg, size, cropExtra = False): """ Provides the string padded with whitespace to the given length. - + Arguments: msg - string to be padded size - length to be padded to cropExtra - crops string if it's longer than the size if true """ - + if cropExtra: msg = msg[:size] return ("%%-%is" % size) % msg
def drawBox(panel, top, left, width, height, attr=curses.A_NORMAL): """ Draws a box in the panel with the given bounds. - + Arguments: panel - panel in which to draw top - vertical position of the box's top @@ -294,15 +294,15 @@ def drawBox(panel, top, left, width, height, attr=curses.A_NORMAL): height - height of the drawn box attr - text attributes """ - + # draws the top and bottom panel.hline(top, left + 1, width - 2, attr) panel.hline(top + height - 1, left + 1, width - 2, attr) - + # draws the left and right sides panel.vline(top + 1, left, height - 2, attr) panel.vline(top + 1, left + width - 1, height - 2, attr) - + # draws the corners panel.addch(top, left, curses.ACS_ULCORNER, attr) panel.addch(top, left + width - 1, curses.ACS_URCORNER, attr) @@ -311,22 +311,22 @@ def drawBox(panel, top, left, width, height, attr=curses.A_NORMAL): def isSelectionKey(key): """ Returns true if the keycode matches the enter or space keys. - + Argument: key - keycode to be checked """ - + return key in (curses.KEY_ENTER, 10, ord(' '))
def isScrollKey(key): """ Returns true if the keycode is recognized by the getScrollPosition function for scrolling. - + Argument: key - keycode to be checked """ - + return key in SCROLL_KEYS
def getScrollPosition(key, position, pageHeight, contentHeight, isCursor = False): @@ -338,9 +338,9 @@ def getScrollPosition(key, position, pageHeight, contentHeight, isCursor = False Page Up / Page Down - scrolls by the pageHeight Home - top of the content End - bottom of the content - + This provides the input position if the key doesn't correspond to the above. - + Arguments: key - keycode for the user's input position - starting position @@ -348,7 +348,7 @@ def getScrollPosition(key, position, pageHeight, contentHeight, isCursor = False contentHeight - total lines of content that can be scrolled isCursor - tracks a cursor position rather than scroll if true """ - + if isScrollKey(key): shift = 0 if key == curses.KEY_UP: shift = -1 @@ -357,7 +357,7 @@ def getScrollPosition(key, position, pageHeight, contentHeight, isCursor = False elif key == curses.KEY_NPAGE: shift = pageHeight - 1 if isCursor else pageHeight elif key == curses.KEY_HOME: shift = -contentHeight elif key == curses.KEY_END: shift = contentHeight - + # returns the shift, restricted to valid bounds maxLoc = contentHeight - 1 if isCursor else contentHeight - pageHeight return max(0, min(position + shift, maxLoc)) @@ -368,59 +368,59 @@ class Scroller: Tracks the scrolling position when there might be a visible cursor. This expects that there is a single line displayed per an entry in the contents. """ - + def __init__(self, isCursorEnabled): self.scrollLoc, self.cursorLoc = 0, 0 self.cursorSelection = None self.isCursorEnabled = isCursorEnabled - + def getScrollLoc(self, content, pageHeight): """ Provides the scrolling location, taking into account its cursor's location content size, and page height. - + Arguments: content - displayed content pageHeight - height of the display area for the content """ - + if content and pageHeight: self.scrollLoc = max(0, min(self.scrollLoc, len(content) - pageHeight + 1)) - + if self.isCursorEnabled: self.getCursorSelection(content) # resets the cursor location - + # makes sure the cursor is visible if self.cursorLoc < self.scrollLoc: self.scrollLoc = self.cursorLoc elif self.cursorLoc > self.scrollLoc + pageHeight - 1: self.scrollLoc = self.cursorLoc - pageHeight + 1 - + # checks if the bottom would run off the content (this could be the # case when the content's size is dynamic and entries are removed) if len(content) > pageHeight: self.scrollLoc = min(self.scrollLoc, len(content) - pageHeight) - + return self.scrollLoc - + def getCursorSelection(self, content): """ Provides the selected item in the content. This is the same entry until the cursor moves or it's no longer available (in which case it moves on to the next entry). - + Arguments: content - displayed content """ - + # TODO: needs to handle duplicate entries when using this for the # connection panel - + if not self.isCursorEnabled: return None elif not content: self.cursorLoc, self.cursorSelection = 0, None return None - + self.cursorLoc = min(self.cursorLoc, len(content) - 1) if self.cursorSelection != None and self.cursorSelection in content: # moves cursor location to track the selection @@ -428,24 +428,24 @@ class Scroller: else: # select the next closest entry self.cursorSelection = content[self.cursorLoc] - + return self.cursorSelection - + def handleKey(self, key, content, pageHeight): """ Moves either the scroll or cursor according to the given input. - + Arguments: key - key code of user input content - displayed content pageHeight - height of the display area for the content """ - + if self.isCursorEnabled: self.getCursorSelection(content) # resets the cursor location startLoc = self.cursorLoc else: startLoc = self.scrollLoc - + newLoc = getScrollPosition(key, startLoc, pageHeight, len(content), self.isCursorEnabled) if startLoc != newLoc: if self.isCursorEnabled: self.cursorSelection = content[newLoc] @@ -458,16 +458,16 @@ def _isWideCharactersAvailable(): True if curses has wide character support (which is required to print unicode). False otherwise. """ - + try: # gets the dynamic library used by the interpretor for curses - + import _curses cursesLib = _curses.__file__ - + # Uses 'ldd' (Linux) or 'otool -L' (Mac) to determine the curses # library dependencies. - # + # # atagar@fenrir:~/Desktop$ ldd /usr/lib/python2.6/lib-dynload/_curses.so # linux-gate.so.1 => (0x00a51000) # libncursesw.so.5 => /lib/libncursesw.so.5 (0x00faa000) @@ -475,24 +475,24 @@ def _isWideCharactersAvailable(): # libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00158000) # libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0x00398000) # /lib/ld-linux.so.2 (0x00ca8000) - # + # # atagar$ otool -L /System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-dynload/_curses.so # /System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-dynload/_curses.so: # /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0) # /usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0) # /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 111.1.6) - + libDependencyLines = None if system.is_available("ldd"): libDependencyLines = system.call("ldd %s" % cursesLib) elif system.is_available("otool"): libDependencyLines = system.call("otool -L %s" % cursesLib) - + if libDependencyLines: for line in libDependencyLines: if "libncursesw" in line: return True except: pass - + return False
def _initColors(): @@ -500,7 +500,7 @@ def _initColors(): Initializes color mappings usable by curses. This can only be done after calling curses.initscr(). """ - + global COLOR_ATTR_INITIALIZED, COLOR_IS_SUPPORTED if not COLOR_ATTR_INITIALIZED: # hack to replace all ACS characters with '+' if ACS support has been @@ -509,27 +509,27 @@ def _initColors(): for item in curses.__dict__: if item.startswith("ACS_"): curses.__dict__[item] = ord('+') - + # replace a few common border pipes that are better rendered as '|' or # '-' instead - + curses.ACS_SBSB = ord('|') curses.ACS_VLINE = ord('|') curses.ACS_BSBS = ord('-') curses.ACS_HLINE = ord('-') - + COLOR_ATTR_INITIALIZED = True COLOR_IS_SUPPORTED = False if not CONFIG["features.colorInterface"]: return - + try: COLOR_IS_SUPPORTED = curses.has_colors() except curses.error: return # initscr hasn't been called yet - + # initializes color mappings if color support is available if COLOR_IS_SUPPORTED: colorpair = 0 log.info("Terminal color support detected and enabled") - + for colorName in COLOR_LIST: fgColor = COLOR_LIST[colorName] bgColor = -1 # allows for default (possibly transparent) background diff --git a/setup.py b/setup.py index 64d4986..616222c 100644 --- a/setup.py +++ b/setup.py @@ -10,18 +10,18 @@ def getResources(dst, sourceDir): """ Provides a list of tuples of the form... [(destination, (file1, file2...)), ...] - + for the given contents of the arm directory (that's right, distutils isn't smart enough to know how to copy directories). """ - + results = [] - + for root, _, files in os.walk(os.path.join("arm", sourceDir)): if files: fileListing = tuple([os.path.join(root, file) for file in files]) results.append((os.path.join(dst, root[4:]), fileListing)) - + return results
# Use 'tor-arm' instead of 'arm' in the path for the sample armrc if we're @@ -43,7 +43,7 @@ try: docPathFlagIndex = sys.argv.index("--docPath") if docPathFlagIndex < len(sys.argv) - 1: docPath = sys.argv[docPathFlagIndex + 1] - + # remove the custom --docPath argument (otherwise the setup call will # complain about them) del sys.argv[docPathFlagIndex:docPathFlagIndex + 3] @@ -62,28 +62,28 @@ except ValueError: pass # --docPath flag not found manFilename = "arm/resoureces/arm.1" if "install" in sys.argv: sys.argv += ["--install-purelib", "/usr/share"] - + # Compresses the man page. This is a temporary file that we'll install. If # something goes wrong then we'll print the issue and use the uncompressed man # page instead. - + try: manInputFile = open('arm/resources/arm.1', 'r') manContents = manInputFile.read() manInputFile.close() - + # temporary destination for the man page guarenteed to be unoccupied (to # avoid conflicting with files that are already there) tmpFilename = tempfile.mktemp("/arm.1.gz") - + # make dir if the path doesn't already exist baseDir = os.path.dirname(tmpFilename) if not os.path.exists(baseDir): os.makedirs(baseDir) - + manOutputFile = gzip.open(tmpFilename, 'wb') manOutputFile.write(manContents) manOutputFile.close() - + # places in tmp rather than a relative path to avoid having this copy appear # in the deb and rpm builds manFilename = tmpFilename @@ -105,7 +105,7 @@ setup(name='arm', ("/usr/share/man/man1", [manFilename]), (docPath, ["armrc.sample"]), ("/usr/share/arm/gui", ["arm/gui/arm.xml"]), - ("/usr/share/arm", ["arm/settings.cfg", "arm/uninstall"])] + + ("/usr/share/arm", ["arm/settings.cfg", "arm/uninstall"])] + getResources("/usr/share/arm", "resources"), )
tor-commits@lists.torproject.org