[tor-commits] [arm/master] Removing all trailing whitespace
atagar at torproject.org
atagar at torproject.org
Mon Sep 16 22:17:30 UTC 2013
commit 058dee1d2ae6570d2ab1eb7254783c276d32c838
Author: Damian Johnson <atagar at 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 at 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-descriptors.html
# 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 at 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"),
)
More information about the tor-commits
mailing list